nocobase/packages/database/src/database.ts

473 lines
12 KiB
TypeScript
Raw Normal View History

2020-10-24 07:34:43 +00:00
import {
Options,
Sequelize,
SyncOptions as SequelizeSyncOptions,
} from 'sequelize';
import glob from 'glob';
2021-01-13 07:43:41 +00:00
import Table, { MergeOptions, TableOptions } from './table';
2020-10-24 07:34:43 +00:00
import { Model, ModelCtor } from './model';
import { requireModule } from './utils';
2020-12-15 12:16:55 +00:00
import _ from 'lodash';
import { EventEmitter } from 'events';
2020-10-24 07:34:43 +00:00
export interface SyncOptions extends SequelizeSyncOptions {
/**
* tables
*/
tables?: string[] | Table[] | Map<string, Table>;
}
export interface ImportOptions {
/**
*
*/
directory: string;
/**
* ['js', 'ts', 'json']
*/
extensions?: string[];
}
export interface DatabaseOptions extends Options {
}
2021-09-07 16:18:41 +00:00
// export type HookType = 'beforeTableInit' | 'afterTableInit' | 'beforeAddField' | 'afterAddField';
2020-12-15 12:16:55 +00:00
2021-01-14 02:35:15 +00:00
export class Extend {
tableOptions: TableOptions;
mergeOptions: MergeOptions
constructor(tableOptions: TableOptions, mergeOptions?: MergeOptions) {
this.tableOptions = tableOptions;
this.mergeOptions = mergeOptions;
}
}
export function extend(tableOptions: TableOptions, mergeOptions: MergeOptions = {}) {
return new Extend(tableOptions, mergeOptions);
}
2021-09-07 16:18:41 +00:00
type HookType =
'beforeValidate' |
'afterValidate' |
'beforeCreate' |
'afterCreate' |
'beforeDestroy' |
'afterDestroy' |
'beforeRestore' |
'afterRestore' |
'beforeUpdate' |
'afterUpdate' |
'beforeSave' |
'afterSave' |
'beforeBulkCreate' |
'afterBulkCreate' |
'beforeBulkDestroy' |
'afterBulkDestroy' |
'beforeBulkRestore' |
'afterBulkRestore' |
'beforeBulkUpdate' |
'afterBulkUpdate' |
'beforeSync' |
'afterSync' |
'beforeBulkSync' |
'afterBulkSync' |
'beforeDefine' |
'afterDefine' |
'beforeInit' |
'afterInit' |
'beforeConnect' |
'afterConnect' |
'beforeDisconnect' |
'afterDisconnect';
export default class Database extends EventEmitter {
2020-10-24 07:34:43 +00:00
public readonly sequelize: Sequelize;
/**
* Model
*/
public readonly associating = new Set<string>();
/**
*
*/
public readonly throughTables = new Map<string, Array<string>>();
protected tables = new Map<string, Table>();
protected options: DatabaseOptions;
2020-10-24 07:34:43 +00:00
2020-12-15 12:16:55 +00:00
protected hooks = {};
protected extTableOptions = new Map<string, any>();
2021-09-07 16:18:41 +00:00
protected hookTypes = new Map(Object.entries({
beforeValidate: 1,
afterValidate: 1,
beforeCreate: 1,
afterCreate: 1,
beforeDestroy: 1,
afterDestroy: 1,
beforeRestore: 1,
afterRestore: 1,
beforeUpdate: 1,
afterUpdate: 1,
beforeSave: 1,
afterSave: 1,
beforeBulkCreate: 2,
afterBulkCreate: 2,
beforeBulkDestroy: 3,
afterBulkDestroy: 3,
beforeBulkRestore: 3,
afterBulkRestore: 3,
beforeBulkUpdate: 3,
afterBulkUpdate: 3,
beforeSync: 4,
afterSync: 4,
beforeBulkSync: 4,
afterBulkSync: 4,
beforeDefine: 0,
afterDefine: 0,
beforeInit: 0,
afterInit: 0,
beforeConnect: 0,
afterConnect: 0,
beforeDisconnect: 0,
afterDisconnect: 0,
}));
constructor(options?: DatabaseOptions) {
super();
2020-10-24 07:34:43 +00:00
this.options = options;
this.sequelize = new Sequelize(options);
}
private _getHookType(event: any) {
if (typeof event === 'string') {
event = event.split('.');
}
if (!Array.isArray(event)) {
return;
}
const hookType = [...event].pop();
if (!this.hookTypes.has(hookType)) {
return;
}
return hookType;
}
2021-09-07 16:18:41 +00:00
on(event: HookType | Omit<string, HookType> | symbol, listener: (...args: any[]) => void) {
const hookType = this._getHookType(event);
if (hookType) {
2021-09-07 16:18:41 +00:00
const state = this.hookTypes.get(hookType);
2021-09-08 06:36:18 +00:00
console.log('sequelize.addHook', event, hookType)
2021-09-07 16:18:41 +00:00
this.sequelize.addHook(hookType, async (...args: any[]) => {
let modelName: string;
switch (state) {
case 1:
modelName = args?.[0]?.constructor?.name;
break;
case 2:
modelName = args?.[1]?.model?.name;
break;
case 3:
modelName = args?.[0]?.model?.name;
break;
}
2021-09-08 06:36:18 +00:00
// console.log({ modelName, args });
2021-09-07 16:18:41 +00:00
if (modelName) {
await this.emitAsync(`${modelName}.${hookType}`, ...args);
}
await this.emitAsync(hookType, ...args);
});
this.hookTypes.delete(hookType);
}
2021-09-07 16:18:41 +00:00
return super.on(event as any, listener);
}
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;
}
2020-10-24 07:34:43 +00:00
/**
* tables
*
* TODO: 配置的文件驱动现在会全部初始化
*
* @param {object} [options]
* @param {string} [options.directory]
* @param {array} [options.extensions = ['js', 'ts', 'json']]
*/
public import(options: ImportOptions): Map<string, Table> {
const { extensions = ['js', 'ts', 'json'], directory } = options;
const patten = `${directory}/*.{${extensions.join(',')}}`;
const files = glob.sync(patten, {
ignore: [
'**/*.d.ts'
]
});
2020-10-24 07:34:43 +00:00
const tables = new Map<string, Table>();
files.forEach((file: string) => {
2021-01-13 07:43:41 +00:00
const result = requireModule(file);
2021-01-14 02:35:15 +00:00
if (result instanceof Extend) {
// 如果还没初始化extend 的先暂存起来,后续处理
if (!this.tables.has(result.tableOptions.name)) {
this.extTableOptions.set(result.tableOptions.name, result);
} else {
const table = this.extend(result.tableOptions, result.mergeOptions);
tables.set(table.getName(), table);
}
2021-01-14 02:35:15 +00:00
} else {
let table = this.extend(typeof result === 'function' ? result(this) : result);
// 如果有未处理的 extend 取回来合并
if (this.extTableOptions.has(table.getName())) {
const result = this.extTableOptions.get(table.getName());
table = this.extend(result.tableOptions, result.mergeOptions);
this.extTableOptions.delete(table.getName());
}
2021-01-14 02:35:15 +00:00
tables.set(table.getName(), table);
}
2020-10-24 07:34:43 +00:00
});
return tables;
}
/**
*
*
* @param options
*/
public table(options: TableOptions): Table {
const { name } = options;
const table = new Table(options, { database: this });
this.tables.set(name, table);
// 在 source 或 target 之后定义 through需要更新 source 和 target 的 model
if (this.throughTables.has(name)) {
const [sourceTable, targetTable] = this.getTables(this.throughTables.get(name));
sourceTable && sourceTable.modelInit(true);
targetTable && targetTable.modelInit(true);
// this.throughTables.delete(name);
}
return table;
}
/**
* API
*
* @param options
*/
2021-01-13 07:43:41 +00:00
public extend(options: TableOptions, mergeOptions?: MergeOptions): Table {
2020-10-24 07:34:43 +00:00
const { name } = options;
let table: Table;
if (this.tables.has(name)) {
table = this.tables.get(name);
2021-01-13 07:43:41 +00:00
table.extend(options, mergeOptions);
2020-10-24 07:34:43 +00:00
} else {
table = this.table(options);
this.tables.set(name, table);
}
return table;
}
/**
*
*
* @param name
*/
public isDefined(name: string): boolean {
return this.sequelize.isDefined(name);
}
/**
* Model
*
* TODO: 动态初始化并加载配置
*
*
* @param name
*/
public getModel(name: string): ModelCtor<Model> {
return this.isDefined(name) ? this.sequelize.model(name) as any : undefined;
}
/**
* names Models
*
* @param names
*/
2021-01-13 07:43:41 +00:00
public getModels(names: string[] = []): Array<ModelCtor<Model>> {
if (names.length === 0) {
return this.sequelize.models as any;
}
2020-10-24 07:34:43 +00:00
return names.map(name => this.getModel(name));
}
/**
* table
*
* TODO:
* table Model
*
*
* @param name
*/
public getTable(name: string): Table {
return this.tables.has(name) ? this.tables.get(name) : undefined;
}
/**
* names table
*
* @param names
*/
2021-01-13 07:43:41 +00:00
public getTables(names: string[] = []): Array<Table> {
if (names.length === 0) {
return [...this.tables.values()];
}
2020-10-24 07:34:43 +00:00
return names.map(name => this.getTable(name));
}
/**
*
*
* Model.init
*/
public associate() {
for (const name of this.associating) {
const Model: any = this.getModel(name);
Model.associate && Model.associate(this.sequelize.models);
}
}
/**
*
*
* TODO: 细节待定
*
* @param plugin
* @param options
*/
public async plugin(plugin: any, options = {}) {
await plugin(this, options);
}
/**
*
*
* @param options
*/
public async sync(options: SyncOptions = {}) {
const { tables = [], ...restOptions } = options;
let items: Array<any>;
if (tables instanceof Map) {
items = Array.from(tables.values());
} else {
items = tables;
}
/**
* sequelize.sync model
* Model.sync Model
* database.sync tables
*/
if (items.length > 0) {
// 指定 tables 时,新建 sequelize 实例来单独处理这些 tables 相关 models 的 sync
const sequelize = new Sequelize(this.options);
const names = new Set<string>();
for (const key in items) {
let table = items[key];
if (typeof table === 'string') {
table = this.getTable(table);
}
if (table instanceof Table) {
for (const name of table.getRelatedTableNames()) {
names.add(name);
}
}
}
for (const name of names) {
// @ts-ignore
2020-12-17 13:47:23 +00:00
const model = this.getModel(name);
if (model) {
sequelize.modelManager.addModel(model);
}
2020-10-24 07:34:43 +00:00
}
await sequelize.sync(restOptions);
await sequelize.close();
} else {
await this.sequelize.sync(restOptions);
}
}
/**
*
*/
public async close() {
2021-09-09 15:57:01 +00:00
this.removeAllListeners();
return this.sequelize.close();
2020-10-24 07:34:43 +00:00
}
2020-12-15 12:16:55 +00:00
2020-12-29 06:53:39 +00:00
public getFieldByPath(fieldPath: string) {
const [tableName, fieldName] = fieldPath.split('.');
return this.getTable(tableName).getField(fieldName);
}
2020-10-24 07:34:43 +00:00
}