feat: multiple apps admin (#1431)
* fix: dynamic routerBase * fix: start sub app with empty options * chore: sync options * fix: sub app create database * fix: test * fix: deps * feat: register app db creator * feat: default db creator * feat: app options factory * chore: api name * fix: test * fix: running sub app * fix: beforeGetApplication hook * fix: mysql test * fix: appManager get selector * chore: callback --------- Co-authored-by: Chareice <chareice@live.com>
This commit is contained in:
parent
176bd6e7ac
commit
05740672a0
@ -36,5 +36,9 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script>
|
||||
var match = location.pathname.match(/^\/apps\/([^/]*)\//);
|
||||
window.routerBase = match ? match[0] : "/";
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -24,6 +24,10 @@ export class APIClient extends APIClientSDK {
|
||||
interceptors() {
|
||||
this.axios.interceptors.request.use((config) => {
|
||||
config.headers['X-With-ACL-Meta'] = true;
|
||||
const match = location.pathname.match(/^\/apps\/([^/]*)\//);
|
||||
if (match) {
|
||||
config.headers['X-App'] = match[1];
|
||||
}
|
||||
return config;
|
||||
});
|
||||
super.interceptors();
|
||||
|
@ -8,7 +8,7 @@ type AppSelector = (req: IncomingMessage) => Application | string | undefined |
|
||||
export class AppManager extends EventEmitter {
|
||||
public applications: Map<string, Application> = new Map<string, Application>();
|
||||
|
||||
constructor(private app: Application) {
|
||||
constructor(public app: Application) {
|
||||
super();
|
||||
|
||||
app.on('beforeStop', async (mainApp, options) => {
|
||||
@ -63,10 +63,13 @@ export class AppManager extends EventEmitter {
|
||||
|
||||
callback() {
|
||||
return async (req: IncomingMessage, res: ServerResponse) => {
|
||||
let handleApp: any = this.appSelector(req) || this.app;
|
||||
const appManager = this.app.appManager;
|
||||
|
||||
let handleApp: any = appManager.appSelector(req) || appManager.app;
|
||||
|
||||
if (typeof handleApp === 'string') {
|
||||
handleApp = await this.getApplication(handleApp);
|
||||
handleApp = await appManager.getApplication(handleApp);
|
||||
|
||||
if (!handleApp) {
|
||||
res.statusCode = 404;
|
||||
return res.end(
|
||||
@ -74,7 +77,7 @@ export class AppManager extends EventEmitter {
|
||||
redirectTo: process.env.APP_NOT_FOUND_REDIRECT_TO,
|
||||
errors: [
|
||||
{
|
||||
message: 'Not Found',
|
||||
message: 'Application Not Found',
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
@ -41,6 +41,7 @@ export interface ApplicationOptions {
|
||||
acl?: boolean;
|
||||
logger?: AppLoggerOptions;
|
||||
pmSock?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface DefaultState extends KoaDefaultState {
|
||||
@ -220,6 +221,10 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
return this._logger;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.options.name || 'main';
|
||||
}
|
||||
|
||||
protected init() {
|
||||
const options = this.options;
|
||||
const logger = createAppLogger(options.logger);
|
||||
@ -523,6 +528,12 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
}
|
||||
|
||||
declare emitAsync: (event: string | symbol, ...args: any[]) => Promise<boolean>;
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
appName: this.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
applyMixins(Application, [AsyncEmitter]);
|
||||
|
1
packages/plugins/mdg
Symbolic link
1
packages/plugins/mdg
Symbolic link
@ -0,0 +1 @@
|
||||
/Users/chareice/projects/plugins-mdg/
|
@ -21,11 +21,39 @@ describe('multiple apps create', () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('should register db creator', async () => {
|
||||
const fn = jest.fn();
|
||||
|
||||
const appPlugin = app.getPlugin<PluginMultiAppManager>('PluginMultiAppManager');
|
||||
const defaultDbCreator = appPlugin.appDbCreator;
|
||||
|
||||
appPlugin.setAppDbCreator(async (app) => {
|
||||
fn();
|
||||
await defaultDbCreator(app);
|
||||
});
|
||||
|
||||
const name = `td_${uid()}`;
|
||||
await db.getRepository('applications').create({
|
||||
values: {
|
||||
name,
|
||||
options: {
|
||||
plugins: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
await app.appManager.removeApplication(name);
|
||||
|
||||
expect(fn).toBeCalled();
|
||||
});
|
||||
|
||||
it('should create application', async () => {
|
||||
const name = `td_${uid()}`;
|
||||
const miniApp = await db.getRepository('applications').create({
|
||||
values: {
|
||||
name,
|
||||
options: {
|
||||
plugins: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -37,6 +65,9 @@ describe('multiple apps create', () => {
|
||||
await db.getRepository('applications').create({
|
||||
values: {
|
||||
name,
|
||||
options: {
|
||||
plugins: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -1,27 +1,28 @@
|
||||
import Database, { IDatabaseOptions, Model, Transactionable } from '@nocobase/database';
|
||||
import { Model, Transactionable } from '@nocobase/database';
|
||||
import { Application } from '@nocobase/server';
|
||||
import lodash from 'lodash';
|
||||
import * as path from 'path';
|
||||
import { AppDbCreator, AppOptionsFactory } from '../server';
|
||||
|
||||
export interface registerAppOptions extends Transactionable {
|
||||
skipInstall?: boolean;
|
||||
dbCreator: AppDbCreator;
|
||||
appOptionsFactory: AppOptionsFactory;
|
||||
}
|
||||
|
||||
export class ApplicationModel extends Model {
|
||||
static getDatabaseConfig(app: Application): IDatabaseOptions {
|
||||
const oldConfig =
|
||||
app.options.database instanceof Database
|
||||
? (app.options.database as Database).options
|
||||
: (app.options.database as IDatabaseOptions);
|
||||
|
||||
return lodash.cloneDeep(lodash.omit(oldConfig, ['migrator']));
|
||||
}
|
||||
|
||||
static async handleAppStart(app: Application, options: registerAppOptions) {
|
||||
await app.load();
|
||||
if (!options?.skipInstall) {
|
||||
|
||||
if (!(await app.isInstalled())) {
|
||||
await app.db.sync({
|
||||
force: false,
|
||||
alter: {
|
||||
drop: false,
|
||||
},
|
||||
});
|
||||
|
||||
await app.install();
|
||||
}
|
||||
|
||||
await app.start();
|
||||
}
|
||||
|
||||
@ -31,47 +32,26 @@ export class ApplicationModel extends Model {
|
||||
|
||||
const AppModel = this.constructor as typeof ApplicationModel;
|
||||
|
||||
const createDatabase = async () => {
|
||||
const database = appName;
|
||||
const { host, port, username, password, dialect } = mainApp.db.options;
|
||||
|
||||
if (dialect === 'mysql') {
|
||||
const mysql = require('mysql2/promise');
|
||||
const connection = await mysql.createConnection({ host, port, user: username, password });
|
||||
await connection.query(`CREATE DATABASE IF NOT EXISTS \`${database}\`;`);
|
||||
await connection.close();
|
||||
}
|
||||
|
||||
if (dialect === 'postgres') {
|
||||
const { Client } = require('pg');
|
||||
|
||||
const client = new Client({
|
||||
host,
|
||||
port,
|
||||
user: username,
|
||||
password,
|
||||
database: 'postgres',
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
||||
try {
|
||||
await client.query(`CREATE DATABASE "${database}"`);
|
||||
} catch (e) {}
|
||||
|
||||
await client.end();
|
||||
}
|
||||
};
|
||||
|
||||
if (!options?.skipInstall) {
|
||||
await createDatabase();
|
||||
}
|
||||
|
||||
const app = mainApp.appManager.createApplication(appName, {
|
||||
...AppModel.initOptions(appName, mainApp),
|
||||
...options.appOptionsFactory(appName, mainApp),
|
||||
...appOptions,
|
||||
});
|
||||
|
||||
const isInstalled = await (async () => {
|
||||
try {
|
||||
return await app.isInstalled();
|
||||
} catch (e) {
|
||||
if (e.message.includes('does not exist') || e.message.includes('Unknown database')) {
|
||||
return false;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
})();
|
||||
|
||||
if (!isInstalled) {
|
||||
await options.dbCreator(app);
|
||||
}
|
||||
|
||||
await AppModel.handleAppStart(app, options);
|
||||
|
||||
await AppModel.update(
|
||||
@ -87,25 +67,4 @@ export class ApplicationModel extends Model {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static initOptions(appName: string, mainApp: Application) {
|
||||
const rawDatabaseOptions = this.getDatabaseConfig(mainApp);
|
||||
|
||||
if (rawDatabaseOptions.dialect === 'sqlite') {
|
||||
const mainAppStorage = rawDatabaseOptions.storage;
|
||||
if (mainAppStorage !== ':memory:') {
|
||||
const mainStorageDir = path.dirname(mainAppStorage);
|
||||
rawDatabaseOptions.storage = path.join(mainStorageDir, `${appName}.sqlite`);
|
||||
}
|
||||
} else {
|
||||
rawDatabaseOptions.database = appName;
|
||||
}
|
||||
|
||||
return {
|
||||
database: {
|
||||
...rawDatabaseOptions,
|
||||
tablePrefix: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,91 @@
|
||||
import { AppManager, InstallOptions, Plugin } from '@nocobase/server';
|
||||
import Application, { AppManager, InstallOptions, Plugin } from '@nocobase/server';
|
||||
import { resolve } from 'path';
|
||||
import * as path from 'path';
|
||||
import { ApplicationModel } from './models/application';
|
||||
import Database, { IDatabaseOptions, Model, Transactionable } from '@nocobase/database';
|
||||
import lodash from 'lodash';
|
||||
|
||||
export type AppDbCreator = (app: Application) => Promise<void>;
|
||||
export type AppOptionsFactory = (appName: string, mainApp: Application) => any;
|
||||
|
||||
const defaultDbCreator = async (app: Application) => {
|
||||
const databaseOptions = app.options.database as any;
|
||||
const { host, port, username, password, dialect, database } = databaseOptions;
|
||||
|
||||
if (dialect === 'mysql') {
|
||||
const mysql = require('mysql2/promise');
|
||||
const connection = await mysql.createConnection({ host, port, user: username, password });
|
||||
await connection.query(`CREATE DATABASE IF NOT EXISTS \`${database}\`;`);
|
||||
await connection.close();
|
||||
}
|
||||
|
||||
if (dialect === 'postgres') {
|
||||
const { Client } = require('pg');
|
||||
|
||||
const client = new Client({
|
||||
host,
|
||||
port,
|
||||
user: username,
|
||||
password,
|
||||
database: 'postgres',
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
||||
try {
|
||||
await client.query(`CREATE DATABASE "${database}"`);
|
||||
} catch (e) {}
|
||||
|
||||
await client.end();
|
||||
}
|
||||
};
|
||||
|
||||
const defaultAppOptionsFactory = (appName: string, mainApp: Application) => {
|
||||
const rawDatabaseOptions = PluginMultiAppManager.getDatabaseConfig(mainApp);
|
||||
|
||||
if (rawDatabaseOptions.dialect === 'sqlite') {
|
||||
const mainAppStorage = rawDatabaseOptions.storage;
|
||||
if (mainAppStorage !== ':memory:') {
|
||||
const mainStorageDir = path.dirname(mainAppStorage);
|
||||
rawDatabaseOptions.storage = path.join(mainStorageDir, `${appName}.sqlite`);
|
||||
}
|
||||
} else {
|
||||
rawDatabaseOptions.database = appName;
|
||||
}
|
||||
|
||||
return {
|
||||
database: {
|
||||
...rawDatabaseOptions,
|
||||
tablePrefix: '',
|
||||
},
|
||||
plugins: ['nocobase'],
|
||||
resourcer: {
|
||||
prefix: '/api',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export class PluginMultiAppManager extends Plugin {
|
||||
appDbCreator: AppDbCreator = defaultDbCreator;
|
||||
appOptionsFactory: AppOptionsFactory = defaultAppOptionsFactory;
|
||||
|
||||
setAppOptionsFactory(factory: AppOptionsFactory) {
|
||||
this.appOptionsFactory = factory;
|
||||
}
|
||||
|
||||
setAppDbCreator(appDbCreator: AppDbCreator) {
|
||||
this.appDbCreator = appDbCreator;
|
||||
}
|
||||
|
||||
static getDatabaseConfig(app: Application): IDatabaseOptions {
|
||||
const oldConfig =
|
||||
app.options.database instanceof Database
|
||||
? (app.options.database as Database).options
|
||||
: (app.options.database as IDatabaseOptions);
|
||||
|
||||
return lodash.cloneDeep(lodash.omit(oldConfig, ['migrator']));
|
||||
}
|
||||
|
||||
async install(options?: InstallOptions) {
|
||||
const repo = this.db.getRepository<any>('collections');
|
||||
if (repo) {
|
||||
@ -10,6 +93,12 @@ export class PluginMultiAppManager extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
beforeLoad(): void {
|
||||
this.app.appManager.setAppSelector((req) => {
|
||||
return (req.headers['x-app'] || null) as any;
|
||||
});
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.db.registerModels({
|
||||
ApplicationModel,
|
||||
@ -22,7 +111,11 @@ export class PluginMultiAppManager extends Plugin {
|
||||
this.db.on('applications.afterCreateWithAssociations', async (model: ApplicationModel, options) => {
|
||||
const { transaction } = options;
|
||||
|
||||
await model.registerToMainApp(this.app, { transaction });
|
||||
await model.registerToMainApp(this.app, {
|
||||
transaction,
|
||||
dbCreator: this.appDbCreator,
|
||||
appOptionsFactory: this.appOptionsFactory,
|
||||
});
|
||||
});
|
||||
|
||||
this.db.on('applications.afterDestroy', async (model: ApplicationModel) => {
|
||||
@ -31,7 +124,7 @@ export class PluginMultiAppManager extends Plugin {
|
||||
|
||||
this.app.appManager.on(
|
||||
'beforeGetApplication',
|
||||
async function lazyLoadApplication({ appManager, name }: { appManager: AppManager; name: string }) {
|
||||
async ({ appManager, name }: { appManager: AppManager; name: string }) => {
|
||||
if (!appManager.applications.has(name)) {
|
||||
const existsApplication = (await this.app.db.getRepository('applications').findOne({
|
||||
filter: {
|
||||
@ -40,7 +133,10 @@ export class PluginMultiAppManager extends Plugin {
|
||||
})) as ApplicationModel | null;
|
||||
|
||||
if (existsApplication) {
|
||||
await existsApplication.registerToMainApp(this.app, { skipInstall: true });
|
||||
await existsApplication.registerToMainApp(this.app, {
|
||||
dbCreator: this.appDbCreator,
|
||||
appOptionsFactory: this.appOptionsFactory,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -48,9 +144,7 @@ export class PluginMultiAppManager extends Plugin {
|
||||
|
||||
this.app.acl.registerSnippet({
|
||||
name: `pm.${this.name}.applications`,
|
||||
actions: [
|
||||
'applications:*',
|
||||
],
|
||||
actions: ['applications:*'],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@
|
||||
"@nocobase/plugin-export": "0.9.0-alpha.2",
|
||||
"@nocobase/plugin-file-manager": "0.9.0-alpha.2",
|
||||
"@nocobase/plugin-graph-collection-manager": "0.9.0-alpha.2",
|
||||
"@nocobase/plugin-multi-app-manager": "0.9.0-alpha.2",
|
||||
"@nocobase/plugin-iframe-block": "0.9.0-alpha.2",
|
||||
"@nocobase/plugin-import": "0.9.0-alpha.2",
|
||||
"@nocobase/plugin-map": "0.9.0-alpha.2",
|
||||
|
@ -32,7 +32,15 @@ export class PresetNocoBase extends Plugin {
|
||||
}
|
||||
|
||||
getLocalPlugins() {
|
||||
const localPlugins = ['sample-hello', 'oidc', 'saml', 'map', 'snapshot-field', 'graph-collection-manager'];
|
||||
const localPlugins = [
|
||||
'sample-hello',
|
||||
'multi-app-manager',
|
||||
'oidc',
|
||||
'saml',
|
||||
'map',
|
||||
'snapshot-field',
|
||||
'graph-collection-manager',
|
||||
];
|
||||
return localPlugins;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user