@trapi/metadata
Version:
Generate REST-API metadata scheme from TypeScript Decorators.
1,009 lines • 54 kB
JavaScript
"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,