db structure abstraction; async collection; update/insert queries; model saving

This commit is contained in:
garrettmills
2020-07-06 09:53:03 -05:00
parent eddb4f1fbe
commit e4f5da7ac6
73 changed files with 3301 additions and 57 deletions

View File

@@ -0,0 +1,50 @@
import {escape, 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";
import {Insert} from "./type/Insert.ts";
export function raw(value: string) {
return new RawValue(value)
}
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}.`)
}
}
export class Builder {
// create table, insert, delete, alter table, drop table, select
public select(...fields: FieldSet[]) {
fields = fields.flat()
const select = new Select()
return select.fields(...fields)
}
public update(target?: QuerySource, alias?: string) {
const update = new Update()
if ( target ) update.to(target, alias)
return update
}
public insert(target?: QuerySource, alias?: string) {
const insert = new Insert()
if ( target ) insert.into(target, alias)
return insert
}
public statement(statement: string, ...interpolations: EscapedValue[]) {
return new Statement(statement, interpolations)
}
public static raw(value: string) {
return new RawValue(value)
}
public static default() {
return this.raw('DEFAULT')
}
}

View File

@@ -0,0 +1,5 @@
export default class RawValue {
constructor(
public readonly value: string
) {}
}

View File

@@ -0,0 +1,36 @@
import {EscapedValue, escape} from './types.ts'
import {IncorrectInterpolationError} from './Builder.ts'
import ConnectionExecutable from './type/ConnectionExecutable.ts'
export class Statement extends ConnectionExecutable {
constructor(
public statement: string,
public interpolations: EscapedValue[]
) {
super()
}
sql(): string {
const statement = this.statement
const interpolations = [...this.interpolations].reverse()
const expected_interpolations = (statement.match(/\?/g) || []).length
if ( expected_interpolations !== interpolations.length ) {
throw new IncorrectInterpolationError(expected_interpolations, interpolations.length)
}
const query_chars = []
for ( const char of statement.split('') ) {
if ( char === '?' ) {
const val = interpolations.pop()
if ( typeof val !== 'undefined' )
query_chars.push(escape(val))
else
throw new TypeError('Got an undefined interpolation value. Unable to continue.')
} else {
query_chars.push(char)
}
}
return query_chars.join('')
}
}

View File

@@ -0,0 +1,89 @@
import {QueryResult} from '../../db/types.ts'
import {make} from '../../../../di/src/global.ts'
import Database from '../../service/Database.ts'
import {logger} from '../../../../lib/src/service/logging/global.ts'
import {Connection} from '../../db/Connection.ts'
import {ResultCollection} from './result/ResultCollection.ts'
import {ResultIterable} from './result/ResultIterable.ts'
import ResultOperator from './result/ResultOperator.ts'
import ObjectResultOperator from './result/ObjectResultOperator.ts'
export default abstract class ConnectionExecutable {
abstract sql(level: number): string
to_count(): string {
return `SELECT COUNT(*) AS to_count FROM (${this.sql(0)}) AS target_query`
}
async get_row(i: number) {
if ( !(this.__target_connection instanceof Connection) ) {
throw new Error('Unable to execute database item: no target connection.')
}
const query = `SELECT * FROM (${this.sql(0)}) AS target_query OFFSET ${i} LIMIT 1`
const result = await this.__target_connection.query(query)
const row = result.rows.first()
if ( row ) return this.__target_operator.inflate_row(row)
}
async get_range(start: number, end: number) {
if ( !(this.__target_connection instanceof Connection) ) {
throw new Error('Unable to execute database item: no target connection.')
}
const query = `SELECT * FROM (${this.sql(0)}) AS target_query OFFSET ${start} LIMIT ${(end - start) + 1}`
const result = await this.__target_connection.query(query)
return result.rows.map(row => this.__target_operator.inflate_row(row))
}
iterator() {
return new ResultIterable(this)
}
results(chunk_size = 1000) {
return new ResultCollection(this.iterator(), chunk_size)
}
__target_connection?: Connection
__target_operator: ResultOperator<any> = new ObjectResultOperator()
target_connection(connection: string | Connection) {
this.__target_connection = typeof connection === 'string' ? make(Database).connection(connection) : connection
return this
}
target_operator(operator: ResultOperator<any>) {
this.__target_operator = operator
return this
}
async execute(): Promise<QueryResult> {
if ( !(this.__target_connection instanceof Connection) ) {
throw new Error('Unable to execute database item: no target connection.')
}
return this.execute_in_connection(this.__target_connection)
}
async count(): Promise<number> {
if ( !(this.__target_connection instanceof Connection) ) {
throw new Error('Unable to execute database item: no target connection.')
}
const result = await this.__target_connection.query(this.to_count())
const row = result.rows.first()
if ( row ) return Number(row.to_count)
return 0
}
async execute_in_connection(connection: string | Connection): Promise<QueryResult> {
const conn = typeof connection === 'string' ? make(Database).connection(connection) : connection
logger.debug(`Executing statement in connection: ${conn.name}`)
const sql = this.sql(0)
logger.verbose(sql)
return conn.query(sql)
}
}

View File

@@ -0,0 +1,37 @@
import ConnectionExecutable from './ConnectionExecutable.ts'
import {QueryResult, QueryRow} from "../../db/types.ts";
import {Connection} from "../../db/Connection.ts";
import {Collection} from "../../../../lib/src/collection/Collection.ts";
export default abstract class ConnectionMutable extends ConnectionExecutable {
__execution_result?: QueryResult
async get_row(i: number) {
const result = await this.get_execution_result()
const row = result.rows.at(i)
if ( row ) return this.__target_operator.inflate_row(row)
}
async get_range(start: number, end: number) {
const result = await this.get_execution_result()
const rows: Collection<QueryRow> = result.rows.slice(start, end + 1) as Collection<QueryRow>
return rows.map(row => this.__target_operator.inflate_row(row))
}
async count() {
const result = await this.get_execution_result()
return result.row_count
}
async get_execution_result(): Promise<QueryResult> {
if ( this.__execution_result ) return this.__execution_result
else return this.execute()
}
async execute_in_connection(connection: string | Connection): Promise<QueryResult> {
const result = await super.execute_in_connection(connection)
this.__execution_result = result
return result
}
}

View File

@@ -0,0 +1,107 @@
import {
EscapedValue,
HavingPreOperator,
HavingStatement,
SQLHavingOperator,
escape,
isHavingClause,
isHavingGroup
} from '../types.ts'
import {HavingBuilderFunction} from './Select.ts'
export class HavingBuilder {
protected _havings: HavingStatement[] = []
get having_items() {
return this._havings
}
havings_to_sql(havings?: HavingStatement[], level = 0): string {
const indent = Array(level * 2).fill(' ').join('')
let statements = []
for ( const having of havings || this._havings ) {
if ( isHavingClause(having) ) {
statements.push(`${indent}${statements.length < 1 ? '' : having.preop + ' '}${having.field} ${having.operator} ${having.operand}`)
} else if ( isHavingGroup(having) ) {
statements.push(`${indent}${statements.length < 1 ? '' : having.preop + ' '}(\n${this.havings_to_sql(having.items, level + 1)}\n${indent})`)
}
}
return statements.filter(Boolean).join('\n')
}
private _createHaving(preop: HavingPreOperator, field: string | HavingBuilderFunction, operator?: SQLHavingOperator, operand?: any) {
if ( typeof field === 'function' ) {
const having_builder = new HavingBuilder()
field(having_builder)
this._havings.push({
preop,
items: having_builder.having_items,
})
} else if ( field && operator && typeof operand !== 'undefined' ) {
this._havings.push({
field, operator, operand: escape(operand), preop
})
}
}
having(field: string | HavingBuilderFunction, operator?: SQLHavingOperator, operand?: any) {
this._createHaving('AND', field, operator, operand)
return this
}
havingIn(field: string, values: EscapedValue) {
this._havings.push({
field,
operator: 'IN',
operand: escape(values),
preop: 'AND',
})
return this
}
havingNot(field: string | HavingBuilderFunction, operator?: SQLHavingOperator, operand?: EscapedValue) {
this._createHaving('AND NOT', field, operator, operand)
return this
}
havingNotIn(field: string, values: EscapedValue) {
this._havings.push({
field,
operator: 'NOT IN',
operand: escape(values),
preop: 'AND'
})
return this
}
orHaving(field: string | HavingBuilderFunction, operator?: SQLHavingOperator, operand?: EscapedValue) {
this._createHaving('OR', field, operator, operand)
return this
}
orHavingNot(field: string | HavingBuilderFunction, operator?: SQLHavingOperator, operand?: EscapedValue) {
this._createHaving('OR NOT', field, operator, operand)
return this
}
orHavingIn(field: string, values: EscapedValue) {
this._havings.push({
field,
operator: 'IN',
operand: escape(values),
preop: 'OR',
})
return this
}
orHavingNotIn(field: string, values: EscapedValue) {
this._havings.push({
field,
operator: 'NOT IN',
operand: escape(values),
preop: 'OR',
})
return this
}
}

View File

@@ -0,0 +1,118 @@
import ConnectionMutable from './ConnectionMutable.ts'
import {EscapedValue, FieldValueObject, QuerySource} from '../types.ts'
import {MalformedSQLGrammarError} from './Select.ts'
import {TableRefBuilder} from './TableRefBuilder.ts'
import {applyMixins} from '../../../../lib/src/support/mixins.ts'
import {escape, FieldSet} from '../types.ts'
import {raw} from '../Builder.ts'
// TODO support DEFAULT VALUES
// TODO support ON CONFLICT
export class Insert extends ConnectionMutable {
protected _target?: QuerySource = undefined
protected _columns: string[] = []
protected _rows: string[] = []
protected _fields: string[] = []
protected _return_all = false
sql(level = 0): string {
const indent = Array(level * 2).fill(' ').join('')
if ( typeof this._target === 'undefined' )
throw new MalformedSQLGrammarError('No table reference has been provided.')
if ( this._rows.length < 1 )
throw new MalformedSQLGrammarError('There are no rows to insert.')
const table_ref = this.source_alias_to_table_ref(this._target)
const returning = this._return_all ? this._columns.join(', ') : this._fields.join(', ')
const fields = escape(this._columns.map(x => raw(x)))
return [
`INSERT INTO ${this.serialize_table_ref(table_ref)}`,
` ${fields}`,
'VALUES',
` ${this._rows.join(',\n ')}`,
...(returning.trim() ? [`RETURNING ${returning}`] : [])
].filter(x => String(x).trim()).join(`\n${indent}`)
}
into(source: QuerySource, alias?: string) {
if ( !alias ) this._target = source
else this._target = { ref: source, alias }
return this
}
columns(...columns: string[]) {
this._columns = columns
return this
}
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.`)
this._rows.push(escape(row))
return this
}
row(row: FieldValueObject) {
const columns = []
const row_raw = []
for ( const field in row ) {
if ( !row.hasOwnProperty(field) ) continue
columns.push(field)
row_raw.push(row[field])
}
this.columns(...columns)
this.row_raw(...row_raw)
return this
}
rows(rows: FieldValueObject[]) {
const [initial, ...rest] = rows
const columns = []
const initial_raw = []
for ( const field in initial ) {
if ( !initial.hasOwnProperty(field) ) continue
columns.push(field)
initial_raw.push(initial[field])
}
this.columns(...columns)
this.row_raw(...initial_raw)
for ( const row of rest ) {
const values = []
for ( const col of columns ) {
values.push(row[col])
}
this.row_raw(...values)
}
return this
}
returning(...fields: FieldSet[]) {
for ( const field_set of fields ) {
if ( typeof field_set === 'string' ) {
if ( !this._fields.includes(field_set) )
this._fields.push(field_set)
} else {
for ( const field of field_set ) {
if ( !this._fields.includes(field) )
this._fields.push(field)
}
}
}
this._return_all = this._fields.length === 0
return this
}
}
export interface Insert extends TableRefBuilder {}
applyMixins(Insert, [TableRefBuilder])

