nestapp/src/modules/restful/restful.ts
2025-06-14 20:57:47 +08:00

161 lines
5.7 KiB
TypeScript

import { INestApplication, Type } from '@nestjs/common';
import { RouterModule } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { omit, trim } from 'lodash';
import { BaseRestful } from './base';
import {
ApiConfig,
ApiDocOption,
ApiDocSource,
RouteOption,
SwaggerOption,
VersionOption,
} from './types';
import { trimPath } from './utils';
export class Restful extends BaseRestful {
protected _docs!: { [version: string]: ApiDocOption };
protected excludeVersionModules: string[] = [];
get docs() {
return this._docs;
}
async create(config: ApiConfig) {
this.createConfig(config);
await this.createRoutes();
this.createDocs();
}
getModuleImports() {
return [RouterModule.register(this.routes), ...Object.values(this.modules)];
}
protected getRouteDocs(
option: Omit<SwaggerOption, 'include'>,
routes: RouteOption[],
parent?: string,
): { [key: string]: SwaggerOption } {
const mergeDoc = (vDoc: Omit<SwaggerOption, 'include'>, route: RouteOption) => ({
...vDoc,
...route.doc,
tags: Array.from(new Set([...(vDoc.tags ?? []), ...(route.doc?.tags ?? [])])),
path: genDocPath(route.path, this.config.docuri, parent),
include: this.getRouteModules([route], parent),
});
let routeDocs: { [key: string]: SwaggerOption } = {};
const hasAdditional = (doc?: ApiDocSource) =>
doc && Object.keys(omit(doc, 'tags')).length > 0;
for (const route of routes) {
const { name, doc, children } = route;
const moduleName = parent ? `${parent}.${name}` : name;
if (hasAdditional(doc) || parent) {
this.excludeVersionModules.push(moduleName);
}
if (hasAdditional(doc)) {
routeDocs[moduleName.replace(`${option.version}.`, '')] = mergeDoc(option, route);
}
if (children) {
routeDocs = { ...routeDocs, ...this.getRouteDocs(option, children, moduleName) };
}
}
return routeDocs;
}
protected filterExcludeModules(routeModules: Type<any>[]) {
const excludeModules: Type<any>[] = [];
const excludeNames = Array.from(new Set(this.excludeVersionModules));
for (const [name, module] of Object.entries(this._modules)) {
if (excludeNames.includes(name)) {
excludeModules.push(module);
}
}
return routeModules.filter(
(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<T extends INestApplication>(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) {
return trimPath(`${prefix}${version ? `/${version.toLowerCase()}/` : '/'}${routePath}`, false);
}