2021-12-06 21:12:54 +08:00
|
|
|
import { Sequelize, ModelCtor, Model, ModelOptions } from 'sequelize';
|
2021-09-27 15:28:32 +08:00
|
|
|
import { EventEmitter } from 'events';
|
2021-09-18 00:23:21 +08:00
|
|
|
import { Database } from './database';
|
2021-12-06 21:12:54 +08:00
|
|
|
import { Field, FieldOptions } from './fields';
|
|
|
|
|
2021-09-18 00:23:21 +08:00
|
|
|
import _ from 'lodash';
|
2021-09-25 23:56:26 +08:00
|
|
|
import { Repository } from './repository';
|
2021-12-06 21:12:54 +08:00
|
|
|
import { SyncOptions } from 'sequelize/types/lib/sequelize';
|
|
|
|
import lodash from 'lodash';
|
|
|
|
import merge from 'deepmerge';
|
|
|
|
const { hooks } = require('sequelize/lib/hooks');
|
|
|
|
|
|
|
|
export type RepositoryType = typeof Repository;
|
2021-09-18 00:23:21 +08:00
|
|
|
|
2021-12-06 21:12:54 +08:00
|
|
|
export interface CollectionOptions extends Omit<ModelOptions, 'name'> {
|
2021-09-27 15:28:32 +08:00
|
|
|
name: string;
|
|
|
|
tableName?: string;
|
2021-12-06 21:12:54 +08:00
|
|
|
fields?: FieldOptions[];
|
|
|
|
model?: string | ModelCtor<Model>;
|
|
|
|
repository?: string | RepositoryType;
|
2021-12-06 21:23:34 +08:00
|
|
|
[key: string]: any;
|
2021-09-18 00:23:21 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
export interface CollectionContext {
|
|
|
|
database: Database;
|
|
|
|
}
|
|
|
|
|
2021-12-06 21:12:54 +08:00
|
|
|
export class Collection<
|
|
|
|
TModelAttributes extends {} = any,
|
|
|
|
TCreationAttributes extends {} = TModelAttributes,
|
|
|
|
> extends EventEmitter {
|
2021-09-18 00:23:21 +08:00
|
|
|
options: CollectionOptions;
|
|
|
|
context: CollectionContext;
|
2021-12-06 21:12:54 +08:00
|
|
|
isThrough?: boolean;
|
|
|
|
fields: Map<string, any> = new Map<string, any>();
|
|
|
|
model: ModelCtor<Model<TModelAttributes, TCreationAttributes>>;
|
|
|
|
repository: Repository<TModelAttributes, TCreationAttributes>;
|
2021-09-18 00:23:21 +08:00
|
|
|
|
|
|
|
get name() {
|
|
|
|
return this.options.name;
|
|
|
|
}
|
|
|
|
|
2021-09-27 15:28:32 +08:00
|
|
|
constructor(options: CollectionOptions, context?: CollectionContext) {
|
|
|
|
super();
|
2021-09-18 00:23:21 +08:00
|
|
|
this.context = context;
|
2021-12-06 21:12:54 +08:00
|
|
|
this.options = options;
|
|
|
|
this.bindFieldEventListener();
|
|
|
|
this.modelInit();
|
|
|
|
this.setFields(options.fields);
|
|
|
|
this.setRepository(options.repository);
|
|
|
|
}
|
|
|
|
|
|
|
|
private sequelizeModelOptions() {
|
|
|
|
const { name, tableName } = this.options;
|
|
|
|
return {
|
|
|
|
..._.omit(this.options, ['name', 'fields']),
|
2021-09-18 00:23:21 +08:00
|
|
|
modelName: name,
|
2021-12-06 21:12:54 +08:00
|
|
|
sequelize: this.context.database.sequelize,
|
2021-09-18 00:23:21 +08:00
|
|
|
tableName: tableName || name,
|
2021-12-06 21:12:54 +08:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* TODO
|
|
|
|
*/
|
|
|
|
modelInit() {
|
|
|
|
if (this.model) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const { name, model } = this.options;
|
|
|
|
let M = Model;
|
|
|
|
if (this.context.database.sequelize.isDefined(name)) {
|
|
|
|
const m = this.context.database.sequelize.model(name);
|
|
|
|
if ((m as any).isThrough) {
|
|
|
|
this.model = m;
|
2021-12-07 15:21:16 +08:00
|
|
|
Object.defineProperty(this.model, 'database', { value: this.context.database });
|
2021-12-06 21:12:54 +08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (typeof model === 'string') {
|
|
|
|
M = this.context.database.models.get(model) || Model;
|
|
|
|
} else if (model) {
|
|
|
|
M = model;
|
|
|
|
}
|
|
|
|
this.model = class extends M {};
|
|
|
|
this.model.init(null, this.sequelizeModelOptions());
|
2021-12-07 15:21:16 +08:00
|
|
|
Object.defineProperty(this.model, 'database', { value: this.context.database });
|
2021-12-06 21:12:54 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
setRepository(repository?: RepositoryType | string) {
|
|
|
|
let repo = Repository;
|
|
|
|
if (typeof repository === 'string') {
|
|
|
|
repo = this.context.database.repositories.get(repository) || Repository;
|
|
|
|
}
|
|
|
|
this.repository = new repo(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
private bindFieldEventListener() {
|
|
|
|
this.on('field.afterAdd', (field: Field) => {
|
|
|
|
field.bind();
|
2021-09-18 00:23:21 +08:00
|
|
|
});
|
2021-09-27 15:28:32 +08:00
|
|
|
this.on('field.afterRemove', (field) => field.unbind());
|
2021-09-18 00:23:21 +08:00
|
|
|
}
|
|
|
|
|
2021-09-27 15:28:32 +08:00
|
|
|
forEachField(callback: (field: Field) => void) {
|
|
|
|
return [...this.fields.values()].forEach(callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
findField(callback: (field: Field) => boolean) {
|
|
|
|
return [...this.fields.values()].find(callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
hasField(name: string) {
|
|
|
|
return this.fields.has(name);
|
|
|
|
}
|
|
|
|
|
2021-12-06 21:23:34 +08:00
|
|
|
getField<F extends Field>(name: string): F {
|
2021-09-27 15:28:32 +08:00
|
|
|
return this.fields.get(name);
|
|
|
|
}
|
|
|
|
|
2021-12-06 21:23:34 +08:00
|
|
|
addField(name: string, options: FieldOptions): Field {
|
2021-12-06 21:12:54 +08:00
|
|
|
return this.setField(name, options);
|
|
|
|
}
|
|
|
|
|
2021-12-06 21:23:34 +08:00
|
|
|
setField(name: string, options: FieldOptions): Field {
|
2021-09-27 15:28:32 +08:00
|
|
|
const { database } = this.context;
|
2021-12-06 21:12:54 +08:00
|
|
|
|
|
|
|
const field = database.buildField(
|
|
|
|
{ name, ...options },
|
|
|
|
{
|
|
|
|
...this.context,
|
|
|
|
collection: this,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
2021-09-27 15:28:32 +08:00
|
|
|
this.fields.set(name, field);
|
|
|
|
this.emit('field.afterAdd', field);
|
2021-12-06 21:12:54 +08:00
|
|
|
return field;
|
2021-09-27 15:28:32 +08:00
|
|
|
}
|
|
|
|
|
2021-12-06 21:12:54 +08:00
|
|
|
setFields(fields: FieldOptions[], resetFields = true) {
|
|
|
|
if (!Array.isArray(fields)) {
|
|
|
|
return;
|
2021-09-27 15:28:32 +08:00
|
|
|
}
|
2021-12-06 21:12:54 +08:00
|
|
|
|
|
|
|
if (resetFields) {
|
|
|
|
this.resetFields();
|
2021-09-27 15:28:32 +08:00
|
|
|
}
|
2021-12-06 21:12:54 +08:00
|
|
|
|
|
|
|
for (const { name, ...options } of fields) {
|
|
|
|
this.addField(name, options);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
resetFields() {
|
|
|
|
const fieldNames = this.fields.keys();
|
|
|
|
for (const fieldName of fieldNames) {
|
|
|
|
this.removeField(fieldName);
|
2021-09-27 15:28:32 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
removeField(name) {
|
|
|
|
const field = this.fields.get(name);
|
|
|
|
const bool = this.fields.delete(name);
|
|
|
|
if (bool) {
|
|
|
|
this.emit('field.afterRemove', field);
|
|
|
|
}
|
|
|
|
return bool;
|
|
|
|
}
|
|
|
|
|
2021-12-06 21:12:54 +08:00
|
|
|
/**
|
|
|
|
* TODO
|
|
|
|
*
|
|
|
|
* @param name
|
|
|
|
* @param options
|
|
|
|
*/
|
|
|
|
updateOptions(options: CollectionOptions, mergeOptions?: any) {
|
|
|
|
let newOptions = lodash.cloneDeep(options);
|
|
|
|
newOptions = merge(this.options, newOptions, mergeOptions);
|
|
|
|
|
|
|
|
this.context.database.emit('beforeUpdateCollection', this, newOptions);
|
|
|
|
|
|
|
|
this.setFields(options.fields, false);
|
|
|
|
this.setRepository(options.repository);
|
|
|
|
|
|
|
|
if (newOptions.hooks) {
|
|
|
|
this.setUpHooks(newOptions.hooks);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.context.database.emit('afterUpdateCollection', this);
|
|
|
|
}
|
|
|
|
|
|
|
|
setUpHooks(bindHooks) {
|
|
|
|
(<any>this.model)._setupHooks(bindHooks);
|
2021-09-27 15:28:32 +08:00
|
|
|
}
|
|
|
|
|
2021-12-06 21:12:54 +08:00
|
|
|
/**
|
|
|
|
* TODO
|
|
|
|
*
|
|
|
|
* @param name
|
|
|
|
* @param options
|
|
|
|
*/
|
|
|
|
updateField(name: string, options: FieldOptions) {
|
|
|
|
if (!this.hasField(name)) {
|
|
|
|
throw new Error(`field ${name} not exists`);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (options.name && options.name !== name) {
|
|
|
|
this.removeField(name);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setField(options.name || name, options);
|
|
|
|
}
|
|
|
|
|
|
|
|
async sync(syncOptions?: SyncOptions) {
|
|
|
|
const modelNames = [this.model.name];
|
|
|
|
|
|
|
|
const associations = this.model.associations;
|
|
|
|
|
|
|
|
for (const associationKey in associations) {
|
|
|
|
const association = associations[associationKey];
|
|
|
|
modelNames.push(association.target.name);
|
|
|
|
if ((<any>association).through) {
|
|
|
|
modelNames.push((<any>association).through.model.name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const models: ModelCtor<Model>[] = [];
|
|
|
|
// @ts-ignore
|
|
|
|
this.context.database.sequelize.modelManager.forEachModel((model) => {
|
|
|
|
if (modelNames.includes(model.name)) {
|
|
|
|
models.push(model);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
for (const model of models) {
|
|
|
|
await model.sync(syncOptions);
|
|
|
|
}
|
2021-09-18 00:23:21 +08:00
|
|
|
}
|
|
|
|
}
|