View File

@@ -0,0 +1,204 @@
import {
FieldSet,
QuerySource,
WhereStatement,
TableRef,
OrderStatement,
OrderDirection, HavingStatement
} from '../types.ts'
import {WhereBuilder} from './WhereBuilder.ts'
import {applyMixins} from '../../../../lib/src/support/mixins.ts'
import {TableRefBuilder} from './TableRefBuilder.ts'
import {Join} from './join/Join.ts'
import {LeftJoin} from './join/LeftJoin.ts'
import {LeftOuterJoin} from './join/LeftOuterJoin.ts'
import {CrossJoin} from './join/CrossJoin.ts'
import {InnerJoin} from './join/InnerJoin.ts'
import {RightJoin} from './join/RightJoin.ts'
import {RightOuterJoin} from './join/RightOuterJoin.ts'
import {FullOuterJoin} from './join/FullOuterJoin.ts'
import {HavingBuilder} from './HavingBuilder.ts'
import ConnectionExecutable from './ConnectionExecutable.ts'
export type WhereBuilderFunction = (group: WhereBuilder) => any
export type HavingBuilderFunction = (group: HavingBuilder) => any
export type JoinFunction = (join: Join) => any
export class MalformedSQLGrammarError extends Error {}
export class Select extends ConnectionExecutable {
protected _fields: string[] = []
protected _source?: QuerySource = undefined
protected _wheres: WhereStatement[] = []
protected _havings: HavingStatement[] = []
protected _limit?: number
protected _offset?: number
protected _joins: Join[] = []
protected _distinct = false
protected _group_by: string[] = []
protected _order: OrderStatement[] = []
distinct() {
this._distinct = true
return this
}
sql(level = 0): string {
const indent = Array(level * 2).fill(' ').join('')
if ( typeof this._source === 'undefined' )
throw new MalformedSQLGrammarError(`No table reference has been provided.`)
const table_ref = this.source_alias_to_table_ref(this._source)
let order = ''
if ( this._order.length > 0 ) {
order = this._order.map(x => `${x.field} ${x.direction}`).join(', ')
}
const wheres = this.wheres_to_sql(this._wheres, level + 1)
const havings = this.havings_to_sql(this._havings, level + 1)
return [
`SELECT ${this._distinct ? 'DISTINCT ' : ''}${this._fields.join(', ')}`,
`FROM ${this.serialize_table_ref(table_ref)}`,
...this._joins.map(join => join.sql(level + 1)),
...(wheres.trim() ? ['WHERE', wheres] : []),
...[typeof this._limit !== 'undefined' ? [`LIMIT ${this._limit}`] : []],
...[typeof this._offset !== 'undefined' ? [`OFFSET ${this._offset}`] : []],
...[this._group_by.length > 0 ? 'GROUP BY ' + this._group_by.join(', ') : []],
...[order ? [`ORDER BY ${order}`] : []],
...(havings.trim() ? ['HAVING', havings] : []),
].filter(x => String(x).trim()).join(`\n${indent}`)
}
field(field: string) {
if ( !this._fields.includes(field) ) {
this._fields.push(field)
}
return this
}
group_by(...groupings: string[]) {
this._group_by = groupings
return this
}
fields(...fields: FieldSet[]) {
for ( const field_set of fields ) {
if ( typeof field_set === 'string' ) {
if ( !this._fields.includes(field_set) )
this._fields.push(field_set)
} else {
for ( const field of field_set ) {
if ( !this._fields.includes(field) )
this._fields.push(field)
}
}
}
return this
}
from(source: QuerySource, alias?: string) {
if ( !alias ) this._source = source
else this._source = { ref: source, alias }
return this
}
limit(num: number) {
this._limit = Number(num)
return this
}
offset(num: number) {
this._offset = Number(num)
return this
}
skip(num: number) {
this._offset = Number(num)
return this
}
take(num: number) {
this._limit = Number(num)
return this
}
join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) {
this._createJoin(Join, source, alias_or_func, func)
return this
}
left_join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) {
this._createJoin(LeftJoin, source, alias_or_func, func)
return this
}
left_outer_join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) {
this._createJoin(LeftOuterJoin, source, alias_or_func, func)
return this
}
cross_join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) {
this._createJoin(CrossJoin, source, alias_or_func, func)
return this
}
inner_join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) {
this._createJoin(InnerJoin, source, alias_or_func, func)
return this
}
right_join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) {
this._createJoin(RightJoin, source, alias_or_func, func)
return this
}
right_outer_join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) {
this._createJoin(RightOuterJoin, source, alias_or_func, func)
return this
}
full_outer_join(source: QuerySource, alias_or_func: string | JoinFunction, func?: JoinFunction) {
this._createJoin(FullOuterJoin, source, alias_or_func, func)
return this
}
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)
this._joins.push(join)
join_func(join)
}
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
let join_func = undefined
if ( func ) join_func = func
else if ( typeof alias_or_func === 'function' ) join_func = alias_or_func
else {
throw new TypeError('Missing required join function handler!')
}
return [this.source_alias_to_table_ref(source, alias), join_func]
}
order_by(field: string, direction: OrderDirection = 'ASC') {
this._order.push({ field, direction })
return this
}
order_asc(field: string) {
return this.order_by(field, 'ASC')
}
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 extends WhereBuilder, TableRefBuilder, HavingBuilder {}
applyMixins(Select, [WhereBuilder, TableRefBuilder, HavingBuilder])

