tachybase_todo/packages/database/src/table.ts

415 lines
11 KiB
TypeScript
Raw Normal View History

2020-10-24 15:34:43 +08:00
import {
InitOptions,
ModelAttributes,
ModelOptions,
ModelIndexesOptions,
Utils,
SyncOptions,
} from 'sequelize';
import {
buildField,
FieldOptions,
Relation,
BELONGSTO,
BELONGSTOMANY,
SORT
2020-10-24 15:34:43 +08:00
} from './fields';
import Database from './database';
import { Model, ModelCtor } from './model';
import _ from 'lodash';
2021-01-13 15:43:41 +08:00
import merge from 'deepmerge';
export interface MergeOptions extends merge.Options {
}
2020-10-24 15:34:43 +08:00
const registeredModels = new Map<string, any>();
export function registerModel(key: string, model: any) {
registeredModels.set(key, model);
}
export function registerModels(models) {
for (const key in models) {
if (models.hasOwnProperty(key)) {
registerModel(key, models[key]);
}
}
}
// TODO: 判断如果 key 是 model 直接返回
export function getRegisteredModel(key) {
if (typeof key === 'string') {
return registeredModels.get(key);
}
return key;
}
2021-03-28 13:34:51 +08:00
export interface TableOptions extends Omit<ModelOptions<Model>, 'name' | 'modelName'> {
2020-10-24 15:34:43 +08:00
/**
* ModelOptions name
*
* name tableNamemodelName tableName modelName
* name tables models
* tableName nametableNamemodelName
*
* TODO: name, tableName, modelNamefreezeTableNameunderscored
*/
name: string;
/**
* model
*/
model?: ModelCtor<Model> | string;
2020-10-24 15:34:43 +08:00
/**
*
*/
fields?: Array<FieldOptions>;
/**
*
*/
[key: string]: any;
}
/**
* Tabel
*/
export interface TabelContext {
database: Database;
}
/**
* Model model
*
* - reinitialize = false Model.init
* - reinitialize = true Model Model.init Model.associate
* - reinitialize = modelOnly Model Model.init
*/
export type Reinitialize = boolean | 'modelOnly';
/**
*
*
* Model
*/
export class Table {
protected database: Database;
protected options: TableOptions;
protected fields = new Map<string, any>();
protected modelAttributes: ModelAttributes;
protected modelOptions: InitOptions;
/**
*
*/
protected associations = new Map<string, Relation>();
/**
* association
*/
protected associating = new Map<string, Relation>();
/**
*
*/
protected indexes = new Map<string, any>();
protected Model: ModelCtor<Model>;
protected defaultModel: ModelCtor<Model>;
2020-10-24 15:34:43 +08:00
/**
*
*/
public isThroughTable: boolean = false;
public relationTables = new Set<string>();
get sortable(): boolean {
return Array.from(this.fields.values()).some(field => field instanceof SORT);
}
2020-10-24 15:34:43 +08:00
constructor(options: TableOptions, context: TabelContext) {
const { database } = context;
2020-12-15 20:16:55 +08:00
database.runHooks('beforeTableInit', options);
2020-10-24 15:34:43 +08:00
const {
2021-01-13 15:43:41 +08:00
model,
2020-10-24 15:34:43 +08:00
fields = [],
indexes = [],
} = options;
this.options = options;
this.database = database;
// 初始化的时候获取
this.defaultModel = getRegisteredModel(model);
2020-10-24 15:34:43 +08:00
this.modelAttributes = {};
// 在 set fields 之前 model init 的原因是因为关系字段可能需要用到 model 的相关配置
// @ts-ignore
2020-10-24 15:34:43 +08:00
this.addIndexes(indexes, 'modelOnly');
// this.modelInit('modelOnly');
this.setFields(fields);
2020-12-15 20:16:55 +08:00
database.runHooks('afterTableInit', this);
2020-10-24 15:34:43 +08:00
}
public modelInit(reinitialize: Reinitialize = false) {
if (reinitialize || !this.Model) {
2021-03-28 13:34:51 +08:00
this.Model = this.defaultModel || class extends Model { };
2020-10-24 15:34:43 +08:00
this.Model.database = this.database;
// 关系的建立是在 model.init 之后在配置中表字段Column和关系Relation都在 fields
// 所以需要单独提炼出 associations 字段,并在 Model.init 之后执行 Model.associate
// @ts-ignore
2021-03-28 13:34:51 +08:00
this.Model.associate = (models: { [key: string]: ModelCtor<Model> }) => {
2020-10-24 15:34:43 +08:00
for (const [key, association] of this.associating) {
const { type, target } = association.getAssociationArguments();
if (this.database.isDefined(target)) {
const TargetModel = this.database.getModel(target);
// 如果关系表在之后才定义,未设置 targetKey 时targetKey 默认值需要在 target model 初始化之后才能取到
if (association instanceof BELONGSTO || association instanceof BELONGSTOMANY) {
2020-10-24 15:34:43 +08:00
association.updateOptionsAfterTargetModelBeDefined();
}
this.Model[type](TargetModel, association.getAssociationOptions());
// 建立关系之后,需要删除待处理的 associating避免重复和提高效率
this.associating.delete(key);
}
}
}
}
this.Model.init(this.getModelAttributes(), this.getModelOptions());
if (reinitialize === true) {
this.associating = new Map(this.associations);
// 需要额外处理 associating 的情况
// 建立表关系需要遍历多个 Model所以在这里需要标记哪些已定义的 Model 需要建立表关系
if (this.associating.size > 0) {
this.database.associating.add(this.options.name);
} else {
this.database.associating.delete(this.options.name);
}
this.database.associate();
}
}
public getName(): string {
return this.options.name;
}
public getTableName(): string {
2021-01-13 15:43:41 +08:00
return this.options.name;
2020-10-24 15:34:43 +08:00
}
/**
*
* @param key key
*/
public getOptions(key?: any): TableOptions {
return key ? _.get(this.options, key) : this.options;
2020-10-24 15:34:43 +08:00
}
public getModel(): ModelCtor<Model> {
return this.database.getModel(this.getName());
}
public getModelAttributes(): ModelAttributes {
return this.modelAttributes;
}
public getModelOptions(): InitOptions {
2021-01-13 15:43:41 +08:00
const {
name,
underscored = true,
...restOptions
} = this.options;
const hooks = _.get(this.getModel(), 'options.hooks') || this.options.hooks || {};
2020-10-24 15:34:43 +08:00
return {
underscored,
2021-01-13 15:43:41 +08:00
modelName: name,
tableName: name,
sequelize: this.database.sequelize,
2020-10-24 15:34:43 +08:00
createdAt: Utils.underscoredIf('createdAt', underscored),
updatedAt: Utils.underscoredIf('updatedAt', underscored),
indexes: Array.from(this.indexes.values()),
// freezeTableName: true,
2021-01-13 15:43:41 +08:00
..._.omit(restOptions, ['model', 'fields', 'indexes']),
hooks,
2020-10-24 15:34:43 +08:00
};
}
/**
* table names sync
*/
public getRelatedTableNames(): Set<string> {
const names = new Set<string>();
2020-12-17 21:47:23 +08:00
this.options.name && names.add(this.options.name);
2020-10-24 15:34:43 +08:00
for (const association of this.associations.values()) {
const target = association.getTarget();
2020-12-17 21:47:23 +08:00
target && names.add(target);
if (association instanceof BELONGSTOMANY) {
2020-12-17 21:47:23 +08:00
const throughName = association.getThroughName();
throughName && names.add(throughName);
2020-10-24 15:34:43 +08:00
}
}
return names;
}
public getAssociations() {
return this.associations;
}
public getFields() {
return this.fields;
}
public setFields(fields: Array<FieldOptions>) {
this.fields.clear();
this.associating.clear();
this.associations.clear();
for (const key in fields) {
this.addField(fields[key], false);
}
this.modelInit(true);
}
public hasField(name: string) {
return this.fields.has(name);
}
public getField(name: string) {
return this.fields.get(name);
}
/**
*
*
* @param options
* @param reinitialize
*/
public addField(options: FieldOptions, reinitialize: Reinitialize = true) {
2020-12-15 20:16:55 +08:00
this.database.runHooks('beforeAddField', options, this);
2020-10-24 15:34:43 +08:00
const { name, index } = options;
const field = buildField(options, {
sourceTable: this,
database: this.database,
});
// 添加字段后 table.options 中的 fields 并不会更新,这导致 table.getOptions() 拿不到最新的字段配置
// 所以在同时更新 table.options.fields 数组
if (!this.options.fields) {
this.options.fields = [];
}
const existIndex = this.options.fields.findIndex(field => field.name === name);
if (existIndex !== -1) {
this.options.fields.splice(existIndex, 1, options);
} else {
this.options.fields.push(options);
}
2020-10-24 15:34:43 +08:00
this.fields.set(name, field);
2020-10-24 15:34:43 +08:00
if (field instanceof Relation) {
// 关系字段先放到 associating 里待处理,等相关 target model 初始化之后,再通过 associate 建立关系
this.associating.set(name, field);
this.associations.set(name, field);
} else {
if (index === true) {
this.addIndex(name, false);
} else if (typeof index === 'object') {
this.addIndex({
fields: [name],
...index,
}, false);
}
this.modelAttributes[name] = field.getAttributeOptions();
}
this.modelInit(reinitialize);
2020-12-15 20:16:55 +08:00
this.database.runHooks('afterAddField', field, this);
2020-10-24 15:34:43 +08:00
return field;
}
/**
*
*
* @param indexOptions
2020-10-24 15:34:43 +08:00
* @param reinitialize
*/
public addIndex(indexOptions: string | ModelIndexesOptions, reinitialize: Reinitialize = true) {
const options = typeof indexOptions === 'string' ? {
fields: [indexOptions],
} : indexOptions;
2020-10-24 15:34:43 +08:00
// @ts-ignore
2021-01-13 15:43:41 +08:00
const index = Utils.nameIndex(options, this.options.name);
console.log(this.options, { index, options });
2020-10-24 15:34:43 +08:00
this.indexes.set(index.name, {
type: '',
parser: null,
...index,
});
this.modelInit(reinitialize);
}
/**
*
*
* @param indexes
* @param reinitialize
*/
public addIndexes(indexes: Array<string | ModelIndexesOptions>, reinitialize: Reinitialize = true) {
for (const index in indexes) {
this.addIndex(indexes[index], false);
}
this.modelInit(reinitialize);
}
/**
* API
*
* @param options
*/
2021-01-13 15:43:41 +08:00
public extend(options: TableOptions, mergeOptions: MergeOptions = {}) {
const {
fields = [],
indexes = [],
model,
...restOptions
} = options;
if (model) {
this.defaultModel = getRegisteredModel(model);
}
const { arrayMerge = (target: any[], source: any[]) => source } = mergeOptions;
this.options = merge(this.options, restOptions, {
arrayMerge,
...mergeOptions,
});
2020-10-24 15:34:43 +08:00
for (const key in fields) {
this.addField(fields[key], false);
}
// @ts-ignore
2020-10-24 15:34:43 +08:00
this.addIndexes(indexes, false);
this.modelInit(true);
}
/**
*
*
* @param options
*/
public async sync(options: SyncOptions = {}) {
const tables = [];
for (const name of this.getRelatedTableNames()) {
tables.push(name);
}
return this.database.sync({
...options,
tables,
});
}
}
export default Table;