UNPKG

@trapi/metadata

Version:

Generate REST-API metadata scheme from TypeScript Decorators.

556 lines 23.3 kB
"use strict"; /* * Copyright (c) 2021-2023. * 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.ParameterGenerator = void 0; const locter_1 = require("locter"); const ts = __importStar(require("typescript")); const decorator_1 = require("../../decorator"); const resolver_1 = require("../../resolver"); const utils_1 = require("../../utils"); const constants_1 = require("./constants"); const error_1 = require("./error"); const parameterKeys = [ decorator_1.DecoratorID.CONTEXT, decorator_1.DecoratorID.PARAM, decorator_1.DecoratorID.PARAMS, decorator_1.DecoratorID.QUERY, decorator_1.DecoratorID.FORM, decorator_1.DecoratorID.BODY, decorator_1.DecoratorID.HEADER, decorator_1.DecoratorID.HEADERS, decorator_1.DecoratorID.COOKIE, decorator_1.DecoratorID.COOKIES, decorator_1.DecoratorID.PATH, decorator_1.DecoratorID.PATHS, decorator_1.DecoratorID.FILE, decorator_1.DecoratorID.FILES, ]; class ParameterGenerator { constructor(parameter, method, path, current) { this.parameter = parameter; this.method = method; this.path = path; this.current = current; } generate() { const decorators = (0, utils_1.getNodeDecorators)(this.parameter); for (let i = 0; i < parameterKeys.length; i++) { const manager = this.current.decoratorResolver.match(parameterKeys[i], decorators); if (typeof manager === 'undefined') { continue; } switch (manager.representation.id) { case decorator_1.DecoratorID.CONTEXT: return this.getContextParameter(); case decorator_1.DecoratorID.PARAM: case decorator_1.DecoratorID.PARAMS: return this.getParamParameter(manager); case decorator_1.DecoratorID.FORM: return this.getFormParameter(manager); case decorator_1.DecoratorID.QUERY: return this.getQueryParameter(manager); case decorator_1.DecoratorID.BODY: return this.getBodyParameter(manager); case decorator_1.DecoratorID.HEADER: case decorator_1.DecoratorID.HEADERS: return this.getHeaderParameter(manager); case decorator_1.DecoratorID.COOKIE: case decorator_1.DecoratorID.COOKIES: return this.getCookieParameter(manager); case decorator_1.DecoratorID.PATH: case decorator_1.DecoratorID.PATHS: return this.getPathParameter(manager); case decorator_1.DecoratorID.FILE: case decorator_1.DecoratorID.FILES: return this.getFileParameter(manager); } } return this.getBodyParameter(); } buildParametersForObject(type, details) { if (type.properties.length === 0) { return []; } const parameterName = this.parameter.name.text; const initializerValue = (0, utils_1.getInitializerValue)(this.parameter.initializer, this.current.typeChecker, type); const output = []; for (let i = 0; i < type.properties.length; i++) { const property = type.properties[i]; let propertyDefaultValue = property.default; if (typeof propertyDefaultValue === 'undefined' && (0, locter_1.isObject)(initializerValue)) { propertyDefaultValue = initializerValue[property.name]; } let propertyRequired = !this.parameter.questionToken; if (propertyRequired) { propertyRequired = property.required; } output.push({ ...details, default: propertyDefaultValue, description: property.description || details.description || this.getParameterDescription(), name: property.name, parameterName, required: propertyRequired, type: property.type, deprecated: property.deprecated || this.getParameterDeprecation(), }); // todo: example, format } return output; } getParamParameter(manager) { return [ ...this.getBodyParameter(manager), ...this.getCookieParameter(manager), ]; } getContextParameter() { const parameterName = this.parameter.name.text; // todo: req, res, next should maybe be separate parameter sources. return [ { description: this.getParameterDescription(), in: constants_1.ParameterSource.CONTEXT, name: parameterName, parameterName, required: !this.parameter.questionToken, type: null, }, ]; } getFileParameter(manager) { const parameterName = this.parameter.name.text; let name = parameterName; const value = manager.get('value'); if (typeof value === 'string') { name = value; } if (!this.isBodySupportedForMethod(this.method)) { throw error_1.ParameterError.methodUnsupported({ decoratorName: manager.representation.name, propertyName: name, method: this.method, node: this.parameter, }); } const elementType = { typeName: resolver_1.TypeName.FILE }; let type; if (manager.representation.id === decorator_1.DecoratorID.FILES) { type = { typeName: resolver_1.TypeName.ARRAY, elementType }; } else { type = elementType; } const { examples, exampleLabels } = this.getParameterExample(parameterName); return [ { default: (0, utils_1.getInitializerValue)(this.parameter.initializer, this.current.typeChecker, type), description: this.getParameterDescription(), examples, exampleLabels, in: constants_1.ParameterSource.FORM_DATA, name: name || parameterName, parameterName, required: !this.parameter.questionToken && !this.parameter.initializer, type, deprecated: this.getParameterDeprecation(), validators: (0, utils_1.getDeclarationValidators)(this.parameter, parameterName), }, ]; } getFormParameter(manager) { const parameterName = this.parameter.name.text; let name = parameterName; const type = this.getValidatedType(this.parameter); if (!this.isBodySupportedForMethod(this.method)) { throw error_1.ParameterError.methodUnsupported({ decoratorName: manager.representation.name, propertyName: name, method: this.method, node: this.parameter, }); } const value = manager.get('value'); if (typeof value === 'string') { name = value; } const { examples, exampleLabels } = this.getParameterExample(parameterName); return [ { default: (0, utils_1.getInitializerValue)(this.parameter.initializer, this.current.typeChecker, type), description: this.getParameterDescription(), examples, exampleLabels, in: constants_1.ParameterSource.FORM_DATA, name: name || parameterName, parameterName, required: !this.parameter.questionToken && !this.parameter.initializer, type, deprecated: this.getParameterDeprecation(), validators: (0, utils_1.getDeclarationValidators)(this.parameter, parameterName), }, ]; } getCookieParameter(manager) { const parameterName = this.parameter.name.text; let name = parameterName; const type = this.getValidatedType(this.parameter); const { examples, exampleLabels } = this.getParameterExample(parameterName); if ((0, resolver_1.isNestedObjectLiteralType)(type) || (0, resolver_1.isRefObjectType)(type)) { return this.buildParametersForObject(type, { in: constants_1.ParameterSource.COOKIE, examples, exampleLabels, }); } if (!this.isTypeSupported(type)) { throw error_1.ParameterError.typeUnsupported({ decoratorName: manager.representation.name, propertyName: name, type, node: this.parameter, }); } const value = manager.get('value'); if (typeof value === 'string') { name = value; } return [ { default: (0, utils_1.getInitializerValue)(this.parameter.initializer, this.current.typeChecker, type), description: this.getParameterDescription(), examples, exampleLabels, in: constants_1.ParameterSource.COOKIE, name: name || parameterName, parameterName, required: !this.parameter.questionToken && !this.parameter.initializer, type, deprecated: this.getParameterDeprecation(), validators: (0, utils_1.getDeclarationValidators)(this.parameter, parameterName), }, ]; } getBodyParameter(manager) { const parameterName = this.parameter.name.text; let name = parameterName; let source = constants_1.ParameterSource.BODY; if (manager) { const value = manager.get('value'); if (typeof value === 'string') { name = value; source = constants_1.ParameterSource.BODY_PROP; } } const type = this.getValidatedType(this.parameter); if (!this.isBodySupportedForMethod(this.method)) { throw error_1.ParameterError.methodUnsupported({ decoratorName: manager ? manager.representation.name : 'Body', propertyName: name, method: this.method, node: this.parameter, }); } const { examples, exampleLabels } = this.getParameterExample(parameterName); return [ { default: (0, utils_1.getInitializerValue)(this.parameter.initializer, this.current.typeChecker, type), description: this.getParameterDescription(), examples, exampleLabels, in: source, name: name || parameterName, parameterName, required: !this.parameter.questionToken && !this.parameter.initializer, type, deprecated: this.getParameterDeprecation(), validators: (0, utils_1.getDeclarationValidators)(this.parameter, parameterName), }, ]; } getHeaderParameter(manager) { const parameterName = this.parameter.name.text; let name = parameterName; const type = this.getValidatedType(this.parameter); const value = manager.get('value'); if (typeof value === 'string') { name = value; } const { examples, exampleLabels } = this.getParameterExample(parameterName); if ((0, resolver_1.isNestedObjectLiteralType)(type) || (0, resolver_1.isRefObjectType)(type)) { return this.buildParametersForObject(type, { in: constants_1.ParameterSource.HEADER, examples, exampleLabels, }); } if (!this.isTypeSupported(type)) { throw error_1.ParameterError.typeUnsupported({ decoratorName: manager.representation.name, propertyName: name, type, node: this.parameter, }); } return [ { default: (0, utils_1.getInitializerValue)(this.parameter.initializer, this.current.typeChecker, type), description: this.getParameterDescription(), examples, exampleLabels, in: constants_1.ParameterSource.HEADER, name: name || parameterName, parameterName, required: !this.parameter.questionToken && !this.parameter.initializer, type, deprecated: this.getParameterDeprecation(), validators: (0, utils_1.getDeclarationValidators)(this.parameter, parameterName), }, ]; } getQueryParameter(manager) { const parameterName = this.parameter.name.text; const type = this.getValidatedType(this.parameter); let name = parameterName; let options = {}; let source = constants_1.ParameterSource.QUERY; const nameValue = manager.get('value'); if (typeof nameValue === 'string') { name = nameValue; source = constants_1.ParameterSource.QUERY_PROP; } const optionsValue = manager.get('options'); if ((0, locter_1.isObject)(optionsValue)) { options = optionsValue; } const { examples, exampleLabels } = this.getParameterExample(parameterName); if (source === constants_1.ParameterSource.QUERY) { // yeah! we can transform the object to individual properties. if ((0, resolver_1.isNestedObjectLiteralType)(type) || (0, resolver_1.isRefObjectType)(type)) { return this.buildParametersForObject(type, { in: constants_1.ParameterSource.QUERY_PROP, examples, exampleLabels, }); // todo: transform ( type.typeName === 'array') } } const properties = { allowEmptyValue: options.allowEmptyValue, collectionFormat: options.collectionFormat, default: (0, utils_1.getInitializerValue)(this.parameter.initializer, this.current.typeChecker, type), description: this.getParameterDescription(), examples, exampleLabels, in: source, maxItems: options.maxItems, minItems: options.minItems, name, parameterName, required: !this.parameter.questionToken && !this.parameter.initializer, type, deprecated: this.getParameterDeprecation(), validators: (0, utils_1.getDeclarationValidators)(this.parameter, parameterName), }; if ((0, resolver_1.isArrayType)(type)) { if (!this.isTypeSupported(type.elementType)) { throw error_1.ParameterError.typeUnsupported({ decoratorName: manager.representation.name, propertyName: name, type: type.elementType, node: this.parameter, }); } return [{ ...properties, collectionFormat: constants_1.CollectionFormat.MULTI, type, }]; } // todo: investigate if this refEnum and union are valid types if (!this.isTypeSupportedForQueryParameter(type)) { throw error_1.ParameterError.typeUnsupported({ decoratorName: manager.representation.name, propertyName: name, type, node: this.parameter, }); } return [properties]; } isTypeSupportedForQueryParameter(type) { return this.isTypeSupported(type) || (0, resolver_1.isRefEnumType)(type) || (0, resolver_1.isUnionType)(type); } getPathParameter(manager) { const parameterName = this.parameter.name.text; let name = parameterName; const type = this.getValidatedType(this.parameter); const value = manager.get('value'); if (typeof value === 'string') { name = value; } const { examples, exampleLabels } = this.getParameterExample(parameterName); if ((0, resolver_1.isNestedObjectLiteralType)(type) || (0, resolver_1.isRefObjectType)(type)) { const output = this.buildParametersForObject(type, { in: constants_1.ParameterSource.PATH, examples, exampleLabels, }); for (let i = 0; i < output.length; i++) { if ((!this.path.includes(`{${output[i].name}}`)) && (!this.path.includes(`:${output[i].name}`))) { throw error_1.ParameterError.invalidPathMatch({ decoratorName: manager.representation.name, propertyName: name, path: this.path, node: this.parameter, }); } } return output; } if (!this.isTypeSupported(type)) { throw error_1.ParameterError.typeUnsupported({ decoratorName: manager.representation.name, propertyName: name, type, node: this.parameter, }); } if ((!this.path.includes(`{${name}}`)) && (!this.path.includes(`:${name}`))) { throw error_1.ParameterError.invalidPathMatch({ decoratorName: manager.representation.name, propertyName: name, path: this.path, node: this.parameter, }); } return [ { default: (0, utils_1.getInitializerValue)(this.parameter.initializer, this.current.typeChecker, type), description: this.getParameterDescription(), examples, exampleLabels, in: constants_1.ParameterSource.PATH, name: name || parameterName, parameterName, required: true, type, deprecated: this.getParameterDeprecation(), validators: (0, utils_1.getDeclarationValidators)(this.parameter, parameterName), }, ]; } // ------------------------------------------------------------------------------------- getParameterDescription() { const symbol = this.current.typeChecker.getSymbolAtLocation(this.parameter.name); if (symbol) { const comments = symbol.getDocumentationComment(this.current.typeChecker); if (comments.length) { return ts.displayPartsToString(comments); } } return ''; } getParameterDeprecation() { if ((0, utils_1.hasJSDocTag)(this.parameter, utils_1.JSDocTagName.DEPRECATED)) { return true; } const match = this.current.decoratorResolver.match(decorator_1.DecoratorID.DEPRECATED, this.parameter); return !!match; } getParameterExample(parameterName) { const exampleLabels = []; const examples = (0, utils_1.getJSDocTags)(this.parameter.parent, (tag) => { const comment = (0, utils_1.transformJSDocComment)(tag.comment); const isExample = (tag.tagName.text === utils_1.JSDocTagName.EXAMPLE || tag.tagName.escapedText === utils_1.JSDocTagName.EXAMPLE) && !!comment && comment.startsWith(parameterName); if (isExample && comment) { const hasExampleLabel = (comment.split(' ')[0].indexOf('.') || -1) > 0; // custom example label is delimited by first '.' and the rest will all be included as example label exampleLabels.push(hasExampleLabel ? comment.split(' ')[0].split('.').slice(1).join('.') : undefined); } return isExample ?? false; }).map((tag) => ((0, utils_1.transformJSDocComment)(tag.comment) || '') .replace(`${(0, utils_1.transformJSDocComment)(tag.comment)?.split(' ')[0] || ''}`, '') .replace(/\r/g, '')); if (examples.length === 0) { return { examples: undefined, exampleLabels: undefined, }; } try { return { examples: examples.map((example) => JSON.parse(example)), exampleLabels, }; } catch (e) { throw error_1.ParameterError.invalidExampleSchema(); } } isBodySupportedForMethod(method) { return ['delete', 'post', 'put', 'patch', 'get'].some((m) => m === method); } isTypeSupported(parameterType) { return [ resolver_1.TypeName.STRING, resolver_1.TypeName.INTEGER, resolver_1.TypeName.LONG, resolver_1.TypeName.FLOAT, resolver_1.TypeName.DOUBLE, resolver_1.TypeName.DATE, resolver_1.TypeName.DATETIME, resolver_1.TypeName.BUFFER, resolver_1.TypeName.BOOLEAN, resolver_1.TypeName.ENUM, ].find((t) => t === parameterType.typeName); } getValidatedType(parameter) { let typeNode = parameter.type; if (!typeNode) { const type = this.current.typeChecker.getTypeAtLocation(parameter); typeNode = this.current.typeChecker.typeToTypeNode(type, undefined, ts.NodeBuilderFlags.NoTruncation); } return new resolver_1.TypeNodeResolver(typeNode, this.current, parameter).resolve(); } } exports.ParameterGenerator = ParameterGenerator; //# sourceMappingURL=module.js.map