View File

@@ -0,0 +1,41 @@
import {TableRef, QuerySource} from '../types.ts'
export class TableRefBuilder {
resolve_table_name(from: string): TableRef {
const parts = from.split('.')
const ref: any = {}
if ( parts.length > 1 ) {
ref.database = parts[0]
ref.table = parts[1]
} else {
ref.table = parts[0]
}
const alias_parts = ref.table.split(/\s+/)
if ( alias_parts.length > 1 ) {
ref.table = alias_parts[0]
ref.alias = alias_parts[1]
}
return ref as TableRef
}
serialize_table_ref(ref: TableRef): string {
return `${ref.database ? ref.database+'.' : ''}${ref.table}${ref.alias ? ' '+ref.alias : ''}`
}
source_alias_to_table_ref(source: QuerySource, alias?: string) {
let string = ''
if ( typeof source === 'string' ) {
string = source
if ( typeof alias === 'string' ) string += ` ${alias}`
}
else if ( typeof source === 'object' ) {
string = `${source.ref}`
if ( source.alias ) string += ` ${source.alias}`
else if ( typeof alias === 'string' ) string += ` ${alias}`
}
return this.resolve_table_name(string)
}
}

View File

@@ -0,0 +1,88 @@
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'
import {applyMixins} from '../../../../lib/src/support/mixins.ts'
import {TableRefBuilder} from './TableRefBuilder.ts'
import {MalformedSQLGrammarError} from './Select.ts'
import ConnectionMutable from './ConnectionMutable.ts'
// TODO FROM
// TODO WHERE CURRENT OF
export class Update extends ConnectionMutable {
protected _target?: QuerySource = undefined
protected _only = false
protected _sets: Collection<FieldValue> = new Collection<FieldValue>()
protected _wheres: WhereStatement[] = []
protected _fields: string[] = []
sql(level = 0): string {
const indent = Array(level * 2).fill(' ').join('')
if ( typeof this._target === 'undefined' )
throw new MalformedSQLGrammarError('No table reference has been provided.')
const table_ref = this.source_alias_to_table_ref(this._target)
const wheres = this.wheres_to_sql(this._wheres, level + 1)
const returning_fields = this._fields.join(', ')
return [
`UPDATE ${this._only ? 'ONLY ' : ''}${this.serialize_table_ref(table_ref)}`,
`SET`,
this.serialize_sets(this._sets, level + 1),
...(wheres.trim() ? ['WHERE', wheres] : []),
...(returning_fields.trim() ? [`RETURNING ${returning_fields}`] : []),
].filter(x => String(x).trim()).join(`\n${indent}`)
}
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}`)
}
to(source: QuerySource, alias?: string) {
if ( !alias ) this._target = source
else this._target = { ref: source, alias }
return this
}
only() {
this._only = true
return this
}
set(field: string, value: EscapedValue) {
const existing = this._sets.firstWhere('field', '=', field)
if ( existing ) {
existing.value = value
} else {
this._sets.push({ field, value })
}
return this
}
data(values: FieldValueObject) {
for ( const field in values ) {
if ( !values.hasOwnProperty(field) ) continue
this.set(field, values[field])
}
return this
}
returning(...fields: FieldSet[]) {
for ( const field_set of fields ) {
if ( typeof field_set === 'string' ) {
if ( !this._fields.includes(field_set) )
this._fields.push(field_set)
} else {
for ( const field of field_set ) {
if ( !this._fields.includes(field) )
this._fields.push(field)
}
}
}
return this
}
}
export interface Update extends WhereBuilder, TableRefBuilder {}
applyMixins(Update, [WhereBuilder, TableRefBuilder])

View File

@@ -0,0 +1,100 @@
import {EscapedValue, isWhereClause, isWhereGroup, WhereStatement} from '../types.ts'
import {escape, SQLWhereOperator, WherePreOperator} from '../types.ts'
import {WhereBuilderFunction} from "./Select.ts";
export class WhereBuilder {
protected _wheres: WhereStatement[] = []
get where_items() {
return this._wheres
}
wheres_to_sql(wheres?: WhereStatement[], level = 0): string {
const indent = Array(level * 2).fill(' ').join('')
let statements = []
for ( const where of wheres || this._wheres ) {
if ( isWhereClause(where) ) {
statements.push(`${indent}${statements.length < 1 ? '' : where.preop + ' '}${where.field} ${where.operator} ${where.operand}`)
} else if ( isWhereGroup(where) ) {
statements.push(`${indent}${statements.length < 1 ? '' : where.preop + ' '}(\n${this.wheres_to_sql(where.items, level + 1)}\n${indent})`)
}
}
return statements.filter(Boolean).join('\n')
}
private _createWhere(preop: WherePreOperator, field: string | WhereBuilderFunction, operator?: SQLWhereOperator, operand?: any) {
if ( typeof field === 'function' ) {
const where_builder = new WhereBuilder()
field(where_builder)
this._wheres.push({
preop,
items: where_builder.where_items,
})
} else if ( field && operator && typeof operand !== 'undefined' ) {
this._wheres.push({
field, operator, operand: escape(operand), preop
})
}
}
where(field: string | WhereBuilderFunction, operator?: SQLWhereOperator, operand?: any) {
this._createWhere('AND', field, operator, operand)
return this
}
whereIn(field: string, values: EscapedValue) {
this._wheres.push({
field,
operator: 'IN',
operand: escape(values),
preop: 'AND',
})
return this
}
whereNot(field: string | WhereBuilderFunction, operator?: SQLWhereOperator, operand?: EscapedValue) {
this._createWhere('AND NOT', field, operator, operand)
return this
}
whereNotIn(field: string, values: EscapedValue) {
this._wheres.push({
field,
operator: 'NOT IN',
operand: escape(values),
preop: 'AND'
})
return this
}
orWhere(field: string | WhereBuilderFunction, operator?: SQLWhereOperator, operand?: EscapedValue) {
this._createWhere('OR', field, operator, operand)
return this
}
orWhereNot(field: string | WhereBuilderFunction, operator?: SQLWhereOperator, operand?: EscapedValue) {
this._createWhere('OR NOT', field, operator, operand)
return this
}
orWhereIn(field: string, values: EscapedValue) {
this._wheres.push({
field,
operator: 'IN',
operand: escape(values),
preop: 'OR',
})
return this
}
orWhereNotIn(field: string, values: EscapedValue) {
this._wheres.push({
field,
operator: 'NOT IN',
operand: escape(values),
preop: 'OR',
})
return this
}
}

View File

@@ -0,0 +1,13 @@
import {Join} from './Join.ts'
import {JoinOperator} from '../../types.ts'
export class CrossJoin extends Join {
public readonly operator: JoinOperator = 'CROSS JOIN'
sql(level = 0): string {
const indent = Array(level * 2).fill(' ').join('')
return [
`${this.operator} ${this.serialize_table_ref(this.table_ref)}`,
].filter(Boolean).join(`\n${indent}`)
}
}

View File

@@ -0,0 +1,6 @@
import {Join} from './Join.ts'
import {JoinOperator} from '../../types.ts'
export class FullOuterJoin extends Join {
public readonly operator: JoinOperator = 'FULL OUTER JOIN'
}

View File

@@ -0,0 +1,6 @@
import {Join} from './Join.ts'
import {JoinOperator} from '../../types.ts'
export class InnerJoin extends Join {
public readonly operator: JoinOperator = 'INNER JOIN'
}

View File

@@ -0,0 +1,25 @@
import {JoinOperator, TableRef, WhereStatement} from '../../types.ts'
import {TableRefBuilder} from '../TableRefBuilder.ts'
import {applyMixins} from '../../../../../lib/src/support/mixins.ts'
import {WhereBuilder} from '../WhereBuilder.ts'
export class Join {
public readonly operator: JoinOperator = 'JOIN'
protected _wheres: WhereStatement[] = []
constructor(
public readonly table_ref: TableRef
) {}
sql(level = 0): string {
const indent = Array(level * 2).fill(' ').join('')
return [
`${this.operator} ${this.serialize_table_ref(this.table_ref)}`,
...[this._wheres.length > 0 ? ['ON'] : []],
this.wheres_to_sql(this._wheres, level),
].filter(Boolean).join(`\n${indent}`)
}
}
export interface Join extends TableRefBuilder, WhereBuilder {}
applyMixins(Join, [TableRefBuilder, WhereBuilder])

View File

@@ -0,0 +1,6 @@
import {Join} from './Join.ts'
import {JoinOperator} from '../../types.ts'
export class LeftJoin extends Join {
public readonly operator: JoinOperator = 'LEFT JOIN'
}

View File

@@ -0,0 +1,6 @@
import {Join} from './Join.ts'
import {JoinOperator} from '../../types.ts'
export class LeftOuterJoin extends Join {
public readonly operator: JoinOperator = 'LEFT OUTER JOIN'
}

View File

@@ -0,0 +1,6 @@
import {Join} from './Join.ts'
import {JoinOperator} from '../../types.ts'
export class RightJoin extends Join {
public readonly operator: JoinOperator = 'RIGHT JOIN'
}

View File

@@ -0,0 +1,6 @@
import {Join} from './Join.ts'
import {JoinOperator} from '../../types.ts'
export class RightOuterJoin extends Join {
public readonly operator: JoinOperator = 'RIGHT OUTER JOIN'
}

View File

@@ -0,0 +1,14 @@
import ResultOperator from './ResultOperator.ts'
import {QueryRow} from '../../../db/types.ts'
export default class ObjectResultOperator extends ResultOperator<QueryRow> {
inflate_row(row: QueryRow): QueryRow {
return row
}
deflate_row(item: QueryRow): QueryRow {
return item
}
}

View File

@@ -0,0 +1,22 @@
import {AsyncCollection} from '../../../../../lib/src/collection/AsyncCollection.ts'
import {ResultIterable} from './ResultIterable.ts'
import {Collection} from "../../../../../lib/src/collection/Collection.ts";
export class ResultCollection extends AsyncCollection<any> {
constructor(
executable: ResultIterable,
chunk_size: number = 1000
) {
super(executable, chunk_size)
}
then(func?: (items: Collection<any>) => any) {
if ( func ) {
this.collect().then((items: Collection<any>) => func(items))
} else {
return new Promise(res => [
this.collect().then((items: Collection<any>) => res(items))
])
}
}
}

View File

@@ -0,0 +1,26 @@
import {Iterable} from '../../../../../lib/src/collection/Iterable.ts'
import ConnectionExecutable from '../ConnectionExecutable.ts'
import {Collection} from '../../../../../lib/src/collection/Collection.ts'
export class ResultIterable extends Iterable<any> {
constructor(
protected executable: ConnectionExecutable
) { super() }
async at_index(i: number): Promise<any> {
return this.executable.get_row(i)
}
async from_range(start: number, end: number): Promise<Collection<any>> {
return this.executable.get_range(start, end)
}
async count() {
return this.executable.count()
}
clone() {
return new ResultIterable(this.executable)
}
}

View File

@@ -0,0 +1,8 @@
import {QueryRow} from '../../../db/types.ts'
export default abstract class ResultOperator<T> {
abstract inflate_row(row: QueryRow): T
abstract deflate_row(item: T): QueryRow
}

View File

@@ -0,0 +1,33 @@
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";
export abstract class ResultSet<T> extends Iterable<any> {
protected constructor(
protected executeable: ConnectionExecutable,
) {
super()
}
abstract async process_row(row: QueryRow): Promise<T>
async at_index(i: number) {
return this.process_row(await this.executeable.get_row(i))
}
async from_range(start: number, end: number) {
const results = await this.executeable.get_range(start, end)
const returns = new Collection<T>()
for ( const row of results ) {
returns.push(await this.process_row(row))
}
return returns
}
async count() {
return this.executeable.count()
}
}

102
orm/src/builder/types.ts Normal file
View File

@@ -0,0 +1,102 @@
import {WhereOperator} from '../../../lib/src/collection/Where.ts'
import RawValue from './RawValue.ts'
export type FieldSet = string | string[]
export type QuerySource = string | { ref: QuerySource, alias: string }
export type JoinOperator = 'JOIN' | 'LEFT JOIN' | 'LEFT OUTER JOIN' | 'RIGHT JOIN' | 'RIGHT OUTER JOIN' | 'FULL OUTER JOIN' | 'INNER JOIN' | 'CROSS JOIN'
export type WherePreOperator = 'AND' | 'OR' | 'AND NOT' | 'OR NOT'
export type WhereClause = { field: string, operator: SQLWhereOperator, operand: string, preop: WherePreOperator }
export type WhereGroup = { items: WhereStatement[], preop: WherePreOperator }
export type WhereStatement = WhereClause | WhereGroup
export type SQLWhereOperator = WhereOperator | 'IN' | 'NOT IN' | 'LIKE'
export type OrderDirection = 'ASC' | 'DESC'
export type OrderStatement = { direction: OrderDirection, field: string }
export type HavingPreOperator = WherePreOperator
export type HavingClause = WhereClause
export type HavingGroup = WhereGroup
export type HavingStatement = HavingClause | HavingGroup
export type SQLHavingOperator = SQLWhereOperator
export type EscapedValue = string | number | boolean | Date | RawValue | EscapedValue[]
export type FieldValue = { field: string, value: EscapedValue }
export type FieldValueObject = { [field: string]: EscapedValue }
export type TableRef = {
table: string,
database?: string,
alias?: string
}
export function isTableRef(something: any): something is TableRef {
let is = true
is = is && typeof something?.table === 'string'
if ( typeof something?.database !== 'undefined' ) {
is = typeof something?.database === 'string'
}
if ( typeof something?.alias !== 'undefined' ) {
is = typeof something?.alias === 'string'
}
return is
}
export function isWherePreOperator(something: any): something is WherePreOperator {
return ['AND', 'OR', 'AND NOT', 'OR NOT'].includes(something)
}
export function isHavingClause(something: any): something is HavingClause {
return isWhereClause(something)
}
export function isWhereClause(something: any): something is WhereClause {
return typeof something?.field === 'string'
&& typeof something?.operator === 'string' // TODO check this better
&& typeof something?.operand === 'string'
&& isWherePreOperator(something?.preop)
}
export function isHavingGroup(something: any): something is HavingGroup {
return isWhereGroup(something)
}
export function isWhereGroup(something: any): something is WhereGroup {
return Array.isArray(something?.items)
&& something.items.every((item: any) => isWhereStatement(item))
&& isWherePreOperator(something?.preop)
}
export function isWhereStatement(something: any): something is WhereStatement {
return isWhereClause(something) || isWhereGroup(something)
}
export function escape(value: EscapedValue): string {
if ( value instanceof RawValue ) {
return value.value
} else if ( Array.isArray(value) ) {
return `(${value.map(escape).join(',')})`
} else if ( String(value).toLowerCase() === 'true' ) {
return 'TRUE'
} else if ( String(value).toLowerCase() === 'false' ) {
return 'FALSE'
} else if ( typeof value === 'number' ) {
return `${value}`
} else if ( value === true ) {
return 'TRUE'
} else if ( value === false ) {
return 'FALSE'
} else if ( value instanceof Date ) { // TODO custom formattig
const pad = (val: number) => val < 10 ? `0${val}` : `${val}`
return `'${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())} ${pad(value.getHours())}:${pad(value.getMinutes())}:${pad(value.getSeconds())}'`
} else if ( !isNaN(Number(value)) ) {
return String(Number(value))
} else {
const escaped = value.replace(/"/g, '\\"').replace(/'/g, '\\\'').replace(/`/g, '\\`')
return `'${escaped}'`
}
}

