2023-03-19 23:40:42 +08:00
|
|
|
import Database, { IDatabaseOptions, Transactionable } from '@nocobase/database';
|
|
|
|
import Application, { AppManager, Plugin } from '@nocobase/server';
|
2023-03-10 19:16:00 +08:00
|
|
|
import lodash from 'lodash';
|
2023-02-14 15:30:58 +08:00
|
|
|
import * as path from 'path';
|
2023-03-10 19:16:00 +08:00
|
|
|
import { resolve } from 'path';
|
2022-03-28 22:01:10 +08:00
|
|
|
import { ApplicationModel } from './models/application';
|
2023-02-14 15:30:58 +08:00
|
|
|
|
2023-03-19 23:40:42 +08:00
|
|
|
export type AppDbCreator = (app: Application, transaction?: Transactionable) => Promise<void>;
|
2023-02-14 15:30:58 +08:00
|
|
|
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',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
};
|
2022-03-28 22:01:10 +08:00
|
|
|
|
2022-04-24 20:22:50 +08:00
|
|
|
export class PluginMultiAppManager extends Plugin {
|
2023-02-14 15:30:58 +08:00
|
|
|
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']));
|
|
|
|
}
|
|
|
|
|
2023-03-10 19:16:00 +08:00
|
|
|
beforeLoad() {
|
|
|
|
this.db.registerModels({
|
|
|
|
ApplicationModel,
|
2023-02-14 15:30:58 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-03-28 22:01:10 +08:00
|
|
|
async load() {
|
2023-03-10 19:16:00 +08:00
|
|
|
this.app.appManager.setAppSelector(async (req) => {
|
|
|
|
if (req.headers['x-app']) {
|
|
|
|
return req.headers['x-app'];
|
|
|
|
}
|
|
|
|
if (req.headers['x-hostname']) {
|
|
|
|
const appInstance = await this.db.getRepository('applications').findOne({
|
|
|
|
filter: {
|
|
|
|
cname: req.headers['x-hostname'],
|
|
|
|
},
|
|
|
|
});
|
|
|
|
if (appInstance) {
|
|
|
|
return appInstance.name;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
2022-03-28 22:01:10 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
await this.db.import({
|
|
|
|
directory: resolve(__dirname, 'collections'),
|
|
|
|
});
|
|
|
|
|
2023-03-19 23:40:42 +08:00
|
|
|
// after application created
|
2022-03-28 22:01:10 +08:00
|
|
|
this.db.on('applications.afterCreateWithAssociations', async (model: ApplicationModel, options) => {
|
|
|
|
const { transaction } = options;
|
2022-04-29 20:00:50 +08:00
|
|
|
|
2023-03-19 23:40:42 +08:00
|
|
|
const subApp = model.registerToMainApp(this.app, {
|
2023-02-14 15:30:58 +08:00
|
|
|
appOptionsFactory: this.appOptionsFactory,
|
|
|
|
});
|
2023-03-19 23:40:42 +08:00
|
|
|
|
|
|
|
// create database
|
|
|
|
await this.appDbCreator(subApp, transaction);
|
|
|
|
|
|
|
|
// reload subApp plugin
|
|
|
|
await subApp.reload();
|
|
|
|
|
|
|
|
// sync subApp collections
|
|
|
|
await subApp.db.sync();
|
|
|
|
|
|
|
|
// install subApp
|
|
|
|
await subApp.install();
|
|
|
|
|
|
|
|
await subApp.reload();
|
2022-03-28 22:01:10 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
this.db.on('applications.afterDestroy', async (model: ApplicationModel) => {
|
|
|
|
await this.app.appManager.removeApplication(model.get('name') as string);
|
|
|
|
});
|
|
|
|
|
2023-03-19 23:40:42 +08:00
|
|
|
// lazy load application
|
|
|
|
// if application not in appManager, load it from database
|
|
|
|
this.app.on(
|
2022-03-28 22:01:10 +08:00
|
|
|
'beforeGetApplication',
|
2023-03-19 23:40:42 +08:00
|
|
|
async ({ appManager, name, options }: { appManager: AppManager; name: string; options: any }) => {
|
|
|
|
if (appManager.applications.has(name)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const applicationRecord = (await this.app.db.getRepository('applications').findOne({
|
|
|
|
filter: {
|
|
|
|
name,
|
|
|
|
},
|
|
|
|
})) as ApplicationModel | null;
|
|
|
|
|
|
|
|
if (!applicationRecord) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const subApp = await applicationRecord.registerToMainApp(this.app, {
|
|
|
|
appOptionsFactory: this.appOptionsFactory,
|
|
|
|
});
|
|
|
|
|
|
|
|
// must skip load on upgrade
|
|
|
|
if (!options?.upgrading) {
|
|
|
|
await subApp.load();
|
2022-03-28 22:01:10 +08:00
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
2023-01-09 07:35:48 +08:00
|
|
|
|
2023-03-19 23:40:42 +08:00
|
|
|
this.app.on('afterUpgrade', async (app, options) => {
|
|
|
|
const cliArgs = options?.cliArgs;
|
|
|
|
const repository = this.db.getRepository('applications');
|
|
|
|
const instances = await repository.find();
|
|
|
|
for (const instance of instances) {
|
|
|
|
const subApp = await this.app.appManager.getApplication(instance.name, {
|
|
|
|
upgrading: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
try {
|
|
|
|
console.log(`${instance.name}: upgrading...`);
|
|
|
|
|
|
|
|
await subApp.upgrade({
|
|
|
|
cliArgs,
|
|
|
|
});
|
|
|
|
|
|
|
|
await subApp.stop({
|
|
|
|
cliArgs,
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
console.log(`${instance.name}: upgrade failed`);
|
|
|
|
this.app.logger.error(error);
|
|
|
|
console.error(error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2023-03-10 19:16:00 +08:00
|
|
|
this.app.resourcer.registerActionHandlers({
|
|
|
|
'applications:listPinned': async (ctx, next) => {
|
|
|
|
const items = await this.db.getRepository('applications').find({
|
|
|
|
filter: {
|
|
|
|
pinned: true,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
ctx.body = items;
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
this.app.acl.allow('applications', 'listPinned', 'loggedIn');
|
|
|
|
|
2023-01-09 07:35:48 +08:00
|
|
|
this.app.acl.registerSnippet({
|
2023-03-19 23:40:42 +08:00
|
|
|
name: `
|
|
|
|
pm.$;
|
|
|
|
{
|
|
|
|
this.name;
|
|
|
|
}
|
|
|
|
.
|
|
|
|
applications`,
|
2023-02-14 15:30:58 +08:00
|
|
|
actions: ['applications:*'],
|
2023-01-09 07:35:48 +08:00
|
|
|
});
|
2022-03-28 22:01:10 +08:00
|
|
|
}
|
|
|
|
}
|