featPlugin multiple apps (#248)
* feat: multiple apps plugin * feat: multipleAppManager in Application * stage * fix: export error * test: multiple app * application model * feat: create application with plugins * load and install after sub application created * create subApp database beforeInstall * sub apps listen to main app start & stop events * refactor: getPluginName as package name * feat: load apps on mainApp starts * fix: test * feat: beforeGetApplication event * fix: test * fix: test with sqlite memory database * test: lazyLoad application * fix: test with sqlite memory * chore: clone database collection & promise.all
This commit is contained in:
parent
c0a33b6e3e
commit
81978711e4
@ -14,6 +14,9 @@ describe('database', () => {
|
||||
});
|
||||
|
||||
test('close state', async () => {
|
||||
if (db.isSqliteMemory()) {
|
||||
return;
|
||||
}
|
||||
expect(db.closed()).toBeFalsy();
|
||||
await db.close();
|
||||
expect(db.closed()).toBeTruthy();
|
||||
|
@ -11,6 +11,7 @@ export type RepositoryType = typeof Repository;
|
||||
|
||||
export type CollectionSortable = string | boolean | { name?: string; scopeKey?: string };
|
||||
|
||||
|
||||
export interface CollectionOptions extends Omit<ModelOptions, 'name' | 'hooks'> {
|
||||
name: string;
|
||||
tableName?: string;
|
||||
|
@ -1,15 +1,7 @@
|
||||
import { applyMixins, AsyncEmitter } from '@nocobase/utils';
|
||||
import merge from 'deepmerge';
|
||||
import { EventEmitter } from 'events';
|
||||
import {
|
||||
ModelCtor,
|
||||
Op,
|
||||
Options,
|
||||
QueryInterfaceDropAllTablesOptions,
|
||||
Sequelize,
|
||||
SyncOptions,
|
||||
Utils
|
||||
} from 'sequelize';
|
||||
import { ModelCtor, Op, Options, QueryInterfaceDropAllTablesOptions, Sequelize, SyncOptions, Utils } from 'sequelize';
|
||||
import { Collection, CollectionOptions, RepositoryType } from './collection';
|
||||
import { ImporterReader, ImportFileExtension } from './collection-importer';
|
||||
import * as FieldTypes from './fields';
|
||||
@ -19,6 +11,7 @@ import { ModelHook } from './model-hook';
|
||||
import extendOperators from './operators';
|
||||
import { RelationRepository } from './relation-repository/relation-repository';
|
||||
import { Repository } from './repository';
|
||||
import lodash from 'lodash';
|
||||
|
||||
export interface MergeOptions extends merge.Options {}
|
||||
|
||||
@ -250,7 +243,14 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
public isSqliteMemory() {
|
||||
return this.sequelize.getDialect() === 'sqlite' && lodash.get(this.options, 'storage') == ':memory:';
|
||||
}
|
||||
|
||||
async reconnect() {
|
||||
if (this.isSqliteMemory()) {
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
const ConnectionManager = this.sequelize.dialect.connectionManager.constructor;
|
||||
// @ts-ignore
|
||||
@ -267,6 +267,10 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (this.isSqliteMemory()) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.sequelize.close();
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ export class MockDatabase extends Database {
|
||||
super({
|
||||
storage: ':memory:',
|
||||
tablePrefix: `mock_${uid(6)}_`,
|
||||
dialect: 'sqlite',
|
||||
...options,
|
||||
});
|
||||
this.sequelize.beforeDefine((model, opts) => {
|
||||
@ -24,7 +25,10 @@ export function getConfigByEnv() {
|
||||
port: process.env.DB_PORT,
|
||||
dialect: process.env.DB_DIALECT,
|
||||
logging: process.env.DB_LOG_SQL === 'on' ? console.log : false,
|
||||
storage: process.env.DB_STORAGE && process.env.DB_STORAGE !== ':memory:' ? resolve(process.cwd(), process.env.DB_STORAGE) : ':memory:',
|
||||
storage:
|
||||
process.env.DB_STORAGE && process.env.DB_STORAGE !== ':memory:'
|
||||
? resolve(process.cwd(), process.env.DB_STORAGE)
|
||||
: ':memory:',
|
||||
define: {
|
||||
charset: 'utf8mb4',
|
||||
collate: 'utf8mb4_unicode_ci',
|
||||
@ -33,5 +37,6 @@ export function getConfigByEnv() {
|
||||
}
|
||||
|
||||
export function mockDatabase(options: IDatabaseOptions = {}): MockDatabase {
|
||||
return new MockDatabase(merge(getConfigByEnv(), options));
|
||||
const dbOptions = merge(getConfigByEnv(), options);
|
||||
return new MockDatabase(dbOptions);
|
||||
}
|
||||
|
@ -278,6 +278,10 @@ export class PluginACL extends Plugin {
|
||||
|
||||
this.app.resourcer.use(this.acl.middleware());
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return this.getPackageName(__dirname);
|
||||
}
|
||||
}
|
||||
|
||||
export default PluginACL;
|
||||
|
@ -14,4 +14,8 @@ export default class PluginActionLogs extends Plugin {
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return this.getPackageName(__dirname);
|
||||
}
|
||||
}
|
||||
|
@ -62,6 +62,10 @@ export class ChinaRegionPlugin extends Plugin {
|
||||
const count = await ChinaRegion.count();
|
||||
console.log(`${count} rows of region data imported in ${(Date.now() - timer) / 1000}s`);
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return this.getPackageName(__dirname);
|
||||
}
|
||||
}
|
||||
|
||||
export default ChinaRegionPlugin;
|
||||
|
@ -60,6 +60,10 @@ export class ClientPlugin extends Plugin {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return this.getPackageName(__dirname);
|
||||
}
|
||||
}
|
||||
|
||||
export default ClientPlugin;
|
||||
|
@ -86,6 +86,10 @@ export class CollectionManagerPlugin extends Plugin {
|
||||
directory: path.resolve(__dirname, './collections'),
|
||||
});
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return this.getPackageName(__dirname);
|
||||
}
|
||||
}
|
||||
|
||||
export default CollectionManagerPlugin;
|
||||
|
@ -7,6 +7,10 @@ import enUS from './locale/en_US';
|
||||
import zhCN from './locale/zh_CN';
|
||||
|
||||
export class PluginErrorHandler extends Plugin {
|
||||
getName(): string {
|
||||
return this.getPackageName(__dirname);
|
||||
}
|
||||
|
||||
errorHandler: ErrorHandler = new ErrorHandler();
|
||||
i18nNs: string = 'error-handler';
|
||||
|
||||
|
@ -36,4 +36,8 @@ export default class PluginFileManager extends Plugin {
|
||||
await getStorageConfig(STORAGE_TYPE_LOCAL).middleware(this.app);
|
||||
}
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return this.getPackageName(__dirname);
|
||||
}
|
||||
}
|
||||
|
7
packages/plugin-multiple-apps/.npmignore
Normal file
7
packages/plugin-multiple-apps/.npmignore
Normal file
@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
*.log
|
||||
docs
|
||||
__tests__
|
||||
tsconfig.json
|
||||
src
|
||||
.fatherrc.ts
|
16
packages/plugin-multiple-apps/package.json
Normal file
16
packages/plugin-multiple-apps/package.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-multiple-apps",
|
||||
"version": "0.6.0-alpha.0",
|
||||
"main": "lib/index.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "rimraf -rf lib esm dist && npm run build:cjs && npm run build:esm",
|
||||
"build:cjs": "tsc --project tsconfig.build.json",
|
||||
"build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir esm"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nocobase/server": "^0.6.0-alpha.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
}
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
import { Plugin } from '@nocobase/server';
|
||||
import { ApplicationModel } from '../models/application';
|
||||
import { mockServer } from '@nocobase/test';
|
||||
import { PluginMultipleApps } from '../server';
|
||||
|
||||
describe('test with start', () => {
|
||||
it('should load subApp on create', async () => {
|
||||
const loadFn = jest.fn();
|
||||
const installFn = jest.fn();
|
||||
|
||||
class TestPlugin extends Plugin {
|
||||
getName(): string {
|
||||
return 'test-package';
|
||||
}
|
||||
|
||||
async load(): Promise<void> {
|
||||
loadFn();
|
||||
}
|
||||
|
||||
async install() {
|
||||
installFn();
|
||||
}
|
||||
}
|
||||
|
||||
const mockGetPluginByName = jest.fn();
|
||||
mockGetPluginByName.mockReturnValue(TestPlugin);
|
||||
ApplicationModel.getPluginByName = mockGetPluginByName;
|
||||
|
||||
const app = mockServer();
|
||||
await app.cleanDb();
|
||||
app.plugin(PluginMultipleApps);
|
||||
|
||||
await app.loadAndInstall();
|
||||
await app.start();
|
||||
|
||||
const db = app.db;
|
||||
|
||||
await db.getRepository('applications').create({
|
||||
values: {
|
||||
name: 'sub1',
|
||||
plugins: [
|
||||
{
|
||||
name: 'test-package',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(loadFn).toHaveBeenCalledTimes(1);
|
||||
expect(installFn).toHaveBeenCalledTimes(1);
|
||||
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('should install into difference database', async () => {
|
||||
const app = mockServer();
|
||||
await app.cleanDb();
|
||||
app.plugin(PluginMultipleApps);
|
||||
|
||||
await app.loadAndInstall();
|
||||
await app.start();
|
||||
|
||||
const db = app.db;
|
||||
|
||||
await db.getRepository('applications').create({
|
||||
values: {
|
||||
name: 'sub1',
|
||||
plugins: [
|
||||
{
|
||||
name: '@nocobase/plugin-ui-schema-storage',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('should lazy load applications', async () => {
|
||||
class TestPlugin extends Plugin {
|
||||
getName(): string {
|
||||
return 'test-package';
|
||||
}
|
||||
}
|
||||
|
||||
let app = mockServer();
|
||||
await app.cleanDb();
|
||||
app.plugin(PluginMultipleApps);
|
||||
|
||||
await app.loadAndInstall();
|
||||
await app.start();
|
||||
|
||||
const db = app.db;
|
||||
|
||||
const mockGetPluginByName = jest.fn();
|
||||
mockGetPluginByName.mockReturnValue(TestPlugin);
|
||||
ApplicationModel.getPluginByName = mockGetPluginByName;
|
||||
|
||||
await db.getRepository('applications').create({
|
||||
values: {
|
||||
name: 'sub1',
|
||||
plugins: [
|
||||
{
|
||||
name: 'test-package',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(app.appManager.applications.get('sub1')).toBeDefined();
|
||||
|
||||
await app.stop();
|
||||
|
||||
let newApp = mockServer({
|
||||
database: app.db,
|
||||
});
|
||||
|
||||
newApp.plugin(PluginMultipleApps);
|
||||
await newApp.db.reconnect();
|
||||
|
||||
await newApp.load();
|
||||
await newApp.start();
|
||||
|
||||
expect(await newApp.db.getRepository('applications').count()).toEqual(1);
|
||||
expect(newApp.appManager.applications.get('sub1')).not.toBeDefined();
|
||||
|
||||
newApp.appManager.setAppSelector(() => {
|
||||
return 'sub1';
|
||||
});
|
||||
|
||||
await newApp.agent().resource('test').test();
|
||||
expect(newApp.appManager.applications.get('sub1')).toBeDefined();
|
||||
|
||||
await app.destroy();
|
||||
});
|
||||
});
|
@ -0,0 +1,92 @@
|
||||
import { mockServer, MockServer } from '@nocobase/test';
|
||||
import { Database } from '@nocobase/database';
|
||||
import { PluginMultipleApps } from '../server';
|
||||
|
||||
describe('multiple apps create', () => {
|
||||
let app: MockServer;
|
||||
let db: Database;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = mockServer({});
|
||||
db = app.db;
|
||||
await app.cleanDb();
|
||||
app.plugin(PluginMultipleApps);
|
||||
|
||||
await app.loadAndInstall();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('should create application', async () => {
|
||||
const miniApp = await db.getRepository('applications').create({
|
||||
values: {
|
||||
name: 'miniApp',
|
||||
},
|
||||
});
|
||||
|
||||
expect(app.appManager.applications.get('miniApp')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should remove application', async () => {
|
||||
await db.getRepository('applications').create({
|
||||
values: {
|
||||
name: 'miniApp',
|
||||
},
|
||||
});
|
||||
|
||||
expect(app.appManager.applications.get('miniApp')).toBeDefined();
|
||||
|
||||
await db.getRepository('applications').destroy({
|
||||
filter: {
|
||||
name: 'miniApp',
|
||||
},
|
||||
});
|
||||
|
||||
expect(app.appManager.applications.get('miniApp')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create with plugins', async () => {
|
||||
await db.getRepository('applications').create({
|
||||
values: {
|
||||
name: 'miniApp',
|
||||
plugins: [
|
||||
{
|
||||
name: '@nocobase/plugin-ui-schema-storage',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const miniApp = app.appManager.applications.get('miniApp');
|
||||
expect(miniApp).toBeDefined();
|
||||
|
||||
expect(miniApp.pm.get('@nocobase/plugin-ui-schema-storage')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should lazy load applications', async () => {
|
||||
await db.getRepository('applications').create({
|
||||
values: {
|
||||
name: 'miniApp',
|
||||
plugins: [
|
||||
{
|
||||
name: '@nocobase/plugin-ui-schema-storage',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await app.appManager.removeApplication('miniApp');
|
||||
|
||||
app.appManager.setAppSelector(() => {
|
||||
return 'miniApp';
|
||||
});
|
||||
|
||||
expect(app.appManager.applications.has('miniApp')).toBeFalsy();
|
||||
|
||||
await app.agent().resource('test').test();
|
||||
|
||||
expect(app.appManager.applications.has('miniApp')).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,17 @@
|
||||
import { defineCollection } from '@nocobase/database';
|
||||
|
||||
export default defineCollection({
|
||||
name: 'applicationPlugins',
|
||||
timestamps: false,
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'application',
|
||||
foreignKey: 'applicationName',
|
||||
},
|
||||
],
|
||||
});
|
@ -0,0 +1,24 @@
|
||||
import { defineCollection } from '@nocobase/database';
|
||||
|
||||
export default defineCollection({
|
||||
name: 'applications',
|
||||
model: 'ApplicationModel',
|
||||
autoGenId: false,
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
primaryKey: true,
|
||||
},
|
||||
{
|
||||
type: 'json',
|
||||
name: 'options',
|
||||
},
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'plugins',
|
||||
target: 'applicationPlugins',
|
||||
foreignKey: 'applicationName',
|
||||
},
|
||||
],
|
||||
});
|
92
packages/plugin-multiple-apps/src/models/application.ts
Normal file
92
packages/plugin-multiple-apps/src/models/application.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import Database, { IDatabaseOptions, Model, TransactionAble } from '@nocobase/database';
|
||||
import { Application } from '@nocobase/server';
|
||||
import lodash from 'lodash';
|
||||
import * as path from 'path';
|
||||
|
||||
interface registerAppOptions extends TransactionAble {
|
||||
skipInstall?: boolean;
|
||||
}
|
||||
|
||||
export class ApplicationModel extends Model {
|
||||
static getPluginByName(pluginName: string) {
|
||||
return require(pluginName).default;
|
||||
}
|
||||
|
||||
static getDatabaseConfig(app: Application): IDatabaseOptions {
|
||||
return lodash.cloneDeep(
|
||||
lodash.isPlainObject(app.options.database)
|
||||
? (app.options.database as IDatabaseOptions)
|
||||
: (app.options.database as Database).options,
|
||||
);
|
||||
}
|
||||
|
||||
async registerToMainApp(mainApp: Application, options: registerAppOptions) {
|
||||
const { transaction } = options;
|
||||
const appName = this.get('name') as string;
|
||||
const app = mainApp.appManager.createApplication(appName, ApplicationModel.initOptions(appName, mainApp));
|
||||
|
||||
// @ts-ignore
|
||||
const plugins = await this.getPlugins({ transaction });
|
||||
|
||||
for (const pluginInstance of plugins) {
|
||||
const plugin = ApplicationModel.getPluginByName(pluginInstance.get('name') as string);
|
||||
app.plugin(plugin);
|
||||
}
|
||||
|
||||
app.on('beforeInstall', async function createDatabase() {
|
||||
const { host, port, username, password, database, dialect } = ApplicationModel.getDatabaseConfig(app);
|
||||
|
||||
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({
|
||||
user: username,
|
||||
host,
|
||||
password: password,
|
||||
port,
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
||||
try {
|
||||
await client.query(`CREATE DATABASE "${database}"`);
|
||||
} catch (e) {}
|
||||
|
||||
await client.end();
|
||||
}
|
||||
});
|
||||
|
||||
await app.load();
|
||||
|
||||
if (!lodash.get(options, 'skipInstall', false)) {
|
||||
await app.install();
|
||||
}
|
||||
|
||||
await app.start();
|
||||
}
|
||||
|
||||
static initOptions(appName: string, mainApp: Application) {
|
||||
const rawDatabaseOptions = ApplicationModel.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,
|
||||
};
|
||||
}
|
||||
}
|
46
packages/plugin-multiple-apps/src/server.ts
Normal file
46
packages/plugin-multiple-apps/src/server.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { Plugin } from '@nocobase/server';
|
||||
import { resolve } from 'path';
|
||||
import { ApplicationModel } from './models/application';
|
||||
import { AppManager } from '@nocobase/server';
|
||||
|
||||
export class PluginMultipleApps extends Plugin {
|
||||
getName(): string {
|
||||
return this.getPackageName(__dirname);
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.db.registerModels({
|
||||
ApplicationModel,
|
||||
});
|
||||
|
||||
await this.db.import({
|
||||
directory: resolve(__dirname, 'collections'),
|
||||
});
|
||||
|
||||
this.db.on('applications.afterCreateWithAssociations', async (model: ApplicationModel, options) => {
|
||||
const { transaction } = options;
|
||||
await model.registerToMainApp(this.app, { transaction });
|
||||
});
|
||||
|
||||
this.db.on('applications.afterDestroy', async (model: ApplicationModel) => {
|
||||
await this.app.appManager.removeApplication(model.get('name') as string);
|
||||
});
|
||||
|
||||
this.app.appManager.on(
|
||||
'beforeGetApplication',
|
||||
async function lazyLoadApplication({ appManager, name }: { appManager: AppManager; name: string }) {
|
||||
if (!appManager.applications.has(name)) {
|
||||
const existsApplication = (await this.app.db.getRepository('applications').findOne({
|
||||
filter: {
|
||||
name,
|
||||
},
|
||||
})) as ApplicationModel | null;
|
||||
|
||||
if (existsApplication) {
|
||||
await existsApplication.registerToMainApp(this.app, { skipInstall: true });
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
9
packages/plugin-multiple-apps/tsconfig.build.json
Normal file
9
packages/plugin-multiple-apps/tsconfig.build.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.build.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./lib",
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
|
||||
"exclude": ["./src/__tests__/*", "./esm/*", "./lib/*"]
|
||||
}
|
5
packages/plugin-multiple-apps/tsconfig.json
Normal file
5
packages/plugin-multiple-apps/tsconfig.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
|
||||
"exclude": ["./esm/*", "./lib/*"]
|
||||
}
|
@ -7,4 +7,8 @@ export default class PluginNotifications extends Plugin {
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return this.getPackageName(__dirname);
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,10 @@ export class SystemSettingsPlugin extends Plugin {
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return this.getPackageName(__dirname);
|
||||
}
|
||||
}
|
||||
|
||||
export default SystemSettingsPlugin;
|
||||
|
@ -5,6 +5,10 @@ import { resolve } from 'path';
|
||||
import { getAccessible } from './actions/getAccessible';
|
||||
|
||||
export class UiRoutesStoragePlugin extends Plugin {
|
||||
getName(): string {
|
||||
return this.getPackageName(__dirname);
|
||||
}
|
||||
|
||||
async install() {
|
||||
const repository = this.app.db.getRepository('uiRoutes');
|
||||
const routes = [
|
||||
|
@ -30,7 +30,7 @@ describe('server hooks', () => {
|
||||
|
||||
uiSchemaRepository = db.getRepository('uiSchemas');
|
||||
|
||||
uiSchemaPlugin = app.getPlugin<UiSchemaStoragePlugin>('UiSchemaStoragePlugin');
|
||||
uiSchemaPlugin = app.getPlugin<UiSchemaStoragePlugin>('@nocobase/plugin-ui-schema-storage');
|
||||
});
|
||||
|
||||
it('should clean row struct', async () => {
|
||||
|
@ -69,7 +69,7 @@ describe('server hooks', () => {
|
||||
uiSchemaRepository = db.getRepository('uiSchemas');
|
||||
await uiSchemaRepository.insert(schema);
|
||||
|
||||
uiSchemaPlugin = app.getPlugin<UiSchemaStoragePlugin>('UiSchemaStoragePlugin');
|
||||
uiSchemaPlugin = app.getPlugin<UiSchemaStoragePlugin>('@nocobase/plugin-ui-schema-storage');
|
||||
});
|
||||
|
||||
it('should call server hooks onFieldDestroy', async () => {
|
||||
|
@ -70,6 +70,10 @@ export class UiSchemaStoragePlugin extends Plugin {
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return this.getPackageName(__dirname);
|
||||
}
|
||||
}
|
||||
|
||||
export default UiSchemaStoragePlugin;
|
||||
|
@ -107,4 +107,8 @@ export default class UsersPlugin extends Plugin {
|
||||
await repo.db2cm('users');
|
||||
}
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return this.getPackageName(__dirname);
|
||||
}
|
||||
}
|
||||
|
@ -28,12 +28,10 @@ export default class WorkflowPlugin extends Plugin {
|
||||
this.app.on('beforeStart', async () => {
|
||||
const { model } = db.getCollection('workflows');
|
||||
await (model as typeof WorkflowModel).mount();
|
||||
})
|
||||
});
|
||||
|
||||
// [Life Cycle]: initialize all necessary seed data
|
||||
this.app.on('db.init', async () => {
|
||||
|
||||
});
|
||||
this.app.on('db.init', async () => {});
|
||||
|
||||
// const [Automation, AutomationJob] = database.getModels(['automations', 'automations_jobs']);
|
||||
|
||||
@ -60,4 +58,8 @@ export default class WorkflowPlugin extends Plugin {
|
||||
// await model.cancel();
|
||||
// });
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return this.getPackageName(__dirname);
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@
|
||||
"@nocobase/resourcer": "^0.6.0-alpha.0",
|
||||
"commander": "^8.1.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"find-package-json": "^1.2.0",
|
||||
"i18next": "^21.3.2",
|
||||
"koa": "^2.13.4",
|
||||
"koa-bodyparser": "^4.3.0",
|
||||
|
103
packages/server/src/__tests__/multiple-application.test.ts
Normal file
103
packages/server/src/__tests__/multiple-application.test.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { mockServer, MockServer } from '@nocobase/test';
|
||||
import { IncomingMessage } from 'http';
|
||||
import * as url from 'url';
|
||||
|
||||
describe('multiple apps', () => {
|
||||
it('should emit beforeGetApplication event', async () => {
|
||||
const beforeGetApplicationFn = jest.fn();
|
||||
|
||||
const app = mockServer();
|
||||
|
||||
app.appManager.on('beforeGetApplication', beforeGetApplicationFn);
|
||||
|
||||
app.appManager.createApplication('sub1', {
|
||||
database: app.db,
|
||||
});
|
||||
|
||||
app.appManager.setAppSelector(() => 'sub1');
|
||||
|
||||
await app.agent().resource('test').test({});
|
||||
|
||||
await app.agent().resource('test').test({});
|
||||
|
||||
expect(beforeGetApplicationFn).toHaveBeenCalledTimes(2);
|
||||
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('should listen stop event', async () => {
|
||||
const app = mockServer();
|
||||
|
||||
const subApp1 = app.appManager.createApplication('sub1', {
|
||||
database: app.db,
|
||||
});
|
||||
|
||||
const subApp1StopFn = jest.fn();
|
||||
|
||||
subApp1.on('afterStop', subApp1StopFn);
|
||||
|
||||
await app.stop();
|
||||
|
||||
expect(subApp1StopFn).toBeCalledTimes(1);
|
||||
|
||||
await app.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple application', () => {
|
||||
let app: MockServer;
|
||||
beforeEach(async () => {
|
||||
app = mockServer();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('should create multiple apps', async () => {
|
||||
const subApp1 = app.appManager.createApplication('sub1', {
|
||||
database: app.db,
|
||||
});
|
||||
|
||||
subApp1.resourcer.define({
|
||||
name: 'test',
|
||||
actions: {
|
||||
async test(ctx) {
|
||||
ctx.body = 'sub1';
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const subApp2 = app.appManager.createApplication('sub2', {
|
||||
database: app.db,
|
||||
});
|
||||
|
||||
subApp2.resourcer.define({
|
||||
name: 'test',
|
||||
actions: {
|
||||
async test(ctx) {
|
||||
ctx.body = 'sub2';
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let response = await app.agent().resource('test').test();
|
||||
expect(response.statusCode).toEqual(404);
|
||||
|
||||
app.appManager.setAppSelector((req: IncomingMessage) => {
|
||||
const queryObject = url.parse(req.url, true).query;
|
||||
return queryObject['app'] as string;
|
||||
});
|
||||
|
||||
response = await app.agent().resource('test').test({
|
||||
app: 'sub1',
|
||||
});
|
||||
|
||||
expect(response.statusCode).toEqual(200);
|
||||
|
||||
response = await app.agent().resource('test').test({
|
||||
app: 'sub2',
|
||||
});
|
||||
expect(response.statusCode).toEqual(200);
|
||||
});
|
||||
});
|
@ -18,7 +18,11 @@ describe('plugin', () => {
|
||||
|
||||
describe('define', () => {
|
||||
it('should add plugin with options', async () => {
|
||||
class MyPlugin extends Plugin {}
|
||||
class MyPlugin extends Plugin {
|
||||
getName(): string {
|
||||
return 'test';
|
||||
}
|
||||
}
|
||||
|
||||
const plugin = app.plugin(MyPlugin, {
|
||||
test: 'hello',
|
||||
@ -35,6 +39,10 @@ describe('plugin', () => {
|
||||
async load() {
|
||||
this.options.a;
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return 'MyPlugin';
|
||||
}
|
||||
}
|
||||
const plugin = app.plugin<Options>(MyPlugin, {
|
||||
a: 'aa',
|
||||
@ -68,6 +76,10 @@ describe('plugin', () => {
|
||||
name: 'tests',
|
||||
});
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return 'test';
|
||||
}
|
||||
},
|
||||
);
|
||||
await app.load();
|
||||
|
@ -6,4 +6,8 @@ export default class Plugin1 extends Plugin {
|
||||
name: 'tests',
|
||||
});
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return 'Plugin1';
|
||||
}
|
||||
}
|
||||
|
@ -6,4 +6,8 @@ export default class Plugin2 extends Plugin {
|
||||
name: 'tests',
|
||||
});
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return 'Plugin2';
|
||||
}
|
||||
}
|
||||
|
@ -6,4 +6,8 @@ export default class Plugin3 extends Plugin {
|
||||
name: 'tests',
|
||||
});
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return 'Plugin3';
|
||||
}
|
||||
}
|
||||
|
79
packages/server/src/app-manager.ts
Normal file
79
packages/server/src/app-manager.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import Application, { ApplicationOptions } from './application';
|
||||
import http, { IncomingMessage } from 'http';
|
||||
import EventEmitter from 'events';
|
||||
import { applyMixins, AsyncEmitter } from '@nocobase/utils';
|
||||
|
||||
type AppSelector = (ctx) => Application | string;
|
||||
|
||||
export class AppManager extends EventEmitter {
|
||||
public applications: Map<string, Application> = new Map<string, Application>();
|
||||
|
||||
constructor(private app: Application) {
|
||||
super();
|
||||
|
||||
app.on('beforeStop', async (mainApp, options) => {
|
||||
return await Promise.all(
|
||||
[...this.applications.values()].map((application: Application) => application.stop(options)),
|
||||
);
|
||||
});
|
||||
|
||||
app.on('afterDestroy', async (mainApp, options) => {
|
||||
return await Promise.all(
|
||||
[...this.applications.values()].map((application: Application) => application.destroy(options)),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
appSelector: AppSelector = (req: IncomingMessage) => this.app;
|
||||
|
||||
createApplication(name: string, options: ApplicationOptions): Application {
|
||||
const application = new Application(options);
|
||||
this.applications.set(name, application);
|
||||
return application;
|
||||
}
|
||||
|
||||
async removeApplication(name: string) {
|
||||
const application = this.applications.get(name);
|
||||
if (!application) {
|
||||
return;
|
||||
}
|
||||
|
||||
await application.destroy();
|
||||
|
||||
this.applications.delete(name);
|
||||
}
|
||||
|
||||
setAppSelector(selector: AppSelector) {
|
||||
this.appSelector = selector;
|
||||
}
|
||||
|
||||
listen(...args) {
|
||||
const server = http.createServer(this.callback());
|
||||
return server.listen(...args);
|
||||
}
|
||||
|
||||
async getApplication(appName: string): Promise<null | Application> {
|
||||
await this.emitAsync('beforeGetApplication', {
|
||||
appManager: this,
|
||||
name: appName,
|
||||
});
|
||||
|
||||
return this.applications.get(appName);
|
||||
}
|
||||
|
||||
callback() {
|
||||
return async (req, res) => {
|
||||
let handleApp = this.appSelector(req);
|
||||
|
||||
if (typeof handleApp === 'string') {
|
||||
handleApp = (await this.getApplication(handleApp)) || this.app;
|
||||
}
|
||||
|
||||
handleApp.callback()(req, res);
|
||||
};
|
||||
}
|
||||
|
||||
emitAsync: (event: string | symbol, ...args: any[]) => Promise<boolean>;
|
||||
}
|
||||
|
||||
applyMixins(AppManager, [AsyncEmitter]);
|
@ -1,6 +1,6 @@
|
||||
import { ACL } from '@nocobase/acl';
|
||||
import { registerActions } from '@nocobase/actions';
|
||||
import Database, { CleanOptions, CollectionOptions, IDatabaseOptions, SyncOptions } from '@nocobase/database';
|
||||
import Database, { CollectionOptions, IDatabaseOptions } from '@nocobase/database';
|
||||
import Resourcer, { ResourceOptions } from '@nocobase/resourcer';
|
||||
import { applyMixins, AsyncEmitter } from '@nocobase/utils';
|
||||
import { Command, CommandOptions } from 'commander';
|
||||
@ -12,6 +12,7 @@ import { createACL } from './acl';
|
||||
import { createCli, createDatabase, createI18n, createResourcer, registerMiddlewares } from './helper';
|
||||
import { Plugin } from './plugin';
|
||||
import { PluginManager, InstallOptions } from './plugin-manager';
|
||||
import { AppManager } from './app-manager';
|
||||
|
||||
export interface ResourcerOptions {
|
||||
prefix?: string;
|
||||
@ -84,11 +85,13 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
|
||||
public readonly acl: ACL;
|
||||
|
||||
public readonly appManager: AppManager;
|
||||
|
||||
protected plugins = new Map<string, Plugin>();
|
||||
|
||||
public listenServer: Server;
|
||||
|
||||
constructor(options: ApplicationOptions) {
|
||||
constructor(public options: ApplicationOptions) {
|
||||
super();
|
||||
|
||||
this.acl = createACL();
|
||||
@ -101,6 +104,8 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
app: this,
|
||||
});
|
||||
|
||||
this.appManager = new AppManager(this);
|
||||
|
||||
registerMiddlewares(this, options);
|
||||
|
||||
if (options.registerActions !== false) {
|
||||
@ -176,11 +181,20 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
await this.emitAsync('afterStart', this, options);
|
||||
}
|
||||
|
||||
listen(...args): Server {
|
||||
return this.appManager.listen(...args);
|
||||
}
|
||||
|
||||
async stop(options?: any) {
|
||||
await this.emitAsync('beforeStop', this, options);
|
||||
|
||||
// close database connection
|
||||
await this.db.close();
|
||||
try {
|
||||
// close database connection
|
||||
// silent if database already closed
|
||||
await this.db.close();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
// close http server
|
||||
if (this.listenServer) {
|
||||
|
@ -2,3 +2,4 @@ export * from './application';
|
||||
export * as middlewares from './middlewares';
|
||||
export * from './plugin';
|
||||
export { Application as default } from './application';
|
||||
export { AppManager } from './app-manager';
|
||||
|
@ -20,6 +20,10 @@ export class PluginManager {
|
||||
this.app = options.app;
|
||||
}
|
||||
|
||||
getPlugins() {
|
||||
return this.plugins;
|
||||
}
|
||||
|
||||
get(name: string) {
|
||||
return this.plugins.get(name);
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Database } from '@nocobase/database';
|
||||
import { Application } from './application';
|
||||
import path from 'path';
|
||||
import finder from 'find-package-json';
|
||||
|
||||
import { InstallOptions } from './plugin-manager';
|
||||
|
||||
export interface PluginInterface {
|
||||
@ -38,7 +39,9 @@ export abstract class Plugin<O = any> implements PluginInterface {
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return this.constructor.name;
|
||||
const path = require.main.children[require.main.children.length - 1].path;
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
beforeLoad() {}
|
||||
@ -57,4 +60,10 @@ export abstract class Plugin<O = any> implements PluginInterface {
|
||||
collectionPath() {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected getPackageName(dirname: string) {
|
||||
const f = finder(dirname);
|
||||
const packageObj = f.next().value;
|
||||
return packageObj['name'];
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { mockDatabase } from '@nocobase/database';
|
||||
import { Database, mockDatabase } from '@nocobase/database';
|
||||
import Application, { ApplicationOptions } from '@nocobase/server';
|
||||
import qs from 'qs';
|
||||
import supertest, { SuperAgentTest } from 'supertest';
|
||||
@ -72,7 +72,7 @@ export class MockServer extends Application {
|
||||
}
|
||||
|
||||
agent(): SuperAgentTest & { resource: (name: string, resourceOf?: any) => Resource } {
|
||||
const agent = supertest.agent(this.callback());
|
||||
const agent = supertest.agent(this.appManager.callback());
|
||||
const prefix = this.resourcer.options.prefix;
|
||||
const proxy = new Proxy(agent, {
|
||||
get(target, method: string, receiver) {
|
||||
@ -130,7 +130,13 @@ export class MockServer extends Application {
|
||||
}
|
||||
|
||||
export function mockServer(options: ApplicationOptions = {}) {
|
||||
const database = mockDatabase(<any>options?.database || {});
|
||||
let database;
|
||||
if (options?.database instanceof Database) {
|
||||
database = options.database;
|
||||
} else {
|
||||
database = mockDatabase(<any>options?.database || {});
|
||||
}
|
||||
|
||||
return new MockServer({
|
||||
...options,
|
||||
database,
|
||||
|
25
yarn.lock
25
yarn.lock
@ -3310,7 +3310,14 @@
|
||||
resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
|
||||
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
||||
|
||||
"@types/react-dom@^16.9.8", "@types/react-dom@^17.0.0":
|
||||
"@types/react-dom@^16.9.8":
|
||||
version "16.9.14"
|
||||
resolved "https://registry.npmmirror.com/@types/react-dom/-/react-dom-16.9.14.tgz#674b8f116645fe5266b40b525777fc6bb8eb3bcd"
|
||||
integrity sha512-FIX2AVmPTGP30OUJ+0vadeIFJJ07Mh1m+U0rxfgyW34p3rTlXI+nlenvAxNn4BP36YyI9IJ/+UJ7Wu22N1pI7A==
|
||||
dependencies:
|
||||
"@types/react" "^16"
|
||||
|
||||
"@types/react-dom@^17.0.0":
|
||||
version "17.0.11"
|
||||
resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.11.tgz#e1eadc3c5e86bdb5f7684e00274ae228e7bcc466"
|
||||
integrity sha512-f96K3k+24RaLGVu/Y2Ng3e1EbZ8/cVJvypZWd7cy0ofCBaf2lcM46xNhycMZ2xGwbBjRql7hOlZ+e2WlJ5MH3Q==
|
||||
@ -3370,7 +3377,7 @@
|
||||
"@types/history" "*"
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@>=16.9.11", "@types/react@^16.9.43", "@types/react@^17.0.0":
|
||||
"@types/react@*", "@types/react@>=16.9.11", "@types/react@^17.0.0":
|
||||
version "17.0.34"
|
||||
resolved "https://registry.npmjs.org/@types/react/-/react-17.0.34.tgz#797b66d359b692e3f19991b6b07e4b0c706c0102"
|
||||
integrity sha512-46FEGrMjc2+8XhHXILr+3+/sTe3OfzSPU9YGKILLrUYbQ1CLQC9Daqo1KzENGXAWwrFwiY0l4ZbF20gRvgpWTg==
|
||||
@ -3379,6 +3386,15 @@
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/react@^16", "@types/react@^16.9.43":
|
||||
version "16.14.24"
|
||||
resolved "https://registry.npmmirror.com/@types/react/-/react-16.14.24.tgz#f2c5e9fa78f83f769884b83defcf7924b9eb5c82"
|
||||
integrity sha512-e7U2WC8XQP/xfR7bwhOhNFZKPTfW1ph+MiqtudKb8tSV8RyCsovQx2sNVtKoOryjxFKpHPPC/yNiGfdeVM5Gyw==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/sax@^1.2.1":
|
||||
version "1.2.3"
|
||||
resolved "https://registry.npmjs.org/@types/sax/-/sax-1.2.3.tgz#b630ac1403ebd7812e0bf9a10de9bf5077afb348"
|
||||
@ -7324,6 +7340,11 @@ find-cache-dir@^2.0.0:
|
||||
make-dir "^2.0.0"
|
||||
pkg-dir "^3.0.0"
|
||||
|
||||
find-package-json@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmmirror.com/find-package-json/-/find-package-json-1.2.0.tgz#4057d1b943f82d8445fe52dc9cf456f6b8b58083"
|
||||
integrity sha512-+SOGcLGYDJHtyqHd87ysBhmaeQ95oWspDKnMXBrnQ9Eq4OkLNqejgoaD8xVWu6GPa0B6roa6KinCMEMcVeqONw==
|
||||
|
||||
find-root@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4"
|
||||
|
Loading…
Reference in New Issue
Block a user