add route module
This commit is contained in:
parent
c74757d692
commit
fc9b8f7f2a
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": []
|
||||
}
|
116
src/modules/restful/base.ts
Normal file
116
src/modules/restful/base.ts
Normal 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;
|
||||
}
|
||||
}
|
78
src/modules/restful/restful.ts
Normal file
78
src/modules/restful/restful.ts
Normal 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);
|
||||
}
|
@ -17,7 +17,7 @@ export interface ApiDocSource {
|
||||
export interface ApiConfig extends ApiDocSource {
|
||||
docuri?: string;
|
||||
default: string;
|
||||
enable: string;
|
||||
enabled: string[];
|
||||
versions: Record<string, VersionOption>;
|
||||
}
|
||||
|
||||
@ -32,3 +32,14 @@ export interface RouteOption {
|
||||
children?: RouteOption[];
|
||||
doc?: ApiDocSource;
|
||||
}
|
||||
|
||||
export interface SwaggerOption extends ApiDocSource {
|
||||
version: string;
|
||||
path: string;
|
||||
include: Type<any>[];
|
||||
}
|
||||
|
||||
export interface ApiDocOption {
|
||||
default?: SwaggerOption;
|
||||
routes?: { [key: string]: SwaggerOption };
|
||||
}
|
||||
|
86
src/modules/restful/utils.ts
Normal file
86
src/modules/restful/utils.ts
Normal 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;
|
||||
}),
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user