feat(gateway): extend app selector as middleware (#2761)

* feat: extend app selector as middleware

* fix: test

* chore: prevent duplicate middleware additions
This commit is contained in:
ChengLei Shao 2023-10-09 13:02:36 +08:00 committed by GitHub
parent 869f3001e4
commit caa75877ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 102 additions and 30 deletions

View File

@ -17,6 +17,43 @@ describe('gateway', () => {
await AppSupervisor.getInstance().destroy(); await AppSupervisor.getInstance().destroy();
}); });
describe('app selector', () => {
it('should get app as default main app', async () => {
expect(
await gateway.getRequestHandleAppName({
url: '/test',
headers: {},
}),
).toBe('main');
});
it('should add middleware into app selector', async () => {
gateway.addAppSelectorMiddleware(async (ctx, next) => {
ctx.resolvedAppName = 'test';
await next();
});
expect(
await gateway.getRequestHandleAppName({
url: '/test',
headers: {},
}),
).toEqual('test');
});
it('should add same middleware into app selector once', async () => {
const fn = async (ctx, next) => {
ctx.resolvedAppName = 'test';
await next();
};
gateway.addAppSelectorMiddleware(fn);
gateway.addAppSelectorMiddleware(fn);
expect(gateway.getAppSelectorMiddlewares().nodes.length).toBe(2);
});
});
describe('http api', () => { describe('http api', () => {
it('should return error when app not found', async () => { it('should return error when app not found', async () => {
const res = await supertest.agent(gateway.getCallback()).get('/api/app:getInfo'); const res = await supertest.agent(gateway.getCallback()).get('/api/app:getInfo');

View File

@ -1,4 +1,4 @@
import { uid } from '@nocobase/utils'; import { Toposort, ToposortOptions, uid } from '@nocobase/utils';
import { createStoragePluginsSymlink } from '@nocobase/utils/plugin-symlink'; import { createStoragePluginsSymlink } from '@nocobase/utils/plugin-symlink';
import { Command } from 'commander'; import { Command } from 'commander';
import compression from 'compression'; import compression from 'compression';
@ -18,6 +18,7 @@ import { applyErrorWithArgs, getErrorWithCode } from './errors';
import { IPCSocketClient } from './ipc-socket-client'; import { IPCSocketClient } from './ipc-socket-client';
import { IPCSocketServer } from './ipc-socket-server'; import { IPCSocketServer } from './ipc-socket-server';
import { WSServer } from './ws-server'; import { WSServer } from './ws-server';
import compose from 'koa-compose';
const compress = promisify(compression()); const compress = promisify(compression());
@ -27,6 +28,7 @@ export interface IncomingRequest {
} }
export type AppSelector = (req: IncomingRequest) => string | Promise<string>; export type AppSelector = (req: IncomingRequest) => string | Promise<string>;
export type AppSelectorMiddleware = (ctx: AppSelectorMiddlewareContext, next: () => Promise<void>) => void;
interface StartHttpServerOptions { interface StartHttpServerOptions {
port: number; port: number;
@ -38,12 +40,18 @@ interface RunOptions {
mainAppOptions: ApplicationOptions; mainAppOptions: ApplicationOptions;
} }
export interface AppSelectorMiddlewareContext {
req: IncomingRequest;
resolvedAppName: string | null;
}
export class Gateway extends EventEmitter { export class Gateway extends EventEmitter {
private static instance: Gateway; private static instance: Gateway;
/** /**
* use main app as default app to handle request * use main app as default app to handle request
*/ */
appSelector: AppSelector; selectorMiddlewares: Toposort<AppSelectorMiddleware> = new Toposort<AppSelectorMiddleware>();
public server: http.Server | null = null; public server: http.Server | null = null;
public ipcSocketServer: IPCSocketServer | null = null; public ipcSocketServer: IPCSocketServer | null = null;
private port: number = process.env.APP_PORT ? parseInt(process.env.APP_PORT) : null; private port: number = process.env.APP_PORT ? parseInt(process.env.APP_PORT) : null;
@ -73,18 +81,28 @@ export class Gateway extends EventEmitter {
} }
public reset() { public reset() {
this.setAppSelector(async (req) => { this.selectorMiddlewares = new Toposort<AppSelectorMiddleware>();
const appName = qs.parse(parse(req.url).query)?.__appName;
if (appName) {
return appName;
}
if (req.headers['x-app']) { this.addAppSelectorMiddleware(
return req.headers['x-app']; async (ctx: AppSelectorMiddlewareContext, next) => {
} const { req } = ctx;
const appName = qs.parse(parse(req.url).query)?.__appName as string | null;
return null; if (appName) {
}); ctx.resolvedAppName = appName;
}
if (req.headers['x-app']) {
ctx.resolvedAppName = req.headers['x-app'];
}
await next();
},
{
tag: 'core',
group: 'core',
},
);
if (this.server) { if (this.server) {
this.server.close(); this.server.close();
@ -97,8 +115,12 @@ export class Gateway extends EventEmitter {
} }
} }
setAppSelector(selector: AppSelector) { addAppSelectorMiddleware(middleware: AppSelectorMiddleware, options?: ToposortOptions) {
this.appSelector = selector; if (this.selectorMiddlewares.nodes.some((existingFunc) => existingFunc.toString() === middleware.toString())) {
return;
}
this.selectorMiddlewares.add(middleware, options);
this.emit('appSelectorChanged'); this.emit('appSelectorChanged');
} }
@ -194,8 +216,25 @@ export class Gateway extends EventEmitter {
app.callback()(req, res); app.callback()(req, res);
} }
getAppSelectorMiddlewares() {
return this.selectorMiddlewares;
}
async getRequestHandleAppName(req: IncomingRequest) { async getRequestHandleAppName(req: IncomingRequest) {
return (await this.appSelector(req)) || 'main'; const appSelectorMiddlewares = this.selectorMiddlewares.sort();
const ctx: AppSelectorMiddlewareContext = {
req,
resolvedAppName: null,
};
await compose(appSelectorMiddlewares)(ctx);
if (!ctx.resolvedAppName) {
ctx.resolvedAppName = 'main';
}
return ctx.resolvedAppName;
} }
getCallback() { getCallback() {

View File

@ -190,7 +190,7 @@ describe('multiple apps', () => {
expect(AppSupervisor.getInstance().hasApp(name)).toBeFalsy(); expect(AppSupervisor.getInstance().hasApp(name)).toBeFalsy();
Gateway.getInstance().appSelector = () => name; Gateway.getInstance().addAppSelectorMiddleware((ctx) => (ctx.resolvedAppName = name));
await AppSupervisor.getInstance().getApp(name); await AppSupervisor.getInstance().getApp(name);

View File

@ -224,21 +224,16 @@ export class PluginMultiAppManager extends Plugin {
AppSupervisor.getInstance().setAppBootstrapper(LazyLoadApplication); AppSupervisor.getInstance().setAppBootstrapper(LazyLoadApplication);
Gateway.getInstance().setAppSelector(async (req) => { Gateway.getInstance().addAppSelectorMiddleware(async (ctx, next) => {
const appName = qs.parse(parse(req.url).query)?.__appName; const { req } = ctx;
if (appName) {
return appName;
}
if (req.headers['x-app']) { if (!ctx.resolvedAppName && req.headers['x-hostname']) {
return req.headers['x-app'];
}
if (req.headers['x-hostname']) {
const repository = this.db.getRepository('applications'); const repository = this.db.getRepository('applications');
if (!repository) { if (!repository) {
return null; await next();
return;
} }
const appInstance = await repository.findOne({ const appInstance = await repository.findOne({
filter: { filter: {
cname: req.headers['x-hostname'], cname: req.headers['x-hostname'],
@ -246,11 +241,11 @@ export class PluginMultiAppManager extends Plugin {
}); });
if (appInstance) { if (appInstance) {
return appInstance.name; ctx.resolvedAppName = appInstance.name;
} }
} }
return null; await next();
}); });
this.app.on('afterStart', async (app) => { this.app.on('afterStart', async (app) => {
@ -258,8 +253,9 @@ export class PluginMultiAppManager extends Plugin {
const appSupervisor = AppSupervisor.getInstance(); const appSupervisor = AppSupervisor.getInstance();
this.app.setMaintainingMessage('starting sub applications...'); this.app.setMaintainingMessage('starting sub applications...');
if (appSupervisor.runningMode == 'single') { if (appSupervisor.runningMode == 'single') {
Gateway.getInstance().setAppSelector(() => appSupervisor.singleAppName); Gateway.getInstance().addAppSelectorMiddleware((ctx) => (ctx.resolvedAppName = appSupervisor.singleAppName));
// If the sub application is running in single mode, register the application automatically // If the sub application is running in single mode, register the application automatically
try { try {