feat(server): improve application (#177)

* feat: getRepository

* getRepository return type

* export action

* refactor(plugin-acl): plugin

* refactor(plugin-action-logs): plugin class

* refactor(plugin-china-region): plugin class

* refactor: plugin class

* fix: cli start command

* feat: pass install-command options into app.install

* fix: cli args

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
ChengLei Shao 2022-02-07 01:14:00 +08:00 committed by GitHub
parent de25f56a79
commit 8e4336cbe1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 168 additions and 171 deletions

View File

@ -82,17 +82,13 @@ export default class PluginACL extends Plugin {
}); });
} }
async load() { async beforeLoad() {
this.app.db.registerModels({
RoleResourceActionModel,
RoleResourceModel,
});
const acl = createACL(); const acl = createACL();
this.acl = acl; this.acl = acl;
await this.app.db.import({ this.app.db.registerModels({
directory: path.resolve(__dirname, 'collections'), RoleResourceActionModel,
RoleResourceModel,
}); });
this.registerAssociationFieldsActions(); this.registerAssociationFieldsActions();
@ -100,8 +96,6 @@ export default class PluginACL extends Plugin {
this.app.resourcer.define(availableActionResource); this.app.resourcer.define(availableActionResource);
this.app.resourcer.define(roleCollectionsResource); this.app.resourcer.define(roleCollectionsResource);
this.app.resourcer.use(this.acl.middleware());
this.app.db.on('roles.afterSave', async (model, options) => { this.app.db.on('roles.afterSave', async (model, options) => {
const { transaction } = options; const { transaction } = options;
const roleName = model.get('name'); const roleName = model.get('name');
@ -187,4 +181,12 @@ export default class PluginACL extends Plugin {
} }
}); });
} }
async load() {
await this.app.db.import({
directory: path.resolve(__dirname, 'collections'),
});
this.app.resourcer.use(this.acl.middleware());
}
} }

View File

@ -1,16 +1,17 @@
import path from 'path'; import path from 'path';
import { Plugin } from '@nocobase/server';
import { afterCreate, afterUpdate, afterDestroy } from './hooks'; import { afterCreate, afterUpdate, afterDestroy } from './hooks';
import { Plugin } from '@nocobase/server';
export default class PluginActionLogs extends Plugin {
async beforeLoad() {
this.db.on('afterCreate', afterCreate);
this.db.on('afterUpdate', afterUpdate);
this.db.on('afterDestroy', afterDestroy);
}
export default class extends Plugin {
name: 'action-logs';
async load() { async load() {
const database = this.app.db; await this.db.import({
await database.import({
directory: path.resolve(__dirname, 'collections'), directory: path.resolve(__dirname, 'collections'),
}); });
database.on('afterCreate', afterCreate);
database.on('afterUpdate', afterUpdate);
database.on('afterDestroy', afterDestroy);
} }
}; }

View File

@ -51,18 +51,17 @@ async function importData(model) {
console.log(`${count} rows of region data imported in ${(Date.now() - timer) / 1000}s`); console.log(`${count} rows of region data imported in ${(Date.now() - timer) / 1000}s`);
} }
export default { export default class PluginChinaRegion extends Plugin {
name: 'china-region', async beforeLoad() {
async load(this: Plugin) { this.app.on('installing', async () => {
const db = this.app.db; const ChinaRegion = this.db.getCollection('china_regions').model;
await db.import({
directory: path.resolve(__dirname, 'collections'),
});
this.app.on('db.init', async () => {
const ChinaRegion = db.getCollection('china_regions').model;
await importData(ChinaRegion); await importData(ChinaRegion);
}); });
}, }
};
async load() {
await this.db.import({
directory: path.resolve(__dirname, 'collections'),
});
}
}

View File

