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:
parent
869f3001e4
commit
caa75877ab
@ -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');
|
||||||
|
@ -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() {
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user