add route module

This commit is contained in:
liuyi 2025-06-13 23:24:31 +08:00
parent c74757d692
commit fc9b8f7f2a
5 changed files with 295 additions and 1 deletions

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": []
}

116
src/modules/restful/base.ts Normal file
View File

@ -0,0 +1,116 @@
import { Type } from '@nestjs/common';
import { Routes } from '@nestjs/core';
import { pick } from 'lodash';
import { Configure } from '../config/configure';
import { ApiConfig, RouteOption } from './types';
import { createRouteModuleTree, getCleanRoutes, getRoutePath } from './utils';
export abstract class BaseRestful {
constructor(protected configure: Configure) {}
abstract create(_config: ApiConfig): void;
protected config!: ApiConfig;
protected _routes: Routes = [];
protected _default!: string;
protected _versions: string[] = [];
protected _modules: { [key: string]: Type<any> } = {};
get routes() {
return this._routes;
}
get default() {
return this._default;
}
get versions() {
return this._versions;
}
get modules() {
return this._modules;
}
protected createConfig(config: ApiConfig) {
if (!config.default) {
throw new Error('default api version name should be config!');
}
const versionMaps = Object.entries(config.versions)
.filter(([name]) => (config.default === name ? true : config.enabled.includes(name)))
.map(([name, version]) => [
name,
{
...pick(config, ['title', 'description', 'auth']),
...version,
tags: Array.from(new Set([...(config.tags ?? []), ...(version.tags ?? [])])),
routes: getCleanRoutes(version.routes ?? []),
},
]);
config.versions = Object.fromEntries(versionMaps);
this._versions = Object.keys(config.versions);
this._default = config.default;
if (!this._versions.includes(this._default)) {
throw new Error(`Default api version named ${this._default} not found!`);
}
this.config = config;
}
protected async createRoutes() {
const prefix = await this.configure.get<string>('app.prefix');
const versionMaps = Object.entries(this.config.versions);
this._routes = (
await Promise.all(
versionMaps.map(async ([name, version]) =>
(
await createRouteModuleTree(
this.configure,
this._modules,
version.routes ?? [],
name,
)
).map((route) => ({
...route,
path: getRoutePath(route.path, prefix, name),
})),
),
)
).reduce((o, n) => [...o, ...n], []);
const defaultVersion = this.config.versions[this._default];
this._routes = [
...this._routes,
...(
await createRouteModuleTree(
this.configure,
this._modules,
defaultVersion.routes ?? [],
)
).map((route) => ({ ...route, path: getRoutePath(route.path, prefix) })),
];
}
protected getRouteModules(routes: RouteOption[], parent?: string) {
const result = routes
.map(({ name, children }) => {
const routeName = parent ? `${parent}.${name}` : name;
let modules: Type<any>[] = [this._modules[routeName]];
if (children) {
modules = [...modules, ...this.getRouteModules(children, routeName)];
}
return modules;
})
.reduce((o, n) => [...o, ...n], [])
.filter((i) => !!i);
return result;
}
}

View File

@ -0,0 +1,78 @@
import { Type } from '@nestjs/common';
import { RouterModule } from '@nestjs/core';
import { omit } from 'lodash';
import { BaseRestful } from './base';
import { ApiConfig, ApiDocOption, ApiDocSource, RouteOption, SwaggerOption } 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),
);
}
}
export function genDocPath(routePath: string, prefix?: string, version?: string) {
return trimPath(`${prefix}${version ? `/${version.toLowerCase()}/` : '/'}${routePath}`, false);
}

View File

@ -17,7 +17,7 @@ export interface ApiDocSource {
export interface ApiConfig extends ApiDocSource { export interface ApiConfig extends ApiDocSource {
docuri?: string; docuri?: string;
default: string; default: string;
enable: string; enabled: string[];
versions: Record<string, VersionOption>; versions: Record<string, VersionOption>;
} }
@ -32,3 +32,14 @@ export interface RouteOption {
children?: RouteOption[]; children?: RouteOption[];
doc?: ApiDocSource; doc?: ApiDocSource;
} }
export interface SwaggerOption extends ApiDocSource {
version: string;
path: string;
include: Type<any>[];
}
export interface ApiDocOption {
default?: SwaggerOption;
routes?: { [key: string]: SwaggerOption };
}

View File

@ -0,0 +1,86 @@
import { Type } from '@nestjs/common';
import { Routes, RouteTree } from '@nestjs/core';
import { ApiTags } from '@nestjs/swagger';
import { camelCase, isNil, omit, trim, upperFirst } from 'lodash';
import { Configure } from '../config/configure';
import { CreateModule } from '../core/helpers';
import { CONTROLLER_DEPENDS } from './constants';
import { RouteOption } from './types';
export const trimPath = (routePath: string, addPrefix = true) =>
`${addPrefix ? '/' : ''}${trim(routePath.replace('//', '/'), '/')}`;
export const getCleanRoutes = (data: RouteOption[]): RouteOption[] =>
data.map((option) => {
const route: RouteOption = {
...omit(option, 'children'),
path: trimPath(option.path),
};
if (option.children && option.children.length > 0) {
route.children = getCleanRoutes(option.children);
} else {
delete route.children;
}
return route;
});
export function getRoutePath(routePath: string, prefix?: string, version?: string) {
const addVersion = `${version ? `/${version.toLowerCase()}/` : '/'}${routePath}`;
return isNil(prefix) ? trimPath(addVersion) : trimPath(`${prefix}${addVersion}`);
}
export function createRouteModuleTree(
configure: Configure,
modules: { [key: string]: Type<any> },
routes: RouteOption[],
parentModule?: string,
): Promise<Routes> {
return Promise.all(
routes.map(async ({ name, path, children, controllers, doc }) => {
const moduleName = parentModule ? `${parentModule}.${name}` : name;
if (Object.keys(modules).includes(moduleName)) {
throw new Error(
`route name must be unique in same level, ${moduleName} has exists`,
);
}
const depends = controllers
.map((o) => Reflect.getMetadata(CONTROLLER_DEPENDS, o) || [])
.reduce((o: Type<any>[], n) => [...o, ...n], [])
.reduce((o: Type<any>[], n: Type<any>) => {
if (o.find((i) => i === n)) {
return o;
}
return [...o, n];
}, []);
if (doc?.tags && doc.tags.length > 0) {
controllers.forEach((o) => {
!Reflect.getMetadata('swagger/apiUseTags', o) &&
ApiTags(
...doc.tags.map((tag) => (typeof tag === 'string' ? tag : tag.name)),
)(o);
});
}
const module = CreateModule(`${upperFirst(camelCase(name))}RouteModule`, () => ({
controllers,
imports: depends,
}));
modules[moduleName] = module;
const route: RouteTree = { path, module };
if (children) {
route.children = await createRouteModuleTree(
configure,
modules,
children,
moduleName,
);
}
return route;
}),
);
}