20
orm/src/db/Connection.ts Normal file
View File

@@ -0,0 +1,20 @@
import {QueryResult} from './types.ts'
export class ConnectionNotReadyError extends Error {
constructor(name = '') {
super(`The connection ${name} is not ready and cannot execute queries.`)
}
}
export abstract class Connection {
constructor(
public readonly name: string,
public readonly config: any = {},
) {}
public abstract async init(): Promise<void>
public abstract async query(query: string): Promise<QueryResult> // TODO query result
public abstract async close(): Promise<void>
}

View File

@@ -0,0 +1,50 @@
import { Connection, ConnectionNotReadyError } from './Connection.ts'
import { Client } from '../../../lib/src/external/db.ts'
import {collect, Collection} from '../../../lib/src/collection/Collection.ts'
import { QueryResult, QueryRow } from './types.ts'
import { logger } from '../../../lib/src/service/logging/global.ts'
export default class PostgresConnection extends Connection {
private _client?: Client
public async init() {
this._client = new Client(this.config)
logger.info(`Opening PostgreSQL database for connection: ${this.name}`)
await this._client.connect()
}
public async query(query: string) {
if ( !this._client ) throw new ConnectionNotReadyError(this.name)
const result = await this._client.query(query)
let base_i = 0
const cols = collect(result?.rowDescription?.columns || []).sortBy('index').map(col => {
col.index = base_i
base_i += 1
return col
})
const rows = new Collection<QueryRow>()
for ( const row of result.rows ) {
const row_obj: { [key: string]: any } = {}
for ( const col of cols ) {
// @ts-ignore
row_obj[col.name] = row[col.index]
}
rows.push(row_obj)
}
logger.verbose(`Query result returned ${result.rowCount} row(s).`)
return {
rows,
row_count: result.rowCount,
} as QueryResult
}
public async close() {
if ( this._client )
await this._client.end()
}
}

