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:
ChengLei Shao 2022-03-28 22:01:10 +08:00 committed by GitHub
parent c0a33b6e3e
commit 81978711e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 797 additions and 29 deletions

View File

@ -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();

View File

@ -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;

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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;

View File

@ -14,4 +14,8 @@ export default class PluginActionLogs extends Plugin {
directory: path.resolve(__dirname, 'collections'),
});
}
getName(): string {
return this.getPackageName(__dirname);
}
}

View File

@ -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;

View File

@ -60,6 +60,10 @@ export class ClientPlugin extends Plugin {
}
});
}
getName(): string {
return this.getPackageName(__dirname);
}
}
export default ClientPlugin;

View File

@ -86,6 +86,10 @@ export class CollectionManagerPlugin extends Plugin {
directory: path.resolve(__dirname, './collections'),
});
}
getName(): string {
return this.getPackageName(__dirname);
}
}
export default CollectionManagerPlugin;

View File

@ -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';

View File

@ -36,4 +36,8 @@ export default class PluginFileManager extends Plugin {
await getStorageConfig(STORAGE_TYPE_LOCAL).middleware(this.app);
}
}
getName(): string {
return this.getPackageName(__dirname);
}
}

View File

@ -0,0 +1,7 @@
node_modules
*.log
docs
__tests__
tsconfig.json
src
.fatherrc.ts

View 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": {
}
}

View File

@ -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();
});
});

View File

@ -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();
});
});

View File

@ -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',
},
],
});

View File

@ -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',
},
],
});

View 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,
};
}
}

View 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 });
}
}
},
);
}
}

View File

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.build.json",
"compilerOptions": {
"outDir": "./lib",
"declaration": true
},
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
"exclude": ["./src/__tests__/*", "./esm/*", "./lib/*"]
}

View File

@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.json",
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
"exclude": ["./esm/*", "./lib/*"]
}

View File

@ -7,4 +7,8 @@ export default class PluginNotifications extends Plugin {
directory: path.resolve(__dirname, 'collections'),
});
}
getName(): string {
return this.getPackageName(__dirname);
}
}

View File

@ -29,6 +29,10 @@ export class SystemSettingsPlugin extends Plugin {
}),
);
}
getName(): string {
return this.getPackageName(__dirname);
}
}
export default SystemSettingsPlugin;

View File

@ -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 = [

View File

@ -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 () => {

View File

@ -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 () => {

View File

@ -70,6 +70,10 @@ export class UiSchemaStoragePlugin extends Plugin {
directory: path.resolve(__dirname, 'collections'),
});
}
getName(): string {
return this.getPackageName(__dirname);
}
}
export default UiSchemaStoragePlugin;

View File

@ -107,4 +107,8 @@ export default class UsersPlugin extends Plugin {
await repo.db2cm('users');
}
}
getName(): string {
return this.getPackageName(__dirname);
}
}

View File

@ -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);
}
}

View File

@ -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",

View 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);
});
});

View File

@ -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();

View File

@ -6,4 +6,8 @@ export default class Plugin1 extends Plugin {
name: 'tests',
});
}
getName(): string {
return 'Plugin1';
}
}

View File

@ -6,4 +6,8 @@ export default class Plugin2 extends Plugin {
name: 'tests',
});
}
getName(): string {
return 'Plugin2';
}
}

View File

@ -6,4 +6,8 @@ export default class Plugin3 extends Plugin {
name: 'tests',
});
}
getName(): string {
return 'Plugin3';
}
}

View 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]);

View File

@ -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) {

View File

@ -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';

View File

@ -20,6 +20,10 @@ export class PluginManager {
this.app = options.app;
}
getPlugins() {
return this.plugins;
}
get(name: string) {
return this.plugins.get(name);
}

View File

@ -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'];
}
}

View File

@ -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,

View File

@ -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"