UNPKG

@nestjs/swagger

Version:

Nest - modern, fast, powerful node.js web framework (@swagger)

233 lines (232 loc) 10.9 kB
import { NotFoundException } from '@nestjs/common'; import { loadPackageSync } from '@nestjs/common/utils/load-package.util.js'; import * as jsyaml from 'js-yaml'; import { createRequire } from 'node:module'; import { MetadataLoader } from './plugin/metadata-loader.js'; import { SwaggerScanner } from './swagger-scanner.js'; import { buildSwaggerHTML, buildSwaggerInitJS, getSwaggerAssetsAbsoluteFSPath } from './swagger-ui/index.js'; import { assignTwoLevelsDeep } from './utils/assign-two-levels-deep.js'; import { getGlobalPrefix } from './utils/get-global-prefix.js'; import { isOas31OrLater } from './utils/is-oas31-or-later.util.js'; import { normalizeRelPath } from './utils/normalize-rel-path.js'; import { convertNullableToOas31 } from './utils/nullable-to-oas31.util.js'; import { resolvePath } from './utils/resolve-path.util.js'; import { validateGlobalPrefix } from './utils/validate-global-prefix.util.js'; import { validatePath } from './utils/validate-path.util.js'; const require = createRequire(import.meta.url); export class SwaggerModule { static mergeWebhooks(configWebhooks, scannedWebhooks) { if (!configWebhooks && !scannedWebhooks) { return undefined; } return assignTwoLevelsDeep({}, configWebhooks || {}, scannedWebhooks || {}); } static createDocument(app, config, options = {}) { const swaggerScanner = new SwaggerScanner(); const document = swaggerScanner.scanApplication(app, options); const { webhooks: configWebhooks, ...configWithoutWebhooks } = config; document.components = assignTwoLevelsDeep({}, config.components, document.components); const { webhooks: scannedWebhooks, webhookPaths, ...documentWithoutWebhooks } = document; const mergedWebhooks = SwaggerModule.mergeWebhooks(configWebhooks, scannedWebhooks); const isOas31 = isOas31OrLater(config.openapi ?? '3.0.0'); const baseDocument = { openapi: '3.0.0', paths: {}, ...configWithoutWebhooks, ...documentWithoutWebhooks }; const finalDocument = isOas31 ? { ...baseDocument, ...(mergedWebhooks ? { webhooks: mergedWebhooks } : {}) } : { ...baseDocument, paths: webhookPaths ? assignTwoLevelsDeep({}, baseDocument.paths || {}, webhookPaths) : baseDocument.paths }; if (isOas31) { convertNullableToOas31(finalDocument); } return finalDocument; } static async loadPluginMetadata(metadataFn) { const metadata = await metadataFn(); return this.metadataLoader.load(metadata); } static serveStatic(finalPath, app, customStaticPath) { const httpAdapter = app.getHttpAdapter(); const swaggerAssetsPath = customStaticPath ? resolvePath(customStaticPath) : getSwaggerAssetsAbsoluteFSPath(); if (httpAdapter && httpAdapter.getType() === 'fastify') { const fastifyStaticModule = loadPackageSync('@fastify/static', 'SwaggerModule', () => require('@fastify/static')); const fastifyStatic = fastifyStaticModule.default ?? fastifyStaticModule; httpAdapter.getInstance().register(fastifyStatic, { root: swaggerAssetsPath, prefix: finalPath, decorateReply: false }); } else { app.useStaticAssets(swaggerAssetsPath, { prefix: finalPath }); } } static serveDocuments(finalPath, urlLastSubdirectory, httpAdapter, documentOrFactory, options) { let document; const getBuiltDocument = () => { if (!document) { document = typeof documentOrFactory === 'function' ? documentOrFactory() : documentOrFactory; } return document; }; if (options.ui) { this.serveSwaggerUi(finalPath, urlLastSubdirectory, httpAdapter, getBuiltDocument, options.swaggerOptions); } if (options.raw === true || (Array.isArray(options.raw) && options.raw.length > 0)) { const serveJson = options.raw === true || options.raw.includes('json'); const serveYaml = options.raw === true || options.raw.includes('yaml'); this.serveDefinitions(httpAdapter, getBuiltDocument, options, { serveJson, serveYaml }); } } static serveSwaggerUi(finalPath, urlLastSubdirectory, httpAdapter, getBuiltDocument, swaggerOptions) { const baseUrlForSwaggerUI = normalizeRelPath(`./${urlLastSubdirectory}/`); let swaggerUiHtml; let swaggerUiHtmlForTrailingSlash; let swaggerUiInitJS; httpAdapter.get(normalizeRelPath(`${finalPath}/swagger-ui-init.js`), async (req, res) => { res.type('application/javascript'); const document = getBuiltDocument(); if (swaggerOptions.patchDocumentOnRequest) { const documentToSerialize = await Promise.resolve(swaggerOptions.patchDocumentOnRequest(req, res, document)); const swaggerInitJsPerRequest = buildSwaggerInitJS(documentToSerialize, swaggerOptions); return res.send(swaggerInitJsPerRequest); } if (!swaggerUiInitJS) { swaggerUiInitJS = buildSwaggerInitJS(document, swaggerOptions); } return res.send(swaggerUiInitJS); }); try { httpAdapter.get(normalizeRelPath(`${finalPath}/${urlLastSubdirectory}/swagger-ui-init.js`), async (req, res) => { res.type('application/javascript'); const document = getBuiltDocument(); if (swaggerOptions.patchDocumentOnRequest) { const documentToSerialize = await Promise.resolve(swaggerOptions.patchDocumentOnRequest(req, res, document)); const swaggerInitJsPerRequest = buildSwaggerInitJS(documentToSerialize, swaggerOptions); return res.send(swaggerInitJsPerRequest); } if (!swaggerUiInitJS) { swaggerUiInitJS = buildSwaggerInitJS(document, swaggerOptions); } return res.send(swaggerUiInitJS); }); } catch { } const getSwaggerHtml = () => { if (!swaggerUiHtml) { swaggerUiHtml = buildSwaggerHTML(baseUrlForSwaggerUI, swaggerOptions); } return swaggerUiHtml; }; const getTrailingSlashSwaggerHtml = () => { if (!swaggerUiHtmlForTrailingSlash) { swaggerUiHtmlForTrailingSlash = buildSwaggerHTML('./', swaggerOptions); } return swaggerUiHtmlForTrailingSlash; }; function serveSwaggerHtml(req, res) { res.type('text/html'); const url = httpAdapter.getRequestUrl(req); const hasTrailingSlash = url.endsWith('/'); const swaggerUiHtml = hasTrailingSlash ? getTrailingSlashSwaggerHtml() : getSwaggerHtml(); res.send(swaggerUiHtml); } httpAdapter.get(finalPath, serveSwaggerHtml); httpAdapter.get(`${finalPath}/index.html`, serveSwaggerHtml); httpAdapter.get(`${finalPath}/LICENSE`, () => { throw new NotFoundException(); }); try { httpAdapter.get(normalizeRelPath(`${finalPath}/`), serveSwaggerHtml); } catch { } } static serveDefinitions(httpAdapter, getBuiltDocument, options, serveOptions) { if (serveOptions.serveJson) { httpAdapter.get(normalizeRelPath(options.jsonDocumentUrl), async (req, res) => { res.type('application/json'); const document = getBuiltDocument(); const documentToSerialize = options.swaggerOptions .patchDocumentOnRequest ? await Promise.resolve(options.swaggerOptions.patchDocumentOnRequest(req, res, document)) : document; return res.send(JSON.stringify(documentToSerialize)); }); } if (serveOptions.serveYaml) { httpAdapter.get(normalizeRelPath(options.yamlDocumentUrl), async (req, res) => { res.type('text/yaml'); const document = getBuiltDocument(); const documentToSerialize = options.swaggerOptions .patchDocumentOnRequest ? await Promise.resolve(options.swaggerOptions.patchDocumentOnRequest(req, res, document)) : document; const yamlDocument = jsyaml.dump(documentToSerialize, { skipInvalid: true, noRefs: true }); return res.send(yamlDocument); }); } } static setup(path, app, documentOrFactory, options) { const globalPrefix = getGlobalPrefix(app); const finalPath = validatePath(options?.useGlobalPrefix && validateGlobalPrefix(globalPrefix) ? `${globalPrefix}${validatePath(path)}` : path); const urlLastSubdirectory = finalPath.split('/').slice(-1).pop() || ''; const validatedGlobalPrefix = options?.useGlobalPrefix && validateGlobalPrefix(globalPrefix) ? validatePath(globalPrefix) : ''; const finalJSONDocumentPath = options?.jsonDocumentUrl ? `${validatedGlobalPrefix}${validatePath(options.jsonDocumentUrl)}` : `${finalPath}-json`; const finalYAMLDocumentPath = options?.yamlDocumentUrl ? `${validatedGlobalPrefix}${validatePath(options.yamlDocumentUrl)}` : `${finalPath}-yaml`; const ui = options?.ui ?? options?.swaggerUiEnabled ?? true; const raw = options?.raw ?? true; const httpAdapter = app.getHttpAdapter(); SwaggerModule.serveDocuments(finalPath, urlLastSubdirectory, httpAdapter, documentOrFactory, { ui, raw, jsonDocumentUrl: finalJSONDocumentPath, yamlDocumentUrl: finalYAMLDocumentPath, swaggerOptions: options || {} }); if (ui) { SwaggerModule.serveStatic(finalPath, app, options?.customSwaggerUiPath); if (finalPath === `/${urlLastSubdirectory}`) { return; } const serveStaticSlashEndingPath = `${finalPath}/${urlLastSubdirectory}`; SwaggerModule.serveStatic(serveStaticSlashEndingPath, app); } } } SwaggerModule.metadataLoader = new MetadataLoader();