import {Inject} from "../../di"; import {DatabaseService} from "../DatabaseService"; import { Constraint, ConstraintConnectionOperator, ConstraintOperator, OrderDirection, OrderStatement, QueryResult, QuerySource, SpecifiedField } from "../types"; import {Connection} from "../connection/Connection"; import {deepCopy, ErrorWithContext} from "../../util"; import {EscapeValue, QuerySafeValue, raw} from "../dialect/SQLDialect"; import {ResultCollection} from "./result/ResultCollection"; import {AbstractResultIterable} from "./result/AbstractResultIterable"; import {AppClass} from "../../lifecycle/AppClass"; /** * Type alias for a function that applies some constraints to a builder group. */ export type ConstraintGroupClosure = (group: AbstractBuilder) => any /** * A base class that facilitates building database queries using a fluent interface. * This can be specialized by child-classes to yield query results of the given type `T`. */ export abstract class AbstractBuilder extends AppClass { @Inject() protected readonly databaseService!: DatabaseService /** Constraints applied to this query. */ protected constraints: Constraint[] = [] /** The source table to query from. */ protected source?: QuerySource /** The fields to query from the table. */ protected _fields: SpecifiedField[] = [] /** The number of records to skip before the result set. */ protected _skip?: number /** The max number of records to include in the result set. */ protected _take?: number /** If true, the query should refer to distinct records. */ protected _distinct: boolean = false /** Array of SQL group-by clauses. */ protected _groupings: string[] = [] /** Array of SQL order-by clauses. */ protected _orders: OrderStatement[] = [] /** The connection on which the query should be executed. */ protected _connection?: Connection /** * Create a new, empty, instance of the current builder. */ public abstract getNewInstance(): AbstractBuilder /** * Get a result iterable for the built query. */ public abstract getResultIterable(): AbstractResultIterable /** * Clone the current query to a new AbstractBuilder instance with the same properties. */ public clone(): AbstractBuilder { const bldr = this.getNewInstance() bldr.constraints = deepCopy(this.constraints) bldr.source = deepCopy(this.source) bldr._fields = deepCopy(this._fields) bldr._skip = deepCopy(this._skip) bldr._take = deepCopy(this._take) bldr._distinct = deepCopy(this._distinct) bldr._groupings = deepCopy(this._groupings) bldr._orders = deepCopy(this._orders) bldr._connection = this._connection return bldr } /** Get the constraints applied to this query. */ public get appliedConstraints() { return deepCopy(this.constraints) } /** Get the fields that should be included in this query. */ public get appliedFields() { return deepCopy(this._fields) } /** Get the skip/take values of this query. */ public get appliedPagination() { return { skip: this._skip, take: this._take } } /** True if the query should be DISTINCT */ public get appliedDistinction() { return this._distinct } /** Get the SQL group-by clauses applied to this query. */ public get appliedGroupings() { return deepCopy(this._groupings) } /** Get the SQL order-by clauses applied to this query. */ public get appliedOrder() { return deepCopy(this._orders) } /** Get the source table for this query. */ public get querySource() { if ( this.source ) return deepCopy(this.source) } /** * Set the source table (and optional alias) for this query. * @param table * @param alias */ from(table: string, alias?: string) { if ( alias ) { this.source = { table, alias } } else { this.source = table } return this } /** * Alias of `from()`. * @param table * @param alias */ table(table: string, alias?: string) { return this.from(table, alias) } /** * Include the given field (and optional alias) in the query. * @param field * @param alias */ field(field: string | QuerySafeValue, alias?: string) { if ( alias ) { this._fields.push({ field, alias }) } else { this._fields.push(field) } return this } /** * Include the given fields in the query. * @param fields */ fields(...fields: SpecifiedField[]): this { this._fields = [...this._fields, ...fields] return this } /** * Alias of `fields()`. * @param fields */ returning(...fields: SpecifiedField[]): this { return this.fields(...fields) } /** * Alias of `fields()`. * @param fields */ select(...fields: SpecifiedField[]): this { return this.fields(...fields) } /** * Remove all selected fields from this query. */ clearFields() { this._fields = [] return this } /** * Apply a new WHERE constraint to the query. * @param field * @param operator * @param operand */ where(field: string | ConstraintGroupClosure, operator?: ConstraintOperator, operand?: EscapeValue) { this.createConstraint('AND', field, operator, operand) return this } /** * Apply a new WHERE constraint to the query, without escaping `operand`. Prefer `where()`. * @param field * @param operator * @param operand */ whereRaw(field: string, operator: ConstraintOperator, operand: string) { this.createConstraint('AND', field, operator, raw(operand)) return this } /** * Apply a new WHERE NOT constraint to the query. * @param field * @param operator * @param operand */ whereNot(field: string | ConstraintGroupClosure, operator?: ConstraintOperator, operand?: EscapeValue) { this.createConstraint('AND NOT', field, operator, operand) return this } /** * Apply an OR WHERE constraint to the query. * @param field * @param operator * @param operand */ orWhere(field: string | ConstraintGroupClosure, operator?: ConstraintOperator, operand?: EscapeValue) { this.createConstraint('OR', field, operator, operand) return this } /** * Apply an OR WHERE NOT constraint to the query. * @param field * @param operator * @param operand */ orWhereNot(field: string | ConstraintGroupClosure, operator?: ConstraintOperator, operand?: EscapeValue) { this.createConstraint('OR NOT', field, operator, operand) return this } /** * Apply an OR WHERE constraint to the query, without escaping `operand`. Prefer `orWhere()`. * @param field * @param operator * @param operand */ orWhereRaw(field: string, operator: ConstraintOperator, operand: string) { this.createConstraint('OR', field, operator, raw(operand)) return this } /** * Apply a WHERE IN constraint to the query, escaping the values in the set. * @param field * @param values */ whereIn(field: string, values: EscapeValue) { this.constraints.push({ field, operator: 'IN', operand: values, preop: 'AND', }) return this } /** * Apply a WHERE NOT IN constraint to the query, escaping the values in the set. * @param field * @param values */ whereNotIn(field: string, values: EscapeValue) { this.constraints.push({ field, operator: 'NOT IN', operand: values, preop: 'AND', }) return this } /** * Apply an OR WHERE IN constraint to the query, escaping the values in the set. * @param field * @param values */ orWhereIn(field: string, values: EscapeValue) { this.constraints.push({ field, operator: 'IN', operand: values, preop: 'OR' }) return this } /** * Apply an OR WHERE NOT IN constraint to the query, escaping the values in the set. * @param field * @param values */ orWhereNotIn(field: string, values: EscapeValue) { this.constraints.push({ field, operator: 'NOT IN', operand: values, preop: 'OR' }) return this } /** * Limit the query to a maximum number of rows. * @param rows */ limit(rows: number) { this._take = rows return this } /** * Alias of `limit()`. * @param rows */ take(rows: number) { return this.limit(rows) } /** * Skip the first `rows` many rows in the result set. * @param rows */ skip(rows: number) { this._skip = rows return this } /** * Alias of `skip()`. * @param rows */ offset(rows: number) { return this.skip(rows) } /** * Make the query return only distinct rows. */ distinct() { this._distinct = true return this } /** * Allow the query to return non-distinct rows. (Undoes `distinct()`.) */ notDistinct() { this._distinct = false return this } /** * Apply `skip()` and `take()` calls to retrieve the records that should appear on * the `pageNum` page, assuming each page has `pageSize` many records. * @param pageNum * @param pageSize */ page(pageNum: number = 1, pageSize: number = 20) { this.skip(pageSize * (pageNum - 1)) this.take(pageSize) return this } /** * Apply one or more GROUP-BY clauses to the query. * @param groupings */ groupBy(...groupings: string[]) { this._groupings = groupings return this } /** * Order the query by the given field. * @param field * @param direction */ orderBy(field: string, direction: OrderDirection = 'ASC') { this._orders.push({ field, direction }) return this } /** * Order the query by the given field, ascending. * @param field */ orderByAscending(field: string) { return this.orderBy(field, 'ASC') } /** * Order the query by the given field, descending. * @param field */ orderByDescending(field: string) { return this.orderBy(field, 'DESC') } /** * Specify the connection name or instance to execute the query on. * @param nameOrInstance */ connection(nameOrInstance: string | Connection) { if ( nameOrInstance instanceof Connection ) { this._connection = nameOrInstance } else { this._connection = this.databaseService.get(nameOrInstance) } return this } /** * Get a result iterable for the rows of this query. */ iterator(): AbstractResultIterable { if ( !this._connection ) { throw new ErrorWithContext(`No connection specified to fetch iterator for query.`) } return this.getResultIterable(); } /** * Get an async collection of the rows resulting from this query. */ get(): ResultCollection { return new ResultCollection(this.iterator()) } /** * Get the first record matched by this query, if it exists. */ async first(): Promise { return this.iterator().at(0) } /** * Run an UPDATE query for all rows matched by this query, setting the given data. * * @example * ```typescript * query.table('my_table').update({ my_col: 4 }) * ``` * * This is equivalent to: * ```sql * UPDATE TO my_table * SET * my_col = 4 * ``` * * @param data */ async update(data: {[key: string]: EscapeValue}): Promise { if ( !this._connection ) { throw new ErrorWithContext(`No connection specified to execute update query.`) } const query = this._connection.dialect().renderUpdate(this, data) return this._connection.query(query) } /** * Execute a DELETE based on this query. * * @example * ```typescript * query.table('my_table').where('id', <, 44).delete() * ``` * * This is equivalent to: * ```sql * DELETE * FROM my_table * WHERE * id < 44 * ``` * */ async delete(): Promise { if ( !this._connection ) { throw new ErrorWithContext(`No connection specified to execute update query.`) } const query = this._connection.dialect().renderDelete(this) return this._connection.query(query) } /** * Insert the given rows into the table for this query, returning the fields specified in this query. * * @example * ```typescript * const rows = [ * { name: 'A' }, * { name: 'B' }, * ] * * query.table('my_table') * .returning('id', 'name') * .insert(rows) * ``` * * This is equivalent to: * ```sql * INSERT INTO my_table (name) * VALUES ('A'), ('B') * RETURNING id, name * ``` * * @param rowOrRows */ async insert(rowOrRows: {[key: string]: EscapeValue}|{[key: string]: EscapeValue}[]) { if ( !this._connection ) { throw new ErrorWithContext(`No connection specified to execute update query.`) } const query = this._connection.dialect().renderInsert(this, rowOrRows) return this._connection.query(query) } /** * Returns true if at least one row matches the current query. */ async exists() { if ( !this._connection ) { throw new ErrorWithContext(`No connection specified to execute update query.`) } const query = this._connection.dialect().renderExistential(this) const result = await this._connection.query(query) return !!result.rows.first() } /** * Adds a constraint to this query. This is used internally by the various `where`, `whereIn`, `orWhereNot`, &c. * @param preop * @param field * @param operator * @param operand * @private */ private createConstraint(preop: ConstraintConnectionOperator, field: string | ConstraintGroupClosure, operator?: ConstraintOperator, operand?: any) { if ( typeof field === 'function' ) { const builder = this.getNewInstance() field(builder) this.constraints.push({ preop, items: builder.appliedConstraints }) } else if ( field && operator && typeof operand !== 'undefined' ) { this.constraints.push({ field, operator, operand, preop, // FIXME escape operand }) } } }