swaf/src/db/ModelQuery.ts

329 lines
10 KiB
TypeScript

import {query, QueryResult} from "./MysqlConnectionManager";
import {Connection} from "mysql";
import Model from "./Model";
import Pagination from "../Pagination";
import ModelRelation from "./ModelRelation";
import ModelFactory from "./ModelFactory";
export default class ModelQuery<M extends Model> {
public static select<M extends Model>(factory: ModelFactory<M>, ...fields: string[]): ModelQuery<M> {
return new ModelQuery(QueryType.SELECT, factory, fields.length > 0 ? fields : ['*']);
}
public static update<M extends Model>(factory: ModelFactory<M>, data: {
[key: string]: any
}): ModelQuery<M> {
const fields = [];
for (let key in data) {
if (data.hasOwnProperty(key)) {
fields.push(new UpdateFieldValue(key, data[key], false));
}
}
return new ModelQuery(QueryType.UPDATE, factory, fields);
}
public static delete<M extends Model>(factory: ModelFactory<M>): ModelQuery<M> {
return new ModelQuery(QueryType.DELETE, factory);
}
private readonly type: QueryType;
private readonly factory: ModelFactory<M>;
private readonly table: string;
private 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, factory: ModelFactory<M>, fields?: (string | SelectFieldValue | UpdateFieldValue)[]) {
this.type = type;
this.factory = factory;
this.table = factory.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> | 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 = '';
// Prevent wildcard and fields from conflicting
if (this._leftJoin) {
this.fields = this.fields.map(f => f.toString().split('.').length === 1 ? `\`${this.table}\`.${f}` : f);
}
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 => (<FieldValue>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<QueryResult> {
return await query(this.build(), this.variables, connection);
}
public async get(connection?: Connection): Promise<ModelQueryResult<M>> {
const queryResult = await this.execute(connection);
const models: ModelQueryResult<M> = [];
if (this._pivot) models.pivot = [];
// Eager loading init
const relationMap: { [p: string]: ModelRelation<any, any, any>[] } = {};
for (const relation of this.relations) {
relationMap[relation] = [];
}
for (const result of queryResult.results) {
const modelData: any = {};
for (const field of Object.keys(result)) {
modelData[field.split('.')[1] || field] = result[field];
}
const model = this.factory.create(modelData);
models.push(model);
if (this._pivot) {
const pivotData: any = {};
for (const field of this._pivot) {
pivotData[field] = result[field.split('.')[1]];
}
models.pivot!.push(pivotData);
}
// Eager loading init map
for (const relation of this.relations) {
if (model[relation] === undefined) throw new Error(`Relation ${relation} doesn't exist on ${model.constructor.name}.`);
relationMap[relation].push(model[relation]);
}
}
// Eager loading execute
for (const relationName of this.relations) {
const relations = relationMap[relationName];
if (relations.length > 0) {
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<ModelQueryResult<M>> {
this.limit(perPage, (page - 1) * perPage);
const result = await this.get(connection);
result.pagination = new Pagination<M>(result, page, perPage, await this.count(true, connection));
return result;
}
public async first(): Promise<M | null> {
const models = await this.limit(1).get();
return models.length > 0 ? models[0] : null;
}
public async count(removeLimit: boolean = false, connection?: Connection): Promise<number> {
if (removeLimit) {
this._limit = undefined;
this._offset = undefined;
}
this._sortBy = undefined;
this._sortDirection = undefined;
this.fields.splice(0, this.fields.length);
this.fields.push('COUNT(*)');
let queryResult = await this.execute(connection);
return queryResult.results[0]['COUNT(*)'];
}
}
export interface ModelQueryResult<M extends Model> extends Array<M> {
pagination?: Pagination<M>;
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 {
const valueStr = this.raw || this.value === null || this.value instanceof ModelQuery ? this.value :
(Array.isArray(this.value) ? `(${'?'.repeat(this.value.length).split('').join(',')})` : '?');
let field = this.field.split('.').map(p => `\`${p}\``).join('.');
return `${first ? '' : ','}${field}${this.test}${valueStr}`;
}
protected get test(): string {
return '=';
}
public get variables(): any[] {
if (this.value instanceof ModelQuery) return this.value.variables;
if (this.raw || this.value === null) return [];
if (Array.isArray(this.value)) return this.value;
return [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 {
if (this.value === null) {
if (this._test === WhereTest.EQ) {
return ' IS ';
} else if (this._test === WhereTest.NE) {
return ' IS NOT ';
}
}
return this._test;
}
}