Application (#175)
* feat: getRepository * getRepository return type * export action * add: acl * feat: setResourceAction * feat: action alias * chore: code struct * feat: removeResourceAction * chore: file name * ignorecase * remove ACL * feat: ACL * feat: role toJSON * using emit * chore: test * feat: plugin-acl * feat: acl with predicate * grant universal action test * grant action test * update resource action test * revoke resource action * usingActionsConfig switch * plugin-ui-schema-storage * remove global acl instance * fix: collection manager with sqlite * add own action listener * add acl middleware * add acl allowConfigure strategy option * add plugin-acl allowConfigure * change acl resourceName * add acl middleware merge params * bugfix * append fields on acl action params * acl middleware parse template * fix: collection-manager migrate * add acl association field test * feat(plugin-acl): grant association field actions * chore(plugin-acl): type name * feat(plugin-acl): regrant actions on resource action update * feat(plugin-acl): regrant action on field destroy * fix(plugin-acl): test * fix(plugin-acl): test run * feat(plugin-acl): set default role * feat(plugin-users): set user default role * test(plugin-users): create user with role * feat(plugin-users): create user with role * feat(application): application hook * feat(database): reconnect * feat(database): application life cycle * feat(database): sync with option * feat(database): hook position * feat(database): hook position * feat(database): remove load in start * fix(application): get plugin * feat(test): loadAndInstall * feat: improve code * feat: improve code * fix: listen options * fix: bug * test(database): add test case Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
7a7ab2ef41
commit
15950ece05
@ -1,8 +1,25 @@
|
||||
import { mockDatabase } from './index';
|
||||
import path from 'path';
|
||||
import { Model } from '..';
|
||||
import { mockDatabase } from './index';
|
||||
|
||||
describe('database', () => {
|
||||
test('close state', async () => {
|
||||
const db = mockDatabase();
|
||||
expect(db.closed()).toBeFalsy();
|
||||
await db.close();
|
||||
expect(db.closed()).toBeTruthy();
|
||||
await db.reconnect();
|
||||
expect(db.closed()).toBeFalsy();
|
||||
});
|
||||
|
||||
test('reconnect', async () => {
|
||||
const db = mockDatabase();
|
||||
await db.sequelize.authenticate();
|
||||
await db.close();
|
||||
await db.reconnect();
|
||||
await db.sequelize.authenticate();
|
||||
});
|
||||
|
||||
test('get repository', async () => {
|
||||
const db = mockDatabase();
|
||||
db.collection({
|
||||
|
@ -1,18 +1,15 @@
|
||||
import { Sequelize, ModelCtor, Model, Options, SyncOptions, Op, Utils } from 'sequelize';
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import { Collection, CollectionOptions, RepositoryType } from './collection';
|
||||
import * as FieldTypes from './fields';
|
||||
import { BaseFieldOptions, Field, FieldContext, FieldOptions, RelationField } from './fields';
|
||||
import { applyMixins, AsyncEmitter } from '@nocobase/utils';
|
||||
|
||||
import merge from 'deepmerge';
|
||||
import { ModelHook } from './model-hook';
|
||||
import { EventEmitter } from 'events';
|
||||
import { Model, 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';
|
||||
import { Field, FieldContext, RelationField } from './fields';
|
||||
import { ModelHook } from './model-hook';
|
||||
import extendOperators from './operators';
|
||||
import { Repository } from './repository';
|
||||
import { RelationRepository } from './relation-repository/relation-repository';
|
||||
import { Repository } from './repository';
|
||||
|
||||
export interface MergeOptions extends merge.Options {}
|
||||
|
||||
@ -33,6 +30,10 @@ interface RegisterOperatorsContext {
|
||||
field?: Field;
|
||||
}
|
||||
|
||||
export interface CleanOptions extends QueryInterfaceDropAllTablesOptions {
|
||||
drop?: boolean;
|
||||
}
|
||||
|
||||
type OperatorFunc = (value: any, ctx?: RegisterOperatorsContext) => any;
|
||||
|
||||
export class Database extends EventEmitter implements AsyncEmitter {
|
||||
@ -219,6 +220,29 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
||||
return result;
|
||||
}
|
||||
|
||||
async clean(options: CleanOptions) {
|
||||
const { drop, ...others } = options;
|
||||
if (drop) {
|
||||
await this.sequelize.getQueryInterface().dropAllTables(others);
|
||||
}
|
||||
}
|
||||
|
||||
async reconnect() {
|
||||
// @ts-ignore
|
||||
const ConnectionManager = this.sequelize.dialect.connectionManager.constructor;
|
||||
// @ts-ignore
|
||||
const connectionManager = new ConnectionManager(this.sequelize.dialect, this.sequelize);
|
||||
// @ts-ignore
|
||||
this.sequelize.dialect.connectionManager = connectionManager;
|
||||
// @ts-ignore
|
||||
this.sequelize.connectionManager = connectionManager;
|
||||
}
|
||||
|
||||
closed() {
|
||||
// @ts-ignore
|
||||
return this.sequelize.connectionManager.pool._draining;
|
||||
}
|
||||
|
||||
async close() {
|
||||
return this.sequelize.close();
|
||||
}
|
||||
|
@ -1,14 +1,15 @@
|
||||
export * from './database';
|
||||
export { Model, ModelCtor, SyncOptions } from 'sequelize';
|
||||
export * from './collection';
|
||||
export * from './repository';
|
||||
export * from './database';
|
||||
export { Database as default } from './database';
|
||||
export * from './fields';
|
||||
export * from './magic-attribute-model';
|
||||
export * from './relation-repository/belongs-to-many-repository';
|
||||
export * from './relation-repository/belongs-to-repository';
|
||||
export * from './relation-repository/hasmany-repository';
|
||||
export * from './relation-repository/single-relation-repository';
|
||||
export * from './relation-repository/multiple-relation-repository';
|
||||
|
||||
export { Model, ModelCtor } from 'sequelize';
|
||||
export * from './fields';
|
||||
export * from './relation-repository/single-relation-repository';
|
||||
export * from './repository';
|
||||
export * from './update-associations';
|
||||
export * from './magic-attribute-model';
|
||||
|
||||
|
||||
|
@ -19,7 +19,6 @@ export async function prepareApp() {
|
||||
registerActions: true,
|
||||
});
|
||||
|
||||
await app.cleanDb();
|
||||
app.plugin(PluginUiSchema);
|
||||
app.plugin(PluginCollectionManager);
|
||||
|
||||
@ -30,7 +29,7 @@ export async function prepareApp() {
|
||||
});
|
||||
|
||||
app.plugin(PluginACL);
|
||||
await app.loadAndSync();
|
||||
await app.loadAndInstall();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
@ -2,7 +2,9 @@ import supertest from 'supertest';
|
||||
import { Application } from '../application';
|
||||
import { Plugin } from '../plugin';
|
||||
|
||||
class MyPlugin extends Plugin {}
|
||||
class MyPlugin extends Plugin {
|
||||
load() {}
|
||||
}
|
||||
|
||||
describe('application', () => {
|
||||
let app: Application;
|
||||
|
53
packages/server/src/__tests__/life-cycle.test.ts
Normal file
53
packages/server/src/__tests__/life-cycle.test.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import Application from '../application';
|
||||
import { Plugin } from '../plugin';
|
||||
|
||||
describe('application life cycle', () => {
|
||||
it('should start application', async () => {
|
||||
const app = new Application({
|
||||
database: {
|
||||
dialect: 'sqlite',
|
||||
storage: ':memory:',
|
||||
},
|
||||
});
|
||||
|
||||
const loadFn = jest.fn();
|
||||
const installFn = jest.fn();
|
||||
|
||||
// register plugin
|
||||
class TestPlugin extends Plugin {
|
||||
beforeLoad() {}
|
||||
|
||||
getName() {
|
||||
return 'Test';
|
||||
}
|
||||
|
||||
load() {
|
||||
loadFn();
|
||||
this.app.on('beforeInstall', () => {
|
||||
installFn();
|
||||
});
|
||||
}
|
||||
}
|
||||
app.plugin(TestPlugin);
|
||||
await app.load();
|
||||
expect(loadFn).toHaveBeenCalledTimes(1);
|
||||
expect(installFn).toHaveBeenCalledTimes(0);
|
||||
await app.install();
|
||||
expect(installFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should listen application', async () => {
|
||||
const app = new Application({
|
||||
database: {
|
||||
dialect: 'sqlite',
|
||||
storage: ':memory:',
|
||||
},
|
||||
});
|
||||
|
||||
await app.start({ listen: { port: 13090 } });
|
||||
expect(app.listenServer).not.toBeNull();
|
||||
|
||||
await app.stop();
|
||||
expect(app.listenServer).toBeNull();
|
||||
});
|
||||
});
|
@ -1,7 +1,7 @@
|
||||
import supertest from 'supertest';
|
||||
import { Application } from '../application';
|
||||
import { Plugin } from '../plugin';
|
||||
import path from 'path';
|
||||
import Plugin1 from './plugins/plugin1';
|
||||
import Plugin2 from './plugins/plugin2';
|
||||
import Plugin3 from './plugins/plugin3';
|
||||
|
||||
describe('plugin', () => {
|
||||
@ -29,101 +29,55 @@ describe('plugin', () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
return app.db.close();
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
describe('define', () => {
|
||||
it('plugin name', async () => {
|
||||
const plugin = app.plugin(function abc() {});
|
||||
expect(plugin).toBeInstanceOf(Plugin);
|
||||
expect(plugin.getName()).toBe('abc');
|
||||
});
|
||||
it('should add plugin with options', async () => {
|
||||
class MyPlugin extends Plugin {
|
||||
load() {}
|
||||
}
|
||||
|
||||
it('plugin name', async () => {
|
||||
const plugin = app.plugin({
|
||||
name: 'plugin-name2',
|
||||
async load() {},
|
||||
const plugin = app.plugin(MyPlugin, {
|
||||
test: 'hello',
|
||||
});
|
||||
expect(plugin).toBeInstanceOf(Plugin);
|
||||
expect(plugin.getName()).toBe('plugin-name2');
|
||||
|
||||
expect(plugin.options['test']).toEqual('hello');
|
||||
});
|
||||
|
||||
it('plugin name', async () => {
|
||||
const plugin = app.plugin({
|
||||
name: 'plugin-name3',
|
||||
load: function () {},
|
||||
interface Options {
|
||||
a?: string;
|
||||
}
|
||||
class MyPlugin extends Plugin<Options> {
|
||||
load() {
|
||||
this.options.a;
|
||||
}
|
||||
}
|
||||
const plugin = app.plugin<Options>(MyPlugin, {
|
||||
a: 'aa',
|
||||
});
|
||||
expect(plugin).toBeInstanceOf(Plugin);
|
||||
expect(plugin.getName()).toBe('plugin-name3');
|
||||
});
|
||||
|
||||
it('plugin name', async () => {
|
||||
class MyPlugin extends Plugin {}
|
||||
const plugin = app.plugin(MyPlugin);
|
||||
plugin.setOptions({
|
||||
a: 'a'
|
||||
})
|
||||
expect(plugin).toBeInstanceOf(MyPlugin);
|
||||
expect(plugin.getName()).toBe('MyPlugin');
|
||||
});
|
||||
|
||||
it('plugin name', async () => {
|
||||
class MyPlugin extends Plugin {}
|
||||
const plugin = app.plugin({
|
||||
plugin: MyPlugin,
|
||||
});
|
||||
expect(plugin).toBeInstanceOf(MyPlugin);
|
||||
expect(plugin.getName()).toBe('MyPlugin');
|
||||
});
|
||||
|
||||
it('plugin name', async () => {
|
||||
const plugin = app.plugin(path.resolve(__dirname, './plugins/plugin1'));
|
||||
const plugin = app.plugin(Plugin1);
|
||||
expect(plugin).toBeInstanceOf(Plugin);
|
||||
expect(plugin.getName()).toBe('abc');
|
||||
expect(plugin.getName()).toBe('Plugin1');
|
||||
});
|
||||
|
||||
it('plugin name', async () => {
|
||||
const plugin = app.plugin(path.resolve(__dirname, './plugins/plugin3'));
|
||||
const plugin = app.plugin(Plugin3);
|
||||
expect(plugin).toBeInstanceOf(Plugin3);
|
||||
expect(plugin.getName()).toBe('Plugin3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('load', () => {
|
||||
it('plugin load', async () => {
|
||||
app.plugin(function abc(this: Plugin) {
|
||||
this.app.collection({
|
||||
name: 'tests',
|
||||
});
|
||||
});
|
||||
await app.load();
|
||||
const Test = app.db.getCollection('tests');
|
||||
expect(Test).toBeDefined();
|
||||
});
|
||||
|
||||
it('plugin load', async () => {
|
||||
app.plugin({
|
||||
load() {
|
||||
app.collection({
|
||||
name: 'tests',
|
||||
});
|
||||
},
|
||||
});
|
||||
await app.load();
|
||||
const Test = app.db.getCollection('tests');
|
||||
expect(Test).toBeDefined();
|
||||
});
|
||||
|
||||
it('plugin load', async () => {
|
||||
app.plugin({
|
||||
load: () => {
|
||||
app.collection({
|
||||
name: 'tests',
|
||||
});
|
||||
},
|
||||
});
|
||||
await app.load();
|
||||
const Test = app.db.getCollection('tests');
|
||||
expect(Test).toBeDefined();
|
||||
});
|
||||
|
||||
it('plugin load', async () => {
|
||||
app.plugin(
|
||||
class MyPlugin extends Plugin {
|
||||
@ -140,21 +94,21 @@ describe('plugin', () => {
|
||||
});
|
||||
|
||||
it('plugin load', async () => {
|
||||
app.plugin(path.resolve(__dirname, './plugins/plugin1'));
|
||||
app.plugin(Plugin1);
|
||||
await app.load();
|
||||
const Test = app.db.getCollection('tests');
|
||||
expect(Test).toBeDefined();
|
||||
});
|
||||
|
||||
it('plugin load', async () => {
|
||||
app.plugin(path.resolve(__dirname, './plugins/plugin2'));
|
||||
app.plugin(Plugin2);
|
||||
await app.load();
|
||||
const Test = app.db.getCollection('tests');
|
||||
expect(Test).toBeDefined();
|
||||
});
|
||||
|
||||
it('plugin load', async () => {
|
||||
app.plugin(path.resolve(__dirname, './plugins/plugin3'));
|
||||
app.plugin(Plugin3);
|
||||
await app.load();
|
||||
const Test = app.db.getCollection('tests');
|
||||
expect(Test).toBeDefined();
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { Plugin } from '../../plugin';
|
||||
|
||||
export default function abc(this: Plugin) {
|
||||
this.app.collection({
|
||||
name: 'tests',
|
||||
});
|
||||
export default class Plugin1 extends Plugin {
|
||||
async load() {
|
||||
this.app.collection({
|
||||
name: 'tests',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { IPlugin } from '../../plugin';
|
||||
import { Plugin } from '../../plugin';
|
||||
|
||||
export default {
|
||||
load() {
|
||||
export default class Plugin2 extends Plugin {
|
||||
async load() {
|
||||
this.app.collection({
|
||||
name: 'tests',
|
||||
});
|
||||
},
|
||||
} as IPlugin;
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,15 @@
|
||||
import Koa from 'koa';
|
||||
import { Command, CommandOptions } from 'commander';
|
||||
import Database, { DatabaseOptions, CollectionOptions } from '@nocobase/database';
|
||||
import Resourcer, { ResourceOptions } from '@nocobase/resourcer';
|
||||
import { PluginType, Plugin, PluginOptions } from './plugin';
|
||||
import { registerActions } from '@nocobase/actions';
|
||||
import { createCli, createI18n, createDatabase, createResourcer, registerMiddlewares } from './helper';
|
||||
import Database, { CleanOptions, CollectionOptions, DatabaseOptions, SyncOptions } from '@nocobase/database';
|
||||
import Resourcer, { ResourceOptions } from '@nocobase/resourcer';
|
||||
import { applyMixins, AsyncEmitter } from '@nocobase/utils';
|
||||
import { Command, CommandOptions } from 'commander';
|
||||
import { Server } from 'http';
|
||||
import { i18n, InitOptions } from 'i18next';
|
||||
import Koa from 'koa';
|
||||
import { isBoolean } from 'lodash';
|
||||
import { createCli, createDatabase, createI18n, createResourcer, registerMiddlewares } from './helper';
|
||||
import { Plugin } from './plugin';
|
||||
import { PluginManager } from './plugin-manager';
|
||||
|
||||
export interface ResourcerOptions {
|
||||
prefix?: string;
|
||||
@ -45,7 +49,31 @@ interface ActionsOptions {
|
||||
resourceNames?: string[];
|
||||
}
|
||||
|
||||
export class Application<StateT = DefaultState, ContextT = DefaultContext> extends Koa {
|
||||
interface ListenOptions {
|
||||
port?: number | undefined;
|
||||
host?: string | undefined;
|
||||
backlog?: number | undefined;
|
||||
path?: string | undefined;
|
||||
exclusive?: boolean | undefined;
|
||||
readableAll?: boolean | undefined;
|
||||
writableAll?: boolean | undefined;
|
||||
/**
|
||||
* @default false
|
||||
*/
|
||||
ipv6Only?: boolean | undefined;
|
||||
signal?: AbortSignal | undefined;
|
||||
}
|
||||
|
||||
interface StartOptions {
|
||||
listen?: ListenOptions;
|
||||
}
|
||||
|
||||
interface InstallOptions {
|
||||
clean?: CleanOptions | boolean;
|
||||
sync?: SyncOptions;
|
||||
}
|
||||
|
||||
export class Application<StateT = DefaultState, ContextT = DefaultContext> extends Koa implements AsyncEmitter {
|
||||
public readonly db: Database;
|
||||
|
||||
public readonly resourcer: Resourcer;
|
||||
@ -54,8 +82,12 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
|
||||
public readonly i18n: i18n;
|
||||
|
||||
public readonly pm: PluginManager;
|
||||
|
||||
protected plugins = new Map<string, Plugin>();
|
||||
|
||||
public listenServer: Server;
|
||||
|
||||
constructor(options: ApplicationOptions) {
|
||||
super();
|
||||
|
||||
@ -64,12 +96,20 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
this.cli = createCli(this, options);
|
||||
this.i18n = createI18n(options);
|
||||
|
||||
this.pm = new PluginManager({
|
||||
app: this,
|
||||
});
|
||||
|
||||
registerMiddlewares(this, options);
|
||||
if (options.registerActions !== false) {
|
||||
registerActions(this);
|
||||
}
|
||||
}
|
||||
|
||||
plugin<O = any>(pluginClass: any, options?: O): Plugin<O> {
|
||||
return this.pm.add(pluginClass, options);
|
||||
}
|
||||
|
||||
use<NewStateT = {}, NewContextT = {}>(
|
||||
middleware: Koa.Middleware<StateT & NewStateT, ContextT & NewContextT>,
|
||||
options?: MiddlewareOptions,
|
||||
@ -98,110 +138,12 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
return (this.cli as any)._findCommand(name);
|
||||
}
|
||||
|
||||
plugin(options?: PluginType | PluginOptions, ext?: PluginOptions): Plugin {
|
||||
if (typeof options === 'string') {
|
||||
return this.plugin(require(options).default, ext);
|
||||
}
|
||||
let instance: Plugin;
|
||||
if (typeof options === 'function') {
|
||||
try {
|
||||
// @ts-ignore
|
||||
instance = new options({
|
||||
name: options.name,
|
||||
...ext,
|
||||
app: this,
|
||||
});
|
||||
if (!(instance instanceof Plugin)) {
|
||||
throw new Error('plugin must be instanceof Plugin');
|
||||
}
|
||||
} catch (err) {
|
||||
// console.log(err);
|
||||
instance = new Plugin({
|
||||
name: options.name,
|
||||
...ext,
|
||||
// @ts-ignore
|
||||
load: options,
|
||||
app: this,
|
||||
});
|
||||
}
|
||||
} else if (typeof options === 'object') {
|
||||
const plugin = options.plugin || Plugin;
|
||||
instance = new plugin({
|
||||
name: options.plugin ? plugin.name : undefined,
|
||||
...options,
|
||||
...ext,
|
||||
app: this,
|
||||
});
|
||||
}
|
||||
const name = instance.getName();
|
||||
if (this.plugins.has(name)) {
|
||||
throw new Error(`plugin name [${name}] is repeated`);
|
||||
}
|
||||
this.plugins.set(name, instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
async load() {
|
||||
await this.emitAsync('plugins.beforeLoad');
|
||||
for (const [name, plugin] of this.plugins) {
|
||||
await this.emitAsync(`plugins.${name}.beforeLoad`);
|
||||
await plugin.load();
|
||||
await this.emitAsync(`plugins.${name}.afterLoad`);
|
||||
}
|
||||
await this.emitAsync('plugins.afterLoad');
|
||||
await this.pm.load();
|
||||
}
|
||||
|
||||
getPlugin<P extends Plugin>(name: string) {
|
||||
return this.plugins.get(name) as P;
|
||||
}
|
||||
|
||||
async emitAsync(event: string | symbol, ...args: any[]): Promise<boolean> {
|
||||
// @ts-ignore
|
||||
const events = this._events;
|
||||
let callbacks = events[event];
|
||||
if (!callbacks) {
|
||||
return false;
|
||||
}
|
||||
// helper function to reuse as much code as possible
|
||||
const run = (cb) => {
|
||||
switch (args.length) {
|
||||
// fast cases
|
||||
case 0:
|
||||
cb = cb.call(this);
|
||||
break;
|
||||
case 1:
|
||||
cb = cb.call(this, args[0]);
|
||||
break;
|
||||
case 2:
|
||||
cb = cb.call(this, args[0], args[1]);
|
||||
break;
|
||||
case 3:
|
||||
cb = cb.call(this, args[0], args[1], args[2]);
|
||||
break;
|
||||
// slower
|
||||
default:
|
||||
cb = cb.apply(this, args);
|
||||
}
|
||||
|
||||
if (cb && (cb instanceof Promise || typeof cb.then === 'function')) {
|
||||
return cb;
|
||||
}
|
||||
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
|
||||
if (typeof callbacks === 'function') {
|
||||
await run(callbacks);
|
||||
} else if (typeof callbacks === 'object') {
|
||||
callbacks = callbacks.slice().filter(Boolean);
|
||||
await callbacks.reduce((prev, next) => {
|
||||
return prev.then((res) => {
|
||||
return run(next).then((result) => Promise.resolve(res.concat(result)));
|
||||
});
|
||||
}, Promise.resolve([]));
|
||||
}
|
||||
|
||||
return true;
|
||||
return this.pm.get(name) as P;
|
||||
}
|
||||
|
||||
async parse(argv = process.argv) {
|
||||
@ -209,9 +151,73 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
||||
return this.cli.parseAsync(argv);
|
||||
}
|
||||
|
||||
async destroy() {
|
||||
await this.db.close();
|
||||
async start(options?: StartOptions) {
|
||||
// reconnect database
|
||||
if (this.db.closed()) {
|
||||
await this.db.reconnect();
|
||||
}
|
||||
|
||||
await this.emitAsync('beforeStart', this, options);
|
||||
|
||||
if (options?.listen?.port) {
|
||||
const listen = () =>
|
||||
new Promise((resolve) => {
|
||||
const Server = this.listen(options?.listen, () => {
|
||||
resolve(Server);
|
||||
});
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
this.listenServer = await listen();
|
||||
}
|
||||
|
||||
await this.emitAsync('afterStart', this, options);
|
||||
}
|
||||
|
||||
async stop() {
|
||||
await this.emitAsync('beforeStop', this);
|
||||
|
||||
// close database connection
|
||||
await this.db.close();
|
||||
|
||||
// close http server
|
||||
if (this.listenServer) {
|
||||
const closeServer = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
this.listenServer.close((err) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
this.listenServer = null;
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
|
||||
await closeServer();
|
||||
}
|
||||
|
||||
await this.emitAsync('afterStop', this);
|
||||
}
|
||||
|
||||
async destroy() {
|
||||
await this.emitAsync('beforeDestroy', this);
|
||||
await this.stop();
|
||||
await this.emitAsync('afterDestroy', this);
|
||||
}
|
||||
|
||||
async install(options?: InstallOptions) {
|
||||
if (options?.clean) {
|
||||
await this.db.clean(isBoolean(options.clean) ? { drop: options.clean } : options.clean);
|
||||
}
|
||||
await this.db.sync(options?.sync);
|
||||
await this.emitAsync('beforeInstall', this, options);
|
||||
await this.emitAsync('afterInstall', this, options);
|
||||
}
|
||||
|
||||
emitAsync: (event: string | symbol, ...args: any[]) => Promise<boolean>;
|
||||
}
|
||||
|
||||
applyMixins(Application, [AsyncEmitter]);
|
||||
|
||||
export default Application;
|
||||
|
@ -86,7 +86,7 @@ export function createCli(app: Application, options: ApplicationOptions) {
|
||||
const cli = args.pop();
|
||||
const opts = cli.opts();
|
||||
await app.emitAsync('beforeStart');
|
||||
app.listen(opts.port || 3000);
|
||||
await app.start(opts.port || 3000);
|
||||
console.log(`http://localhost:${opts.port || 3000}/`);
|
||||
});
|
||||
return cli;
|
||||
|
49
packages/server/src/plugin-manager.ts
Normal file
49
packages/server/src/plugin-manager.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import Application from './application';
|
||||
import { Plugin } from './plugin';
|
||||
|
||||
interface PluginManagerOptions {
|
||||
app: Application;
|
||||
}
|
||||
|
||||
export class PluginManager {
|
||||
app: Application;
|
||||
protected plugins = new Map<string, Plugin>();
|
||||
|
||||
constructor(options: PluginManagerOptions) {
|
||||
this.app = options.app;
|
||||
}
|
||||
|
||||
get(name: string) {
|
||||
return this.plugins.get(name);
|
||||
}
|
||||
|
||||
add<P = Plugin, O = any>(pluginClass: any, options?: O): P {
|
||||
const instance = new pluginClass(this.app, options);
|
||||
|
||||
const name = instance.getName();
|
||||
|
||||
if (this.plugins.has(name)) {
|
||||
throw new Error(`plugin name [${name}] `);
|
||||
}
|
||||
|
||||
this.plugins.set(name, instance);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
async load() {
|
||||
await this.app.emitAsync('beforeLoadAll');
|
||||
|
||||
for (const [name, plugin] of this.plugins) {
|
||||
await plugin.beforeLoad();
|
||||
}
|
||||
|
||||
for (const [name, plugin] of this.plugins) {
|
||||
await this.app.emitAsync('beforeLoadPlugin', plugin);
|
||||
await plugin.load();
|
||||
await this.app.emitAsync('afterLoadPlugin', plugin);
|
||||
}
|
||||
|
||||
await this.app.emitAsync('afterLoadAll');
|
||||
}
|
||||
}
|
@ -1,14 +1,13 @@
|
||||
import { uid } from '@nocobase/utils';
|
||||
import { Database } from '@nocobase/database';
|
||||
import { Application } from './application';
|
||||
import _ from 'lodash';
|
||||
|
||||
export interface IPlugin {
|
||||
install?: (this: Plugin) => void;
|
||||
load?: (this: Plugin) => void;
|
||||
export interface PluginInterface {
|
||||
beforeLoad?: () => void;
|
||||
load();
|
||||
getName(): string;
|
||||
}
|
||||
|
||||
export interface PluginOptions {
|
||||
name?: string;
|
||||
activate?: boolean;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
@ -19,42 +18,28 @@ export interface PluginOptions {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type PluginFn = (this: Plugin) => void;
|
||||
export type PluginType = typeof Plugin;
|
||||
|
||||
export type PluginType = string | PluginFn | typeof Plugin;
|
||||
|
||||
export class Plugin implements IPlugin {
|
||||
options: PluginOptions = {};
|
||||
export abstract class Plugin<O = any> implements PluginInterface {
|
||||
options: O;
|
||||
app: Application;
|
||||
callbacks: IPlugin = {};
|
||||
db: Database;
|
||||
|
||||
constructor(options?: PluginOptions & { app?: Application }) {
|
||||
this.app = options?.app;
|
||||
this.options = options;
|
||||
this.callbacks = _.pick(options, ['load', 'activate']);
|
||||
constructor(app: Application, options?: O) {
|
||||
this.app = app;
|
||||
this.db = app.db;
|
||||
this.setOptions(options);
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.options.name || uid();
|
||||
setOptions(options: O) {
|
||||
this.options = options || ({} as any);
|
||||
}
|
||||
|
||||
async activate() {
|
||||
this.options.activate = true;
|
||||
getName(): string {
|
||||
return this.constructor.name;
|
||||
}
|
||||
|
||||
async install() {
|
||||
await this.call('install');
|
||||
}
|
||||
beforeLoad() {}
|
||||
|
||||
async call(name: string) {
|
||||
if (!this.callbacks[name]) {
|
||||
return;
|
||||
}
|
||||
const callback = this.callbacks[name].bind(this);
|
||||
await callback();
|
||||
}
|
||||
|
||||
async load() {
|
||||
await this.call('load');
|
||||
}
|
||||
abstract load();
|
||||
}
|
||||
|
@ -55,14 +55,9 @@ interface Resource {
|
||||
}
|
||||
|
||||
export class MockServer extends Application {
|
||||
async loadAndSync() {
|
||||
async loadAndInstall() {
|
||||
await this.load();
|
||||
await this.db.sync({
|
||||
force: false,
|
||||
alter: {
|
||||
drop: false,
|
||||
},
|
||||
});
|
||||
await this.install({ clean: true });
|
||||
}
|
||||
|
||||
async cleanDb() {
|
||||
|
Loading…
Reference in New Issue
Block a user