2020-04-22 15:52:17 +02:00
|
|
|
import MysqlConnectionManager, {query} from "./MysqlConnectionManager";
|
|
|
|
import Validator from "./Validator";
|
|
|
|
import {Connection} from "mysql";
|
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";
|
2020-06-27 14:36:50 +02:00
|
|
|
import {Type} from "../Utils";
|
|
|
|
|
|
|
|
export interface ModelClass<M extends Model> extends Type<M> {
|
|
|
|
getFactory<M extends Model>(factory?: ModelFactory<M>): ModelFactory<M>;
|
|
|
|
|
|
|
|
table: string;
|
|
|
|
|
|
|
|
getPrimaryKey(modelData: any): string;
|
|
|
|
|
|
|
|
getPrimaryKeyFields(): string[];
|
|
|
|
|
|
|
|
select<M extends Model>(...fields: string[]): ModelQuery<M>;
|
|
|
|
|
|
|
|
update<M extends Model>(data: { [key: string]: any }): ModelQuery<M>;
|
|
|
|
|
|
|
|
delete<M extends Model>(): ModelQuery<M>;
|
|
|
|
}
|
2020-04-22 15:52:17 +02:00
|
|
|
|
|
|
|
export default abstract class Model {
|
2020-06-27 14:36:50 +02:00
|
|
|
public static get table(): string {
|
|
|
|
return this.name
|
|
|
|
.replace(/(?:^|\.?)([A-Z])/g, (x, y) => '_' + y.toLowerCase())
|
|
|
|
.replace(/^_/, '')
|
|
|
|
+ 's';
|
|
|
|
}
|
2020-04-22 15:52:17 +02:00
|
|
|
|
2020-06-27 14:36:50 +02:00
|
|
|
public static async getById<T extends Model>(this: ModelClass<T>, id: number): Promise<T | null> {
|
|
|
|
return this.select<T>().where('id', id).first();
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
|
2020-06-27 14:36:50 +02:00
|
|
|
public static async paginate<T extends Model>(this: ModelClass<T>, request: Request, perPage: number = 20, query?: ModelQuery<T>): Promise<ModelQueryResult<T>> {
|
2020-04-22 15:52:17 +02:00
|
|
|
let page = request.params.page ? parseInt(request.params.page) : 1;
|
2020-06-14 21:47:51 +02:00
|
|
|
if (!query) query = this.select();
|
2020-04-22 15:52:17 +02:00
|
|
|
if (request.params.sortBy) {
|
|
|
|
const dir = request.params.sortDirection;
|
|
|
|
query = query.sortBy(request.params.sortBy, dir === 'ASC' || dir === 'DESC' ? dir : undefined);
|
|
|
|
} else {
|
|
|
|
query = query.sortBy('id');
|
|
|
|
}
|
2020-06-27 14:36:50 +02:00
|
|
|
return await query.paginate(page, perPage);
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
|
2020-06-27 14:36:50 +02:00
|
|
|
public static select<T extends Model>(this: ModelClass<T>, ...fields: string[]): ModelQuery<T> {
|
|
|
|
return ModelQuery.select(this, ...fields);
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
|
2020-06-27 14:36:50 +02:00
|
|
|
public static update<T extends Model>(this: ModelClass<T>, data: { [key: string]: any }): ModelQuery<T> {
|
|
|
|
return ModelQuery.update(this, data);
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
|
2020-06-27 14:36:50 +02:00
|
|
|
public static delete<T extends Model>(this: ModelClass<T>): ModelQuery<T> {
|
|
|
|
return ModelQuery.delete(this);
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
|
2020-06-27 14:36:50 +02:00
|
|
|
public static getPrimaryKey(modelData: any): string {
|
2020-06-04 14:59:23 +02:00
|
|
|
return this.getPrimaryKeyFields().map(f => `${modelData[f]}`).join(',');
|
|
|
|
}
|
|
|
|
|
2020-06-27 14:36:50 +02:00
|
|
|
public static getPrimaryKeyFields(): string[] {
|
2020-06-04 14:59:23 +02:00
|
|
|
return ['id'];
|
|
|
|
}
|
|
|
|
|
2020-04-22 15:52:17 +02:00
|
|
|
public static async loadRelation<T extends Model>(models: T[], relation: string, model: Function, localField: string) {
|
|
|
|
const loadMap: { [p: number]: (model: T) => void } = {};
|
|
|
|
const ids = models.map(m => {
|
|
|
|
m.relations[relation] = null;
|
|
|
|
if (m[localField]) loadMap[m[localField]] = v => m.relations[relation] = v;
|
|
|
|
return m[localField];
|
|
|
|
}).filter(id => id);
|
|
|
|
for (const v of await (<any>model).models((<any>model).select().whereIn('id', ids))) {
|
|
|
|
loadMap[v.id!](v);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-27 14:36:50 +02:00
|
|
|
public static getFactory<T extends Model>(this: ModelClass<T>, factory?: ModelFactory<T>): ModelFactory<T> {
|
2020-04-22 15:52:17 +02:00
|
|
|
if (factory === undefined) {
|
|
|
|
factory = (<any>this).FACTORY;
|
|
|
|
if (factory === undefined) factory = data => new (<any>this)(data);
|
|
|
|
}
|
|
|
|
return factory;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-06-27 14:36:50 +02:00
|
|
|
protected readonly modelClass: ModelClass<this> = <ModelClass<this>>this.constructor;
|
2020-04-22 15:52:17 +02:00
|
|
|
protected readonly properties: ModelProperty<any>[] = [];
|
|
|
|
public id?: number;
|
2020-06-04 14:59:23 +02:00
|
|
|
private readonly automaticIdProperty: boolean;
|
2020-04-22 15:52:17 +02:00
|
|
|
|
|
|
|
[key: string]: any;
|
|
|
|
|
2020-06-04 14:59:23 +02:00
|
|
|
public constructor(data: any, automaticIdProperty: boolean = true) {
|
|
|
|
this.automaticIdProperty = automaticIdProperty;
|
|
|
|
if (automaticIdProperty) {
|
2020-06-27 14:36:50 +02:00
|
|
|
this.addProperty<number>('id', new Validator());
|
2020-06-04 14:59:23 +02:00
|
|
|
}
|
2020-06-27 14:36:50 +02:00
|
|
|
this.init();
|
2020-04-22 15:52:17 +02:00
|
|
|
this.updateWithData(data);
|
|
|
|
}
|
|
|
|
|
2020-06-04 14:59:23 +02:00
|
|
|
public getPrimaryKey(): string {
|
2020-06-27 14:36:50 +02:00
|
|
|
return this.modelClass.getPrimaryKey(this);
|
2020-06-04 14:59:23 +02:00
|
|
|
}
|
|
|
|
|
2020-06-27 14:36:50 +02:00
|
|
|
public getPrimaryKeyFields(): string[] {
|
|
|
|
return this.modelClass.getPrimaryKeyFields();
|
2020-06-04 14:59:23 +02:00
|
|
|
}
|
|
|
|
|
2020-06-27 14:36:50 +02:00
|
|
|
protected abstract init(): void;
|
2020-04-22 15:52:17 +02:00
|
|
|
|
2020-06-27 14:36:50 +02:00
|
|
|
protected addProperty<T>(name: string, validator?: Validator<T> | RegExp) {
|
2020-04-22 15:52:17 +02:00
|
|
|
if (validator === undefined) validator = new Validator();
|
|
|
|
if (validator instanceof RegExp) {
|
|
|
|
const regexp = validator;
|
|
|
|
validator = new Validator().regexp(regexp);
|
|
|
|
}
|
|
|
|
|
|
|
|
const prop = new ModelProperty<T>(name, validator);
|
|
|
|
this.properties.push(prop);
|
|
|
|
Object.defineProperty(this, name, {
|
|
|
|
get: () => prop.value,
|
|
|
|
set: (value: T) => prop.value = value,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private updateWithData(data: any) {
|
2020-06-04 14:59:23 +02:00
|
|
|
if (this.automaticIdProperty) this.id = data['id'];
|
2020-04-22 15:52:17 +02:00
|
|
|
|
|
|
|
for (const prop of this.properties) {
|
|
|
|
if (data[prop.name] !== undefined) {
|
|
|
|
this[prop.name] = data[prop.name];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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> {
|
|
|
|
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-06-27 14:36:50 +02:00
|
|
|
this.updateWithData((await this.modelClass.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);
|
|
|
|
if (exists && this.hasOwnProperty('updated_at')) {
|
|
|
|
this.updated_at = new Date();
|
|
|
|
}
|
|
|
|
|
|
|
|
const props = [];
|
|
|
|
const values = [];
|
|
|
|
|
|
|
|
if (exists) {
|
2020-06-04 14:59:23 +02:00
|
|
|
const data: any = {};
|
2020-04-22 15:52:17 +02:00
|
|
|
for (const prop of this.properties) {
|
|
|
|
if (prop.value !== undefined) {
|
2020-06-04 14:59:23 +02:00
|
|
|
data[prop.name] = prop.value;
|
2020-04-22 15:52:17 +02:00
|
|
|
} else {
|
|
|
|
needs_full_update = true;
|
|
|
|
}
|
|
|
|
}
|
2020-06-27 14:36:50 +02:00
|
|
|
let query = this.modelClass.update(data);
|
2020-06-04 14:59:23 +02:00
|
|
|
for (const indexField of this.getPrimaryKeyFields()) {
|
|
|
|
query = query.where(indexField, this[indexField]);
|
|
|
|
}
|
|
|
|
await query.execute(connection);
|
2020-04-22 15:52:17 +02:00
|
|
|
} else {
|
|
|
|
const props_holders = [];
|
|
|
|
for (const prop of this.properties) {
|
|
|
|
if (prop.value !== undefined) {
|
|
|
|
props.push(prop.name);
|
|
|
|
props_holders.push('?');
|
|
|
|
values.push(prop.value);
|
|
|
|
} else {
|
|
|
|
needs_full_update = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const result = await query(`INSERT INTO ${this.table} (${props.join(', ')}) VALUES(${props_holders.join(', ')})`, values, connection);
|
|
|
|
|
2020-06-04 14:59:23 +02:00
|
|
|
if (this.automaticIdProperty) this.id = result.other.insertId;
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return needs_full_update;
|
|
|
|
}
|
|
|
|
|
|
|
|
public get table(): string {
|
2020-06-27 14:36:50 +02:00
|
|
|
return this.modelClass.table;
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public async exists(): Promise<boolean> {
|
|
|
|
if (!this.id) return false;
|
|
|
|
|
2020-06-27 14:36:50 +02:00
|
|
|
let query = this.modelClass.select('1');
|
2020-06-04 14:59:23 +02:00
|
|
|
for (const indexField of this.getPrimaryKeyFields()) {
|
|
|
|
query = query.where(indexField, this[indexField]);
|
|
|
|
}
|
2020-06-27 14:36:50 +02:00
|
|
|
return typeof (await query.first()) !== 'undefined';
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public async delete(): Promise<void> {
|
|
|
|
if (!(await this.exists())) throw new Error('This model instance doesn\'t exist in DB.');
|
|
|
|
|
2020-06-27 14:36:50 +02:00
|
|
|
let query = this.modelClass.delete();
|
2020-06-04 14:59:23 +02:00
|
|
|
for (const indexField of this.getPrimaryKeyFields()) {
|
|
|
|
query = query.where(indexField, this[indexField]);
|
|
|
|
}
|
|
|
|
await query.execute();
|
|
|
|
if (this.automaticIdProperty) this.id = undefined;
|
2020-04-22 15:52:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public async validate(onlyFormat: boolean = false, connection?: Connection): Promise<void[]> {
|
|
|
|
return await Promise.all(this.properties.map(prop => prop.validate(onlyFormat, connection)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface ModelFactory<T extends Model> {
|
|
|
|
(data: any): T;
|
|
|
|
}
|
|
|
|
|
|
|
|
class ModelProperty<T> {
|
|
|
|
public readonly name: string;
|
|
|
|
private readonly validator: Validator<T>;
|
|
|
|
private val?: T;
|
|
|
|
|
|
|
|
constructor(name: string, validator: Validator<T>) {
|
|
|
|
this.name = name;
|
|
|
|
this.validator = validator;
|
|
|
|
}
|
|
|
|
|
|
|
|
public async validate(onlyFormat: boolean, connection?: Connection): Promise<void> {
|
|
|
|
return await this.validator.execute(this.name, this.value, onlyFormat, connection);
|
|
|
|
}
|
|
|
|
|
|
|
|
public get value(): T | undefined {
|
|
|
|
return this.val;
|
|
|
|
}
|
|
|
|
|
|
|
|
public set value(val: T | undefined) {
|
|
|
|
this.val = val;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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])?)+$/;
|