UNPKG

@nestia/sdk

Version:

Nestia SDK and Swagger generator

156 lines (147 loc) 5.62 kB
import { INestApplication } from "@nestjs/common"; import { OpenApiV3, OpenApiV3_1, SwaggerV2 } from "@typia/interface"; import { OpenApiConverter } from "@typia/utils"; import path from "path"; import { TreeMap } from "tstl"; import { OpenApi } from "typia"; import { INestiaConfig } from "./INestiaConfig"; import { AccessorAnalyzer } from "./analyses/AccessorAnalyzer"; import { ConfigAnalyzer } from "./analyses/ConfigAnalyzer"; import { PathAnalyzer } from "./analyses/PathAnalyzer"; import { ReflectControllerAnalyzer } from "./analyses/ReflectControllerAnalyzer"; import { TypedHttpRouteAnalyzer } from "./analyses/TypedHttpRouteAnalyzer"; import { SwaggerGenerator } from "./generates/SwaggerGenerator"; import { IMetadataDictionary } from "./internal/legacy"; import { INestiaProject } from "./structures/INestiaProject"; import { INestiaSdkInput } from "./structures/INestiaSdkInput"; import { IOperationMetadata } from "./structures/IOperationMetadata"; import { IReflectController } from "./structures/IReflectController"; import { IReflectOperationError } from "./structures/IReflectOperationError"; import { ITypedHttpRoute } from "./structures/ITypedHttpRoute"; import { VersioningStrategy } from "./utils/VersioningStrategy"; export namespace NestiaSwaggerComposer { export const document = async ( app: INestApplication, config: Omit<INestiaConfig.ISwaggerConfig, "output">, ): Promise< | OpenApi.IDocument | OpenApiV3_1.IDocument | OpenApiV3.IDocument | SwaggerV2.IDocument > => { const input: INestiaSdkInput = await ConfigAnalyzer.application(app); const document: OpenApi.IDocument = await SwaggerGenerator.compose({ config, routes: analyze(input), document: await SwaggerGenerator.initialize(config), }); return (config.openapi ?? "3.2") === "3.2" ? document : OpenApiConverter.downgradeDocument(document, config.openapi as "2.0"); }; const analyze = (input: INestiaSdkInput): ITypedHttpRoute[] => { // GET REFLECT CONTROLLERS const unique: WeakSet<any> = new WeakSet(); const project: Omit<INestiaProject, "config"> = { input, errors: [], warnings: [], }; const controllers: IReflectController[] = project.input.controllers .map((c) => ReflectControllerAnalyzer.analyze({ project, controller: c, unique }), ) .filter((c): c is IReflectController => c !== null); if (project.errors.length) throw report({ type: "error", errors: project.errors }); // METADATA COMPONENTS const collection: IMetadataDictionary = TypedHttpRouteAnalyzer.dictionary(controllers); // CONVERT TO TYPED OPERATIONS const routes: ITypedHttpRoute[] = []; for (const c of controllers) for (const o of c.operations) { if (o.protocol !== "http") continue; const pathList: Set<string> = new Set(); const versions: string[] = VersioningStrategy.merge(project)([ ...(c.versions ?? []), ...(o.versions ?? []), ]); for (const v of versions) for (const prefix of wrapPaths(c.prefixes)) for (const cPath of wrapPaths(c.paths)) for (const filePath of wrapPaths(o.paths)) { const localPath: string = PathAnalyzer.join( prefix, cPath, filePath, ); pathList.add( PathAnalyzer.joinWithGlobalPrefix({ globalPrefix: project.input.globalPrefix?.prefix ?? "", exclude: project.input.globalPrefix?.exclude, excludePath: localPath, method: o.method, path: PathAnalyzer.join(v, localPath), }), ); } routes.push( ...TypedHttpRouteAnalyzer.analyze({ controller: c, errors: project.errors, dictionary: collection, operation: o, paths: Array.from(pathList), }), ); } if (project.errors.length) throw report({ type: "error", errors: project.errors }); AccessorAnalyzer.analyze(routes); return routes; }; } const report = (props: { type: "error" | "warning"; errors: IReflectOperationError[]; }): void => { const map: TreeMap< IReflectOperationError.Key, Array<string | IOperationMetadata.IError> > = new TreeMap(); for (const e of props.errors) map.take(new IReflectOperationError.Key(e), () => []).push(...e.contents); const messages: string[] = []; for (const { first: { error }, second: contents, } of map) { if (error.contents.length === 0) continue; const location: string = path.relative(process.cwd(), error.file); messages.push( [ `${location} - `, error.class, ...(error.function !== null ? [`.${error.function}()`] : [""]), ...(error.from !== null ? [` from ${error.from}`] : [""]), ":\n", contents .map((c) => { if (typeof c === "string") return ` - ${c}`; else return [ c.accessor ? ` - ${c.name}: ` : ` - ${c.name} (${c.accessor}): `, ...c.messages.map((msg) => ` - ${msg}`), ].join("\n"); }) .join("\n"), ].join(""), ); } throw new Error(`Error on NestiaSwaggerComposer.compose():\n${messages}`); }; const wrapPaths = (paths: string[]): string[] => paths.length === 0 ? [""] : paths;