db structure abstraction; async collection; update/insert queries; model saving
This commit is contained in:
parent
eddb4f1fbe
commit
e4f5da7ac6
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
||||
.idea*
|
||||
test*
|
||||
|
5
app/configs/app/app.config.ts
Normal file
5
app/configs/app/app.config.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { env } from '../../../lib/src/unit/Scaffolding.ts';
|
||||
|
||||
export default {
|
||||
name: env('APP_NAME', 'Daton'),
|
||||
}
|
4
app/configs/server.config.ts
Normal file
4
app/configs/server.config.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export default {
|
||||
port: 8080,
|
||||
use_ssl: false,
|
||||
}
|
5
app/http/controllers/Test.controller.ts
Normal file
5
app/http/controllers/Test.controller.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import Controller from "../../../lib/src/http/Controller.ts";
|
||||
|
||||
export default class TestController extends Controller {
|
||||
|
||||
}
|
@ -33,6 +33,16 @@ class Container {
|
||||
private factories: Collection<AbstractFactory> = new Collection<AbstractFactory>()
|
||||
private instances: Collection<InstanceRef> = new Collection<InstanceRef>()
|
||||
|
||||
constructor() {
|
||||
this.register(Container)
|
||||
this.instances.push({
|
||||
key: Container,
|
||||
value: this,
|
||||
})
|
||||
|
||||
this.register_singleton('injector', this)
|
||||
}
|
||||
|
||||
register(dependency: Instantiable<any>) {
|
||||
if ( this.resolve(dependency) )
|
||||
throw new DuplicateFactoryKeyError(dependency)
|
||||
@ -134,14 +144,22 @@ class Container {
|
||||
}
|
||||
|
||||
make(target: DependencyKey, ...parameters: any[]) {
|
||||
if ( this.has_key(target) ) {
|
||||
if ( this.has_key(target) )
|
||||
return this.resolve_and_create(target, ...parameters)
|
||||
}
|
||||
else if ( typeof target !== 'string' )
|
||||
return this.produce_factory(new Factory(target), parameters)
|
||||
else
|
||||
throw new TypeError(`Invalid or unknown make target: ${target}`)
|
||||
}
|
||||
|
||||
get_dependencies(target: DependencyKey): Collection<DependencyKey> {
|
||||
const factory = this.resolve(target)
|
||||
|
||||
if ( !factory )
|
||||
throw new InvalidDependencyKeyError(target)
|
||||
|
||||
return factory.get_dependency_keys().pluck('key')
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
|
26
lib/src/collection/ArrayIterable.ts
Normal file
26
lib/src/collection/ArrayIterable.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Iterable } from './Iterable.ts'
|
||||
import { collect } from './Collection.ts'
|
||||
|
||||
export class ArrayIterable<T> extends Iterable<T> {
|
||||
constructor(
|
||||
protected items: T[],
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async at_index(i: number) {
|
||||
return this.items[i]
|
||||
}
|
||||
|
||||
async from_range(start: number, end: number) {
|
||||
return collect(this.items.slice(start, end + 1))
|
||||
}
|
||||
|
||||
async count() {
|
||||
return this.items.length
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new ArrayIterable([...this.items])
|
||||
}
|
||||
}
|
663
lib/src/collection/AsyncCollection.ts
Normal file
663
lib/src/collection/AsyncCollection.ts
Normal file
@ -0,0 +1,663 @@
|
||||
import {
|
||||
AssociatedCollectionItem,
|
||||
Collection, CollectionIndex,
|
||||
CollectionItem, ComparisonFunction,
|
||||
DeterminesEquality, KeyFunction,
|
||||
KeyOperator, KeyReducerFunction, MaybeCollectionIndex, MaybeCollectionItem
|
||||
} from './Collection.ts'
|
||||
import {Iterable, StopIteration} from './Iterable.ts'
|
||||
import {applyWhere, WhereOperator} from './Where.ts'
|
||||
type AsyncCollectionComparable<T> = CollectionItem<T>[] | Collection<T> | AsyncCollection<T>
|
||||
type AsyncKeyFunction<T, T2> = (item: CollectionItem<T>, index: number) => CollectionItem<T2> | Promise<CollectionItem<T2>>
|
||||
type AsyncCollectionFunction<T, T2> = (items: AsyncCollection<T>) => T2
|
||||
|
||||
export class AsyncCollection<T> {
|
||||
constructor(
|
||||
private _items: Iterable<T>,
|
||||
private _chunk_size: number = 1000, // TODO fix this. It's just for testing
|
||||
) {}
|
||||
|
||||
private async _chunk(callback: (items: Collection<T>) => any): Promise<void> {
|
||||
await this._items.chunk(this._chunk_size, async items => {
|
||||
await callback(items)
|
||||
})
|
||||
await this._items.reset()
|
||||
}
|
||||
|
||||
private async _chunk_all<T2>(key: KeyOperator<T, T2>, callback: (items: Collection<T2>) => any): Promise<void> {
|
||||
await this._items.chunk(this._chunk_size, async items => {
|
||||
await callback(items.pluck(key))
|
||||
})
|
||||
await this._items.reset()
|
||||
}
|
||||
|
||||
private async _chunk_all_numbers<T2>(key: KeyOperator<T, T2>, callback: (items: number[]) => any): Promise<void> {
|
||||
await this._items.chunk(this._chunk_size, async items => {
|
||||
await callback(items.pluck(key).map(x => Number(x)).all())
|
||||
})
|
||||
await this._items.reset()
|
||||
}
|
||||
|
||||
private async _chunk_all_associate<T2>(key: KeyOperator<T, T2>, callback: (items: AssociatedCollectionItem<T2, T>[]) => any): Promise<void> {
|
||||
await this._items.chunk(this._chunk_size, async items => {
|
||||
const assoc_items: AssociatedCollectionItem<T2, T>[] = []
|
||||
if ( typeof key === 'function' ) {
|
||||
items.map((item, index) => {
|
||||
const key_item = key(item, index)
|
||||
assoc_items.push({ key: key_item, item })
|
||||
})
|
||||
} else if ( typeof key === 'string' ) {
|
||||
items.map((item, index) => {
|
||||
assoc_items.push({ key: (<any>item)[key], item })
|
||||
})
|
||||
}
|
||||
|
||||
await callback(assoc_items)
|
||||
})
|
||||
await this._items.reset()
|
||||
}
|
||||
|
||||
async all(): Promise<CollectionItem<T>[]> {
|
||||
return (await this._items.from_range(0, await this._items.count())).all()
|
||||
}
|
||||
|
||||
async collect(): Promise<Collection<T>> {
|
||||
return this._items.from_range(0, await this._items.count())
|
||||
}
|
||||
|
||||
async average<T2>(key?: KeyOperator<T, T2>): Promise<number> {
|
||||
let running_total = 0
|
||||
let running_items = 0
|
||||
|
||||
const chunk_helper = (items: number[]) => {
|
||||
running_items += items.length
|
||||
running_total += items.reduce((prev, curr) => prev + curr)
|
||||
}
|
||||
|
||||
if ( key ) await this._chunk_all_numbers(key, chunk_helper)
|
||||
else await this._chunk((items) => {
|
||||
chunk_helper(items.map(x => Number(x)).all())
|
||||
})
|
||||
|
||||
return running_total / running_items
|
||||
}
|
||||
|
||||
async median<T2>(key?: KeyOperator<T, T2>): Promise<number> {
|
||||
let items: number[] = []
|
||||
|
||||
const chunk_helper = (next_items: number[]) => {
|
||||
items = items.concat(next_items)
|
||||
}
|
||||
|
||||
if ( key ) await this._chunk_all_numbers(key, chunk_helper)
|
||||
else await this._chunk(items => {
|
||||
chunk_helper(items.map(x => Number(x)).all())
|
||||
})
|
||||
|
||||
items = items.sort((a, b) => a - b)
|
||||
const middle = Math.floor((items.length - 1) / 2)
|
||||
if ( items.length % 2 ) return items[middle]
|
||||
else return (items[middle] + items[middle + 1]) / 2
|
||||
}
|
||||
|
||||
async mode<T2>(key?: KeyOperator<T, T2>): Promise<number> {
|
||||
let counts: any = {}
|
||||
|
||||
const chunk_helper = (items: number[]) => {
|
||||
for ( const item of items ) {
|
||||
if ( !counts[item] ) counts[item] = 1
|
||||
else counts[item] += 1
|
||||
}
|
||||
}
|
||||
|
||||
if ( key ) await this._chunk_all_numbers(key, chunk_helper)
|
||||
else await this._chunk(items => {
|
||||
chunk_helper(items.map(x => Number(x)).all())
|
||||
})
|
||||
|
||||
return Number(Object.keys(counts).reduce((a, b) => counts[a] > counts[b] ? a : b)[0])
|
||||
}
|
||||
|
||||
async collapse(): Promise<Collection<any>> {
|
||||
const items = await this.collect()
|
||||
return items.collapse() as Collection<any>
|
||||
}
|
||||
|
||||
async contains<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): Promise<boolean> {
|
||||
let contains = false
|
||||
|
||||
await this._chunk_all_associate(key, (items: AssociatedCollectionItem<T2, T>[]) => {
|
||||
const matches = applyWhere(items, operator, operand)
|
||||
if ( matches.length > 0 ) {
|
||||
contains = true
|
||||
throw new StopIteration()
|
||||
}
|
||||
})
|
||||
|
||||
return contains
|
||||
}
|
||||
|
||||
async clone(): Promise<AsyncCollection<T>> {
|
||||
return new AsyncCollection<T>(await this._items.clone())
|
||||
}
|
||||
|
||||
async diff(items: AsyncCollectionComparable<T>): Promise<Collection<T>> {
|
||||
const matches: T[] = []
|
||||
|
||||
await this._chunk(async chunk => {
|
||||
for ( const item of chunk.all() ) {
|
||||
if ( !(await items.includes(item)) ) {
|
||||
matches.push(item)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return new Collection<T>(matches)
|
||||
}
|
||||
|
||||
async diffUsing(items: AsyncCollectionComparable<T>, compare: DeterminesEquality<T>): Promise<Collection<T>> {
|
||||
const matches: T[] = []
|
||||
|
||||
await this._chunk(async chunk => {
|
||||
for ( const item of chunk.all() ) {
|
||||
if ( !(await items.some(exc => compare(item, exc))) )
|
||||
matches.push(item)
|
||||
}
|
||||
})
|
||||
|
||||
return new Collection<T>(matches)
|
||||
}
|
||||
|
||||
async includes(item: CollectionItem<T>): Promise<boolean> {
|
||||
let contains = false
|
||||
|
||||
await this._chunk(items => {
|
||||
if ( items.includes(item) ) {
|
||||
contains = true
|
||||
throw new StopIteration()
|
||||
}
|
||||
})
|
||||
|
||||
return contains
|
||||
}
|
||||
|
||||
async some(operator: (item: T) => boolean): Promise<boolean> {
|
||||
let contains = false
|
||||
|
||||
await this._chunk(items => {
|
||||
for ( const item of items.all() ) {
|
||||
if ( operator(item) ) {
|
||||
contains = true
|
||||
throw new StopIteration()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return contains
|
||||
}
|
||||
|
||||
async each<T2>(func: AsyncKeyFunction<T, T2>): Promise<void> {
|
||||
let index = 0
|
||||
|
||||
await this._chunk(async items => {
|
||||
for ( const item of items.all() ) {
|
||||
await func(item, index)
|
||||
index += 1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async map<T2>(func: AsyncKeyFunction<T, T2>): Promise<Collection<T2>> {
|
||||
const new_items: CollectionItem<T2>[] = []
|
||||
await this.each(async (item, index) => {
|
||||
new_items.push(await func(item, index))
|
||||
})
|
||||
return new Collection<T2>(new_items)
|
||||
}
|
||||
|
||||
async every<T2>(func: AsyncKeyFunction<T, T2>): Promise<boolean> {
|
||||
let pass = true
|
||||
let index = 0
|
||||
|
||||
await this._chunk(async items => {
|
||||
for ( const item of items.all() ) {
|
||||
if ( !(await func(item, index)) ) {
|
||||
pass = false
|
||||
throw new StopIteration()
|
||||
}
|
||||
|
||||
index += 1
|
||||
}
|
||||
})
|
||||
|
||||
return pass
|
||||
}
|
||||
|
||||
async everyWhere<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): Promise<boolean> {
|
||||
let pass = true
|
||||
|
||||
await this._chunk(async items => {
|
||||
pass = pass && items.everyWhere(key, operator, operand)
|
||||
if ( !pass ) {
|
||||
throw new StopIteration()
|
||||
}
|
||||
})
|
||||
|
||||
return pass
|
||||
}
|
||||
|
||||
async filter<T2>(func: KeyFunction<T, T2>): Promise<Collection<T>> {
|
||||
let new_items: CollectionItem<T>[] = []
|
||||
|
||||
await this._chunk(async items => {
|
||||
new_items = new_items.concat(items.filter(func).all())
|
||||
})
|
||||
|
||||
return new Collection<T>(new_items)
|
||||
}
|
||||
|
||||
when<T2>(bool: boolean, then: AsyncCollectionFunction<T, T2>): AsyncCollection<T> {
|
||||
if ( bool ) then(this)
|
||||
return this
|
||||
}
|
||||
|
||||
unless<T2>(bool: boolean, then: AsyncCollectionFunction<T, T2>): AsyncCollection<T> {
|
||||
if ( !bool ) then(this)
|
||||
return this
|
||||
}
|
||||
|
||||
async where<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): Promise<Collection<T>> {
|
||||
let new_items: CollectionItem<T>[] = []
|
||||
await this._chunk(async items => {
|
||||
new_items = new_items.concat(items.where(key, operator, operand).all())
|
||||
})
|
||||
return new Collection<T>(new_items)
|
||||
}
|
||||
|
||||
async whereNot<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): Promise<Collection<T>> {
|
||||
let new_items: CollectionItem<T>[] = []
|
||||
await this._chunk(async items => {
|
||||
new_items = new_items.concat(items.whereNot(key, operator, operand).all())
|
||||
})
|
||||
return new Collection<T>(new_items)
|
||||
}
|
||||
|
||||
async whereIn<T2>(key: KeyOperator<T, T2>, items: AsyncCollectionComparable<T2>): Promise<Collection<T>> {
|
||||
let new_items: CollectionItem<T>[] = []
|
||||
await this._chunk_all_associate(key,async chunk => {
|
||||
for ( const item of chunk ) {
|
||||
if ( await items.includes(item.key) ) {
|
||||
new_items.push(item.item)
|
||||
}
|
||||
}
|
||||
})
|
||||
return new Collection<T>(new_items)
|
||||
}
|
||||
|
||||
async whereNotIn<T2>(key: KeyOperator<T, T2>, items: AsyncCollectionComparable<T2>): Promise<Collection<T>> {
|
||||
let new_items: CollectionItem<T>[] = []
|
||||
await this._chunk_all_associate(key,async chunk => {
|
||||
for ( const item of chunk ) {
|
||||
if ( !(await items.includes(item.key)) ) {
|
||||
new_items.push(item.item)
|
||||
}
|
||||
}
|
||||
})
|
||||
return new Collection<T>(new_items)
|
||||
}
|
||||
|
||||
async first(): Promise<MaybeCollectionItem<T>> {
|
||||
if ( await this._items.count() > 0 ) {
|
||||
return this._items.at_index(0)
|
||||
}
|
||||
}
|
||||
|
||||
async firstWhere<T2>(key: KeyOperator<T, T2>, operator: WhereOperator = '=', operand: any = true): Promise<MaybeCollectionItem<T>> {
|
||||
let item = undefined
|
||||
await this._chunk_all_associate(key, async items => {
|
||||
const matches = applyWhere(items, operator, operand)
|
||||
if ( matches.length > 0 ) {
|
||||
item = matches[0]
|
||||
throw new StopIteration()
|
||||
}
|
||||
})
|
||||
return item
|
||||
}
|
||||
|
||||
async firstWhereNot<T2>(key: KeyOperator<T, T2>, operator: WhereOperator = '=', operand: any = true): Promise<MaybeCollectionItem<T>> {
|
||||
let item: MaybeCollectionItem<T> = undefined
|
||||
await this._chunk(async items => {
|
||||
const matches = items.whereNot(key, operator, operand)
|
||||
if ( matches.length > 0 ) {
|
||||
item = matches.first()
|
||||
throw new StopIteration()
|
||||
}
|
||||
})
|
||||
return item
|
||||
}
|
||||
|
||||
async count() {
|
||||
return this._items.count()
|
||||
}
|
||||
|
||||
async length() {
|
||||
return this._items.count()
|
||||
}
|
||||
|
||||
async get(index: number, fallback?: any) {
|
||||
if ( (await this.count()) > index ) return this._items.at_index(index)
|
||||
else return fallback
|
||||
}
|
||||
|
||||
async at(index: number): Promise<MaybeCollectionItem<T>> {
|
||||
return this.get(index)
|
||||
}
|
||||
|
||||
async groupBy<T2>(key: KeyOperator<T, T2>): Promise<any> {
|
||||
return (await this.collect()).groupBy(key)
|
||||
}
|
||||
|
||||
async associate<T2>(key: KeyOperator<T, T2>): Promise<any> {
|
||||
return (await this.collect()).associate(key)
|
||||
}
|
||||
|
||||
async join(delimiter: string): Promise<string> {
|
||||
let running_strings: string[] = []
|
||||
|
||||
await this._chunk(async items => {
|
||||
running_strings.push(items.join(delimiter))
|
||||
})
|
||||
|
||||
return running_strings.join(delimiter)
|
||||
}
|
||||
|
||||
async implode(delimiter: string): Promise<string> {
|
||||
return this.join(delimiter)
|
||||
}
|
||||
|
||||
// TODO intersect
|
||||
|
||||
async isEmpty(): Promise<boolean> {
|
||||
return (await this._items.count()) < 1
|
||||
}
|
||||
|
||||
async isNotEmpty(): Promise<boolean> {
|
||||
return (await this._items.count()) > 0
|
||||
}
|
||||
|
||||
async last(): Promise<MaybeCollectionItem<T>> {
|
||||
const length = await this._items.count()
|
||||
if ( length > 0 ) return this._items.at_index(length - 1)
|
||||
}
|
||||
|
||||
async lastWhere<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): Promise<MaybeCollectionItem<T>> {
|
||||
return (await this.where(key, operator, operand)).last()
|
||||
}
|
||||
|
||||
async lastWhereNot<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): Promise<MaybeCollectionItem<T>> {
|
||||
return (await this.whereNot(key, operator, operand)).last()
|
||||
}
|
||||
|
||||
async pluck<T2>(key: KeyOperator<T, T2>): Promise<Collection<T2>> {
|
||||
let new_items: CollectionItem<T2>[] = []
|
||||
|
||||
await this._chunk_all(key, async items => {
|
||||
new_items = new_items.concat(items.all())
|
||||
})
|
||||
|
||||
return new Collection<T2>(new_items)
|
||||
}
|
||||
|
||||
async max<T2>(key: KeyOperator<T, T2>): Promise<number> {
|
||||
let running_max: number
|
||||
|
||||
await this._chunk_all_numbers(key, async items => {
|
||||
const local_max = Math.max(...items)
|
||||
if ( typeof running_max === 'undefined' ) running_max = local_max
|
||||
else running_max = Math.max(running_max, local_max)
|
||||
})
|
||||
|
||||
// @ts-ignore
|
||||
return running_max
|
||||
}
|
||||
|
||||
async whereMax<T2>(key: KeyOperator<T, T2>): Promise<Collection<T>> {
|
||||
return this.where(key, '=', await this.max(key))
|
||||
}
|
||||
|
||||
async min<T2>(key: KeyOperator<T, T2>): Promise<number> {
|
||||
let running_min: number
|
||||
|
||||
await this._chunk_all_numbers(key, async items => {
|
||||
const local_min = Math.min(...items)
|
||||
if ( typeof running_min === 'undefined' ) running_min = local_min
|
||||
else running_min = Math.min(running_min, local_min)
|
||||
})
|
||||
|
||||
// @ts-ignore
|
||||
return running_min
|
||||
}
|
||||
|
||||
async whereMin<T2>(key: KeyOperator<T, T2>): Promise<Collection<T>> {
|
||||
return this.where(key, '=', await this.min(key))
|
||||
}
|
||||
|
||||
async merge<T2>(merge_with: AsyncCollectionComparable<T2>): Promise<Collection<T|T2>> {
|
||||
let items: T2[]
|
||||
if ( merge_with instanceof Collection ) items = await merge_with.all()
|
||||
else if ( merge_with instanceof AsyncCollection ) items = await merge_with.all()
|
||||
else if ( Array.isArray(merge_with) ) items = merge_with
|
||||
|
||||
// @ts-ignore
|
||||
return new Collection<T|T2>([...items, ...await this.all()])
|
||||
}
|
||||
|
||||
async nth(n: number): Promise<Collection<T>> {
|
||||
const matches: CollectionItem<T>[] = []
|
||||
let current = 1
|
||||
|
||||
await this._chunk(async chunk => {
|
||||
for ( const item of chunk.all() ) {
|
||||
if ( current === 1 ) matches.push(item)
|
||||
current += 1
|
||||
if ( current > n ) current = 1
|
||||
}
|
||||
})
|
||||
|
||||
return new Collection<T>(matches)
|
||||
}
|
||||
|
||||
async forPage(page: number, per_page: number): Promise<Collection<T>> {
|
||||
const start = page * per_page - per_page
|
||||
const end = page * per_page - 1
|
||||
return this._items.from_range(start, end)
|
||||
}
|
||||
|
||||
pipe<T2>(func: AsyncCollectionFunction<T, T2>): any {
|
||||
return func(this)
|
||||
}
|
||||
|
||||
/*async pop(): Promise<MaybeCollectionItem<T>> {
|
||||
const next_item = await this._items.next()
|
||||
if ( !next_item.done ) {
|
||||
return next_item.value
|
||||
}
|
||||
}*/ // TODO Fix this
|
||||
|
||||
async random(n: number): Promise<Collection<T>> {
|
||||
const random_items: CollectionItem<T>[] = []
|
||||
const fetched_indices: number[] = []
|
||||
|
||||
const max_n = await this._items.count()
|
||||
if ( n > max_n ) n = max_n
|
||||
|
||||
while ( random_items.length < n ) {
|
||||
const index = Math.floor(Math.random() * max_n)
|
||||
|
||||
if ( !fetched_indices.includes(index) ) {
|
||||
fetched_indices.push(index)
|
||||
random_items.push(await this._items.at_index(index))
|
||||
}
|
||||
}
|
||||
|
||||
return new Collection<T>(random_items)
|
||||
}
|
||||
|
||||
async reduce<T2>(reducer: KeyReducerFunction<T, T2>, initial_value?: T2): Promise<T2 | undefined> {
|
||||
let current_value = initial_value
|
||||
let index = 0
|
||||
|
||||
await this._chunk(async items => {
|
||||
for ( const item of items.all() ) {
|
||||
current_value = reducer(current_value, item, index)
|
||||
index += 1
|
||||
}
|
||||
})
|
||||
|
||||
return current_value
|
||||
}
|
||||
|
||||
async reject<T2>(truth_test: AsyncKeyFunction<T, T2>): Promise<Collection<T>> {
|
||||
let rejected: CollectionItem<T>[] = []
|
||||
|
||||
await this._chunk(async items => {
|
||||
rejected = rejected.concat(items.all().filter((item, index) => {
|
||||
return !truth_test(item, index)
|
||||
}))
|
||||
})
|
||||
|
||||
return new Collection<T>(rejected)
|
||||
}
|
||||
|
||||
async reverse(): Promise<Collection<T>> {
|
||||
return (await this.collect()).reverse()
|
||||
}
|
||||
|
||||
async search(item: CollectionItem<T>): Promise<MaybeCollectionIndex> {
|
||||
let found_index
|
||||
let index = 0
|
||||
|
||||
await this._chunk(async items => {
|
||||
items.some(possible_item => {
|
||||
if ( possible_item === item ) {
|
||||
found_index = index
|
||||
throw new StopIteration()
|
||||
}
|
||||
|
||||
index += 1
|
||||
return false
|
||||
})
|
||||
})
|
||||
|
||||
return found_index
|
||||
}
|
||||
|
||||
async shift(): Promise<MaybeCollectionItem<T>> {
|
||||
const next_item = await this._items.next()
|
||||
if ( !next_item.done ) {
|
||||
return next_item.value
|
||||
}
|
||||
}
|
||||
|
||||
async shuffle(): Promise<Collection<T>> {
|
||||
return (await this.collect()).shuffle()
|
||||
}
|
||||
|
||||
async slice(start: number, end: number): Promise<Collection<T>> {
|
||||
return this._items.from_range(start, end - 1)
|
||||
}
|
||||
|
||||
async sort(compare_func?: ComparisonFunction<T>): Promise<Collection<T>> {
|
||||
return (await this.collect()).sort(compare_func)
|
||||
}
|
||||
|
||||
async sortBy<T2>(key?: KeyOperator<T, T2>): Promise<Collection<T>> {
|
||||
return (await this.collect()).sortBy(key)
|
||||
}
|
||||
|
||||
async sortDesc(compare_func?: ComparisonFunction<T>): Promise<Collection<T>> {
|
||||
return (await this.collect()).sortDesc(compare_func)
|
||||
}
|
||||
|
||||
async sortByDesc<T2>(key?: KeyOperator<T, T2>): Promise<Collection<T>> {
|
||||
return (await this.collect()).sortByDesc(key)
|
||||
}
|
||||
|
||||
async splice(start: CollectionIndex, deleteCount?: number): Promise<Collection<T>> {
|
||||
return (await this.collect()).splice(start, deleteCount)
|
||||
}
|
||||
|
||||
async sum<T2>(key?: KeyOperator<T, T2>): Promise<number> {
|
||||
let running_sum: number = 0
|
||||
|
||||
const chunk_handler = (items: number[]) => {
|
||||
for ( const item of items ) {
|
||||
running_sum += item
|
||||
}
|
||||
}
|
||||
|
||||
if ( key ) await this._chunk_all_numbers(key, chunk_handler)
|
||||
else await this._chunk(async chunk => {
|
||||
chunk_handler(chunk.map(x => Number(x)).all())
|
||||
})
|
||||
|
||||
return running_sum
|
||||
}
|
||||
|
||||
async take(limit: number): Promise<Collection<T>> {
|
||||
if ( limit === 0 ) return new Collection<T>()
|
||||
else if ( limit > 0 ) {
|
||||
return this.slice(0, limit)
|
||||
} else {
|
||||
const cnt = await this._items.count()
|
||||
return this._items.from_range(cnt - (-1 * limit), cnt - 1)
|
||||
}
|
||||
}
|
||||
|
||||
async tap<T2>(func: AsyncCollectionFunction<T, T2>): Promise<AsyncCollection<T>> {
|
||||
await func(this)
|
||||
return this
|
||||
}
|
||||
|
||||
async unique<T2>(key?: KeyOperator<T, T2>): Promise<Collection<T|T2>> {
|
||||
const has: CollectionItem<T|T2>[] = []
|
||||
|
||||
if ( !key ) {
|
||||
await this._chunk(async items => {
|
||||
for ( const item of items.all() ) {
|
||||
if ( !has.includes(item) ) has.push(item)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
await this._chunk_all(key, async items => {
|
||||
for ( const item of items.all() ) {
|
||||
if ( !has.includes(item) ) has.push(item)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return new Collection<T|T2>(has)
|
||||
}
|
||||
|
||||
async toArray(): Promise<any[]> {
|
||||
const returns: any = []
|
||||
for ( const item of (await this.all()) ) {
|
||||
if ( item instanceof Collection ) returns.push(item.toArray())
|
||||
else if ( item instanceof AsyncCollection ) returns.push(await item.toArray())
|
||||
else returns.push(item)
|
||||
}
|
||||
return returns
|
||||
}
|
||||
|
||||
async toJSON(replacer = undefined, space = 4): Promise<string> {
|
||||
return JSON.stringify(this.toArray(), replacer, space)
|
||||
}
|
||||
|
||||
[Symbol.asyncIterator]() {
|
||||
return this._items.clone()
|
||||
}
|
||||
|
||||
iterator() {
|
||||
return this._items.clone()
|
||||
}
|
||||
}
|
@ -108,7 +108,7 @@ class Collection<T> {
|
||||
|
||||
const middle = Math.floor((items.length - 1) / 2)
|
||||
if ( items.length % 2 ) return items[middle]
|
||||
else return (items[middle] + items[middle + 1]) / 2;
|
||||
else return (items[middle] + items[middle + 1]) / 2
|
||||
}
|
||||
|
||||
mode<T2>(key?: KeyOperator<T, T2>): number {
|
||||
@ -121,7 +121,7 @@ class Collection<T> {
|
||||
counts[item] = (counts[item] ?? -1) + 1
|
||||
}
|
||||
|
||||
return Math.max(...Object.values(counts).map(Number))
|
||||
return Number(Object.keys(counts).reduce((a, b) => counts[a] > counts[b] ? a : b)[0])
|
||||
}
|
||||
|
||||
collapse(): Collection<any> {
|
||||
@ -153,6 +153,10 @@ class Collection<T> {
|
||||
|
||||
// TODO crossJoin
|
||||
|
||||
clone(): Collection<T> {
|
||||
return new Collection<T>(this._items)
|
||||
}
|
||||
|
||||
diff<T2>(items: CollectionComparable<T|T2>): Collection<T> {
|
||||
const exclude = items instanceof Collection ? items.all() : items
|
||||
const matches = []
|
||||
@ -172,12 +176,20 @@ class Collection<T> {
|
||||
return new Collection(matches)
|
||||
}
|
||||
|
||||
each<T2>(func: KeyFunction<T, T2>): Collection<T2> {
|
||||
return new Collection(this._items.map(func))
|
||||
some(func: (item: T) => boolean): boolean {
|
||||
return this._items.some(func)
|
||||
}
|
||||
|
||||
each<T2>(func: KeyFunction<T, T2>): void {
|
||||
this._items.map(func)
|
||||
}
|
||||
|
||||
map<T2>(func: KeyFunction<T, T2>): Collection<T2> {
|
||||
return this.each(func)
|
||||
const new_items: CollectionItem<T2>[] = []
|
||||
this.each(((item, index) => {
|
||||
new_items.push(func(item, index))
|
||||
}))
|
||||
return new Collection<T2>(new_items)
|
||||
}
|
||||
|
||||
every<T2>(func: KeyFunction<T, T2>): boolean {
|
||||
@ -396,16 +408,6 @@ class Collection<T> {
|
||||
return this
|
||||
}
|
||||
|
||||
pull(index: number, fallback: any): MaybeCollectionItem<T> {
|
||||
let value
|
||||
const new_items = []
|
||||
this._items.forEach((item, item_index) => {
|
||||
if ( item_index !== index ) new_items.push(item)
|
||||
else value = item
|
||||
})
|
||||
return value ?? fallback
|
||||
}
|
||||
|
||||
put(index: number, item: CollectionItem<T>): Collection<T> {
|
||||
const new_items = []
|
||||
let inserted = false
|
||||
@ -582,7 +584,7 @@ class Collection<T> {
|
||||
return {
|
||||
current_index: 0,
|
||||
next() {
|
||||
if ( items.length < 1 || this.current_index + 1 >= items.length ) {
|
||||
if ( items.length < 1 || this.current_index + 1 > items.length ) {
|
||||
return { done: true }
|
||||
}
|
||||
|
||||
|
62
lib/src/collection/Iterable.ts
Normal file
62
lib/src/collection/Iterable.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import {Collection} from './Collection.ts'
|
||||
|
||||
export type MaybeIterationItem<T> = { done: boolean, value?: T }
|
||||
export type ChunkCallback<T> = (items: Collection<T>) => any
|
||||
|
||||
export class StopIteration extends Error {}
|
||||
|
||||
export abstract class Iterable<T> {
|
||||
protected index = 0
|
||||
|
||||
abstract async at_index(i: number): Promise<T>
|
||||
abstract async from_range(start: number, end: number): Promise<Collection<T>>
|
||||
abstract async count(): Promise<number>
|
||||
abstract clone(): Iterable<T>
|
||||
|
||||
public async next(): Promise<MaybeIterationItem<T>> {
|
||||
const i = this.index
|
||||
|
||||
if ( i >= await this.count() ) {
|
||||
return { done: true }
|
||||
}
|
||||
|
||||
this.index = i + 1
|
||||
return { done: false, value: await this.at_index(i) }
|
||||
}
|
||||
|
||||
[Symbol.asyncIterator]() {
|
||||
return this
|
||||
}
|
||||
|
||||
public async chunk(size: number, callback: ChunkCallback<T>) {
|
||||
const total = await this.count()
|
||||
|
||||
while ( this.index < total ) {
|
||||
const items = await this.from_range(this.index, this.index + size - 1)
|
||||
|
||||
try {
|
||||
await callback(items)
|
||||
} catch ( error ) {
|
||||
if ( error instanceof StopIteration ) break
|
||||
else throw error
|
||||
}
|
||||
|
||||
this.index += size
|
||||
}
|
||||
}
|
||||
|
||||
public async seek(index: number) {
|
||||
if ( index < 0 ) throw new TypeError('Cannot seek to negative index.')
|
||||
else if ( index >= await this.count() ) throw new TypeError('Cannot seek past last item.')
|
||||
this.index = index
|
||||
}
|
||||
|
||||
public async peek(): Promise<T | undefined> {
|
||||
if ( this.index + 1 >= await this.count() ) return undefined
|
||||
else return this.at_index(this.index + 1)
|
||||
}
|
||||
|
||||
public async reset() {
|
||||
this.index = 0
|
||||
}
|
||||
}
|
@ -1,15 +1,17 @@
|
||||
const STATUS_STOPPED = Symbol('status stopped')
|
||||
const STATUS_STARTING = Symbol('status starting')
|
||||
const STATUS_RUNNING = Symbol('status running')
|
||||
const STATUS_STOPPING = Symbol('status stopping')
|
||||
const STATUS_ERROR = Symbol('status error')
|
||||
enum Status {
|
||||
Stopped = 'stopped',
|
||||
Starting = 'starting',
|
||||
Running = 'running',
|
||||
Stopping = 'stopping',
|
||||
Error = 'error',
|
||||
}
|
||||
|
||||
const isStatus = (something: any) => [
|
||||
STATUS_STOPPED,
|
||||
STATUS_STARTING,
|
||||
STATUS_RUNNING,
|
||||
STATUS_STOPPING,
|
||||
STATUS_ERROR,
|
||||
Status.Stopped,
|
||||
Status.Starting,
|
||||
Status.Running,
|
||||
Status.Stopping,
|
||||
Status.Error,
|
||||
].includes(something)
|
||||
|
||||
export { STATUS_STOPPED, STATUS_STARTING, STATUS_RUNNING, STATUS_STOPPING, STATUS_ERROR, isStatus }
|
||||
export { Status, isStatus }
|
||||
|
39
lib/src/error/RunLevelErrorHandler.ts
Normal file
39
lib/src/error/RunLevelErrorHandler.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { red, bgRed } from '../external/std.ts'
|
||||
import { Service } from '../../../di/src/decorator/Service.ts'
|
||||
import { Logging } from '../service/logging/Logging.ts'
|
||||
|
||||
@Service()
|
||||
export default class RunLevelErrorHandler {
|
||||
constructor(
|
||||
protected logger: Logging,
|
||||
) {}
|
||||
|
||||
get handle(): (e: Error) => void {
|
||||
return (e: Error) => {
|
||||
this.display(e)
|
||||
Deno.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
display(e: Error) {
|
||||
try {
|
||||
const error_string = `RunLevelErrorHandler invoked:
|
||||
|
||||
${bgRed(' ')}
|
||||
${bgRed(' UNCAUGHT TOP-LEVEL ERROR ')}
|
||||
${bgRed(' ')}
|
||||
|
||||
${e.constructor ? e.constructor.name : e.name}
|
||||
${red(`---------------------------------------------------`)}
|
||||
${e.stack}
|
||||
`
|
||||
this.logger.error(error_string, true)
|
||||
} catch (display_e) {
|
||||
// The error display encountered an error...
|
||||
// just throw the original so it makes it out
|
||||
console.error('RunLevelErrorHandler encountered an error:', display_e.message)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
}
|
1
lib/src/external/db.ts
vendored
Normal file
1
lib/src/external/db.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export * from 'https://deno.land/x/postgres/mod.ts'
|
2
lib/src/external/http.ts
vendored
Normal file
2
lib/src/external/http.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export * from 'https://deno.land/std@0.53.0/http/server.ts'
|
||||
export * from 'https://deno.land/std@0.53.0/http/cookie.ts'
|
1
lib/src/external/postgres.ts
vendored
Normal file
1
lib/src/external/postgres.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export * from 'https://deno.land/x/postgres/mod.ts'
|
5
lib/src/external/std.ts
vendored
5
lib/src/external/std.ts
vendored
@ -1,3 +1,4 @@
|
||||
export { serve } from 'https://deno.land/std/http/server.ts'
|
||||
export * from 'https://deno.land/std/fmt/colors.ts'
|
||||
export * from 'https://deno.land/std@0.53.0/fmt/colors.ts'
|
||||
export { config as dotenv } from 'https://deno.land/x/dotenv/mod.ts'
|
||||
export * as path from 'https://deno.land/std@0.53.0/path/mod.ts'
|
||||
export * as fs from 'https://deno.land/std@0.53.0/fs/mod.ts'
|
||||
|
4
lib/src/http/Controller.ts
Normal file
4
lib/src/http/Controller.ts
Normal file
@ -0,0 +1,4 @@
|
||||
|
||||
export default class Controller {
|
||||
|
||||
}
|
69
lib/src/http/CookieJar.ts
Normal file
69
lib/src/http/CookieJar.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { Injectable } from '../../../di/src/decorator/Injection.ts'
|
||||
import { getCookies, setCookie, delCookie, ServerRequest } from '../external/http.ts'
|
||||
import { InMemCache } from '../support/InMemCache.ts'
|
||||
import { HTTPRequest } from './type/HTTPRequest.ts'
|
||||
|
||||
export interface Cookie {
|
||||
key: string,
|
||||
original_value: string,
|
||||
value: any,
|
||||
}
|
||||
|
||||
export type MaybeCookie = Cookie | undefined
|
||||
|
||||
// TODO cookie options (http only, expires, &c.)
|
||||
@Injectable()
|
||||
export class CookieJar {
|
||||
protected _parsed: { [key: string]: string } = {}
|
||||
protected _cache = new InMemCache()
|
||||
|
||||
constructor(
|
||||
protected request: HTTPRequest,
|
||||
) {
|
||||
this._parsed = getCookies(this.request.to_native)
|
||||
}
|
||||
|
||||
public async get(key: string): Promise<MaybeCookie> {
|
||||
// Try the cache
|
||||
if ( await this._cache.has(key) )
|
||||
return this._cache.fetch(key)
|
||||
|
||||
// If the cache missed, try to parse it and load in cache
|
||||
if ( key in this._parsed ) {
|
||||
let value = this._parsed[key]
|
||||
try {
|
||||
value = JSON.parse(atob(this._parsed[key]))
|
||||
} catch(e) {}
|
||||
|
||||
const cookie = {
|
||||
key,
|
||||
value,
|
||||
original_value: this._parsed[key],
|
||||
}
|
||||
|
||||
await this._cache.put(key, cookie)
|
||||
return cookie
|
||||
}
|
||||
}
|
||||
|
||||
public async set(key: string, value: any): Promise<void> {
|
||||
const original_value = btoa(JSON.stringify(value))
|
||||
const cookie = {
|
||||
key,
|
||||
value,
|
||||
original_value,
|
||||
}
|
||||
|
||||
await this._cache.put(key, value)
|
||||
setCookie(this.request.response, { name: key, value: original_value })
|
||||
}
|
||||
|
||||
public async has(key: string): Promise<boolean> {
|
||||
return (await this._cache.has(key)) || key in this._parsed
|
||||
}
|
||||
|
||||
public async delete(key: string): Promise<void> {
|
||||
await this._cache.drop(key)
|
||||
delCookie(this.request.response, key)
|
||||
}
|
||||
}
|
3
lib/src/http/Middleware.ts
Normal file
3
lib/src/http/Middleware.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export class Middleware {
|
||||
|
||||
}
|
115
lib/src/http/Request.ts
Normal file
115
lib/src/http/Request.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import { ServerRequest } from '../external/http.ts'
|
||||
import { HTTPProtocol, HTTPRequest, RemoteHost } from './type/HTTPRequest.ts'
|
||||
import { Response } from './Response.ts'
|
||||
import { HTTPResponse } from './type/HTTPResponse.ts'
|
||||
import Utility from '../service/utility/Utility.ts'
|
||||
import { Injectable } from '../../../di/src/decorator/Injection.ts'
|
||||
|
||||
@Injectable()
|
||||
export class Request implements HTTPRequest {
|
||||
public readonly response: HTTPResponse
|
||||
private readonly _deno_req: ServerRequest
|
||||
private _body: any
|
||||
private _query: { [key: string]: any } = {}
|
||||
|
||||
public readonly url: string
|
||||
public readonly method: string
|
||||
public readonly protocol: HTTPProtocol
|
||||
public readonly connection: Deno.Conn
|
||||
public readonly secure: boolean = false
|
||||
|
||||
public get headers() {
|
||||
return this._deno_req.headers
|
||||
}
|
||||
|
||||
get to_native(): ServerRequest {
|
||||
return this._deno_req
|
||||
}
|
||||
|
||||
get cookies() {
|
||||
return this.response.cookies
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected utility: Utility,
|
||||
from: ServerRequest
|
||||
) {
|
||||
this._deno_req = from
|
||||
this.url = this._deno_req.url
|
||||
this.method = this._deno_req.method.toLowerCase()
|
||||
this.protocol = {
|
||||
string: this._deno_req.proto,
|
||||
major: this._deno_req.protoMajor,
|
||||
minor: this._deno_req.protoMinor,
|
||||
}
|
||||
this.connection = this._deno_req.conn
|
||||
this.response = new Response(this)
|
||||
}
|
||||
|
||||
public async prepare() {
|
||||
this._body = await Deno.readAll(this._deno_req.body)
|
||||
|
||||
const url_params = new URLSearchParams(this.url.substr(1))
|
||||
const param_obj = Object.fromEntries(url_params)
|
||||
const params: any = {}
|
||||
for ( const key in param_obj ) {
|
||||
if ( !param_obj.hasOwnProperty(key) ) continue
|
||||
if ( param_obj[key] === '' ) params[key] = true
|
||||
else params[key] = this.utility.infer(param_obj[key])
|
||||
}
|
||||
|
||||
this._query = params
|
||||
}
|
||||
|
||||
respond(res: any) {
|
||||
return this._deno_req.respond(res)
|
||||
}
|
||||
|
||||
// public body: RequestBody = {}
|
||||
// public original_body: RequestBody = {}
|
||||
|
||||
get remote() {
|
||||
return this.connection.remoteAddr as RemoteHost
|
||||
}
|
||||
|
||||
get body() {
|
||||
return this._body
|
||||
}
|
||||
|
||||
get query() {
|
||||
return this._query
|
||||
}
|
||||
|
||||
get hostname() {
|
||||
return this.headers.get('host')?.split(':')[0]
|
||||
}
|
||||
|
||||
get path() {
|
||||
return this.url.split('?')[0]
|
||||
}
|
||||
|
||||
get xhr() {
|
||||
return this.headers.get('x-requested-with')?.toLowerCase() === 'xmlhttprequest'
|
||||
}
|
||||
|
||||
/*
|
||||
body
|
||||
fresh/stale - cache
|
||||
remote ips (proxy)
|
||||
params
|
||||
route?
|
||||
signedCookies
|
||||
*/
|
||||
|
||||
/*
|
||||
accepts content type
|
||||
accepts charsets
|
||||
accepts encodings
|
||||
accepts languages
|
||||
get header
|
||||
is content type
|
||||
get param with default value
|
||||
get input with default value
|
||||
range header parser
|
||||
*/
|
||||
}
|
30
lib/src/http/Response.ts
Normal file
30
lib/src/http/Response.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { HTTPResponse } from './type/HTTPResponse.ts'
|
||||
import { HTTPRequest } from './type/HTTPRequest.ts'
|
||||
import { ServerRequest } from '../external/http.ts'
|
||||
import {CookieJar} from "./CookieJar.ts";
|
||||
|
||||
export class Response implements HTTPResponse {
|
||||
public status = 200
|
||||
public headers = new Headers()
|
||||
public body = ''
|
||||
public readonly cookies: CookieJar
|
||||
|
||||
private readonly _deno_req: ServerRequest
|
||||
private readonly _request: HTTPRequest
|
||||
|
||||
private _sent = false
|
||||
get sent() {
|
||||
return this._sent
|
||||
}
|
||||
|
||||
constructor(to: HTTPRequest) {
|
||||
this._deno_req = to.to_native
|
||||
this._request = to
|
||||
this.cookies = new CookieJar(to)
|
||||
}
|
||||
|
||||
send() {
|
||||
this._sent = true
|
||||
return this._deno_req.respond(this)
|
||||
}
|
||||
}
|
6
lib/src/http/SecureRequest.ts
Normal file
6
lib/src/http/SecureRequest.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { HTTPRequest } from './type/HTTPRequest.ts'
|
||||
import { Request } from './Request.ts'
|
||||
|
||||
export default class SecureRequest extends Request implements HTTPRequest {
|
||||
public readonly secure: boolean = true
|
||||
}
|
30
lib/src/http/type/HTTPRequest.ts
Normal file
30
lib/src/http/type/HTTPRequest.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { ServerRequest } from '../../external/http.ts'
|
||||
import {HTTPResponse} from "./HTTPResponse.ts";
|
||||
|
||||
export interface HTTPProtocol {
|
||||
string: string,
|
||||
major: number,
|
||||
minor: number,
|
||||
}
|
||||
|
||||
export interface RemoteHost {
|
||||
hostname: string,
|
||||
port: number,
|
||||
transport: string,
|
||||
}
|
||||
|
||||
export interface HTTPRequest {
|
||||
url: string
|
||||
method: string
|
||||
protocol: HTTPProtocol
|
||||
headers: Headers
|
||||
connection: Deno.Conn
|
||||
response: HTTPResponse
|
||||
to_native: ServerRequest
|
||||
|
||||
remote: RemoteHost
|
||||
body: any
|
||||
query: any
|
||||
hostname: string | undefined
|
||||
secure: boolean
|
||||
}
|
12
lib/src/http/type/HTTPResponse.ts
Normal file
12
lib/src/http/type/HTTPResponse.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import {CookieJar} from '../CookieJar.ts'
|
||||
|
||||
export interface HTTPResponse {
|
||||
status: number
|
||||
headers: Headers
|
||||
body: Uint8Array | Deno.Reader | string
|
||||
trailers?: () => Promise<Headers> | Headers
|
||||
sent: boolean
|
||||
cookies: CookieJar,
|
||||
|
||||
send: () => void
|
||||
}
|
43
lib/src/lifecycle/Application.ts
Normal file
43
lib/src/lifecycle/Application.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { Service } from '../../../di/src/decorator/Service.ts'
|
||||
import { Logging } from '../service/logging/Logging.ts'
|
||||
import Unit from './Unit.ts'
|
||||
import { container, make } from '../../../di/src/global.ts'
|
||||
import { DependencyKey } from '../../../di/src/type/DependencyKey.ts'
|
||||
import RunLevelErrorHandler from '../error/RunLevelErrorHandler.ts'
|
||||
|
||||
@Service()
|
||||
export default class Application {
|
||||
constructor(
|
||||
protected logger: Logging,
|
||||
protected rleh: RunLevelErrorHandler,
|
||||
protected units: Unit[],
|
||||
) {}
|
||||
|
||||
make(token: DependencyKey) {
|
||||
return make(token)
|
||||
}
|
||||
|
||||
container() {
|
||||
return container
|
||||
}
|
||||
|
||||
async up() {
|
||||
|
||||
}
|
||||
|
||||
async down() {
|
||||
|
||||
}
|
||||
|
||||
async run() {
|
||||
try {
|
||||
this.logger.info('Starting Daton...')
|
||||
} catch (e) {
|
||||
await this.app_error(e)
|
||||
}
|
||||
}
|
||||
|
||||
async app_error(e: Error) {
|
||||
this.rleh.handle(e)
|
||||
}
|
||||
}
|
@ -1,19 +1,42 @@
|
||||
import { STATUS_STOPPED, isStatus } from '../const/status.ts'
|
||||
import { Status, isStatus } from '../const/status.ts'
|
||||
import { Unit } from './decorators.ts'
|
||||
import { Collection } from '../collection/Collection.ts'
|
||||
import {container, make} from '../../../di/src/global.ts'
|
||||
import {DependencyKey} from "../../../di/src/type/DependencyKey.ts";
|
||||
import Instantiable, {isInstantiable} from "../../../di/src/type/Instantiable.ts";
|
||||
|
||||
const isLifecycleUnit = (something: any): something is (typeof LifecycleUnit) => {
|
||||
return isInstantiable(something) && something.prototype instanceof LifecycleUnit
|
||||
}
|
||||
|
||||
export default abstract class LifecycleUnit {
|
||||
private _status = STATUS_STOPPED
|
||||
private _status = Status.Stopped
|
||||
|
||||
public get status() {
|
||||
return this._status
|
||||
}
|
||||
|
||||
public set status(status) {
|
||||
if ( !isStatus(status) )
|
||||
throw new TypeError('Invalid unit status: '+status.description)
|
||||
|
||||
this._status = status
|
||||
}
|
||||
|
||||
public async up(): Promise<void> {};
|
||||
public async down(): Promise<void> {};
|
||||
|
||||
public static get_dependencies(): Collection<typeof LifecycleUnit> {
|
||||
if ( isInstantiable(this) ) {
|
||||
const deps = new Collection<typeof LifecycleUnit>()
|
||||
for ( const dependency of container.get_dependencies(this) ) {
|
||||
if ( isLifecycleUnit(dependency) )
|
||||
deps.push(dependency)
|
||||
}
|
||||
|
||||
return deps
|
||||
}
|
||||
return new Collection<typeof LifecycleUnit>()
|
||||
}
|
||||
|
||||
protected make<T>(target: Instantiable<T>|DependencyKey, ...parameters: any[]) {
|
||||
return make(target, ...parameters)
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,11 @@ class Logging {
|
||||
const message = this.build_message(level, output)
|
||||
if ( this._level >= level || force ) {
|
||||
for ( const logger of this._loggers ) {
|
||||
logger.write(message)
|
||||
try {
|
||||
logger.write(message)
|
||||
} catch (e) {
|
||||
console.error('logging error', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
5
lib/src/service/logging/global.ts
Normal file
5
lib/src/service/logging/global.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { make } from '../../../../di/src/global.ts'
|
||||
import { Logging } from './Logging.ts'
|
||||
|
||||
const logger = make(Logging)
|
||||
export { logger }
|
@ -1,11 +1,11 @@
|
||||
enum LoggingLevel {
|
||||
Silent = 0,
|
||||
Success = 1,
|
||||
Error = 1,
|
||||
Warning = 2,
|
||||
Info = 3,
|
||||
Debug = 4,
|
||||
Verbose = 5,
|
||||
Error = 2,
|
||||
Warning = 3,
|
||||
Info = 4,
|
||||
Debug = 5,
|
||||
Verbose = 6,
|
||||
}
|
||||
|
||||
const isLoggingLevel = (something: any): something is LoggingLevel => {
|
||||
|
55
lib/src/service/utility/Utility.ts
Normal file
55
lib/src/service/utility/Utility.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { Service } from '../../../../di/src/decorator/Service.ts'
|
||||
import { Logging } from '../logging/Logging.ts'
|
||||
|
||||
@Service()
|
||||
export default class Utility {
|
||||
constructor(
|
||||
protected logger: Logging
|
||||
) {}
|
||||
|
||||
deep_copy<T>(target: T): T {
|
||||
if ( target === null )
|
||||
return target
|
||||
|
||||
if ( target instanceof Date )
|
||||
return new Date(target.getTime()) as any
|
||||
|
||||
if ( target instanceof Array ) {
|
||||
const copy = [] as any[]
|
||||
(target as any[]).forEach(item => { copy.push(item) })
|
||||
return copy.map((item: any) => this.deep_copy<any>(item)) as any
|
||||
}
|
||||
|
||||
if ( typeof target === 'object' && target !== {} ) {
|
||||
const copy = { ...(target as {[key: string]: any }) } as { [key: string]: any }
|
||||
Object.keys(copy).forEach(key => {
|
||||
copy[key] = this.deep_copy<any>(copy[key])
|
||||
})
|
||||
}
|
||||
|
||||
return target
|
||||
}
|
||||
|
||||
// TODO deep_merge
|
||||
|
||||
infer(val: string): any {
|
||||
if ( !val ) return undefined
|
||||
else if ( val.toLowerCase() === 'true' ) return true
|
||||
else if ( val.toLowerCase() === 'false' ) return false
|
||||
else if ( !isNaN(Number(val)) ) return Number(val)
|
||||
else if ( this.is_json(val) ) return JSON.parse(val)
|
||||
else if ( val.toLowerCase() === 'null' ) return null
|
||||
else if ( val.toLowerCase() === 'undefined' ) return undefined
|
||||
else return val
|
||||
}
|
||||
|
||||
is_json(val: string): boolean {
|
||||
try {
|
||||
JSON.parse(val)
|
||||
return true
|
||||
} catch(e) {
|
||||
this.logger.verbose(`Error encountered while checking is_json. Might be invalid. Error: ${e.message}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
6
lib/src/support/Cache.ts
Normal file
6
lib/src/support/Cache.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export default abstract class Cache {
|
||||
public abstract async fetch(key: string): Promise<any>;
|
||||
public abstract async put(key: string, value: any): Promise<void>;
|
||||
public abstract async has(key: string): Promise<boolean>;
|
||||
public abstract async drop(key: string): Promise<void>;
|
||||
}
|
25
lib/src/support/CacheFactory.ts
Normal file
25
lib/src/support/CacheFactory.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import AbstractFactory from '../../../di/src/factory/AbstractFactory.ts'
|
||||
import Cache from './Cache.ts'
|
||||
import { InMemCache } from './InMemCache.ts'
|
||||
import {DependencyRequirement} from '../../../di/src/type/DependencyRequirement.ts'
|
||||
import {Collection} from '../collection/Collection.ts'
|
||||
|
||||
// TODO add support for configurable Cache backends
|
||||
|
||||
export default class CacheFactory extends AbstractFactory {
|
||||
constructor() {
|
||||
super({})
|
||||
}
|
||||
|
||||
produce(dependencies: any[], parameters: any[]): Cache {
|
||||
return new InMemCache()
|
||||
}
|
||||
|
||||
match(something: any) {
|
||||
return something === Cache
|
||||
}
|
||||
|
||||
get_dependency_keys(): Collection<DependencyRequirement> {
|
||||
return new Collection<DependencyRequirement>()
|
||||
}
|
||||
}
|
30
lib/src/support/InMemCache.ts
Normal file
30
lib/src/support/InMemCache.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import Cache from './Cache.ts'
|
||||
import { Collection } from '../collection/Collection.ts'
|
||||
|
||||
export interface InMemCacheItem {
|
||||
key: string,
|
||||
item: any,
|
||||
}
|
||||
|
||||
export class InMemCache extends Cache {
|
||||
protected items: Collection<InMemCacheItem> = new Collection<InMemCacheItem>()
|
||||
|
||||
public async fetch(key: string) {
|
||||
const item = this.items.firstWhere('key', '=', key)
|
||||
if ( item ) return item.item
|
||||
}
|
||||
|
||||
public async put(key: string, item: any) {
|
||||
const existing = this.items.firstWhere('key', '=', key)
|
||||
if ( existing ) existing.item = item
|
||||
else this.items.push({ key, item })
|
||||
}
|
||||
|
||||
public async has(key: string) {
|
||||
return this.items.where('key', '=', key).length > 0
|
||||
}
|
||||
|
||||
public async drop(key: string) {
|
||||
this.items = this.items.whereNot('key', '=', key)
|
||||
}
|
||||
}
|
9
lib/src/support/mixins.ts
Normal file
9
lib/src/support/mixins.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export function applyMixins(derivedCtor: any, baseCtors: any[]) {
|
||||
baseCtors.forEach(baseCtor => {
|
||||
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
|
||||
const desc = Object.getOwnPropertyDescriptor(baseCtor.prototype, name)
|
||||
if ( typeof desc !== 'undefined' )
|
||||
Object.defineProperty(derivedCtor.prototype, name, desc)
|
||||
})
|
||||
})
|
||||
}
|
35
lib/src/unit/Canon.ts
Normal file
35
lib/src/unit/Canon.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import {Service} from '../../../di/src/decorator/Service.ts'
|
||||
|
||||
export type CanonicalResolver = (key: string) => any
|
||||
|
||||
export class DuplicateResolverKeyError extends Error {
|
||||
constructor(key: string) {
|
||||
super(`There is already a canonical resource with the scope ${key} registered.`)
|
||||
}
|
||||
}
|
||||
|
||||
@Service()
|
||||
export class Canon {
|
||||
protected resources: { [key: string]: any } = {}
|
||||
|
||||
get(key: string): any {
|
||||
const key_parts = key.split('::')
|
||||
let desc_value = this.resources
|
||||
key_parts.forEach(part => {
|
||||
if ( typeof desc_value === 'function' ) {
|
||||
desc_value = desc_value(part)
|
||||
} else {
|
||||
desc_value = desc_value[part]
|
||||
}
|
||||
})
|
||||
return desc_value
|
||||
}
|
||||
|
||||
register_resource(scope: string, resolver: CanonicalResolver) {
|
||||
if ( this.resources[scope] ) {
|
||||
throw new DuplicateResolverKeyError(scope)
|
||||
}
|
||||
|
||||
this.resources[scope] = resolver
|
||||
}
|
||||
}
|
58
lib/src/unit/Canonical.ts
Normal file
58
lib/src/unit/Canonical.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import LifecycleUnit from '../lifecycle/Unit.ts'
|
||||
import {fs, path} from '../external/std.ts'
|
||||
import {Canon} from './Canon.ts'
|
||||
|
||||
export interface CanonicalDefinition {
|
||||
canonical_name: string,
|
||||
original_name: string,
|
||||
imported: any,
|
||||
}
|
||||
|
||||
export class Canonical extends LifecycleUnit {
|
||||
protected base_path: string = '.'
|
||||
protected suffix: string = '.ts'
|
||||
protected canonical_item: string = ''
|
||||
protected _items: { [key: string]: any } = {}
|
||||
|
||||
public get path(): string {
|
||||
return path.resolve(this.base_path)
|
||||
}
|
||||
|
||||
public get canonical_items() {
|
||||
return `${this.canonical_item}s`
|
||||
}
|
||||
|
||||
public async up() {
|
||||
for await ( const entry of fs.walk(this.path) ) {
|
||||
if ( !entry.isFile || !entry.path.endsWith(this.suffix) ) continue
|
||||
const def = await this._get_canonical_definition(entry.path)
|
||||
this._items[def.canonical_name] = await this.init_canonical_item(def)
|
||||
}
|
||||
|
||||
this.make(Canon).register_resource(this.canonical_items, (key: string) => this.get(key))
|
||||
}
|
||||
|
||||
public async init_canonical_item(definition: CanonicalDefinition): Promise<any> {
|
||||
return definition.imported.default
|
||||
}
|
||||
|
||||
private async _get_canonical_definition(file_path: string): Promise<CanonicalDefinition> {
|
||||
const original_name = file_path.replace(this.path, '').substr(1)
|
||||
const path_regex = new RegExp(path.SEP, 'g')
|
||||
const canonical_name = original_name.replace(path_regex, ':')
|
||||
.split('').reverse().join('')
|
||||
.substr(this.suffix.length)
|
||||
.split('').reverse().join('')
|
||||
const imported = await import(file_path)
|
||||
return { canonical_name, original_name, imported }
|
||||
}
|
||||
|
||||
public get(key: string): any {
|
||||
const key_parts = key.split('.')
|
||||
let desc_value = this._items
|
||||
key_parts.forEach(part => {
|
||||
desc_value = desc_value[part]
|
||||
})
|
||||
return desc_value
|
||||
}
|
||||
}
|
9
lib/src/unit/Config.ts
Normal file
9
lib/src/unit/Config.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import {Canonical} from './Canonical.ts'
|
||||
import { Unit } from '../lifecycle/decorators.ts'
|
||||
|
||||
@Unit()
|
||||
export default class Config extends Canonical {
|
||||
protected base_path = './app/configs'
|
||||
protected suffix = '.config.ts'
|
||||
protected canonical_item = 'config'
|
||||
}
|
20
lib/src/unit/Controllers.ts
Normal file
20
lib/src/unit/Controllers.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { InstantiableCanonical } from './InstantiableCanonical.ts'
|
||||
import { CanonicalDefinition } from './Canonical.ts'
|
||||
import Controller from '../http/Controller.ts'
|
||||
import { Unit } from '../lifecycle/decorators.ts'
|
||||
|
||||
@Unit()
|
||||
export default class Controllers extends InstantiableCanonical {
|
||||
protected base_path = './app/http/controllers'
|
||||
protected canonical_item = 'controller'
|
||||
protected suffix = '.controller.ts'
|
||||
|
||||
public async init_canonical_item(def: CanonicalDefinition) {
|
||||
const item = await super.init_canonical_item(def)
|
||||
if ( !(item instanceof Controller) ) {
|
||||
throw new TypeError(`Invalid controller definition: ${def.original_name}. Controllers must extend from Daton's base Controller class.`)
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
}
|
18
lib/src/unit/InstantiableCanonical.ts
Normal file
18
lib/src/unit/InstantiableCanonical.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import {Canonical, CanonicalDefinition} from './Canonical.ts'
|
||||
import {isInstantiable} from '../../../di/src/type/Instantiable.ts'
|
||||
|
||||
export class InvalidCanonicalExportError extends Error {
|
||||
constructor(name: string) {
|
||||
super(`Unable to import canonical item from "${name}". The default export of this file is invalid.`)
|
||||
}
|
||||
}
|
||||
|
||||
export class InstantiableCanonical extends Canonical {
|
||||
public async init_canonical_item(def: CanonicalDefinition) {
|
||||
if ( isInstantiable(def.imported.default) ) {
|
||||
return this.make(def.imported.default)
|
||||
}
|
||||
|
||||
throw new InvalidCanonicalExportError(def.original_name)
|
||||
}
|
||||
}
|
20
lib/src/unit/Middlewares.ts
Normal file
20
lib/src/unit/Middlewares.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { InstantiableCanonical } from './InstantiableCanonical.ts'
|
||||
import { CanonicalDefinition } from './Canonical.ts'
|
||||
import { Middleware } from '../http/Middleware.ts'
|
||||
import { Unit } from '../lifecycle/decorators.ts'
|
||||
|
||||
@Unit()
|
||||
export default class Middlewares extends InstantiableCanonical {
|
||||
protected base_path = './app/http/middleware'
|
||||
protected canonical_item = 'middleware'
|
||||
protected suffix = '.middleware.ts'
|
||||
|
||||
public async init_canonical_item(def: CanonicalDefinition) {
|
||||
const item = await super.init_canonical_item(def)
|
||||
if ( !(item instanceof Middleware) ) {
|
||||
throw new TypeError(`Invalid middleware definition: ${def.original_name}. Middleware must extend from Daton's base Middleware class.`)
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
}
|
@ -1,30 +1,55 @@
|
||||
import LifecycleUnit from '../lifecycle/Unit.ts'
|
||||
import { Unit } from '../lifecycle/decorators.ts'
|
||||
import { dotenv } from '../external/std.ts'
|
||||
import { Logging } from '../service/logging/Logging.ts'
|
||||
import StandardLogger from '../service/logging/StandardLogger.ts'
|
||||
import { isLoggingLevel } from '../service/logging/types.ts'
|
||||
import Utility from '../service/utility/Utility.ts'
|
||||
import { make } from '../../../di/src/global.ts'
|
||||
import 'https://deno.land/x/dotenv/load.ts'
|
||||
import { Container } from '../../../di/src/Container.ts'
|
||||
import { Inject } from '../../../di/src/decorator/Injection.ts'
|
||||
import CacheFactory from "../support/CacheFactory.ts";
|
||||
|
||||
const env = (name: string, fallback: any) => {
|
||||
const scaffolding = make(Scaffolding)
|
||||
return scaffolding.env(name) ?? fallback
|
||||
}
|
||||
|
||||
export { env }
|
||||
|
||||
@Unit()
|
||||
export default class Scaffolding extends LifecycleUnit {
|
||||
private config = {}
|
||||
|
||||
constructor(
|
||||
protected logger: Logging
|
||||
) {
|
||||
super()
|
||||
}
|
||||
protected logger: Logging,
|
||||
protected utility: Utility,
|
||||
@Inject('injector') protected injector: Container,
|
||||
) { super() }
|
||||
|
||||
public refresh_env() {
|
||||
this.config = dotenv()
|
||||
}
|
||||
|
||||
public setup_logging() {
|
||||
StandardLogger.register()
|
||||
this.logger.info('Logging initialized.', true)
|
||||
public env(name: string) {
|
||||
return this.utility.infer(Deno.env.get(name) ?? '')
|
||||
}
|
||||
|
||||
public async up() {
|
||||
this.setup_logging()
|
||||
this.refresh_env()
|
||||
}
|
||||
|
||||
public setup_logging() {
|
||||
StandardLogger.register()
|
||||
|
||||
try {
|
||||
this.logger.verbose('Attempting to load logging level from the environment')
|
||||
const env_level = this.env('DATON_LOGGING_LEVEL')
|
||||
this.logger.verbose(`Read logging level: ${env_level}`)
|
||||
if ( isLoggingLevel(env_level) ) {
|
||||
this.logger.verbose('Logging level is valid.')
|
||||
this.logger.level = env_level
|
||||
this.logger.debug(`Set logging level from environment: ${env_level}`)
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
this.logger.info('Logging initialized.', true)
|
||||
|
||||
this.logger.verbose('Adding the cache production factory to the container...')
|
||||
this.injector.register_factory(new CacheFactory())
|
||||
}
|
||||
}
|
||||
|
50
orm/src/builder/Builder.ts
Normal file
50
orm/src/builder/Builder.ts
Normal 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')
|
||||
}
|
||||
}
|
5
orm/src/builder/RawValue.ts
Normal file
5
orm/src/builder/RawValue.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default class RawValue {
|
||||
constructor(
|
||||
public readonly value: string
|
||||
) {}
|
||||
}
|
36
orm/src/builder/Statement.ts
Normal file
36
orm/src/builder/Statement.ts
Normal 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('')
|
||||
}
|
||||
}
|
89
orm/src/builder/type/ConnectionExecutable.ts
Normal file
89
orm/src/builder/type/ConnectionExecutable.ts
Normal 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)
|
||||
}
|
||||
}
|
37
orm/src/builder/type/ConnectionMutable.ts
Normal file
37
orm/src/builder/type/ConnectionMutable.ts
Normal 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
|
||||
}
|
||||
|
||||
}
|
107
orm/src/builder/type/HavingBuilder.ts
Normal file
107
orm/src/builder/type/HavingBuilder.ts
Normal 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
|
||||
}
|
||||
}
|
118
orm/src/builder/type/Insert.ts
Normal file
118
orm/src/builder/type/Insert.ts
Normal 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])
|
204
orm/src/builder/type/Select.ts
Normal file
204
orm/src/builder/type/Select.ts
Normal 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])
|
41
orm/src/builder/type/TableRefBuilder.ts
Normal file
41
orm/src/builder/type/TableRefBuilder.ts
Normal 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)
|
||||
}
|
||||
}
|
88
orm/src/builder/type/Update.ts
Normal file
88
orm/src/builder/type/Update.ts
Normal 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])
|
100
orm/src/builder/type/WhereBuilder.ts
Normal file
100
orm/src/builder/type/WhereBuilder.ts
Normal 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
|
||||
}
|
||||
}
|
13
orm/src/builder/type/join/CrossJoin.ts
Normal file
13
orm/src/builder/type/join/CrossJoin.ts
Normal 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}`)
|
||||
}
|
||||
}
|
6
orm/src/builder/type/join/FullOuterJoin.ts
Normal file
6
orm/src/builder/type/join/FullOuterJoin.ts
Normal 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'
|
||||
}
|
6
orm/src/builder/type/join/InnerJoin.ts
Normal file
6
orm/src/builder/type/join/InnerJoin.ts
Normal 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'
|
||||
}
|
25
orm/src/builder/type/join/Join.ts
Normal file
25
orm/src/builder/type/join/Join.ts
Normal 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])
|
6
orm/src/builder/type/join/LeftJoin.ts
Normal file
6
orm/src/builder/type/join/LeftJoin.ts
Normal 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'
|
||||
}
|
6
orm/src/builder/type/join/LeftOuterJoin.ts
Normal file
6
orm/src/builder/type/join/LeftOuterJoin.ts
Normal 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'
|
||||
}
|
6
orm/src/builder/type/join/RightJoin.ts
Normal file
6
orm/src/builder/type/join/RightJoin.ts
Normal 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'
|
||||
}
|
6
orm/src/builder/type/join/RightOuterJoin.ts
Normal file
6
orm/src/builder/type/join/RightOuterJoin.ts
Normal 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'
|
||||
}
|
14
orm/src/builder/type/result/ObjectResultOperator.ts
Normal file
14
orm/src/builder/type/result/ObjectResultOperator.ts
Normal 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
|
||||
}
|
||||
|
||||
}
|
22
orm/src/builder/type/result/ResultCollection.ts
Normal file
22
orm/src/builder/type/result/ResultCollection.ts
Normal 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))
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
26
orm/src/builder/type/result/ResultIterable.ts
Normal file
26
orm/src/builder/type/result/ResultIterable.ts
Normal 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)
|
||||
}
|
||||
}
|
8
orm/src/builder/type/result/ResultOperator.ts
Normal file
8
orm/src/builder/type/result/ResultOperator.ts
Normal 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
|
||||
|
||||
}
|
33
orm/src/builder/type/result/ResultSet.ts
Normal file
33
orm/src/builder/type/result/ResultSet.ts
Normal 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
102
orm/src/builder/types.ts
Normal 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
20
orm/src/db/Connection.ts
Normal 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>
|
||||
|
||||
}
|
50
orm/src/db/PostgresConnection.ts
Normal file
50
orm/src/db/PostgresConnection.ts
Normal 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
63
orm/src/db/types.ts
Normal 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
52
orm/src/model/Field.ts
Normal 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
356
orm/src/model/Model.ts
Normal 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}`
|
||||
}
|
||||
}
|
26
orm/src/model/ModelResultOperator.ts
Normal file
26
orm/src/model/ModelResultOperator.ts
Normal 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
0
orm/src/schema/Schema.ts
Normal file
37
orm/src/service/Database.ts
Normal file
37
orm/src/service/Database.ts
Normal 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]
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user