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 path from 'path';
|
||||||
import { Model } from '..';
|
import { Model } from '..';
|
||||||
|
import { mockDatabase } from './index';
|
||||||
|
|
||||||
describe('database', () => {
|
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 () => {
|
test('get repository', async () => {
|
||||||
const db = mockDatabase();
|
const db = mockDatabase();
|
||||||
db.collection({
|
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 { applyMixins, AsyncEmitter } from '@nocobase/utils';
|
||||||
|
|
||||||
import merge from 'deepmerge';
|
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 { 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 extendOperators from './operators';
|
||||||
import { Repository } from './repository';
|
|
||||||
import { RelationRepository } from './relation-repository/relation-repository';
|
import { RelationRepository } from './relation-repository/relation-repository';
|
||||||
|
import { Repository } from './repository';
|
||||||
|
|
||||||
export interface MergeOptions extends merge.Options {}
|
export interface MergeOptions extends merge.Options {}
|
||||||
|
|
||||||
@ -33,6 +30,10 @@ interface RegisterOperatorsContext {
|
|||||||
field?: Field;
|
field?: Field;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CleanOptions extends QueryInterfaceDropAllTablesOptions {
|
||||||
|
drop?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
type OperatorFunc = (value: any, ctx?: RegisterOperatorsContext) => any;
|
type OperatorFunc = (value: any, ctx?: RegisterOperatorsContext) => any;
|
||||||
|
|
||||||
export class Database extends EventEmitter implements AsyncEmitter {
|
export class Database extends EventEmitter implements AsyncEmitter {
|
||||||
@ -219,6 +220,29 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|||||||
return result;
|
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() {
|
async close() {
|
||||||
return this.sequelize.close();
|
return this.sequelize.close();
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
export * from './database';
|
export { Model, ModelCtor, SyncOptions } from 'sequelize';
|
||||||
export * from './collection';
|
export * from './collection';
|
||||||
export * from './repository';
|
export * from './database';
|
||||||
export { Database as default } 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-many-repository';
|
||||||
export * from './relation-repository/belongs-to-repository';
|
export * from './relation-repository/belongs-to-repository';
|
||||||
export * from './relation-repository/hasmany-repository';
|
export * from './relation-repository/hasmany-repository';
|
||||||
export * from './relation-repository/single-relation-repository';
|
|
||||||
export * from './relation-repository/multiple-relation-repository';
|
export * from './relation-repository/multiple-relation-repository';
|
||||||
|
export * from './relation-repository/single-relation-repository';
|
||||||
export { Model, ModelCtor } from 'sequelize';
|
export * from './repository';
|
||||||
export * from './fields';
|
|
||||||
export * from './update-associations';
|
export * from './update-associations';
|
||||||
export * from './magic-attribute-model';
|
|
||||||
|
|
||||||
|
@ -19,7 +19,6 @@ export async function prepareApp() {
|
|||||||
registerActions: true,
|
registerActions: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
await app.cleanDb();
|
|
||||||
app.plugin(PluginUiSchema);
|
app.plugin(PluginUiSchema);
|
||||||
app.plugin(PluginCollectionManager);
|
app.plugin(PluginCollectionManager);
|
||||||
|
|
||||||
@ -30,7 +29,7 @@ export async function prepareApp() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.plugin(PluginACL);
|
app.plugin(PluginACL);
|
||||||
await app.loadAndSync();
|
await app.loadAndInstall();
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,9 @@ import supertest from 'supertest';
|
|||||||
import { Application } from '../application';
|
import { Application } from '../application';
|
||||||
import { Plugin } from '../plugin';
|
import { Plugin } from '../plugin';
|
||||||
|
|
||||||
class MyPlugin extends Plugin {}
|
class MyPlugin extends Plugin {
|
||||||
|
load() {}
|
||||||
|
}
|
||||||
|
|
||||||
describe('application', () => {
|
describe('application', () => {
|
||||||
let app: 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 { Application } from '../application';
|
||||||
import { Plugin } from '../plugin';
|
import { Plugin } from '../plugin';
|
||||||
import path from 'path';
|
import Plugin1 from './plugins/plugin1';
|
||||||
|
import Plugin2 from './plugins/plugin2';
|
||||||
import Plugin3 from './plugins/plugin3';
|
import Plugin3 from './plugins/plugin3';
|
||||||
|
|
||||||
describe('plugin', () => {
|
describe('plugin', () => {
|
||||||
@ -29,101 +29,55 @@ describe('plugin', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
return app.db.close();
|
await app.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('define', () => {
|
describe('define', () => {
|
||||||
it('plugin name', async () => {
|
it('should add plugin with options', async () => {
|
||||||
const plugin = app.plugin(function abc() {});
|
class MyPlugin extends Plugin {
|
||||||
expect(plugin).toBeInstanceOf(Plugin);
|
load() {}
|
||||||
expect(plugin.getName()).toBe('abc');
|
}
|
||||||
|
|
||||||
|
const plugin = app.plugin(MyPlugin, {
|
||||||
|
test: 'hello',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(plugin.options['test']).toEqual('hello');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('plugin name', async () => {
|
it('plugin name', async () => {
|
||||||
const plugin = app.plugin({
|
interface Options {
|
||||||
name: 'plugin-name2',
|
a?: string;
|
||||||
async load() {},
|
}
|
||||||
|
class MyPlugin extends Plugin<Options> {
|
||||||
|
load() {
|
||||||
|
this.options.a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const plugin = app.plugin<Options>(MyPlugin, {
|
||||||
|
a: 'aa',
|
||||||
});
|
});
|
||||||
expect(plugin).toBeInstanceOf(Plugin);
|
plugin.setOptions({
|
||||||
expect(plugin.getName()).toBe('plugin-name2');
|
a: 'a'
|
||||||
});
|
})
|
||||||
|
|
||||||
it('plugin name', async () => {
|
|
||||||
const plugin = app.plugin({
|
|
||||||
name: 'plugin-name3',
|
|
||||||
load: function () {},
|
|
||||||
});
|
|
||||||
expect(plugin).toBeInstanceOf(Plugin);
|
|
||||||
expect(plugin.getName()).toBe('plugin-name3');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('plugin name', async () => {
|
|
||||||
class MyPlugin extends Plugin {}
|
|
||||||
const plugin = app.plugin(MyPlugin);
|
|
||||||
expect(plugin).toBeInstanceOf(MyPlugin);
|
expect(plugin).toBeInstanceOf(MyPlugin);
|
||||||
expect(plugin.getName()).toBe('MyPlugin');
|
expect(plugin.getName()).toBe('MyPlugin');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('plugin name', async () => {
|
it('plugin name', async () => {
|
||||||
class MyPlugin extends Plugin {}
|
const plugin = app.plugin(Plugin1);
|
||||||
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'));
|
|
||||||
expect(plugin).toBeInstanceOf(Plugin);
|
expect(plugin).toBeInstanceOf(Plugin);
|
||||||
expect(plugin.getName()).toBe('abc');
|
expect(plugin.getName()).toBe('Plugin1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('plugin name', async () => {
|
it('plugin name', async () => {
|
||||||
const plugin = app.plugin(path.resolve(__dirname, './plugins/plugin3'));
|
const plugin = app.plugin(Plugin3);
|
||||||
expect(plugin).toBeInstanceOf(Plugin3);
|
expect(plugin).toBeInstanceOf(Plugin3);
|
||||||
expect(plugin.getName()).toBe('Plugin3');
|
expect(plugin.getName()).toBe('Plugin3');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('load', () => {
|
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 () => {
|
it('plugin load', async () => {
|
||||||
app.plugin(
|
app.plugin(
|
||||||
class MyPlugin extends Plugin {
|
class MyPlugin extends Plugin {
|
||||||
@ -140,21 +94,21 @@ describe('plugin', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('plugin load', async () => {
|
it('plugin load', async () => {
|
||||||
app.plugin(path.resolve(__dirname, './plugins/plugin1'));
|
app.plugin(Plugin1);
|
||||||
await app.load();
|
await app.load();
|
||||||
const Test = app.db.getCollection('tests');
|
const Test = app.db.getCollection('tests');
|
||||||
expect(Test).toBeDefined();
|
expect(Test).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('plugin load', async () => {
|
it('plugin load', async () => {
|
||||||
app.plugin(path.resolve(__dirname, './plugins/plugin2'));
|
app.plugin(Plugin2);
|
||||||
await app.load();
|
await app.load();
|
||||||
const Test = app.db.getCollection('tests');
|
const Test = app.db.getCollection('tests');
|
||||||
expect(Test).toBeDefined();
|
expect(Test).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('plugin load', async () => {
|
it('plugin load', async () => {
|
||||||
app.plugin(path.resolve(__dirname, './plugins/plugin3'));
|
app.plugin(Plugin3);
|
||||||
await app.load();
|
await app.load();
|
||||||
const Test = app.db.getCollection('tests');
|
const Test = app.db.getCollection('tests');
|
||||||
expect(Test).toBeDefined();
|
expect(Test).toBeDefined();
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { Plugin } from '../../plugin';
|
import { Plugin } from '../../plugin';
|
||||||
|
|
||||||
export default function abc(this: Plugin) {
|
export default class Plugin1 extends Plugin {
|
||||||
|
async load() {
|
||||||
this.app.collection({
|
this.app.collection({
|
||||||
name: 'tests',
|
name: 'tests',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { IPlugin } from '../../plugin';
|
import { Plugin } from '../../plugin';
|
||||||
|
|
||||||
export default {
|
export default class Plugin2 extends Plugin {
|
||||||
load() {
|
async load() {
|
||||||
this.app.collection({
|
this.app.collection({
|
||||||
name: 'tests',
|
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 { 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 { 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 {
|
export interface ResourcerOptions {
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
@ -45,7 +49,31 @@ interface ActionsOptions {
|
|||||||
resourceNames?: string[];
|
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 db: Database;
|
||||||
|
|
||||||
public readonly resourcer: Resourcer;
|
public readonly resourcer: Resourcer;
|
||||||
@ -54,8 +82,12 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
|||||||
|
|
||||||
public readonly i18n: i18n;
|
public readonly i18n: i18n;
|
||||||
|
|
||||||
|
public readonly pm: PluginManager;
|
||||||
|
|
||||||
protected plugins = new Map<string, Plugin>();
|
protected plugins = new Map<string, Plugin>();
|
||||||
|
|
||||||
|
public listenServer: Server;
|
||||||
|
|
||||||
constructor(options: ApplicationOptions) {
|
constructor(options: ApplicationOptions) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@ -64,12 +96,20 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
|||||||
this.cli = createCli(this, options);
|
this.cli = createCli(this, options);
|
||||||
this.i18n = createI18n(options);
|
this.i18n = createI18n(options);
|
||||||
|
|
||||||
|
this.pm = new PluginManager({
|
||||||
|
app: this,
|
||||||
|
});
|
||||||
|
|
||||||
registerMiddlewares(this, options);
|
registerMiddlewares(this, options);
|
||||||
if (options.registerActions !== false) {
|
if (options.registerActions !== false) {
|
||||||
registerActions(this);
|
registerActions(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
plugin<O = any>(pluginClass: any, options?: O): Plugin<O> {
|
||||||
|
return this.pm.add(pluginClass, options);
|
||||||
|
}
|
||||||
|
|
||||||
use<NewStateT = {}, NewContextT = {}>(
|
use<NewStateT = {}, NewContextT = {}>(
|
||||||
middleware: Koa.Middleware<StateT & NewStateT, ContextT & NewContextT>,
|
middleware: Koa.Middleware<StateT & NewStateT, ContextT & NewContextT>,
|
||||||
options?: MiddlewareOptions,
|
options?: MiddlewareOptions,
|
||||||
@ -98,110 +138,12 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
|||||||
return (this.cli as any)._findCommand(name);
|
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() {
|
async load() {
|
||||||
await this.emitAsync('plugins.beforeLoad');
|
await this.pm.load();
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getPlugin<P extends Plugin>(name: string) {
|
getPlugin<P extends Plugin>(name: string) {
|
||||||
return this.plugins.get(name) as P;
|
return this.pm.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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async parse(argv = process.argv) {
|
async parse(argv = process.argv) {
|
||||||
@ -209,9 +151,73 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
|
|||||||
return this.cli.parseAsync(argv);
|
return this.cli.parseAsync(argv);
|
||||||
}
|
}
|
||||||
|
|
||||||
async destroy() {
|
async start(options?: StartOptions) {
|
||||||
await this.db.close();
|
// 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;
|
export default Application;
|
||||||
|
@ -86,7 +86,7 @@ export function createCli(app: Application, options: ApplicationOptions) {
|
|||||||
const cli = args.pop();
|
const cli = args.pop();
|
||||||
const opts = cli.opts();
|
const opts = cli.opts();
|
||||||
await app.emitAsync('beforeStart');
|
await app.emitAsync('beforeStart');
|
||||||
app.listen(opts.port || 3000);
|
await app.start(opts.port || 3000);
|
||||||
console.log(`http://localhost:${opts.port || 3000}/`);
|
console.log(`http://localhost:${opts.port || 3000}/`);
|
||||||
});
|
});
|
||||||
return cli;
|
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 { Application } from './application';
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
export interface IPlugin {
|
export interface PluginInterface {
|
||||||
install?: (this: Plugin) => void;
|
beforeLoad?: () => void;
|
||||||
load?: (this: Plugin) => void;
|
load();
|
||||||
|
getName(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginOptions {
|
export interface PluginOptions {
|
||||||
name?: string;
|
|
||||||
activate?: boolean;
|
activate?: boolean;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@ -19,42 +18,28 @@ export interface PluginOptions {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PluginFn = (this: Plugin) => void;
|
export type PluginType = typeof Plugin;
|
||||||
|
|
||||||
export type PluginType = string | PluginFn | typeof Plugin;
|
export abstract class Plugin<O = any> implements PluginInterface {
|
||||||
|
options: O;
|
||||||
export class Plugin implements IPlugin {
|
|
||||||
options: PluginOptions = {};
|
|
||||||
app: Application;
|
app: Application;
|
||||||
callbacks: IPlugin = {};
|
db: Database;
|
||||||
|
|
||||||
constructor(options?: PluginOptions & { app?: Application }) {
|
constructor(app: Application, options?: O) {
|
||||||
this.app = options?.app;
|
this.app = app;
|
||||||
this.options = options;
|
this.db = app.db;
|
||||||
this.callbacks = _.pick(options, ['load', 'activate']);
|
this.setOptions(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
setOptions(options: O) {
|
||||||
return this.options.name || uid();
|
this.options = options || ({} as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
async activate() {
|
getName(): string {
|
||||||
this.options.activate = true;
|
return this.constructor.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
async install() {
|
beforeLoad() {}
|
||||||
await this.call('install');
|
|
||||||
}
|
|
||||||
|
|
||||||
async call(name: string) {
|
abstract load();
|
||||||
if (!this.callbacks[name]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const callback = this.callbacks[name].bind(this);
|
|
||||||
await callback();
|
|
||||||
}
|
|
||||||
|
|
||||||
async load() {
|
|
||||||
await this.call('load');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -55,14 +55,9 @@ interface Resource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class MockServer extends Application {
|
export class MockServer extends Application {
|
||||||
async loadAndSync() {
|
async loadAndInstall() {
|
||||||
await this.load();
|
await this.load();
|
||||||
await this.db.sync({
|
await this.install({ clean: true });
|
||||||
force: false,
|
|
||||||
alter: {
|
|
||||||
drop: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanDb() {
|
async cleanDb() {
|
||||||
|
Loading…
Reference in New Issue
Block a user