2020-11-11 18:16:18 +08:00
|
|
|
import Koa from 'koa';
|
2021-09-05 23:59:38 +08:00
|
|
|
import cors from '@koa/cors';
|
|
|
|
import bodyParser from 'koa-bodyparser';
|
2021-09-14 11:09:26 +08:00
|
|
|
import { Command, CommandOptions } from 'commander';
|
|
|
|
import Database, { DatabaseOptions, TableOptions } from '@nocobase/database';
|
|
|
|
import Resourcer, { ResourceOptions } from '@nocobase/resourcer';
|
2021-09-09 13:05:33 +08:00
|
|
|
import { dataWrapping, table2resource } from './middlewares';
|
2021-09-14 11:09:26 +08:00
|
|
|
import { PluginType, Plugin, PluginOptions } from './plugin';
|
2021-09-16 00:38:48 +08:00
|
|
|
import { registerActions } from '@nocobase/actions';
|
2021-09-09 13:05:33 +08:00
|
|
|
|
|
|
|
export interface ResourcerOptions {
|
|
|
|
prefix?: string;
|
|
|
|
}
|
2020-11-12 11:48:27 +08:00
|
|
|
|
|
|
|
export interface ApplicationOptions {
|
2021-09-09 13:05:33 +08:00
|
|
|
database?: DatabaseOptions;
|
|
|
|
resourcer?: ResourcerOptions;
|
|
|
|
bodyParser?: any;
|
|
|
|
cors?: any;
|
|
|
|
dataWrapping?: boolean;
|
2020-11-12 11:48:27 +08:00
|
|
|
}
|
2020-11-11 18:16:18 +08:00
|
|
|
|
2021-09-16 15:57:21 +08:00
|
|
|
interface DefaultState {
|
|
|
|
currentUser?: any;
|
|
|
|
[key: string]: any;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface DefaultContext {
|
|
|
|
db: Database;
|
|
|
|
resourcer: Resourcer;
|
|
|
|
[key: string]: any;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface MiddlewareOptions {
|
|
|
|
name?: string;
|
|
|
|
resourceName?: string;
|
|
|
|
resourceNames?: string[];
|
|
|
|
insertBefore?: string;
|
|
|
|
insertAfter?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface ActionsOptions {
|
|
|
|
resourceName?: string;
|
|
|
|
resourceNames?: string[];
|
|
|
|
}
|
|
|
|
|
|
|
|
export class Application<
|
|
|
|
StateT = DefaultState,
|
|
|
|
ContextT = DefaultContext
|
|
|
|
> extends Koa {
|
2021-09-16 00:38:48 +08:00
|
|
|
|
2021-09-14 11:09:26 +08:00
|
|
|
public readonly db: Database;
|
2021-01-13 16:23:15 +08:00
|
|
|
|
|
|
|
public readonly resourcer: Resourcer;
|
2020-11-11 18:16:18 +08:00
|
|
|
|
2021-09-05 23:59:38 +08:00
|
|
|
public readonly cli: Command;
|
|
|
|
|
2021-09-14 11:09:26 +08:00
|
|
|
protected plugins = new Map<string, Plugin>();
|
2020-11-12 11:48:27 +08:00
|
|
|
|
2021-01-13 16:23:15 +08:00
|
|
|
constructor(options: ApplicationOptions) {
|
|
|
|
super();
|
2021-09-09 13:05:33 +08:00
|
|
|
|
2021-09-16 00:38:48 +08:00
|
|
|
if (options.database instanceof Database) {
|
|
|
|
this.db = options.database;
|
|
|
|
} else {
|
|
|
|
this.db = new Database(options.database);
|
|
|
|
}
|
|
|
|
|
2021-09-09 13:05:33 +08:00
|
|
|
this.resourcer = new Resourcer({ ...options.resourcer });
|
2021-09-05 23:59:38 +08:00
|
|
|
this.cli = new Command();
|
|
|
|
|
2021-09-28 18:20:21 +08:00
|
|
|
if (options.bodyParser !== false) {
|
|
|
|
this.use(
|
|
|
|
bodyParser({
|
|
|
|
...options.bodyParser,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
2021-09-05 23:59:38 +08:00
|
|
|
|
2021-09-09 13:05:33 +08:00
|
|
|
this.use(
|
|
|
|
cors({
|
|
|
|
exposeHeaders: ['content-disposition'],
|
|
|
|
...options.cors,
|
|
|
|
}),
|
|
|
|
);
|
2021-09-05 23:59:38 +08:00
|
|
|
|
2021-09-16 15:57:21 +08:00
|
|
|
this.use<DefaultState, DefaultContext>(async (ctx, next) => {
|
2021-09-14 11:09:26 +08:00
|
|
|
ctx.db = this.db;
|
2021-09-09 13:05:33 +08:00
|
|
|
ctx.resourcer = this.resourcer;
|
2021-09-05 23:59:38 +08:00
|
|
|
await next();
|
|
|
|
});
|
|
|
|
|
2021-09-09 13:05:33 +08:00
|
|
|
if (options.dataWrapping !== false) {
|
2021-09-16 00:38:48 +08:00
|
|
|
this.use(dataWrapping());
|
2021-09-09 13:05:33 +08:00
|
|
|
}
|
2021-09-05 23:59:38 +08:00
|
|
|
|
2021-09-16 00:38:48 +08:00
|
|
|
this.use(table2resource());
|
2021-09-09 13:05:33 +08:00
|
|
|
this.use(this.resourcer.restApiMiddleware());
|
2021-09-05 23:59:38 +08:00
|
|
|
|
2021-09-16 00:38:48 +08:00
|
|
|
registerActions(this);
|
|
|
|
|
2021-09-05 23:59:38 +08:00
|
|
|
this.cli
|
2021-09-16 15:57:21 +08:00
|
|
|
.command('db:sync')
|
2021-09-05 23:59:38 +08:00
|
|
|
.option('-f, --force')
|
|
|
|
.action(async (...args) => {
|
2021-09-16 00:38:48 +08:00
|
|
|
console.log('db sync...');
|
2021-09-05 23:59:38 +08:00
|
|
|
const cli = args.pop();
|
2021-09-14 11:09:26 +08:00
|
|
|
const force = cli.opts()?.force;
|
|
|
|
await this.db.sync(
|
|
|
|
force
|
|
|
|
? {
|
2021-09-16 00:38:48 +08:00
|
|
|
force: true,
|
|
|
|
alter: {
|
|
|
|
drop: true,
|
|
|
|
},
|
|
|
|
}
|
2021-09-14 11:09:26 +08:00
|
|
|
: {},
|
|
|
|
);
|
|
|
|
await this.destroy();
|
2021-09-05 23:59:38 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
this.cli
|
2021-09-16 15:57:21 +08:00
|
|
|
.command('init')
|
2021-09-05 23:59:38 +08:00
|
|
|
// .option('-f, --force')
|
|
|
|
.action(async (...args) => {
|
|
|
|
const cli = args.pop();
|
2021-09-14 11:09:26 +08:00
|
|
|
await this.db.sync({
|
|
|
|
force: true,
|
|
|
|
alter: {
|
|
|
|
drop: true,
|
|
|
|
},
|
|
|
|
});
|
2021-09-05 23:59:38 +08:00
|
|
|
await this.emitAsync('db.init');
|
2021-09-09 13:05:33 +08:00
|
|
|
await this.destroy();
|
2021-09-05 23:59:38 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
this.cli
|
|
|
|
.command('start')
|
|
|
|
.option('-p, --port [port]')
|
|
|
|
.action(async (...args) => {
|
|
|
|
const cli = args.pop();
|
|
|
|
console.log(args);
|
|
|
|
const opts = cli.opts();
|
2021-09-23 00:16:04 +08:00
|
|
|
await this.emitAsync('beforeStart');
|
2021-09-05 23:59:38 +08:00
|
|
|
this.listen(opts.port || 3000);
|
|
|
|
console.log(`http://localhost:${opts.port || 3000}/`);
|
|
|
|
});
|
2020-12-18 19:54:53 +08:00
|
|
|
}
|
|
|
|
|
2021-09-16 15:57:21 +08:00
|
|
|
use<NewStateT = {}, NewContextT = {}>(
|
|
|
|
middleware: Koa.Middleware<StateT & NewStateT, ContextT & NewContextT>,
|
|
|
|
options?: MiddlewareOptions,
|
2021-09-23 00:16:04 +08:00
|
|
|
) {
|
2021-09-16 15:57:21 +08:00
|
|
|
// @ts-ignore
|
|
|
|
return super.use(middleware);
|
|
|
|
}
|
|
|
|
|
2021-09-14 11:09:26 +08:00
|
|
|
collection(options: TableOptions) {
|
|
|
|
return this.db.table(options);
|
|
|
|
}
|
|
|
|
|
|
|
|
resource(options: ResourceOptions) {
|
|
|
|
return this.resourcer.define(options);
|
|
|
|
}
|
|
|
|
|
2021-09-16 15:57:21 +08:00
|
|
|
actions(handlers: any, options?: ActionsOptions) {
|
2021-09-16 00:38:48 +08:00
|
|
|
return this.resourcer.registerActions(handlers);
|
|
|
|
}
|
|
|
|
|
2021-09-14 11:09:26 +08:00
|
|
|
command(nameAndArgs: string, opts?: CommandOptions) {
|
|
|
|
return this.cli.command(nameAndArgs, opts);
|
|
|
|
}
|
|
|
|
|
2021-09-23 00:16:04 +08:00
|
|
|
plugin(options?: PluginType | PluginOptions): Plugin {
|
|
|
|
if (typeof options === 'string') {
|
|
|
|
return this.plugin(require(options).default);
|
2021-09-14 11:09:26 +08:00
|
|
|
}
|
|
|
|
let instance: Plugin;
|
2021-09-23 00:16:04 +08:00
|
|
|
if (typeof options === 'function') {
|
|
|
|
try {
|
2021-09-14 11:09:26 +08:00
|
|
|
// @ts-ignore
|
2021-09-23 00:16:04 +08:00
|
|
|
instance = new options({
|
|
|
|
name: options.name,
|
|
|
|
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,
|
|
|
|
// @ts-ignore
|
|
|
|
load: options,
|
2021-09-14 11:09:26 +08:00
|
|
|
app: this,
|
|
|
|
});
|
|
|
|
}
|
2021-09-23 00:16:04 +08:00
|
|
|
} else if (typeof options === 'object') {
|
|
|
|
const plugin = options.plugin || Plugin;
|
|
|
|
instance = new plugin({
|
|
|
|
name: options.plugin ? plugin.name : undefined,
|
2021-09-14 11:09:26 +08:00
|
|
|
...options,
|
|
|
|
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');
|
|
|
|
}
|
|
|
|
|
2021-09-05 14:17:45 +08:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2021-09-09 13:05:33 +08:00
|
|
|
if (cb && (cb instanceof Promise || typeof cb.then === 'function')) {
|
2021-09-05 14:17:45 +08:00
|
|
|
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) => {
|
2021-09-09 13:05:33 +08:00
|
|
|
return run(next).then((result) =>
|
|
|
|
Promise.resolve(res.concat(result)),
|
|
|
|
);
|
2021-09-05 14:17:45 +08:00
|
|
|
});
|
|
|
|
}, Promise.resolve([]));
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2021-09-16 00:38:48 +08:00
|
|
|
async parse(argv = process.argv) {
|
2021-09-28 00:18:09 +08:00
|
|
|
await this.load();
|
2021-09-05 23:59:38 +08:00
|
|
|
return this.cli.parseAsync(argv);
|
2020-12-18 19:54:53 +08:00
|
|
|
}
|
|
|
|
|
2021-09-09 13:05:33 +08:00
|
|
|
async destroy() {
|
2021-09-14 11:09:26 +08:00
|
|
|
await this.db.close();
|
2021-09-09 13:05:33 +08:00
|
|
|
}
|
2020-11-11 18:16:18 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
export default Application;
|