Start JSDocs

This commit is contained in:
2020-08-16 14:31:47 -05:00
parent 673fbc84f8
commit c2a7c3f914
29 changed files with 830 additions and 16 deletions

View File

@@ -1,8 +1,15 @@
import { Iterable } from './Iterable.ts'
import { collect } from './Collection.ts'
/**
* A basic Iterable implementation that uses an array as a backend.
* @extends Iterable
*/
export class ArrayIterable<T> extends Iterable<T> {
constructor(
/**
* Items to use for this iterable.
*/
protected items: T[],
) {
super()

View File

@@ -11,9 +11,21 @@ type AsyncCollectionComparable<T> = CollectionItem<T>[] | Collection<T> | AsyncC
type AsyncKeyFunction<T, T2> = (item: CollectionItem<T>, index: number) => CollectionItem<T2> | Promise<CollectionItem<T2>>
type AsyncCollectionFunction<T, T2> = (items: AsyncCollection<T>) => T2
/**
* Like a collection, but asynchronous.
*/
export class AsyncCollection<T> {
constructor(
/**
* Iterable of items to base this collction on.
* @type Iterable
*/
private _items: Iterable<T>,
/**
* Size to use when chunking results for memory-optimization.
* @type number
*/
private _chunk_size: number = 1000, // TODO fix this. It's just for testing
) {}
@@ -57,14 +69,27 @@ export class AsyncCollection<T> {
await this._items.reset()
}
/**
* Get all items in this collection as an array.
* @return Promise<array>
*/
async all(): Promise<CollectionItem<T>[]> {
return (await this._items.from_range(0, await this._items.count())).all()
}
/**
* Get all items in this collection as a synchronous Collection
* @return Promise<Collection>
*/
async collect(): Promise<Collection<T>> {
return this._items.from_range(0, await this._items.count())
}
/**
* Get the average value of the collection or one of its keys.
* @param {KeyOperator} key
* @return Promise<number>
*/
async average<T2>(key?: KeyOperator<T, T2>): Promise<number> {
let running_total = 0
let running_items = 0
@@ -82,6 +107,11 @@ export class AsyncCollection<T> {
return running_total / running_items
}
/**
* Get the median value of the collection or one of its keys.
* @param {KeyOperator} key
* @return Promise<number>
*/
async median<T2>(key?: KeyOperator<T, T2>): Promise<number> {
let items: number[] = []
@@ -100,6 +130,11 @@ export class AsyncCollection<T> {
else return (items[middle] + items[middle + 1]) / 2
}
/**
* Get the mode value of the collection or one of its keys.
* @param {KeyOperator} key
* @return Promise<number>
*/
async mode<T2>(key?: KeyOperator<T, T2>): Promise<number> {
let counts: any = {}
@@ -118,11 +153,24 @@ export class AsyncCollection<T> {
return Number(Object.keys(counts).reduce((a, b) => counts[a] > counts[b] ? a : b)[0])
}
/**
* If this collection contains nested collections, collapse them to a single level.
* @return Promise<Collection>
*/
async collapse(): Promise<Collection<any>> {
const items = await this.collect()
return items.collapse() as Collection<any>
}
/**
* Returns true if the collection contains an item satisfying the given collection.
* @example
* collection.contains('id', '>', 4)
* @param {KeyOperator} key
* @param {WhereOperator} operator
* @param [operand]
* @return Promise<boolean>
*/
async contains<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): Promise<boolean> {
let contains = false
@@ -137,10 +185,19 @@ export class AsyncCollection<T> {
return contains
}
/**
* Returns a clean instance of this collection pointing to the same result set of the iterable.
* @return Promise<AsyncCollection>
*/
async clone(): Promise<AsyncCollection<T>> {
return new AsyncCollection<T>(await this._items.clone())
}
/**
* Returns the elements that are different between the two collections.
* @param {AsyncCollectionComparable} items
* @return Promise<Collection>
*/
async diff(items: AsyncCollectionComparable<T>): Promise<Collection<T>> {
const matches: T[] = []
@@ -155,6 +212,13 @@ export class AsyncCollection<T> {
return new Collection<T>(matches)
}
/**
* Returns the elements that are different between the two collections, using the given function
* as a comparator for the elements.
* @param {AsyncCollectionComparable} items
* @param {DeterminesEquality} compare
* @return Promise<Collection>
*/
async diffUsing(items: AsyncCollectionComparable<T>, compare: DeterminesEquality<T>): Promise<Collection<T>> {
const matches: T[] = []
@@ -168,6 +232,11 @@ export class AsyncCollection<T> {
return new Collection<T>(matches)
}
/**
* Returns true if the given item is present in the collection.
* @param item
* @return Promise<boolean>
*/
async includes(item: CollectionItem<T>): Promise<boolean> {
let contains = false
@@ -181,6 +250,11 @@ export class AsyncCollection<T> {
return contains
}
/**
* Returns true if there is an item in the collection for which the given operator returns true.
* @param {function} operator - item => boolean
* @return Promise<boolean>
*/
async some(operator: (item: T) => boolean): Promise<boolean> {
let contains = false
@@ -195,7 +269,12 @@ export class AsyncCollection<T> {
return contains
}
/**
* Applies a callback to each item in the collection.
* @param {AsyncKeyFunction} func
* @return Promise<void>
*/
async each<T2>(func: AsyncKeyFunction<T, T2>): Promise<void> {
let index = 0
@@ -207,6 +286,11 @@ export class AsyncCollection<T> {
})
}
/**
* Applies a callback to each item in the collection and returns the results as a collection.
* @param {AsyncKeyFunction} func
* @return Promise<Collection>
*/
async map<T2>(func: AsyncKeyFunction<T, T2>): Promise<Collection<T2>> {
const new_items: CollectionItem<T2>[] = []
await this.each(async (item, index) => {
@@ -215,6 +299,11 @@ export class AsyncCollection<T> {
return new Collection<T2>(new_items)
}
/**
* Returns true if the given operator returns true for every item in the collection.
* @param {AsyncKeyFunction} func
* @return Promise<boolean>
*/
async every<T2>(func: AsyncKeyFunction<T, T2>): Promise<boolean> {
let pass = true
let index = 0
@@ -233,6 +322,12 @@ export class AsyncCollection<T> {
return pass
}
/**
* Returns true if every item in the collection satisfies the given where clause.
* @param {KeyOperator} key
* @param {WhereOperator} operator
* @param [operand]
*/
async everyWhere<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): Promise<boolean> {
let pass = true
@@ -246,6 +341,11 @@ export class AsyncCollection<T> {
return pass
}
/**
* Applies a filter to every item in the collection and returns the results that pass the filter.
* @param {KeyFunction} func
* @return Promise<Collection>
*/
async filter<T2>(func: KeyFunction<T, T2>): Promise<Collection<T>> {
let new_items: CollectionItem<T>[] = []
@@ -256,16 +356,35 @@ export class AsyncCollection<T> {
return new Collection<T>(new_items)
}
/**
* Calls the passed in function if the boolean condition is true. Allows for functional syntax.
* @param {boolean} bool
* @param {AsyncCollectionFunction} then
* @return AsyncCollection
*/
when<T2>(bool: boolean, then: AsyncCollectionFunction<T, T2>): AsyncCollection<T> {
if ( bool ) then(this)
return this
}
/**
* Calls the passed in function if the boolean condition is false. Allows for functional syntax.
* @param {boolean} bool
* @param {AsyncCollectionFunction} then
* @return AsyncCollection
*/
unless<T2>(bool: boolean, then: AsyncCollectionFunction<T, T2>): AsyncCollection<T> {
if ( !bool ) then(this)
return this
}
/**
* Applies the given where condition to the collection and returns a new collection of the results.
* @param {KeyOperator} key
* @param {WhereOperator} operator
* @param [operand]
* @return Promise<Collection>
*/
async where<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): Promise<Collection<T>> {
let new_items: CollectionItem<T>[] = []
await this._chunk(async items => {
@@ -274,6 +393,14 @@ export class AsyncCollection<T> {
return new Collection<T>(new_items)
}
/**
* Applies the given where condition to the collection and returns a new collection of the items
* that did not satisfy the condition.
* @param {KeyOperator} key
* @param {WhereOperator} operator
* @param [operand]
* @return Promise<Collection>
*/
async whereNot<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): Promise<Collection<T>> {
let new_items: CollectionItem<T>[] = []
await this._chunk(async items => {
@@ -282,6 +409,12 @@ export class AsyncCollection<T> {
return new Collection<T>(new_items)
}
/**
* Applies a WHERE ... IN ... condition to the collection an returns a new collection of the results.
* @param {KeyOperator} key
* @param {AsyncCollectionComparable} items
* @return Promise<Collection>
*/
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 => {
@@ -294,6 +427,13 @@ export class AsyncCollection<T> {
return new Collection<T>(new_items)
}
/**
* Applies a WHERE ... IN ... condition to the collection and returns a new collection of the items
* that did not satisfy the condition.
* @param {KeyOperator} key
* @param {AsyncCollectionComparable} items
* @return Promise<Collection>
*/
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 => {
@@ -306,12 +446,23 @@ export class AsyncCollection<T> {
return new Collection<T>(new_items)
}
/**
* Returns the first item in the collection, if one exists.
* @return Promise<MaybeCollectionItem>
*/
async first(): Promise<MaybeCollectionItem<T>> {
if ( await this._items.count() > 0 ) {
return this._items.at_index(0)
}
}
/**
* Return the first item in the collection that satisfies the given where condition, if one exists.
* @param {KeyOperator} key
* @param {WhereOperator} [operator = '=']
* @param [operand = true]
* @return Promise<MaybeCollectionItem>
*/
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 => {
@@ -324,6 +475,12 @@ export class AsyncCollection<T> {
return item
}
/**
* Return the first item in the collection that does not satisfy the given where condition, if one exists.
* @param {KeyOperator} key
* @param {WhereOperator} [operator = '=']
* @param [operand = true]
*/
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 => {
@@ -336,31 +493,67 @@ export class AsyncCollection<T> {
return item
}
/**
* Returns the number of elements in this collection.
* @return Promise<number>
*/
async count() {
return this._items.count()
}
/**
* Returns the number of elements in this collection.
* @return Promise<number>
*/
async length() {
return this._items.count()
}
/**
* Get the item at the given index of this collection, if one exists.
* If none exists and a fallback value is provided, that value will be returned.
* @param {number} index
* @param [fallback]
* @return Promise<any>
*/
async get(index: number, fallback?: any) {
if ( (await this.count()) > index ) return this._items.at_index(index)
else return fallback
}
/**
* Get the item at the given index of this collection, if one exists.
* @param {number} index
*/
async at(index: number): Promise<MaybeCollectionItem<T>> {
return this.get(index)
}
/**
* Return an object which maps key values to arrays of items in the collection that satisfy that value.
* @param {KeyOperator} key
* @return Promise<object>
*/
async groupBy<T2>(key: KeyOperator<T, T2>): Promise<any> {
return (await this.collect()).groupBy(key)
}
/**
* Return an object mapping the given key value to items in this collection.
* @param {KeyOperator} key
* @return Promise<object>
*/
async associate<T2>(key: KeyOperator<T, T2>): Promise<any> {
return (await this.collect()).associate(key)
}
/**
* Join the items in this collection with the given delimiter.
* @example
* await collection.join(',') // => '1,2,3,4'
* @param {string} delimiter
* @return Promise<string>
*/
async join(delimiter: string): Promise<string> {
let running_strings: string[] = []
@@ -371,33 +564,74 @@ export class AsyncCollection<T> {
return running_strings.join(delimiter)
}
/**
* Join the items in this collection with the given delimiter.
* @example
* await collection.implode(',') // => '1,2,3,4'
* @param {string} delimiter
* @return Promise<string>
*/
async implode(delimiter: string): Promise<string> {
return this.join(delimiter)
}
// TODO intersect
/**
* Returns true if there are no items in this collection.
* @return Promise<boolean>
*/
async isEmpty(): Promise<boolean> {
return (await this._items.count()) < 1
}
/**
* Returns true if there is at least one item in this collection.
* @return Promise<boolean>
*/
async isNotEmpty(): Promise<boolean> {
return (await this._items.count()) > 0
}
/**
* Return the last item in this collection, if one exists.
* @return Promise<MaybeCollectionItem>
*/
async last(): Promise<MaybeCollectionItem<T>> {
const length = await this._items.count()
if ( length > 0 ) return this._items.at_index(length - 1)
}
/**
* Return the last item in this collection which satisfies the given where condition, if one exists.
* @param {KeyOperator} key
* @param {WhereOperator} operator
* @param [operand]
* @return Promise<MaybeCollectionItem>
*/
async lastWhere<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): Promise<MaybeCollectionItem<T>> {
return (await this.where(key, operator, operand)).last()
}
/**
* Return the last item in this collection which does not satisfy the given condition, if one exists.
* @param {KeyOperator} key
* @param {WhereOperator} operator
* @param [operand]
* @return Promise<MaybeCollectionItem>
*/
async lastWhereNot<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): Promise<MaybeCollectionItem<T>> {
return (await this.whereNot(key, operator, operand)).last()
}
/**
* Builds a collection of the values of a given key for each item in this collection.
* @example
* // collection has { a: 1 }, { a: 2 }, { a: 3 }
* await collection.pluck('a') // => [1, 2, 3]
* @param {KeyOperator} key
* @return Promise<Collection>
*/
async pluck<T2>(key: KeyOperator<T, T2>): Promise<Collection<T2>> {
let new_items: CollectionItem<T2>[] = []
@@ -408,6 +642,11 @@ export class AsyncCollection<T> {
return new Collection<T2>(new_items)
}
/**
* Return the max value of the given key.
* @param {KeyOperator} key
* @return Promise<number>
*/
async max<T2>(key: KeyOperator<T, T2>): Promise<number> {
let running_max: number
@@ -421,10 +660,20 @@ export class AsyncCollection<T> {
return running_max
}
/**
* Return a collection of items that have the max value of the given key.
* @param {KeyOperator} key
* @return Promise<Collection>
*/
async whereMax<T2>(key: KeyOperator<T, T2>): Promise<Collection<T>> {
return this.where(key, '=', await this.max(key))
}
/**
* Return the min value of the given key.
* @param {KeyOperator} key
* @return Promise<number>
*/
async min<T2>(key: KeyOperator<T, T2>): Promise<number> {
let running_min: number
@@ -438,10 +687,20 @@ export class AsyncCollection<T> {
return running_min
}
/**
* Return a collection of items that have the min value of the given key.
* @param {KeyOperator} key
* @return Promise<Collection>
*/
async whereMin<T2>(key: KeyOperator<T, T2>): Promise<Collection<T>> {
return this.where(key, '=', await this.min(key))
}
/**
* Merge the two collections.
* @param {AsyncCollectionComparable} merge_with
* @return Promise<Collection>
*/
async merge<T2>(merge_with: AsyncCollectionComparable<T2>): Promise<Collection<T|T2>> {
let items: T2[]
if ( merge_with instanceof Collection ) items = await merge_with.all()
@@ -452,6 +711,11 @@ export class AsyncCollection<T> {
return new Collection<T|T2>([...items, ...await this.all()])
}
/**
* Return a collection of every nth item in this collection.
* @param {number} n
* @return Promise<Collection>
*/
async nth(n: number): Promise<Collection<T>> {
const matches: CollectionItem<T>[] = []
let current = 1
@@ -467,12 +731,21 @@ export class AsyncCollection<T> {
return new Collection<T>(matches)
}
/**
* Return a collection containing the items that would be on the given page, with the given number of items per page.
* @param {number} page
* @param {number} per_page
*/
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)
}
/**
* Return the value of the function, passing this collection to it.
* @param {AsyncCollectionFunction} func
*/
pipe<T2>(func: AsyncCollectionFunction<T, T2>): any {
return func(this)
}
@@ -484,6 +757,12 @@ export class AsyncCollection<T> {
}
}*/ // TODO Fix this
/**
* Get n random items from this collection.
* @todo add safety check for it loop exceeds max number of items
* @param {number} n
* @return Promise<Collection>
*/
async random(n: number): Promise<Collection<T>> {
const random_items: CollectionItem<T>[] = []
const fetched_indices: number[] = []
@@ -505,6 +784,12 @@ export class AsyncCollection<T> {
return new Collection<T>(random_items)
}
/**
* Collapse the collection into a single value using a reducer function.
* @param {KeyReducerFunction} reducer
* @param [initial_value]
* @return Promise<any>
*/
async reduce<T2>(reducer: KeyReducerFunction<T, T2>, initial_value?: T2): Promise<T2 | undefined> {
let current_value = initial_value
let index = 0
@@ -519,6 +804,11 @@ export class AsyncCollection<T> {
return current_value
}
/**
* Returns a collection of items that fail the truth test.
* @param {AsyncKeyFunction} truth_test
* @return Promise<Collection>
*/
async reject<T2>(truth_test: AsyncKeyFunction<T, T2>): Promise<Collection<T>> {
let rejected: CollectionItem<T>[] = []
@@ -531,10 +821,19 @@ export class AsyncCollection<T> {
return new Collection<T>(rejected)
}
/**
* Get a reversed collection of this collection's items.
* @return Promise<Collection>
*/
async reverse(): Promise<Collection<T>> {
return (await this.collect()).reverse()
}
/**
* Search the collection and return the index of that item, if one exists.
* @param {CollectionItem} item
* @return Promise<MaybeCollectionIndex>
*/
async search(item: CollectionItem<T>): Promise<MaybeCollectionIndex> {
let found_index
let index = 0
@@ -554,6 +853,10 @@ export class AsyncCollection<T> {
return found_index
}
/**
* Get the next item in the collection and remove it.
* @return Promise<MaybeCollectionItem>
*/
async shift(): Promise<MaybeCollectionItem<T>> {
const next_item = await this._items.next()
if ( !next_item.done ) {
@@ -561,34 +864,75 @@ export class AsyncCollection<T> {
}
}
/**
* Shuffle the items in the collection to a random order.
* @return Promise<Collection>
*/
async shuffle(): Promise<Collection<T>> {
return (await this.collect()).shuffle()
}
/**
* Return a slice of this collection.
* @param {number} start - the starting index
* @param {number} end - the ending index
* @return Promise<Collection>
*/
async slice(start: number, end: number): Promise<Collection<T>> {
return this._items.from_range(start, end - 1)
}
/**
* Sort the collection, optionally with the given comparison function.
* @param {ComparisonFunction} compare_func
* @return Promise<Collection>
*/
async sort(compare_func?: ComparisonFunction<T>): Promise<Collection<T>> {
return (await this.collect()).sort(compare_func)
}
/**
* Sort the collection by the given key.
* @param {KeyOperator} key
* @return Promise<Collection>
*/
async sortBy<T2>(key?: KeyOperator<T, T2>): Promise<Collection<T>> {
return (await this.collect()).sortBy(key)
}
/**
* Reverse sort the collection, optionally with the given comparison function.
* @param {ComparisonFunction} compare_func
* @return Promise<Collection>
*/
async sortDesc(compare_func?: ComparisonFunction<T>): Promise<Collection<T>> {
return (await this.collect()).sortDesc(compare_func)
}
/**
* Reverse sort the collection by the given key.
* @param {KeyOperator} key
* @return Promise<Collection>
*/
async sortByDesc<T2>(key?: KeyOperator<T, T2>): Promise<Collection<T>> {
return (await this.collect()).sortByDesc(key)
}
/**
* Splice the collection at the given index. Optionally, removing the given number of items.
* @param {CollectionIndex} start
* @param {number} [deleteCount]
* @return Promise<Collection>
*/
async splice(start: CollectionIndex, deleteCount?: number): Promise<Collection<T>> {
return (await this.collect()).splice(start, deleteCount)
}
/**
* Sum the items in the collection, or the values of the given key.
* @param {KeyOperator} key
* @return Promise<number>
*/
async sum<T2>(key?: KeyOperator<T, T2>): Promise<number> {
let running_sum: number = 0
@@ -606,6 +950,11 @@ export class AsyncCollection<T> {
return running_sum
}
/**
* Take the first n items from the front or back of the collection.
* @param {number} limit
* @return Promise<Collection>
*/
async take(limit: number): Promise<Collection<T>> {
if ( limit === 0 ) return new Collection<T>()
else if ( limit > 0 ) {
@@ -616,11 +965,21 @@ export class AsyncCollection<T> {
}
}
/**
* Call the given function, passing in this collection. Allows functional syntax.
* @param {AsyncCollectionFunction} func
* @return Promise<AsyncCollection>
*/
async tap<T2>(func: AsyncCollectionFunction<T, T2>): Promise<AsyncCollection<T>> {
await func(this)
return this
}
/**
* Return all the unique values in the collection, or the unique values of the given key.
* @param {KeyOperator} key
* @return Promise<Collection>
*/
async unique<T2>(key?: KeyOperator<T, T2>): Promise<Collection<T|T2>> {
const has: CollectionItem<T|T2>[] = []
@@ -641,6 +1000,10 @@ export class AsyncCollection<T> {
return new Collection<T|T2>(has)
}
/**
* Cast this collection to an array.
* @return Promise<array>
*/
async toArray(): Promise<any[]> {
const returns: any = []
for ( const item of (await this.all()) ) {
@@ -651,14 +1014,29 @@ export class AsyncCollection<T> {
return returns
}
/**
* Cast this collection to a JSON string.
* @param [replacer] - the replacer to use
* @param {number} [space = 4] number of indentation spaces to use
*/
async toJSON(replacer = undefined, space = 4): Promise<string> {
return JSON.stringify(this.toArray(), replacer, space)
}
/**
* Iterator to support async iteration:
*
* @example
* for await (const item of collection) {}
*/
[Symbol.asyncIterator]() {
return this._items.clone()
}
/**
* Get a clone of the underlying iterator of this collection.
* @return Iterable
*/
iterator() {
return this._items.clone()
}

View File

@@ -5,14 +5,48 @@ export type ChunkCallback<T> = (items: Collection<T>) => any
export class StopIteration extends Error {}
/**
* Abstract class representing an iterable, lazy-loaded dataset.
* @abstract
*/
export abstract class Iterable<T> {
/**
* The current index of the iterable.
* @type number
*/
protected index = 0
/**
* Get the item of this iterable at the given index, if one exists.
* @param {number} i
* @return Promise<any|undefined>
*/
abstract async at_index(i: number): Promise<T | undefined>
/**
* Get the collection of items in the given range of this iterable.
* @param {number} start
* @param {number} end
* @return Promise<Collection>
*/
abstract async from_range(start: number, end: number): Promise<Collection<T>>
/**
* Count the number of items in this collection.
* @return Promise<number>
*/
abstract async count(): Promise<number>
/**
* Get a copy of this iterable.
* @return Iterable
*/
abstract clone(): Iterable<T>
/**
* Advance to the next value of this iterable.
* @return Promise<MaybeIterationItem>
*/
public async next(): Promise<MaybeIterationItem<T>> {
const i = this.index
@@ -24,10 +58,22 @@ export abstract class Iterable<T> {
return { done: false, value: await this.at_index(i) }
}
/**
* Function to enable async iteration.
*
* @example
* for await (const item of iterable) {}
*/
[Symbol.asyncIterator]() {
return this
}
/**
* Chunk the iterable into the given size and call the callback passing the chunk along.
* @param {number} size
* @param {ChunkCallback} callback
* @return Promise<void>
*/
public async chunk(size: number, callback: ChunkCallback<T>) {
const total = await this.count()
@@ -45,17 +91,30 @@ export abstract class Iterable<T> {
}
}
/**
* Advance the iterable to the given index.
* @param {number} index
* @return Promise<void>
*/
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
}
/**
* Peek at the next value of the iterable, without advancing.
* @return Promise<any|undefined>
*/
public async peek(): Promise<T | undefined> {
if ( this.index + 1 >= await this.count() ) return undefined
else return this.at_index(this.index + 1)
}
/**
* Reset the iterable to the first index.
* @return Promise<any>
*/
public async reset() {
this.index = 0
}

View File

@@ -1,20 +1,25 @@
/**
* Type representing a valid where operator.
*/
type WhereOperator = '&' | '>' | '>=' | '<' | '<=' | '!=' | '<=>' | '%' | '|' | '!' | '~' | '=' | '^'
/**
* Type associating search items with a key.
*/
type AssociatedSearchItem = { key: any, item: any }
/**
* Type representing the result of a where.
*/
type WhereResult = any[]
// bitwise and
// greater than
// greater than equal
// less than
// less than equal
// not equal
// null-safe equal
// modulo
// bitwise or
// not
// bitwise negation
// equal
/**
* Returns true if the given item satisfies the given where clause.
* @param {AssociatedSearchItem} item
* @param {WhereOperator} operator
* @param [operand]
* @return boolean
*/
const whereMatch = (item: AssociatedSearchItem, operator: WhereOperator, operand?: any): boolean => {
switch ( operator ) {
case '&':
@@ -62,6 +67,12 @@ const whereMatch = (item: AssociatedSearchItem, operator: WhereOperator, operand
return false
}
/**
* Apply the given where clause to the items and return those that match.
* @param {Array<AssociatedSearchItem>} items
* @param {WhereOperator} operator
* @param [operand]
*/
const applyWhere = (items: AssociatedSearchItem[], operator: WhereOperator, operand?: any): WhereResult => {
const matches: WhereResult = []
for ( const item of items ) {

View File

@@ -1,3 +1,11 @@
/**
* Enum of HTTP statuses.
* @example
* HTTPStatus.http200 // => 200
*
* @example
* HTTPStatus.REQUEST_TIMEOUT // => 408
*/
export enum HTTPStatus {
http100 = 100,
http101 = 101,
@@ -110,6 +118,9 @@ export enum HTTPStatus {
NETWORK_AUTHENTICATION_REQUIRED = 511,
}
/**
* Maps HTTP status code to default status message.
*/
export const Message = {
100: 'Continue',
101: 'Switching Protocols',

View File

@@ -1,3 +1,6 @@
/**
* Enum of unit statuses.
*/
enum Status {
Stopped = 'stopped',
Starting = 'starting',
@@ -6,6 +9,11 @@ enum Status {
Error = 'error',
}
/**
* Returns true if the given value is a valid unit status.
* @param something
* @return boolean
*/
const isStatus = (something: any) => [
Status.Stopped,
Status.Starting,

View File

@@ -1,8 +1,20 @@
import {HTTPStatus, Message} from '../const/http.ts'
/**
* Error class representing an HTTP error.
* @extends Error
*/
export default class HTTPError extends Error {
constructor(
/**
* The associated HTTP status code.
* @type HTTPStatus
*/
public readonly http_status: HTTPStatus,
/**
* The associated message.
* @type string
*/
public readonly http_message?: string
) {
super(`HTTP ${http_status}: ${http_message || Message[http_status]}`)

View File

@@ -2,12 +2,19 @@ import { red, bgRed } from '../external/std.ts'
import { Service } from '../../../di/src/decorator/Service.ts'
import { Logging } from '../service/logging/Logging.ts'
/**
* Service class for handling and displaying top-level errors.
*/
@Service()
export default class RunLevelErrorHandler {
constructor(
protected logger: Logging,
) {}
/**
* Get the error handler function.
* @type (e: Error) => void
*/
get handle(): (e: Error) => void {
return (e: Error) => {
this.display(e)
@@ -15,6 +22,10 @@ export default class RunLevelErrorHandler {
}
}
/**
* Log the error to the logger.
* @param {Error} e
*/
display(e: Error) {
try {
const error_string = `RunLevelErrorHandler invoked:

View File

@@ -6,6 +6,9 @@ import {Service} from '../../../../di/src/decorator/Service.ts'
import {Request} from '../Request.ts'
import {Logging} from '../../service/logging/Logging.ts'
/**
* Interface for fluently registering kernel modules into the kernel.
*/
export interface ModuleRegistrationFluency {
before: (other?: Instantiable<Module>) => Kernel,
after: (other?: Instantiable<Module>) => Kernel,
@@ -14,18 +17,45 @@ export interface ModuleRegistrationFluency {
core: () => Kernel,
}
/**
* Error thrown when a kernel module is requested that does not exist w/in the kernel.
* @extends Error
*/
export class KernelModuleNotFoundError extends Error {
constructor(mod_name: string) {
super(`The kernel module ${mod_name} is not registered with the kernel.`)
}
}
/**
* A basic HTTP kernel used to process incoming and outgoing middleware.
* @extends AppClass
*/
@Service()
export default class Kernel extends AppClass {
/**
* Collection of preflight modules to apply.
* @type Collection<Module>
*/
protected preflight: Collection<Module> = new Collection<Module>()
/**
* Module considered to be the main handler.
* @type Module
*/
protected inflight?: Module
/**
* Collection of postflight modules to apply.
* @type Collection<Module>
*/
protected postflight: Collection<Module> = new Collection<Module>()
/**
* Handle the incoming request, applying the preflight modules, inflight module, then postflight modules.
* @param {Request} request
* @return Promise<Request>
*/
public async handle(request: Request): Promise<Request> {
const logger = this.make(Logging)
@@ -47,6 +77,11 @@ export default class Kernel extends AppClass {
return request
}
/**
* Get a fluent interface for registering the given kernel module.
* @param {Instantiable<Module>} module
* @return ModuleRegistrationFluency
*/
public register(module: Instantiable<Module>): ModuleRegistrationFluency {
this.make(Logging).verbose(`Registering HTTP kernel module: ${module.name}`)
return {

View File

@@ -2,15 +2,33 @@ import {Request} from '../Request.ts'
import Kernel from './Kernel.ts'
import AppClass from '../../lifecycle/AppClass.ts'
/**
* Base class for HTTP kernel modules.
* @extends AppClass
*/
export default class Module extends AppClass {
/**
* Returns true if the given module should be applied to the incoming request.
* @param {Request} request
* @return Promise<boolean>
*/
public async match(request: Request): Promise<boolean> {
return true
}
/**
* Apply the module to the incoming request.
* @param {Request} request
* @return Promise<Request>
*/
public async apply(request: Request): Promise<Request> {
return request
}
/**
* Register this module with the given HTTP kernel.
* @param {Kernel} kernel
*/
public static register(kernel: Kernel) {
kernel.register(this).before()
}

View File

@@ -7,6 +7,10 @@ import ResponseFactory from '../../response/ResponseFactory.ts'
import {http, error} from '../../response/helpers.ts'
import {HTTPStatus} from '../../../const/http.ts'
/**
* HTTP kernel module to apply route handlers to the incoming request.
* @extends Module
*/
@Injectable()
export default class ApplyRouteHandlers extends Module {
public static register(kernel: Kernel) {
@@ -19,6 +23,11 @@ export default class ApplyRouteHandlers extends Module {
super()
}
/**
* Apply the route handler to the request.
* @param {Request} request
* @return Promise<Request>
*/
public async apply(request: Request): Promise<Request> {
if ( !request.route ) { // Route not found
const factory = http(HTTPStatus.NOT_FOUND)

View File

@@ -6,6 +6,10 @@ import SessionManager from '../../session/SessionManager.ts'
import {Logging} from '../../../service/logging/Logging.ts'
import {Injectable} from '../../../../../di/src/decorator/Injection.ts'
/**
* HTTP kernel module to retrieve and inject the session into the request.
* @extends Module
*/
@Injectable()
export default class InjectSession extends Module {
public static register(kernel: Kernel) {
@@ -19,6 +23,10 @@ export default class InjectSession extends Module {
super()
}
/**
* Lookup or create the session object and inject it into the request.
* @param {Request} request
*/
public async apply(request: Request): Promise<Request> {
if ( request.session ) return request

View File

@@ -8,6 +8,10 @@ import Config from '../../../unit/Config.ts'
import ActivatedRoute from '../../routing/ActivatedRoute.ts'
import {Injectable} from '../../../../../di/src/decorator/Injection.ts'
/**
* HTTP kernel middleware to resolve and mount the registered route onto the request.
* @extends Module
*/
@Injectable()
export default class MountActivatedRoute extends Module {
public static register(kernel: Kernel) {
@@ -22,6 +26,10 @@ export default class MountActivatedRoute extends Module {
super()
}
/**
* Parse and resolve the route and mount it into the request object.
* @param {Request} request
*/
public async apply(request: Request): Promise<Request> {
let incoming = this.routing.resolve([request.path])
this.logger.info(`${request.method} ${incoming}`)

View File

@@ -2,11 +2,19 @@ import Module from '../Module.ts'
import Kernel from '../Kernel.ts'
import {Request} from '../../Request.ts'
/**
* Persist the session data before closing the request.
* @extends Module
*/
export default class PersistSession extends Module {
public static register(kernel: Kernel) {
kernel.register(this).last()
}
/**
* Persist the session.
* @param {Request} request
*/
public async apply(request: Request): Promise<Request> {
await request.session.persist()
return request

View File

@@ -2,12 +2,20 @@ import Module from '../Module.ts'
import Kernel from '../Kernel.ts'
import {Request} from '../../Request.ts'
/**
* HTTP kernel module to call the request's prepare method.
* @extends Module
*/
export default class PrepareRequest extends Module {
public static register(kernel: Kernel) {
kernel.register(this).first()
}
/**
* Prepare the request for Daton processing.
* @param {Request} request
*/
public async apply(request: Request): Promise<Request> {
await request.prepare()
return request

View File

@@ -4,6 +4,10 @@ import {Request} from '../../Request.ts'
import {Injectable} from '../../../../../di/src/decorator/Injection.ts'
import Config from '../../../unit/Config.ts'
/**
* Apply the default Daton headers to the outgoing response.
* @extends Module
*/
@Injectable()
export default class SetDatonHeaders extends Module {
public static register(kernel: Kernel) {
@@ -16,6 +20,10 @@ export default class SetDatonHeaders extends Module {
super()
}
/**
* Apply the outgoing response headers.
* @param request
*/
public async apply(request: Request): Promise<Request> {
const text = this.config.get('server.powered_by.text', 'Daton')
request.response.headers.set('X-Powered-By', text)

View File

@@ -6,6 +6,10 @@ import Utility from '../../../service/utility/Utility.ts'
import {Injectable} from '../../../../../di/src/decorator/Injection.ts'
import {Logging} from '../../../service/logging/Logging.ts'
/**
* HTTP kernel module to set the Daton session cookie, if it doesn't exist.
* @extends Module
*/
@Injectable()
export default class SetSessionCookie extends Module {
@@ -19,6 +23,10 @@ export default class SetSessionCookie extends Module {
super()
}
/**
* If one doesn't exist, generate and set the daton.session cookie.
* @param {Request} request
*/
public async apply(request: Request): Promise<Request> {
if ( !(await request.cookies.has('daton.session')) ) {
const cookie = `${this.utility.uuid()}-${this.utility.uuid()}`