63
orm/src/db/types.ts Normal file
View File

@@ -0,0 +1,63 @@
import { Collection } from '../../../lib/src/collection/Collection.ts'
export type QueryRow = { [key: string]: any }
export interface QueryResult {
rows: Collection<QueryRow>,
row_count: number,
}
export enum Type {
bigint = 'bigint',
int8 = 'bigint',
bigserial = 'bigserial',
serial8 = 'bigserial',
bit = 'bit',
bit_varying = 'bit varying',
varbit = 'bit varying',
boolean = 'boolean',
bool = 'boolean',
box = 'box',
bytea = 'bytea',
character = 'character',
char = 'character',
character_varying = 'character varying',
varchar = 'character varying',
cidr = 'cidr',
circle = 'circle',
date = 'date',
double_precision = 'double precision',
float8 = 'double precision',
inet = 'inet',
integer = 'integer',
int = 'integer',
int4 = 'integer',
interval = 'interval',
json = 'json',
line = 'line',
lseg = 'lseg',
macaddr = 'macaddr',
money = 'money',
numeric = 'numeric',
decimal = 'numeric',
path = 'path',
point = 'point',
polygon = 'polygon',
real = 'real',
float4 = 'real',
smallint = 'smallint',
int2 = 'smallint',
smallserial = 'smallserial',
serial2 = 'smallserial',
serial = 'serial',
serial4 = 'serial',
text = 'text',
time = 'time',
timestamp = 'timestamp',
tsquery = 'tsquery',
tsvector = 'tsvector',
txid_snapshot = 'txid_snapshot',
uuid = 'uuid',
xml = 'xml',
other = 'other',
}