@ -10,16 +10,12 @@ import { afterCreateForReverseField } from './hooks/afterCreateForReverseField';
export * from './repositories/collection-repository'; export * from './repositories/collection-repository';
export default class CollectionManagerPlugin extends Plugin { export default class CollectionManagerPlugin extends Plugin {
async load() { async beforeLoad() {
this.app.db.registerModels({ this.app.db.registerModels({
CollectionModel, CollectionModel,
FieldModel, FieldModel,
}); });
await this.app.db.import({
directory: path.resolve(__dirname, './collections'),
});
// 要在 beforeInitOptions 之前处理 // 要在 beforeInitOptions 之前处理
this.app.db.on('fields.beforeCreate', beforeCreateForReverseField(this.app.db)); this.app.db.on('fields.beforeCreate', beforeCreateForReverseField(this.app.db));
this.app.db.on('fields.beforeCreate', beforeCreateForChildrenCollection(this.app.db)); this.app.db.on('fields.beforeCreate', beforeCreateForChildrenCollection(this.app.db));
@ -47,4 +43,10 @@ export default class CollectionManagerPlugin extends Plugin {
} }
}); });
} }
async load() {
await this.app.db.import({
directory: path.resolve(__dirname, './collections'),
});
}
} }

View File

@ -1,44 +1,41 @@
import path from 'path'; import path from 'path';
import Database from '@nocobase/database'; import { Plugin } from '@nocobase/server';
import Resourcer from '@nocobase/resourcer';
import { PluginOptions } from '@nocobase/server';
import { action as uploadAction, middleware as uploadMiddleware } from './actions/upload'; import { action as uploadAction, middleware as uploadMiddleware } from './actions/upload';
import { getStorageConfig } from './storages'; import { getStorageConfig } from './storages';
import { STORAGE_TYPE_LOCAL } from './constants'; import { STORAGE_TYPE_LOCAL } from './constants';
export default { export default class PluginFileManager extends Plugin {
name: 'file-manager', storageType() {
async load() { return process.env.DEFAULT_STORAGE_TYPE;
const database: Database = this.app.db;
const resourcer: Resourcer = this.app.resourcer;
await database.import({
directory: path.resolve(__dirname, 'collections'),
});
// 暂时中间件只能通过 use 加进来
resourcer.use(uploadMiddleware);
resourcer.registerActionHandler('upload', uploadAction);
const { DEFAULT_STORAGE_TYPE } = process.env;
if (process.env.NOCOBASE_ENV !== 'production') {
await getStorageConfig(STORAGE_TYPE_LOCAL).middleware(this.app);
} }
async beforeLoad() {
this.app.on('db.init', async () => { this.app.on('installing', async () => {
const defaultStorageConfig = getStorageConfig(DEFAULT_STORAGE_TYPE); const defaultStorageConfig = getStorageConfig(this.storageType());
if (defaultStorageConfig) { if (defaultStorageConfig) {
const Storage = database.getCollection('storages'); const Storage = this.db.getCollection('storages');
await Storage.repository.create({ await Storage.repository.create({
values: { values: {
...defaultStorageConfig.defaults(), ...defaultStorageConfig.defaults(),
type: DEFAULT_STORAGE_TYPE, type: this.storageType(),
default: true, default: true,
}, },
}); });
} }
}); });
}, }
} as PluginOptions;
async load() {
await this.db.import({
directory: path.resolve(__dirname, 'collections'),
});
// 暂时中间件只能通过 use 加进来
this.app.resourcer.use(uploadMiddleware);
this.app.resourcer.registerActionHandler('upload', uploadAction);
if (process.env.NOCOBASE_ENV !== 'production') {
await getStorageConfig(STORAGE_TYPE_LOCAL).middleware(this.app);
}
}
}

View File

@ -1,11 +1,10 @@
import path from 'path'; import path from 'path';
import { PluginOptions } from '@nocobase/server'; import { Plugin } from '@nocobase/server';
export default { export default class PluginNotifications extends Plugin {
name: 'notifications',
async load() { async load() {
await this.app.db.import({ await this.app.db.import({
directory: path.resolve(__dirname, 'collections'), directory: path.resolve(__dirname, 'collections'),
}); });
} }
} as PluginOptions }

View File

