import {query, QueryResult} from "./MysqlConnectionManager"; import {Connection} from "mysql"; import Model, {ModelClass} from "./Model"; import Pagination from "../Pagination"; import ModelRelation from "./ModelRelation"; export default class ModelQuery { public static select(modelClass: ModelClass, ...fields: string[]): ModelQuery { return new ModelQuery(QueryType.SELECT, modelClass, fields.length > 0 ? fields : ['*']); } public static update(modelClass: ModelClass, data: { [key: string]: any }): ModelQuery { const fields = []; for (let key in data) { if (data.hasOwnProperty(key)) { fields.push(new UpdateFieldValue(key, data[key], false)); } } return new ModelQuery(QueryType.UPDATE, modelClass, fields); } public static delete(modelClass: ModelClass): ModelQuery { return new ModelQuery(QueryType.DELETE, modelClass); } private readonly type: QueryType; private readonly modelClass: ModelClass; private readonly table: string; private readonly fields: (string | SelectFieldValue | UpdateFieldValue)[]; private _leftJoin?: string; private _leftJoinOn: WhereFieldValue[] = []; private _where: WhereFieldValue[] = []; private _limit?: number; private _offset?: number; private _sortBy?: string; private _sortDirection?: 'ASC' | 'DESC'; private readonly relations: string[] = []; private _pivot?: string[]; private constructor(type: QueryType, modelClass: ModelClass, fields?: (string | SelectFieldValue | UpdateFieldValue)[]) { this.type = type; this.modelClass = modelClass; this.table = modelClass.table; this.fields = fields || []; } public leftJoin(table: string): this { this._leftJoin = table; return this; } public on(field1: string, field2: string, test: WhereTest = WhereTest.EQ, operator: WhereOperator = WhereOperator.AND): this { this._leftJoinOn.push(new WhereFieldValue(field1, field2, true, test, operator)); return this; } public where(field: string, value: string | Date | ModelQuery | any, test: WhereTest = WhereTest.EQ, operator: WhereOperator = WhereOperator.AND): this { this._where.push(new WhereFieldValue(field, value, false, test, operator)); return this; } public limit(limit: number, offset: number = 0): this { this._limit = limit; this._offset = offset; return this; } public sortBy(field: string, direction: 'ASC' | 'DESC' = 'ASC'): this { this._sortBy = field; this._sortDirection = direction; return this; } /** * @param relation The relation field name to eagerload */ public with(relation: string): this { this.relations.push(relation); return this; } public pivot(...fields: string[]): this { this._pivot = fields; return this; } public toString(final: boolean = false): string { let query = ''; if (this._pivot) this.fields.push(...this._pivot); let fields = this.fields.join(','); let join = ''; if (this._leftJoin) { join = `LEFT JOIN ${this._leftJoin} ON ${this._leftJoinOn[0]}`; for (let i = 1; i < this._leftJoinOn.length; i++) { join += this._leftJoinOn[i].toString(false); } } let where = ''; if (this._where.length > 0) { where = `WHERE ${this._where[0]}`; for (let i = 1; i < this._where.length; i++) { where += this._where[i].toString(false); } } let limit = ''; if (typeof this._limit === 'number') { limit = `LIMIT ${this._limit}`; if (typeof this._offset === 'number' && this._offset !== 0) { limit += ` OFFSET ${this._offset}`; } } let orderBy = ''; if (typeof this._sortBy === 'string') { orderBy = `ORDER BY ${this._sortBy} ${this._sortDirection}`; } switch (this.type) { case QueryType.SELECT: query = `SELECT ${fields} FROM ${this.table} ${join} ${where} ${orderBy} ${limit}`; break; case QueryType.UPDATE: query = `UPDATE ${this.table} SET ${fields} ${where} ${orderBy} ${limit}`; break; case QueryType.DELETE: query = `DELETE FROM ${this.table} ${where} ${orderBy} ${limit}`; break; } return final ? query : `(${query})`; } public build(): string { return this.toString(true); } public get variables(): any[] { const variables: any[] = []; this.fields?.filter(v => v instanceof FieldValue) .flatMap(v => (v).variables) .forEach(v => variables.push(v)); this._where.flatMap(v => v.variables) .forEach(v => variables.push(v)); return variables; } public async execute(connection?: Connection): Promise { return await query(this.build(), this.variables, connection); } public async get(connection?: Connection): Promise> { const queryResult = await this.execute(); const models: ModelQueryResult = []; if (this._pivot) models.pivot = []; // Eager loading init const relationMap: { [p: string]: ModelRelation[] } = {}; for (const relation of this.relations) { relationMap[relation] = []; } const factory = this.modelClass.getFactory(); for (const result of queryResult.results) { const model = factory(result); models.push(model); if (this._pivot) { const obj: any = {}; for (const field of this._pivot) { obj[field] = result[field]; } models.pivot!.push(obj); } // Eager loading init map for (const relation of this.relations) { relationMap[relation].push(model[relation]); } } // Eager loading execute for (const relationName of this.relations) { const relations = relationMap[relationName]; const allModels = await relations[0].eagerLoad(relations); await Promise.all(relations.map(r => r.populate(allModels))); } return models; } public async paginate(page: number, perPage: number, connection?: Connection): Promise> { this.limit(perPage, (page - 1) * perPage); const result = await this.get(connection); result.pagination = new Pagination(result, page, perPage, await this.count(true, connection)); return result; } public async first(): Promise { const models = await this.limit(1).get(); return models.length > 0 ? models[0] : null; } public async count(removeLimit: boolean = false, connection?: Connection): Promise { if (removeLimit) { this._limit = undefined; this._offset = undefined; } this._sortBy = undefined; this._sortDirection = undefined; this.fields.push('COUNT(*)'); let queryResult = await this.execute(connection); return queryResult.results.length; } } export interface ModelQueryResult extends Array { pagination?: Pagination; pivot?: { [p: string]: any }[]; } export enum QueryType { SELECT, UPDATE, DELETE, } export enum WhereOperator { AND = 'AND', OR = 'OR', } export enum WhereTest { EQ = '=', NE = '!=', GT = '>', GE = '>=', LT = '<', LE = '<=', IN = ' IN ', } class FieldValue { protected readonly field: string; protected value: any; protected raw: boolean; constructor(field: string, value: any, raw: boolean) { this.field = field; this.value = value; this.raw = raw; } public toString(first: boolean = true): string { return `${!first ? ',' : ''}${this.field}${this.test}${this.raw || this.value instanceof ModelQuery ? this.value : (Array.isArray(this.value) ? '(?)' : '?')}`; } protected get test(): string { return '='; } public get variables(): any[] { return this.value instanceof ModelQuery ? this.value.variables : [this.value]; } } class SelectFieldValue extends FieldValue { public toString(first: boolean = true): string { return `(${this.value instanceof ModelQuery ? this.value : '?'}) AS ${this.field}`; } } class UpdateFieldValue extends FieldValue { } class WhereFieldValue extends FieldValue { private readonly _test: WhereTest; private readonly operator: WhereOperator; constructor(field: string, value: any, raw: boolean, test: WhereTest, operator: WhereOperator) { super(field, value, raw); this._test = test; this.operator = operator; } public toString(first: boolean = true): string { return (!first ? ` ${this.operator} ` : '') + super.toString(true); } protected get test(): string { return this._test; } }