openapi-metadata
Version:
Auto-Generate OpenAPI specifications from Typescript decorators
226 lines (214 loc) • 7.26 kB
JavaScript
import { P as PropertyMetadataStorage, O as OperationBodyMetadataStorage, a as OperationParameterMetadataStorage, b as OperationResponseMetadataStorage, c as OperationSecurityMetadataStorage, E as ExtraModelsMetadataStorage, d as OperationMetadataStorage, e as ExcludeMetadataStorage } from './shared/openapi-metadata.Dc_PLA6h.mjs';
import { i as isThunk } from './shared/openapi-metadata.C_V3A0DP.mjs';
import deepmerge from 'deepmerge';
import './shared/openapi-metadata.BKn6E22y.mjs';
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 = 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 = 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 = OperationBodyMetadataStorage.getMetadata(target, propertyKey);
if (body) {
operation.requestBody = await generateOperationBody(context, body);
}
const parameters = OperationParameterMetadataStorage.getMetadata(target, propertyKey, true);
operation.parameters = [];
for (const parameter of parameters) {
operation.parameters.push(await generateOperationParameters(context, parameter));
}
const responses = OperationResponseMetadataStorage.getMetadata(target, propertyKey, true);
for (const [status, response] of Object.entries(responses)) {
operation.responses[status] = await generateOperationResponse(context, response);
}
const security = 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 = ExtraModelsMetadataStorage.getMetadata(target);
await Promise.all(extraModels.map((m) => loadType(context, { type: m })));
for (const key of keys) {
const metadata = OperationMetadataStorage.getMetadata(target, key, true);
if (!metadata) {
continue;
}
if (!metadata.path || !metadata.methods) {
continue;
}
const excludeController = ExcludeMetadataStorage.getMetadata(target);
if (excludeController === true) {
continue;
}
for (const method of metadata.methods) {
const excludeOperation = 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(options.document, {
openapi: "3.0.0",
paths: await generatePaths(context, options.controllers),
components: {
schemas: context.schemas
}
});
}
export { generateDocument, getSchemaPath };