import {EscapedValue, isWhereClause, isWhereGroup, WhereStatement} from '../types.ts' import {escape, SQLWhereOperator, WherePreOperator} from '../types.ts' import {WhereBuilderFunction} from './Select.ts' import {apply_filter_to_where, QueryFilter} from '../../model/filter.ts' import {Scope} from '../Scope.ts' import {FunctionScope, ScopeFunction} from '../scope/FunctionScope.ts' import RawValue from '../RawValue.ts' /** * Query builder mixin for queries that have WHERE clauses. */ export class WhereBuilder { /** * The where clauses to be applied. * @type Array */ protected _wheres: WhereStatement[] = [] /** * The scopes to be applied. * @type Array */ protected _scopes: Scope[] = [] /** * Get the where clauses applied to the query. * @type Array */ get where_items() { return this._wheres } /** * Remove a scope from this query. * @param {typeof Scope} scope * @return self */ without_scope(scope: typeof Scope) { this._scopes = this._scopes.filter(x => !(x instanceof Scope)) return this } /** * Add a scope to this query. * @param {Scope | ScopeFunction} scope * @return self */ with_scope(scope: Scope | ScopeFunction) { if ( scope instanceof Scope ) { this._scopes.push(scope) } else { this._scopes.push(new FunctionScope(scope)) } return this } /** * Add multiple scopes to this query. * @param {Array} scopes * @return self */ with_scopes(scopes: (Scope | ScopeFunction)[]) { scopes.forEach(scope => this.with_scope(scope)) return this } /** * Cast the where clause to raw SQL. * @param {Array} [wheres] * @param {number} [level = 0] - the indentation level */ wheres_to_sql(wheres?: WhereStatement[], level = 0): string { this._scopes.forEach(scope => scope.apply(this)) const indent = Array(level * 2).fill(' ').join('') let statements = [] for ( const where of wheres || this._wheres ) { if ( isWhereClause(where) ) { statements.push(`${indent}${statements.length < 1 ? '' : where.preop + ' '}${where.field} ${where.operator} ${where.operand}`) } else if ( isWhereGroup(where) ) { statements.push(`${indent}${statements.length < 1 ? '' : where.preop + ' '}(\n${this.wheres_to_sql(where.items, level + 1)}\n${indent})`) } } return statements.filter(Boolean).join('\n') } /** * Internal helper method for creating where clauses. * @param {WherePreOperator} preop * @param {string | WhereBuilderFunction} field * @param {SQLWhereOperator} [operator] * @param [operand] * @private */ private _createWhere(preop: WherePreOperator, field: string | WhereBuilderFunction, operator?: SQLWhereOperator, operand?: any) { if ( typeof field === 'function' ) { const where_builder = new WhereBuilder() field(where_builder) this._wheres.push({ preop, items: where_builder.where_items, }) } else if ( field && operator && typeof operand !== 'undefined' ) { this._wheres.push({ field, operator, operand: escape(operand), preop }) } } /** * Add a basic where clause to the query. * @param {string | WhereBuilderFunction} field * @param {SQLWhereOperator} [operator] * @param [operand] * @return self */ where(field: string | WhereBuilderFunction, operator?: SQLWhereOperator, operand?: any) { this._createWhere('AND', field, operator, operand) return this } /** * Add a where clause to the query, without escaping the operand. * @param {string | WhereBuilderFunction} field * @param {SQLWhereOperator} [operator] * @param [operand] * @return self */ whereRaw(field: string, operator: SQLWhereOperator, operand: string) { this._createWhere('AND', field, operator, new RawValue(operand)) return this } /** * Add an OR WHERE clause to the query, without escaping the operand. * @param {string | WhereBuilderFunction} field * @param {SQLWhereOperator} [operator] * @param [operand] * @return self */ orWhereRaw(field: string, operator: SQLWhereOperator, operand: string) { this._createWhere('OR', field, operator, new RawValue(operand)) return this } /** * Add a WHERE ... IN (...) clause to the query. * @param {string} field * @param {EscapedValue} values * @return self */ whereIn(field: string, values: EscapedValue) { this._wheres.push({ field, operator: 'IN', operand: escape(values), preop: 'AND', }) return this } /** * Add a WHERE NOT ... clause to the query. * @param {string | WhereBuilderFunction} field * @param {SQLWhereOperator} [operator] * @param [operand] * @return self */ whereNot(field: string | WhereBuilderFunction, operator?: SQLWhereOperator, operand?: EscapedValue) { this._createWhere('AND NOT', field, operator, operand) return this } /** * Add a WHERE ... NOT IN (...) clause to the query. * @param {string} field * @param {EscapedValue} values * @return self */ whereNotIn(field: string, values: EscapedValue) { this._wheres.push({ field, operator: 'NOT IN', operand: escape(values), preop: 'AND' }) return this } /** * Add an OR WHERE ... clause to the query. * @param {string | WhereBuilderFunction} field * @param {SQLWhereOperator} [operator] * @param [operand] * @return self */ orWhere(field: string | WhereBuilderFunction, operator?: SQLWhereOperator, operand?: EscapedValue) { this._createWhere('OR', field, operator, operand) return this } /** * Add an OR WHERE NOT clause to the query. * @param {string | WhereBuilderFunction} field * @param {SQLWhereOperator} [operator] * @param [operand] * @return self */ orWhereNot(field: string | WhereBuilderFunction, operator?: SQLWhereOperator, operand?: EscapedValue) { this._createWhere('OR NOT', field, operator, operand) return this } /** * Add an OR WHERE ... IN (...) clause to the query. * @param {string} field * @param {EscapedValue} values * @return self */ orWhereIn(field: string, values: EscapedValue) { this._wheres.push({ field, operator: 'IN', operand: escape(values), preop: 'OR', }) return this } /** * Add an OR WHERE ... NOT IN (...) clause to the query. * @param {string} field * @param {EscapedValue} values * @return self */ orWhereNotIn(field: string, values: EscapedValue) { this._wheres.push({ field, operator: 'NOT IN', operand: escape(values), preop: 'OR', }) return this } /** * Add a WHERE ... BETWEEN ... AND ... clause to the query. * @param {string} field * @param {EscapedValue} lower_bound * @param {EscapedValue} upper_bound * @return self */ whereBetween(field: string, lower_bound: EscapedValue, upper_bound: EscapedValue) { this._wheres.push({ field, operator: 'BETWEEN', operand: `${escape(lower_bound)} AND ${escape(upper_bound)}`, preop: 'AND', }) return this } /** * Add an OR WHERE ... BETWEEN ... AND ... clause to the query. * @param {string} field * @param {EscapedValue} lower_bound * @param {EscapedValue} upper_bound * @return self */ orWhereBetween(field: string, lower_bound: EscapedValue, upper_bound: EscapedValue) { this._wheres.push({ field, operator: 'BETWEEN', operand: `${escape(lower_bound)} AND ${escape(upper_bound)}`, preop: 'OR', }) return this } /** * Add a WHERE ... NOT BETWEEN ... AND ... clause to the query. * @param {string} field * @param {EscapedValue} lower_bound * @param {EscapedValue} upper_bound * @return self */ whereNotBetween(field: string, lower_bound: EscapedValue, upper_bound: EscapedValue) { this._wheres.push({ field, operator: 'NOT BETWEEN', operand: `${escape(lower_bound)} AND ${escape(upper_bound)}`, preop: 'AND', }) return this } /** * Add an OR WHERE ... NOT BETWEEN ... AND ... clause to the query. * @param {string} field * @param {EscapedValue} lower_bound * @param {EscapedValue} upper_bound * @return self */ orWhereNotBetween(field: string, lower_bound: EscapedValue, upper_bound: EscapedValue) { this._wheres.push({ field, operator: 'NOT BETWEEN', operand: `${escape(lower_bound)} AND ${escape(upper_bound)}`, preop: 'OR', }) return this } /** * Apply a filter object to the query. * @param {QueryFilter} filter * @return self */ filter(filter: QueryFilter) { return apply_filter_to_where(filter, this) } }