@ -1,19 +1,10 @@
import path from 'path'; import path from 'path';
import { PluginOptions } from '@nocobase/server'; import { Plugin, PluginOptions } from '@nocobase/server';
export default { export default class PluginSystemSettings extends Plugin {
name: 'system-settings', async beforeLoad() {
async load() { this.app.on('installing', async () => {
const database = this.app.db; await this.db.getRepository('system_settings').create({
await database.import({
directory: path.resolve(__dirname, 'collections'),
});
const SystemSetting = database.getCollection('system_settings');
this.app.on('db.init', async () => {
await SystemSetting.repository.create({
values: { values: {
title: 'NocoBase', title: 'NocoBase',
logo: { logo: {
@ -26,5 +17,11 @@ export default {
}, },
}); });
}); });
}, }
} as PluginOptions;
async load() {
await this.app.db.import({
directory: path.resolve(__dirname, 'collections'),
});
}
}

View File

@ -1,6 +1,6 @@
import { mockServer, MockServer } from '@nocobase/test'; import { mockServer, MockServer } from '@nocobase/test';
import { Collection, Database } from '@nocobase/database'; import { Collection, Database } from '@nocobase/database';
import PluginUiSchema, { UiSchemaRepository } from '@nocobase/plugin-ui-schema'; import PluginUiSchema, { UiSchemaRepository } from '..';
describe('ui schema model', () => { describe('ui schema model', () => {
let app: MockServer; let app: MockServer;

View File

@ -1,7 +1,7 @@
import { ISchema } from '@formily/json-schema'; import { ISchema } from '@formily/json-schema';
import { mockServer, MockServer } from '@nocobase/test'; import { mockServer, MockServer } from '@nocobase/test';
import { Collection, Database } from '@nocobase/database'; import { Collection, Database } from '@nocobase/database';
import PluginUiSchema, { UiSchemaRepository } from '@nocobase/plugin-ui-schema'; import PluginUiSchema, { UiSchemaRepository } from '..';
describe('ui-schema', () => { describe('ui-schema', () => {
let app: MockServer; let app: MockServer;

View File

@ -11,17 +11,13 @@ export default class PluginUiSchema extends Plugin {
}); });
} }
async load() { async beforeLoad() {
const db = this.app.db; const db = this.app.db;
this.app.db.registerModels({ MagicAttributeModel }); this.app.db.registerModels({ MagicAttributeModel });
this.registerRepository(); this.registerRepository();
await db.import({
directory: path.resolve(__dirname, 'collections'),
});
db.on('ui_schemas.beforeCreate', (model) => { db.on('ui_schemas.beforeCreate', (model) => {
model.set('uid', model.get('x-uid')); model.set('uid', model.get('x-uid'));
}); });
@ -44,23 +40,15 @@ export default class PluginUiSchema extends Plugin {
}); });
}); });
await db.getCollection('ui_schemas').sync({
force: false,
alter: {
drop: false,
},
});
await db.getCollection('ui_schema_tree_path').sync({
force: false,
alter: {
drop: false,
},
});
this.app.resourcer.define({ this.app.resourcer.define({
name: 'ui_schemas', name: 'ui_schemas',
actions: uiSchemaActions, actions: uiSchemaActions,
}); });
} }
async load() {
await this.db.import({
directory: path.resolve(__dirname, 'collections'),
});
}
} }

View File

@ -11,7 +11,7 @@ describe('role', () => {
await api.cleanDb(); await api.cleanDb();
api.plugin(require('../server').default); api.plugin(require('../server').default);
api.plugin(PluginACL); api.plugin(PluginACL);
await api.loadAndSync(); await api.loadAndInstall();
db = api.db; db = api.db;
}); });

View File

