diff --git a/e2e/manual-e2e.ts b/e2e/manual-e2e.ts index 4d45937e7..ae1a66df5 100644 --- a/e2e/manual-e2e.ts +++ b/e2e/manual-e2e.ts @@ -1,9 +1,16 @@ import { NestFactory } from '@nestjs/core'; import { ApplicationModule } from './src/app.module'; import { DocumentBuilder, SwaggerModule } from '../lib'; -import { FastifyAdapter } from '@nestjs/platform-fastify'; +import { + FastifyAdapter, + NestFastifyApplication +} from '@nestjs/platform-fastify'; import { INestApplication, Logger } from '@nestjs/common'; -import { ExpressAdapter } from '@nestjs/platform-express'; +import { + ExpressAdapter, + NestExpressApplication +} from '@nestjs/platform-express'; +import { join } from 'path'; const port = 4001; const host = 'localhost'; @@ -12,6 +19,7 @@ const docRelPath = '/api-docs'; const USE_FASTIFY = false; const adapter = USE_FASTIFY ? new FastifyAdapter() : new ExpressAdapter(); +const publicFolderPath = join(__dirname, '../../e2e', 'public'); async function bootstrap() { const app = await NestFactory.create( @@ -48,7 +56,17 @@ async function bootstrap() { swaggerOptions: { persistAuthorization: true, defaultModelsExpandDepth: -1 - } + }, + customfavIcon: '/public/favicon.ico', + customCssUrl: '/public/theme.css', // to showcase that in new implementation u can use custom css with fastify + uiHooks: USE_FASTIFY + ? { + onRequest: (req: any, res: any, next: any) => { + console.log('FASTIFY HOOK POC 1'); + next(); + } + } + : undefined }); SwaggerModule.setup('/swagger-docs', app, document, { @@ -56,9 +74,27 @@ async function bootstrap() { uiConfig: { persistAuthorization: true, defaultModelsExpandDepth: -1 - } + }, + uiHooks: USE_FASTIFY + ? { + onRequest: (req: any, res: any, next: any) => { + console.log('FASTIFY HOOK POC 2'); + next(); + } + } + : undefined }); + USE_FASTIFY + ? (app as NestFastifyApplication).useStaticAssets({ + root: publicFolderPath, + prefix: `/public`, + decorateReply: false + }) + : (app as NestExpressApplication).useStaticAssets(publicFolderPath, { + prefix: '/public' + }); + await app.listen(port, host); const baseUrl = `http://${host}:${port}`; const startMessage = `Server started at ${baseUrl}; SwaggerUI at ${ diff --git a/e2e/public/favicon.ico b/e2e/public/favicon.ico new file mode 100644 index 000000000..3c2d34204 Binary files /dev/null and b/e2e/public/favicon.ico differ diff --git a/e2e/public/logo.png b/e2e/public/logo.png new file mode 100644 index 000000000..3c10d98ac Binary files /dev/null and b/e2e/public/logo.png differ diff --git a/e2e/public/theme.css b/e2e/public/theme.css new file mode 100644 index 000000000..6f2ea999e --- /dev/null +++ b/e2e/public/theme.css @@ -0,0 +1,24 @@ +.swagger-ui .btn.authorize { + line-height: 1; + display: inline; + color: #336E7B; + border-color: #336E7B; +} +.swagger-ui .btn.authorize svg { + fill: #ef0505; +} + + +.swagger-ui body { + margin: 0; + background: #fafafa +} + +img[alt="Swagger UI"] { + display: block; + -moz-box-sizing: border-box; + box-sizing: border-box; + content: url('/public/logo.png'); + max-width: 100%; + max-height: 100%; +} diff --git a/lib/backward-compatilibity-layer.ts b/lib/backward-compatilibity-layer.ts new file mode 100644 index 000000000..e76f96732 --- /dev/null +++ b/lib/backward-compatilibity-layer.ts @@ -0,0 +1,127 @@ +import { + FastifySwaggerCustomOptions, + SwaggerCustomOptions, + SwaggerCustomOptionsLegacy +} from './interfaces'; +import { HttpServer } from '@nestjs/common/interfaces/http/http-server.interface'; + +export interface FastifyExtra { + initOAuth?: Record; + staticCSP?: boolean | string | Record; + transformStaticCSP?: (header: string) => string; + uiHooks?: { + onRequest?: Function; + preHandler?: Function; + }; +} + +export interface ProcessSwaggerOptionsOutput { + customOptions: SwaggerCustomOptions; + extra: FastifyExtra; +} + +export function processSwaggerOptions( + options: SwaggerCustomOptionsLegacy = {} +): ProcessSwaggerOptionsOutput { + const unifiedOptions: SwaggerCustomOptions = options; + const fastifyOptions: FastifySwaggerCustomOptions = options; + + const customOptions: SwaggerCustomOptions = { + useGlobalPrefix: unifiedOptions.useGlobalPrefix, + explorer: unifiedOptions.explorer, + customCss: unifiedOptions.customCss, + customCssUrl: unifiedOptions.customCssUrl, + customJs: unifiedOptions.customJs, + customfavIcon: unifiedOptions.customfavIcon, + swaggerUrl: unifiedOptions.swaggerUrl, + customSiteTitle: unifiedOptions.customSiteTitle, + validatorUrl: unifiedOptions.validatorUrl, + url: unifiedOptions.url, + urls: unifiedOptions.urls, + initOAuth: unifiedOptions.initOAuth, + + swaggerOptions: unifiedOptions.swaggerOptions || fastifyOptions.uiConfig + }; + + const extra = { + uiHooks: fastifyOptions.uiHooks + }; + + return { customOptions, extra }; +} + +export function serveDocumentsFastify( + finalPath: string, + httpAdapter: HttpServer, + swaggerInitJS: string, + yamlDocument: string, + jsonDocument: string, + html: string, + fastifyExtras: FastifyExtra +) { + const httpServer = httpAdapter as any; + + // Workaround for older versions of the @nestjs/platform-fastify package + // where "isParserRegistered" getter is not defined. + const hasParserGetterDefined = ( + Object.getPrototypeOf(httpServer) as Object + ).hasOwnProperty('isParserRegistered'); + if (hasParserGetterDefined && !httpServer.isParserRegistered) { + httpServer.registerParserMiddleware(); + } + + httpServer.register(async (fastifyApp: any) => { + const hooks = Object.create(null); + + if (fastifyExtras.uiHooks) { + const additionalHooks = ['onRequest', 'preHandler']; + for (const hook of additionalHooks) { + hooks[hook] = fastifyExtras.uiHooks[hook]; + } + } + + fastifyApp.route({ + url: finalPath, + method: 'GET', + schema: { hide: true }, + ...hooks, + handler: (req: any, reply: any) => { + reply.type('text/html'); + reply.send(html); + } + }); + + fastifyApp.route({ + url: `${finalPath}/swagger-ui-init.js`, + method: 'GET', + schema: { hide: true }, + ...hooks, + handler: (req: any, reply: any) => { + reply.type('application/javascript'); + reply.send(swaggerInitJS); + } + }); + + fastifyApp.route({ + url: `${finalPath}-json`, + method: 'GET', + schema: { hide: true }, + ...hooks, + handler: (req: any, reply: any) => { + reply.type('text/json'); + reply.send(jsonDocument); + } + }); + + fastifyApp.route({ + url: `${finalPath}-yaml`, + method: 'GET', + schema: { hide: true }, + ...hooks, + handler: (req: any, reply: any) => { + reply.type('text/yaml'); + reply.send(yamlDocument); + } + }); + }); +} diff --git a/lib/interfaces/swagger-custom-options-legacy.instace.ts b/lib/interfaces/swagger-custom-options-legacy.instace.ts index 3d7af628d..c9675600c 100644 --- a/lib/interfaces/swagger-custom-options-legacy.instace.ts +++ b/lib/interfaces/swagger-custom-options-legacy.instace.ts @@ -50,8 +50,8 @@ export interface FastifySwaggerCustomOptions queryConfigEnabled: boolean; }>; initOAuth?: Record; - staticCSP?: boolean | string | Record; - transformStaticCSP?: (header: string) => string; + staticCSP?: boolean | string | Record; // not supported + transformStaticCSP?: (header: string) => string; // not supported uiHooks?: { onRequest?: Function; preHandler?: Function; diff --git a/lib/interfaces/swagger-custom-options.interface.ts b/lib/interfaces/swagger-custom-options.interface.ts index fa0a73a58..b795cb9dc 100644 --- a/lib/interfaces/swagger-custom-options.interface.ts +++ b/lib/interfaces/swagger-custom-options.interface.ts @@ -11,4 +11,5 @@ export interface SwaggerCustomOptions { validatorUrl?: string; url?: string; urls?: Record<'url' | 'name', string>[]; + initOAuth?: Record; // https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/ } diff --git a/lib/swagger-module.ts b/lib/swagger-module.ts index e99eea89d..d4b31c6d7 100644 --- a/lib/swagger-module.ts +++ b/lib/swagger-module.ts @@ -16,7 +16,11 @@ import { } from './swagger-ui'; import { NestFastifyApplication } from '@nestjs/platform-fastify'; import { NestExpressApplication } from '@nestjs/platform-express'; -import { processSwaggerOptions } from './utils/backward-compatilibity-layer'; +import { + processSwaggerOptions, + serveDocumentsFastify +} from './backward-compatilibity-layer'; +import { HttpServer } from '@nestjs/common/interfaces/http/http-server.interface'; export class SwaggerModule { public static createDocument( @@ -60,14 +64,12 @@ export class SwaggerModule { private static serveDocuments( finalPath: string, - app: INestApplication, + httpAdapter: HttpServer, swaggerInitJS: string, yamlDocument: string, jsonDocument: string, html: string ) { - const httpAdapter = app.getHttpAdapter(); - httpAdapter.get(`${finalPath}/swagger-ui-init.js`, (req, res) => { res.type('application/javascript'); res.send(swaggerInitJS); @@ -117,14 +119,33 @@ export class SwaggerModule { const html = buildSwaggerHTML(finalPath, document, customOptions); const swaggerInitJS = buildSwaggerInitJS(document, customOptions); - SwaggerModule.serveDocuments( - finalPath, - app, - swaggerInitJS, - yamlDocument, - jsonDocument, - html - ); + const httpAdapter = app.getHttpAdapter(); + + // START: fastify backward compatibility layer + const IS_FASTIFY = httpAdapter && httpAdapter.getType() === 'fastify'; + + if (IS_FASTIFY) { + serveDocumentsFastify( + finalPath, + httpAdapter, + swaggerInitJS, + yamlDocument, + jsonDocument, + html, + extra + ); + } else { + SwaggerModule.serveDocuments( + finalPath, + httpAdapter, + swaggerInitJS, + yamlDocument, + jsonDocument, + html + ); + } + // END: fastify backward compatibility layer + SwaggerModule.serveStatic(finalPath, app); } } diff --git a/lib/swagger-ui/constants.ts b/lib/swagger-ui/constants.ts index 762218328..7c196a68e 100644 --- a/lib/swagger-ui/constants.ts +++ b/lib/swagger-ui/constants.ts @@ -118,8 +118,8 @@ window.onload = function() { } let ui = SwaggerUIBundle(swaggerOptions) - if (customOptions.oauth) { - ui.initOAuth(customOptions.oauth) + if (customOptions.initOAuth) { + ui.initOAuth(customOptions.initOAuth) } if (customOptions.authAction) { diff --git a/lib/swagger-ui/swagger-ui.ts b/lib/swagger-ui/swagger-ui.ts index cd9cd0261..1f00c802a 100644 --- a/lib/swagger-ui/swagger-ui.ts +++ b/lib/swagger-ui/swagger-ui.ts @@ -25,7 +25,7 @@ export function buildSwaggerInitJS( * Stores absolute path to swagger-ui assets */ export const swaggerAssetsAbsoluteFSPath = swaggerUi.getAbsoluteFSPath(); - +console.log(swaggerAssetsAbsoluteFSPath); /** * Used to build swagger-ui custom html */ diff --git a/lib/utils/backward-compatilibity-layer.ts b/lib/utils/backward-compatilibity-layer.ts deleted file mode 100644 index 903ec4826..000000000 --- a/lib/utils/backward-compatilibity-layer.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { - FastifySwaggerCustomOptions, - SwaggerCustomOptions, - SwaggerCustomOptionsLegacy -} from '../interfaces'; - -export interface FastifyExtra { - initOAuth?: Record; - staticCSP?: boolean | string | Record; - transformStaticCSP?: (header: string) => string; - uiHooks?: { - onRequest?: Function; - preHandler?: Function; - }; -} - -export interface ProcessSwaggerOptionsOutput { - customOptions: SwaggerCustomOptions; - extra: FastifyExtra; -} - -export function processSwaggerOptions( - options: SwaggerCustomOptionsLegacy = {} -): ProcessSwaggerOptionsOutput { - const unifiedOptions: SwaggerCustomOptions = options; - const fastifyOptions: FastifySwaggerCustomOptions = options; - - const customOptions: SwaggerCustomOptions = { - useGlobalPrefix: unifiedOptions.useGlobalPrefix, - explorer: unifiedOptions.explorer, - customCss: unifiedOptions.customCss, - customCssUrl: unifiedOptions.customCssUrl, - customJs: unifiedOptions.customJs, - customfavIcon: unifiedOptions.customfavIcon, - swaggerUrl: unifiedOptions.swaggerUrl, - customSiteTitle: unifiedOptions.customSiteTitle, - validatorUrl: unifiedOptions.validatorUrl, - url: unifiedOptions.url, - urls: unifiedOptions.urls, - - swaggerOptions: unifiedOptions.swaggerOptions || fastifyOptions.uiConfig - }; - - const extra = { - initOAuth: fastifyOptions.initOAuth, - uiHooks: fastifyOptions.uiHooks - }; - - return { customOptions, extra }; -}