You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
lib/src/orm/builder/AbstractBuilder.ts

575 lines
15 KiB

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<T> = (group: AbstractBuilder<T>) => 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<T> 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<T>
/**
* Get a result iterable for the built query.
*/
public abstract getResultIterable(): AbstractResultIterable<T>
/**
* Clone the current query to a new AbstractBuilder instance with the same properties.
*/
public clone(): AbstractBuilder<T> {
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<T>, 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<T>, 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<T>, 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<T>, 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<T> {
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<T> {
return new ResultCollection<T>(this.iterator())
}
/**
* Get the first record matched by this query, if it exists.
*/
async first(): Promise<T | undefined> {
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<QueryResult> {
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<QueryResult> {
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<T>, 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
})
}
}
}