JSDoc all the things!

This commit is contained in:
2020-08-17 09:44:23 -05:00
parent c2a7c3f914
commit f67ae37923
121 changed files with 2855 additions and 63 deletions

View File

@@ -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(

View File

@@ -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'

View File

@@ -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')
}

View File

@@ -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
) {}
}

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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' ) {

View File

@@ -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,

View File

@@ -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' ) {

View File

@@ -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 {}

View File

@@ -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' ) {

View File

@@ -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

View File

@@ -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' ) {

View File

@@ -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)
}

View File

@@ -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'

View File

@@ -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'
}

View File

@@ -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'
}

View File

@@ -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 [

View File

@@ -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'
}

View File

@@ -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'
}

View File

@@ -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'
}

View File

@@ -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'
}

View File

@@ -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 {

View File

@@ -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>,

View File

@@ -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() }

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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)})`

View File

@@ -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>
}

View File

@@ -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() {

View File

@@ -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',

View File

@@ -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)

View File

@@ -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)

View File

@@ -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]()

View File

@@ -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()

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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>> {

View File

@@ -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> {

View File

@@ -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)
}

View File

@@ -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()
}

View File

@@ -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()

View File

@@ -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)

View File

@@ -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)