52
orm/src/model/Field.ts Normal file
View File

@@ -0,0 +1,52 @@
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'
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) ) {
return new Collection<ModelField>()
}
return fields as Collection<ModelField>
}
export function set_model_fields_meta(model: any, fields: Collection<ModelField>) {
Reflect.defineMetadata(DATON_ORM_MODEL_FIELDS_METADATA_KEY, fields, model.constructor)
}
export interface ModelField {
database_key: string,
model_key: string | symbol,
type: any,
}
export function Field(type: Type, database_key?: string): PropertyDecorator {
return (target, model_key) => {
if ( !database_key ) database_key = String(model_key)
const fields = get_fields_meta(target)
logger.debug(`Registering field mapping ${database_key} => ${String(model_key)} as ${type} for model.`)
logger.verbose(target)
const existing_field = fields.firstWhere('model_key', '=', model_key)
if ( existing_field ) {
existing_field.database_key = database_key
existing_field.type = type
return set_model_fields_meta(target, fields)
}
fields.push({
database_key,
model_key,
type,
})
set_model_fields_meta(target, fields)
}
}

356
orm/src/model/Model.ts Normal file
View File

@@ -0,0 +1,356 @@
import { Builder } from '../builder/Builder.ts'
import {FieldSet, FieldValueObject, QuerySource} from '../builder/types.ts'
import {make} from '../../../di/src/global.ts'
import Database from '../service/Database.ts'
import {QueryRow} from '../db/types.ts'
import ModelResultOperator from './ModelResultOperator.ts'
import {get_fields_meta, ModelField, set_model_fields_meta} from './Field.ts'
import {Collection} from '../../../lib/src/collection/Collection.ts'
import {logger} from '../../../lib/src/service/logging/global.ts'
import ObjectResultOperator from '../builder/type/result/ObjectResultOperator.ts'
// TODO separate read/write connections
// TODO manual dirty flags
export abstract class Model extends Builder {
protected static connection: string
protected static table: string
protected static key: string
protected static readonly CREATED_AT = 'created_at'
protected static readonly UPDATED_AT = 'updated_at'
protected static timestamps = false
protected _original?: QueryRow
public static table_name() {
return this.table
}
public static connection_name() {
return this.connection
}
public static get_connection() {
return make(Database).connection(this.connection_name())
}
public static select(...fields: FieldSet[]) {
return this.prototype.select(...fields)
}
public static update() {
return this.prototype.update()
}
public static insert() {
return this.prototype.insert()
}
public update(target?: QuerySource, alias?: string) {
const constructor = (this.constructor as typeof Model)
return super.update()
.to(constructor.table_name())
.target_connection(constructor.get_connection())
.target_operator(make(ModelResultOperator, constructor))
}
public select(...fields: FieldSet[]) {
const constructor = (this.constructor as typeof Model)
return super.select(...fields)
.from(constructor.table_name())
.target_connection(constructor.get_connection())
.target_operator(make(ModelResultOperator, constructor))
}
public insert(target?: QuerySource, alias?: string) {
const constructor = (this.constructor as typeof Model)
return super.insert()
.into(constructor.table_name())
.target_connection(constructor.get_connection())
.target_operator(make(ModelResultOperator, constructor))
}
constructor(
values?: any
) {
super()
this.boot(values)
}
public boot(values?: any) {
if ( values ) {
get_fields_meta(this).each(field_def => {
// TODO special type casting
// @ts-ignore
this[field_def.model_key] = values[field_def.model_key]
})
}
}
public assume_from_source(row: QueryRow) {
this._original = row
get_fields_meta(this).each(field_def => {
// TODO special type casting
// @ts-ignore
this[field_def.model_key] = row[field_def.database_key]
})
return this
}
// changes
// date format
// appends
// caching
// relations
// timestamps
// hidden
// fillable
// guarded
// key type
// with
// per page
// exists
protected get _is_dirty() {
return (field_def: ModelField) => {
// @ts-ignore
return this[field_def.model_key] !== this._original[field_def.database_key]
}
}
// to_row
public to_row(): QueryRow {
const data = {}
const meta = (this.constructor as typeof Model).fields()
meta.each(field => {
})
return {}
}
// relations_to_row
// dirty_to_row
public dirty_to_row(): QueryRow {
const row = {}
this.field_defs()
.filter(this._is_dirty)
.each(field_def => {
// TODO additional casting and serializing logic here
// @ts-ignore
row[field_def.database_key] = this[field_def.model_key]
})
return row
}
// attributes
/**
* Get a collection of field definitions that contains information
* on which database fields correspond to which model fields, and
* their types.
* @return Collection<ModelField>
*/
public static fields(): Collection<ModelField> {
return get_fields_meta(this.prototype)
}
public field_defs(): Collection<ModelField> {
return (this.constructor as typeof Model).fields()
}
/**
* Sets the model field metadata to the specified collection of
* model field definitions. You should rarely need to use this.
* @param Collection<ModelField> fields
*/
public static set_fields(fields: Collection<ModelField>) {
set_model_fields_meta(this.prototype, fields)
}
/**
* Get the original values of the model as they were retrieved from
* the database. These are never updated when the model is modified.
* @return QueryRow
*/
public get_original_values() {
return this._original
}
/**
* Get an object with only the fields specified as arguments.
* Note that this is NOT a QueryRow.
* @param {...string} fields
*/
public only(...fields: string[]) {
const row = {}
for ( const field of fields ) {
// @ts-ignore
row[field] = this[field]
}
return row
}
/**
* Returns true if any of the defined fields have been modified from
* the values that were originally fetched from the database.
* @return boolean
*/
public is_dirty() {
return this.field_defs().some(this._is_dirty)
}
/**
* Get an array of model field names that have been modified from
* the values that were originally fetched from the database.
* @return string[]
*/
public dirty_fields() {
return this.field_defs()
.filter(this._is_dirty)
.pluck('model_key')
.toArray()
}
/**
* Returns true if the model has an ID from, and therefore exists in,
* the database backend.
* @return boolean
*/
public exists(): boolean {
return !!this._original && !!this.key()
}
/**
* Returns true if none of the defined fields have been modified from
* the values that were originally fetched from the database.
* @return boolean
*/
public is_clean() {
return !this.is_dirty()
}
// was changed - pass attribute(s)
// observe/observers - retrieved, saving, saved, updating, updated, creating, created, deleting, deleted
// global scopes
// non-global scopes
// has one
// morph one
// belongs to
// morph to
// has many
// has many through
// belongs to many
// morph to many
// morphed by many
// is relation loaded
// touch - update update timestamp, created if necessary
// touch created - update update and created timestamp
// set created at/set updated at
// is fillable
// is guarded
// without touching
// all
// load relations
// load missing relations
// increment column
// decrement column
// update - bulk
// push - update single
// save - update or create instance
public async save(): Promise<Model> {
const constructor = (this.constructor as typeof Model)
// TODO timestamps
if ( this.exists() && this.is_dirty() ) { // We're updating an existing record
const mutable = this.update()
.data(this.dirty_to_row())
.where(constructor.qualified_key_name(), '=', this.key())
.returning(...this._loaded_database_fields())
.target_operator(make(ObjectResultOperator))
.results()
const result = await mutable
const modified_rows = await mutable.count()
if ( modified_rows !== 1 ) {
logger.warn(`Model update modified ${modified_rows} rows! (Key: ${constructor.qualified_key_name()})`)
}
this.assume_from_source(result.firstWhere(this.key_name(), '=', this.key()))
} else if ( !this.exists() ) { // We're inserting a new record
const insert_object: FieldValueObject = this._build_insert_field_object()
const mutable = this.insert()
.row(insert_object)
.returning(this.key_name(), ...Object.keys(insert_object))
.target_operator(make(ObjectResultOperator))
.results()
const result = await mutable
const inserted_rows = await mutable.count()
if ( inserted_rows !== 1 ) {
logger.warn(`Model insert created ${inserted_rows} rows! (Key: ${constructor.qualified_key_name()})`)
}
this.assume_from_source(result.first())
}
return this
}
protected _build_insert_field_object(): FieldValueObject {
const fields = this.field_defs()
const values = {}
fields.each(field_def => {
// @ts-ignore
values[field_def.database_key] = this[field_def.model_key]
})
return values
}
protected _loaded_database_fields(): string[] {
if ( typeof this._original === 'undefined' ) return []
return Object.keys(this._original).map(String)
}
// destroy - bulk
// delete single
// force delete - for soft deleted models
// without scope
// without global scope
// without global scopes
// to object
// to json
// fresh - get new instance of this model
// refresh - reload this instance
// replicate to new instance
// is - check if two models are the same
// isNot
/**
* Returns the field name of the primary key for this model.
* @return string
*/
public key_name() {
return (this.constructor as typeof Model).key
}
/**
* If defined, returns the value of the primary key for this model.
*/
public key() {
return this?._original?.[(this.constructor as typeof Model).key]
}
/**
* Returns the table-qualified field name of the primary key for this model.
*/
public static qualified_key_name() {
return `${this.table_name()}.${this.key}`
}
}

