UNPKG

@nestjs/swagger

Version:

Nest - modern, fast, powerful node.js web framework (@swagger)

605 lines (604 loc) 31.4 kB
import { compact, flatten, head } from 'es-toolkit/compat'; import { posix } from 'path'; import * as ts from 'typescript'; import { factory } from 'typescript'; import { ApiHideProperty, ApiProperty } from '../../decorators/index.js'; import { decoratorsProperties, decoratorsPropertiesMappingType } from '../../services/decorators-properties.js'; import { METADATA_FACTORY_NAME } from '../plugin-constants.js'; import { pluginDebugLogger } from '../plugin-debug-logger.js'; import { createBooleanLiteral, createLiteralFromAnyValue, createPrimitiveLiteral, getDecoratorArguments, getMainCommentOfNode, getText, getTsDocTagsOfNode, isEnum } from '../utils/ast-utils.js'; import { canReferenceNode, convertPath, createAdditionalPropertiesValueSchema, extractTypeArgumentIfArray, getDecoratorOrUndefinedByNames, getRecordValueType, getStringLiteralUnionValues, getTypeReferenceAsString, hasPropertyKey, isAutoGeneratedEnumUnion, isAutoGeneratedTypeUnion } from '../utils/plugin-utils.js'; import { resolvePluginOptionsForFile } from '../utils/module-format.util.js'; import { typeReferenceToIdentifier } from '../utils/type-reference-to-identifier.util.js'; import { AbstractFileVisitor } from './abstract.visitor.js'; export class ModelClassVisitor extends AbstractFileVisitor { constructor() { super(...arguments); this._typeImports = {}; this._collectedMetadata = {}; } get typeImports() { return this._typeImports; } collectedMetadata() { return this.buildMetadataImports(this._collectedMetadata); } visit(sourceFile, ctx, program, options) { options = resolvePluginOptionsForFile(options, sourceFile, program.getCompilerOptions()); const typeChecker = program.getTypeChecker(); this._hoistedTypeImports.clear(); sourceFile = this.updateImports(sourceFile, ctx.factory, program, options); const propertyNodeVisitorFactory = (metadata) => (node) => { const visit = () => { if (ts.isPropertyDeclaration(node)) { this.visitPropertyNodeDeclaration(node, ctx, typeChecker, options, sourceFile, metadata); } else if (options.parameterProperties && ts.isConstructorDeclaration(node)) { this.visitConstructorDeclarationNode(node, typeChecker, options, sourceFile, metadata); } return node; }; const visitedNode = visit(); if (!options.readonly) { return visitedNode; } }; const visitClassNode = (node) => { if (ts.isClassDeclaration(node)) { const metadata = {}; const isExported = node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword); if (options.readonly) { if (isExported) { ts.forEachChild(node, propertyNodeVisitorFactory(metadata)); } else { if (options.debug) { pluginDebugLogger.debug(`Skipping class "${node.name.getText()}" because it's not exported.`); } } } else { node = ts.visitEachChild(node, propertyNodeVisitorFactory(metadata), ctx); } if ((isExported && options.readonly) || !options.readonly) { const declaration = this.addMetadataFactory(ctx.factory, node, metadata, sourceFile, options); if (!options.readonly) { return declaration; } } } if (options.readonly) { ts.forEachChild(node, visitClassNode); } else { return ts.visitEachChild(node, visitClassNode, ctx); } }; const visitedSourceFile = ts.visitNode(sourceFile, visitClassNode); if (options.readonly) { return visitedSourceFile; } return this.insertHoistedTypeImports(visitedSourceFile, ctx.factory); } visitPropertyNodeDeclaration(node, ctx, typeChecker, options, sourceFile, metadata) { const isPropertyStatic = (node.modifiers || []).some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword); if (isPropertyStatic) { return node; } const isPrivateProperty = ts.isPrivateIdentifier(node.name); if (isPrivateProperty) { return node; } const decorators = ts.canHaveDecorators(node) && ts.getDecorators(node); const classTransformerShim = options.classTransformerShim; const hidePropertyDecoratorExists = getDecoratorOrUndefinedByNames(classTransformerShim ? [ApiHideProperty.name, 'Exclude'] : [ApiHideProperty.name], decorators, factory); const annotatePropertyDecoratorExists = getDecoratorOrUndefinedByNames(classTransformerShim ? [ApiProperty.name, 'Expose'] : [ApiProperty.name], decorators, factory); if (!annotatePropertyDecoratorExists && (hidePropertyDecoratorExists || classTransformerShim === 'exclusive')) { return node; } else if (annotatePropertyDecoratorExists && hidePropertyDecoratorExists) { if (options.debug) { pluginDebugLogger.debug(`"${node.parent.name.getText()}->${node.name.getText()}" has conflicting decorators, excluding as @ApiHideProperty() takes priority.`); } return node; } try { this.inspectPropertyDeclaration(ctx.factory, node, typeChecker, options, sourceFile.fileName, sourceFile, metadata); } catch (err) { return node; } } visitConstructorDeclarationNode(constructorNode, typeChecker, options, sourceFile, metadata) { constructorNode.forEachChild((node) => { if (ts.isParameter(node) && node.modifiers != null && node.modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.ReadonlyKeyword || modifier.kind === ts.SyntaxKind.PrivateKeyword || modifier.kind === ts.SyntaxKind.PublicKeyword || modifier.kind === ts.SyntaxKind.ProtectedKeyword)) { const objectLiteralExpr = this.createDecoratorObjectLiteralExpr(factory, node, typeChecker, factory.createNodeArray(), options, sourceFile.fileName, sourceFile); const propertyName = node.name.getText(); metadata[propertyName] = objectLiteralExpr; } }); } addMetadataFactory(factory, node, classMetadata, sourceFile, options) { const returnValue = factory.createObjectLiteralExpression(Object.keys(classMetadata).map((key) => factory.createPropertyAssignment(factory.createIdentifier(key), classMetadata[key]))); if (options.readonly) { const filePath = this.normalizeImportPath(options.pathToSource, sourceFile.fileName); this.registerOutputExtension(filePath, sourceFile, options); if (!this._collectedMetadata[filePath]) { this._collectedMetadata[filePath] = {}; } const attributeKey = node.name.getText(); this._collectedMetadata[filePath][attributeKey] = returnValue; return; } const method = factory.createMethodDeclaration([factory.createModifier(ts.SyntaxKind.StaticKeyword)], undefined, factory.createIdentifier(METADATA_FACTORY_NAME), undefined, undefined, [], undefined, factory.createBlock([factory.createReturnStatement(returnValue)], true)); return factory.updateClassDeclaration(node, node.modifiers, node.name, node.typeParameters, node.heritageClauses, [...node.members, method]); } inspectPropertyDeclaration(factory, compilerNode, typeChecker, options, hostFilename, sourceFile, metadata) { const objectLiteralExpr = this.createDecoratorObjectLiteralExpr(factory, compilerNode, typeChecker, factory.createNodeArray(), options, hostFilename, sourceFile); this.addClassMetadata(compilerNode, objectLiteralExpr, sourceFile, metadata); } createDecoratorObjectLiteralExpr(factory, node, typeChecker, existingProperties = factory.createNodeArray(), options = {}, hostFilename = '', sourceFile) { const isRequired = !node.questionToken; const properties = [ ...existingProperties, !hasPropertyKey('required', existingProperties) && factory.createPropertyAssignment('required', createBooleanLiteral(factory, isRequired)), ...this.createTypePropertyAssignments(factory, node.type, typeChecker, existingProperties, hostFilename, options), ...this.createDescriptionAndTsDocTagPropertyAssignments(factory, node, typeChecker, existingProperties, options, sourceFile), this.createDefaultPropertyAssignment(factory, node, existingProperties, options), this.createEnumPropertyAssignment(factory, node, typeChecker, existingProperties, hostFilename, options) ]; if ((ts.isPropertyDeclaration(node) || ts.isPropertySignature(node)) && options.classValidatorShim) { properties.push(this.createValidationPropertyAssignments(factory, node, options)); } return factory.createObjectLiteralExpression(compact(flatten(properties))); } createRecordTypePropertyAssignment(factory, recordValueType, typeChecker) { const additionalPropertiesExpr = createAdditionalPropertiesValueSchema(recordValueType, typeChecker, factory); return [ factory.createPropertyAssignment('type', factory.createStringLiteral('object')), factory.createPropertyAssignment('additionalProperties', additionalPropertiesExpr) ]; } createTypePropertyAssignments(factory, node, typeChecker, existingProperties, hostFilename, options) { const key = 'type'; if (hasPropertyKey(key, existingProperties)) { return []; } if (node) { if (ts.isArrayTypeNode(node) && ts.isTypeLiteralNode(node.elementType)) { const initializer = this.createInitializerForArrayLiteralTypeNode(node, factory, typeChecker, existingProperties, hostFilename, options); return [factory.createPropertyAssignment(key, initializer)]; } if (ts.isTypeLiteralNode(node) && node.members.length > 0 && node.members.every(ts.isIndexSignatureDeclaration)) { const literalType = typeChecker.getTypeAtLocation(node); const recordValueType = getRecordValueType(literalType, typeChecker); if (recordValueType) { return this.createRecordTypePropertyAssignment(factory, recordValueType, typeChecker); } } if (ts.isTypeLiteralNode(node)) { const initializer = this.createInitializerForTypeLiteralNode(node, factory, typeChecker, existingProperties, hostFilename, options); return [factory.createPropertyAssignment(key, initializer)]; } if (ts.isUnionTypeNode(node)) { const { nullableType, isNullable } = this.isNullableUnion(node); const remainingTypes = node.types.filter((t) => t !== nullableType); if (remainingTypes.length === 1) { const nonNullishNode = remainingTypes[0]; const resolved = typeChecker.getTypeAtLocation(nonNullishNode); let candidateType = resolved; const arrayTuple = extractTypeArgumentIfArray(candidateType); if (arrayTuple) { candidateType = arrayTuple.type; } let isEnumType = false; if (candidateType) { if (isEnum(candidateType)) { isEnumType = true; } else { const maybeEnum = isAutoGeneratedEnumUnion(candidateType, typeChecker); if (maybeEnum || getStringLiteralUnionValues(candidateType)) { isEnumType = true; } } } if (isEnumType) { return isNullable ? [ factory.createPropertyAssignment('nullable', createBooleanLiteral(factory, true)) ] : []; } const propertyAssignments = this.createTypePropertyAssignments(factory, nonNullishNode, typeChecker, existingProperties, hostFilename, options); if (!isNullable) { return propertyAssignments; } return [ ...propertyAssignments, factory.createPropertyAssignment('nullable', createBooleanLiteral(factory, true)) ]; } } } const type = typeChecker.getTypeAtLocation(node); if (!type) { return []; } const stringLiteralUnion = getStringLiteralUnionValues(type); if (stringLiteralUnion) { if (stringLiteralUnion.isNullable && !hasPropertyKey('nullable', existingProperties)) { return [ factory.createPropertyAssignment('nullable', createBooleanLiteral(factory, true)) ]; } return []; } const recordValueType = getRecordValueType(type, typeChecker); if (recordValueType) { return this.createRecordTypePropertyAssignment(factory, recordValueType, typeChecker); } const typeReferenceDescriptor = getTypeReferenceAsString(type, typeChecker); if (!typeReferenceDescriptor.typeName) { return []; } const identifier = typeReferenceToIdentifier(typeReferenceDescriptor, hostFilename, options, factory, type, this._typeImports, this._hoistedTypeImports); const initializer = factory.createArrowFunction(undefined, undefined, [], undefined, undefined, identifier); return [factory.createPropertyAssignment(key, initializer)]; } createInitializerForArrayLiteralTypeNode(node, factory, typeChecker, existingProperties, hostFilename, options) { const elementType = node.elementType; const propertyAssignments = Array.from(elementType.members || []).map((member) => { const literalExpr = this.createDecoratorObjectLiteralExpr(factory, member, typeChecker, existingProperties, options, hostFilename); return factory.createPropertyAssignment(factory.createIdentifier(member.name.getText()), literalExpr); }); const initializer = factory.createArrowFunction(undefined, undefined, [], undefined, undefined, factory.createArrayLiteralExpression([ factory.createParenthesizedExpression(factory.createObjectLiteralExpression(propertyAssignments)) ])); return initializer; } createInitializerForTypeLiteralNode(node, factory, typeChecker, existingProperties, hostFilename, options) { const propertyAssignments = Array.from(node.members || []).map((member) => { const literalExpr = this.createDecoratorObjectLiteralExpr(factory, member, typeChecker, existingProperties, options, hostFilename); return factory.createPropertyAssignment(factory.createIdentifier(member.name.getText()), literalExpr); }); const initializer = factory.createArrowFunction(undefined, undefined, [], undefined, undefined, factory.createParenthesizedExpression(factory.createObjectLiteralExpression(propertyAssignments))); return initializer; } isNullableUnion(node) { const nullableType = node.types.find((type) => type.kind === ts.SyntaxKind.NullKeyword || (ts.SyntaxKind.LiteralType && type.getText() === 'null')); const isNullable = !!nullableType; return { nullableType, isNullable }; } createEnumPropertyAssignment(factory, node, typeChecker, existingProperties, hostFilename, options) { const key = 'enum'; if (hasPropertyKey(key, existingProperties)) { return undefined; } let type; try { if (node.type) { type = typeChecker.getTypeFromTypeNode(node.type); } } catch (e) { } if (!type) { type = typeChecker.getTypeAtLocation(node); } if (!type) { return undefined; } if ((type.flags & ts.TypeFlags.Union) !== 0) { const union = type; const nonNullish = union.types.filter((t) => (t.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) === 0); if (nonNullish.length === 1) { type = nonNullish[0]; } } if (isAutoGeneratedTypeUnion(type)) { const types = type.types; const nonUndefined = types.find((t) => t.intrinsicName !== 'undefined'); if (nonUndefined) { type = nonUndefined; } } if (isAutoGeneratedTypeUnion(type)) { const types = type.types; type = types[types.length - 1]; } const typeIsArrayTuple = extractTypeArgumentIfArray(type); if (!typeIsArrayTuple) { return undefined; } const isArrayType = typeIsArrayTuple.isArray; type = typeIsArrayTuple.type; const stringLiteralUnion = getStringLiteralUnionValues(type); if (stringLiteralUnion) { const enumProperty = factory.createPropertyAssignment(key, factory.createArrayLiteralExpression(stringLiteralUnion.values.map((v) => typeof v === 'number' ? factory.createNumericLiteral(v) : factory.createStringLiteral(v)))); const result = compact([ enumProperty, isArrayType && factory.createPropertyAssignment('isArray', factory.createIdentifier('true')) ]); return result.length === 1 ? result[0] : result; } const isEnumMember = type.symbol && type.symbol.flags === ts.SymbolFlags.EnumMember; if (!isEnum(type) || isEnumMember) { if (!isEnumMember) { type = isAutoGeneratedEnumUnion(type, typeChecker); } if (!type) { return undefined; } const typeIsArrayTuple = extractTypeArgumentIfArray(type); if (!typeIsArrayTuple) { return undefined; } type = typeIsArrayTuple.type; } const typeReferenceDescriptor = { typeName: getText(type, typeChecker) }; const enumIdentifier = typeReferenceToIdentifier(typeReferenceDescriptor, hostFilename, options, factory, type, this._typeImports, this._hoistedTypeImports); const enumProperty = factory.createPropertyAssignment(key, enumIdentifier); const extraProperties = []; if (options.autoFillEnumName && !hasPropertyKey('enumName', existingProperties)) { const fullTypeName = typeReferenceDescriptor.typeName; const enumTypeName = fullTypeName.includes('.') ? fullTypeName.slice(fullTypeName.lastIndexOf('.') + 1) : fullTypeName; extraProperties.push(factory.createPropertyAssignment('enumName', factory.createStringLiteral(enumTypeName))); } if (isArrayType) { const isArrayKey = 'isArray'; const isArrayProperty = factory.createPropertyAssignment(isArrayKey, factory.createIdentifier('true')); return [enumProperty, isArrayProperty, ...extraProperties]; } return extraProperties.length > 0 ? [enumProperty, ...extraProperties] : enumProperty; } createDefaultPropertyAssignment(factory, node, existingProperties, options) { const key = 'default'; if (options.skipDefaultValues) { return undefined; } if (hasPropertyKey(key, existingProperties)) { return undefined; } if (ts.isPropertySignature(node)) { return undefined; } if (node.initializer == null) { return undefined; } let initializer = node.initializer; if (ts.isAsExpression(initializer)) { initializer = initializer.expression; } initializer = this.clonePrimitiveLiteral(factory, initializer) ?? initializer; if (!canReferenceNode(initializer, options)) { if (options.debug) { const parentFilePath = node.getSourceFile().fileName; const propertyName = node.name.getText(); pluginDebugLogger.debug(`Skipping registering default value for "${propertyName}" property in "${parentFilePath}" file because it is not a referenceable value ("${initializer.getText()}").`); } return undefined; } return factory.createPropertyAssignment(key, initializer); } createValidationPropertyAssignments(factory, node, options) { const assignments = []; const decorators = ts.canHaveDecorators(node) && ts.getDecorators(node); if (!options.readonly) { this.addPropertiesByValidationDecorator(factory, 'IsIn', decorators, assignments, (decoratorRef) => { const decoratorArguments = getDecoratorArguments(decoratorRef); const result = []; const argumentValue = head(decoratorArguments); if (!canReferenceNode(argumentValue, options)) { return result; } const assignment = this.clonePrimitiveLiteral(factory, argumentValue) ?? argumentValue; result.push(factory.createPropertyAssignment('enum', assignment)); if (this.isEachOptionEnabled(decoratorArguments[1])) { result.push(factory.createPropertyAssignment('isArray', factory.createIdentifier('true'))); } return result; }); } decoratorsProperties.forEach((decoratorProperty) => { if (decoratorProperty.mappingType === decoratorsPropertiesMappingType.DIRECT) { this.addPropertyByValidationDecorator(factory, decoratorProperty.decorator, decoratorProperty.property, decorators, assignments, options); } else if (decoratorProperty.mappingType === decoratorsPropertiesMappingType.INDIRECT_VALUE) { this.addPropertiesByValidationDecorator(factory, decoratorProperty.decorator, decorators, assignments, () => { return [ factory.createPropertyAssignment(decoratorProperty.property, createPrimitiveLiteral(factory, decoratorProperty.value)) ]; }); } else if (decoratorProperty.mappingType === decoratorsPropertiesMappingType.INDIRECT_ARGUMENT) { this.addPropertiesByValidationDecorator(factory, decoratorProperty.decorator, decorators, assignments, (decoratorRef) => { const decoratorArguments = getDecoratorArguments(decoratorRef); const result = []; const argumentValue = head(decoratorArguments); if (!canReferenceNode(argumentValue, options)) { return result; } const clonedArgumentValue = this.clonePrimitiveLiteral(factory, argumentValue); if (clonedArgumentValue) { result.push(factory.createPropertyAssignment(decoratorProperty.property, clonedArgumentValue)); } return result; }); } }); this.addPropertiesByValidationDecorator(factory, 'Length', decorators, assignments, (decoratorRef) => { const decoratorArguments = getDecoratorArguments(decoratorRef); const result = []; const minLength = head(decoratorArguments); if (!canReferenceNode(minLength, options)) { return result; } const clonedMinLength = this.clonePrimitiveLiteral(factory, minLength) ?? minLength; if (clonedMinLength) { result.push(factory.createPropertyAssignment('minLength', clonedMinLength)); } if (decoratorArguments.length > 1) { const maxLength = decoratorArguments[1]; if (!canReferenceNode(maxLength, options)) { return result; } const clonedMaxLength = this.clonePrimitiveLiteral(factory, maxLength) ?? maxLength; if (clonedMaxLength) { result.push(factory.createPropertyAssignment('maxLength', clonedMaxLength)); } } return result; }); this.addPropertiesByValidationDecorator(factory, 'Matches', decorators, assignments, (decoratorRef) => { const decoratorArguments = getDecoratorArguments(decoratorRef); const firstArg = head(decoratorArguments); if (!firstArg) { return []; } let patternText; if (ts.isRegularExpressionLiteral(firstArg)) { const match = firstArg.text.match(/^\/(.*)\/([gimsuy]*)$/); if (match) { patternText = match[1]; } } else if (ts.isStringLiteral(firstArg)) { patternText = firstArg.text; } if (patternText === undefined) { return []; } return [ factory.createPropertyAssignment('pattern', createPrimitiveLiteral(factory, patternText)) ]; }); return assignments; } addPropertyByValidationDecorator(factory, decoratorName, propertyKey, decorators, assignments, options) { this.addPropertiesByValidationDecorator(factory, decoratorName, decorators, assignments, (decoratorRef) => { const argument = head(getDecoratorArguments(decoratorRef)); const assignment = this.clonePrimitiveLiteral(factory, argument) ?? argument; if (!canReferenceNode(assignment, options)) { return []; } return [factory.createPropertyAssignment(propertyKey, assignment)]; }); } addPropertiesByValidationDecorator(factory, decoratorName, decorators, assignments, addPropertyAssignments) { const decoratorRef = getDecoratorOrUndefinedByNames([decoratorName], decorators, factory); if (!decoratorRef) { return; } assignments.push(...addPropertyAssignments(decoratorRef)); } isEachOptionEnabled(optionsArgument) { if (!optionsArgument || !ts.isObjectLiteralExpression(optionsArgument)) { return false; } return optionsArgument.properties.some((property) => { if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name) || property.name.text !== 'each') { return false; } return property.initializer.kind === ts.SyntaxKind.TrueKeyword; }); } addClassMetadata(node, objectLiteral, sourceFile, metadata) { const hostClass = node.parent; const className = hostClass.name && hostClass.name.getText(); if (!className) { return; } const propertyName = node.name && node.name.getText(sourceFile); if (!propertyName || (node.name && node.name.kind === ts.SyntaxKind.ComputedPropertyName)) { return; } metadata[propertyName] = objectLiteral; } createDescriptionAndTsDocTagPropertyAssignments(factory, node, typeChecker, existingProperties = factory.createNodeArray(), options = {}, sourceFile) { if (!options.introspectComments || !sourceFile) { return []; } const propertyAssignments = []; const comments = getMainCommentOfNode(node); const tags = getTsDocTagsOfNode(node, typeChecker); const keyOfComment = options.dtoKeyOfComment; if (!hasPropertyKey(keyOfComment, existingProperties) && comments) { const descriptionPropertyAssignment = factory.createPropertyAssignment(keyOfComment, factory.createStringLiteral(comments)); propertyAssignments.push(descriptionPropertyAssignment); } const hasExampleOrExamplesKey = hasPropertyKey('example', existingProperties) || hasPropertyKey('examples', existingProperties); if (!hasExampleOrExamplesKey && tags.example?.length) { if (tags.example.length === 1) { const examplePropertyAssignment = factory.createPropertyAssignment('example', createLiteralFromAnyValue(factory, tags.example[0])); propertyAssignments.push(examplePropertyAssignment); } else { const examplesPropertyAssignment = factory.createPropertyAssignment('examples', createLiteralFromAnyValue(factory, tags.example)); propertyAssignments.push(examplesPropertyAssignment); } } const hasDeprecatedKey = hasPropertyKey('deprecated', existingProperties); if (!hasDeprecatedKey && tags.deprecated) { const deprecatedPropertyAssignment = factory.createPropertyAssignment('deprecated', createLiteralFromAnyValue(factory, tags.deprecated)); propertyAssignments.push(deprecatedPropertyAssignment); } return propertyAssignments; } normalizeImportPath(pathToSource, path) { let relativePath = posix.relative(convertPath(pathToSource), convertPath(path)); relativePath = relativePath[0] !== '.' ? './' + relativePath : relativePath; return relativePath; } clonePrimitiveLiteral(factory, node) { const primitiveTypeName = this.getInitializerPrimitiveTypeName(node); if (!primitiveTypeName) { return undefined; } const text = node.text ?? node.getText(); return createPrimitiveLiteral(factory, text, primitiveTypeName); } getInitializerPrimitiveTypeName(node) { if (ts.isIdentifier(node) && (node.text === 'true' || node.text === 'false')) { return 'boolean'; } if (ts.isNumericLiteral(node) || ts.isPrefixUnaryExpression(node)) { return 'number'; } if (ts.isStringLiteral(node)) { return 'string'; } return undefined; } }