diff --git a/src/mapping.ts b/src/mapping.ts index 72ef7109..e849555f 100644 --- a/src/mapping.ts +++ b/src/mapping.ts @@ -41,7 +41,10 @@ const dynamicTemplates: ElasticsearchDynamicTemplate[] = []; let errors: string[] = []; let showErrors = true; +let aggregatablePaths: string[] = []; + const indexableTag = 'indexable'; +const aggregatableTag = 'aggregatable'; let ignoredTagsList = ['indexable', 'validatable']; @@ -86,12 +89,13 @@ export function getAllIndexableInterfaces(projectReflection: ProjectReflection): * Composes error messages, that are readable and contain a certain minumum of information * * @param path the path where the error took place + * @param topTypeName the name of the SCThingType * @param typeName the name of the object, with which something went wrong * @param object the object or name * @param message the error message */ -function composeErrorMessage(path: string, typeName: string, object: string, message: string) { - const error = `At "${path.substr(0, path.length - 1)}" for ${typeName} "${object}": ${message}`; +function composeErrorMessage(path: string, topTypeName: string, typeName: string, object: string, message: string) { + const error = `At "${topTypeName}::${path.substr(0, path.length - 1)}" for ${typeName} "${object}": ${message}`; errors.push(error); if (showErrors) { // tslint:disable-next-line:no-floating-promises @@ -108,11 +112,12 @@ function composeErrorMessage(path: string, typeName: string, object: string, mes * @param type the ReferenceType of a DeclarationReflection * @param out the previous reflection, it then overrides all parameters or keeps old ones * @param path the current path to the object we are in + * @param topTypeName the name of the SCThingType * @param tags any tags attached to the type */ function getReflectionGeneric(type: ReferenceType, out: ReflectionGeneric[], - path: string, tags: CommentTag[]): ReflectionGeneric[] { + path: string, topTypeName: string, tags: CommentTag[]): ReflectionGeneric[] { if (typeof type.typeArguments !== 'undefined' && type.reflection instanceof DeclarationReflection && typeof type.reflection.typeParameters !== 'undefined' @@ -121,14 +126,14 @@ function getReflectionGeneric(type: ReferenceType, let replaced = false; for (const old of out) { if (old.name === type.reflection.typeParameters[i].name) { - old.value = handleType(type.typeArguments[i], out, path, tags); + old.value = handleType(type.typeArguments[i], out, path, topTypeName, tags); replaced = true; } } if (!replaced) { out.push({ name: type.reflection.typeParameters[i].name, - value: handleType(type.typeArguments[i], out, path, tags), + value: handleType(type.typeArguments[i], out, path, topTypeName, tags), }); } } @@ -143,25 +148,27 @@ function getReflectionGeneric(type: ReferenceType, * @param ref the ReferenceType * @param generics the generics from levels above, so we can use them without having access to the parent * @param path the current path to the object we are in + * @param topTypeName the name of the SCThingType * @param tags any tags attached to the type */ function handleRefWithoutReflection(ref: ReferenceType, generics: ReflectionGeneric[], - path: string, tags: CommentTag[]): ElasticsearchValue { + path: string, topTypeName: string, tags: CommentTag[]): ElasticsearchValue { for (const premap in premaps) { if (premap === ref.name) { - return readFieldTags(premaps[premap], path, tags); + return readFieldTags(premaps[premap], path, topTypeName, tags); } } if (ref.name === 'Array') { // basically an external type, but Array is quite common, especially with generics if (typeof ref.typeArguments === 'undefined' || typeof ref.typeArguments[0] === 'undefined') { - composeErrorMessage(path, 'Array with generics', 'array', 'Failed to parse'); + composeErrorMessage(path, topTypeName, 'Array with generics', 'array', 'Failed to parse'); return {type: ElasticsearchDataType.parse_error}; } - return readFieldTags(handleType(ref.typeArguments[0], getReflectionGeneric(ref, generics, path, tags), path, tags), - path, tags); + return readFieldTags(handleType(ref.typeArguments[0], getReflectionGeneric(ref, generics, path, topTypeName, tags), + path, topTypeName, tags), + path, topTypeName, tags); } if (ref.name === '__type') { // empty object return { @@ -170,9 +177,9 @@ function handleRefWithoutReflection(ref: ReferenceType, generics: ReflectionGene }; } - composeErrorMessage(path, 'external type', ref.name, 'Missing pre-map'); + composeErrorMessage(path, topTypeName, 'external type', ref.name, 'Missing pre-map'); - return readFieldTags({type: ElasticsearchDataType.missing_premap}, path, tags); + return readFieldTags({type: ElasticsearchDataType.missing_premap}, path, topTypeName, tags); } /** @@ -181,15 +188,17 @@ function handleRefWithoutReflection(ref: ReferenceType, generics: ReflectionGene * @param decl the DeclarationReflection of the object * @param generics the generics from levels above, so we can use them without having access to the parent * @param path the current path to the object we are in + * @param topTypeName the name of the SCThingType */ function handleDeclarationReflection(decl: DeclarationReflection, generics: ReflectionGeneric[], - path: string): + path: string, + topTypeName: string): ElasticsearchValue { // check if we have an object referencing a generic for (const gRefl of generics) { if (gRefl.name === decl.name) { // if the object name is the same as the generic name - return readFieldTags(gRefl.value, path, + return readFieldTags(gRefl.value, path, topTypeName, typeof decl.comment !== 'undefined' ? typeof decl.comment.tags !== 'undefined' ? decl.comment.tags : [] : []); // use the value defined by the generic } @@ -208,7 +217,7 @@ function handleDeclarationReflection(decl: DeclarationReflection, empty = false; const template: ElasticsearchDynamicTemplate = {}; template[decl.name] = { - mapping: handleDeclarationReflection(param as DeclarationReflection, generics, path), + mapping: handleDeclarationReflection(param as DeclarationReflection, generics, path, topTypeName), match: '*', match_mapping_type: '*', path_match: `${path}*`, @@ -221,21 +230,21 @@ function handleDeclarationReflection(decl: DeclarationReflection, if (typeof decl.children !== 'undefined' && decl.children.length > 0) { for (const child of decl.children) { empty = false; - out.properties[child.name] = handleDeclarationReflection(child, generics, `${path}${child.name}.`); + out.properties[child.name] = handleDeclarationReflection(child, generics, `${path}${child.name}.`, topTypeName); } } else if (decl.type instanceof Type) { // if the object is a type, so we are dealing with a PROPERTY - return handleType(decl.type, generics, path, + return handleType(decl.type, generics, path, topTypeName, typeof decl.comment !== 'undefined' ? typeof decl.comment.tags !== 'undefined' ? decl.comment.tags : [] : []); } else if (decl.kindString === 'Enumeration member') { - return readTypeTags(typeof decl.defaultValue, path, + return readTypeTags(typeof decl.defaultValue, path, topTypeName, typeof decl.comment !== 'undefined' ? typeof decl.comment.tags !== 'undefined' ? decl.comment.tags : [] : []); } if (empty) { - composeErrorMessage(path, 'object', decl.name, 'Empty object'); + composeErrorMessage(path, topTypeName, 'object', decl.name, 'Empty object'); } - return readFieldTags(out, path, + return readFieldTags(out, path, topTypeName, typeof decl.comment !== 'undefined' ? typeof decl.comment.tags !== 'undefined' ? decl.comment.tags : [] : []); } @@ -248,11 +257,13 @@ function handleDeclarationReflection(decl: DeclarationReflection, * @param type the type object * @param generics the generics from levels above, so we can use them without having access to the parent * @param path the current path to the object we are in + * @param topTypeName the name of the SCThingType * @param tags any tags attached to the type */ function handleUnionType(type: UnionType, generics: ReflectionGeneric[], path: string, + topTypeName: string, tags: CommentTag[]): ElasticsearchValue { const list: ElasticsearchValue[] = []; @@ -260,7 +271,7 @@ function handleUnionType(type: UnionType, if (subType instanceof IntrinsicType && subType.name === 'undefined') { continue; } - list.push(handleType(subType, generics, path, tags)); + list.push(handleType(subType, generics, path, topTypeName, tags)); } if (list.length > 0) { @@ -273,7 +284,8 @@ function handleUnionType(type: UnionType, return out; } - composeErrorMessage(path, 'Union Type', stringify(list), 'Empty union type. This is likely not a user error.'); + composeErrorMessage(path, topTypeName, 'Union Type', stringify(list), + 'Empty union type. This is likely not a user error.'); return {type: ElasticsearchDataType.parse_error}; } @@ -284,30 +296,32 @@ function handleUnionType(type: UnionType, * @param type the type object * @param generics the generics from levels above, so we can use them without having access to the parent * @param path the current path to the object we are in + * @param topTypeName the name of the SCThingType * @param tags any tags attached to the type */ -function handleType(type: Type, generics: ReflectionGeneric[], path: string, tags: CommentTag[]): ElasticsearchValue { +function handleType(type: Type, generics: ReflectionGeneric[], path: string, topTypeName: string, tags: CommentTag[]): + ElasticsearchValue { // logger.log((type as any).name); if (type instanceof ArrayType) { // array is irrelevant in Elasticsearch, so just go with the element type - return handleType(type.elementType, generics, path, tags); + return handleType(type.elementType, generics, path, topTypeName, tags); } if (type.type === 'stringLiteral') { // a string literal, usually for type - return readTypeTags(type.type, path, tags); + return readTypeTags(type.type, path, topTypeName, tags); } if (type instanceof IntrinsicType) { // the absolute default type, like strings - return readTypeTags(type.name, path, tags); + return readTypeTags(type.name, path, topTypeName, tags); } if (type instanceof UnionType) { // the union type... - return handleUnionType(type, generics, path, tags); + return handleUnionType(type, generics, path, topTypeName, tags); } if (type instanceof ReferenceType) { if (typeof type.reflection !== 'undefined') { // there is really no way to make this typesafe, every element in DeclarationReflection is optional. return handleDeclarationReflection(type.reflection as DeclarationReflection, - getReflectionGeneric(type, generics, path, tags), path); + getReflectionGeneric(type, generics, path, topTypeName, tags), path, topTypeName); } - return handleRefWithoutReflection(type, generics, path, tags); + return handleRefWithoutReflection(type, generics, path, topTypeName, tags); } if (type instanceof TypeParameterType) { // check if we have an object referencing a generic @@ -316,16 +330,17 @@ function handleType(type: Type, generics: ReflectionGeneric[], path: string, tag return gRefl.value; // use the value defined by the generic } } - composeErrorMessage(path, 'Generic', type.name, 'Missing reflection, please report!'); + composeErrorMessage(path, topTypeName, 'Generic', type.name, 'Missing reflection, please report!'); return {type: ElasticsearchDataType.parse_error}; } if (type instanceof ReflectionType) { - return readFieldTags(handleDeclarationReflection(type.declaration, generics, path), path, tags); + return readFieldTags(handleDeclarationReflection(type.declaration, generics, path, topTypeName), + path, topTypeName, tags); } - composeErrorMessage(path, 'type', stringify(type), 'Not implemented type'); + composeErrorMessage(path, topTypeName, 'type', stringify(type), 'Not implemented type'); return {type: ElasticsearchDataType.parse_error}; } @@ -335,14 +350,21 @@ function handleType(type: Type, generics: ReflectionGeneric[], path: string, tag * * @param prev the previous ElasticsearchValue, for example and object * @param path the current path to the object we are in + * @param topTypeName the name of the SCThingType * @param tags tags attached to the value * @param dataType the ElasticsearchDataType, for checking if a tag is a type tag */ function readFieldTags(prev: ElasticsearchValue, path: string, + topTypeName: string, tags: CommentTag[], dataType?: string): ElasticsearchValue { for (const tag of tags) { + if (tag.tagName === aggregatableTag) { + // push type.path and remove the '.' at the end of the path + aggregatablePaths.push(`${topTypeName}.${path.slice(0, -1)}`); + } + if (!ignoredTagsList.includes(tag.tagName)) { if (typeof fieldmap[tag.tagName] !== 'undefined') { if (typeof prev.fields === 'undefined') { @@ -357,7 +379,7 @@ function readFieldTags(prev: ElasticsearchValue, prev.fields = {...prev.fields, ...fieldmap[tag.tagName][tag.text.trim()]}; } else if (!fieldmap[tag.tagName].ignore.includes(tag.text.trim())) { // when there is an unidentified tag - composeErrorMessage(path, 'tag', tag.tagName, `Not implemented tag param "${tag.text.trim()}"`); + composeErrorMessage(path, topTypeName, 'tag', tag.tagName, `Not implemented tag param "${tag.text.trim()}"`); } } else if (tag.tagName === filterableTagName) { if (typeof prev.fields === 'undefined') { @@ -369,13 +391,13 @@ function readFieldTags(prev: ElasticsearchValue, // merge fields prev.fields = {...prev.fields, ...{raw: {type: type}}}; } else { - composeErrorMessage(path, 'tag', tag.tagName, `Not implemented for ${prev.type}`); + composeErrorMessage(path, topTypeName, 'tag', tag.tagName, `Not implemented for ${prev.type}`); } } else { - composeErrorMessage(path, 'tag', tag.tagName, 'Not applicable for object types'); + composeErrorMessage(path, topTypeName, 'tag', tag.tagName, 'Not applicable for object types'); } } else if (typeof dataType === 'undefined' || typeof typemap[dataType][tag.tagName] === 'undefined') { - composeErrorMessage(path, 'tag', tag.tagName, `Not implemented tag`); + composeErrorMessage(path, topTypeName, 'tag', tag.tagName, `Not implemented tag`); } } } @@ -388,9 +410,10 @@ function readFieldTags(prev: ElasticsearchValue, * * @param type the type of the value * @param path the current path to the object we are in + * @param topTypeName the name of the SCThingType * @param tags tags attached to the value */ -function readTypeTags(type: string, path: string, tags: CommentTag[]): ElasticsearchValue { +function readTypeTags(type: string, path: string, topTypeName: string, tags: CommentTag[]): ElasticsearchValue { let out: ElasticsearchValue = {type: ElasticsearchDataType.parse_error}; if (typeof typemap[type] !== 'undefined') { // first look if the value has a definition in the typemap @@ -398,8 +421,8 @@ function readTypeTags(type: string, path: string, tags: CommentTag[]): Elasticse if (!ignoredTagsList.includes(tags[i].tagName) && typeof typemap[type][tags[i].tagName] !== 'undefined') { // if we have a tag that indicates a type if (out.type !== ElasticsearchDataType.parse_error) { - composeErrorMessage(path, 'type', type, `Type conflict; "${typemap[type][tags[i].tagName]}" would` + - ` override "${out.type}"`); + composeErrorMessage(path, topTypeName, 'type', type, + `Type conflict; "${typemap[type][tags[i].tagName]}" would override "${out.type}"`); out.type = ElasticsearchDataType.type_conflict; continue; } @@ -411,7 +434,7 @@ function readTypeTags(type: string, path: string, tags: CommentTag[]): Elasticse out.type = typemap[type].default; } - out = readFieldTags(out, path, tags, type); + out = readFieldTags(out, path, topTypeName, tags, type); return out; } @@ -422,7 +445,7 @@ function readTypeTags(type: string, path: string, tags: CommentTag[]): Elasticse }; } - composeErrorMessage(path, 'type', type, 'Not implemented type'); + composeErrorMessage(path, topTypeName, 'type', type, 'Not implemented type'); return out; } @@ -440,8 +463,9 @@ function readTypeTags(type: string, path: string, tags: CommentTag[]): Elasticse */ export function generateTemplate(projectReflection: ProjectReflection, ignoredTags: string[], showErrorOutput = true): // tslint:disable-next-line:completed-docs - { errors: string[]; template: ElasticsearchTemplate; } { + { aggregations: string[]; errors: string[]; template: ElasticsearchTemplate; } { errors = []; + aggregatablePaths = []; showErrors = showErrorOutput; ignoredTagsList = ['indexable', 'validatable']; @@ -498,10 +522,10 @@ export function generateTemplate(projectReflection: ProjectReflection, ignoredTa } out.mappings._default_.properties[typeName] = - handleDeclarationReflection(_interface, [], '') as ElasticsearchObject; + handleDeclarationReflection(_interface, [], '', typeName) as ElasticsearchObject; } out.mappings._default_.dynamic_templates = dynamicTemplates; - return {template: out, errors}; + return {aggregations: aggregatablePaths, template: out, errors}; }