@trapi/swagger
Version:
Generate Swagger files from a decorator APIs.
1,295 lines (1,294 loc) • 46.1 kB
JavaScript
import { BaseError } from "@ebec/core";
import { ParameterSource, TypeName, isAnyType, isArrayType, isBinaryType, isEnumType, isIntersectionType, isMetadata, isNestedObjectLiteralType, isNeverType, isPrimitiveType, isRefAliasType, isRefEnumType, isRefObjectType, isReferenceType, isTupleType, isUndefinedType, isUnionType, isVoidType } from "@trapi/core";
import { URL } from "node:url";
import { isObject, merge } from "smob";
import path from "node:path";
import fs from "node:fs";
import process from "node:process";
import YAML from "yamljs";
//#region src/core/config/utils.ts
function buildSpecGeneratorOptions(input) {
const servers = [];
if (input.servers) if (Array.isArray(input.servers)) for (const server of input.servers) if (typeof server === "string") servers.push({ url: server });
else servers.push(server);
else if (typeof input.servers === "string") servers.push({ url: input.servers });
else servers.push(input.servers);
return {
...input,
servers
};
}
//#endregion
//#region src/core/constants.ts
const Version = {
V2: "v2",
V3: "v3",
V3_1: "v3.1",
V3_2: "v3.2"
};
const DocumentFormat = {
YAML: "yaml",
JSON: "json"
};
const SecurityType = {
API_KEY: "apiKey",
BASIC: "basic",
HTTP: "http",
OAUTH2: "oauth2"
};
//#endregion
//#region src/core/error/codes.ts
const SwaggerErrorCode = {
SPEC_NOT_BUILT: "SWAGGER_SPEC_NOT_BUILT",
ENUM_UNSUPPORTED_TYPE: "SWAGGER_ENUM_UNSUPPORTED_TYPE",
BODY_PARAMETER_DUPLICATE: "SWAGGER_BODY_PARAMETER_DUPLICATE",
BODY_FORM_CONFLICT: "SWAGGER_BODY_FORM_CONFLICT",
PARAMETER_SOURCE_UNSUPPORTED: "SWAGGER_PARAMETER_SOURCE_UNSUPPORTED",
METADATA_INVALID: "SWAGGER_METADATA_INVALID"
};
//#endregion
//#region src/core/error/module.ts
var SwaggerError = class extends BaseError {};
//#endregion
//#region src/core/schema/v2/constants.ts
const ParameterSourceV2 = {
BODY: "body",
FORM_DATA: "formData",
HEADER: "header",
PATH: "path",
QUERY: "query"
};
//#endregion
//#region src/core/schema/v3/constants.ts
const ParameterSourceV3 = {
COOKIE: "cookie",
HEADER: "header",
PATH: "path",
QUERY: "query"
};
//#endregion
//#region src/core/schema/constants.ts
const TransferProtocol = {
HTTP: "http",
HTTPS: "https",
WS: "ws",
WSS: "wss"
};
const DataFormatName = {
INT_32: "int32",
INT_64: "int64",
FLOAT: "float",
DOUBLE: "double",
BYTE: "byte",
BINARY: "binary",
DATE: "date",
DATE_TIME: "date-time",
PASSWORD: "password"
};
const DataTypeName = {
VOID: "void",
INTEGER: "integer",
NUMBER: "number",
BOOLEAN: "boolean",
STRING: "string",
ARRAY: "array",
OBJECT: "object",
FILE: "file"
};
//#endregion
//#region src/core/utils/character.ts
function removeDuplicateSlashes(str) {
return str.replace(/([^:]\/)\/+/g, "$1");
}
function removeFinalCharacter(str, character) {
while (str.charAt(str.length - 1) === character && str.length > 0) str = str.slice(0, -1);
return str;
}
//#endregion
//#region src/core/utils/path.ts
function normalizePathParameters(str) {
str = str.replace(/<:([^/]+)>/g, "{$1}");
str = str.replace(/:([^/]+)/g, "{$1}");
str = str.replace(/<([^/]+)>/g, "{$1}");
return str;
}
function joinPaths(...segments) {
let result = segments.join("/").replace(/\/{2,}/g, "/");
if (!result.startsWith("/")) result = `/${result}`;
if (result.length > 1 && result.endsWith("/")) result = result.slice(0, -1);
return result;
}
//#endregion
//#region src/core/utils/object.ts
function hasOwnProperty(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop);
}
//#endregion
//#region src/core/utils/value.ts
function transformValueTo(type, value) {
if (value === null) return null;
switch (type) {
case "integer":
case "number": return Number(value);
case "boolean": return !!value;
default: return String(value);
}
}
//#endregion
//#region src/adapters/generator/abstract.ts
var AbstractSpecGenerator = class {
spec;
metadata;
config;
constructor(metadata, config) {
this.metadata = metadata;
this.config = buildSpecGeneratorOptions(config);
}
buildInfo() {
const info = {
title: this.config.name || "Documentation",
version: this.config.version || "1.0.0"
};
if (this.config.description) info.description = this.config.description;
if (this.config.license) info.license = { name: this.config.license };
return info;
}
buildTags() {
const tagMap = /* @__PURE__ */ new Map();
for (const controller of this.metadata.controllers) {
if (controller.hidden) continue;
const extensions = controller.extensions ?? [];
if (extensions.length === 0) continue;
const tagNames = controller.tags.length > 0 ? controller.tags : [controller.name];
const extensionFields = this.transformExtensions(extensions);
for (const tagName of tagNames) {
let entry = tagMap.get(tagName);
if (!entry) {
entry = { name: tagName };
tagMap.set(tagName, entry);
}
Object.assign(entry, extensionFields);
}
}
return Array.from(tagMap.values());
}
getSchemaForType(type) {
if (isVoidType(type) || isUndefinedType(type) || isNeverType(type)) return {};
if (isReferenceType(type)) return this.getSchemaForReferenceType(type);
if (isPrimitiveType(type)) return this.getSchemaForPrimitiveType(type);
if (isArrayType(type)) return this.getSchemaForArrayType(type);
if (isTupleType(type)) return this.getSchemaForTupleType(type);
if (isEnumType(type)) return this.getSchemaForEnumType(type);
if (isUnionType(type)) return this.getSchemaForUnionType(type);
if (isIntersectionType(type)) return this.getSchemaForIntersectionType(type);
if (isNestedObjectLiteralType(type)) return this.getSchemaForObjectLiteralType(type);
return {};
}
getSchemaForEnumType(enumType) {
const nullable = enumType.members.includes(null);
const nonNullMembers = enumType.members.filter((m) => m !== null);
const type = this.decideEnumType(nonNullMembers);
const schema = {
type,
enum: enumType.members.map((member) => transformValueTo(type, member))
};
this.applyNullable(schema, nullable);
return schema;
}
getSchemaForPrimitiveType(type) {
return {
[TypeName.ANY]: { additionalProperties: true },
[TypeName.BINARY]: {
type: DataTypeName.STRING,
format: DataFormatName.BINARY
},
[TypeName.BOOLEAN]: { type: DataTypeName.BOOLEAN },
[TypeName.BUFFER]: {
type: DataTypeName.STRING,
format: DataFormatName.BYTE
},
[TypeName.BYTE]: {
type: DataTypeName.STRING,
format: DataFormatName.BYTE
},
[TypeName.DATE]: {
type: DataTypeName.STRING,
format: DataFormatName.DATE
},
[TypeName.DATETIME]: {
type: DataTypeName.STRING,
format: DataFormatName.DATE_TIME
},
[TypeName.DOUBLE]: {
type: DataTypeName.NUMBER,
format: DataFormatName.DOUBLE
},
[TypeName.FILE]: {
type: DataTypeName.STRING,
format: DataFormatName.BINARY
},
[TypeName.FLOAT]: {
type: DataTypeName.NUMBER,
format: DataFormatName.FLOAT
},
[TypeName.BIGINT]: { type: DataTypeName.INTEGER },
[TypeName.INTEGER]: {
type: DataTypeName.INTEGER,
format: DataFormatName.INT_32
},
[TypeName.LONG]: {
type: DataTypeName.INTEGER,
format: DataFormatName.INT_64
},
[TypeName.OBJECT]: {
type: DataTypeName.OBJECT,
additionalProperties: true
},
[TypeName.STRING]: { type: DataTypeName.STRING },
[TypeName.UNDEFINED]: {}
}[type.typeName] || { type: DataTypeName.OBJECT };
}
getSchemaForArrayType(arrayType) {
return {
type: DataTypeName.ARRAY,
items: this.getSchemaForType(arrayType.elementType)
};
}
getSchemaForTupleType(tupleType) {
if (tupleType.elements.length === 0) return {
type: DataTypeName.ARRAY,
items: {}
};
const elementSchemas = tupleType.elements.map((el) => this.getSchemaForType(el.type));
if (elementSchemas.length === 1) return {
type: DataTypeName.ARRAY,
items: elementSchemas[0]
};
return {
type: DataTypeName.ARRAY,
items: { anyOf: elementSchemas }
};
}
getSchemaForObjectLiteralType(objectLiteral) {
const properties = this.buildProperties(objectLiteral.properties);
const additionalProperties = objectLiteral.additionalProperties && this.getSchemaForType(objectLiteral.additionalProperties);
const required = objectLiteral.properties.filter((prop) => prop.required && !this.isUndefinedProperty(prop)).map((prop) => prop.name);
return {
properties,
...additionalProperties && { additionalProperties },
...required && required.length && { required },
type: DataTypeName.OBJECT
};
}
getSchemaForReferenceType(referenceType) {
return { $ref: `${this.getRefPrefix()}${referenceType.refName}` };
}
buildSchemaForRefAlias(referenceType) {
const swaggerType = this.getSchemaForType(referenceType.type);
const format = referenceType.format;
return {
...swaggerType,
default: referenceType.default ?? swaggerType.default,
example: referenceType.example ?? swaggerType.example,
format: format ?? swaggerType.format,
description: referenceType.description ?? swaggerType.description,
...this.transformValidators(referenceType.validators)
};
}
buildSchemaForRefEnum(referenceType) {
const output = {
...this.getSchemaForEnumType({
typeName: TypeName.ENUM,
members: referenceType.members
}),
description: referenceType.description
};
if (typeof referenceType.memberNames !== "undefined" && referenceType.members.length === referenceType.memberNames.length) output["x-enum-varnames"] = referenceType.memberNames;
return output;
}
buildSchemasForReferenceTypes(extendFn) {
const output = {};
for (const referenceType of Object.values(this.metadata.referenceTypes)) {
switch (referenceType.typeName) {
case TypeName.REF_ALIAS:
output[referenceType.refName] = this.buildSchemaForRefAlias(referenceType);
break;
case TypeName.REF_ENUM:
output[referenceType.refName] = this.buildSchemaForRefEnum(referenceType);
break;
case TypeName.REF_OBJECT:
output[referenceType.refName] = this.buildSchemaForRefObject(referenceType);
break;
}
if (typeof extendFn === "function") extendFn(output[referenceType.refName], referenceType);
}
return output;
}
isUndefinedProperty(input) {
return isUndefinedType(input.type) || isUnionType(input.type) && input.type.members.some((el) => isUndefinedType(el));
}
buildProperties(properties) {
const output = {};
properties.forEach((property) => {
const swaggerType = this.getSchemaForType(property.type);
if (swaggerType.$ref && this.shouldStripRefSiblings()) {
output[property.name] = { $ref: swaggerType.$ref };
return;
}
swaggerType.description = property.description;
swaggerType.example = property.example;
swaggerType.format = property.format || swaggerType.format;
this.assignPropertyDefaults(swaggerType, property);
if (property.deprecated) this.markPropertyDeprecated(swaggerType);
const extensions = this.transformExtensions(property.extensions);
const validators = this.transformValidators(property.validators);
output[property.name] = {
...swaggerType,
...validators,
...extensions
};
});
return output;
}
shouldStripRefSiblings() {
return true;
}
assignPropertyDefaults(_schema, _property) {}
buildSchemaForRefObject(referenceType) {
const required = referenceType.properties.filter((p) => p.required && !this.isUndefinedProperty(p)).map((p) => p.name);
const output = {
description: referenceType.description,
properties: this.buildProperties(referenceType.properties),
required: required && required.length > 0 ? Array.from(new Set(required)) : void 0,
type: DataTypeName.OBJECT
};
if (referenceType.additionalProperties) output.additionalProperties = this.resolveAdditionalProperties(referenceType.additionalProperties);
if (referenceType.example !== void 0) output.example = referenceType.example;
return output;
}
determineTypesUsedInEnum(anEnum) {
const set = /* @__PURE__ */ new Set();
for (const element of anEnum) {
if (element === null) continue;
set.add(typeof element);
}
return Array.from(set);
}
decideEnumType(input) {
const types = this.determineTypesUsedInEnum(input);
if (types.length === 1) {
const value = types[0];
if (value === "string" || value === "number" || value === "boolean") return value;
throw new SwaggerError({
message: `Enum contains unsupported type '${types[0] || "unknown"}'. Only string, number, and boolean values are allowed.`,
code: SwaggerErrorCode.ENUM_UNSUPPORTED_TYPE
});
}
const unsupportedTypes = types.filter((type) => type !== "string" && type !== "number" && type !== "boolean");
if (unsupportedTypes.length > 0) throw new SwaggerError({
message: `Enum contains unsupported types: ${unsupportedTypes.join(", ")}. Only string, number, and boolean values are allowed.`,
code: SwaggerErrorCode.ENUM_UNSUPPORTED_TYPE
});
return "string";
}
getOperationId(name) {
return name.charAt(0).toUpperCase() + name.substring(1);
}
groupParameters(items) {
const output = {};
for (const item of items) (output[item.in] ?? (output[item.in] = [])).push(item);
return output;
}
transformExtensions(input) {
if (!input) return {};
const output = {};
for (const extension of input) {
const key = extension.key.startsWith("x-") ? extension.key : `x-${extension.key}`;
output[key] = extension.value;
}
return output;
}
transformValidators(input) {
if (!isObject(input)) return {};
const output = {};
for (const [name, validator] of Object.entries(input)) {
const mapping = validator.meta?.openApi ?? DEFAULT_VALIDATOR_OPENAPI_MAPPINGS[name];
if (!mapping || mapping.kind === "ignore") continue;
if (mapping.kind === "keyword") output[mapping.key] = validator.value;
else if (mapping.kind === "format") output.format = mapping.format;
}
return output;
}
};
const DEFAULT_VALIDATOR_OPENAPI_MAPPINGS = {
maxLength: {
kind: "keyword",
key: "maxLength"
},
minLength: {
kind: "keyword",
key: "minLength"
},
maximum: {
kind: "keyword",
key: "maximum"
},
minimum: {
kind: "keyword",
key: "minimum"
},
pattern: {
kind: "keyword",
key: "pattern"
},
maxItems: {
kind: "keyword",
key: "maxItems"
},
minItems: {
kind: "keyword",
key: "minItems"
},
uniqueItems: {
kind: "keyword",
key: "uniqueItems"
}
};
//#endregion
//#region src/adapters/generator/v2/module.ts
function uniqueOperationId$1(base, used) {
if (!used.has(base)) {
used.add(base);
return base;
}
let counter = 2;
while (used.has(`${base}_${counter}`)) counter += 1;
const candidate = `${base}_${counter}`;
used.add(candidate);
return candidate;
}
var V2Generator = class V2Generator extends AbstractSpecGenerator {
async build() {
if (typeof this.spec !== "undefined") return this.spec;
let spec = {
definitions: this.buildSchemasForReferenceTypes(),
info: this.buildInfo(),
paths: this.buildPaths(),
swagger: "2.0"
};
spec.securityDefinitions = this.config.securityDefinitions ? V2Generator.translateSecurityDefinitions(this.config.securityDefinitions) : {};
if (this.config.consumes) spec.consumes = this.config.consumes;
if (this.config.produces) spec.produces = this.config.produces;
const firstServer = this.config.servers?.[0];
if (firstServer) {
const url = new URL(firstServer.url, "http://localhost:3000/");
spec.host = url.host;
if (url.pathname) spec.basePath = url.pathname;
}
const tags = this.buildTags();
if (tags.length > 0) spec.tags = tags;
if (this.config.specificationExtra) spec = merge(spec, this.config.specificationExtra);
this.spec = spec;
return spec;
}
static translateSecurityDefinitions(securityDefinitions) {
const definitions = {};
for (const [key, securityDefinition] of Object.entries(securityDefinitions)) switch (securityDefinition.type) {
case "http":
if (securityDefinition.scheme === "basic") definitions[key] = { type: "basic" };
break;
case "apiKey":
definitions[key] = securityDefinition;
break;
case "oauth2":
if (securityDefinition.flows.implicit) definitions[`${key}Implicit`] = {
type: "oauth2",
flow: "implicit",
authorizationUrl: securityDefinition.flows.implicit.authorizationUrl,
scopes: securityDefinition.flows.implicit.scopes
};
if (securityDefinition.flows.password) definitions[`${key}Password`] = {
type: "oauth2",
flow: "password",
tokenUrl: securityDefinition.flows.password.tokenUrl,
scopes: securityDefinition.flows.password.scopes
};
if (securityDefinition.flows.authorizationCode) definitions[`${key}AccessCode`] = {
type: "oauth2",
flow: "accessCode",
tokenUrl: securityDefinition.flows.authorizationCode.tokenUrl,
authorizationUrl: securityDefinition.flows.authorizationCode.authorizationUrl,
scopes: securityDefinition.flows.authorizationCode.scopes
};
if (securityDefinition.flows.clientCredentials) definitions[`${key}Application`] = {
type: "oauth2",
flow: "application",
tokenUrl: securityDefinition.flows.clientCredentials.tokenUrl,
scopes: securityDefinition.flows.clientCredentials.scopes
};
break;
}
return definitions;
}
resolveAdditionalProperties(_type) {
return true;
}
markPropertyDeprecated(schema) {
schema["x-deprecated"] = true;
}
buildPaths() {
const output = {};
const usedOperationIds = /* @__PURE__ */ new Set();
const unique = (input) => [...new Set(input)];
this.metadata.controllers.forEach((controller) => {
if (controller.hidden) return;
const controllerPaths = controller.paths.length === 0 ? [""] : controller.paths;
controller.methods.forEach((method) => {
if (method.hidden) return;
method.consumes = unique([...controller.consumes, ...method.consumes]);
method.produces = unique([...controller.produces, ...method.produces]);
method.tags = unique([...controller.tags, ...method.tags]);
if (!method.security?.length) method.security = controller.security;
method.deprecated = method.deprecated || controller.deprecated;
method.responses = unique([...controller.responses, ...method.responses]);
for (const controllerPath of controllerPaths) {
const fullPath = normalizePathParameters(joinPaths(controllerPath, method.path));
const pathItem = output[fullPath] ?? (output[fullPath] = {});
pathItem[method.method] = this.buildMethod(method, fullPath, usedOperationIds);
}
});
});
return output;
}
buildMethod(method, emittedPath, usedOperationIds) {
const output = this.buildOperation(method);
output.consumes = this.buildMethodConsumes(method);
output.operationId = uniqueOperationId$1(method.operationId || output.operationId, usedOperationIds);
output.description = method.description;
if (method.summary) output.summary = method.summary;
if (method.deprecated) output.deprecated = method.deprecated;
if (method.tags.length) output.tags = method.tags;
if (method.security?.length) output.security = method.security;
const parameters = this.groupParameters(method.parameters);
output.parameters = [
...(parameters[ParameterSource.PATH] || []).filter((p) => emittedPath.includes(`{${p.name}}`)),
...parameters[ParameterSource.QUERY_PROP] || [],
...parameters[ParameterSource.HEADER] || [],
...parameters[ParameterSource.FORM_DATA] || []
].map((p) => this.buildParameter(p));
const bodyParameters = parameters[ParameterSource.BODY] || [];
if (bodyParameters.length > 1) throw new SwaggerError({
message: `Only one body parameter allowed per method, but ${bodyParameters.length} found in '${method.name}'.`,
code: SwaggerErrorCode.BODY_PARAMETER_DUPLICATE
});
const bodyParameter = bodyParameters[0] ? this.buildParameter(bodyParameters[0]) : void 0;
const bodyPropParams = parameters[ParameterSource.BODY_PROP] || [];
if (bodyPropParams.length > 0) {
const schema = {
type: DataTypeName.OBJECT,
title: "Body",
properties: {}
};
const required = [];
for (const bodyPropParam of bodyPropParams) {
const bodyProp = this.getSchemaForType(bodyPropParam.type);
bodyProp.default = bodyPropParam.default;
bodyProp.description = bodyPropParam.description;
bodyProp.example = bodyPropParam.examples;
if (bodyProp.required) required.push(bodyPropParam.name);
schema.properties[bodyPropParam.name] = bodyProp;
}
if (bodyParameter && bodyParameter.in === ParameterSourceV2.BODY) {
if (bodyParameter.schema.type === DataTypeName.OBJECT) {
bodyParameter.schema.properties = {
...bodyParameter.schema.properties || {},
...schema.properties
};
bodyParameter.schema.required = [...bodyParameter.schema.required || [], ...required];
} else bodyParameter.schema = schema;
output.parameters.push(bodyParameter);
} else {
const parameter = {
in: ParameterSourceV2.BODY,
name: "body",
schema
};
if (required.length) parameter.schema.required = required;
output.parameters.push(parameter);
}
} else if (bodyParameter) output.parameters.push(bodyParameter);
Object.assign(output, this.transformExtensions(method.extensions));
return output;
}
transformParameterSource(source) {
if (source === ParameterSource.BODY) return ParameterSourceV2.BODY;
if (source === ParameterSource.FORM_DATA) return ParameterSourceV2.FORM_DATA;
if (source === ParameterSource.HEADER) return ParameterSourceV2.HEADER;
if (source === ParameterSource.PATH) return ParameterSourceV2.PATH;
if (source === ParameterSource.QUERY || source === ParameterSource.QUERY_PROP) return ParameterSourceV2.QUERY;
}
buildParameter(input) {
const sourceIn = this.transformParameterSource(input.in);
if (!sourceIn) throw new SwaggerError({
message: `The parameter source '${input.in}' for parameter '${input.name}' is not supported in OpenAPI 2.0.`,
code: SwaggerErrorCode.PARAMETER_SOURCE_UNSUPPORTED
});
const parameter = {
description: input.description,
in: sourceIn,
name: input.name,
required: input.required
};
Object.assign(parameter, this.transformExtensions(input.extensions));
if (input.in !== ParameterSource.BODY && isRefEnumType(input.type)) input.type = {
typeName: TypeName.ENUM,
members: input.type.members
};
if (parameter.in === ParameterSourceV2.FORM_DATA && input.type.typeName === TypeName.FILE) {
parameter.type = "file";
Object.assign(parameter, this.transformValidators(input.validators));
return parameter;
}
const parameterType = this.getSchemaForType(input.type);
if (parameter.in !== ParameterSourceV2.BODY && parameterType.format) parameter.format = parameterType.format;
if ((parameter.in === ParameterSourceV2.FORM_DATA || parameter.in === ParameterSourceV2.QUERY) && (input.type.typeName === TypeName.ARRAY || parameterType.type === DataTypeName.ARRAY)) parameter.collectionFormat = input.collectionFormat || this.config.collectionFormat || "multi";
if (parameter.in === ParameterSourceV2.BODY) {
if (input.type.typeName === TypeName.ARRAY || parameterType.type === DataTypeName.ARRAY) parameter.schema = {
items: parameterType.items,
type: DataTypeName.ARRAY
};
else if (input.type.typeName === TypeName.ANY) parameter.schema = { type: DataTypeName.OBJECT };
else parameter.schema = parameterType;
parameter.schema = {
...parameter.schema,
...this.transformValidators(input.validators)
};
return parameter;
}
Object.assign(parameter, this.transformValidators(input.validators));
if (input.type.typeName === TypeName.ANY) parameter.type = DataTypeName.STRING;
else if (parameterType.type && !Array.isArray(parameterType.type)) parameter.type = parameterType.type;
if (parameterType.items) parameter.items = parameterType.items;
if (parameterType.enum) parameter.enum = parameterType.enum;
if (typeof input.default !== "undefined") parameter.default = input.default;
return parameter;
}
buildMethodConsumes(method) {
if (method.consumes && method.consumes.length > 0) return method.consumes;
if (this.hasFileParams(method)) return ["multipart/form-data"];
if (this.hasFormParams(method)) return ["application/x-www-form-urlencoded"];
if (this.supportsBodyParameters(method.method)) return ["application/json"];
return [];
}
hasFileParams(method) {
return method.parameters.some((p) => p.in === ParameterSource.FORM_DATA && p.type.typeName === "file");
}
hasFormParams(method) {
return method.parameters.some((p) => p.in === ParameterSource.FORM_DATA);
}
supportsBodyParameters(method) {
return [
"post",
"put",
"patch"
].includes(method);
}
applyNullable(schema, nullable) {
schema["x-nullable"] = nullable;
}
getRefPrefix() {
return "#/definitions/";
}
getSchemaForIntersectionType(type) {
const properties = type.members.reduce((acc, type) => {
if (isRefObjectType(type)) {
const refType = this.metadata.referenceTypes[type.refName];
const props = refType && refType.properties && refType.properties.reduce((pAcc, prop) => ({
...pAcc,
[prop.name]: this.getSchemaForType(prop.type)
}), {});
return {
...acc,
...props
};
}
return { ...acc };
}, {});
return {
type: DataTypeName.OBJECT,
properties
};
}
getSchemaForUnionType(type) {
const members = [];
const enumTypeMember = {
typeName: TypeName.ENUM,
members: []
};
for (const member of type.members) {
if (isEnumType(member)) enumTypeMember.members.push(...member.members);
if (!isAnyType(member) && !isUndefinedType(member) && !isNeverType(member) && !isEnumType(member)) members.push(member);
}
if (members.length === 0 && enumTypeMember.members.length > 0) return this.getSchemaForEnumType(enumTypeMember);
const isNullEnum = enumTypeMember.members.every((member) => member === null);
if (members.length === 1) {
const single = members[0];
if (isNullEnum) {
const memberType = this.getSchemaForType(single);
if (memberType.$ref) return memberType;
memberType["x-nullable"] = true;
return memberType;
}
if (enumTypeMember.members.length === 0) return this.getSchemaForType(single);
}
return {
type: DataTypeName.OBJECT,
...isNullEnum ? { "x-nullable": true } : {}
};
}
buildOperation(method) {
const operation = {
operationId: this.getOperationId(method.name),
consumes: method.consumes || [],
produces: method.produces || [],
responses: {}
};
const produces = [];
method.responses.forEach((res) => {
operation.responses[res.status] = { description: res.description };
if (res.schema && !isVoidType(res.schema) && !isNeverType(res.schema)) {
if (res.produces) produces.push(...res.produces);
else if (isBinaryType(res.schema)) produces.push("application/octet-stream");
operation.responses[res.status].schema = this.getSchemaForType(res.schema);
}
const example = res.examples?.[0];
if (example?.value) operation.responses[res.status].examples = { "application/json": example.value };
});
const consumes = operation.consumes;
if (consumes.length === 0) {
if (method.parameters.some((parameter) => parameter.in === ParameterSource.BODY || parameter.in === ParameterSource.BODY_PROP)) consumes.push("application/json");
if (method.parameters.some((parameter) => parameter.in === ParameterSource.FORM_DATA)) consumes.push("multipart/form-data");
}
if (operation.produces.length === 0 && produces.length > 0) operation.produces = [...new Set(produces)];
if (operation.produces.length === 0) operation.produces = ["application/json"];
return operation;
}
};
//#endregion
//#region src/adapters/generator/v3/module.ts
const OPENAPI_VERSION_MAP = {
v3: "3.0.0",
"v3.1": "3.1.0",
"v3.2": "3.2.0"
};
function uniqueOperationId(base, used) {
if (!used.has(base)) {
used.add(base);
return base;
}
let counter = 2;
while (used.has(`${base}_${counter}`)) counter += 1;
const candidate = `${base}_${counter}`;
used.add(candidate);
return candidate;
}
var V3Generator = class V3Generator extends AbstractSpecGenerator {
openApiVersion;
constructor(metadata, config, version = "v3.2") {
super(metadata, config);
this.openApiVersion = OPENAPI_VERSION_MAP[version] || "3.2.0";
}
async build() {
if (typeof this.spec !== "undefined") return this.spec;
let spec = {
components: this.buildComponents(),
info: this.buildInfo(),
openapi: this.openApiVersion,
paths: this.buildPaths(),
servers: this.buildServers(),
tags: this.buildTags()
};
if (this.config.specificationExtra) spec = merge(spec, this.config.specificationExtra);
this.spec = spec;
return spec;
}
buildComponents() {
const components = {
examples: {},
headers: {},
parameters: {},
requestBodies: {},
responses: {},
schemas: this.buildSchemasForReferenceTypes((output, referenceType) => {
if (referenceType.deprecated) output.deprecated = true;
}),
securitySchemes: {}
};
if (this.config.securityDefinitions) components.securitySchemes = V3Generator.translateSecurityDefinitions(this.config.securityDefinitions);
return components;
}
static translateSecurityDefinitions(securityDefinitions) {
const output = {};
for (const [key, securityDefinition] of Object.entries(securityDefinitions)) switch (securityDefinition.type) {
case "http":
output[key] = securityDefinition;
break;
case "oauth2":
output[key] = securityDefinition;
break;
case "apiKey":
output[key] = securityDefinition;
break;
}
return output;
}
buildPaths() {
const output = {};
const usedOperationIds = /* @__PURE__ */ new Set();
for (const controller of this.metadata.controllers) {
if (controller.hidden) continue;
const controllerPaths = controller.paths.length === 0 ? [""] : controller.paths;
for (const method of controller.methods) {
if (method.hidden) continue;
method.deprecated = method.deprecated || controller.deprecated;
if (!method.security?.length) method.security = controller.security;
for (const controllerPath of controllerPaths) {
const path = normalizePathParameters(joinPaths(controllerPath, method.path));
const pathItem = output[path] ?? (output[path] = {});
pathItem[method.method] = this.buildMethod(controller.name, method, path, usedOperationIds);
}
}
}
return output;
}
buildMethod(controllerName, method, emittedPath, usedOperationIds) {
const output = this.buildOperation(controllerName, method);
output.description = method.description;
output.summary = method.summary;
output.tags = method.tags;
output.operationId = uniqueOperationId(method.operationId || output.operationId, usedOperationIds);
if (method.deprecated) output.deprecated = method.deprecated;
if (method.security?.length) output.security = method.security;
const parameters = this.groupParameters(method.parameters);
const pathParams = (parameters[ParameterSource.PATH] || []).filter((p) => emittedPath.includes(`{${p.name}}`));
output.parameters = [
...parameters[ParameterSource.QUERY_PROP] || [],
...parameters[ParameterSource.HEADER] || [],
...pathParams,
...parameters[ParameterSource.COOKIE] || []
].map((p) => this.buildParameter(p));
const bodyParams = parameters[ParameterSource.BODY] || [];
const formParams = parameters[ParameterSource.FORM_DATA] || [];
if (bodyParams.length > 1) throw new SwaggerError({
message: `Only one body parameter allowed per method, but ${bodyParams.length} found in '${method.name}'.`,
code: SwaggerErrorCode.BODY_PARAMETER_DUPLICATE
});
if (bodyParams.length > 0 && formParams.length > 0) throw new SwaggerError({
message: `Cannot mix body and form parameters in method '${method.name}'.`,
code: SwaggerErrorCode.BODY_FORM_CONFLICT
});
const bodyPropParams = parameters[ParameterSource.BODY_PROP] || [];
const firstBodyProp = bodyPropParams[0];
if (firstBodyProp) {
if (bodyParams.length === 0) bodyParams.push({
in: ParameterSource.BODY,
name: "body",
description: "",
parameterName: firstBodyProp.parameterName || "body",
required: true,
type: {
typeName: TypeName.NESTED_OBJECT_LITERAL,
properties: []
},
validators: {},
deprecated: false,
extensions: []
});
const firstBody = bodyParams[0];
if (isNestedObjectLiteralType(firstBody.type)) for (const bodyPropParam of bodyPropParams) firstBody.type.properties.push({
default: bodyPropParam.default,
validators: bodyPropParam.validators,
description: bodyPropParam.description,
name: bodyPropParam.name,
type: bodyPropParam.type,
required: bodyPropParam.required,
deprecated: bodyPropParam.deprecated ?? false
});
}
const firstBodyParam = bodyParams[0];
if (firstBodyParam) output.requestBody = this.buildRequestBody(firstBodyParam);
else if (formParams.length > 0) output.requestBody = this.buildRequestBodyWithFormData(formParams);
Object.assign(output, this.transformExtensions(method.extensions));
return output;
}
buildRequestBodyWithFormData(parameters) {
const required = [];
const properties = {};
for (const parameter of parameters) {
properties[parameter.name] = this.buildMediaType(parameter).schema;
if (parameter.required) required.push(parameter.name);
}
return {
required: required.length > 0,
content: { "multipart/form-data": { schema: {
type: DataTypeName.OBJECT,
properties,
...required && required.length && { required }
} } }
};
}
buildRequestBody(parameter) {
const mediaType = this.buildMediaType(parameter);
return {
description: parameter.description,
required: parameter.required,
content: { "application/json": mediaType }
};
}
buildMediaType(parameter) {
const examples = this.transformParameterExamples(parameter);
return {
schema: this.getSchemaForType(parameter.type),
...Object.keys(examples).length > 0 && { examples }
};
}
buildResponses(input) {
const output = {};
for (const res of input) {
const name = res.status || "default";
const response = { description: res.description };
output[name] = response;
if (res.schema && !isVoidType(res.schema) && !isNeverType(res.schema)) {
const examples = {};
if (res.examples) for (const [i, ex] of res.examples.entries()) {
const label = ex.label || `example${i + 1}`;
examples[label] = { value: ex.value };
}
const content = response.content ?? (response.content = {});
const contentTypes = res.produces || ["application/json"];
for (const contentType of contentTypes) content[contentType] = {
schema: this.getSchemaForType(res.schema),
...Object.keys(examples).length > 0 && { examples }
};
}
if (res.headers) {
const headers = {};
if (isRefObjectType(res.headers)) headers[res.headers.refName] = {
schema: this.getSchemaForReferenceType(res.headers),
description: res.headers.description
};
else if (isNestedObjectLiteralType(res.headers)) res.headers.properties.forEach((each) => {
headers[each.name] = {
schema: this.getSchemaForType(each.type),
description: each.description,
required: each.required
};
});
response.headers = headers;
}
}
return output;
}
buildOperation(_controllerName, method) {
const operation = {
operationId: this.getOperationId(method.name),
responses: this.buildResponses(method.responses)
};
if (method.description) operation.description = method.description;
if (method.security?.length) operation.security = method.security;
if (method.deprecated) operation.deprecated = method.deprecated;
return operation;
}
transformParameterSource(source) {
if (source === ParameterSource.COOKIE) return ParameterSourceV3.COOKIE;
if (source === ParameterSource.HEADER) return ParameterSourceV3.HEADER;
if (source === ParameterSource.PATH) return ParameterSourceV3.PATH;
if (source === ParameterSource.QUERY_PROP || source === ParameterSource.QUERY) return ParameterSourceV3.QUERY;
}
buildParameter(input) {
const sourceIn = this.transformParameterSource(input.in);
if (!sourceIn) throw new SwaggerError({
message: `The parameter source '${input.in}' for parameter '${input.name}' is not supported in OpenAPI 3.x.`,
code: SwaggerErrorCode.PARAMETER_SOURCE_UNSUPPORTED
});
const parameter = {
allowEmptyValue: false,
deprecated: false,
description: input.description,
in: sourceIn,
name: input.name,
required: input.required,
schema: {
default: input.default,
format: void 0,
...this.transformValidators(input.validators)
}
};
Object.assign(parameter, this.transformExtensions(input.extensions));
if (input.deprecated) parameter.deprecated = true;
const parameterType = this.getSchemaForType(input.type);
const schema = parameter.schema;
if (parameterType.format) schema.format = parameterType.format;
if (parameterType.$ref) {
parameter.schema = parameterType;
return parameter;
}
if (isAnyType(input.type)) schema.type = DataTypeName.STRING;
else {
if (parameterType.type) schema.type = parameterType.type;
schema.items = parameterType.items;
schema.enum = parameterType.enum;
}
parameter.examples = this.transformParameterExamples(input);
return parameter;
}
transformParameterExamples(parameter) {
const output = {};
if (parameter.examples) for (const [i, ex] of parameter.examples.entries()) {
const label = ex.label || `example${i + 1}`;
output[label] = { value: ex.value };
}
return output;
}
buildServers() {
const servers = [];
const configured = this.config.servers ?? [];
for (const entry of configured) {
const url = new URL(entry.url, "http://localhost:3000/");
servers.push({
url: `${url.protocol}//${url.host}${url.pathname || ""}`,
...entry.description ? { description: entry.description } : {}
});
}
return servers;
}
resolveAdditionalProperties(type) {
return this.getSchemaForType(type);
}
markPropertyDeprecated(schema) {
schema.deprecated = true;
}
assignPropertyDefaults(schema, property) {
schema.default = property.default;
}
buildSchemaForRefEnum(referenceType) {
const typesUsed = this.determineTypesUsedInEnum(referenceType.members);
if (typesUsed.length === 1) return super.buildSchemaForRefEnum(referenceType);
const schema = {
description: referenceType.description,
anyOf: []
};
for (const element of typesUsed) schema.anyOf.push({
type: element,
enum: referenceType.members.filter((e) => typeof e === element)
});
return schema;
}
isV31OrLater() {
return !this.openApiVersion.startsWith("3.0");
}
shouldStripRefSiblings() {
return !this.isV31OrLater();
}
getSchemaForIntersectionType(type) {
return { allOf: type.members.map((x) => this.getSchemaForType(x)) };
}
applyNullable(schema, nullable) {
if (!nullable) return;
if (this.isV31OrLater()) {
if (schema.type && !Array.isArray(schema.type)) schema.type = [schema.type, "null"];
else if (Array.isArray(schema.type) && !schema.type.includes("null")) schema.type = [...schema.type, "null"];
} else schema.nullable = true;
}
getRefPrefix() {
return "#/components/schemas/";
}
getSchemaForUnionType(type) {
const members = [];
let nullable = false;
const enumMembers = {};
for (const member of type.members) {
if (isEnumType(member)) for (const memberChild of member.members) {
if (memberChild === null || memberChild === void 0) {
nullable = true;
continue;
}
const typeOf = typeof memberChild;
if (typeOf === "string" || typeOf === "number" || typeOf === "boolean") (enumMembers[typeOf] ?? (enumMembers[typeOf] = [])).push(memberChild);
}
if (!isAnyType(member) && !isUndefinedType(member) && !isNeverType(member) && !isEnumType(member)) members.push(member);
}
const schemas = [];
for (const member of members) schemas.push(this.getSchemaForType(member));
const enumMembersKeys = Object.keys(enumMembers);
for (const enumMembersKey of enumMembersKeys) {
const enumType = {
typeName: "enum",
members: enumMembers[enumMembersKey]
};
schemas.push(this.getSchemaForEnumType(enumType));
}
const useOneOf = members.length > 0 && enumMembersKeys.length === 0 && members.every((m) => V3Generator.isObjectLikeType(m));
const compositionKey = useOneOf ? "oneOf" : "anyOf";
if (this.isV31OrLater()) {
if (nullable) schemas.push({ type: "null" });
if (schemas.length === 1) return schemas[0];
const schema = { [compositionKey]: schemas };
if (useOneOf) this.applyDiscriminator(schema, members);
return schema;
}
if (schemas.length === 1) {
const schema = schemas[0];
if (schema.$ref) return {
allOf: [schema],
nullable
};
return {
...schema,
nullable
};
}
const schema = {
[compositionKey]: schemas,
...nullable ? { nullable } : {}
};
if (useOneOf) this.applyDiscriminator(schema, members);
return schema;
}
static isObjectLikeType(type) {
if (isRefObjectType(type) || isNestedObjectLiteralType(type) || isIntersectionType(type)) return true;
if (isRefAliasType(type)) return V3Generator.isObjectLikeType(type.type);
return false;
}
applyDiscriminator(schema, members) {
const discriminator = this.detectDiscriminator(members);
if (discriminator) schema.discriminator = discriminator;
}
detectDiscriminator(members) {
const resolvedMembers = members.map((m) => this.resolveDiscriminatorMember(m));
if (resolvedMembers.some((m) => !m)) return;
const firstProps = resolvedMembers[0].properties;
for (const prop of firstProps) {
if (prop.type.typeName !== "enum") continue;
if (prop.type.members.length !== 1) continue;
const propName = prop.name;
const mapping = {};
let isDiscriminator = true;
for (const member of resolvedMembers) {
const memberProp = member.properties.find((p) => p.name === propName);
if (!memberProp || !memberProp.required || memberProp.type.typeName !== "enum" || memberProp.type.members.length !== 1) {
isDiscriminator = false;
break;
}
const value = String(memberProp.type.members[0]);
if (mapping[value]) {
isDiscriminator = false;
break;
}
mapping[value] = `${this.getRefPrefix()}${member.refName}`;
}
if (isDiscriminator && Object.keys(mapping).length === members.length) return {
propertyName: propName,
mapping
};
}
}
/**
* Resolve a union member to its refName and properties for discriminator
* detection. Accepts both refObject and refAlias members, unwrapping
* aliases to find the underlying properties while preserving the
* original refName for $ref mapping.
*/
resolveDiscriminatorMember(member) {
if (!isRefObjectType(member) && !isRefAliasType(member)) return;
const { refName } = member;
const referenceType = this.metadata.referenceTypes[refName];
if (!referenceType) return;
if (referenceType.typeName === "refObject") return {
refName,
properties: referenceType.properties
};
if (referenceType.typeName === "refAlias") {
let inner = referenceType.type;
for (let depth = 0; depth < 10; depth++) {
if (isRefObjectType(inner)) {
const resolved = this.metadata.referenceTypes[inner.refName];
if (resolved?.typeName === "refObject") return {
refName,
properties: resolved.properties
};
return;
}
if (isNestedObjectLiteralType(inner)) return {
refName,
properties: inner.properties
};
if (isRefAliasType(inner)) {
inner = inner.type;
continue;
}
return;
}
}
}
};
//#endregion
//#region src/app/module.ts
function toSpecGeneratorOptionsInput(options) {
const { data } = options;
if (!data) return {};
return {
name: data.name,
version: data.version,
description: data.description,
license: data.license,
servers: data.servers,
securityDefinitions: data.securityDefinitions,
consumes: data.consumes,
produces: data.produces,
collectionFormat: data.collectionFormat,
specificationExtra: data.extra
};
}
async function generateSwagger(options) {
const { metadata } = options;
if (!isMetadata(metadata)) throw new SwaggerError({
message: "Expected `options.metadata` to be a pre-built Metadata object ({ controllers, referenceTypes }). Run `generateMetadata` from `@trapi/metadata` first, or supply your own Metadata-shaped value.",
code: SwaggerErrorCode.METADATA_INVALID
});
const specGeneratorOptionsInput = toSpecGeneratorOptionsInput(options);
switch (options.version) {
case Version.V3:
case Version.V3_1:
case Version.V3_2: return await new V3Generator(metadata, specGeneratorOptionsInput, options.version).build();
default: return await new V2Generator(metadata, specGeneratorOptionsInput).build();
}
}
//#endregion
//#region src/app/save.ts
const EXTENSION_PATTERN = /\.(json|ya?ml)$/i;
function resolveFileName(name, format) {
return `${(name ?? "swagger").replace(EXTENSION_PATTERN, "")}.${format}`;
}
function serialise(spec, format) {
if (format === DocumentFormat.YAML) return YAML.stringify(spec, 1e3);
return JSON.stringify(spec, null, 4);
}
async function saveSwagger(spec, options = {}) {
const format = options.format ?? DocumentFormat.JSON;
let cwd = process.cwd();
if (options.cwd) cwd = path.isAbsolute(options.cwd) ? options.cwd : path.join(process.cwd(), options.cwd);
const name = resolveFileName(options.name, format);
const filePath = path.join(cwd, name);
await fs.promises.mkdir(cwd, { recursive: true });
const content = serialise(spec, format);
await fs.promises.writeFile(filePath, content, { encoding: "utf-8" });
return {
path: filePath,
name,
content
};
}
//#endregion
export { AbstractSpecGenerator, DataFormatName, DataTypeName, DocumentFormat, ParameterSourceV2, ParameterSourceV3, SecurityType, SwaggerError, SwaggerErrorCode, TransferProtocol, V2Generator, V3Generator, Version, buildSpecGeneratorOptions, generateSwagger, hasOwnProperty, joinPaths, normalizePathParameters, removeDuplicateSlashes, removeFinalCharacter, saveSwagger, transformValueTo };
//# sourceMappingURL=index.mjs.map