import MysqlConnectionManager, {query} from "./MysqlConnectionManager"; import Validator from "./Validator"; import {Connection} from "mysql"; import ModelComponent from "./ModelComponent"; import {Type} from "../Utils"; import ModelFactory from "./ModelFactory"; import ModelRelation from "./ModelRelation"; import ModelQuery, {ModelQueryResult, SelectFields, SelectFieldValue} from "./ModelQuery"; import {Request} from "express"; 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 create(this: Type, data: any): T { return ModelFactory.get(this).create(data); } public static select(this: Type, ...fields: string[]): ModelQuery { return ModelFactory.get(this).select(...fields); } public static update(this: Type, data: { [key: string]: any }): ModelQuery { return ModelFactory.get(this).update(data); } public static delete(this: Type): ModelQuery { return ModelFactory.get(this).delete(); } public static async getById(this: Type, ...id: any): Promise { return ModelFactory.get(this).getById(...id); } public static async paginate(this: Type, request: Request, perPage: number = 20, query?: ModelQuery): Promise> { return ModelFactory.get(this).paginate(request, perPage, query); } protected readonly _factory: ModelFactory; private readonly _components: ModelComponent[] = []; private readonly _validators: { [key: string]: Validator } = {}; private _cached_exists?: boolean = undefined; [key: string]: any; public constructor(factory: ModelFactory) { if (!factory || !(factory instanceof ModelFactory)) throw new Error('Cannot instantiate model directly.'); this._factory = factory; this.init(); } protected abstract init(): void; protected setValidation(propertyName: keyof this): Validator { const validator = new Validator(); this._validators[propertyName as string] = validator; return validator; } public addComponent(modelComponent: ModelComponent): void { modelComponent.applyToModel(); this._components.push(modelComponent); } public as>(type: Type): T { for (const component of this._components) { if (component instanceof type) { return this; } } throw new Error(`Component ${type.name} was not initialized for this ${this.constructor.name}.`); } public updateWithData(data: any) { for (const property of this._properties) { if (data[property] !== undefined) { this[property] = data[property]; } } } /** * Override this to automatically fill obvious missing data i.e. from relation or default value that are fetched * asynchronously. */ protected async autoFill(): Promise { } protected async beforeSave(exists: boolean, connection: Connection): Promise { } protected async afterSave(): Promise { } public async save(connection?: Connection, postHook?: (callback: () => Promise) => void): Promise { await this.autoFill(); 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._factory.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('created_at')) { this.created_at = new Date(); } if (exists && this.hasOwnProperty('updated_at')) { this.updated_at = new Date(); } const properties = []; const values = []; if (exists) { const data: any = {}; for (const property of this._properties) { const value = this[property]; if (value !== undefined) { data[property] = value; } else { needs_full_update = true; } } let query = this._factory.update(data); for (const indexField of this._factory.getPrimaryKeyFields()) { query = query.where(indexField, this[indexField]); } await query.execute(connection); } else { const props_holders = []; for (const property of this._properties) { const value = this[property]; if (value !== undefined) { properties.push(property); props_holders.push('?'); values.push(value); } else { needs_full_update = true; } } const fieldNames = properties.map(f => `\`${f}\``).join(', '); const result = await query(`INSERT INTO ${this.table} (${fieldNames}) VALUES(${props_holders.join(', ')})`, values, connection); if (this.hasOwnProperty('id')) this.id = result.other.insertId; this._cached_exists = true; } return needs_full_update; } public async delete(): Promise { if (!(await this.exists())) throw new Error('This model instance doesn\'t exist in DB.'); let query = this._factory.delete(); for (const indexField of this._factory.getPrimaryKeyFields()) { query = query.where(indexField, this[indexField]); } await query.execute(); this._cached_exists = false; } public async validate(onlyFormat: boolean = false, connection?: Connection): Promise { return await Promise.all(this._properties.map( prop => this._validators[prop]?.execute(prop, this[prop], onlyFormat, connection) )); } public async exists(): Promise { if (this._cached_exists === undefined) { const query = this._factory.select(''); for (const indexField of this._factory.getPrimaryKeyFields()) { query.where(indexField, this[indexField]); } this._cached_exists = (await query.limit(1).execute()).results.length > 0; } return this._cached_exists; } public equals(model: this): boolean { for (const field of this._factory.getPrimaryKeyFields()) { if (this[field] !== model[field]) return false; } return true; } public get table(): string { return this._factory.table; } private get _properties(): string[] { return Object.getOwnPropertyNames(this).filter(p => { return !p.startsWith('_') && typeof this[p] !== 'function' && !(this[p] instanceof ModelRelation); }); } }