2020-04-22 15:52:17 +02:00
|
|
|
import MysqlConnectionManager, {query} from "./MysqlConnectionManager";
|
|
|
|
import Validator from "./Validator";
|
|
|
|
import {Connection} from "mysql";
|
2020-07-24 12:13:28 +02:00
|
|
|
import ModelComponent from "./ModelComponent";
|
|
|
|
import {Type} from "../Utils";
|
|
|
|
import ModelFactory from "./ModelFactory";
|
|
|
|
import ModelRelation from "./ModelRelation";
|
2020-06-27 14:36:50 +02:00
|
|
|
import ModelQuery, {ModelQueryResult} from "./ModelQuery";
|
2020-04-22 15:52:17 +02:00
|
|
|
import {Request} from "express";
|
|
|
|
|
|
|
|
export default abstract class Model {
|
2020-07-27 10:53:46 +02:00
|
|
|
public static get table(): string {
|
|
|
|
return this.name
|
|
|
|
.replace(/(?:^|\.?)([A-Z])/g, (x, y) => '_' + y.toLowerCase())
|
|
|
|
.replace(/^_/, '')
|
|
|
|
+ 's';
|
|
|
|
}
|
|
|
|
|
2020-07-27 10:52:39 +02:00
|
|
|
public static create<T extends Model>(this: Type<T>, data: any): T {
|
|
|
|
return ModelFactory.get(this).create(data);
|
|
|
|
}
|
|
|
|
|
2020-07-24 12:13:28 +02:00
|
|
|
public static select<T extends Model>(this: Type<T>, ...fields: string[]): ModelQuery<T> {
|
|
|
|
return ModelFactory.get(this).select(...fields);
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
|
2020-07-24 12:13:28 +02:00
|
|
|
public static update<T extends Model>(this: Type<T>, data: { [key: string]: any }): ModelQuery<T> {
|
|
|
|
return ModelFactory.get(this).update(data);
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
|
2020-07-24 12:13:28 +02:00
|
|
|
public static delete<T extends Model>(this: Type<T>): ModelQuery<T> {
|
|
|
|
return ModelFactory.get(this).delete();
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
|
2020-07-24 12:13:28 +02:00
|
|
|
public static async getById<T extends Model>(this: Type<T>, ...id: any): Promise<T | null> {
|
2020-07-27 10:54:08 +02:00
|
|
|
return ModelFactory.get(this).getById(...id);
|
2020-06-04 14:59:23 +02:00
|
|
|
}
|
|
|
|
|
2020-07-24 12:13:28 +02:00
|
|
|
public static async paginate<T extends Model>(this: Type<T>, request: Request, perPage: number = 20, query?: ModelQuery<T>): Promise<ModelQueryResult<T>> {
|
|
|
|
return ModelFactory.get(this).paginate(request, perPage, query);
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
|
2020-07-24 15:40:10 +02:00
|
|
|
protected readonly _factory: ModelFactory<any>;
|
2020-07-24 12:13:28 +02:00
|
|
|
private readonly _components: ModelComponent<any>[] = [];
|
|
|
|
private readonly _validators: { [key: string]: Validator<any> } = {};
|
2020-08-26 14:18:02 +02:00
|
|
|
private _cached_exists?: boolean = undefined;
|
2020-04-22 15:52:17 +02:00
|
|
|
|
|
|
|
[key: string]: any;
|
|
|
|
|
2020-07-24 15:40:10 +02:00
|
|
|
public constructor(factory: ModelFactory<any>) {
|
|
|
|
if (!factory || !(factory instanceof ModelFactory)) throw new Error('Cannot instantiate model directly.');
|
|
|
|
this._factory = factory;
|
2020-06-27 14:36:50 +02:00
|
|
|
this.init();
|
2020-06-04 14:59:23 +02:00
|
|
|
}
|
|
|
|
|
2020-07-24 12:13:28 +02:00
|
|
|
protected abstract init(): void;
|
|
|
|
|
|
|
|
protected setValidation<T>(propertyName: keyof this): Validator<T> {
|
|
|
|
const validator = new Validator<T>();
|
|
|
|
this._validators[propertyName as string] = validator;
|
|
|
|
return validator;
|
2020-06-04 14:59:23 +02:00
|
|
|
}
|
|
|
|
|
2020-07-24 12:13:28 +02:00
|
|
|
public addComponent(modelComponent: ModelComponent<this>): void {
|
2020-07-26 11:37:01 +02:00
|
|
|
modelComponent.applyToModel();
|
2020-07-24 12:13:28 +02:00
|
|
|
this._components.push(modelComponent);
|
|
|
|
}
|
2020-04-22 15:52:17 +02:00
|
|
|
|
2020-07-24 12:13:28 +02:00
|
|
|
public as<T extends ModelComponent<any>>(type: Type<T>): T {
|
|
|
|
for (const component of this._components) {
|
|
|
|
if (component instanceof type) {
|
|
|
|
return <any>this;
|
|
|
|
}
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
2020-07-24 12:13:28 +02:00
|
|
|
throw new Error(`Component ${type.name} was not initialized for this ${this.constructor.name}.`);
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
|
2020-07-24 15:40:10 +02:00
|
|
|
public updateWithData(data: any) {
|
2020-07-24 12:13:28 +02:00
|
|
|
for (const property of this._properties) {
|
|
|
|
if (data[property] !== undefined) {
|
|
|
|
this[property] = data[property];
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-28 14:16:06 +02:00
|
|
|
/**
|
|
|
|
* Override this to automatically fill obvious missing data i.e. from relation or default value that are fetched
|
|
|
|
* asynchronously.
|
|
|
|
*/
|
2020-08-28 15:40:18 +02:00
|
|
|
protected async autoFill(): Promise<void> {
|
2020-08-28 14:16:06 +02:00
|
|
|
}
|
|
|
|
|
2020-04-22 15:52:17 +02:00
|
|
|
protected async beforeSave(exists: boolean, connection: Connection): Promise<void> {
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async afterSave(): Promise<void> {
|
|
|
|
}
|
|
|
|
|
|
|
|
public async save(connection?: Connection, postHook?: (callback: () => Promise<void>) => void): Promise<void> {
|
2020-08-28 14:16:06 +02:00
|
|
|
await this.autoFill();
|
2020-04-22 15:52:17 +02:00
|
|
|
await this.validate(false, connection);
|
|
|
|
|
|
|
|
const exists = await this.exists();
|
|
|
|
let needs_full_update = false;
|
|
|
|
|
|
|
|
if (connection) {
|
|
|
|
needs_full_update = await this.saveTransaction(connection, exists, needs_full_update);
|
|
|
|
} else {
|
|
|
|
needs_full_update = await MysqlConnectionManager.wrapTransaction(async connection => this.saveTransaction(connection, exists, needs_full_update));
|
|
|
|
}
|
|
|
|
|
|
|
|
const callback = async () => {
|
|
|
|
if (needs_full_update) {
|
2020-07-24 12:13:28 +02:00
|
|
|
this.updateWithData((await this._factory.select().where('id', this.id!).limit(1).execute()).results[0]);
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
await this.afterSave();
|
|
|
|
};
|
|
|
|
|
|
|
|
if (connection) {
|
|
|
|
postHook!(callback);
|
|
|
|
} else {
|
|
|
|
await callback();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private async saveTransaction(connection: Connection, exists: boolean, needs_full_update: boolean): Promise<boolean> {
|
|
|
|
// Before save
|
|
|
|
await this.beforeSave(exists, connection);
|
2020-08-28 14:16:25 +02:00
|
|
|
if (!exists && this.hasOwnProperty('created_at')) {
|
|
|
|
this.created_at = new Date();
|
|
|
|
}
|
2020-04-22 15:52:17 +02:00
|
|
|
if (exists && this.hasOwnProperty('updated_at')) {
|
|
|
|
this.updated_at = new Date();
|
|
|
|
}
|
|
|
|
|
2020-07-24 12:13:28 +02:00
|
|
|
const properties = [];
|
2020-04-22 15:52:17 +02:00
|
|
|
const values = [];
|
|
|
|
|
|
|
|
if (exists) {
|
2020-06-04 14:59:23 +02:00
|
|
|
const data: any = {};
|
2020-07-24 12:13:28 +02:00
|
|
|
for (const property of this._properties) {
|
|
|
|
const value = this[property];
|
|
|
|
if (value !== undefined) {
|
|
|
|
data[property] = value;
|
2020-04-22 15:52:17 +02:00
|
|
|
} else {
|
|
|
|
needs_full_update = true;
|
|
|
|
}
|
|
|
|
}
|
2020-07-24 12:13:28 +02:00
|
|
|
let query = this._factory.update(data);
|
2020-07-24 15:40:10 +02:00
|
|
|
for (const indexField of this._factory.getPrimaryKeyFields()) {
|
2020-06-04 14:59:23 +02:00
|
|
|
query = query.where(indexField, this[indexField]);
|
|
|
|
}
|
|
|
|
await query.execute(connection);
|
2020-04-22 15:52:17 +02:00
|
|
|
} else {
|
|
|
|
const props_holders = [];
|
2020-07-24 12:13:28 +02:00
|
|
|
for (const property of this._properties) {
|
|
|
|
const value = this[property];
|
|
|
|
if (value !== undefined) {
|
|
|
|
properties.push(property);
|
2020-04-22 15:52:17 +02:00
|
|
|
props_holders.push('?');
|
2020-07-24 12:13:28 +02:00
|
|
|
values.push(value);
|
2020-04-22 15:52:17 +02:00
|
|
|
} else {
|
|
|
|
needs_full_update = true;
|
|
|
|
}
|
|
|
|
}
|
2020-07-24 12:13:28 +02:00
|
|
|
const result = await query(`INSERT INTO ${this.table} (${properties.join(', ')}) VALUES(${props_holders.join(', ')})`, values, connection);
|
2020-04-22 15:52:17 +02:00
|
|
|
|
2020-07-24 12:13:28 +02:00
|
|
|
if (this.hasOwnProperty('id')) this.id = result.other.insertId;
|
2020-08-26 14:18:02 +02:00
|
|
|
this._cached_exists = true;
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return needs_full_update;
|
|
|
|
}
|
|
|
|
|
|
|
|
public async delete(): Promise<void> {
|
|
|
|
if (!(await this.exists())) throw new Error('This model instance doesn\'t exist in DB.');
|
|
|
|
|
2020-07-24 12:13:28 +02:00
|
|
|
let query = this._factory.delete();
|
2020-07-24 15:40:10 +02:00
|
|
|
for (const indexField of this._factory.getPrimaryKeyFields()) {
|
2020-06-04 14:59:23 +02:00
|
|
|
query = query.where(indexField, this[indexField]);
|
|
|
|
}
|
|
|
|
await query.execute();
|
2020-08-26 14:18:02 +02:00
|
|
|
this._cached_exists = false;
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
|
2020-09-02 14:07:40 +02:00
|
|
|
public async validate(onlyFormat: boolean = false, connection?: Connection): Promise<void[]> {
|
|
|
|
return await Promise.all(this._properties.map(
|
|
|
|
prop => this._validators[prop]?.execute(prop, this[prop], onlyFormat, connection)
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
2020-08-05 10:45:13 +02:00
|
|
|
public async exists(): Promise<boolean> {
|
2020-08-26 14:18:02 +02:00
|
|
|
if (this._cached_exists === undefined) {
|
|
|
|
const query = this._factory.select('1');
|
|
|
|
for (const indexField of this._factory.getPrimaryKeyFields()) {
|
|
|
|
query.where(indexField, this[indexField]);
|
|
|
|
}
|
|
|
|
this._cached_exists = (await query.limit(1).execute()).results.length > 0;
|
2020-08-05 10:45:13 +02:00
|
|
|
}
|
2020-08-26 14:18:02 +02:00
|
|
|
|
|
|
|
return this._cached_exists;
|
2020-08-05 10:45:13 +02:00
|
|
|
}
|
|
|
|
|
2020-09-02 14:07:40 +02:00
|
|
|
public equals(model: this): boolean {
|
|
|
|
for (const field of this._factory.getPrimaryKeyFields()) {
|
|
|
|
if (this[field] !== model[field]) return false;
|
|
|
|
}
|
|
|
|
return true;
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
|
2020-07-24 12:13:28 +02:00
|
|
|
public get table(): string {
|
|
|
|
return this._factory.table;
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
|
2020-07-24 12:13:28 +02:00
|
|
|
private get _properties(): string[] {
|
|
|
|
return Object.getOwnPropertyNames(this).filter(p => {
|
2020-07-26 11:37:01 +02:00
|
|
|
return !p.startsWith('_') &&
|
|
|
|
typeof this[p] !== 'function' &&
|
|
|
|
!(this[p] instanceof ModelRelation);
|
2020-07-24 12:13:28 +02:00
|
|
|
});
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-24 12:13:28 +02:00
|
|
|
export const EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/;
|