nocobase/packages/database/src/database.ts

288 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
Options,
Sequelize,
SyncOptions as SequelizeSyncOptions,
} from 'sequelize';
import glob from 'glob';
import Table, { TableOptions } from './table';
import { Model, ModelCtor } from './model';
import { requireModule } from './utils';
import _ from 'lodash';
export interface SyncOptions extends SequelizeSyncOptions {
/**
* 指定需要更新字段的 tables
*/
tables?: string[] | Table[] | Map<string, Table>;
}
export interface ImportOptions {
/**
* 指定配置所在路径
*/
directory: string;
/**
* 文件后缀,默认值 ['js', 'ts', 'json']
*/
extensions?: string[];
}
export type HookType = 'beforeTableInit' | 'afterTableInit' | 'beforeAddField' | 'afterAddField';
export default class Database {
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: Options;
protected hooks = {};
constructor(options: Options) {
this.options = options;
this.sequelize = new Sequelize(options);
}
/**
* 载入指定目录下 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'
]
});
const tables = new Map<string, Table>();
files.forEach((file: string) => {
const options = requireModule(file);
const table = this.table(typeof options === 'function' ? options(this) : options);
tables.set(table.getName(), table);
});
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
*/
public extend(options: TableOptions): Table {
const { name } = options;
let table: Table;
if (this.tables.has(name)) {
table = this.tables.get(name);
table.extend(options);
} 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
*/
public getModels(names: string[]): Array<ModelCtor<Model>> {
if (names.length === 0) {
return this.sequelize.models as any;
}
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
*/
public getTables(names: string[]): Array<Table> {
if (names.length === 0) {
return [...this.tables.values()];
}
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
const model = this.getModel(name);
if (model) {
sequelize.modelManager.addModel(model);
}
}
await sequelize.sync(restOptions);
await sequelize.close();
} else {
await this.sequelize.sync(restOptions);
}
}
/**
* 关闭数据库连接
*/
public async close() {
return this.sequelize.close();
}
/**
* 添加 hook
*
* @param hookType
* @param fn
*/
public addHook(hookType: HookType | string, fn: Function) {
const hooks = this.hooks[hookType] || [];
hooks.push(fn);
this.hooks[hookType] = hooks;
}
/**
* 运行 hook
*
* @param hookType
* @param args
*/
public async runHooks(hookType: HookType | string, ...args) {
const hooks = this.hooks[hookType] || [];
for (const hook of hooks) {
if (typeof hook === 'function') {
await hook(...args);
}
}
}
}