diff --git a/packages/app/client/src/pages/document.ejs b/packages/app/client/src/pages/document.ejs index c5197757b..9f44edc55 100644 --- a/packages/app/client/src/pages/document.ejs +++ b/packages/app/client/src/pages/document.ejs @@ -36,5 +36,9 @@
+ \ No newline at end of file diff --git a/packages/core/client/src/api-client/APIClient.ts b/packages/core/client/src/api-client/APIClient.ts index 19a5a7b91..aec01b572 100644 --- a/packages/core/client/src/api-client/APIClient.ts +++ b/packages/core/client/src/api-client/APIClient.ts @@ -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(); diff --git a/packages/core/server/src/app-manager.ts b/packages/core/server/src/app-manager.ts index fe8f8b0c7..67f85ba85 100644 --- a/packages/core/server/src/app-manager.ts +++ b/packages/core/server/src/app-manager.ts @@ -8,7 +8,7 @@ type AppSelector = (req: IncomingMessage) => Application | string | undefined | export class AppManager extends EventEmitter { public applications: Map = new Map(); - 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', }, ], }), diff --git a/packages/core/server/src/application.ts b/packages/core/server/src/application.ts index 80ac85902..b0838456b 100644 --- a/packages/core/server/src/application.ts +++ b/packages/core/server/src/application.ts @@ -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 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 exten } declare emitAsync: (event: string | symbol, ...args: any[]) => Promise; + + toJSON() { + return { + appName: this.name, + }; + } } applyMixins(Application, [AsyncEmitter]); diff --git a/packages/plugins/mdg b/packages/plugins/mdg new file mode 120000 index 000000000..3a812bbe4 --- /dev/null +++ b/packages/plugins/mdg @@ -0,0 +1 @@ +/Users/chareice/projects/plugins-mdg/ \ No newline at end of file diff --git a/packages/plugins/multi-app-manager/src/__tests__/multiple-apps.test.ts b/packages/plugins/multi-app-manager/src/__tests__/multiple-apps.test.ts index da7908a35..5b7327588 100644 --- a/packages/plugins/multi-app-manager/src/__tests__/multiple-apps.test.ts +++ b/packages/plugins/multi-app-manager/src/__tests__/multiple-apps.test.ts @@ -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'); + 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: [], + }, }, }); diff --git a/packages/plugins/multi-app-manager/src/models/application.ts b/packages/plugins/multi-app-manager/src/models/application.ts index 80d983847..1cb36c0b4 100644 --- a/packages/plugins/multi-app-manager/src/models/application.ts +++ b/packages/plugins/multi-app-manager/src/models/application.ts @@ -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: '', - }, - }; - } } diff --git a/packages/plugins/multi-app-manager/src/server.ts b/packages/plugins/multi-app-manager/src/server.ts index 7017bb14f..012e580ff 100644 --- a/packages/plugins/multi-app-manager/src/server.ts +++ b/packages/plugins/multi-app-manager/src/server.ts @@ -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; +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('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:*'], }); } } diff --git a/packages/presets/nocobase/package.json b/packages/presets/nocobase/package.json index 1b86e4eaa..325c3de14 100644 --- a/packages/presets/nocobase/package.json +++ b/packages/presets/nocobase/package.json @@ -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", diff --git a/packages/presets/nocobase/src/index.ts b/packages/presets/nocobase/src/index.ts index d68ac6d68..92c0a78dd 100644 --- a/packages/presets/nocobase/src/index.ts +++ b/packages/presets/nocobase/src/index.ts @@ -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; }