View File

@@ -0,0 +1,26 @@
import ResultOperator from '../builder/type/result/ResultOperator.ts'
import {Model} from './Model.ts'
import {Injectable} from '../../../di/src/decorator/Injection.ts'
import {Container} from '../../../di/src/Container.ts'
import {QueryRow} from '../db/types.ts'
import Instantiable from '../../../di/src/type/Instantiable.ts'
@Injectable()
export default class ModelResultOperator extends ResultOperator<Model> {
constructor(
protected injector: Container,
protected ModelClass: Instantiable<Model>,
) {
super();
}
inflate_row(row: QueryRow): Model {
return this.injector.make(this.ModelClass).assume_from_source(row)
}
deflate_row(item: Model): QueryRow {
return item.to_row()
}
}

0
orm/src/schema/Schema.ts Normal file
View File

View File

@@ -0,0 +1,37 @@
import { Service } from '../../../di/src/decorator/Service.ts'
import { Connection } from '../db/Connection.ts'
import PostgresConnection from '../db/PostgresConnection.ts'
export class DuplicateConnectionNameError extends Error {
constructor(connection_name: string) {
super(`A database connection with the name "${connection_name}" already exists.`)
}
}
export class NoSuchDatabaseConnectionError extends Error {
constructor(connection_name: string) {
super(`No database connection exists with the name: "${connection_name}"`)
}
}
@Service()
export default class Database {
private connections: { [name: string]: Connection } = {}
async postgres(name: string, config: { [key: string]: any }): Promise<PostgresConnection> {
if ( this.connections[name] )
throw new DuplicateConnectionNameError(name)
const conn = new PostgresConnection(name, config)
this.connections[name] = conn
await conn.init()
return conn
}
connection(name: string): Connection {
if ( !this.connections[name] )
throw new NoSuchDatabaseConnectionError(name)
return this.connections[name]
}
}