UNPKG

openapi-metadata

Version:

Auto-Generate OpenAPI specifications from Typescript decorators

233 lines (218 loc) 7.37 kB
'use strict'; const extraModels = require('./shared/openapi-metadata.CegLGNUQ.cjs'); const metadata = require('./shared/openapi-metadata.xCtn3sZr.cjs'); const deepmerge = require('deepmerge'); require('./shared/openapi-metadata.B1TUxh8W.cjs'); function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; } const deepmerge__default = /*#__PURE__*/_interopDefaultCompat(deepmerge); class Context { schemas = {}; typeLoaders; logger; constructor(logger, typeLoaders) { this.logger = logger ?? console; this.typeLoaders = typeLoaders ?? []; } } function getEnumType(values) { return values.some((v) => typeof v === "string") ? "string" : "number"; } function getEnumValues(enumType) { if (Array.isArray(enumType)) { return enumType; } const numericValues = Object.values(enumType).filter((value) => typeof value === "number").map((value) => value.toString()); return Object.keys(enumType).filter((key) => !numericValues.includes(key)).map((key) => enumType[key]); } function getSchemaPath(model) { const modelName = typeof model === "string" ? model : model.name; return `#/components/schemas/${modelName}`; } const PrimitiveTypeLoader = async (_context, value) => { if (typeof value === "string") { return { type: value }; } if (value == String) { return { type: "string" }; } if (value == Boolean) { return { type: "boolean" }; } if (value == Number) { return { type: "number" }; } }; const ArrayTypeLoader = async (context, value) => { if (!Array.isArray(value)) { return; } if (value.length <= 0) { context.logger.warn("You tried to specify an array type without any item"); return; } if (value.length > 1) { context.logger.warn( "You tried to specify an array type with multiple items. Please use the 'enum' option if you want to specify an enum." ); return; } const itemsSchema = await loadType(context, { type: value[0] }); if (!itemsSchema) { context.logger.warn("You tried to specify an array type with an item that resolves to undefined."); return; } return { type: "array", items: itemsSchema }; }; const ClassTypeLoader = async (context, value) => { if (typeof value !== "function" || !value.prototype) { return; } const model = value.name; if (context.schemas[model]) { return { $ref: getSchemaPath(model) }; } const schema = { type: "object", properties: {}, required: [] }; const properties = extraModels.PropertyMetadataStorage.getMetadata(value.prototype); if (!properties) { context.logger.warn(`You tried to use '${model}' as a type but it does not contain any ApiProperty.`); } context.schemas[model] = schema; for (const [key, property] of Object.entries(properties)) { const { required, type, name, enum: e, schema: s, ...metadata } = property; schema.properties[key] = { ...await loadType(context, property), ...metadata }; if (property.required) { schema.required.push(key); } } return { $ref: getSchemaPath(model) }; }; async function loadType(context, options) { if (options.schema) { return options.schema; } if (options.enum) { const enumValues = getEnumValues(options.enum); const enumType = getEnumType(enumValues); return { type: enumType, enum: enumValues }; } if (!options.type) { context.logger.warn("Failed to infer type from property"); return; } const thunk = metadata.isThunk(options.type); const value = thunk ? options.type(context) : options.type; for (const loader of [PrimitiveTypeLoader, ArrayTypeLoader, ...context.typeLoaders, ClassTypeLoader]) { const result = await loader(context, value, options.type); if (result) { return result; } } context.logger.warn(`You tried to use '${options.type.toString()}' as a type but no loader supports it ${thunk}`); } async function generateOperationBody(context, metadata) { const schema = await loadType(context, metadata); return { content: { [metadata.mediaType]: { schema } } }; } async function generateOperationParameters(context, metadata) { const { schema: s, enum: e, type, ...parameter } = metadata; return { ...parameter, schema: await loadType(context, { type: "string", ...metadata }) }; } async function generateOperationResponse(context, metadata) { const { type, schema: s, enum: e, mediaType, status, ...response } = metadata; return { description: "", ...response, content: { [mediaType]: { schema: await loadType(context, metadata) } } }; } async function generateOperation(context, controller, propertyKey, { path, methods, ...metadata }) { const operation = { ...metadata, responses: {} }; const target = controller.prototype; const body = extraModels.OperationBodyMetadataStorage.getMetadata(target, propertyKey); if (body) { operation.requestBody = await generateOperationBody(context, body); } const parameters = extraModels.OperationParameterMetadataStorage.getMetadata(target, propertyKey, true); operation.parameters = []; for (const parameter of parameters) { operation.parameters.push(await generateOperationParameters(context, parameter)); } const responses = extraModels.OperationResponseMetadataStorage.getMetadata(target, propertyKey, true); for (const [status, response] of Object.entries(responses)) { operation.responses[status] = await generateOperationResponse(context, response); } const security = extraModels.OperationSecurityMetadataStorage.getMetadata(target, propertyKey, true); operation.security = Object.keys(security).length > 0 ? [security] : []; return operation; } async function generatePaths(context, controllers) { const paths = {}; for (const controller of controllers) { const target = controller.prototype; const keys = Object.getOwnPropertyNames(target); const extraModels$1 = extraModels.ExtraModelsMetadataStorage.getMetadata(target); await Promise.all(extraModels$1.map((m) => loadType(context, { type: m }))); for (const key of keys) { const metadata = extraModels.OperationMetadataStorage.getMetadata(target, key, true); if (!metadata) { continue; } if (!metadata.path || !metadata.methods) { continue; } const excludeController = extraModels.ExcludeMetadataStorage.getMetadata(target); if (excludeController === true) { continue; } for (const method of metadata.methods) { const excludeOperation = extraModels.ExcludeMetadataStorage.getMetadata(target, key); if (excludeOperation === true) { continue; } paths[metadata.path] = { ...paths[metadata.path], [method]: await generateOperation(context, controller, key, metadata) }; } } } return paths; } async function generateDocument(options) { const context = new Context(options.customLogger, options.loaders); return deepmerge__default(options.document, { openapi: "3.0.0", paths: await generatePaths(context, options.controllers), components: { schemas: context.schemas } }); } exports.generateDocument = generateDocument; exports.getSchemaPath = getSchemaPath;