2023-08-24 17:47:45 +08:00
|
|
|
import { Database, IDatabaseOptions, Transactionable } from '@nocobase/database';
|
|
|
|
import Application, { AppSupervisor, Gateway, Plugin } from '@nocobase/server';
|
2023-06-06 10:47:50 +08:00
|
|
|
import { Mutex } from 'async-mutex';
|
2023-08-24 17:47:45 +08:00
|
|
|
import lodash from 'lodash';
|
|
|
|
import path, { resolve } from 'path';
|
|
|
|
import { ApplicationModel } from '../server';
|
2023-02-14 15:30:58 +08:00
|
|
|
|
2023-10-21 22:50:40 +08:00
|
|
|
export type AppDbCreator = (app: Application, options?: Transactionable & { context?: any }) => Promise<void>;
|
2023-02-14 15:30:58 +08:00
|
|
|
export type AppOptionsFactory = (appName: string, mainApp: Application) => any;
|
2023-09-27 15:16:15 +08:00
|
|
|
export type SubAppUpgradeHandler = (mainApp: Application) => Promise<void>;
|
|
|
|
|
|
|
|
const defaultSubAppUpgradeHandle: SubAppUpgradeHandler = async (mainApp: Application) => {
|
|
|
|
const repository = mainApp.db.getRepository('applications');
|
|
|
|
const findOptions = {};
|
|
|
|
|
|
|
|
const appSupervisor = AppSupervisor.getInstance();
|
|
|
|
|
|
|
|
if (appSupervisor.runningMode == 'single') {
|
|
|
|
findOptions['filter'] = {
|
|
|
|
name: appSupervisor.singleAppName,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
const instances = await repository.find(findOptions);
|
|
|
|
|
|
|
|
for (const instance of instances) {
|
|
|
|
const instanceOptions = instance.get('options');
|
|
|
|
|
|
|
|
// skip standalone deployment application
|
|
|
|
if (instanceOptions?.standaloneDeployment && appSupervisor.runningMode !== 'single') {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const beforeSubAppStatus = AppSupervisor.getInstance().getAppStatus(instance.name);
|
|
|
|
|
|
|
|
const subApp = await appSupervisor.getApp(instance.name, {
|
|
|
|
upgrading: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
console.log({ beforeSubAppStatus });
|
|
|
|
try {
|
|
|
|
mainApp.setMaintainingMessage(`upgrading sub app ${instance.name}...`);
|
|
|
|
console.log(`${instance.name}: upgrading...`);
|
|
|
|
|
|
|
|
await subApp.runAsCLI(['upgrade'], { from: 'user' });
|
|
|
|
if (!beforeSubAppStatus && AppSupervisor.getInstance().getAppStatus(instance.name) === 'initialized') {
|
|
|
|
await AppSupervisor.getInstance().removeApp(instance.name);
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.log(`${instance.name}: upgrade failed`);
|
|
|
|
mainApp.logger.error(error);
|
|
|
|
console.error(error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
2023-02-14 15:30:58 +08:00
|
|
|
|
|
|
|
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}"`);
|
2023-08-24 17:47:45 +08:00
|
|
|
} catch (e) {
|
|
|
|
console.log(e);
|
|
|
|
}
|
2023-02-14 15:30:58 +08:00
|
|
|
|
|
|
|
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;
|
2023-09-27 15:16:15 +08:00
|
|
|
subAppUpgradeHandler: SubAppUpgradeHandler = defaultSubAppUpgradeHandle;
|
2023-02-14 15:30:58 +08:00
|
|
|
|
2023-06-06 10:47:50 +08:00
|
|
|
private beforeGetApplicationMutex = new Mutex();
|
|
|
|
|
2023-02-14 15:30:58 +08:00
|
|
|
static getDatabaseConfig(app: Application): IDatabaseOptions {
|
2023-10-17 22:22:45 +08:00
|
|
|
let oldConfig =
|
2023-02-14 15:30:58 +08:00
|
|
|
app.options.database instanceof Database
|
|
|
|
? (app.options.database as Database).options
|
|
|
|
: (app.options.database as IDatabaseOptions);
|
|
|
|
|
2023-10-17 22:22:45 +08:00
|
|
|
if (!oldConfig && app.db) {
|
|
|
|
oldConfig = app.db.options;
|
|
|
|
}
|
2023-02-14 15:30:58 +08:00
|
|
|
return lodash.cloneDeep(lodash.omit(oldConfig, ['migrator']));
|
|
|
|
}
|
|
|
|
|
2023-09-27 15:16:15 +08:00
|
|
|
setSubAppUpgradeHandler(handler: SubAppUpgradeHandler) {
|
|
|
|
this.subAppUpgradeHandler = handler;
|
|
|
|
}
|
|
|
|
|
2023-08-24 17:47:45 +08:00
|
|
|
setAppOptionsFactory(factory: AppOptionsFactory) {
|
|
|
|
this.appOptionsFactory = factory;
|
|
|
|
}
|
|
|
|
|
|
|
|
setAppDbCreator(appDbCreator: AppDbCreator) {
|
|
|
|
this.appDbCreator = appDbCreator;
|
|
|
|
}
|
|
|
|
|
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() {
|
|
|
|
await this.db.import({
|
|
|
|
directory: resolve(__dirname, 'collections'),
|
|
|
|
});
|
|
|
|
|
2023-03-19 23:40:42 +08:00
|
|
|
// after application created
|
2023-10-21 22:50:40 +08:00
|
|
|
this.db.on(
|
|
|
|
'applications.afterCreateWithAssociations',
|
|
|
|
async (model: ApplicationModel, options: Transactionable & { context?: any }) => {
|
|
|
|
const { transaction } = options;
|
|
|
|
|
|
|
|
const subApp = model.registerToSupervisor(this.app, {
|
|
|
|
appOptionsFactory: this.appOptionsFactory,
|
|
|
|
});
|
|
|
|
|
|
|
|
// create database
|
|
|
|
await this.appDbCreator(subApp, {
|
|
|
|
transaction,
|
|
|
|
context: options.context,
|
|
|
|
});
|
|
|
|
|
|
|
|
const startPromise = subApp.runAsCLI(['start', '--quickstart'], { from: 'user' });
|
|
|
|
|
|
|
|
if (options?.context?.waitSubAppInstall) {
|
|
|
|
await startPromise;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
2022-03-28 22:01:10 +08:00
|
|
|
|
|
|
|
this.db.on('applications.afterDestroy', async (model: ApplicationModel) => {
|
2023-08-24 17:47:45 +08:00
|
|
|
await AppSupervisor.getInstance().removeApp(model.get('name') as string);
|
2022-03-28 22:01:10 +08:00
|
|
|
});
|
|
|
|
|
2023-08-24 17:47:45 +08:00
|
|
|
const self = this;
|
|
|
|
|
|
|
|
async function LazyLoadApplication({
|
|
|
|
appSupervisor,
|
|
|
|
appName,
|
|
|
|
options,
|
|
|
|
}: {
|
|
|
|
appSupervisor: AppSupervisor;
|
|
|
|
appName: string;
|
|
|
|
options: any;
|
|
|
|
}) {
|
|
|
|
const name = appName;
|
|
|
|
if (appSupervisor.hasApp(name)) {
|
|
|
|
return;
|
|
|
|
}
|
2023-06-06 10:47:50 +08:00
|
|
|
|
2023-08-24 17:47:45 +08:00
|
|
|
const applicationRecord = (await self.app.db.getRepository('applications').findOne({
|
|
|
|
filter: {
|
|
|
|
name,
|
|
|
|
},
|
|
|
|
})) as ApplicationModel | null;
|
2023-06-06 10:47:50 +08:00
|
|
|
|
2023-08-24 17:47:45 +08:00
|
|
|
if (!applicationRecord) {
|
|
|
|
return;
|
|
|
|
}
|
2023-06-06 10:47:50 +08:00
|
|
|
|
2023-08-24 17:47:45 +08:00
|
|
|
const instanceOptions = applicationRecord.get('options');
|
2023-06-06 10:47:50 +08:00
|
|
|
|
2023-08-24 17:47:45 +08:00
|
|
|
if (instanceOptions?.standaloneDeployment && appSupervisor.runningMode !== 'single') {
|
|
|
|
return;
|
|
|
|
}
|
2023-06-06 10:47:50 +08:00
|
|
|
|
2023-08-24 17:47:45 +08:00
|
|
|
if (!applicationRecord) {
|
|
|
|
return;
|
|
|
|
}
|
2023-06-06 10:47:50 +08:00
|
|
|
|
2023-08-24 17:47:45 +08:00
|
|
|
const subApp = applicationRecord.registerToSupervisor(self.app, {
|
|
|
|
appOptionsFactory: self.appOptionsFactory,
|
|
|
|
});
|
|
|
|
|
|
|
|
// must skip load on upgrade
|
|
|
|
if (!options?.upgrading) {
|
|
|
|
await subApp.runCommand('start');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
AppSupervisor.getInstance().setAppBootstrapper(LazyLoadApplication);
|
|
|
|
|
2023-10-09 13:02:36 +08:00
|
|
|
Gateway.getInstance().addAppSelectorMiddleware(async (ctx, next) => {
|
|
|
|
const { req } = ctx;
|
2023-08-24 17:47:45 +08:00
|
|
|
|
2023-10-09 13:02:36 +08:00
|
|
|
if (!ctx.resolvedAppName && req.headers['x-hostname']) {
|
2023-08-24 17:47:45 +08:00
|
|
|
const repository = this.db.getRepository('applications');
|
|
|
|
if (!repository) {
|
2023-10-09 13:02:36 +08:00
|
|
|
await next();
|
|
|
|
return;
|
2023-08-24 17:47:45 +08:00
|
|
|
}
|
2023-10-09 13:02:36 +08:00
|
|
|
|
2023-08-24 17:47:45 +08:00
|
|
|
const appInstance = await repository.findOne({
|
|
|
|
filter: {
|
|
|
|
cname: req.headers['x-hostname'],
|
|
|
|
},
|
2023-03-19 23:40:42 +08:00
|
|
|
});
|
2023-08-24 17:47:45 +08:00
|
|
|
|
|
|
|
if (appInstance) {
|
2023-10-09 13:02:36 +08:00
|
|
|
ctx.resolvedAppName = appInstance.name;
|
2023-08-24 17:47:45 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-09 13:02:36 +08:00
|
|
|
await next();
|
2023-08-24 17:47:45 +08:00
|
|
|
});
|
2023-01-09 07:35:48 +08:00
|
|
|
|
2023-05-26 09:55:11 +08:00
|
|
|
this.app.on('afterStart', async (app) => {
|
|
|
|
const repository = this.db.getRepository('applications');
|
2023-08-24 17:47:45 +08:00
|
|
|
const appSupervisor = AppSupervisor.getInstance();
|
|
|
|
|
|
|
|
this.app.setMaintainingMessage('starting sub applications...');
|
2023-10-09 13:02:36 +08:00
|
|
|
|
2023-08-24 17:47:45 +08:00
|
|
|
if (appSupervisor.runningMode == 'single') {
|
2023-10-09 13:02:36 +08:00
|
|
|
Gateway.getInstance().addAppSelectorMiddleware((ctx) => (ctx.resolvedAppName = appSupervisor.singleAppName));
|
2023-08-24 17:47:45 +08:00
|
|
|
|
2023-05-26 09:55:11 +08:00
|
|
|
// If the sub application is running in single mode, register the application automatically
|
|
|
|
try {
|
2023-08-24 17:47:45 +08:00
|
|
|
await AppSupervisor.getInstance().getApp(appSupervisor.singleAppName);
|
2023-05-26 09:55:11 +08:00
|
|
|
} catch (err) {
|
2023-08-24 17:47:45 +08:00
|
|
|
console.error('Auto register sub application in single mode failed: ', appSupervisor.singleAppName, err);
|
2023-05-26 09:55:11 +08:00
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
2023-08-24 17:47:45 +08:00
|
|
|
|
2023-05-26 09:55:11 +08:00
|
|
|
try {
|
|
|
|
const subApps = await repository.find({
|
|
|
|
filter: {
|
|
|
|
'options.autoStart': true,
|
|
|
|
},
|
|
|
|
});
|
2023-08-24 17:47:45 +08:00
|
|
|
|
|
|
|
const promises = [];
|
|
|
|
|
|
|
|
for (const subAppInstance of subApps) {
|
|
|
|
promises.push(
|
|
|
|
(async () => {
|
|
|
|
await AppSupervisor.getInstance().getApp(subAppInstance.name);
|
|
|
|
})(),
|
|
|
|
);
|
2023-05-26 09:55:11 +08:00
|
|
|
}
|
2023-08-24 17:47:45 +08:00
|
|
|
|
|
|
|
await Promise.all(promises);
|
2023-05-26 09:55:11 +08:00
|
|
|
} catch (err) {
|
|
|
|
console.error('Auto register sub applications failed: ', err);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2023-03-19 23:40:42 +08:00
|
|
|
this.app.on('afterUpgrade', async (app, options) => {
|
2023-09-27 15:16:15 +08:00
|
|
|
await this.subAppUpgradeHandler(app);
|
2023-03-19 23:40:42 +08:00
|
|
|
});
|
|
|
|
|
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-09-05 12:03: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
|
|
|
});
|
2023-08-24 17:47:45 +08:00
|
|
|
|
|
|
|
this.app.resourcer.use(async (ctx, next) => {
|
|
|
|
await next();
|
|
|
|
const { actionName, resourceName, params } = ctx.action;
|
|
|
|
if (actionName === 'list' && resourceName === 'applications') {
|
|
|
|
const applications = ctx.body.rows;
|
|
|
|
for (const application of applications) {
|
|
|
|
const appStatus = AppSupervisor.getInstance().getAppStatus(application.name, 'stopped');
|
|
|
|
application.status = appStatus;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2022-03-28 22:01:10 +08:00
|
|
|
}
|
|
|
|
}
|