@ -2,16 +2,12 @@ import path from 'path';
import * as actions from './actions/users'; import * as actions from './actions/users';
import * as middlewares from './middlewares'; import * as middlewares from './middlewares';
import { Collection } from '@nocobase/database'; import { Collection } from '@nocobase/database';
import { PluginOptions } from '@nocobase/server'; import { Plugin, PluginOptions } from '@nocobase/server';
export default { export default class PluginUsers extends Plugin {
name: 'users', async beforeLoad() {
async load() { this.app.on('installing', async () => {
const database = this.app.db; const User = this.db.getCollection('users');
const resourcer = this.app.resourcer;
this.app.on('db.init', async () => {
const User = database.getCollection('users');
await User.model.create({ await User.model.create({
nickname: 'Super Admin', nickname: 'Super Admin',
email: process.env.ADMIN_EMAIL || 'admin@nocobase.com', email: process.env.ADMIN_EMAIL || 'admin@nocobase.com',
@ -19,7 +15,7 @@ export default {
}); });
}); });
database.on('users.afterCreateWithAssociations', async (model, options) => { this.db.on('users.afterCreateWithAssociations', async (model, options) => {
const { transaction } = options; const { transaction } = options;
const defaultRole = await this.app.db.getRepository('roles').findOne({ const defaultRole = await this.app.db.getRepository('roles').findOne({
@ -34,7 +30,7 @@ export default {
} }
}); });
database.on('afterDefineCollection', (collection: Collection) => { this.db.on('afterDefineCollection', (collection: Collection) => {
let { createdBy, updatedBy } = collection.options; let { createdBy, updatedBy } = collection.options;
if (createdBy === true) { if (createdBy === true) {
collection.setField('createdById', { collection.setField('createdById', {
@ -65,14 +61,16 @@ export default {
} }
}); });
await database.import({
directory: path.resolve(__dirname, 'collections'),
});
for (const [key, action] of Object.entries(actions)) { for (const [key, action] of Object.entries(actions)) {
resourcer.registerActionHandler(`users:${key}`, action); this.app.resourcer.registerActionHandler(`users:${key}`, action);
} }
resourcer.use(middlewares.parseToken({})); this.app.resourcer.use(middlewares.parseToken({}));
}, }
} as PluginOptions;
async load() {
await this.db.import({
directory: path.resolve(__dirname, 'collections'),
});
}
}

View File

@ -3,7 +3,7 @@ import { Application } from '../application';
import { Plugin } from '../plugin'; import { Plugin } from '../plugin';
class MyPlugin extends Plugin { class MyPlugin extends Plugin {
load() {} async load() {}
} }
describe('application', () => { describe('application', () => {

View File

@ -65,10 +65,12 @@ interface ListenOptions {
} }
interface StartOptions { interface StartOptions {
cliArgs?: any[];
listen?: ListenOptions; listen?: ListenOptions;
} }
interface InstallOptions { interface InstallOptions {
cliArgs?: any[];
clean?: CleanOptions | boolean; clean?: CleanOptions | boolean;
sync?: SyncOptions; sync?: SyncOptions;
} }
@ -174,8 +176,8 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
await this.emitAsync('afterStart', this, options); await this.emitAsync('afterStart', this, options);
} }
async stop() { async stop(options?: any) {
await this.emitAsync('beforeStop', this); await this.emitAsync('beforeStop', this, options);
// close database connection // close database connection
await this.db.close(); await this.db.close();
@ -188,7 +190,6 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
if (err) { if (err) {
return reject(err); return reject(err);
} }
this.listenServer = null; this.listenServer = null;
resolve(true); resolve(true);
}); });
@ -197,13 +198,13 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
await closeServer(); await closeServer();
} }
await this.emitAsync('afterStop', this); await this.emitAsync('afterStop', this, options);
} }
async destroy() { async destroy(options?: any) {
await this.emitAsync('beforeDestroy', this); await this.emitAsync('beforeDestroy', this, options);
await this.stop(); await this.stop(options);
await this.emitAsync('afterDestroy', this); await this.emitAsync('afterDestroy', this, options);
} }
async install(options?: InstallOptions) { async install(options?: InstallOptions) {
@ -211,7 +212,9 @@ export class Application<StateT = DefaultState, ContextT = DefaultContext> exten
await this.db.clean(isBoolean(options.clean) ? { drop: options.clean } : options.clean); await this.db.clean(isBoolean(options.clean) ? { drop: options.clean } : options.clean);
} }
await this.db.sync(options?.sync); await this.db.sync(options?.sync);
await this.emitAsync('beforeInstall', this, options); await this.emitAsync('beforeInstall', this, options);
await this.emitAsync('installing', this, options);
await this.emitAsync('afterInstall', this, options); await this.emitAsync('afterInstall', this, options);
} }

View File

@ -1,13 +1,13 @@
import { DefaultContext, DefaultState } from 'koa';
import bodyParser from 'koa-bodyparser';
import cors from '@koa/cors'; import cors from '@koa/cors';
import Database from '@nocobase/database'; import Database from '@nocobase/database';
import Resourcer from '@nocobase/resourcer'; import Resourcer from '@nocobase/resourcer';
import { Command } from 'commander'; import { Command } from 'commander';
import i18next from 'i18next';
import { DefaultContext, DefaultState } from 'koa';
import bodyParser from 'koa-bodyparser';
import Application, { ApplicationOptions } from './application'; import Application, { ApplicationOptions } from './application';
import { dataWrapping } from './middlewares/data-wrapping'; import { dataWrapping } from './middlewares/data-wrapping';
import { table2resource } from './middlewares/table2resource'; import { table2resource } from './middlewares/table2resource';
import i18next from 'i18next';
export function createDatabase(options: ApplicationOptions) { export function createDatabase(options: ApplicationOptions) {
if (options.database instanceof Database) { if (options.database instanceof Database) {
@ -37,12 +37,11 @@ export function createCli(app: Application, options: ApplicationOptions) {
cli cli
.command('db:sync') .command('db:sync')
.option('-f, --force') .option('-f, --force')
.action(async (...args) => { .action(async (...cliArgs) => {
const [opts] = cliArgs;
console.log('db sync...'); console.log('db sync...');
const cli = args.pop();
const force = cli.opts()?.force;
await app.db.sync( await app.db.sync(
force opts.force
? { ? {
force: true, force: true,
alter: { alter: {
@ -51,44 +50,44 @@ export function createCli(app: Application, options: ApplicationOptions) {
} }
: {}, : {},
); );
await app.destroy(); await app.stop({
cliArgs,
});
}); });
cli cli
.command('init') .command('install')
.option('-f, --force') .option('-f, --force')
.action(async (opts, ...args) => { .action(async (...cliArgs) => {
if (!opts?.force) { const [opts] = cliArgs;
const tables = await app.db.sequelize.getQueryInterface().showAllTables(); await app.install({
if (tables.includes('collections')) { cliArgs,
console.log('NocoBase is already installed. To reinstall, please execute:'); sync: {
console.log(); force: opts.force,
let command = 'yarn nocobase init --force'; },
for (const [key, value] of Object.entries(opts || {})) { });
command += value === true ? ` --${key}` : ` --${key}=${value}`; await app.stop({
} cliArgs,
console.log(command);
console.log();
return;
}
}
await app.db.sync({
force: true,
}); });
await app.emitAsync('db.init', opts, ...args);
await app.destroy();
}); });
cli cli
.command('start') .command('start')
.option('-p, --port [port]') .option('-p, --port [port]')
.action(async (...args) => { .action(async (...cliArgs) => {
const cli = args.pop(); const [opts] = cliArgs;
const opts = cli.opts(); const port = opts.port || 3000;
await app.emitAsync('beforeStart');
await app.start(opts.port || 3000); await app.start({
console.log(`http://localhost:${opts.port || 3000}/`); cliArgs,
listen: {
port,
},
}); });
console.log(`http://localhost:${port}/`);
});
return cli; return cli;
} }

View File

@ -1,5 +1,6 @@
import { Database } from '@nocobase/database'; import { Database } from '@nocobase/database';
import { Application } from './application'; import { Application } from './application';
import path from 'path';
export interface PluginInterface { export interface PluginInterface {
beforeLoad?: () => void; beforeLoad?: () => void;
@ -41,5 +42,16 @@ export abstract class Plugin<O = any> implements PluginInterface {
beforeLoad() {} beforeLoad() {}
abstract load(); async load() {
const collectionPath = this.collectionPath();
if (collectionPath) {
await this.db.import({
directory: collectionPath,
});
}
}
collectionPath() {
return null;
}
} }