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