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 {
|
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 };
|
||||||
|
}
|
||||||
|
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