From a5b7a9bd5de142c31ad23f6f72c5e224906f939e Mon Sep 17 00:00:00 2001 From: liuyi Date: Sat, 14 Jun 2025 20:57:47 +0800 Subject: [PATCH] add route module --- src/main.ts | 3 +- src/modules/core/helpers/app.ts | 21 +------ src/modules/restful/restful.module.ts | 24 ++++++++ src/modules/restful/restful.ts | 88 ++++++++++++++++++++++++++- src/modules/restful/utils.ts | 57 ++++++++++++++++- 5 files changed, 168 insertions(+), 25 deletions(-) create mode 100644 src/modules/restful/restful.module.ts diff --git a/src/main.ts b/src/main.ts index deea31e..0bebad4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,5 @@ -import { createApp, listened, startApp } from './modules/core/helpers/app'; +import { createApp, startApp } from './modules/core/helpers/app'; +import { listened } from './modules/restful/utils'; import { createOptions } from './options'; startApp(createApp(createOptions), listened); diff --git a/src/modules/core/helpers/app.ts b/src/modules/core/helpers/app.ts index 074bae1..1191ede 100644 --- a/src/modules/core/helpers/app.ts +++ b/src/modules/core/helpers/app.ts @@ -1,11 +1,9 @@ import { BadGatewayException, Global, Module, ModuleMetadata, Type } from '@nestjs/common'; import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; -import { NestFastifyApplication } from '@nestjs/platform-fastify'; -import chalk from 'chalk'; import { useContainer } from 'class-validator'; -import { isNil, omit } from 'lodash'; +import { omit } from 'lodash'; import { ConfigModule } from '@/modules/config/config.module'; import { Configure } from '@/modules/config/configure'; @@ -104,20 +102,3 @@ export async function startApp( const { port, host } = await configure.get('app'); await container.listen(port, host, listened(app, startTime)); } - -export async function echoApi(configure: Configure, container: NestFastifyApplication) { - const appUrl = await configure.get('app.url'); - const urlPrefix = await configure.get('api.prefix', undefined); - const apiUrl = isNil(urlPrefix) - ? appUrl - : `${appUrl}${urlPrefix.length > 0 ? `/${urlPrefix}` : urlPrefix}`; - console.log(`- RestAPI: ${chalk.green.underline(apiUrl)}`); -} - -export const listened: (app: App, startyTime: Date) => () => Promise = - ({ configure, container }, startTime) => - async () => { - console.log(); - await echoApi(configure, container); - console.log('used time: ', chalk.cyan(`${new Date().getTime() - startTime.getTime()}`)); - }; diff --git a/src/modules/restful/restful.module.ts b/src/modules/restful/restful.module.ts new file mode 100644 index 0000000..24a42eb --- /dev/null +++ b/src/modules/restful/restful.module.ts @@ -0,0 +1,24 @@ +import { DynamicModule } from '@nestjs/common'; + +import { Configure } from '../config/configure'; + +import { Restful } from './restful'; + +export class RestfulModule { + static async forRoot(configure: Configure): Promise { + const restful = new Restful(configure); + await restful.create(await configure.get('api')); + return { + module: RestfulModule, + global: true, + imports: restful.getModuleImports(), + providers: [ + { + provide: Restful, + useValue: restful, + }, + ], + exports: [Restful], + }; + } +} diff --git a/src/modules/restful/restful.ts b/src/modules/restful/restful.ts index 7ace5e9..292acec 100644 --- a/src/modules/restful/restful.ts +++ b/src/modules/restful/restful.ts @@ -1,10 +1,18 @@ -import { Type } from '@nestjs/common'; +import { INestApplication, Type } from '@nestjs/common'; import { RouterModule } from '@nestjs/core'; -import { omit } from 'lodash'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { omit, trim } from 'lodash'; import { BaseRestful } from './base'; -import { ApiConfig, ApiDocOption, ApiDocSource, RouteOption, SwaggerOption } from './types'; +import { + ApiConfig, + ApiDocOption, + ApiDocSource, + RouteOption, + SwaggerOption, + VersionOption, +} from './types'; import { trimPath } from './utils'; export class Restful extends BaseRestful { @@ -71,6 +79,80 @@ export class Restful extends BaseRestful { (module) => !excludeModules.find((emodule) => emodule === module), ); } + + protected getDocOption(name: string, voption: VersionOption, isDefault = false) { + const docConfig: ApiDocOption = {}; + const defaultDoc = { + title: voption.title!, + description: voption.description!, + tags: voption.tags ?? [], + auth: voption.auth ?? false, + version: name, + path: trim(`${this.config.docuri}${isDefault ? '' : `/${name}`}`, '/'), + }; + + const routesDoc = isDefault + ? this.getRouteDocs(defaultDoc, voption.routes ?? []) + : this.getRouteDocs(defaultDoc, voption.routes ?? [], name); + if (Object.keys(routesDoc).length > 0) { + docConfig.routes = routesDoc; + } + const routeModules = isDefault + ? this.getRouteModules(voption.routes ?? []) + : this.getRouteModules(voption.routes ?? [], name); + const include = this.filterExcludeModules(routeModules); + if (include.length > 0 || !docConfig.routes) { + docConfig.default = { ...defaultDoc, include }; + } + return docConfig; + } + + protected createDocs() { + const versionMaps = Object.entries(this.config.versions); + const vDocs = versionMaps.map(([name, version]) => [ + name, + this.getDocOption(name, version), + ]); + this._docs = Object.fromEntries(vDocs); + const defaultVersion = this.config.versions[this._default]; + this._docs.default = this.getDocOption(this._default, defaultVersion, true); + } + + async factoryDocs(container: T) { + const docs = Object.values(this._docs) + .map((doc) => [doc.default, ...Object.values(doc.routes ?? [])]) + .reduce((o, n) => [...o, ...n], []) + .filter((i) => !!i); + + for (const voption of docs) { + const { title, description, version, auth, include, tags } = voption!; + const builder = new DocumentBuilder(); + if (title) { + builder.setTitle(title); + } + if (description) { + builder.setDescription(description); + } + if (auth) { + builder.addBearerAuth(); + } + if (tags) { + tags.forEach((tag) => + typeof tag === 'string' + ? builder.addTag(tag) + : builder.addTag(tag.name, tag.description, tag.externalDocs), + ); + } + builder.setVersion(version); + + const document = SwaggerModule.createDocument(container, builder.build(), { + include: include.length > 0 ? include : [() => undefined as any], + ignoreGlobalPrefix: true, + deepScanRoutes: true, + }); + SwaggerModule.setup(voption!.path, container, document); + } + } } export function genDocPath(routePath: string, prefix?: string, version?: string) { diff --git a/src/modules/restful/utils.ts b/src/modules/restful/utils.ts index d898418..70f37a8 100644 --- a/src/modules/restful/utils.ts +++ b/src/modules/restful/utils.ts @@ -1,14 +1,19 @@ import { Type } from '@nestjs/common'; import { Routes, RouteTree } from '@nestjs/core'; +import { NestFastifyApplication } from '@nestjs/platform-fastify'; import { ApiTags } from '@nestjs/swagger'; +import chalk from 'chalk'; import { camelCase, isNil, omit, trim, upperFirst } from 'lodash'; import { Configure } from '../config/configure'; import { CreateModule } from '../core/helpers'; +import { App } from '../core/types'; + import { CONTROLLER_DEPENDS } from './constants'; -import { RouteOption } from './types'; +import { Restful } from './restful'; +import { ApiDocOption, RouteOption } from './types'; export const trimPath = (routePath: string, addPrefix = true) => `${addPrefix ? '/' : ''}${trim(routePath.replace('//', '/'), '/')}`; @@ -84,3 +89,53 @@ export function createRouteModuleTree( }), ); } + +export async function echoApi(configure: Configure, container: NestFastifyApplication) { + const appUrl = await configure.get('app.url'); + const urlPrefix = await configure.get('api.prefix', undefined); + const apiUrl = isNil(urlPrefix) + ? appUrl + : `${appUrl}${urlPrefix.length > 0 ? `/${urlPrefix}` : urlPrefix}`; + console.log(`- RestAPI: ${chalk.green.underline(apiUrl)}`); + console.log('- RestDocs'); + const factory = container.get(Restful); + const { default: defaultDoc, ...docs } = factory.docs; + await echoApiDocs('default', defaultDoc, appUrl); + for (const [name, doc] of Object.entries(docs)) { + console.log(); + echoApiDocs(name, doc, appUrl); + } +} + +export const listened: (app: App, startyTime: Date) => () => Promise = + ({ configure, container }, startTime) => + async () => { + console.log(); + await echoApi(configure, container); + console.log('used time: ', chalk.cyan(`${new Date().getTime() - startTime.getTime()}`)); + }; + +async function echoApiDocs(name: string, doc: ApiDocOption, appUrl: string) { + const getDocPath = (path: string) => `${appUrl}/${path}`; + if (!doc.routes && doc.default) { + console.log( + `[${chalk.blue(name.toUpperCase())}]:${chalk.green.underline( + getDocPath(doc.default.path), + )}`, + ); + return; + } + console.log(`[${chalk.blue(name.toUpperCase())}]`); + if (doc.default) { + console.log(`default:${chalk.green.underline(getDocPath(doc.default.path))}`); + } + if (doc.routes) { + Object.entries(doc.routes).forEach(([routeName, docs]) => { + console.log( + `<${chalk.yellowBright.bold(docs.title)}>: ${chalk.green.underline( + getDocPath(docs.path), + )}`, + ); + }); + } +}