JSDoc all the things!
This commit is contained in:
@@ -3,6 +3,10 @@ import Config from '../../lib/src/unit/Config.ts'
|
||||
import {Unit} from '../../lib/src/lifecycle/decorators.ts'
|
||||
import Database from './service/Database.ts'
|
||||
|
||||
/**
|
||||
* Lifecycle unit which loads and creates database connections from the database config files.
|
||||
* @extends LifecycleUnit
|
||||
*/
|
||||
@Unit()
|
||||
export class DatabaseUnit extends LifecycleUnit {
|
||||
constructor(
|
||||
|
||||
@@ -3,6 +3,10 @@ import {Model} from './model/Model.ts'
|
||||
import {Unit} from '../../lib/src/lifecycle/decorators.ts'
|
||||
import {StaticCanonical} from '../../lib/src/unit/StaticCanonical.ts'
|
||||
|
||||
/**
|
||||
* Canonical unit which loads ORM models from their directory.
|
||||
* @extends StaticCanonical
|
||||
*/
|
||||
@Unit()
|
||||
export default class ModelsUnit extends StaticCanonical<Model<any>, typeof Model> {
|
||||
protected base_path = './app/models'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {escape, EscapedValue, FieldSet, QuerySource} from './types.ts'
|
||||
import { Select } from './type/Select.ts'
|
||||
import {EscapedValue, FieldSet, QuerySource} from './types.ts'
|
||||
import {Select} from './type/Select.ts'
|
||||
import RawValue from './RawValue.ts'
|
||||
import {Statement} from './Statement.ts'
|
||||
import {Update} from './type/Update.ts'
|
||||
@@ -7,55 +7,111 @@ import {Insert} from './type/Insert.ts'
|
||||
import {Delete} from './type/Delete.ts'
|
||||
import {Truncate} from './type/Truncate.ts'
|
||||
|
||||
/**
|
||||
* Wrap a string so it gets included in the query unescaped.
|
||||
* @param {string} value
|
||||
* @return RawValue
|
||||
*/
|
||||
export function raw(value: string) {
|
||||
return new RawValue(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when an interpolated statement has an incorrect number of arguments.
|
||||
* @extends Error
|
||||
*/
|
||||
export class IncorrectInterpolationError extends Error {
|
||||
constructor(expected: number, received: number) {
|
||||
super(`Unable to interpolate arguments into query. Expected ${expected} argument${expected === 1 ? '' : 's'}, but received ${received}.`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base query builder class used to start various types of queries.
|
||||
*/
|
||||
export class Builder<T> {
|
||||
// create table, alter table, drop table, select
|
||||
|
||||
/**
|
||||
* Get a new SELECT statement.
|
||||
* @param {...FieldSet} fields
|
||||
* @return Select
|
||||
*/
|
||||
public select(...fields: FieldSet[]): Select<T> {
|
||||
fields = fields.flat()
|
||||
const select = new Select<T>()
|
||||
return select.fields(...fields)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new UPDATE statement.
|
||||
* @param {QuerySource} [target]
|
||||
* @param {string} [alias]
|
||||
* @return Update
|
||||
*/
|
||||
public update(target?: QuerySource, alias?: string): Update<T> {
|
||||
const update = new Update<T>()
|
||||
if ( target ) update.to(target, alias)
|
||||
return update
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new DELETE statement.
|
||||
* @param {QuerySource} [target]
|
||||
* @param {string} [alias]
|
||||
* @return Delete
|
||||
*/
|
||||
public delete(target?: QuerySource, alias?: string): Delete<T> {
|
||||
const del = new Delete<T>()
|
||||
if ( target ) del.from(target, alias)
|
||||
return del
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new INSERT statement.
|
||||
* @param {QuerySource} [target]
|
||||
* @param {string} [alias]
|
||||
* @return Insert
|
||||
*/
|
||||
public insert(target?: QuerySource, alias?: string): Insert<T> {
|
||||
const insert = new Insert<T>()
|
||||
if ( target ) insert.into(target, alias)
|
||||
return insert
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new raw SQL statement.
|
||||
* @param {string} statement
|
||||
* @param {...EscapedValue} interpolations
|
||||
* @return Statement
|
||||
*/
|
||||
public statement(statement: string, ...interpolations: EscapedValue[]): Statement<T> {
|
||||
return new Statement<T>(statement, interpolations)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new TRUNCATE statement.
|
||||
* @param {QuerySource} [target]
|
||||
* @param {string} [alias]
|
||||
* @return Truncate
|
||||
*/
|
||||
public truncate(target?: QuerySource, alias?: string): Truncate<T> {
|
||||
return new Truncate<T>(target, alias)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a string so it gets included in the query unescaped.
|
||||
* @param {string} value
|
||||
* @return RawValue
|
||||
*/
|
||||
public static raw(value: string) {
|
||||
return new RawValue(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the 'DEFAULT' operator, raw.
|
||||
* @return RawValue
|
||||
*/
|
||||
public static default() {
|
||||
return this.raw('DEFAULT')
|
||||
}
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
/**
|
||||
* Query builder helper that represents a string that should be directly interpolated
|
||||
* into the SQL of a given query, without being escaped.
|
||||
*/
|
||||
export default class RawValue {
|
||||
constructor(
|
||||
/**
|
||||
* The value to be interpolated.
|
||||
* @type string
|
||||
* @readonly
|
||||
*/
|
||||
public readonly value: string
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import {WhereBuilder} from './type/WhereBuilder.ts'
|
||||
|
||||
/**
|
||||
* Abstract base class for query builder scopes.
|
||||
* @abstract
|
||||
*/
|
||||
export abstract class Scope {
|
||||
/**
|
||||
* Applies this scope to the incoming query.
|
||||
* @param {WhereBuilder} query
|
||||
* @return WhereBuilder
|
||||
* @abstract
|
||||
*/
|
||||
abstract apply(query: WhereBuilder): WhereBuilder
|
||||
}
|
||||
|
||||
@@ -2,9 +2,21 @@ import {EscapedValue, escape} from './types.ts'
|
||||
import {IncorrectInterpolationError} from './Builder.ts'
|
||||
import ConnectionExecutable from './type/ConnectionExecutable.ts'
|
||||
|
||||
/**
|
||||
* Query builder base class for a raw SQL statement.
|
||||
* @extends ConnectionExecutable
|
||||
*/
|
||||
export class Statement<T> extends ConnectionExecutable<T> {
|
||||
constructor(
|
||||
/**
|
||||
* The statement to be executed.
|
||||
* @type string
|
||||
*/
|
||||
public statement: string,
|
||||
/**
|
||||
* The variables to be interpolated into the statement.
|
||||
* @type Array<EscapedValue>
|
||||
*/
|
||||
public interpolations: EscapedValue[]
|
||||
) {
|
||||
super()
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
import {Scope} from '../Scope.ts'
|
||||
import {WhereBuilder} from '../type/WhereBuilder.ts'
|
||||
|
||||
/**
|
||||
* Base type of functions which provide a query scope.
|
||||
*/
|
||||
export type ScopeFunction = (query: WhereBuilder) => WhereBuilder
|
||||
|
||||
/**
|
||||
* Query scope class which builds its clauses by calling an external function.
|
||||
* @extends Scope
|
||||
*/
|
||||
export class FunctionScope extends Scope {
|
||||
constructor(protected _fn: ScopeFunction) {
|
||||
constructor(
|
||||
/**
|
||||
* The scope function used to scope the query.
|
||||
* @type ScopeFunction
|
||||
*/
|
||||
protected _fn: ScopeFunction
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
|
||||
@@ -9,13 +9,31 @@ import ResultOperator from './result/ResultOperator.ts'
|
||||
import {collect, Collection} from '../../../../lib/src/collection/Collection.ts'
|
||||
import NoTargetOperatorError from '../../error/NoTargetOperatorError.ts'
|
||||
|
||||
/**
|
||||
* Base class for a query that can be executed in a database connection.
|
||||
* @abstract
|
||||
*/
|
||||
export default abstract class ConnectionExecutable<T> {
|
||||
/**
|
||||
* Render the query to raw SQL, starting with the base indentation level.
|
||||
* @param {number} level
|
||||
* @return string
|
||||
*/
|
||||
abstract sql(level: number): string
|
||||
|
||||
/**
|
||||
* Cast the query to an SQL statement which counts the incoming rows.
|
||||
* @return string
|
||||
*/
|
||||
to_count(): string {
|
||||
return `SELECT COUNT(*) AS to_count FROM (${this.sql(0)}) AS target_query`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the result row for this query at index i.
|
||||
* @param {number} i
|
||||
* @return Promise<any>
|
||||
*/
|
||||
async get_row(i: number): Promise<T | undefined> {
|
||||
if ( !(this.__target_connection instanceof Connection) ) {
|
||||
throw new Error('Unable to execute database item: no target connection.')
|
||||
@@ -34,6 +52,12 @@ export default abstract class ConnectionExecutable<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a range of resultant rows for this query between the start and end indices.
|
||||
* @param {string} start
|
||||
* @param {string} end
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async get_range(start: number, end: number): Promise<Collection<T>> {
|
||||
if ( !(this.__target_connection instanceof Connection) ) {
|
||||
throw new Error('Unable to execute database item: no target connection.')
|
||||
@@ -52,27 +76,59 @@ export default abstract class ConnectionExecutable<T> {
|
||||
return inflated
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an iterator for this result set.
|
||||
* @return ResultIterable
|
||||
*/
|
||||
iterator(): ResultIterable<T> {
|
||||
return new ResultIterable<T>(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the results as an async collection, with the processing chunk size.
|
||||
* @param {number} chunk_size
|
||||
* @return ResultCollection
|
||||
*/
|
||||
results(chunk_size = 1000) {
|
||||
return new ResultCollection<T>(this.iterator(), chunk_size)
|
||||
}
|
||||
|
||||
/**
|
||||
* The database connection to execute the statement in.
|
||||
* @type Connection
|
||||
*/
|
||||
__target_connection?: Connection
|
||||
|
||||
/**
|
||||
* The result operator to use to process the incoming rows.
|
||||
* @type ResultOperator
|
||||
*/
|
||||
__target_operator?: ResultOperator<T>
|
||||
|
||||
/**
|
||||
* Set the target connection.
|
||||
* @param {string|Connection} connection - the connection or connection name
|
||||
* @return ConnectionExecutable
|
||||
*/
|
||||
target_connection(connection: string | Connection) {
|
||||
this.__target_connection = typeof connection === 'string' ? make(Database).connection(connection) : connection
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the target operator.
|
||||
* @param {ResultOperator} operator
|
||||
* @return ConnectionExecutable
|
||||
*/
|
||||
target_operator(operator: ResultOperator<T>) {
|
||||
this.__target_operator = operator
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query and get back the raw result.
|
||||
* @return Promise<QueryResult>
|
||||
*/
|
||||
async execute(): Promise<QueryResult> {
|
||||
if ( !(this.__target_connection instanceof Connection) ) {
|
||||
throw new Error('Unable to execute database item: no target connection.')
|
||||
@@ -81,6 +137,10 @@ export default abstract class ConnectionExecutable<T> {
|
||||
return this.execute_in_connection(this.__target_connection)
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of returned rows.
|
||||
* @return Promise<number>
|
||||
*/
|
||||
async count(): Promise<number> {
|
||||
if ( !(this.__target_connection instanceof Connection) ) {
|
||||
throw new Error('Unable to execute database item: no target connection.')
|
||||
@@ -92,10 +152,19 @@ export default abstract class ConnectionExecutable<T> {
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the number of rows returned is greater than 0.
|
||||
* @return Promise<boolean>
|
||||
*/
|
||||
async exists(): Promise<boolean> {
|
||||
return (await this.count()) > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query in the given connection and return the raw result.
|
||||
* @param {string|Connection} connection - the connection or connection name
|
||||
* @return Promise<QueryResult>
|
||||
*/
|
||||
async execute_in_connection(connection: string | Connection): Promise<QueryResult> {
|
||||
const conn = typeof connection === 'string' ? make(Database).connection(connection) : connection
|
||||
|
||||
|
||||
@@ -4,7 +4,17 @@ import {Connection} from '../../db/Connection.ts'
|
||||
import {Collection} from '../../../../lib/src/collection/Collection.ts'
|
||||
import NoTargetOperatorError from '../../error/NoTargetOperatorError.ts'
|
||||
|
||||
/**
|
||||
* Variant of the ConnectionExecutable used to build queries that mutate data. This
|
||||
* structure overrides methods to ensure that the query is run only once.
|
||||
* @extends ConnectionExecutable
|
||||
* @abstract
|
||||
*/
|
||||
export default abstract class ConnectionMutable<T> extends ConnectionExecutable<T> {
|
||||
/**
|
||||
* The cached execution result.
|
||||
* @type QueryResult
|
||||
*/
|
||||
__execution_result?: QueryResult
|
||||
|
||||
async get_row(i: number): Promise<T | undefined> {
|
||||
@@ -28,6 +38,11 @@ export default abstract class ConnectionMutable<T> extends ConnectionExecutable<
|
||||
return result.row_count
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query result. Executes the query if it hasn't already. If it has,
|
||||
* return the cached query result.
|
||||
* @return Promise<QueryResult>
|
||||
*/
|
||||
async get_execution_result(): Promise<QueryResult> {
|
||||
if ( this.__execution_result ) return this.__execution_result
|
||||
else return this.execute()
|
||||
|
||||
@@ -6,11 +6,41 @@ import {TableRefBuilder} from './TableRefBuilder.ts'
|
||||
import {MalformedSQLGrammarError} from './Select.ts'
|
||||
import {Scope} from '../Scope.ts'
|
||||
|
||||
/**
|
||||
* Base query builder for DELETE queries.
|
||||
* @extends ConnectionMutable
|
||||
* @extends WhereBuilder
|
||||
* @extends TableRefBuilder
|
||||
*/
|
||||
export class Delete<T> extends ConnectionMutable<T> {
|
||||
/**
|
||||
* The target table.
|
||||
* @type QuerySource
|
||||
*/
|
||||
protected _target?: QuerySource = undefined
|
||||
|
||||
/**
|
||||
* The where clauses.
|
||||
* @type Array<WhereStatement>
|
||||
*/
|
||||
protected _wheres: WhereStatement[] = []
|
||||
|
||||
/**
|
||||
* The applied scopes.
|
||||
* @type Array<Scope>
|
||||
*/
|
||||
protected _scopes: Scope[] = []
|
||||
|
||||
/**
|
||||
* The fields to select.
|
||||
* @type Array<string>
|
||||
*/
|
||||
protected _fields: string[] = []
|
||||
|
||||
/**
|
||||
* Include the ONLY operator?
|
||||
* @type boolean
|
||||
*/
|
||||
protected _only: boolean = false
|
||||
|
||||
sql(level = 0): string {
|
||||
@@ -29,17 +59,34 @@ export class Delete<T> extends ConnectionMutable<T> {
|
||||
].filter(x => String(x).trim()).join(`\n${indent}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Include the only operator.
|
||||
* @example
|
||||
* SELECT ONLY ...
|
||||
* @return Delete
|
||||
*/
|
||||
only() {
|
||||
this._only = true
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the source to delete from.
|
||||
* @param {QuerySource} source
|
||||
* @param {string} alias
|
||||
* @return Delete
|
||||
*/
|
||||
from(source: QuerySource, alias?: string) {
|
||||
if ( !alias ) this._target = source
|
||||
else this._target = { ref: source, alias }
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the fields to be returned from the query.
|
||||
* @param {...FieldSet} fields
|
||||
* @return Delete
|
||||
*/
|
||||
returning(...fields: FieldSet[]) {
|
||||
for ( const field_set of fields ) {
|
||||
if ( typeof field_set === 'string' ) {
|
||||
|
||||
@@ -9,13 +9,30 @@ import {
|
||||
} from '../types.ts'
|
||||
import {HavingBuilderFunction} from './Select.ts'
|
||||
|
||||
/**
|
||||
* Mixin class for queries supporting HAVING clauses.
|
||||
*/
|
||||
export class HavingBuilder {
|
||||
/**
|
||||
* Having clauses to apply to the query.
|
||||
* @type Array<HavingStatement>
|
||||
*/
|
||||
protected _havings: HavingStatement[] = []
|
||||
|
||||
/**
|
||||
* Get the having clauses applied to the query.
|
||||
* @type Array<HavingStatement>
|
||||
*/
|
||||
get having_items() {
|
||||
return this._havings
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast the having statements to SQL.
|
||||
* @param {HavingStatement} [havings]
|
||||
* @param {number} [level = 0] - the indentation level
|
||||
* @return string
|
||||
*/
|
||||
havings_to_sql(havings?: HavingStatement[], level = 0): string {
|
||||
const indent = Array(level * 2).fill(' ').join('')
|
||||
let statements = []
|
||||
@@ -30,6 +47,14 @@ export class HavingBuilder {
|
||||
return statements.filter(Boolean).join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper for creating a HAVING clause.
|
||||
* @param {HavingPreOperator} preop
|
||||
* @param {string | HavingBuilderFunction} field
|
||||
* @param {SQLHavingOperator} [operator]
|
||||
* @param [operand]
|
||||
* @private
|
||||
*/
|
||||
private _createHaving(preop: HavingPreOperator, field: string | HavingBuilderFunction, operator?: SQLHavingOperator, operand?: any) {
|
||||
if ( typeof field === 'function' ) {
|
||||
const having_builder = new HavingBuilder()
|
||||
@@ -45,11 +70,24 @@ export class HavingBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a basic HAVING clause to the query.
|
||||
* @param {string | HavingBuilderFunction} field
|
||||
* @param {SQLHavingOperator} [operator]
|
||||
* @param [operand]
|
||||
* @return HavingBuilder
|
||||
*/
|
||||
having(field: string | HavingBuilderFunction, operator?: SQLHavingOperator, operand?: any) {
|
||||
this._createHaving('AND', field, operator, operand)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a HAVING ... IN (...) clause to the query.
|
||||
* @param {string} field
|
||||
* @param {EscapedValue} values
|
||||
* @return HavingBuilder
|
||||
*/
|
||||
havingIn(field: string, values: EscapedValue) {
|
||||
this._havings.push({
|
||||
field,
|
||||
@@ -60,11 +98,24 @@ export class HavingBuilder {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an HAVING NOT ... clause to the query.
|
||||
* @param {string | HavingBuilderFunction} field
|
||||
* @param {SQLHavingOperator} operator
|
||||
* @param [operand]
|
||||
* @return HavingBuilder
|
||||
*/
|
||||
havingNot(field: string | HavingBuilderFunction, operator?: SQLHavingOperator, operand?: EscapedValue) {
|
||||
this._createHaving('AND NOT', field, operator, operand)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an HAVING NOT ... IN (...) clause to the query.
|
||||
* @param {string} field
|
||||
* @param {EscapedValue} values
|
||||
* @return HavingBuilder
|
||||
*/
|
||||
havingNotIn(field: string, values: EscapedValue) {
|
||||
this._havings.push({
|
||||
field,
|
||||
@@ -75,16 +126,36 @@ export class HavingBuilder {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an OR HAVING ... clause to the query.
|
||||
* @param {string | HavingBuilderFunction} field
|
||||
* @param {SQLHavingOperator} [operator]
|
||||
* @param [operand]
|
||||
* @return HavingBuilder
|
||||
*/
|
||||
orHaving(field: string | HavingBuilderFunction, operator?: SQLHavingOperator, operand?: EscapedValue) {
|
||||
this._createHaving('OR', field, operator, operand)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an HAVING OR NOT ... clause to the query.
|
||||
* @param {string | HavingBuilderFunction} field
|
||||
* @param {SQLHavingOperator} [operator]
|
||||
* @param [operand]
|
||||
* @return HavingBuilder
|
||||
*/
|
||||
orHavingNot(field: string | HavingBuilderFunction, operator?: SQLHavingOperator, operand?: EscapedValue) {
|
||||
this._createHaving('OR NOT', field, operator, operand)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an OR HAVING ... IN (...) clause to the query.
|
||||
* @param {string} field
|
||||
* @param {EscapedValue} values
|
||||
* @return HavingBuilder
|
||||
*/
|
||||
orHavingIn(field: string, values: EscapedValue) {
|
||||
this._havings.push({
|
||||
field,
|
||||
@@ -95,6 +166,12 @@ export class HavingBuilder {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an OR HAVING NOT ... IN (...) clause to the query.
|
||||
* @param {string} field
|
||||
* @param {EscapedValue} values
|
||||
* @return HavingBuilder
|
||||
*/
|
||||
orHavingNotIn(field: string, values: EscapedValue) {
|
||||
this._havings.push({
|
||||
field,
|
||||
|
||||
@@ -8,11 +8,41 @@ import {raw} from '../Builder.ts'
|
||||
|
||||
// TODO support DEFAULT VALUES
|
||||
// TODO support ON CONFLICT
|
||||
|
||||
/**
|
||||
* Query builder base for INSERT queries.
|
||||
* @extends ConnectionMutable
|
||||
* @extends TableRefBuilder
|
||||
*/
|
||||
export class Insert<T> extends ConnectionMutable<T> {
|
||||
/**
|
||||
* The target table to insert into.
|
||||
* @type QuerySource
|
||||
*/
|
||||
protected _target?: QuerySource = undefined
|
||||
|
||||
/**
|
||||
* The columns to insert.
|
||||
* @type Array<string>
|
||||
*/
|
||||
protected _columns: string[] = []
|
||||
|
||||
/**
|
||||
* The row data to insert.
|
||||
* @type Array<string>
|
||||
*/
|
||||
protected _rows: string[] = []
|
||||
|
||||
/**
|
||||
* The fields to insert.
|
||||
* @type Array<string>
|
||||
*/
|
||||
protected _fields: string[] = []
|
||||
|
||||
/**
|
||||
* Return all data?
|
||||
* @type boolean
|
||||
*/
|
||||
protected _return_all = false
|
||||
|
||||
sql(level = 0): string {
|
||||
@@ -36,17 +66,33 @@ export class Insert<T> extends ConnectionMutable<T> {
|
||||
].filter(x => String(x).trim()).join(`\n${indent}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the table to insert into.
|
||||
* @param {QuerySource} source
|
||||
* @param {string} [alias]
|
||||
* @return Insert
|
||||
*/
|
||||
into(source: QuerySource, alias?: string) {
|
||||
if ( !alias ) this._target = source
|
||||
else this._target = { ref: source, alias }
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the columns to insert.
|
||||
* @param {...string} columns
|
||||
* @return Insert
|
||||
*/
|
||||
columns(...columns: string[]) {
|
||||
this._columns = columns
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add raw row data to insert.
|
||||
* @param {...EscapedValue} row
|
||||
* @return Insert
|
||||
*/
|
||||
row_raw(...row: EscapedValue[]) {
|
||||
if ( row.length !== this._columns.length )
|
||||
throw new MalformedSQLGrammarError(`Cannot insert row with ${row.length} values using a query that has ${this._columns.length} columns specified.`)
|
||||
@@ -55,6 +101,11 @@ export class Insert<T> extends ConnectionMutable<T> {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a field value object to insert.
|
||||
* @param {FieldValueObject} row
|
||||
* @return Insert
|
||||
*/
|
||||
row(row: FieldValueObject) {
|
||||
const columns = []
|
||||
const row_raw = []
|
||||
@@ -70,6 +121,11 @@ export class Insert<T> extends ConnectionMutable<T> {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multiple field value objects to insert.
|
||||
* @param {Array<FieldValueObject>}rows
|
||||
* @return Insert
|
||||
*/
|
||||
rows(rows: FieldValueObject[]) {
|
||||
const [initial, ...rest] = rows
|
||||
|
||||
@@ -96,6 +152,11 @@ export class Insert<T> extends ConnectionMutable<T> {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the fields to return after insert.
|
||||
* @param {...FieldSet} fields
|
||||
* @return Insert
|
||||
*/
|
||||
returning(...fields: FieldSet[]) {
|
||||
for ( const field_set of fields ) {
|
||||
if ( typeof field_set === 'string' ) {
|
||||
|
||||
@@ -23,24 +23,105 @@ import ConnectionExecutable from './ConnectionExecutable.ts'
|
||||
import {Scope} from '../Scope.ts'
|
||||
import {isInstantiable} from "../../../../di/src/type/Instantiable.ts";
|
||||
|
||||
/**
|
||||
* Base type for functions that operate on WhereBuilders.
|
||||
*/
|
||||
export type WhereBuilderFunction = (group: WhereBuilder) => any
|
||||
|
||||
/**
|
||||
* Base type for functions that operate on HavingBuilders.
|
||||
*/
|
||||
export type HavingBuilderFunction = (group: HavingBuilder) => any
|
||||
|
||||
/**
|
||||
* Base type for functions that operate on Joins.
|
||||
*/
|
||||
export type JoinFunction = (join: Join) => any
|
||||
|
||||
/**
|
||||
* Error class thrown when the SQL generated will be invalid.
|
||||
* @extends Error
|
||||
*/
|
||||
export class MalformedSQLGrammarError extends Error {}
|
||||
|
||||
/**
|
||||
* Query builder base class for SELECT queries.
|
||||
* @extends ConnectionExecutable
|
||||
* @extends TableRefBuilder
|
||||
* @extends WhereBuilder
|
||||
* @extends HavingBuilder
|
||||
*/
|
||||
export class Select<T> extends ConnectionExecutable<T> {
|
||||
/**
|
||||
* The fields to select.
|
||||
* @type Array<string>
|
||||
*/
|
||||
protected _fields: string[] = []
|
||||
|
||||
/**
|
||||
* The source to select from.
|
||||
* @type QuerySource
|
||||
*/
|
||||
protected _source?: QuerySource = undefined
|
||||
|
||||
/**
|
||||
* Where clauses to apply.
|
||||
* @type Array<WhereStatement>
|
||||
*/
|
||||
protected _wheres: WhereStatement[] = []
|
||||
|
||||
/**
|
||||
* The scopes to apply.
|
||||
* @type Array<Scope>
|
||||
*/
|
||||
protected _scopes: Scope[] = []
|
||||
|
||||
/**
|
||||
* Having clauses to apply.
|
||||
* @type Array<HavingStatement>
|
||||
*/
|
||||
protected _havings: HavingStatement[] = []
|
||||
|
||||
/**
|
||||
* Max number of rows to return.
|
||||
* @type number
|
||||
*/
|
||||
protected _limit?: number
|
||||
|
||||
/**
|
||||
* Number of rows to skip.
|
||||
* @type number
|
||||
*/
|
||||
protected _offset?: number
|
||||
|
||||
/**
|
||||
* Join clauses to apply.
|
||||
* @type Array<Join>
|
||||
*/
|
||||
protected _joins: Join[] = []
|
||||
|
||||
/**
|
||||
* Include the DISTINCT operator?
|
||||
* @type boolean
|
||||
*/
|
||||
protected _distinct = false
|
||||
|
||||
/**
|
||||
* Group by clauses to apply.
|
||||
* @type Array<string>
|
||||
*/
|
||||
protected _group_by: string[] = []
|
||||
|
||||
/**
|
||||
* Order by clauses to apply.
|
||||
* @type Array<OrderStatement>
|
||||
*/
|
||||
protected _order: OrderStatement[] = []
|
||||
|
||||
/**
|
||||
* Include the DISTINCT operator.
|
||||
* @return self
|
||||
*/
|
||||
distinct() {
|
||||
this._distinct = true
|
||||
return this
|
||||
@@ -73,6 +154,12 @@ export class Select<T> extends ConnectionExecutable<T> {
|
||||
].filter(x => String(x).trim()).join(`\n${indent}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Include a field in the results.
|
||||
* @param {string | Select} field
|
||||
* @param {string} [as] - alias
|
||||
* @return self
|
||||
*/
|
||||
field(field: string | Select<any>, as?: string) {
|
||||
if ( field instanceof Select ) {
|
||||
this._fields.push(`${escape(field)}${as ? ' AS '+as : ''}`)
|
||||
@@ -83,11 +170,19 @@ export class Select<T> extends ConnectionExecutable<T> {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the selected fields.
|
||||
* @return self
|
||||
*/
|
||||
clear_fields() {
|
||||
this._fields = []
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a copy of this query.
|
||||
* @return Select
|
||||
*/
|
||||
clone(): Select<T> {
|
||||
const constructor = this.constructor as typeof Select
|
||||
if ( !isInstantiable<Select<T>>(constructor) ) {
|
||||
@@ -114,11 +209,21 @@ export class Select<T> extends ConnectionExecutable<T> {
|
||||
return select
|
||||
}
|
||||
|
||||
/**
|
||||
* Add group by clauses to the query.
|
||||
* @param {...string} groupings
|
||||
* @return self
|
||||
*/
|
||||
group_by(...groupings: string[]) {
|
||||
this._group_by = groupings
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Include the given fields in the result set.
|
||||
* @param {...FieldSet} fields
|
||||
* @return self
|
||||
*/
|
||||
fields(...fields: FieldSet[]) {
|
||||
for ( const field_set of fields ) {
|
||||
if ( typeof field_set === 'string' ) {
|
||||
@@ -134,72 +239,162 @@ export class Select<T> extends ConnectionExecutable<T> {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the source to select from.
|
||||
* @param {QuerySource} source
|
||||
* @param {string} [alias]
|
||||
* @return self
|
||||
*/
|
||||
from(source: QuerySource, alias?: string) {
|
||||
if ( !alias ) this._source = source
|
||||
else this._source = { ref: source, alias }
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit the returned rows.
|
||||
* @param {number} num
|
||||
* @return self
|
||||
*/
|
||||
limit(num: number) {
|
||||
this._limit = Number(num)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip the first num rows.
|
||||
* @param {number} num
|
||||
* @return self
|
||||
*/
|
||||
offset(num: number) {
|
||||
this._offset = Number(num)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip the first num rows.
|
||||
* @param {number} num
|
||||
* @return self
|
||||
*/
|
||||
skip(num: number) {
|
||||
this._offset = Number(num)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Return only the first num rows.
|
||||
* @param {number} num
|
||||
* @return self
|
||||
*/
|
||||
take(num: number) {
|
||||
this._limit = Number(num)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a JOIN clause to the query by alias, or using a function to build the clause.
|
||||
* @param {QuerySource} source
|
||||
* @param {string | JoinFunction} alias_or_func
|
||||
* @param {JoinFunction} [func]
|
||||
* @return self
|
||||
*/
|
||||
join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) {
|
||||
this._createJoin(Join, source, alias_or_func, func)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a LEFT JOIN clause to the query by alias, or using a function to build the clause.
|
||||
* @param {QuerySource} source
|
||||
* @param {string | JoinFunction} alias_or_func
|
||||
* @param {JoinFunction} [func]
|
||||
* @return self
|
||||
*/
|
||||
left_join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) {
|
||||
this._createJoin(LeftJoin, source, alias_or_func, func)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a LEFT OUTER JOIN clause to the query by alias, or using a function to build the clause.
|
||||
* @param {QuerySource} source
|
||||
* @param {string | JoinFunction} alias_or_func
|
||||
* @param {JoinFunction} [func]
|
||||
* @return self
|
||||
*/
|
||||
left_outer_join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) {
|
||||
this._createJoin(LeftOuterJoin, source, alias_or_func, func)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a CROSS JOIN clause to the query by alias, or using a function to build the clause.
|
||||
* @param {QuerySource} source
|
||||
* @param {string | JoinFunction} alias_or_func
|
||||
* @param {JoinFunction} [func]
|
||||
* @return self
|
||||
*/
|
||||
cross_join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) {
|
||||
this._createJoin(CrossJoin, source, alias_or_func, func)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an INNER JOIN clause to the query by alias, or using a function to build the clause.
|
||||
* @param {QuerySource} source
|
||||
* @param {string | JoinFunction} alias_or_func
|
||||
* @param {JoinFunction} [func]
|
||||
* @return self
|
||||
*/
|
||||
inner_join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) {
|
||||
this._createJoin(InnerJoin, source, alias_or_func, func)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a RIGHT JOIN clause to the query by alias, or using a function to build the clause.
|
||||
* @param {QuerySource} source
|
||||
* @param {string | JoinFunction} alias_or_func
|
||||
* @param {JoinFunction} [func]
|
||||
* @return self
|
||||
*/
|
||||
right_join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) {
|
||||
this._createJoin(RightJoin, source, alias_or_func, func)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a RIGHT OUTER JOIN clause to the query by alias, or using a function to build the clause.
|
||||
* @param {QuerySource} source
|
||||
* @param {string | JoinFunction} alias_or_func
|
||||
* @param {JoinFunction} [func]
|
||||
* @return self
|
||||
*/
|
||||
right_outer_join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) {
|
||||
this._createJoin(RightOuterJoin, source, alias_or_func, func)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a FULL OUTER JOIN clause to the query by alias, or using a function to build the clause.
|
||||
* @param {QuerySource} source
|
||||
* @param {string | JoinFunction} alias_or_func
|
||||
* @param {JoinFunction} [func]
|
||||
* @return self
|
||||
*/
|
||||
full_outer_join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) {
|
||||
this._createJoin(FullOuterJoin, source, alias_or_func, func)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal helper for creating join clauses using query builder classes.
|
||||
* @param {typeof Join} Class
|
||||
* @param {QuerySource} source
|
||||
* @param {string | JoinFunction} alias_or_func
|
||||
* @param {JoinFunction} [func]
|
||||
* @private
|
||||
*/
|
||||
private _createJoin(Class: typeof Join, source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) {
|
||||
const [table_ref, join_func] = this.join_ref_to_join_args(source, alias_or_func, func)
|
||||
const join = new Class(table_ref)
|
||||
@@ -207,6 +402,13 @@ export class Select<T> extends ConnectionExecutable<T> {
|
||||
join_func(join)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast a join reference to the arguments required for the JOIN query builder.
|
||||
* @param {QuerySource} source
|
||||
* @param {string | JoinFunction} alias_or_func
|
||||
* @param {JoinFunction} [func]
|
||||
* @return Array
|
||||
*/
|
||||
join_ref_to_join_args(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction): [TableRef, JoinFunction] {
|
||||
let alias = undefined
|
||||
if ( typeof alias_or_func === 'string' ) alias = alias_or_func
|
||||
@@ -221,21 +423,34 @@ export class Select<T> extends ConnectionExecutable<T> {
|
||||
return [this.source_alias_to_table_ref(source, alias), join_func]
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an order by clause to the query.
|
||||
* @param {string} field
|
||||
* @param {string} [direction = 'ASC']
|
||||
* @return self
|
||||
*/
|
||||
order_by(field: string, direction: OrderDirection = 'ASC') {
|
||||
this._order.push({ field, direction })
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an ORDER BY ... ASC clause to the query.
|
||||
* @param {string} field
|
||||
* @return self
|
||||
*/
|
||||
order_asc(field: string) {
|
||||
return this.order_by(field, 'ASC')
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an ORDER BY ... DESC clause to the query.
|
||||
* @param {string} field
|
||||
* @return self
|
||||
*/
|
||||
order_desc(field: string) {
|
||||
return this.order_by(field, 'DESC')
|
||||
}
|
||||
|
||||
// TODO subquery support - https://www.sqlservertutorial.net/sql-server-basics/sql-server-subquery/
|
||||
// TODO raw()
|
||||
}
|
||||
|
||||
export interface Select<T> extends WhereBuilder, TableRefBuilder, HavingBuilder {}
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import {TableRef, QuerySource} from '../types.ts'
|
||||
|
||||
/**
|
||||
* Query builder mixin for queries that resolve table names.
|
||||
*/
|
||||
export class TableRefBuilder {
|
||||
/**
|
||||
* Resolve the raw table name to a table reference.
|
||||
* @param {string} from
|
||||
* @return TableRef
|
||||
*/
|
||||
resolve_table_name(from: string): TableRef {
|
||||
const parts = from.split('.')
|
||||
const ref: any = {}
|
||||
@@ -20,10 +28,21 @@ export class TableRefBuilder {
|
||||
return ref as TableRef
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a table ref to its raw SQL form.
|
||||
* @param {TableRef} ref
|
||||
* @return string
|
||||
*/
|
||||
serialize_table_ref(ref: TableRef): string {
|
||||
return `${ref.database ? ref.database+'.' : ''}${ref.table}${ref.alias ? ' '+ref.alias : ''}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a query source and alias to a table ref.
|
||||
* @param {QuerySource} source
|
||||
* @param {string} [alias]
|
||||
* @return TableRef
|
||||
*/
|
||||
source_alias_to_table_ref(source: QuerySource, alias?: string) {
|
||||
let string = ''
|
||||
if ( typeof source === 'string' ) {
|
||||
|
||||
@@ -1,15 +1,49 @@
|
||||
import ConnectionMutable from "./ConnectionMutable.ts";
|
||||
import {MalformedSQLGrammarError} from "./Select.ts";
|
||||
import {TableRefBuilder} from "./TableRefBuilder.ts";
|
||||
import {applyMixins} from "../../../../lib/src/support/mixins.ts";
|
||||
import {QuerySource} from "../types.ts";
|
||||
import ConnectionMutable from './ConnectionMutable.ts'
|
||||
import {MalformedSQLGrammarError} from './Select.ts'
|
||||
import {TableRefBuilder} from './TableRefBuilder.ts'
|
||||
import {applyMixins} from '../../../../lib/src/support/mixins.ts'
|
||||
import {QuerySource} from '../types.ts'
|
||||
|
||||
/**
|
||||
* Base query builder class for TRUNCATE queries.
|
||||
* @extends ConnectionMutable
|
||||
* @extends TableRefBuilder
|
||||
*/
|
||||
export class Truncate<T> extends ConnectionMutable<T> {
|
||||
/**
|
||||
* The source to be truncated.
|
||||
* @type QuerySource
|
||||
*/
|
||||
protected _source?: QuerySource
|
||||
|
||||
/**
|
||||
* Include the ONLY clause?
|
||||
* @type boolean
|
||||
*/
|
||||
protected _only: boolean = false
|
||||
|
||||
/**
|
||||
* Include the RESTART clause?
|
||||
* @type boolean
|
||||
*/
|
||||
protected _restart: boolean = false
|
||||
|
||||
/**
|
||||
* Include the CONTINUE clause?
|
||||
* @type boolean
|
||||
*/
|
||||
protected _continue: boolean = false
|
||||
|
||||
/**
|
||||
* Include the CASCADE clause?
|
||||
* @type boolean
|
||||
*/
|
||||
protected _cascade: boolean = false
|
||||
|
||||
/**
|
||||
* Include the RESTRICT clause?
|
||||
* @type boolean
|
||||
*/
|
||||
protected _restrict: boolean = false
|
||||
|
||||
constructor(table?: QuerySource, alias?: string) {
|
||||
@@ -37,30 +71,52 @@ export class Truncate<T> extends ConnectionMutable<T> {
|
||||
].filter(x => String(x).trim()).join(`\n${indent}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the table to be truncated.
|
||||
* @param {QuerySource} source
|
||||
* @param {string} [alias]
|
||||
* @return self
|
||||
*/
|
||||
table(source: QuerySource, alias?: string) {
|
||||
if ( !alias ) this._source = source
|
||||
else this._source = { ref: source, alias }
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart the ID column. This adds the RESTART clause.
|
||||
* @return self
|
||||
*/
|
||||
restart_identity() {
|
||||
this._continue = false
|
||||
this._restart = true
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Continue the ID column. This adds the CONTINUE clause.
|
||||
* @return self
|
||||
*/
|
||||
continue_identity() {
|
||||
this._continue = true
|
||||
this._restart = false
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the CASCADE clause.
|
||||
* @return self
|
||||
*/
|
||||
cascade() {
|
||||
this._cascade = true
|
||||
this._restrict = false
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the RESTRICT clause.
|
||||
* @return self
|
||||
*/
|
||||
restrict() {
|
||||
this._cascade = false
|
||||
this._restrict = true
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import ConnectionExecutable from './ConnectionExecutable.ts'
|
||||
import {escape, EscapedValue, FieldValue, FieldValueObject, QuerySource, WhereStatement, FieldSet} from '../types.ts'
|
||||
import {Collection} from '../../../../lib/src/collection/Collection.ts'
|
||||
import {WhereBuilder} from './WhereBuilder.ts'
|
||||
@@ -8,14 +7,49 @@ import {MalformedSQLGrammarError} from './Select.ts'
|
||||
import ConnectionMutable from './ConnectionMutable.ts'
|
||||
import {Scope} from '../Scope.ts'
|
||||
|
||||
// TODO FROM
|
||||
// TODO WHERE CURRENT OF
|
||||
|
||||
/**
|
||||
* Query builder base class for UPDATE queries.
|
||||
* @extends ConnectionMutable
|
||||
* @extends TableRefBuilder
|
||||
* @extends WhereBuilder
|
||||
*/
|
||||
export class Update<T> extends ConnectionMutable<T> {
|
||||
/**
|
||||
* The target table to be updated.
|
||||
* @type QuerySource
|
||||
*/
|
||||
protected _target?: QuerySource = undefined
|
||||
|
||||
/**
|
||||
* Include the ONLY clause?
|
||||
* @type boolean
|
||||
*/
|
||||
protected _only = false
|
||||
|
||||
/**
|
||||
* Field value sets to be updated.
|
||||
* @type Collection<FieldValue>
|
||||
*/
|
||||
protected _sets: Collection<FieldValue> = new Collection<FieldValue>()
|
||||
|
||||
/**
|
||||
* Where clauses to be applied.
|
||||
* @type Array<WhereStatement>
|
||||
*/
|
||||
protected _wheres: WhereStatement[] = []
|
||||
|
||||
/**
|
||||
* Scopes to be applied.
|
||||
* @type Array<Scope>
|
||||
*/
|
||||
protected _scopes: Scope[] = []
|
||||
|
||||
/**
|
||||
* Fields to update.
|
||||
* @type Array<string>
|
||||
*/
|
||||
protected _fields: string[] = []
|
||||
|
||||
sql(level = 0): string {
|
||||
@@ -36,22 +70,44 @@ export class Update<T> extends ConnectionMutable<T> {
|
||||
].filter(x => String(x).trim()).join(`\n${indent}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to serialize field value sets to raw SQL.
|
||||
* @param {Collection<FieldValue>} sets
|
||||
* @param {number} level - the indentation level
|
||||
* @return string
|
||||
*/
|
||||
protected serialize_sets(sets: Collection<FieldValue>, level = 0): string {
|
||||
const indent = Array(level * 2).fill(' ').join('')
|
||||
return indent + sets.map(field_value => `${field_value.field} = ${escape(field_value.value)}`).join(`,\n${indent}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Target table to update records in.
|
||||
* @param {QuerySource} source
|
||||
* @param {string} [alias]
|
||||
* @return self
|
||||
*/
|
||||
to(source: QuerySource, alias?: string) {
|
||||
if ( !alias ) this._target = source
|
||||
else this._target = { ref: source, alias }
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the ONLY clause.
|
||||
* @return self
|
||||
*/
|
||||
only() {
|
||||
this._only = true
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a field and value to the update clause.
|
||||
* @param {string} field
|
||||
* @param {EscapedValue} value
|
||||
* @return self
|
||||
*/
|
||||
set(field: string, value: EscapedValue) {
|
||||
const existing = this._sets.firstWhere('field', '=', field)
|
||||
if ( existing ) {
|
||||
@@ -62,6 +118,11 @@ export class Update<T> extends ConnectionMutable<T> {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a set of fields and values to the update clause.
|
||||
* @param {FieldValueObject} values
|
||||
* @return self
|
||||
*/
|
||||
data(values: FieldValueObject) {
|
||||
for ( const field in values ) {
|
||||
if ( !values.hasOwnProperty(field) ) continue
|
||||
@@ -70,6 +131,11 @@ export class Update<T> extends ConnectionMutable<T> {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the fields to be returned after the update.
|
||||
* @param {...FieldSet} fields
|
||||
* @return self
|
||||
*/
|
||||
returning(...fields: FieldSet[]) {
|
||||
for ( const field_set of fields ) {
|
||||
if ( typeof field_set === 'string' ) {
|
||||
|
||||
@@ -4,22 +4,47 @@ 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 {make} from '../../../../di/src/global.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<WhereStatement>
|
||||
*/
|
||||
protected _wheres: WhereStatement[] = []
|
||||
|
||||
/**
|
||||
* The scopes to be applied.
|
||||
* @type Array<Scope>
|
||||
*/
|
||||
protected _scopes: Scope[] = []
|
||||
|
||||
/**
|
||||
* Get the where clauses applied to the query.
|
||||
* @type Array<WhereStatement>
|
||||
*/
|
||||
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)
|
||||
@@ -29,11 +54,21 @@ export class WhereBuilder {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multiple scopes to this query.
|
||||
* @param {Array<Scope | ScopeFunction>} 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<WhereStatement>} [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('')
|
||||
@@ -49,6 +84,14 @@ export class WhereBuilder {
|
||||
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()
|
||||
@@ -64,21 +107,48 @@ export class WhereBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
@@ -89,11 +159,24 @@ export class WhereBuilder {
|
||||
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,
|
||||
@@ -104,16 +187,36 @@ export class WhereBuilder {
|
||||
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,
|
||||
@@ -124,6 +227,12 @@ export class WhereBuilder {
|
||||
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,
|
||||
@@ -134,6 +243,13 @@ export class WhereBuilder {
|
||||
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,
|
||||
@@ -144,6 +260,13 @@ export class WhereBuilder {
|
||||
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,
|
||||
@@ -154,6 +277,13 @@ export class WhereBuilder {
|
||||
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,
|
||||
@@ -164,6 +294,13 @@ export class WhereBuilder {
|
||||
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,
|
||||
@@ -174,6 +311,11 @@ export class WhereBuilder {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a filter object to the query.
|
||||
* @param {QueryFilter} filter
|
||||
* @return self
|
||||
*/
|
||||
filter(filter: QueryFilter) {
|
||||
return apply_filter_to_where(filter, this)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import {Join} from './Join.ts'
|
||||
import {JoinOperator} from '../../types.ts'
|
||||
|
||||
/**
|
||||
* Query builder class which builds CROSS JOIN statements.
|
||||
* @extends Join
|
||||
*/
|
||||
export class CrossJoin extends Join {
|
||||
public readonly operator: JoinOperator = 'CROSS JOIN'
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import {Join} from './Join.ts'
|
||||
import {JoinOperator} from '../../types.ts'
|
||||
|
||||
/**
|
||||
* Query builder class which builds FULL OUTER JOINs.
|
||||
* @extends Join
|
||||
*/
|
||||
export class FullOuterJoin extends Join {
|
||||
public readonly operator: JoinOperator = 'FULL OUTER JOIN'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import {Join} from './Join.ts'
|
||||
import {JoinOperator} from '../../types.ts'
|
||||
|
||||
/**
|
||||
* Query builder class which builds INNER JOIN clauses.
|
||||
* @extends Join
|
||||
*/
|
||||
export class InnerJoin extends Join {
|
||||
public readonly operator: JoinOperator = 'INNER JOIN'
|
||||
}
|
||||
|
||||
@@ -4,15 +4,40 @@ import {applyMixins} from '../../../../../lib/src/support/mixins.ts'
|
||||
import {WhereBuilder} from '../WhereBuilder.ts'
|
||||
import {Scope} from '../../Scope.ts'
|
||||
|
||||
/**
|
||||
* Query builder class which builds JOIN clauses.
|
||||
*/
|
||||
export class Join {
|
||||
/**
|
||||
* The join operator to use in the SQL.
|
||||
* @type JoinOperator
|
||||
*/
|
||||
public readonly operator: JoinOperator = 'JOIN'
|
||||
|
||||
/**
|
||||
* The where statements applied to this join. (i.e. JOIN table ON ...)
|
||||
* @type Array<WhereStatement>
|
||||
*/
|
||||
protected _wheres: WhereStatement[] = []
|
||||
|
||||
/**
|
||||
* The scopes applied to this join.
|
||||
* @type Array<Scope>
|
||||
*/
|
||||
protected _scopes: Scope[] = []
|
||||
|
||||
constructor(
|
||||
/**
|
||||
* The table ref being joined.
|
||||
* @type TableRef
|
||||
*/
|
||||
public readonly table_ref: TableRef
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Serialize the join to raw SQL.
|
||||
* @param level
|
||||
*/
|
||||
sql(level = 0): string {
|
||||
const indent = Array(level * 2).fill(' ').join('')
|
||||
return [
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import {Join} from './Join.ts'
|
||||
import {JoinOperator} from '../../types.ts'
|
||||
|
||||
/**
|
||||
* Query builder class which creates LEFT JOIN clauses.
|
||||
* @extends Join
|
||||
*/
|
||||
export class LeftJoin extends Join {
|
||||
public readonly operator: JoinOperator = 'LEFT JOIN'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import {Join} from './Join.ts'
|
||||
import {JoinOperator} from '../../types.ts'
|
||||
|
||||
/**
|
||||
* Query builder class which creates LEFT OUTER JOIN clauses.
|
||||
* @extends Join
|
||||
*/
|
||||
export class LeftOuterJoin extends Join {
|
||||
public readonly operator: JoinOperator = 'LEFT OUTER JOIN'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import {Join} from './Join.ts'
|
||||
import {JoinOperator} from '../../types.ts'
|
||||
|
||||
/**
|
||||
* Query builder class which creates RIGHT JOIN clauses.
|
||||
* @extends Join
|
||||
*/
|
||||
export class RightJoin extends Join {
|
||||
public readonly operator: JoinOperator = 'RIGHT JOIN'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import {Join} from './Join.ts'
|
||||
import {JoinOperator} from '../../types.ts'
|
||||
|
||||
/**
|
||||
* Query builder class which creates RIGHT OUTER JOIN clauses.
|
||||
* @extends Join
|
||||
*/
|
||||
export class RightOuterJoin extends Join {
|
||||
public readonly operator: JoinOperator = 'RIGHT OUTER JOIN'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import ResultOperator from './ResultOperator.ts'
|
||||
import {QueryRow} from '../../../db/types.ts'
|
||||
|
||||
/**
|
||||
* Basic result operator which returns query results as object values.
|
||||
* @extends ResultOperator
|
||||
*/
|
||||
export default class ObjectResultOperator extends ResultOperator<QueryRow> {
|
||||
|
||||
inflate_row(row: QueryRow): QueryRow {
|
||||
|
||||
@@ -2,6 +2,10 @@ import {AsyncCollection} from '../../../../../lib/src/collection/AsyncCollection
|
||||
import {ResultIterable} from './ResultIterable.ts'
|
||||
import {Collection} from '../../../../../lib/src/collection/Collection.ts'
|
||||
|
||||
/**
|
||||
* Asynchronous collection representing the results of a query.
|
||||
* @extends AsyncCollection
|
||||
*/
|
||||
export class ResultCollection<T> extends AsyncCollection<T> {
|
||||
constructor(
|
||||
executable: ResultIterable<T>,
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import {Iterable} from '../../../../../lib/src/collection/Iterable.ts'
|
||||
import ConnectionExecutable from '../ConnectionExecutable.ts'
|
||||
import {Collection} from '../../../../../lib/src/collection/Collection.ts'
|
||||
import {QueryRow} from '../../../db/types.ts'
|
||||
|
||||
/**
|
||||
* An Iterable implementation which retrieves results from a database query.
|
||||
* @extends Iterable
|
||||
*/
|
||||
export class ResultIterable<T> extends Iterable<T> {
|
||||
|
||||
constructor(
|
||||
/**
|
||||
* The executable database query to base the iterable on.
|
||||
* @type ConnectionExecutable
|
||||
*/
|
||||
protected executable: ConnectionExecutable<T>
|
||||
) { super() }
|
||||
|
||||
|
||||
@@ -1,11 +1,35 @@
|
||||
import {QueryRow} from '../../../db/types.ts'
|
||||
import ConnectionExecutable from '../ConnectionExecutable.ts'
|
||||
import {Model} from '../../../model/Model.ts'
|
||||
import {Collection} from '../../../../../lib/src/collection/Collection.ts'
|
||||
|
||||
/**
|
||||
* Base class for query result operators which process query rows into other
|
||||
* formats after retrieval.
|
||||
* @abstract
|
||||
*/
|
||||
export default abstract class ResultOperator<T> {
|
||||
/**
|
||||
* Called in bulk before result rows are inflated. This can be used to bulk-preload
|
||||
* additional data that might be added into the dataset.
|
||||
*
|
||||
* For example, the ModelResultOperator uses this to eager load specified relations.
|
||||
*
|
||||
* @param {ConnectionExecutable} query
|
||||
* @param {Collection} results
|
||||
* @return Promise<void>
|
||||
*/
|
||||
public async process_eager_loads<T2>(query: ConnectionExecutable<T2>, results: Collection<T>): Promise<void> { }
|
||||
|
||||
/**
|
||||
* Convert a row from the raw query result to the target format.
|
||||
* @param {QueryRow} row
|
||||
*/
|
||||
abstract inflate_row(row: QueryRow): T
|
||||
|
||||
/**
|
||||
* Convert the target format back to a raw query result.
|
||||
* @param item
|
||||
* @return QueryRow
|
||||
*/
|
||||
abstract deflate_row(item: T): QueryRow
|
||||
}
|
||||
|
||||
@@ -1,22 +1,47 @@
|
||||
import { Iterable } from '../../../../../lib/src/collection/Iterable.ts'
|
||||
import ConnectionExecutable from "../ConnectionExecutable.ts";
|
||||
import {QueryRow} from "../../../db/types.ts";
|
||||
import {Collection} from "../../../../../lib/src/collection/Collection.ts";
|
||||
import ConnectionExecutable from '../ConnectionExecutable.ts'
|
||||
import {QueryRow} from '../../../db/types.ts'
|
||||
import {Collection} from '../../../../../lib/src/collection/Collection.ts'
|
||||
|
||||
/**
|
||||
* Abstract iterable that wraps an executable query.
|
||||
* @extends Iterable
|
||||
* @abstract
|
||||
*/
|
||||
export abstract class ResultSet<T> extends Iterable<any> {
|
||||
|
||||
protected constructor(
|
||||
/**
|
||||
* The executable query to wrap.
|
||||
* @type ConnectionExecutable
|
||||
*/
|
||||
protected executeable: ConnectionExecutable,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single incoming query row to the output format.
|
||||
* @param {QueryRow} row
|
||||
* @return Promise<any>
|
||||
*/
|
||||
abstract async process_row(row: QueryRow): Promise<T>
|
||||
|
||||
/**
|
||||
* Get the result at index i.
|
||||
* @param {number} i
|
||||
* @return Promise<any>
|
||||
*/
|
||||
async at_index(i: number) {
|
||||
return this.process_row(await this.executeable.get_row(i))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a collection of results for the given range of rows.
|
||||
* @param {number} start
|
||||
* @param {number} end
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async from_range(start: number, end: number) {
|
||||
const results = await this.executeable.get_range(start, end)
|
||||
const returns = new Collection<T>()
|
||||
@@ -27,6 +52,10 @@ export abstract class ResultSet<T> extends Iterable<any> {
|
||||
return returns
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of results.
|
||||
* @return Promise<number>
|
||||
*/
|
||||
async count() {
|
||||
return this.executeable.count()
|
||||
}
|
||||
|
||||
@@ -1,37 +1,111 @@
|
||||
import {WhereOperator} from '../../../lib/src/collection/Where.ts'
|
||||
import RawValue from './RawValue.ts'
|
||||
import {Select} from "./type/Select.ts";
|
||||
import {Select} from './type/Select.ts'
|
||||
|
||||
/**
|
||||
* Represents a field or set of fields.
|
||||
*/
|
||||
export type FieldSet = string | string[]
|
||||
|
||||
/**
|
||||
* Represents a table name, or table name and alias.
|
||||
*/
|
||||
export type QuerySource = string | { ref: QuerySource, alias: string }
|
||||
|
||||
/**
|
||||
* Valid JOIN clause operators.
|
||||
*/
|
||||
export type JoinOperator = 'JOIN' | 'LEFT JOIN' | 'LEFT OUTER JOIN' | 'RIGHT JOIN' | 'RIGHT OUTER JOIN' | 'FULL OUTER JOIN' | 'INNER JOIN' | 'CROSS JOIN'
|
||||
|
||||
/**
|
||||
* Valid operators which can join WHERE clauses.
|
||||
*/
|
||||
export type WherePreOperator = 'AND' | 'OR' | 'AND NOT' | 'OR NOT'
|
||||
|
||||
/**
|
||||
* Abstract representation of a single WHERE clause.
|
||||
*/
|
||||
export type WhereClause = { field: string, operator: SQLWhereOperator, operand: string, preop: WherePreOperator }
|
||||
|
||||
/**
|
||||
* Group of where clauses, and the operator which should join them.
|
||||
*/
|
||||
export type WhereGroup = { items: WhereStatement[], preop: WherePreOperator }
|
||||
|
||||
/**
|
||||
* A single WHERE statement.
|
||||
*/
|
||||
export type WhereStatement = WhereClause | WhereGroup
|
||||
|
||||
/**
|
||||
* Operators which can be used in SQL WHERE clauses.
|
||||
*/
|
||||
export type SQLWhereOperator = WhereOperator | 'IN' | 'NOT IN' | 'LIKE' | 'BETWEEN' | 'NOT BETWEEN' | 'IS' | 'IS NOT'
|
||||
|
||||
/**
|
||||
* Directions for ORDER BY clauses.
|
||||
*/
|
||||
export type OrderDirection = 'ASC' | 'DESC'
|
||||
|
||||
/**
|
||||
* Abstract representation of an ORDER BY clause.
|
||||
*/
|
||||
export type OrderStatement = { direction: OrderDirection, field: string }
|
||||
|
||||
/**
|
||||
* Valid operators which can join HAVING clauses.
|
||||
*/
|
||||
export type HavingPreOperator = WherePreOperator
|
||||
|
||||
/**
|
||||
* Abstract representation of a single HAVING clause.
|
||||
*/
|
||||
export type HavingClause = WhereClause
|
||||
|
||||
/**
|
||||
* Group of having clauses, and the operator which should join them.
|
||||
*/
|
||||
export type HavingGroup = WhereGroup
|
||||
|
||||
/**
|
||||
* A single HAVING statement.
|
||||
*/
|
||||
export type HavingStatement = HavingClause | HavingGroup
|
||||
|
||||
/**
|
||||
* Valid operators which can be used in SQL HAVING clauses.
|
||||
*/
|
||||
export type SQLHavingOperator = SQLWhereOperator
|
||||
|
||||
/**
|
||||
* A value which can be escaped to be interpolated into an SQL query.
|
||||
*/
|
||||
export type EscapedValue = string | number | boolean | Date | RawValue | EscapedValue[] | Select<any>
|
||||
|
||||
/**
|
||||
* Representation of a field and its value.
|
||||
*/
|
||||
export type FieldValue = { field: string, value: EscapedValue }
|
||||
|
||||
/**
|
||||
* Object representation of a number of fields and their values.
|
||||
*/
|
||||
export type FieldValueObject = { [field: string]: EscapedValue }
|
||||
|
||||
/**
|
||||
* Abstract reference to a particular database table, and its alias.
|
||||
*/
|
||||
export type TableRef = {
|
||||
table: string,
|
||||
database?: string,
|
||||
alias?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given object is a valid table ref.
|
||||
* @param something
|
||||
* @return boolean
|
||||
*/
|
||||
export function isTableRef(something: any): something is TableRef {
|
||||
let is = true
|
||||
is = is && typeof something?.table === 'string'
|
||||
@@ -47,14 +121,29 @@ export function isTableRef(something: any): something is TableRef {
|
||||
return is
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given item is a valid WHERE pre-operator.
|
||||
* @param something
|
||||
* @return boolean
|
||||
*/
|
||||
export function isWherePreOperator(something: any): something is WherePreOperator {
|
||||
return ['AND', 'OR', 'AND NOT', 'OR NOT'].includes(something)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given item is a valid HAVING clause.
|
||||
* @param something
|
||||
* @return boolean
|
||||
*/
|
||||
export function isHavingClause(something: any): something is HavingClause {
|
||||
return isWhereClause(something)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given item is a valid WHERE clause.
|
||||
* @param something
|
||||
* @return boolean
|
||||
*/
|
||||
export function isWhereClause(something: any): something is WhereClause {
|
||||
return typeof something?.field === 'string'
|
||||
&& typeof something?.operator === 'string' // TODO check this better
|
||||
@@ -62,20 +151,40 @@ export function isWhereClause(something: any): something is WhereClause {
|
||||
&& isWherePreOperator(something?.preop)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given item is a valid HAVING group.
|
||||
* @param something
|
||||
* @return boolean
|
||||
*/
|
||||
export function isHavingGroup(something: any): something is HavingGroup {
|
||||
return isWhereGroup(something)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given item is a valid WHERE group.
|
||||
* @param something
|
||||
* @return boolean
|
||||
*/
|
||||
export function isWhereGroup(something: any): something is WhereGroup {
|
||||
return Array.isArray(something?.items)
|
||||
&& something.items.every((item: any) => isWhereStatement(item))
|
||||
&& isWherePreOperator(something?.preop)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given value is a valid where statement.
|
||||
* @param something
|
||||
* @return boolean
|
||||
*/
|
||||
export function isWhereStatement(something: any): something is WhereStatement {
|
||||
return isWhereClause(something) || isWhereGroup(something)
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes the value so it can be inserted into an SQL query string.
|
||||
* @param {EscapedValue} value
|
||||
* @return string
|
||||
*/
|
||||
export function escape(value: EscapedValue): string {
|
||||
if ( value instanceof Select ) {
|
||||
return `(${value.sql(5)})`
|
||||
|
||||
@@ -1,20 +1,50 @@
|
||||
import {QueryResult} from './types.ts'
|
||||
|
||||
/**
|
||||
* Error thrown when a connection is used before it is ready.
|
||||
* @extends Error
|
||||
*/
|
||||
export class ConnectionNotReadyError extends Error {
|
||||
constructor(name = '') {
|
||||
super(`The connection ${name} is not ready and cannot execute queries.`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base class for database connections.
|
||||
* @abstract
|
||||
*/
|
||||
export abstract class Connection {
|
||||
|
||||
constructor(
|
||||
/**
|
||||
* The name of this connection
|
||||
* @type string
|
||||
*/
|
||||
public readonly name: string,
|
||||
/**
|
||||
* This connection's config object
|
||||
*/
|
||||
public readonly config: any = {},
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Open the connection.
|
||||
* @return Promise<void>
|
||||
*/
|
||||
public abstract async init(): Promise<void>
|
||||
|
||||
/**
|
||||
* Execute an SQL query and get the result.
|
||||
* @param {string} query
|
||||
* @return Promise<QueryResult>
|
||||
*/
|
||||
public abstract async query(query: string): Promise<QueryResult> // TODO query result
|
||||
|
||||
/**
|
||||
* Close the connection.
|
||||
* @return Promise<void>
|
||||
*/
|
||||
public abstract async close(): Promise<void>
|
||||
|
||||
}
|
||||
|
||||
@@ -4,7 +4,15 @@ import {collect, Collection} from '../../../lib/src/collection/Collection.ts'
|
||||
import { QueryResult, QueryRow } from './types.ts'
|
||||
import { logger } from '../../../lib/src/service/logging/global.ts'
|
||||
|
||||
/**
|
||||
* Database connection class for PostgreSQL connections.
|
||||
* @extends Connection
|
||||
*/
|
||||
export default class PostgresConnection extends Connection {
|
||||
/**
|
||||
* The underlying PostgreSQL client.
|
||||
* @type Client
|
||||
*/
|
||||
private _client?: Client
|
||||
|
||||
public async init() {
|
||||
|
||||
@@ -1,13 +1,26 @@
|
||||
import { Collection } from '../../../lib/src/collection/Collection.ts'
|
||||
|
||||
/**
|
||||
* A single query row, as an object.
|
||||
*/
|
||||
export type QueryRow = { [key: string]: any }
|
||||
|
||||
/**
|
||||
* A valid key on a model.
|
||||
*/
|
||||
export type ModelKey = string | number
|
||||
|
||||
/**
|
||||
* Interface for the result of a query execution.
|
||||
*/
|
||||
export interface QueryResult {
|
||||
rows: Collection<QueryRow>,
|
||||
row_count: number,
|
||||
}
|
||||
|
||||
/**
|
||||
* Database column types.
|
||||
*/
|
||||
export enum Type {
|
||||
bigint = 'bigint',
|
||||
int8 = 'bigint',
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* Error thrown when a query is executed but there is no result operator set.
|
||||
* @extends Error
|
||||
*/
|
||||
export default class NoTargetOperatorError extends Error {
|
||||
constructor(msg = 'This query has no defined target operator.') {
|
||||
super(msg)
|
||||
|
||||
@@ -2,10 +2,14 @@ import { Reflect } from '../../../lib/src/external/reflect.ts'
|
||||
import { Collection } from '../../../lib/src/collection/Collection.ts'
|
||||
import { logger } from '../../../lib/src/service/logging/global.ts'
|
||||
import {Type} from '../db/types.ts'
|
||||
import {Model} from "./Model.ts";
|
||||
|
||||
export const DATON_ORM_MODEL_FIELDS_METADATA_KEY = 'daton:orm:modelFields.ts'
|
||||
|
||||
/**
|
||||
* Get the model field metadata from a model class.
|
||||
* @param model
|
||||
* @return Collection<ModelField>
|
||||
*/
|
||||
export function get_fields_meta(model: any): Collection<ModelField> {
|
||||
const fields = Reflect.getMetadata(DATON_ORM_MODEL_FIELDS_METADATA_KEY, model.constructor)
|
||||
if ( !(fields instanceof Collection) ) {
|
||||
@@ -15,16 +19,31 @@ export function get_fields_meta(model: any): Collection<ModelField> {
|
||||
return fields as Collection<ModelField>
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the model field metadata for a model class.
|
||||
* @param model
|
||||
* @param {Collection<ModelField>} fields
|
||||
*/
|
||||
export function set_model_fields_meta(model: any, fields: Collection<ModelField>) {
|
||||
Reflect.defineMetadata(DATON_ORM_MODEL_FIELDS_METADATA_KEY, fields, model.constructor)
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract representation of a field on a model.
|
||||
*/
|
||||
export interface ModelField {
|
||||
database_key: string,
|
||||
model_key: string | symbol,
|
||||
type: any,
|
||||
}
|
||||
|
||||
/**
|
||||
* Property decorator for a field on a model. If no column name is provided, the database column name
|
||||
* will be the same as the property name on the model.
|
||||
* @param {Type} type - the column type
|
||||
* @param {string} [database_key] - the database column name
|
||||
* @constructor
|
||||
*/
|
||||
export function Field(type: Type, database_key?: string): PropertyDecorator {
|
||||
return (target, model_key) => {
|
||||
if ( !database_key ) database_key = String(model_key)
|
||||
|
||||
@@ -22,6 +22,9 @@ import {HasOneOrMany} from './relation/HasOneOrMany.ts'
|
||||
// TODO separate read/write connections
|
||||
// TODO manual dirty flags
|
||||
|
||||
/**
|
||||
* Dehydrated state of this model.
|
||||
*/
|
||||
export type ModelJSONState = {
|
||||
key_name: string,
|
||||
key?: string | number,
|
||||
@@ -29,6 +32,9 @@ export type ModelJSONState = {
|
||||
ephemeral_values?: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached relation value.
|
||||
*/
|
||||
export type CachedRelation = {
|
||||
accessor_name: string | symbol,
|
||||
relation: HasOne<any, any>, // TODO generalize
|
||||
@@ -151,6 +157,10 @@ abstract class Model<T extends Model<T>> extends Builder<T> implements Rehydrata
|
||||
*/
|
||||
protected deleted$ = new BehaviorSubject<Model<T>>()
|
||||
|
||||
/**
|
||||
* Cached relation values.
|
||||
* @type Collection<CachedRelation>
|
||||
*/
|
||||
public readonly relation_cache = new Collection<CachedRelation>()
|
||||
|
||||
/**
|
||||
@@ -1012,6 +1022,11 @@ abstract class Model<T extends Model<T>> extends Builder<T> implements Rehydrata
|
||||
return new HasMany(this as any, related_inst, relation.local_key, relation.foreign_key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relation instance for a given property name.
|
||||
* @param {string} name
|
||||
* @return Relation
|
||||
*/
|
||||
public get_relation<T2 extends Model<T2>>(name: string): Relation<T, T2> {
|
||||
// @ts-ignore
|
||||
const rel: any = this[name]()
|
||||
|
||||
@@ -8,9 +8,17 @@ import ConnectionExecutable from '../builder/type/ConnectionExecutable.ts'
|
||||
import {Collection} from '../../../lib/src/collection/Collection.ts'
|
||||
import {ModelSelect} from './query/ModelSelect.ts'
|
||||
|
||||
/**
|
||||
* Database result operator that instantiates models for each row.
|
||||
* @extends ResultOperator
|
||||
*/
|
||||
@Injectable()
|
||||
export default class ModelResultOperator<T extends Model<T>> extends ResultOperator<T> {
|
||||
constructor(
|
||||
/**
|
||||
* The model class to load rows into.
|
||||
* @type Instantiable<Model>
|
||||
*/
|
||||
protected ModelClass: Instantiable<T>,
|
||||
) {
|
||||
super()
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import ModelResultOperator from "./ModelResultOperator.ts";
|
||||
import {Model} from "./Model.ts";
|
||||
import Instantiable from "../../../di/src/type/Instantiable.ts";
|
||||
import {HasOne} from "./relation/HasOne.ts";
|
||||
import {QueryRow} from "../db/types.ts";
|
||||
|
||||
export default class RelationResultOperator<T extends Model<T>> extends ModelResultOperator<T> {
|
||||
constructor(
|
||||
protected ModelClass: Instantiable<T>,
|
||||
protected relation: HasOne<any, T>,
|
||||
) {
|
||||
super(ModelClass)
|
||||
}
|
||||
|
||||
inflate_row(row: QueryRow): T {
|
||||
const instance = super.inflate_row(row)
|
||||
return instance
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import {WhereBuilder} from '../builder/type/WhereBuilder.ts'
|
||||
import {logger} from '../../../lib/src/service/logging/global.ts'
|
||||
import {EscapedValue} from '../builder/types.ts'
|
||||
|
||||
/**
|
||||
* Operators for filter objects.
|
||||
*/
|
||||
export enum FilterOp {
|
||||
eq = '$eq',
|
||||
in = '$in',
|
||||
@@ -11,8 +13,17 @@ export enum FilterOp {
|
||||
gte = '$gte',
|
||||
}
|
||||
|
||||
/**
|
||||
* An object-like query filter.
|
||||
*/
|
||||
export type QueryFilter = { [key: string]: any }
|
||||
|
||||
/**
|
||||
* Given an object-like query filter, apply it to the database WHERE clause.
|
||||
* @param {QueryFilter} filter
|
||||
* @param {WhereBuilder} where
|
||||
* @return WhereBuilder
|
||||
*/
|
||||
export function apply_filter_to_where(filter: QueryFilter, where: WhereBuilder): WhereBuilder {
|
||||
for ( const field in filter ) {
|
||||
if ( !filter.hasOwnProperty(field) ) continue
|
||||
|
||||
@@ -1,17 +1,40 @@
|
||||
import {Select} from '../../builder/type/Select.ts'
|
||||
|
||||
/**
|
||||
* Query builder SELECT clause, with added features for working with models.
|
||||
* @extends Select
|
||||
*/
|
||||
export class ModelSelect<T> extends Select<T> {
|
||||
/**
|
||||
* List of relations to eager load.
|
||||
* @type Array<string>
|
||||
*/
|
||||
protected _withs: string[] = []
|
||||
|
||||
/**
|
||||
* Eager load a relation.
|
||||
* @example posts
|
||||
* @example posts.comments
|
||||
* @param {string} related
|
||||
* @return self
|
||||
*/
|
||||
public with(related: string) {
|
||||
this._withs.push(related)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relations to eager load.
|
||||
* @type Array<string>
|
||||
*/
|
||||
public get eager_relations(): string[] {
|
||||
return [...this._withs]
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a copy of this query.e
|
||||
* @return ModelSelect
|
||||
*/
|
||||
clone(): ModelSelect<T> {
|
||||
const select = super.clone() as ModelSelect<T>
|
||||
select._withs = this._withs
|
||||
|
||||
@@ -2,8 +2,21 @@ import {Model} from '../Model.ts'
|
||||
import {HasOneOrMany} from './HasOneOrMany.ts'
|
||||
import {Collection} from '../../../../lib/src/collection/Collection.ts'
|
||||
|
||||
/**
|
||||
* Relation class for one-to-many relations.
|
||||
* @extends HasOneOrMany
|
||||
*/
|
||||
export class HasMany<T extends Model<T>, T2 extends Model<T2>> extends HasOneOrMany<T, T2> {
|
||||
/**
|
||||
* The cached value of this relation.
|
||||
* @type Collection
|
||||
*/
|
||||
protected _value?: Collection<T2>
|
||||
|
||||
/**
|
||||
* True if this relation has been loaded.
|
||||
* @type boolean
|
||||
*/
|
||||
protected _loaded = false
|
||||
|
||||
public async get(): Promise<Collection<T2>> {
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
import {Model} from '../Model.ts'
|
||||
import {HasOneOrMany} from './HasOneOrMany.ts'
|
||||
import {Collection} from "../../../../lib/src/collection/Collection.ts";
|
||||
import {Logging} from "../../../../lib/src/service/logging/Logging.ts";
|
||||
import {Collection} from '../../../../lib/src/collection/Collection.ts'
|
||||
import {Logging} from '../../../../lib/src/service/logging/Logging.ts'
|
||||
|
||||
/**
|
||||
* Relation class for one-to-one relations.
|
||||
* @extends HasOneOrMany
|
||||
*/
|
||||
export class HasOne<T extends Model<T>, T2 extends Model<T2>> extends HasOneOrMany<T, T2> {
|
||||
/**
|
||||
* The cached value of this relation.
|
||||
* @type Model
|
||||
*/
|
||||
protected _value?: T2
|
||||
|
||||
/**
|
||||
* True if the relation has been loaded.
|
||||
* @type boolean
|
||||
*/
|
||||
protected _loaded = false
|
||||
|
||||
public async get(): Promise<T2 | undefined> {
|
||||
|
||||
@@ -5,26 +5,63 @@ import {WhereBuilder} from '../../builder/type/WhereBuilder.ts'
|
||||
import {ModelSelect} from '../query/ModelSelect.ts'
|
||||
import {Collection} from '../../../../lib/src/collection/Collection.ts'
|
||||
|
||||
/**
|
||||
* Abstract relation class for one-to-one and one-to-many relations.
|
||||
* @extends Relation
|
||||
* @abstract
|
||||
*/
|
||||
export abstract class HasOneOrMany<T extends Model<T>, T2 extends Model<T2>> extends Relation<T, T2> {
|
||||
constructor(
|
||||
/**
|
||||
* The parent model.
|
||||
* @type Model
|
||||
*/
|
||||
protected parent: T,
|
||||
/**
|
||||
* The model which is related.
|
||||
* @type Model
|
||||
*/
|
||||
public readonly related: T2,
|
||||
/**
|
||||
* The key on the related model.
|
||||
* @type string
|
||||
*/
|
||||
protected foreign_key_spec?: string,
|
||||
/**
|
||||
* The key on the parent model.
|
||||
* @type string
|
||||
*/
|
||||
protected local_key_spec?: string,
|
||||
) { super(parent, related) }
|
||||
|
||||
/**
|
||||
* Get the key to match on the related model.
|
||||
* @type string
|
||||
*/
|
||||
public get foreign_key() {
|
||||
return this.foreign_key_spec || this.parent.key_name()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key to match on the parent model.
|
||||
* @type string
|
||||
*/
|
||||
public get local_key() {
|
||||
return this.local_key_spec || this.foreign_key
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the table-qualified key to match on the related model.
|
||||
* @type string
|
||||
*/
|
||||
public get qualified_foreign_key() {
|
||||
return this.related.qualify_column(this.foreign_key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the table-qualified key to match on the parent model.
|
||||
* @type string
|
||||
*/
|
||||
public get qualified_local_key() {
|
||||
return this.related.qualify_column(this.local_key)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {Model} from '../Model.ts'
|
||||
import AppClass from '../../../../lib/src/lifecycle/AppClass.ts'
|
||||
import RelationResultOperator from '../RelationResultOperator.ts'
|
||||
import ConnectionExecutable from '../../builder/type/ConnectionExecutable.ts'
|
||||
import {AsyncCollection} from '../../../../lib/src/collection/AsyncCollection.ts'
|
||||
import {Collection} from '../../../../lib/src/collection/Collection.ts'
|
||||
@@ -13,41 +12,116 @@ import {Delete} from '../../builder/type/Delete.ts'
|
||||
import ModelResultOperator from '../ModelResultOperator.ts'
|
||||
import {ModelSelect} from '../query/ModelSelect.ts'
|
||||
|
||||
/**
|
||||
* The result of loading a relation.
|
||||
*/
|
||||
export type RelationResult<T> = T | Collection<T> | undefined
|
||||
|
||||
/**
|
||||
* Abstract base class for model-to-model relations.
|
||||
* @extends AppClass
|
||||
* @abstract
|
||||
*/
|
||||
export abstract class Relation<T extends Model<T>, T2 extends Model<T2>> extends AppClass {
|
||||
constructor(
|
||||
/**
|
||||
* The parent model.
|
||||
* @type Model
|
||||
*/
|
||||
protected parent: T,
|
||||
/**
|
||||
* The related model.
|
||||
* @type Model
|
||||
*/
|
||||
public readonly related: T2,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value on the parent model that is used in the relation.
|
||||
*/
|
||||
protected abstract get parent_value(): any
|
||||
|
||||
/**
|
||||
* Get the result operator for this relation.
|
||||
* @return ModelResultOperator
|
||||
*/
|
||||
public get_operator() {
|
||||
const related_class = this.related.constructor as typeof Model
|
||||
return this.make(ModelResultOperator, related_class)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query instance for this relation.
|
||||
* @returns ConnectionExecutable
|
||||
* @abstract
|
||||
*/
|
||||
public abstract query(): ConnectionExecutable<T2>
|
||||
|
||||
/**
|
||||
* Scope an incoming query to the result set of this relation.
|
||||
* @param {WhereBuilder} where
|
||||
* @abstract
|
||||
*/
|
||||
public abstract scope_query(where: WhereBuilder): void
|
||||
|
||||
/**
|
||||
* Build a query to eager load this relation.
|
||||
* @param {ModelSelect} parent_query - the incoming query
|
||||
* @param {Collection} result - the loaded parents
|
||||
* @return ModelSelect
|
||||
* @abstract
|
||||
*/
|
||||
public abstract build_eager_query(parent_query: ModelSelect<T>, result: Collection<T>): ModelSelect<T2>
|
||||
|
||||
/**
|
||||
* Match the results from an eager load to only those belonging to this relation's parent.
|
||||
* @param {Collection} possibly_related
|
||||
* @return Collection
|
||||
* @abstract
|
||||
*/
|
||||
public abstract match_results(possibly_related: Collection<T>): Collection<T>
|
||||
|
||||
/**
|
||||
* Set the value of this relation, caching it.
|
||||
* @param {Collection} related
|
||||
* @abstract
|
||||
*/
|
||||
public abstract set_value(related: Collection<T2>): void
|
||||
|
||||
/**
|
||||
* Get the cached value of this relation.
|
||||
* @return RelationResult
|
||||
* @abstract
|
||||
*/
|
||||
public abstract get_value(): Collection<T2> | T2 | undefined
|
||||
|
||||
/**
|
||||
* Returns true if this relation has been loaded.
|
||||
* @return boolean
|
||||
* @abstract
|
||||
*/
|
||||
public abstract is_loaded(): boolean
|
||||
|
||||
/**
|
||||
* Fetch the results of this relation, by query.
|
||||
* @return AsyncCollection
|
||||
*/
|
||||
public fetch(): AsyncCollection<T2> {
|
||||
return this.query().results()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the results of this relation from the database.
|
||||
* @return Promise<RelationResult>
|
||||
*/
|
||||
public abstract get(): Promise<RelationResult<T2>>
|
||||
|
||||
/**
|
||||
* Allow awaiting the relation to get the cached results, or fetched.
|
||||
* @param callback
|
||||
*/
|
||||
public then(callback: (result: RelationResult<T2>) => any) {
|
||||
if ( this.is_loaded() ) {
|
||||
callback(this.get_value() as RelationResult<T2>)
|
||||
@@ -62,28 +136,53 @@ export abstract class Relation<T extends Model<T>, T2 extends Model<T2>> extends
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current value of the relation.
|
||||
* @type RelationResult
|
||||
*/
|
||||
public get value(): RelationResult<T2> {
|
||||
return this.get_value()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the query source of the related model.
|
||||
* @type QuerySource
|
||||
*/
|
||||
public get related_query_source() {
|
||||
const related_class = this.related.constructor as typeof Model
|
||||
return related_class.query_source()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance of the relation builder for this model.
|
||||
* @return RelationBuilder
|
||||
*/
|
||||
public builder(): RelationBuilder<T2> {
|
||||
return new RelationBuilder(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a select statement for this relation.
|
||||
* @param {...FieldSet} fields
|
||||
* @return Select
|
||||
*/
|
||||
public select(...fields: FieldSet[]): Select<T2> {
|
||||
if ( fields.length < 1 ) fields.push(this.related.qualify_column('*'))
|
||||
return this.builder().select(...fields)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an update statement for this relation.
|
||||
* @return Update
|
||||
*/
|
||||
public update(): Update<T2> {
|
||||
return this.builder().update()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a delete statement for this relation.
|
||||
* @return Delete
|
||||
*/
|
||||
public delete(): Delete<T2> {
|
||||
return this.builder().delete()
|
||||
}
|
||||
|
||||
@@ -6,8 +6,16 @@ import {FieldSet, QuerySource} from '../../builder/types.ts'
|
||||
import {Delete} from '../../builder/type/Delete.ts'
|
||||
import {Model} from '../Model.ts'
|
||||
|
||||
/**
|
||||
* Query builder scoped to relation values.
|
||||
* @extends Builder
|
||||
*/
|
||||
export class RelationBuilder<T extends Model<T>> extends Builder<T> {
|
||||
constructor(
|
||||
/**
|
||||
* The relation whose related model should be the subject of these queries.
|
||||
* @type Relation
|
||||
*/
|
||||
protected relation: Relation<any, T>
|
||||
) {
|
||||
super()
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import {Model} from '../Model.ts'
|
||||
|
||||
/**
|
||||
* Decorator for model relations. This caches the relation value, so lookups are only done once.
|
||||
* @constructor
|
||||
*/
|
||||
export function Relation(): MethodDecorator {
|
||||
return (target: any, propertyKey, descriptor) => {
|
||||
console.log('relation decorator', target, propertyKey, descriptor)
|
||||
|
||||
@@ -2,22 +2,43 @@ import { Service } from '../../../di/src/decorator/Service.ts'
|
||||
import { Connection } from '../db/Connection.ts'
|
||||
import PostgresConnection from '../db/PostgresConnection.ts'
|
||||
|
||||
/**
|
||||
* Error thrown if two connections with the same name are registered.
|
||||
* @extends Error
|
||||
*/
|
||||
export class DuplicateConnectionNameError extends Error {
|
||||
constructor(connection_name: string) {
|
||||
super(`A database connection with the name "${connection_name}" already exists.`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown if a connection name is accessed when there is no corresponding connection registered.
|
||||
* @extends Error
|
||||
*/
|
||||
export class NoSuchDatabaseConnectionError extends Error {
|
||||
constructor(connection_name: string) {
|
||||
super(`No database connection exists with the name: "${connection_name}"`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Service that manages and creates database connections.
|
||||
*/
|
||||
@Service()
|
||||
export default class Database {
|
||||
/**
|
||||
* The created connections, keyed by name.
|
||||
* @type object
|
||||
*/
|
||||
private connections: { [name: string]: Connection } = {}
|
||||
|
||||
/**
|
||||
* Create a new PostgreSQL connection.
|
||||
* @param {string} name
|
||||
* @param {object} config
|
||||
* @return Promise<PostgresConnection>
|
||||
*/
|
||||
async postgres(name: string, config: { [key: string]: any }): Promise<PostgresConnection> {
|
||||
if ( this.connections[name] )
|
||||
throw new DuplicateConnectionNameError(name)
|
||||
@@ -28,6 +49,11 @@ export default class Database {
|
||||
return conn
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a connection by name.
|
||||
* @param name
|
||||
* @return Connection
|
||||
*/
|
||||
connection(name: string): Connection {
|
||||
if ( !this.connections[name] )
|
||||
throw new NoSuchDatabaseConnectionError(name)
|
||||
|
||||
Reference in New Issue
Block a user