import MysqlConnectionManager, {query} from "./MysqlConnectionManager"; import Validator from "./Validator"; import {Connection} from "mysql"; import ModelQuery, {ModelQueryResult} from "./ModelQuery"; import {Request} from "express"; import {Type} from "../Utils"; export interface ModelClass extends Type { getFactory(factory?: ModelFactory): ModelFactory; table: string; getPrimaryKey(modelData: any): string; getPrimaryKeyFields(): string[]; select(...fields: string[]): ModelQuery; update(data: { [key: string]: any }): ModelQuery; delete(): ModelQuery; } export default abstract class Model { public static get table(): string { return this.name .replace(/(?:^|\.?)([A-Z])/g, (x, y) => '_' + y.toLowerCase()) .replace(/^_/, '') + 's'; } public static async getById(this: ModelClass, id: number): Promise { return this.select().where('id', id).first(); } public static async paginate(this: ModelClass, request: Request, perPage: number = 20, query?: ModelQuery): Promise> { let page = request.params.page ? parseInt(request.params.page) : 1; if (!query) query = this.select(); 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'); } return await query.paginate(page, perPage); } public static select(this: ModelClass, ...fields: string[]): ModelQuery { return ModelQuery.select(this, ...fields); } public static update(this: ModelClass, data: { [key: string]: any }): ModelQuery { return ModelQuery.update(this, data); } public static delete(this: ModelClass): ModelQuery { return ModelQuery.delete(this); } public static getPrimaryKey(modelData: any): string { return this.getPrimaryKeyFields().map(f => `${modelData[f]}`).join(','); } public static getPrimaryKeyFields(): string[] { return ['id']; } public static async loadRelation(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 (model).models((model).select().whereIn('id', ids))) { loadMap[v.id!](v); } } public static getFactory(this: ModelClass, factory?: ModelFactory): ModelFactory { if (factory === undefined) { factory = (this).FACTORY; if (factory === undefined) factory = data => new (this)(data); } return factory; } protected readonly modelClass: ModelClass = >this.constructor; protected readonly properties: ModelProperty[] = []; public id?: number; private readonly automaticIdProperty: boolean; [key: string]: any; public constructor(data: any, automaticIdProperty: boolean = true) { this.automaticIdProperty = automaticIdProperty; if (automaticIdProperty) { this.addProperty('id', new Validator()); } this.init(); this.updateWithData(data); } public getPrimaryKey(): string { return this.modelClass.getPrimaryKey(this); } public getPrimaryKeyFields(): string[] { return this.modelClass.getPrimaryKeyFields(); } protected abstract init(): void; protected addProperty(name: string, validator?: Validator | RegExp) { if (validator === undefined) validator = new Validator(); if (validator instanceof RegExp) { const regexp = validator; validator = new Validator().regexp(regexp); } const prop = new ModelProperty(name, validator); this.properties.push(prop); Object.defineProperty(this, name, { get: () => prop.value, set: (value: T) => prop.value = value, }); } private updateWithData(data: any) { if (this.automaticIdProperty) this.id = data['id']; 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 { } protected async afterSave(): Promise { } public async save(connection?: Connection, postHook?: (callback: () => Promise) => void): Promise { 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) { this.updateWithData((await this.modelClass.select().where('id', this.id!).limit(1).execute()).results[0]); } await this.afterSave(); }; if (connection) { postHook!(callback); } else { await callback(); } } private async saveTransaction(connection: Connection, exists: boolean, needs_full_update: boolean): Promise { // Before save await this.beforeSave(exists, connection); if (exists && this.hasOwnProperty('updated_at')) { this.updated_at = new Date(); } const props = []; const values = []; if (exists) { const data: any = {}; for (const prop of this.properties) { if (prop.value !== undefined) { data[prop.name] = prop.value; } else { needs_full_update = true; } } let query = this.modelClass.update(data); for (const indexField of this.getPrimaryKeyFields()) { query = query.where(indexField, this[indexField]); } await query.execute(connection); } 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); if (this.automaticIdProperty) this.id = result.other.insertId; } return needs_full_update; } public get table(): string { return this.modelClass.table; } public async exists(): Promise { if (!this.id) return false; let query = this.modelClass.select('1'); for (const indexField of this.getPrimaryKeyFields()) { query = query.where(indexField, this[indexField]); } return (await query.limit(1).execute()).results.length > 0; } public async delete(): Promise { if (!(await this.exists())) throw new Error('This model instance doesn\'t exist in DB.'); let query = this.modelClass.delete(); for (const indexField of this.getPrimaryKeyFields()) { query = query.where(indexField, this[indexField]); } await query.execute(); if (this.automaticIdProperty) this.id = undefined; } public async validate(onlyFormat: boolean = false, connection?: Connection): Promise { return await Promise.all(this.properties.map(prop => prop.validate(onlyFormat, connection))); } } export interface ModelFactory { (data: any): T; } class ModelProperty { public readonly name: string; private readonly validator: Validator; private val?: T; constructor(name: string, validator: Validator) { this.name = name; this.validator = validator; } public async validate(onlyFormat: boolean, connection?: Connection): Promise { 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])?)+$/;