From e63b5d21fe9fa665a4a18ad39fe79b4e29a4d4ad Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Sun, 14 Jun 2020 11:43:31 +0200 Subject: [PATCH] Add left join to query builder --- src/auth/models/MagicLink.ts | 3 +- src/db/Query.ts | 70 +++++++++++++++++++++--------------- src/db/Validator.ts | 4 +-- 3 files changed, 46 insertions(+), 31 deletions(-) diff --git a/src/auth/models/MagicLink.ts b/src/auth/models/MagicLink.ts index 90b45b0..291f578 100644 --- a/src/auth/models/MagicLink.ts +++ b/src/auth/models/MagicLink.ts @@ -5,6 +5,7 @@ import AuthProof from "../AuthProof"; import Validator from "../../db/Validator"; import User from "./User"; import argon2 from "argon2"; +import {WhereTest} from "../../db/Query"; export default class MagicLink extends Model implements AuthProof { public static async bySessionID(sessionID: string, actionType?: string | string[]): Promise { @@ -13,7 +14,7 @@ export default class MagicLink extends Model implements AuthProof { if (typeof actionType === 'string') { query = query.where('action_type', actionType); } else { - query = query.whereIn('action_type', actionType); + query = query.where('action_type', actionType, WhereTest.IN); } } const links = await this.models(query.first()); diff --git a/src/db/Query.ts b/src/db/Query.ts index 882f5eb..decc9b1 100644 --- a/src/db/Query.ts +++ b/src/db/Query.ts @@ -8,23 +8,25 @@ export default class Query { public static update(table: string, data: { [key: string]: any - }) { + }): Query { const fields = []; for (let key in data) { if (data.hasOwnProperty(key)) { - fields.push(new UpdateFieldValue(key, data[key])); + fields.push(new UpdateFieldValue(key, data[key], false)); } } return new Query(QueryType.UPDATE, table, fields); } - public static delete(table: string) { + public static delete(table: string): Query { return new Query(QueryType.DELETE, table); } private readonly type: QueryType; 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; @@ -38,40 +40,38 @@ export default class Query { this.fields = fields || []; } - public where(field: string, value: string | Date | Query | any, operator: WhereOperator = WhereOperator.AND, test: WhereTest = WhereTest.EQUALS): Query { - this._where.push(new WhereFieldValue(field, value, operator, test)); + public leftJoin(table: string): this { + this._leftJoin = table; return this; } - public whereNot(field: string, value: string | Date | Query | any, operator: WhereOperator = WhereOperator.AND): Query { - return this.where(field, value, operator, WhereTest.DIFFERENT); + 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 orWhere(field: string, value: string | Date | Query | any): Query { - return this.where(field, value, WhereOperator.OR); + public where(field: string, value: string | Date | Query | any, test: WhereTest = WhereTest.EQ, operator: WhereOperator = WhereOperator.AND): this { + this._where.push(new WhereFieldValue(field, value, false, test, operator)); + return this; } - public whereIn(field: string, value: any[]): Query { - return this.where(field, value, WhereOperator.AND, WhereTest.IN); - } - - public limit(limit: number, offset: number = 0): Query { + public limit(limit: number, offset: number = 0): this { this._limit = limit; this._offset = offset; return this; } - public first(): Query { + public first(): this { return this.limit(1); } - public sortBy(field: string, direction: 'ASC' | 'DESC' = 'ASC'): Query { + public sortBy(field: string, direction: 'ASC' | 'DESC' = 'ASC'): this { this._sortBy = field; this._sortDirection = direction; return this; } - public withTotalRowCount(): Query { + public withTotalRowCount(): this { this._foundRows = true; return this; } @@ -81,6 +81,14 @@ export default class Query { 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]}`; @@ -104,7 +112,7 @@ export default class Query { switch (this.type) { case QueryType.SELECT: - query = `SELECT ${this._foundRows ? 'SQL_CALC_FOUND_ROWS' : ''} ${fields} FROM ${this.table} ${where} ${orderBy} ${limit}`; + query = `SELECT ${this._foundRows ? 'SQL_CALC_FOUND_ROWS' : ''} ${fields} FROM ${this.table} ${join} ${where} ${orderBy} ${limit}`; break; case QueryType.UPDATE: query = `UPDATE ${this.table} SET ${fields} ${where} ${orderBy} ${limit}`; @@ -152,28 +160,34 @@ export enum QueryType { DELETE, } -enum WhereOperator { +export enum WhereOperator { AND = 'AND', OR = 'OR', } -enum WhereTest { - EQUALS = '=', - DIFFERENT = '!=', +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) { + 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.value instanceof Query ? this.value : (Array.isArray(this.value) ? '(?)' : '?')}`; + return `${!first ? ',' : ''}${this.field}${this.test}${this.raw || this.value instanceof Query ? this.value : (Array.isArray(this.value) ? '(?)' : '?')}`; } protected get test(): string { @@ -195,13 +209,13 @@ class UpdateFieldValue extends FieldValue { } class WhereFieldValue extends FieldValue { - private readonly operator: WhereOperator; private readonly _test: WhereTest; + private readonly operator: WhereOperator; - constructor(field: string, value: any, operator: WhereOperator, test: WhereTest) { - super(field, value); - this.operator = operator; + 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 { diff --git a/src/db/Validator.ts b/src/db/Validator.ts index 275417e..f4809d3 100644 --- a/src/db/Validator.ts +++ b/src/db/Validator.ts @@ -1,5 +1,5 @@ import Model from "./Model"; -import Query from "./Query"; +import Query, {WhereTest} from "./Query"; import {Connection} from "mysql"; import {Type} from "../Utils"; @@ -184,7 +184,7 @@ export default class Validator { } else { query = (model instanceof Model ? model.constructor : model).select('1').where(foreignKey, val); } - if (model instanceof Model && typeof model.id === 'number') query = query.whereNot('id', model.id); + if (model instanceof Model && typeof model.id === 'number') query = query.where('id', model.id, WhereTest.NE); return (await query.execute(c)).results.length === 0; }, throw: () => new AlreadyExistsValidationError((model).table),