UNPKG

@trapi/metadata

Version:

Generate REST-API metadata scheme from TypeScript Decorators.

1,009 lines 54 kB
"use strict"; /* * Copyright (c) 2021. * Author Peter Placzek (tada5hi) * For the full copyright and license information, * view the LICENSE file that was distributed with this source code. */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TypeNodeResolver = void 0; const ts = __importStar(require("typescript")); const decorator_1 = require("../decorator"); const constants_1 = require("./constants"); const utils_1 = require("../utils"); const error_1 = require("./error"); const extension_1 = require("./extension"); const type_1 = require("./type"); const localReferenceTypeCache = {}; const inProgressTypes = {}; class TypeNodeResolver { constructor(typeNode, current, parentNode, context, referencer) { this.attemptToResolveKindToPrimitive = (syntaxKind) => { if (syntaxKind === ts.SyntaxKind.NumberKeyword) { return 'number'; } if (syntaxKind === ts.SyntaxKind.StringKeyword) { return 'string'; } if (syntaxKind === ts.SyntaxKind.BooleanKeyword) { return 'boolean'; } if (syntaxKind === ts.SyntaxKind.VoidKeyword) { return 'void'; } return undefined; }; this.typeNode = typeNode; this.current = current; this.parentNode = parentNode; this.context = context || {}; this.referencer = referencer; } static clearCache() { Object.keys(localReferenceTypeCache).forEach((key) => { delete localReferenceTypeCache[key]; }); Object.keys(inProgressTypes).forEach((key) => { delete inProgressTypes[key]; }); } resolve() { const primitiveType = this.getPrimitiveType(this.typeNode, this.parentNode); if (primitiveType) { return primitiveType; } if (this.typeNode.kind === ts.SyntaxKind.NullKeyword) { return { typeName: constants_1.TypeName.ENUM, members: [null], }; } if (this.typeNode.kind === ts.SyntaxKind.UndefinedKeyword) { return { typeName: constants_1.TypeName.UNDEFINED, }; } if (ts.isArrayTypeNode(this.typeNode)) { return { typeName: constants_1.TypeName.ARRAY, elementType: new TypeNodeResolver(this.typeNode.elementType, this.current, this.parentNode, this.context).resolve(), }; } if (ts.isUnionTypeNode(this.typeNode)) { const members = this.typeNode.types.map((type) => new TypeNodeResolver(type, this.current, this.parentNode, this.context).resolve()); return { typeName: constants_1.TypeName.UNION, members, }; } if (ts.isIntersectionTypeNode(this.typeNode)) { const members = this.typeNode.types.map((type) => new TypeNodeResolver(type, this.current, this.parentNode, this.context).resolve()); return { typeName: constants_1.TypeName.INTERSECTION, members, }; } if (this.typeNode.kind === ts.SyntaxKind.AnyKeyword || this.typeNode.kind === ts.SyntaxKind.UnknownKeyword) { return { typeName: constants_1.TypeName.ANY, }; } if (ts.isLiteralTypeNode(this.typeNode)) { return { typeName: constants_1.TypeName.ENUM, members: [TypeNodeResolver.getLiteralValue(this.typeNode)], }; } if (ts.isTypeLiteralNode(this.typeNode)) { const properties = this.typeNode.members .filter((member) => ts.isPropertySignature(member)) .reduce((res, propertySignature) => { const type = new TypeNodeResolver(propertySignature.type, this.current, propertySignature, this.context).resolve(); const property = { deprecated: (0, utils_1.hasJSDocTag)(propertySignature, utils_1.JSDocTagName.DEPRECATED), example: this.getNodeExample(propertySignature), extensions: this.getNodeExtensions(propertySignature), default: (0, utils_1.getJSDocTagComment)(propertySignature, utils_1.JSDocTagName.DEFAULT), description: this.getNodeDescription(propertySignature), format: TypeNodeResolver.getNodeFormat(propertySignature), name: propertySignature.name.text, required: !propertySignature.questionToken, type, validators: (0, utils_1.getDeclarationValidators)(propertySignature) || {}, }; return [property, ...res]; }, []); const indexMember = this.typeNode.members.find((member) => ts.isIndexSignatureDeclaration(member)); let additionalType; if (indexMember) { const indexSignatureDeclaration = indexMember; const indexType = new TypeNodeResolver(indexSignatureDeclaration.parameters[0].type, this.current, this.parentNode, this.context).resolve(); if (!(0, type_1.isStringType)(indexType)) { throw new error_1.ResolverError('Only string indexes are supported.', this.typeNode); } additionalType = new TypeNodeResolver(indexSignatureDeclaration.type, this.current, this.parentNode, this.context).resolve(); } return { additionalProperties: indexMember && additionalType, typeName: constants_1.TypeName.NESTED_OBJECT_LITERAL, properties, }; } if (this.typeNode.kind === ts.SyntaxKind.ObjectKeyword || ts.isFunctionTypeNode(this.typeNode)) { return { typeName: constants_1.TypeName.OBJECT }; } if (ts.isMappedTypeNode(this.typeNode) && this.referencer) { const type = this.current.typeChecker.getTypeFromTypeNode(this.referencer); const mappedTypeNode = this.typeNode; const { typeChecker } = this.current; const getDeclaration = (prop) => prop.declarations && prop.declarations[0]; const isIgnored = (prop) => { const declaration = getDeclaration(prop); const tagNames = prop.getJsDocTags(); const tagNameIndex = tagNames.findIndex((tag) => tag.name === utils_1.JSDocTagName.IGNORE); if (tagNameIndex >= 0) { return true; } return (!!declaration && !ts.isPropertyDeclaration(declaration) && !ts.isPropertySignature(declaration) && !ts.isParameter(declaration)); }; const properties = type .getProperties() // Ignore methods, getter, setter and @ignored props .filter((property) => isIgnored(property) === false) // Transform to property .map((property) => { const propertyType = typeChecker.getTypeOfSymbolAtLocation(property, this.typeNode); const declaration = getDeclaration(property); if (declaration && ts.isPropertySignature(declaration)) { return { ...this.propertyFromSignature(declaration, mappedTypeNode.questionToken), name: property.getName() }; } if (declaration && (ts.isPropertyDeclaration(declaration) || ts.isParameter(declaration))) { return { ...this.propertyFromDeclaration(declaration, mappedTypeNode.questionToken), name: property.getName() }; } // Resolve default value, required and typeNode let required = false; const typeNode = this.current.typeChecker.typeToTypeNode(propertyType, undefined, ts.NodeBuilderFlags.NoTruncation); if (mappedTypeNode.questionToken && mappedTypeNode.questionToken.kind === ts.SyntaxKind.MinusToken) { required = true; } else if (mappedTypeNode.questionToken && mappedTypeNode.questionToken.kind === ts.SyntaxKind.QuestionToken) { required = false; } // Push property return { name: property.getName(), required, deprecated: false, type: new TypeNodeResolver(typeNode, this.current, this.typeNode, this.context, this.referencer).resolve(), validators: {}, }; }); return { typeName: constants_1.TypeName.NESTED_OBJECT_LITERAL, properties, }; } if (ts.isConditionalTypeNode(this.typeNode) && this.referencer && ts.isTypeReferenceNode(this.referencer)) { const type = this.current.typeChecker.getTypeFromTypeNode(this.referencer); if (type.aliasSymbol) { let declaration = type.aliasSymbol.declarations[0]; if (declaration.name) { declaration = this.getModelTypeDeclaration(declaration.name); } const name = TypeNodeResolver.getRefTypeName(this.referencer.getText()); return this.handleCachingAndCircularReferences(name, () => { if (ts.isTypeAliasDeclaration(declaration)) { // Note: I don't understand why typescript lose type for `this.referencer` (from above with isTypeReferenceNode()) return this.getTypeAliasReference(declaration, this.current.typeChecker.typeToString(type), this.referencer); } if (ts.isEnumDeclaration(declaration)) { return this.getEnumerateType(declaration.name); } throw new error_1.ResolverError(`Couldn't resolve Conditional to TypeNode. If you think this should be resolvable, please file an Issue. We found an aliasSymbol and it's declaration was of kind ${declaration.kind}`, this.typeNode); }); } if (type.isClassOrInterface()) { let declaration = type.symbol.declarations[0]; if (declaration.name) { declaration = this.getModelTypeDeclaration(declaration.name); } const name = TypeNodeResolver.getRefTypeName(this.referencer.getText()); return this.handleCachingAndCircularReferences(name, () => this.getModelReference(declaration, this.current.typeChecker.typeToString(type))); } try { return new TypeNodeResolver(this.current.typeChecker.typeToTypeNode(type, undefined, ts.NodeBuilderFlags.NoTruncation), this.current, this.typeNode, this.context, this.referencer).resolve(); } catch { throw new error_1.ResolverError(`Couldn't resolve Conditional to TypeNode. If you think this should be resolvable, please file an Issue. The flags on the result of the ConditionalType was ${type.flags}`, this.typeNode); } } if (ts.isTypeOperatorNode(this.typeNode)) { if (this.typeNode.operator === ts.SyntaxKind.KeyOfKeyword) { const type = this.current.typeChecker.getTypeFromTypeNode(this.typeNode); try { return new TypeNodeResolver(this.current.typeChecker.typeToTypeNode(type, undefined, ts.NodeBuilderFlags.NoTruncation), this.current, this.typeNode, this.context, this.referencer).resolve(); } catch (err) { const indexedTypeName = this.current.typeChecker.typeToString(this.current.typeChecker.getTypeFromTypeNode(this.typeNode.type)); throw new error_1.ResolverError(`Could not determine the keys on ${indexedTypeName}`, this.typeNode); } } if (this.typeNode.operator === ts.SyntaxKind.ReadonlyKeyword) { return new TypeNodeResolver(this.typeNode.type, this.current, this.typeNode, this.context, this.referencer).resolve(); } } if (ts.isIndexedAccessTypeNode(this.typeNode) && (this.typeNode.indexType.kind === ts.SyntaxKind.NumberKeyword || this.typeNode.indexType.kind === ts.SyntaxKind.StringKeyword)) { const numberIndexType = this.typeNode.indexType.kind === ts.SyntaxKind.NumberKeyword; const objectType = this.current.typeChecker.getTypeFromTypeNode(this.typeNode.objectType); const type = numberIndexType ? objectType.getNumberIndexType() : objectType.getStringIndexType(); if (type === undefined) { throw new error_1.ResolverError(`Could not determine ${numberIndexType ? 'number' : 'string'} index on ${this.current.typeChecker.typeToString(objectType)}`, this.typeNode); } return new TypeNodeResolver(this.current.typeChecker.typeToTypeNode(type, undefined, undefined), this.current, this.typeNode, this.context, this.referencer).resolve(); } if (ts.isIndexedAccessTypeNode(this.typeNode) && ts.isLiteralTypeNode(this.typeNode.indexType) && (ts.isStringLiteral(this.typeNode.indexType.literal) || ts.isNumericLiteral(this.typeNode.indexType.literal))) { const hasType = (node) => node !== undefined && Object.prototype.hasOwnProperty.call(node, 'type'); const symbol = this.current.typeChecker.getPropertyOfType(this.current.typeChecker.getTypeFromTypeNode(this.typeNode.objectType), this.typeNode.indexType.literal.text); if (symbol === undefined) { throw new error_1.ResolverError(`Could not determine the keys on ${this.current.typeChecker.typeToString(this.current.typeChecker.getTypeFromTypeNode(this.typeNode.objectType))}`, this.typeNode); } if (hasType(symbol.valueDeclaration) && symbol.valueDeclaration.type) { return new TypeNodeResolver(symbol.valueDeclaration.type, this.current, this.typeNode, this.context, this.referencer).resolve(); } const declaration = this.current.typeChecker.getTypeOfSymbolAtLocation(symbol, this.typeNode.objectType); try { return new TypeNodeResolver(this.current.typeChecker.typeToTypeNode(declaration, undefined, undefined), this.current, this.typeNode, this.context, this.referencer).resolve(); } catch { throw new error_1.ResolverError(`Could not determine the keys on ${this.current.typeChecker.typeToString(this.current.typeChecker.getTypeFromTypeNode(this.current.typeChecker.typeToTypeNode(declaration, undefined, undefined)))}`, this.typeNode); } } if (this.typeNode.kind === ts.SyntaxKind.TemplateLiteralType) { const type = this.current.typeChecker.getTypeFromTypeNode(this.referencer || this.typeNode); if (type.isUnion() && type.types.every((unionElementType) => unionElementType.isStringLiteral())) { return { typeName: constants_1.TypeName.ENUM, members: type.types.map((stringLiteralType) => stringLiteralType.value), }; } throw new error_1.ResolverError(`Could not the type of ${this.current.typeChecker.typeToString(this.current.typeChecker.getTypeFromTypeNode(this.typeNode), this.typeNode)}`, this.typeNode); } if (ts.isParenthesizedTypeNode(this.typeNode)) { return new TypeNodeResolver(this.typeNode.type, this.current, this.typeNode, this.context, this.referencer).resolve(); } if (this.typeNode.kind !== ts.SyntaxKind.TypeReference) { throw new error_1.ResolverError(`Unknown type: ${ts.SyntaxKind[this.typeNode.kind]}`, this.typeNode); } const typeReference = this.typeNode; if (typeReference.typeName.kind === ts.SyntaxKind.Identifier) { // Special Utility Type if (typeReference.typeName.text === 'Record') { return { additionalProperties: new TypeNodeResolver(typeReference.typeArguments[1], this.current, this.parentNode, this.context).resolve(), typeName: constants_1.TypeName.NESTED_OBJECT_LITERAL, properties: [], }; } const specialReference = TypeNodeResolver.resolveSpecialReference(typeReference.typeName); if (typeof specialReference !== 'undefined') { return specialReference; } if (typeReference.typeName.text === 'Date') { return this.getDateType(this.parentNode); } if (typeReference.typeName.text === 'Buffer' || typeReference.typeName.text === 'Readable') { return { typeName: constants_1.TypeName.BUFFER }; } if (typeReference.typeName.text === 'Array' && typeReference.typeArguments && typeReference.typeArguments.length >= 1) { return { typeName: constants_1.TypeName.ARRAY, elementType: new TypeNodeResolver(typeReference.typeArguments[0], this.current, this.parentNode, this.context).resolve(), }; } if (typeReference.typeName.text === 'Promise' && typeReference.typeArguments && typeReference.typeArguments.length === 1) { return new TypeNodeResolver(typeReference.typeArguments[0], this.current, this.parentNode, this.context).resolve(); } if (typeReference.typeName.text === 'String') { return { typeName: constants_1.TypeName.STRING }; } if (this.context[typeReference.typeName.text]) { return new TypeNodeResolver(this.context[typeReference.typeName.text], this.current, this.parentNode, this.context).resolve(); } } const referenceType = this.getReferenceType(typeReference); this.current.addReferenceType(referenceType); return referenceType; } // ------------------------------------------------------------------------ // Utility Type(s) // ------------------------------------------------------------------------ static toUtilityType(typeName) { if (typeof typeName === 'undefined') { return undefined; } const values = Object.values(constants_1.UtilityTypeName); const index = values.indexOf(typeof typeName !== 'string' ? typeName.text : typeName); if (index === -1) { return undefined; } return values[index]; } static getUtilityTypeOptions(typeArguments) { const utilityOptions = { keys: [], }; if (typeArguments.length >= 2) { if (ts.isUnionTypeNode(typeArguments[1])) { const args = typeArguments[1].types; for (let i = 0; i < args.length; i++) { if (ts.isLiteralTypeNode(args[i])) { utilityOptions.keys.push(TypeNodeResolver.getLiteralValue(args[i])); } } } if (ts.isLiteralTypeNode(typeArguments[1])) { utilityOptions.keys.push(TypeNodeResolver.getLiteralValue(typeArguments[1])); } } return utilityOptions; } filterUtilityProperties(properties, utilityType, utilityOptions) { if (typeof utilityType === 'undefined' || typeof utilityOptions === 'undefined') { return properties; } return properties .filter((property) => { const name = typeof property.name !== 'string' ? property.name.text : property.name; switch (utilityType) { case constants_1.UtilityTypeName.PICK: return utilityOptions.keys.indexOf(name) !== -1; case constants_1.UtilityTypeName.OMIT: return utilityOptions.keys.indexOf(name) === -1; } return true; }) .map((property) => { if ((0, utils_1.hasOwnProperty)(property, 'required')) { switch (utilityType) { case constants_1.UtilityTypeName.PARTIAL: property.required = false; break; case constants_1.UtilityTypeName.REQUIRED: case constants_1.UtilityTypeName.NON_NULLABLE: property.required = true; break; } } return property; }); } static resolveSpecialReference(node) { switch (node.text) { case 'Buffer': case 'DownloadBinaryData': case 'DownloadResource': return { typeName: constants_1.TypeName.BUFFER }; default: return undefined; } } static getLiteralValue(typeNode) { let value; switch (typeNode.literal.kind) { case ts.SyntaxKind.TrueKeyword: value = true; break; case ts.SyntaxKind.FalseKeyword: value = false; break; case ts.SyntaxKind.StringLiteral: value = typeNode.literal.text; break; case ts.SyntaxKind.NumericLiteral: value = parseFloat(typeNode.literal.text); break; case ts.SyntaxKind.NullKeyword: value = null; break; default: if (Object.prototype.hasOwnProperty.call(typeNode.literal, 'text')) { value = typeNode.literal.text; } else { throw new error_1.ResolverError(`Couldn't resolve literal node: ${typeNode.literal.getText()}`); } } return value; } getPrimitiveType(typeNode, parentNode) { if (!typeNode) { return { typeName: constants_1.TypeName.VOID, }; } const resolvedType = this.attemptToResolveKindToPrimitive(typeNode.kind); if (typeof resolvedType === 'undefined') { return undefined; } if (resolvedType === 'number') { if (!parentNode) { return { typeName: constants_1.TypeName.DOUBLE }; } const lookupTags = [ 'isInt', 'isLong', 'isFloat', 'isDouble', ]; const tags = (0, utils_1.getJSDocTagNames)(parentNode) .filter((name) => lookupTags.some((m) => m.toLowerCase() === name.toLowerCase())) .map((name) => name.toLowerCase()); const decoratorIds = [ decorator_1.DecoratorID.IS_INT, decorator_1.DecoratorID.IS_LONG, decorator_1.DecoratorID.IS_FLOAT, decorator_1.DecoratorID.IS_DOUBLE, ]; let decoratorID; for (let i = 0; i < decoratorIds.length; i++) { const decorator = this.current.decoratorResolver.match(decoratorIds[i], parentNode); if (decorator) { decoratorID = decoratorIds[i]; break; } } if (!decoratorID && tags.length === 0) { return { typeName: constants_1.TypeName.DOUBLE }; } switch (decoratorID || tags[0]) { case decorator_1.DecoratorID.IS_INT: case 'isint': return { typeName: constants_1.TypeName.INTEGER }; case decorator_1.DecoratorID.IS_LONG: case 'islong': return { typeName: constants_1.TypeName.LONG }; case decorator_1.DecoratorID.IS_FLOAT: case 'isfloat': return { typeName: constants_1.TypeName.FLOAT }; case decorator_1.DecoratorID.IS_DOUBLE: case 'isdouble': return { typeName: constants_1.TypeName.DOUBLE }; default: return { typeName: constants_1.TypeName.DOUBLE }; } } else if (resolvedType === 'string') { return { typeName: constants_1.TypeName.STRING, }; } else if (resolvedType === 'boolean') { return { typeName: constants_1.TypeName.BOOLEAN, }; } else if (resolvedType === 'void') { return { typeName: constants_1.TypeName.VOID, }; } else { // todo: should not occur return { typeName: resolvedType, }; } } getDateType(parentNode) { if (!parentNode) { return { typeName: constants_1.TypeName.DATETIME }; } const tags = (0, utils_1.getJSDocTagNames)(parentNode).filter((name) => ['isDate', 'isDateTime'].some((m) => m === name)); if (tags.length === 0) { return { typeName: constants_1.TypeName.DATETIME }; } switch (tags[0]) { case 'isDate': return { typeName: constants_1.TypeName.DATE }; default: return { typeName: constants_1.TypeName.DATETIME }; } } static getDesignatedModels(nodes, typeName) { return nodes; } getEnumerateType(typeName) { const enumName = typeName.text; let enumNodes = this.current.nodes.filter((node) => node.kind === ts.SyntaxKind.EnumDeclaration && node.name.text === enumName); if (!enumNodes.length) { return undefined; } enumNodes = TypeNodeResolver.getDesignatedModels(enumNodes, enumName); if (enumNodes.length > 1) { throw new error_1.ResolverError(`Multiple matching enum found for enum ${enumName}; please make enum names unique.`); } const enumDeclaration = enumNodes[0]; const isNotUndefined = (item) => item !== undefined; const enums = enumDeclaration.members.map(this.current.typeChecker.getConstantValue.bind(this.current.typeChecker)).filter(isNotUndefined); const enumNames = enumDeclaration.members.map((e) => e.name.getText()).filter(isNotUndefined); return { typeName: constants_1.TypeName.REF_ENUM, description: this.getNodeDescription(enumDeclaration), members: enums, memberNames: enumNames, refName: enumName, deprecated: (0, utils_1.hasJSDocTag)(enumDeclaration, utils_1.JSDocTagName.DEPRECATED), }; } getReferenceType(node) { let type; if (ts.isTypeReferenceNode(node)) { type = node.typeName; } else if (ts.isExpressionWithTypeArguments(node)) { type = node.expression; } else { throw new error_1.ResolverError('Can\'t resolve reference type.'); } // Can't invoke getText on Synthetic Nodes let resolvableName = node.pos !== -1 ? node.getText() : type.text; if (node.pos === -1 && 'typeArguments' in node && Array.isArray(node.typeArguments)) { // Add typeArguments for Synthetic nodes (e.g. Record<> in TestClassModel.indexedResponse) const argumentsString = node.typeArguments .map((arg) => { if (ts.isLiteralTypeNode(arg)) { return `'${String(TypeNodeResolver.getLiteralValue(arg))}'`; } const resolvedType = this.attemptToResolveKindToPrimitive(arg.kind); if (typeof resolvedType === 'undefined') { return 'any'; } return resolvedType; }); resolvableName += `<${argumentsString.join(', ')}>`; } const name = this.contextualizedName(resolvableName); // Handle Utility Types const identifierName = type.text; const utilityType = TypeNodeResolver.toUtilityType(identifierName); let utilityTypeOptions; if (utilityType) { const { typeArguments } = type.parent; if (ts.isTypeReferenceNode(typeArguments[0])) { type = typeArguments[0].typeName; } else if (ts.isExpressionWithTypeArguments(typeArguments[0])) { type = typeArguments[0].expression; } else { throw new error_1.ResolverError('Can\'t resolve Reference type.'); } utilityTypeOptions = TypeNodeResolver.getUtilityTypeOptions(typeArguments); } else { this.typeArgumentsToContext(node, type, this.context); } try { const existingType = localReferenceTypeCache[name]; if (existingType) { return existingType; } const refEnumType = this.getEnumerateType(type); if (refEnumType) { localReferenceTypeCache[name] = refEnumType; return refEnumType; } if (inProgressTypes[name]) { return this.createCircularDependencyResolver(name); } inProgressTypes[name] = true; const declaration = this.getModelTypeDeclaration(type); let referenceType; if (ts.isTypeAliasDeclaration(declaration)) { referenceType = this.getTypeAliasReference(declaration, name, node, utilityType, utilityTypeOptions); localReferenceTypeCache[name] = referenceType; return referenceType; } if (ts.isEnumMember(declaration)) { referenceType = { typeName: constants_1.TypeName.REF_ENUM, refName: TypeNodeResolver.getRefTypeName(name, utilityType), members: [this.current.typeChecker.getConstantValue(declaration)], memberNames: [declaration.name.getText()], deprecated: (0, utils_1.hasJSDocTag)(declaration, utils_1.JSDocTagName.DEPRECATED), }; localReferenceTypeCache[name] = referenceType; return referenceType; } // todo: dont cast handle property-signature referenceType = this.getModelReference(declaration, name, utilityType, utilityTypeOptions); localReferenceTypeCache[name] = referenceType; return referenceType; } catch (err) { // eslint-disable-next-line no-console console.error(`There was a problem resolving type of '${name}'.`); throw err; } } getTypeAliasReference(declaration, name, referencer, utilityType, utilityTypeOptions) { const refName = TypeNodeResolver.getRefTypeName(name, utilityType); if (declaration.type.kind === ts.SyntaxKind.TypeReference) { const referenceType = this.getReferenceType(declaration.type); if (referenceType.refName === refName) { return referenceType; } } const type = new TypeNodeResolver(declaration.type, this.current, declaration, this.context, this.referencer || referencer).resolve(); if ((0, type_1.isNestedObjectLiteralType)(type)) { type.properties = this.filterUtilityProperties(type.properties, utilityType, utilityTypeOptions); } const example = this.getNodeExample(declaration); return { typeName: constants_1.TypeName.REF_ALIAS, default: (0, utils_1.getJSDocTagComment)(declaration, utils_1.JSDocTagName.DEFAULT), description: this.getNodeDescription(declaration), refName, format: TypeNodeResolver.getNodeFormat(declaration), type, validators: (0, utils_1.getDeclarationValidators)(declaration) || {}, deprecated: (0, utils_1.hasJSDocTag)(declaration, utils_1.JSDocTagName.DEPRECATED), ...(example && { example }), }; } getModelReference(modelType, name, utilityType, utilityOptions) { const example = this.getNodeExample(modelType); const description = this.getNodeDescription(modelType); const deprecated = (0, utils_1.hasJSDocTag)(modelType, utils_1.JSDocTagName.DEPRECATED) || !!this.current.decoratorResolver.match(decorator_1.DecoratorID.DEPRECATED, modelType); // Handle toJSON methods if (!modelType.name) { throw new error_1.ResolverError('Can\'t get Symbol from anonymous class', modelType); } const type = this.current.typeChecker.getTypeAtLocation(modelType.name); const toJSON = this.current.typeChecker.getPropertyOfType(type, 'toJSON'); if (toJSON && toJSON.valueDeclaration && (ts.isMethodDeclaration(toJSON.valueDeclaration) || ts.isMethodSignature(toJSON.valueDeclaration))) { let nodeType = toJSON.valueDeclaration.type; if (!nodeType) { const signature = this.current.typeChecker.getSignatureFromDeclaration(toJSON.valueDeclaration); const implicitType = this.current.typeChecker.getReturnTypeOfSignature(signature); nodeType = this.current.typeChecker.typeToTypeNode(implicitType, undefined, ts.NodeBuilderFlags.NoTruncation); } return { refName: `${TypeNodeResolver.getRefTypeName(name, utilityType)}Alias`, typeName: constants_1.TypeName.REF_ALIAS, description, type: new TypeNodeResolver(nodeType, this.current).resolve(), deprecated, validators: {}, ...(example && { example }), }; } const properties = this.getModelProperties(modelType, undefined, utilityType, utilityOptions); const additionalProperties = this.getModelAdditionalProperties(modelType); const inheritedProperties = this.getModelInheritedProperties(modelType) || []; const referenceType = { additionalProperties, typeName: constants_1.TypeName.REF_OBJECT, description, properties: this.filterUtilityProperties(inheritedProperties, utilityType, utilityOptions), refName: TypeNodeResolver.getRefTypeName(name, utilityType), deprecated, ...(example && { example }), }; referenceType.properties = referenceType.properties.concat(properties); return referenceType; } static getRefTypeName(name, utilityType) { return encodeURIComponent(name .replace(/<|>/g, '_') .replace(/\s+/g, '') .replace(/,/g, '.') .replace(/'([^']*)'/g, '$1') .replace(/"([^"]*)"/g, '$1') .replace(/&/g, typeof utilityType !== 'undefined' ? '--' : '-and-') .replace(/\|/g, typeof utilityType !== 'undefined' ? '--' : '-or-') .replace(/\[\]/g, '-array') .replace(/{|}/g, '_') // SuccessResponse_{indexesCreated-number}_ -> SuccessResponse__indexesCreated-number__ .replace(/([a-z]+):([a-z]+)/gi, '$1-$2') // SuccessResponse_indexesCreated:number_ -> SuccessResponse_indexesCreated-number_ .replace(/;/g, '--') .replace(/([a-z]+)\[([a-z]+)\]/gi, '$1-at-$2') // Partial_SerializedDatasourceWithVersion[format]_ -> Partial_SerializedDatasourceWithVersion~format~_, .replace(/_/g, '') .replace(/-/g, '')); } contextualizedName(name) { return Object.entries(this.context).reduce((acc, [key, entry]) => acc .replace(new RegExp(`<\\s*([^>]*\\s)*\\s*(${key})(\\s[^>]*)*\\s*>`, 'g'), `<$1${entry.getText()}$3>`) .replace(new RegExp(`<\\s*([^,]*\\s)*\\s*(${key})(\\s[^,]*)*\\s*,`, 'g'), `<$1${entry.getText()}$3,`) .replace(new RegExp(`,\\s*([^>]*\\s)*\\s*(${key})(\\s[^>]*)*\\s*>`, 'g'), `,$1${entry.getText()}$3>`) .replace(new RegExp(`<\\s*([^<]*\\s)*\\s*(${key})(\\s[^<]*)*\\s*<`, 'g'), `<$1${entry.getText()}$3<`), name); } handleCachingAndCircularReferences(name, declarationResolver) { try { const existingType = localReferenceTypeCache[name]; if (existingType) { return existingType; } if (inProgressTypes[name]) { return this.createCircularDependencyResolver(name); } inProgressTypes[name] = true; const reference = declarationResolver(); localReferenceTypeCache[name] = reference; this.current.addReferenceType(reference); return reference; } catch (err) { // eslint-disable-next-line no-console console.error(`There was a problem resolving type of '${name}'.`); throw err; } } createCircularDependencyResolver(refName) { const referenceType = { deprecated: false, properties: [], typeName: constants_1.TypeName.REF_OBJECT, refName, }; this.current.registerDependencyResolver((referenceTypes) => { const realReferenceType = referenceTypes[refName]; if (!realReferenceType) { return; } referenceType.description = realReferenceType.description; if (realReferenceType.typeName === 'refObject' && referenceType.typeName === 'refObject') { referenceType.properties = realReferenceType.properties; } referenceType.typeName = realReferenceType.typeName; referenceType.refName = realReferenceType.refName; }); return referenceType; } static nodeIsUsable(node) { switch (node.kind) { case ts.SyntaxKind.InterfaceDeclaration: case ts.SyntaxKind.ClassDeclaration: case ts.SyntaxKind.TypeAliasDeclaration: case ts.SyntaxKind.EnumDeclaration: case ts.SyntaxKind.EnumMember: return true; default: return false; } } getModelTypeDeclaration(type) { let typeName = type.kind === ts.SyntaxKind.Identifier ? type.text : type.right.text; const symbol = this.getSymbolAtLocation(type); const declarations = symbol.getDeclarations(); if (declarations.length === 0) { throw new error_1.ResolverError(`No models found for referenced type ${typeName}.`); } if (symbol.escapedName !== typeName && symbol.escapedName !== 'default') { typeName = symbol.escapedName; } let modelTypes = declarations.filter((node) => { if (!TypeNodeResolver.nodeIsUsable(node) || !this.current.isExportedNode(node)) { return false; } const modelTypeDeclaration = node; return modelTypeDeclaration.name?.text === typeName; }); if (!modelTypes.length) { throw new error_1.ResolverError(`No matching model found for referenced type ${typeName}. If ${typeName} comes from a dependency, please create an interface in your own code that has the same structure. The compiler can not utilize interfaces from external dependencies.`); } if (modelTypes.length > 1) { // remove types that are from typescript e.g. 'Account' modelTypes = modelTypes.filter((modelType) => modelType.getSourceFile() .fileName.replace(/\\/g, '/').toLowerCase().indexOf('node_modules/typescript') <= -1); modelTypes = TypeNodeResolver.getDesignatedModels(modelTypes, typeName); } if (modelTypes.length > 1) { const conflicts = modelTypes.map((modelType) => modelType.getSourceFile().fileName).join('"; "'); throw new error_1.ResolverError(`Multiple matching models found for referenced type ${typeName}; please make model names unique. Conflicts found: "${conflicts}".`); } return modelTypes[0]; } hasFlag(type, flag) { return (type.flags & flag) === flag; } getSymbolAtLocation(type) { const symbol = this.current.typeChecker.getSymbolAtLocation(type) || type.symbol; // resolve alias if it is an alias, otherwise take symbol directly return (symbol && this.hasFlag(symbol, ts.SymbolFlags.Alias) && this.current.typeChecker.getAliasedSymbol(symbol)) || symbol; } getModelProperties(node, overrideToken, utilityType, utilityOptions) { const isIgnored = (e) => (0, utils_1.hasJSDocTag)(e, utils_1.JSDocTagName.IGNORE); // Interface model if (ts.isInterfaceDeclaration(node)) { return node.members.filter((member) => !isIgnored(member) && ts.isPropertySignature(member)).map((member) => this.propertyFromSignature(member, overrideToken)); } // Class model let properties = node.members .filter((member) => !isIgnored(member) && member.kind === ts.SyntaxKind.PropertyDeclaration && !this.hasStaticModifier(member) && this.hasPublicModifier(member)); const classConstructor = node.members.find((member) => ts.isConstructorDeclaration(member)); if (classConstructor && classConstructor.parameters) { const constructorProperties = classConstructor.parameters.filter((parameter) => this.isAccessibleParameter(parameter)); properties.push(...constructorProperties); } properties = this.filterUtilityProperties(properties, utilityType, utilityOptions); return properties.map((property) => this.propertyFromDeclaration(property, overrideToken, utilityType)); } propertyFromSignature(propertySignature, overrideToken) { const identifier = propertySignature.name; if (!propertySignature.type) { throw new error_1.ResolverError('No valid type found for property declaration.'); } let required = !propertySignature.questionToken; if (overrideToken && overrideToken.kind === ts.SyntaxKind.MinusToken) { required = true; } else if (overrideToken && overrideToken.kind === ts.SyntaxKind.QuestionToken) { required = false; } const property = { deprecated: (0, utils_1.hasJSDocTag)(propertySignature, utils_1.JSDocTagName.DEPRECATED), default: (0, utils_1.getJSDocTagComment)(propertySignature, utils_1.JSDocTagName.DEFAULT), description: this.getNodeDescription(propertySignature), example: this.getNodeExample(propertySignature), extensions: this.getNodeExtensions(propertySignature), format: TypeNodeResolver.getNodeFormat(propertySignature), name: identifier.text, required, type: new TypeNodeResolver(propertySignature.type, this.current, propertySignature.type.parent, this.context, propertySignature.type).resolve(), validators: (0, utils_1.getDeclarationValidators)(propertySignature) || {}, }; return property; } propertyFromDeclaration(propertyDeclaration, overrideToken, utilityType) { const identifier = propertyDeclaration.name; let typeNode = propertyDeclaration.type; if (!typeNode) { const tsType = this.current.typeChecker.getTypeAtLocation(propertyDeclaration); typeNode = this.current.typeChecker.typeToTypeNode(tsType, undefined, ts.NodeBuilderFlags.NoTruncation); } if (!typeNode) { throw new error_1.ResolverError('No valid type found for property declaration.'); } const type = new TypeNodeResolver(typeNode, this.current, propertyDeclaration, this.context, typeNode).resolve(); let required = !propertyDeclaration.questionToken && !propertyDeclaration.initializer; if (overrideToken && overrideToken.kind === ts.SyntaxKind.MinusToken) { required = true; } else if (overrideToken && overrideToken.kind === ts.SyntaxKind.QuestionToken) { required = false; } if (typeof utilityType !== 'undefined') { if (utilityType === 'Partial') { required = false; } if (utilityType === 'Required') { required = true; } } const property = { deprecated: (0, utils_1.hasJSDocTag)(propertyDeclaration, utils_1.JSDocTagName.DEPRECATED), default: (0, utils_1.getInitializerValue)(propertyDeclaration.initializer, this.current.typeChecker), description: this.getNodeDescription(propertyDeclaration), example: this.getNodeExample(propertyDeclaration), extensions: this.getNodeExtensions(propertyDeclaration), format: TypeNodeResolver.getNodeFormat(propertyDeclaration), name: identifier.text, required, type, validators: (0, utils_1.getDeclarationValidators)(propertyDeclaration) || {}, }; return property; } getModelAdditionalProperties(node) { if (node.kind === ts.SyntaxKind.InterfaceDeclaration) { const indexMember = node.members.find((member) => member.kind === ts.SyntaxKind.IndexSignature); if (!indexMember) { return undefined; } const indexSignatureDeclaration = indexMember; const indexType = new TypeNodeResolver(indexSignatureDeclaration.parameters[0].type, this.current, this.parentNode, this.context).resolve(); if (indexType.typeName !== 'string') { throw new error_1.ResolverError('Only string indexers are supported.', this.typeNode); } return new TypeNodeResolver(indexSignatureDeclaration.type, this.current, this.parentNode, this.context).resolve(); } return undefined; } typeArgumentsToContext(type, targetEntity, context) { // this.context = {}; const declaration = this.getModelTypeDeclaration(targetEntity); if (typeof declaration === 'undefined' || !('typeParameters' in declaration)) { return context; } const { typeParameters } = declaration; if (typeParameters) { for (let index = 0; index < typeParameters.length; index++) { const typeParameter = typeParameters[index]; const typeArg = type.typeArguments && type.typeArguments[index]; let resolvedType; // Argument may be a forward reference from context if (typeArg && ts.isTypeReferenceNode(typeArg) && ts.isIdentifier(typeArg.typeName) && context[typeArg.typeName.text]) { resolvedType = context[typeArg.typeName.text]; } else if (typeArg) { resolvedType = typeArg; } else if (typeParameter.default) { resolvedType = typeParameter.default; } else { throw new error_1.ResolverError(`Could not find a value for type parameter ${typeParameter.name.text}`, type); } this.context = { ...this.context, [typeParameter.name.text]: resolvedType, }; } } return context; } getModelInheritedProperties(modelTypeDeclaration) { let properties = []; const { heritageClauses } = modelTypeDeclaration; if (!heritageClauses) { return properties; } heritageClauses.forEach((clause) => { if (!clause.types) { return; } clause.types.forEach((t) => { const baseEntityName = t.expression; // create subContext const resetCtx = this.typeArgumentsToContext(t,