@trapi/swagger
Version:
Generate Swagger files from a decorator APIs.
1 lines • 109 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","names":["uniqueOperationId"],"sources":["../src/core/config/utils.ts","../src/core/constants.ts","../src/core/error/codes.ts","../src/core/error/module.ts","../src/core/schema/v2/constants.ts","../src/core/schema/v3/constants.ts","../src/core/schema/constants.ts","../src/core/utils/character.ts","../src/core/utils/path.ts","../src/core/utils/object.ts","../src/core/utils/value.ts","../src/adapters/generator/abstract.ts","../src/adapters/generator/v2/module.ts","../src/adapters/generator/v3/module.ts","../src/app/module.ts","../src/app/save.ts"],"sourcesContent":["/*\n * Copyright (c) 2023.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport type { ServerOption, SpecGeneratorOptions, SpecGeneratorOptionsInput } from './types';\n\nexport function buildSpecGeneratorOptions(input: SpecGeneratorOptionsInput) : SpecGeneratorOptions {\n const servers : ServerOption[] = [];\n if (input.servers) {\n if (Array.isArray(input.servers)) {\n for (const server of input.servers) {\n if (typeof server === 'string') {\n servers.push({ url: server });\n } else {\n servers.push(server);\n }\n }\n } else if (typeof input.servers === 'string') {\n servers.push({ url: input.servers });\n } else {\n servers.push(input.servers);\n }\n }\n\n return {\n ...input,\n servers,\n };\n}\n","/*\n * Copyright (c) 2023.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nexport const Version = {\n V2: 'v2',\n V3: 'v3',\n V3_1: 'v3.1',\n V3_2: 'v3.2',\n} as const;\nexport type Version = typeof Version[keyof typeof Version];\n\nexport const DocumentFormat = {\n YAML: 'yaml',\n JSON: 'json',\n} as const;\nexport type DocumentFormat = typeof DocumentFormat[keyof typeof DocumentFormat];\n\nexport const SecurityType = {\n API_KEY: 'apiKey',\n BASIC: 'basic', // v2 only\n HTTP: 'http',\n OAUTH2: 'oauth2',\n} as const;\nexport type SecurityType = typeof SecurityType[keyof typeof SecurityType];\n","/*\n * Copyright (c) 2025.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nexport const SwaggerErrorCode = {\n SPEC_NOT_BUILT: 'SWAGGER_SPEC_NOT_BUILT',\n ENUM_UNSUPPORTED_TYPE: 'SWAGGER_ENUM_UNSUPPORTED_TYPE',\n BODY_PARAMETER_DUPLICATE: 'SWAGGER_BODY_PARAMETER_DUPLICATE',\n BODY_FORM_CONFLICT: 'SWAGGER_BODY_FORM_CONFLICT',\n PARAMETER_SOURCE_UNSUPPORTED: 'SWAGGER_PARAMETER_SOURCE_UNSUPPORTED',\n METADATA_INVALID: 'SWAGGER_METADATA_INVALID',\n} as const;\n","/*\n * Copyright (c) 2025.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport { BaseError } from '@ebec/core';\n\nexport class SwaggerError extends BaseError {\n\n}\n","/*\n * Copyright (c) 2023.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nexport const ParameterSourceV2 = {\n BODY: 'body',\n FORM_DATA: 'formData',\n HEADER: 'header',\n PATH: 'path',\n QUERY: 'query',\n} as const;\nexport type ParameterSourceV2 = typeof ParameterSourceV2[keyof typeof ParameterSourceV2];\n","/*\n * Copyright (c) 2023.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nexport const ParameterSourceV3 = {\n COOKIE: 'cookie',\n HEADER: 'header',\n PATH: 'path',\n QUERY: 'query',\n} as const;\nexport type ParameterSourceV3 = typeof ParameterSourceV3[keyof typeof ParameterSourceV3];\n","/*\n * Copyright (c) 2023.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nexport const TransferProtocol = {\n HTTP: 'http',\n HTTPS: 'https',\n WS: 'ws',\n WSS: 'wss',\n} as const;\nexport type TransferProtocol = typeof TransferProtocol[keyof typeof TransferProtocol];\n\nexport const DataFormatName = {\n INT_32: 'int32',\n INT_64: 'int64',\n FLOAT: 'float',\n DOUBLE: 'double',\n BYTE: 'byte',\n BINARY: 'binary',\n DATE: 'date',\n DATE_TIME: 'date-time',\n PASSWORD: 'password',\n} as const;\nexport type DataFormatName = typeof DataFormatName[keyof typeof DataFormatName];\n\nexport const DataTypeName = {\n VOID: 'void',\n INTEGER: 'integer',\n NUMBER: 'number',\n BOOLEAN: 'boolean',\n STRING: 'string',\n ARRAY: 'array',\n OBJECT: 'object',\n FILE: 'file',\n} as const;\nexport type DataTypeName = typeof DataTypeName[keyof typeof DataTypeName];\n","/*\n * Copyright (c) 2021-2022.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nexport function removeDuplicateSlashes(str: string) : string {\n // URL-safe: a `:/` boundary (e.g. `http://`) is not collapsed.\n return str.replace(/([^:]\\/)\\/+/g, '$1');\n}\n\nexport function removeFinalCharacter(\n str: string,\n character: string,\n) {\n while (str.charAt(str.length - 1) === character && str.length > 0) {\n str = str.slice(0, -1);\n }\n\n return str;\n}\n","/*\n * Copyright (c) 2021-2022.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nexport function normalizePathParameters(str: string) : string {\n // <:id> -> {id}\n // todo: maybe escaping / is necessary\n str = str.replace(/<:([^/]+)>/g, '{$1}');\n\n // :id -> {id}\n str = str.replace(/:([^/]+)/g, '{$1}');\n\n // <id> -> {id}\n str = str.replace(/<([^/]+)>/g, '{$1}');\n\n return str;\n}\n\n// OpenAPI 3.x §4.8.8 requires every `paths` key to start with `/`. Joining a\n// controller path with a method path naively (template literal + slash\n// stripping) collapses the root combination — `''` + `'/'`, `'/'` + `''`,\n// `'/'` + `'/'` — to an empty string, which strict validators reject.\nexport function joinPaths(...segments: string[]): string {\n let result = segments.join('/').replace(/\\/{2,}/g, '/');\n if (!result.startsWith('/')) {\n result = `/${result}`;\n }\n if (result.length > 1 && result.endsWith('/')) {\n result = result.slice(0, -1);\n }\n return result;\n}\n","/*\n * Copyright (c) 2021-2022.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nexport function hasOwnProperty<X extends object, Y extends PropertyKey>(obj: X, prop: Y): obj is X & Record<Y, unknown> {\n return Object.prototype.hasOwnProperty.call(obj, prop);\n}\n","/*\n * Copyright (c) 2023.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nexport function transformValueTo(\n type: 'string' | 'number' | 'integer' | 'boolean' | 'bigint',\n value: unknown,\n): string | number | boolean | null {\n if (value === null) {\n return null;\n }\n\n switch (type) {\n case 'integer':\n case 'number':\n return Number(value);\n case 'boolean':\n return !!value;\n case 'string':\n default:\n return String(value);\n }\n}\n","/*\n * Copyright (c) 2021-2023.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport type {\n ArrayType,\n BaseType,\n EnumType,\n Extension,\n IntersectionType,\n Metadata,\n NestedObjectLiteralType,\n Parameter,\n ParameterSource,\n PrimitiveType,\n RefAliasType,\n RefEnumType,\n RefObjectType,\n ReferenceType,\n ResolverProperty,\n TupleType,\n UnionType,\n Validators, \n VariableType, \n} from '@trapi/core';\nimport {\n TypeName,\n isArrayType,\n isEnumType,\n isIntersectionType,\n isNestedObjectLiteralType,\n isNeverType,\n isPrimitiveType,\n isReferenceType,\n isTupleType,\n isUndefinedType,\n isUnionType,\n isVoidType,\n} from '@trapi/core';\n\nimport { isObject } from 'smob';\nimport { buildSpecGeneratorOptions } from '../../core/config';\nimport { SwaggerError, SwaggerErrorCode } from '../../core/error';\nimport type { SpecGeneratorOptions, SpecGeneratorOptionsInput } from '../../core/config';\nimport { DataFormatName, DataTypeName } from '../../core/schema';\nimport type { ValidatorOpenApiMeta } from '../../core/types';\nimport { transformValueTo } from '../../core/utils';\n\nimport type {\n BaseSchema,\n Info,\n SchemaV2,\n SchemaV3,\n SpecV2,\n SpecV3,\n} from '../../core/schema';\n\nexport abstract class AbstractSpecGenerator<Spec extends SpecV2 | SpecV3, Schema extends SchemaV3 | SchemaV2> {\n protected spec: Spec | undefined;\n\n protected readonly metadata: Metadata;\n\n protected readonly config: SpecGeneratorOptions;\n\n constructor(metadata: Metadata, config: SpecGeneratorOptionsInput) {\n this.metadata = metadata;\n this.config = buildSpecGeneratorOptions(config);\n }\n\n public abstract build(): Promise<Spec>;\n\n protected buildInfo() {\n const info: Info = {\n title: this.config.name || 'Documentation',\n version: this.config.version || '1.0.0',\n };\n\n if (this.config.description) {\n info.description = this.config.description;\n }\n\n if (this.config.license) {\n info.license = { name: this.config.license };\n }\n\n return info;\n }\n\n protected buildTags() {\n // Tag entries are emitted only for controllers that declare extensions.\n // When multiple controllers share a tag name, their extensions merge into\n // the same Tag entry; on key conflict, the last controller processed wins\n // (silent — strict-mode validation is a future addition).\n // Hidden controllers are skipped. If a controller declares extensions but\n // no tags, the controller name is used as a synthetic tag name so the\n // extensions still surface in the spec.\n const tagMap = new Map<string, { name: string } & Record<string, unknown>>();\n\n for (const controller of this.metadata.controllers) {\n if (controller.hidden) {\n continue;\n }\n\n const extensions = controller.extensions ?? [];\n if (extensions.length === 0) {\n continue;\n }\n\n const tagNames = controller.tags.length > 0 ? controller.tags : [controller.name];\n const extensionFields = this.transformExtensions(extensions);\n\n for (const tagName of tagNames) {\n let entry = tagMap.get(tagName);\n if (!entry) {\n entry = { name: tagName };\n tagMap.set(tagName, entry);\n }\n\n Object.assign(entry, extensionFields);\n }\n }\n\n return Array.from(tagMap.values());\n }\n\n protected getSchemaForType(type: BaseType): Schema | BaseSchema<Schema> {\n if (isVoidType(type) || isUndefinedType(type) || isNeverType(type)) {\n return {} as Schema;\n } if (isReferenceType(type)) {\n return this.getSchemaForReferenceType(type);\n } if (isPrimitiveType(type)) {\n return this.getSchemaForPrimitiveType(type);\n } if (isArrayType(type)) {\n return this.getSchemaForArrayType(type);\n } if (isTupleType(type)) {\n return this.getSchemaForTupleType(type);\n } if (isEnumType(type)) {\n return this.getSchemaForEnumType(type);\n } if (isUnionType(type)) {\n return this.getSchemaForUnionType(type);\n } if (isIntersectionType(type)) {\n return this.getSchemaForIntersectionType(type);\n } if (isNestedObjectLiteralType(type)) {\n return this.getSchemaForObjectLiteralType(type);\n }\n\n return {} as Schema;\n }\n\n protected abstract getSchemaForIntersectionType(type: IntersectionType): Schema;\n\n protected getSchemaForEnumType(enumType: EnumType): Schema {\n const nullable = enumType.members.includes(null);\n const nonNullMembers = enumType.members.filter(\n (m): m is string | number | boolean => m !== null,\n );\n const type = this.decideEnumType(nonNullMembers);\n\n const schema = {\n type,\n enum: enumType.members.map((member) => transformValueTo(type, member)),\n } as Schema;\n\n this.applyNullable(schema, nullable);\n\n return schema;\n }\n\n protected abstract applyNullable(schema: Schema, nullable: boolean): void;\n\n private getSchemaForPrimitiveType(type: PrimitiveType): BaseSchema<Schema> {\n const PrimitiveSwaggerTypeMap: Partial<Record<TypeName, BaseSchema<Schema>>> = {\n [TypeName.ANY]: { additionalProperties: true },\n [TypeName.BINARY]: { type: DataTypeName.STRING, format: DataFormatName.BINARY },\n [TypeName.BOOLEAN]: { type: DataTypeName.BOOLEAN },\n [TypeName.BUFFER]: { type: DataTypeName.STRING, format: DataFormatName.BYTE },\n [TypeName.BYTE]: { type: DataTypeName.STRING, format: DataFormatName.BYTE },\n [TypeName.DATE]: { type: DataTypeName.STRING, format: DataFormatName.DATE },\n [TypeName.DATETIME]: { type: DataTypeName.STRING, format: DataFormatName.DATE_TIME },\n [TypeName.DOUBLE]: { type: DataTypeName.NUMBER, format: DataFormatName.DOUBLE },\n [TypeName.FILE]: { type: DataTypeName.STRING, format: DataFormatName.BINARY },\n [TypeName.FLOAT]: { type: DataTypeName.NUMBER, format: DataFormatName.FLOAT },\n [TypeName.BIGINT]: { type: DataTypeName.INTEGER },\n [TypeName.INTEGER]: { type: DataTypeName.INTEGER, format: DataFormatName.INT_32 },\n [TypeName.LONG]: { type: DataTypeName.INTEGER, format: DataFormatName.INT_64 },\n [TypeName.OBJECT]: {\n type: DataTypeName.OBJECT,\n additionalProperties: true,\n },\n [TypeName.STRING]: { type: DataTypeName.STRING },\n [TypeName.UNDEFINED]: {},\n };\n\n return PrimitiveSwaggerTypeMap[type.typeName] || { type: DataTypeName.OBJECT };\n }\n\n private getSchemaForArrayType(arrayType: ArrayType): BaseSchema<Schema> {\n return {\n type: DataTypeName.ARRAY,\n items: this.getSchemaForType(arrayType.elementType),\n };\n }\n\n private getSchemaForTupleType(tupleType: TupleType): BaseSchema<Schema> {\n if (tupleType.elements.length === 0) {\n return {\n type: DataTypeName.ARRAY,\n items: {},\n };\n }\n\n const elementSchemas = tupleType.elements.map((el) => this.getSchemaForType(el.type));\n\n if (elementSchemas.length === 1) {\n return {\n type: DataTypeName.ARRAY,\n items: elementSchemas[0],\n };\n }\n\n // Multiple elements → array with anyOf items\n return {\n type: DataTypeName.ARRAY,\n items: { anyOf: elementSchemas },\n };\n }\n\n public getSchemaForObjectLiteralType(objectLiteral: NestedObjectLiteralType): BaseSchema<Schema> {\n const properties = this.buildProperties(objectLiteral.properties);\n\n const additionalProperties = objectLiteral.additionalProperties &&\n this.getSchemaForType(objectLiteral.additionalProperties);\n\n const required = objectLiteral.properties\n .filter((prop: ResolverProperty) => prop.required && !this.isUndefinedProperty(prop))\n .map((prop: ResolverProperty) => prop.name);\n\n // An empty list required: [] is not valid.\n // If all properties are optional, do not specify the required keyword.\n return {\n properties,\n ...(additionalProperties && { additionalProperties }),\n ...(required && required.length && { required }),\n type: DataTypeName.OBJECT,\n } as BaseSchema<Schema>;\n }\n\n protected getSchemaForReferenceType(referenceType: ReferenceType): Schema {\n return { $ref: `${this.getRefPrefix()}${referenceType.refName}` } as Schema;\n }\n\n protected abstract getRefPrefix(): string;\n\n protected abstract getSchemaForUnionType(type: UnionType) : Schema;\n\n // ----------------------------------------------------------------\n\n protected buildSchemaForRefAlias(referenceType: RefAliasType): Schema {\n const swaggerType = this.getSchemaForType(referenceType.type);\n const format = referenceType.format as DataFormatName;\n\n return {\n ...(swaggerType as Schema),\n default: referenceType.default ?? swaggerType.default,\n example: referenceType.example ?? swaggerType.example,\n format: format ?? swaggerType.format,\n description: referenceType.description ?? swaggerType.description,\n ...this.transformValidators(referenceType.validators),\n };\n }\n\n protected buildSchemaForRefEnum(referenceType: RefEnumType): Schema {\n const output = {\n ...this.getSchemaForEnumType({\n typeName: TypeName.ENUM,\n members: referenceType.members,\n }),\n description: referenceType.description,\n } as Schema;\n\n if (\n typeof referenceType.memberNames !== 'undefined' &&\n referenceType.members.length === referenceType.memberNames.length\n ) {\n (output as any)['x-enum-varnames'] = referenceType.memberNames;\n }\n\n return output;\n }\n\n protected buildSchemasForReferenceTypes(extendFn?: (output: Schema, input: ReferenceType) => void) : Record<string, Schema> {\n const output: Record<string, Schema> = {};\n\n for (const referenceType of Object.values(this.metadata.referenceTypes)) {\n switch (referenceType.typeName) {\n case TypeName.REF_ALIAS: {\n output[referenceType.refName] = this.buildSchemaForRefAlias(referenceType);\n break;\n }\n case TypeName.REF_ENUM: {\n output[referenceType.refName] = this.buildSchemaForRefEnum(referenceType);\n break;\n }\n case TypeName.REF_OBJECT: {\n output[referenceType.refName] = this.buildSchemaForRefObject(referenceType);\n break;\n }\n }\n\n if (typeof extendFn === 'function') {\n extendFn(output[referenceType.refName]!, referenceType);\n }\n }\n\n return output;\n }\n\n // ----------------------------------------------------------------\n\n protected isUndefinedProperty(input: ResolverProperty) {\n return isUndefinedType(input.type) ||\n (isUnionType(input.type) && input.type.members.some((el) => isUndefinedType(el)));\n }\n\n protected buildProperties(properties: ResolverProperty[]): Record<string, Schema> {\n const output: Record<string, Schema> = {};\n\n properties.forEach((property) => {\n const swaggerType = this.getSchemaForType(property.type) as Schema;\n\n if (swaggerType.$ref && this.shouldStripRefSiblings()) {\n output[property.name] = { $ref: swaggerType.$ref } as Schema;\n return;\n }\n\n swaggerType.description = property.description;\n swaggerType.example = property.example;\n swaggerType.format = property.format as DataFormatName || swaggerType.format;\n this.assignPropertyDefaults(swaggerType, property);\n\n if (property.deprecated) {\n this.markPropertyDeprecated(swaggerType);\n }\n\n const extensions = this.transformExtensions(property.extensions);\n const validators = this.transformValidators(property.validators);\n output[property.name] = {\n ...swaggerType,\n ...validators,\n ...extensions,\n };\n });\n\n return output;\n }\n\n protected abstract markPropertyDeprecated(schema: Schema): void;\n\n protected shouldStripRefSiblings(): boolean {\n // V2 (Swagger 2.0) and V3 (3.0): $ref must be the only key.\n // V3 (3.1+): $ref siblings are allowed. Override to return false.\n return true;\n }\n\n protected assignPropertyDefaults(_schema: Schema, _property: ResolverProperty): void {\n // No-op by default. V3 overrides to set schema.default = property.default.\n }\n\n protected buildSchemaForRefObject(referenceType: RefObjectType): Schema {\n const required = referenceType.properties\n .filter((p) => p.required && !this.isUndefinedProperty(p))\n .map((p) => p.name);\n\n const output = {\n description: referenceType.description,\n properties: this.buildProperties(referenceType.properties),\n required: required && required.length > 0 ? Array.from(new Set(required)) : undefined,\n type: DataTypeName.OBJECT,\n } as unknown as Schema;\n\n if (referenceType.additionalProperties) {\n (output as any).additionalProperties = this.resolveAdditionalProperties(referenceType.additionalProperties);\n }\n\n if (referenceType.example !== undefined) {\n output.example = referenceType.example;\n }\n\n return output;\n }\n\n protected abstract resolveAdditionalProperties(type: BaseType): Schema | boolean;\n\n protected determineTypesUsedInEnum(anEnum: Array<string | number | boolean | null>) : VariableType[] {\n const set = new Set<VariableType>();\n for (const element of anEnum) {\n if (element === null) {\n continue;\n }\n\n set.add(typeof element);\n }\n\n return Array.from(set);\n }\n\n protected decideEnumType(\n input: Array<string | number | boolean>,\n ): 'string' | 'number' | 'boolean' {\n const types = this.determineTypesUsedInEnum(input);\n\n if (types.length === 1) {\n const value = types[0];\n if (\n value === 'string' ||\n value === 'number' ||\n value === 'boolean'\n ) {\n return value;\n }\n\n throw new SwaggerError({\n message: `Enum contains unsupported type '${types[0] || 'unknown'}'. Only string, number, and boolean values are allowed.`,\n code: SwaggerErrorCode.ENUM_UNSUPPORTED_TYPE,\n });\n }\n\n const unsupportedTypes = types.filter(\n (type) => type !== 'string' && type !== 'number' && type !== 'boolean',\n );\n if (unsupportedTypes.length > 0) {\n throw new SwaggerError({\n message: `Enum contains unsupported types: ${unsupportedTypes.join(', ')}. Only string, number, and boolean values are allowed.`,\n code: SwaggerErrorCode.ENUM_UNSUPPORTED_TYPE,\n });\n }\n\n return 'string';\n }\n\n protected getOperationId(name: string) {\n return name.charAt(0).toUpperCase() + name.substring(1);\n }\n\n protected groupParameters(items: Parameter[]) : Partial<Record<ParameterSource, Parameter[]>> {\n const output : Partial<Record<ParameterSource, Parameter[]>> = {};\n\n for (const item of items) {\n const bucket = output[item.in] ?? (output[item.in] = []);\n bucket.push(item);\n }\n\n return output;\n }\n\n protected transformExtensions(input?: Extension[]) : Record<string, any> {\n if (!input) {\n return {};\n }\n\n const output : Record<string, any> = {};\n for (const extension of input) {\n const key = extension.key.startsWith('x-') ? extension.key : `x-${extension.key}`;\n output[key] = extension.value;\n }\n\n return output;\n }\n\n protected transformValidators(input?: Validators) : Record<string, any> {\n if (!isObject(input)) {\n return {};\n }\n\n const output : Record<string, any> = {};\n for (const [name, validator] of Object.entries(input)) {\n const mapping = validator.meta?.openApi ?? DEFAULT_VALIDATOR_OPENAPI_MAPPINGS[name];\n if (!mapping || mapping.kind === 'ignore') {\n continue;\n }\n\n if (mapping.kind === 'keyword') {\n output[mapping.key] = validator.value;\n } else if (mapping.kind === 'format') {\n output.format = mapping.format;\n }\n }\n\n return output;\n }\n}\n\n// Default OpenAPI mappings for canonical validator names. Validator names that\n// do not appear here and that carry no `meta.openApi` hint are dropped — this\n// keeps third-party / custom validators out of the spec unless their handler\n// declares how to emit them.\nconst DEFAULT_VALIDATOR_OPENAPI_MAPPINGS: Record<string, ValidatorOpenApiMeta> = {\n maxLength: { kind: 'keyword', key: 'maxLength' },\n minLength: { kind: 'keyword', key: 'minLength' },\n maximum: { kind: 'keyword', key: 'maximum' },\n minimum: { kind: 'keyword', key: 'minimum' },\n pattern: { kind: 'keyword', key: 'pattern' },\n maxItems: { kind: 'keyword', key: 'maxItems' },\n minItems: { kind: 'keyword', key: 'minItems' },\n uniqueItems: { kind: 'keyword', key: 'uniqueItems' },\n};\n","/*\n * Copyright (c) 2021-2023.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport type {\n BaseType,\n EnumType,\n IntersectionType,\n Method,\n Parameter,\n RefObjectType,\n Response,\n Type,\n UnionType,\n} from '@trapi/core';\nimport {\n ParameterSource,\n TypeName,\n isAnyType,\n isBinaryType,\n isEnumType,\n isNeverType,\n isRefEnumType,\n isRefObjectType,\n isUndefinedType,\n isVoidType,\n} from '@trapi/core';\nimport { URL } from 'node:url';\nimport { merge } from 'smob';\n\nimport type {\n BaseSchema,\n OperationV2,\n ParameterV2,\n Path,\n ResponseV2,\n SchemaV2,\n SecurityV2,\n SpecV2,\n} from '../../../core/schema';\nimport { DataTypeName, ParameterSourceV2 } from '../../../core/schema';\nimport type { SecurityDefinitions } from '../../../core/types';\nimport { SwaggerError, SwaggerErrorCode } from '../../../core/error';\nimport { joinPaths, normalizePathParameters } from '../../../core/utils';\nimport { AbstractSpecGenerator } from '../abstract';\n\nfunction uniqueOperationId(base: string, used: Set<string>): string {\n if (!used.has(base)) {\n used.add(base);\n return base;\n }\n let counter = 2;\n while (used.has(`${base}_${counter}`)) {\n counter += 1;\n }\n const candidate = `${base}_${counter}`;\n used.add(candidate);\n return candidate;\n}\n\nexport class V2Generator extends AbstractSpecGenerator<SpecV2, SchemaV2> {\n async build() : Promise<SpecV2> {\n if (typeof this.spec !== 'undefined') {\n return this.spec;\n }\n\n let spec: SpecV2 = {\n definitions: this.buildSchemasForReferenceTypes(),\n info: this.buildInfo(),\n paths: this.buildPaths(),\n swagger: '2.0',\n };\n\n spec.securityDefinitions = this.config.securityDefinitions ?\n V2Generator.translateSecurityDefinitions(this.config.securityDefinitions) :\n {};\n\n if (this.config.consumes) {\n spec.consumes = this.config.consumes;\n }\n\n if (this.config.produces) {\n spec.produces = this.config.produces;\n }\n\n const firstServer = this.config.servers?.[0];\n if (firstServer) {\n const url = new URL(firstServer.url, 'http://localhost:3000/');\n\n spec.host = url.host;\n if (url.pathname) {\n spec.basePath = url.pathname;\n }\n }\n\n const tags = this.buildTags();\n if (tags.length > 0) {\n spec.tags = tags;\n }\n\n if (this.config.specificationExtra) {\n spec = merge(spec, this.config.specificationExtra);\n }\n\n this.spec = spec;\n\n return spec;\n }\n\n private static translateSecurityDefinitions(securityDefinitions: SecurityDefinitions) : Record<string, SecurityV2> {\n const definitions : Record<string, SecurityV2> = {};\n\n for (const [key, securityDefinition] of Object.entries(securityDefinitions)) {\n switch (securityDefinition.type) {\n case 'http':\n if (securityDefinition.scheme === 'basic') {\n definitions[key] = { type: 'basic' };\n }\n break;\n case 'apiKey':\n definitions[key] = securityDefinition;\n break;\n case 'oauth2':\n if (securityDefinition.flows.implicit) {\n definitions[`${key}Implicit`] = {\n type: 'oauth2',\n flow: 'implicit',\n authorizationUrl: securityDefinition.flows.implicit.authorizationUrl,\n scopes: securityDefinition.flows.implicit.scopes,\n };\n }\n\n if (securityDefinition.flows.password) {\n definitions[`${key}Password`] = {\n type: 'oauth2',\n flow: 'password',\n tokenUrl: securityDefinition.flows.password.tokenUrl,\n scopes: securityDefinition.flows.password.scopes,\n };\n }\n\n if (securityDefinition.flows.authorizationCode) {\n definitions[`${key}AccessCode`] = {\n type: 'oauth2',\n flow: 'accessCode',\n tokenUrl: securityDefinition.flows.authorizationCode.tokenUrl,\n authorizationUrl: securityDefinition.flows.authorizationCode.authorizationUrl,\n scopes: securityDefinition.flows.authorizationCode.scopes,\n };\n }\n\n if (securityDefinition.flows.clientCredentials) {\n definitions[`${key}Application`] = {\n type: 'oauth2',\n flow: 'application',\n tokenUrl: securityDefinition.flows.clientCredentials.tokenUrl,\n scopes: securityDefinition.flows.clientCredentials.scopes,\n };\n }\n\n break;\n }\n }\n\n return definitions;\n }\n\n protected resolveAdditionalProperties(_type: BaseType): SchemaV2 | boolean {\n return true;\n }\n\n protected markPropertyDeprecated(schema: SchemaV2): void {\n schema['x-deprecated'] = true;\n }\n\n\n /*\n Path & Parameter ( + utils)\n */\n\n private buildPaths() {\n const output: Record<string, Path<OperationV2, ResponseV2>> = {};\n const usedOperationIds = new Set<string>();\n\n const unique = <T extends unknown[]>(input: T) : T => [...new Set(input)] as T;\n\n this.metadata.controllers.forEach((controller) => {\n if (controller.hidden) {\n return;\n }\n\n const controllerPaths = controller.paths.length === 0 ? [''] : controller.paths;\n\n controller.methods.forEach((method) => {\n if (method.hidden) {\n return;\n }\n\n method.consumes = unique([...controller.consumes, ...method.consumes]);\n method.produces = unique([...controller.produces, ...method.produces]);\n method.tags = unique([...controller.tags, ...method.tags]);\n // Inherit controller security only when the method declared none of its own.\n // `[]` is truthy, so a plain `||` short-circuits and never cascades.\n if (!method.security?.length) {\n method.security = controller.security;\n }\n // OpenAPI has no controller-level `deprecated` — cascade\n // controller deprecation to every emitted operation.\n method.deprecated = method.deprecated || controller.deprecated;\n // todo: unique for objects\n method.responses = unique([...controller.responses, ...method.responses]);\n\n for (const controllerPath of controllerPaths) {\n const fullPath = normalizePathParameters(joinPaths(controllerPath, method.path));\n\n const pathItem = output[fullPath] ?? (output[fullPath] = {});\n pathItem[method.method] = this.buildMethod(method, fullPath, usedOperationIds);\n }\n });\n });\n\n return output;\n }\n\n private buildMethod(\n method: Method,\n emittedPath: string,\n usedOperationIds: Set<string>,\n ) : OperationV2 {\n const output = this.buildOperation(method);\n output.consumes = this.buildMethodConsumes(method);\n\n // Prefer an explicit operationId from metadata (matches V3 behaviour),\n // then disambiguate across multi-mount controllers (the same method\n // emitted at multiple paths must not share an operationId).\n const baseOperationId = method.operationId || output.operationId!;\n output.operationId = uniqueOperationId(baseOperationId, usedOperationIds);\n\n output.description = method.description;\n if (method.summary) {\n output.summary = method.summary;\n }\n\n if (method.deprecated) { output.deprecated = method.deprecated; }\n if (method.tags.length) { output.tags = method.tags; }\n if (method.security?.length) {\n output.security = method.security;\n }\n\n const parameters = this.groupParameters(method.parameters);\n\n // Filter path-bound params not present in this specific URL template.\n const pathParams = (parameters[ParameterSource.PATH] || [])\n .filter((p) => emittedPath.includes(`{${p.name}}`));\n\n output.parameters = [\n ...pathParams,\n ...(parameters[ParameterSource.QUERY_PROP] || []),\n ...(parameters[ParameterSource.HEADER] || []),\n ...(parameters[ParameterSource.FORM_DATA] || []),\n ].map((p) => this.buildParameter(p));\n\n // ignore ParameterSource.QUERY!\n\n // ------------------------------------------------------\n\n const bodyParameters = (parameters[ParameterSource.BODY] || []);\n if (bodyParameters.length > 1) {\n throw new SwaggerError({\n message: `Only one body parameter allowed per method, but ${bodyParameters.length} found in '${method.name}'.`,\n code: SwaggerErrorCode.BODY_PARAMETER_DUPLICATE,\n });\n }\n\n const bodyParameter = bodyParameters[0] ?\n this.buildParameter(bodyParameters[0]) :\n undefined;\n\n const bodyPropParams = parameters[ParameterSource.BODY_PROP] || [];\n if (bodyPropParams.length > 0) {\n const schema : BaseSchema<SchemaV2> = {\n type: DataTypeName.OBJECT,\n title: 'Body',\n properties: {},\n };\n\n const required : string[] = [];\n\n for (const bodyPropParam of bodyPropParams) {\n const bodyProp = this.getSchemaForType(bodyPropParam.type);\n bodyProp.default = bodyPropParam.default;\n bodyProp.description = bodyPropParam.description;\n bodyProp.example = bodyPropParam.examples;\n\n if (bodyProp.required) {\n required.push(bodyPropParam.name);\n }\n\n schema.properties![bodyPropParam.name] = bodyProp;\n }\n\n if (\n bodyParameter &&\n bodyParameter.in === ParameterSourceV2.BODY\n ) {\n if (bodyParameter.schema.type === DataTypeName.OBJECT) {\n bodyParameter.schema.properties = {\n ...(bodyParameter.schema.properties || {}),\n ...schema.properties,\n };\n\n bodyParameter.schema.required = [\n ...(bodyParameter.schema.required || []),\n ...required,\n ];\n } else {\n bodyParameter.schema = schema;\n }\n\n output.parameters.push(bodyParameter);\n } else {\n const parameter : ParameterV2 = {\n in: ParameterSourceV2.BODY,\n name: 'body',\n schema,\n };\n\n if (required.length) {\n parameter.schema.required = required;\n }\n\n output.parameters.push(parameter);\n }\n } else if (bodyParameter) {\n output.parameters.push(bodyParameter);\n }\n\n Object.assign(output, this.transformExtensions(method.extensions));\n\n return output;\n }\n\n private transformParameterSource(\n source: `${ParameterSource}`,\n ) : `${ParameterSourceV2}` | undefined {\n if (\n source === ParameterSource.BODY\n ) {\n return ParameterSourceV2.BODY;\n }\n\n if (source === ParameterSource.FORM_DATA) {\n return ParameterSourceV2.FORM_DATA;\n }\n\n if (source === ParameterSource.HEADER) {\n return ParameterSourceV2.HEADER;\n }\n\n if (source === ParameterSource.PATH) {\n return ParameterSourceV2.PATH;\n }\n\n if (source === ParameterSource.QUERY || source === ParameterSource.QUERY_PROP) {\n return ParameterSourceV2.QUERY;\n }\n\n return undefined;\n }\n\n protected buildParameter(input: Parameter): ParameterV2 {\n const sourceIn = this.transformParameterSource(input.in);\n if (!sourceIn) {\n throw new SwaggerError({\n message: `The parameter source '${input.in}' for parameter '${input.name}' is not supported in OpenAPI 2.0.`,\n code: SwaggerErrorCode.PARAMETER_SOURCE_UNSUPPORTED,\n });\n }\n\n const parameter = {\n description: input.description,\n in: sourceIn,\n name: input.name,\n required: input.required,\n } as ParameterV2;\n\n Object.assign(parameter, this.transformExtensions(input.extensions));\n\n if (\n input.in !== ParameterSource.BODY &&\n isRefEnumType(input.type)\n ) {\n input.type = {\n typeName: TypeName.ENUM,\n members: input.type.members,\n };\n }\n\n // Swagger 2.0: formData file parameters use type: 'file' directly\n if (\n parameter.in === ParameterSourceV2.FORM_DATA &&\n input.type.typeName === TypeName.FILE\n ) {\n parameter.type = 'file' as `${DataTypeName}`;\n Object.assign(parameter, this.transformValidators(input.validators));\n return parameter;\n }\n\n const parameterType = this.getSchemaForType(input.type);\n if (\n parameter.in !== ParameterSourceV2.BODY &&\n parameterType.format\n ) {\n parameter.format = parameterType.format;\n }\n\n // collectionFormat, might be valid for all parameters (if value != multi)\n if (\n (parameter.in === ParameterSourceV2.FORM_DATA || parameter.in === ParameterSourceV2.QUERY) &&\n (input.type.typeName === TypeName.ARRAY || parameterType.type === DataTypeName.ARRAY)\n ) {\n parameter.collectionFormat = input.collectionFormat || this.config.collectionFormat || 'multi';\n }\n\n if (parameter.in === ParameterSourceV2.BODY) {\n if ((input.type.typeName === TypeName.ARRAY || parameterType.type === DataTypeName.ARRAY)) {\n parameter.schema = {\n items: parameterType.items,\n type: DataTypeName.ARRAY,\n };\n } else if (input.type.typeName === TypeName.ANY) {\n parameter.schema = { type: DataTypeName.OBJECT };\n } else {\n parameter.schema = parameterType;\n }\n\n parameter.schema = {\n ...parameter.schema,\n ...this.transformValidators(input.validators),\n };\n\n return parameter;\n }\n\n // todo: this is eventually illegal\n Object.assign(parameter, this.transformValidators(input.validators));\n\n if (input.type.typeName === TypeName.ANY) {\n parameter.type = DataTypeName.STRING;\n } else if (parameterType.type && !Array.isArray(parameterType.type)) {\n parameter.type = parameterType.type;\n }\n\n if (parameterType.items) {\n parameter.items = parameterType.items;\n }\n if (parameterType.enum) {\n parameter.enum = parameterType.enum;\n }\n\n if (typeof input.default !== 'undefined') {\n parameter.default = input.default;\n }\n\n return parameter;\n }\n\n private buildMethodConsumes(method: Method) : string[] {\n if (\n method.consumes &&\n method.consumes.length > 0\n ) {\n return method.consumes;\n }\n\n if (this.hasFileParams(method)) {\n return ['multipart/form-data'];\n }\n\n if (this.hasFormParams(method)) {\n return ['application/x-www-form-urlencoded'];\n }\n\n if (this.supportsBodyParameters(method.method)) {\n return ['application/json'];\n }\n\n return [];\n }\n\n private hasFileParams(method: Method) {\n return method.parameters.some((p) => (p.in === ParameterSource.FORM_DATA && p.type.typeName === 'file'));\n }\n\n private hasFormParams(method: Method) {\n return method.parameters.some((p) => (p.in === ParameterSource.FORM_DATA));\n }\n\n private supportsBodyParameters(method: string) {\n return ['post', 'put', 'patch'].includes(method);\n }\n\n /*\n Swagger Type ( + utils)\n */\n\n protected applyNullable(schema: SchemaV2, nullable: boolean): void {\n schema['x-nullable'] = nullable;\n }\n\n protected getRefPrefix(): string {\n return '#/definitions/';\n }\n\n protected getSchemaForIntersectionType(type: IntersectionType) : SchemaV2 {\n // tslint:disable-next-line:no-shadowed-variable\n const properties = type.members.reduce((acc, type) => {\n if (isRefObjectType(type)) {\n const refType = this.metadata.referenceTypes[type.refName] as RefObjectType;\n\n const props = refType &&\n refType.properties &&\n refType.properties.reduce((pAcc, prop) => ({\n ...pAcc,\n [prop.name]: this.getSchemaForType(prop.type),\n }), {});\n return { ...acc, ...props };\n }\n return { ...acc };\n }, {});\n\n return { type: DataTypeName.OBJECT, properties };\n }\n\n\n protected getSchemaForUnionType(type: UnionType) : SchemaV2 {\n const members : Type[] = [];\n\n const enumTypeMember : EnumType = { typeName: TypeName.ENUM, members: [] };\n for (const member of type.members) {\n if (isEnumType(member)) {\n enumTypeMember.members.push(...member.members);\n }\n\n if (\n !isAnyType(member) &&\n !isUndefinedType(member) &&\n !isNeverType(member) &&\n !isEnumType(member)\n ) {\n members.push(member);\n }\n }\n\n if (\n members.length === 0 &&\n enumTypeMember.members.length > 0\n ) {\n return this.getSchemaForEnumType(enumTypeMember);\n }\n\n const isNullEnum = enumTypeMember.members.every((member) => member === null);\n if (members.length === 1) {\n const single = members[0]!;\n if (isNullEnum) {\n const memberType = this.getSchemaForType(single) as SchemaV2;\n if (memberType.$ref) {\n return memberType;\n }\n\n memberType['x-nullable'] = true;\n return memberType;\n }\n\n if (enumTypeMember.members.length === 0) {\n return this.getSchemaForType(single);\n }\n }\n\n return { type: DataTypeName.OBJECT, ...(isNullEnum ? { 'x-nullable': true } : {}) };\n }\n\n private buildOperation(method: Method) {\n const operation : OperationV2 = {\n operationId: this.getOperationId(method.name),\n consumes: method.consumes || [],\n produces: method.produces || [],\n responses: {},\n };\n\n const produces : string[] = [];\n\n method.responses.forEach((res: Response) => {\n operation.responses[res.status] = { description: res.description };\n\n if (\n res.schema &&\n !isVoidType(res.schema) &&\n !isNeverType(res.schema)\n ) {\n if (res.produces) {\n produces.push(...res.produces);\n } else if (isBinaryType(res.schema)) {\n produces.push('application/octet-stream');\n }\n\n operation.responses[res.status]!.schema = this.getSchemaForType(res.schema);\n }\n\n const example = res.examples?.[0];\n if (example?.value) {\n operation.responses[res.status]!.examples = { 'application/json': example.value };\n }\n });\n\n const consumes = operation.consumes!;\n if (consumes.length === 0) {\n const hasBody = method.parameters\n .some((parameter) => parameter.in === ParameterSource.BODY || parameter.in === ParameterSource.BODY_PROP);\n if (hasBody) {\n consumes.push('application/json');\n }\n\n const hasFormData = method.parameters\n .some((parameter) => parameter.in === ParameterSource.FORM_DATA);\n if (hasFormData) {\n consumes.push('multipart/form-data');\n }\n }\n\n if (\n operation.produces!.length === 0 &&\n produces.length > 0\n ) {\n operation.produces = [...new Set(produces)];\n }\n\n if (operation.produces!.length === 0) {\n operation.produces = ['application/json'];\n }\n\n return operation;\n }\n}\n","/*\n * Copyright (c) 2021-2023.\n * Author Peter Placzek (tada5hi)\n * For the full copyright and license information,\n * view the LICENSE file that was distributed with this source code.\n */\n\nimport type {\n BaseType,\n EnumType,\n IntersectionType,\n Metadata,\n Method,\n NestedObjectLiteralType,\n Parameter,\n RefAliasType,\n RefEnumType,\n RefObjectType,\n ResolverProperty,\n Response,\n Type, \n UnionType, \n} from '@trapi/core';\nimport {\n ParameterSource,\n TypeName,\n isAnyType,\n isEnumType,\n isIntersectionType,\n isNestedObjectLiteralType,\n isNeverType,\n isRefAliasType,\n isRefObjectType,\n isUndefinedType,\n isVoidType,\n} from '@trapi/core';\nimport { URL } from 'node:url';\nimport { merge } from 'smob';\nimport type {\n Example,\n HeaderV3,\n