Start JSDocs

This commit is contained in:
Garrett Mills 2020-08-16 14:31:47 -05:00
parent 673fbc84f8
commit c2a7c3f914
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
29 changed files with 830 additions and 16 deletions

View File

@ -11,17 +11,28 @@ type MaybeFactory = AbstractFactory | undefined
type MaybeDependency = any | undefined type MaybeDependency = any | undefined
type ResolvedDependency = { param_index: number, key: DependencyKey, resolved: any } type ResolvedDependency = { param_index: number, key: DependencyKey, resolved: any }
/**
* Interface used to keep track of singleton factory values, by their dependency key.
*/
interface InstanceRef { interface InstanceRef {
key: DependencyKey, key: DependencyKey,
value: any, value: any,
} }
/**
* Error thrown when a factory is registered with a duplicate dependency key.
* @extends Error
*/
class DuplicateFactoryKeyError extends Error { class DuplicateFactoryKeyError extends Error {
constructor(key: DependencyKey) { constructor(key: DependencyKey) {
super(`A factory definition already exists with the key for ${key}.`) super(`A factory definition already exists with the key for ${key}.`)
} }
} }
/**
* Error thrown when a dependency key that has not been registered is passed to a resolver.
* @extends Error
*/
class InvalidDependencyKeyError extends Error { class InvalidDependencyKeyError extends Error {
constructor(key: DependencyKey) { constructor(key: DependencyKey) {
super(`No such dependency is registered with this container: ${key}`) super(`No such dependency is registered with this container: ${key}`)
@ -29,8 +40,20 @@ class InvalidDependencyKeyError extends Error {
} }
/**
* A factory-based inversion-of-control container.
*/
class Container { class Container {
/**
* Collection of factories registered with this container.
* @type Collection<AbstractFactory>
*/
private factories: Collection<AbstractFactory> = new Collection<AbstractFactory>() private factories: Collection<AbstractFactory> = new Collection<AbstractFactory>()
/**
* Collection of singleton instances produced by this container.
* @type Collection<InstanceRef>
*/
private instances: Collection<InstanceRef> = new Collection<InstanceRef>() private instances: Collection<InstanceRef> = new Collection<InstanceRef>()
constructor() { constructor() {
@ -43,6 +66,10 @@ class Container {
this.register_singleton('injector', this) this.register_singleton('injector', this)
} }
/**
* Register a basic instantiable class as a standard Factory with this container.
* @param {Instantiable} dependency
*/
register(dependency: Instantiable<any>) { register(dependency: Instantiable<any>) {
if ( this.resolve(dependency) ) if ( this.resolve(dependency) )
throw new DuplicateFactoryKeyError(dependency) throw new DuplicateFactoryKeyError(dependency)
@ -51,6 +78,11 @@ class Container {
this.factories.push(factory) this.factories.push(factory)
} }
/**
* Register the given function as a factory within the container.
* @param {string} name - unique name to identify the factory in the container
* @param {function} producer - factory to produce a value
*/
register_producer(name: string, producer: () => any) { register_producer(name: string, producer: () => any) {
if ( this.resolve(name) ) if ( this.resolve(name) )
throw new DuplicateFactoryKeyError(name) throw new DuplicateFactoryKeyError(name)
@ -59,6 +91,12 @@ class Container {
this.factories.push(factory) this.factories.push(factory)
} }
/**
* Register a basic instantiable class as a standard Factory with this container,
* identified by a string name rather than static class.
* @param {string} name - unique name to identify the factory in the container
* @param {Instantiable} dependency
*/
register_named(name: string, dependency: Instantiable<any>) { register_named(name: string, dependency: Instantiable<any>) {
if ( this.resolve(name) ) if ( this.resolve(name) )
throw new DuplicateFactoryKeyError(name) throw new DuplicateFactoryKeyError(name)
@ -67,6 +105,12 @@ class Container {
this.factories.push(factory) this.factories.push(factory)
} }
/**
* Register a value as a singleton in the container. It will not be instantiated, but
* can be injected by its unique name.
* @param {string} key - unique name to identify the singleton in the container
* @param value
*/
register_singleton(key: string, value: any) { register_singleton(key: string, value: any) {
if ( this.resolve(key) ) if ( this.resolve(key) )
throw new DuplicateFactoryKeyError(key) throw new DuplicateFactoryKeyError(key)
@ -74,29 +118,55 @@ class Container {
this.factories.push(new SingletonFactory(value, key)) this.factories.push(new SingletonFactory(value, key))
} }
/**
* Register a given factory with the container.
* @param {AbstractFactory} factory
*/
register_factory(factory: AbstractFactory) { register_factory(factory: AbstractFactory) {
if ( !this.factories.includes(factory) ) if ( !this.factories.includes(factory) )
this.factories.push(factory) this.factories.push(factory)
} }
/**
* Returns true if the container has an already-produced value for the given key.
* @param {DependencyKey} key
*/
has_instance(key: DependencyKey): boolean { has_instance(key: DependencyKey): boolean {
return this.instances.where('key', '=', key).isNotEmpty() return this.instances.where('key', '=', key).isNotEmpty()
} }
/**
* Returns true if the container has a factory for the given key.
* @param {DependencyKey} key
*/
has_key(key: DependencyKey): boolean { has_key(key: DependencyKey): boolean {
return !!this.resolve(key) return !!this.resolve(key)
} }
/**
* Get the already-produced value for the given key, if one exists.
* @param {DependencyKey} key
*/
get_existing_instance(key: DependencyKey): MaybeDependency { get_existing_instance(key: DependencyKey): MaybeDependency {
const instances = this.instances.where('key', '=', key) const instances = this.instances.where('key', '=', key)
if ( instances.isNotEmpty() ) return instances.first() if ( instances.isNotEmpty() ) return instances.first()
} }
/**
* Find the factory for the given key, if one is registered with this container.
* @param {DependencyKey} key
*/
resolve(key: DependencyKey): MaybeFactory { resolve(key: DependencyKey): MaybeFactory {
const factory = this.factories.firstWhere(item => item.match(key)) const factory = this.factories.firstWhere(item => item.match(key))
if ( factory ) return factory if ( factory ) return factory
} }
/**
* Resolve the dependency key. If a singleton value for that key already exists in this container,
* return that value. Otherwise, use the factory an given parameters to produce and return the value.
* @param {DependencyKey} key
* @param {...any} parameters
*/
resolve_and_create(key: DependencyKey, ...parameters: any[]): any { resolve_and_create(key: DependencyKey, ...parameters: any[]): any {
// If we've already instantiated this, just return that // If we've already instantiated this, just return that
const instance = this.get_existing_instance(key) const instance = this.get_existing_instance(key)
@ -117,6 +187,12 @@ class Container {
return new_instance return new_instance
} }
/**
* Given a factory and manually-provided parameters, resolve the dependencies for the
* factory and produce its value.
* @param {AbstractFactory} factory
* @param {array} parameters
*/
protected produce_factory(factory: AbstractFactory, parameters: any[]) { protected produce_factory(factory: AbstractFactory, parameters: any[]) {
// Create the dependencies for the factory // Create the dependencies for the factory
const keys = factory.get_dependency_keys().filter(req => this.has_key(req.key)) const keys = factory.get_dependency_keys().filter(req => this.has_key(req.key))
@ -143,6 +219,15 @@ class Container {
return factory.produce(construction_args, params.reverse().all()) return factory.produce(construction_args, params.reverse().all())
} }
/**
* Create an instance of the given target. The target can either be a DependencyKey registered with
* this container (in which case, the singleton value will be returned), or an instantiable class.
*
* If the instantiable class has the Injectable decorator, its injectable parameters will be automatically
* injected into the instance.
* @param {DependencyKey} target
* @param {...any} parameters
*/
make(target: DependencyKey, ...parameters: any[]) { make(target: DependencyKey, ...parameters: any[]) {
if ( this.has_key(target) ) if ( this.has_key(target) )
return this.resolve_and_create(target, ...parameters) return this.resolve_and_create(target, ...parameters)
@ -152,6 +237,10 @@ class Container {
throw new TypeError(`Invalid or unknown make target: ${target}`) throw new TypeError(`Invalid or unknown make target: ${target}`)
} }
/**
* Get a collection of dependency keys required by the given target, if it is registered with this container.
* @param {DependencyKey} target
*/
get_dependencies(target: DependencyKey): Collection<DependencyKey> { get_dependencies(target: DependencyKey): Collection<DependencyKey> {
const factory = this.resolve(target) const factory = this.resolve(target)

View File

@ -3,6 +3,11 @@ import {DEPENDENCY_KEYS_METADATA_KEY, DependencyKey} from '../type/DependencyKey
import {collect, Collection} from '../../../lib/src/collection/Collection.ts' import {collect, Collection} from '../../../lib/src/collection/Collection.ts'
import {DependencyRequirement} from '../type/DependencyRequirement.ts' import {DependencyRequirement} from '../type/DependencyRequirement.ts'
/**
* Get a collection of dependency requirements for the given target object.
* @param {Object} target
* @return Collection<DependencyRequirement>
*/
const initDependencyMetadata = (target: Object): Collection<DependencyRequirement> => { const initDependencyMetadata = (target: Object): Collection<DependencyRequirement> => {
const param_types = Reflect.getMetadata('design:paramtypes', target) const param_types = Reflect.getMetadata('design:paramtypes', target)
return collect<DependencyKey>(param_types).map<DependencyRequirement>((type, index) => { return collect<DependencyKey>(param_types).map<DependencyRequirement>((type, index) => {
@ -14,6 +19,11 @@ const initDependencyMetadata = (target: Object): Collection<DependencyRequiremen
}) })
} }
/**
* Class decorator that marks a class as injectable. When this is applied, dependency
* metadata for the constructors params is resolved and stored in metadata.
* @constructor
*/
const Injectable = (): ClassDecorator => { const Injectable = (): ClassDecorator => {
return (target) => { return (target) => {
const meta = initDependencyMetadata(target) const meta = initDependencyMetadata(target)
@ -47,6 +57,12 @@ const Injectable = (): ClassDecorator => {
} }
} }
/**
* Parameter decorator to manually mark a parameter as being an injection target on injectable
* classes. This can be used to override the dependency key of a given parameter.
* @param {DependencyKey} key
* @constructor
*/
const Inject = (key: DependencyKey): ParameterDecorator => { const Inject = (key: DependencyKey): ParameterDecorator => {
return (target, property, param_index) => { return (target, property, param_index) => {
if ( !Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, target) ) { if ( !Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, target) ) {

View File

@ -4,6 +4,13 @@ import { Injectable } from './Injection.ts'
const injectable = Injectable() const injectable = Injectable()
/**
* Class decorator that marks a class as injectable and singleton in the dependency container.
* When this is applied, the Injectable decorator is automatically applied, and the class is
* marked so that, when it is injected into classes, a singleton instance is used.
* @param {string} [name] - optionally, refer to the dependency by a name, rather than by class type
* @constructor
*/
const Service = (name?: string): ClassDecorator => { const Service = (name?: string): ClassDecorator => {
return (target) => { return (target) => {
if ( isInstantiable(target) ) { if ( isInstantiable(target) ) {

View File

@ -1,12 +1,38 @@
import {Collection} from '../../../lib/src/collection/Collection.ts' import {Collection} from '../../../lib/src/collection/Collection.ts'
import {DependencyRequirement} from '../type/DependencyRequirement.ts' import {DependencyRequirement} from '../type/DependencyRequirement.ts'
/**
* Abstract base class for dependency container factories.
* @abstract
*/
export default abstract class AbstractFactory { export default abstract class AbstractFactory {
protected constructor( protected constructor(
/**
* Token that was registered for this factory. In most cases, this is the static
* form of the item that is to be produced by this factory.
* @var
* @protected
*/
protected token: any protected token: any
) {} ) {}
/**
* Produce an instance of the token.
* @param {Array} dependencies - the resolved dependencies, in order
* @param {Array} parameters - the bound constructor parameters, in order
*/
abstract produce(dependencies: any[], parameters: any[]): any abstract produce(dependencies: any[], parameters: any[]): any
/**
* Should return true if the given identifier matches the token for this factory.
* @param something
* @return boolean
*/
abstract match(something: any): boolean abstract match(something: any): boolean
/**
* Get the dependency requirements required by this factory's token.
* @return Collection<DependencyRequirement>
*/
abstract get_dependency_keys(): Collection<DependencyRequirement> abstract get_dependency_keys(): Collection<DependencyRequirement>
} }

View File

@ -5,6 +5,10 @@ import { Collection } from '../../../lib/src/collection/Collection.ts'
import { DependencyRequirement } from '../type/DependencyRequirement.ts' import { DependencyRequirement } from '../type/DependencyRequirement.ts'
import AbstractFactory from './AbstractFactory.ts' import AbstractFactory from './AbstractFactory.ts'
/**
* Standard factory that produces injected versions of instantiable classes.
* @extends AbstractFactory
*/
export default class Factory extends AbstractFactory { export default class Factory extends AbstractFactory {
constructor( constructor(
protected token: Instantiable<any> protected token: Instantiable<any>

View File

@ -2,9 +2,22 @@ import AbstractFactory from './AbstractFactory.ts'
import {DependencyRequirement} from '../type/DependencyRequirement.ts' import {DependencyRequirement} from '../type/DependencyRequirement.ts'
import {Collection} from '../../../lib/src/collection/Collection.ts' import {Collection} from '../../../lib/src/collection/Collection.ts'
/**
* Container factory that produces an item by calling the token as a function.
* @extends AbstractFactory
*/
export default class FunctionFactory extends AbstractFactory { export default class FunctionFactory extends AbstractFactory {
constructor( constructor(
/**
* The name identifying this factory in the container.
* @type {string}
*/
protected name: string, protected name: string,
/**
* The token, which is a function that returns the value of this factory.
* @type {function}
*/
protected token: () => any, protected token: () => any,
) { ) {
super(token) super(token)

View File

@ -1,9 +1,23 @@
import Factory from './Factory.ts' import Factory from './Factory.ts'
import Instantiable from "../type/Instantiable.ts"; import Instantiable from '../type/Instantiable.ts'
/**
* Container factory that produces an instance of the token, however the token
* is identified by a string name rather than a class reference.
* @extends Factory
*/
export default class NamedFactory extends Factory { export default class NamedFactory extends Factory {
constructor( constructor(
/**
* The name identifying this factory in the container.
* @type {string}
*/
protected name: string, protected name: string,
/**
* The token to be instantiated.
* @type {Instantiable}
*/
protected token: Instantiable<any>, protected token: Instantiable<any>,
) { ) {
super(token) super(token)

View File

@ -2,9 +2,24 @@ import Factory from './Factory.ts'
import {Collection} from '../../../lib/src/collection/Collection.ts' import {Collection} from '../../../lib/src/collection/Collection.ts'
import {DependencyRequirement} from '../type/DependencyRequirement.ts' import {DependencyRequirement} from '../type/DependencyRequirement.ts'
/**
* Container factory which returns its token as its value, without attempting
* to instantiate anything. This is used to register already-produced-singletons
* with the container.
* @extends Factory
*/
export default class SingletonFactory extends Factory { export default class SingletonFactory extends Factory {
constructor( constructor(
/**
* Instantiated value of this factory.
* @type FunctionConstructor
*/
protected token: FunctionConstructor, protected token: FunctionConstructor,
/**
* String name of this singleton identifying it in the container.
* @type string
*/
protected key: string, protected key: string,
) { ) {
super(token) super(token)

View File

@ -1,5 +1,10 @@
import Instantiable from './Instantiable.ts' import Instantiable from './Instantiable.ts'
import {StaticClass} from './StaticClass.ts' import {StaticClass} from './StaticClass.ts'
const DEPENDENCY_KEYS_METADATA_KEY = 'daton:di:dependencyKeys.ts' const DEPENDENCY_KEYS_METADATA_KEY = 'daton:di:dependencyKeys.ts'
/**
* Type used to represent a value that can identify a factory in the container.
*/
type DependencyKey = Instantiable<any> | StaticClass<any, any> | string type DependencyKey = Instantiable<any> | StaticClass<any, any> | string
export { DependencyKey, DEPENDENCY_KEYS_METADATA_KEY } export { DependencyKey, DEPENDENCY_KEYS_METADATA_KEY }

View File

@ -1,5 +1,9 @@
import { DependencyKey } from './DependencyKey.ts' import { DependencyKey } from './DependencyKey.ts'
/**
* Interface used to store dependency requirements by their place in the injectable
* target's constructor parameters.
*/
interface DependencyRequirement { interface DependencyRequirement {
param_index: number, param_index: number,
key: DependencyKey, key: DependencyKey,

View File

@ -1,7 +1,14 @@
/**
* Interface that designates a particular value as able to be constructed.
*/
export default interface Instantiable<T> { export default interface Instantiable<T> {
new(...args: any[]): T new(...args: any[]): T
} }
/**
* Returns true if the given value is instantiable.
* @param what
*/
const isInstantiable = <T>(what: any): what is Instantiable<T> => { const isInstantiable = <T>(what: any): what is Instantiable<T> => {
return (typeof what === 'object' || typeof what === 'function') && 'constructor' in what && typeof what.constructor === 'function' return (typeof what === 'object' || typeof what === 'function') && 'constructor' in what && typeof what.constructor === 'function'
} }

View File

@ -1,5 +1,12 @@
/**
* Type that identifies a value as a static class, even if it is not instantiable.
*/
export type StaticClass<T, T2> = Function & {prototype: T} & T2 export type StaticClass<T, T2> = Function & {prototype: T} & T2
/**
* Returns true if the parameter is a static class.
* @param something
*/
export function isStaticClass<T, T2>(something: any): something is StaticClass<T, T2> { export function isStaticClass<T, T2>(something: any): something is StaticClass<T, T2> {
return typeof something === 'function' && typeof something.prototype !== 'undefined' return typeof something === 'function' && typeof something.prototype !== 'undefined'
} }

View File

@ -1,8 +1,15 @@
import { Iterable } from './Iterable.ts' import { Iterable } from './Iterable.ts'
import { collect } from './Collection.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> { export class ArrayIterable<T> extends Iterable<T> {
constructor( constructor(
/**
* Items to use for this iterable.
*/
protected items: T[], protected items: T[],
) { ) {
super() 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 AsyncKeyFunction<T, T2> = (item: CollectionItem<T>, index: number) => CollectionItem<T2> | Promise<CollectionItem<T2>>
type AsyncCollectionFunction<T, T2> = (items: AsyncCollection<T>) => T2 type AsyncCollectionFunction<T, T2> = (items: AsyncCollection<T>) => T2
/**
* Like a collection, but asynchronous.
*/
export class AsyncCollection<T> { export class AsyncCollection<T> {
constructor( constructor(
/**
* Iterable of items to base this collction on.
* @type Iterable
*/
private _items: Iterable<T>, 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 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() await this._items.reset()
} }
/**
* Get all items in this collection as an array.
* @return Promise<array>
*/
async all(): Promise<CollectionItem<T>[]> { async all(): Promise<CollectionItem<T>[]> {
return (await this._items.from_range(0, await this._items.count())).all() 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>> { async collect(): Promise<Collection<T>> {
return this._items.from_range(0, await this._items.count()) 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> { async average<T2>(key?: KeyOperator<T, T2>): Promise<number> {
let running_total = 0 let running_total = 0
let running_items = 0 let running_items = 0
@ -82,6 +107,11 @@ export class AsyncCollection<T> {
return running_total / running_items 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> { async median<T2>(key?: KeyOperator<T, T2>): Promise<number> {
let items: number[] = [] let items: number[] = []
@ -100,6 +130,11 @@ export class AsyncCollection<T> {
else return (items[middle] + items[middle + 1]) / 2 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> { async mode<T2>(key?: KeyOperator<T, T2>): Promise<number> {
let counts: any = {} 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]) 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>> { async collapse(): Promise<Collection<any>> {
const items = await this.collect() const items = await this.collect()
return items.collapse() as Collection<any> 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> { async contains<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): Promise<boolean> {
let contains = false let contains = false
@ -137,10 +185,19 @@ export class AsyncCollection<T> {
return contains 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>> { async clone(): Promise<AsyncCollection<T>> {
return new AsyncCollection<T>(await this._items.clone()) 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>> { async diff(items: AsyncCollectionComparable<T>): Promise<Collection<T>> {
const matches: T[] = [] const matches: T[] = []
@ -155,6 +212,13 @@ export class AsyncCollection<T> {
return new Collection<T>(matches) 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>> { async diffUsing(items: AsyncCollectionComparable<T>, compare: DeterminesEquality<T>): Promise<Collection<T>> {
const matches: T[] = [] const matches: T[] = []
@ -168,6 +232,11 @@ export class AsyncCollection<T> {
return new Collection<T>(matches) 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> { async includes(item: CollectionItem<T>): Promise<boolean> {
let contains = false let contains = false
@ -181,6 +250,11 @@ export class AsyncCollection<T> {
return contains 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> { async some(operator: (item: T) => boolean): Promise<boolean> {
let contains = false let contains = false
@ -195,7 +269,12 @@ export class AsyncCollection<T> {
return contains 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> { async each<T2>(func: AsyncKeyFunction<T, T2>): Promise<void> {
let index = 0 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>> { async map<T2>(func: AsyncKeyFunction<T, T2>): Promise<Collection<T2>> {
const new_items: CollectionItem<T2>[] = [] const new_items: CollectionItem<T2>[] = []
await this.each(async (item, index) => { await this.each(async (item, index) => {
@ -215,6 +299,11 @@ export class AsyncCollection<T> {
return new Collection<T2>(new_items) 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> { async every<T2>(func: AsyncKeyFunction<T, T2>): Promise<boolean> {
let pass = true let pass = true
let index = 0 let index = 0
@ -233,6 +322,12 @@ export class AsyncCollection<T> {
return pass 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> { async everyWhere<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): Promise<boolean> {
let pass = true let pass = true
@ -246,6 +341,11 @@ export class AsyncCollection<T> {
return pass 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>> { async filter<T2>(func: KeyFunction<T, T2>): Promise<Collection<T>> {
let new_items: CollectionItem<T>[] = [] let new_items: CollectionItem<T>[] = []
@ -256,16 +356,35 @@ export class AsyncCollection<T> {
return new Collection<T>(new_items) 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> { when<T2>(bool: boolean, then: AsyncCollectionFunction<T, T2>): AsyncCollection<T> {
if ( bool ) then(this) if ( bool ) then(this)
return 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> { unless<T2>(bool: boolean, then: AsyncCollectionFunction<T, T2>): AsyncCollection<T> {
if ( !bool ) then(this) if ( !bool ) then(this)
return 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>> { async where<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): Promise<Collection<T>> {
let new_items: CollectionItem<T>[] = [] let new_items: CollectionItem<T>[] = []
await this._chunk(async items => { await this._chunk(async items => {
@ -274,6 +393,14 @@ export class AsyncCollection<T> {
return new Collection<T>(new_items) 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>> { async whereNot<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): Promise<Collection<T>> {
let new_items: CollectionItem<T>[] = [] let new_items: CollectionItem<T>[] = []
await this._chunk(async items => { await this._chunk(async items => {
@ -282,6 +409,12 @@ export class AsyncCollection<T> {
return new Collection<T>(new_items) 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>> { async whereIn<T2>(key: KeyOperator<T, T2>, items: AsyncCollectionComparable<T2>): Promise<Collection<T>> {
let new_items: CollectionItem<T>[] = [] let new_items: CollectionItem<T>[] = []
await this._chunk_all_associate(key,async chunk => { await this._chunk_all_associate(key,async chunk => {
@ -294,6 +427,13 @@ export class AsyncCollection<T> {
return new Collection<T>(new_items) 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>> { async whereNotIn<T2>(key: KeyOperator<T, T2>, items: AsyncCollectionComparable<T2>): Promise<Collection<T>> {
let new_items: CollectionItem<T>[] = [] let new_items: CollectionItem<T>[] = []
await this._chunk_all_associate(key,async chunk => { await this._chunk_all_associate(key,async chunk => {
@ -306,12 +446,23 @@ export class AsyncCollection<T> {
return new Collection<T>(new_items) return new Collection<T>(new_items)
} }
/**
* Returns the first item in the collection, if one exists.
* @return Promise<MaybeCollectionItem>
*/
async first(): Promise<MaybeCollectionItem<T>> { async first(): Promise<MaybeCollectionItem<T>> {
if ( await this._items.count() > 0 ) { if ( await this._items.count() > 0 ) {
return this._items.at_index(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>> { async firstWhere<T2>(key: KeyOperator<T, T2>, operator: WhereOperator = '=', operand: any = true): Promise<MaybeCollectionItem<T>> {
let item = undefined let item = undefined
await this._chunk_all_associate(key, async items => { await this._chunk_all_associate(key, async items => {
@ -324,6 +475,12 @@ export class AsyncCollection<T> {
return item 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>> { async firstWhereNot<T2>(key: KeyOperator<T, T2>, operator: WhereOperator = '=', operand: any = true): Promise<MaybeCollectionItem<T>> {
let item: MaybeCollectionItem<T> = undefined let item: MaybeCollectionItem<T> = undefined
await this._chunk(async items => { await this._chunk(async items => {
@ -336,31 +493,67 @@ export class AsyncCollection<T> {
return item return item
} }
/**
* Returns the number of elements in this collection.
* @return Promise<number>
*/
async count() { async count() {
return this._items.count() return this._items.count()
} }
/**
* Returns the number of elements in this collection.
* @return Promise<number>
*/
async length() { async length() {
return this._items.count() 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) { async get(index: number, fallback?: any) {
if ( (await this.count()) > index ) return this._items.at_index(index) if ( (await this.count()) > index ) return this._items.at_index(index)
else return fallback 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>> { async at(index: number): Promise<MaybeCollectionItem<T>> {
return this.get(index) 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> { async groupBy<T2>(key: KeyOperator<T, T2>): Promise<any> {
return (await this.collect()).groupBy(key) 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> { async associate<T2>(key: KeyOperator<T, T2>): Promise<any> {
return (await this.collect()).associate(key) 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> { async join(delimiter: string): Promise<string> {
let running_strings: string[] = [] let running_strings: string[] = []
@ -371,33 +564,74 @@ export class AsyncCollection<T> {
return running_strings.join(delimiter) 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> { async implode(delimiter: string): Promise<string> {
return this.join(delimiter) return this.join(delimiter)
} }
// TODO intersect // TODO intersect
/**
* Returns true if there are no items in this collection.
* @return Promise<boolean>
*/
async isEmpty(): Promise<boolean> { async isEmpty(): Promise<boolean> {
return (await this._items.count()) < 1 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> { async isNotEmpty(): Promise<boolean> {
return (await this._items.count()) > 0 return (await this._items.count()) > 0
} }
/**
* Return the last item in this collection, if one exists.
* @return Promise<MaybeCollectionItem>
*/
async last(): Promise<MaybeCollectionItem<T>> { async last(): Promise<MaybeCollectionItem<T>> {
const length = await this._items.count() const length = await this._items.count()
if ( length > 0 ) return this._items.at_index(length - 1) 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>> { async lastWhere<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): Promise<MaybeCollectionItem<T>> {
return (await this.where(key, operator, operand)).last() 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>> { async lastWhereNot<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): Promise<MaybeCollectionItem<T>> {
return (await this.whereNot(key, operator, operand)).last() 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>> { async pluck<T2>(key: KeyOperator<T, T2>): Promise<Collection<T2>> {
let new_items: CollectionItem<T2>[] = [] let new_items: CollectionItem<T2>[] = []
@ -408,6 +642,11 @@ export class AsyncCollection<T> {
return new Collection<T2>(new_items) 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> { async max<T2>(key: KeyOperator<T, T2>): Promise<number> {
let running_max: number let running_max: number
@ -421,10 +660,20 @@ export class AsyncCollection<T> {
return running_max 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>> { async whereMax<T2>(key: KeyOperator<T, T2>): Promise<Collection<T>> {
return this.where(key, '=', await this.max(key)) 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> { async min<T2>(key: KeyOperator<T, T2>): Promise<number> {
let running_min: number let running_min: number
@ -438,10 +687,20 @@ export class AsyncCollection<T> {
return running_min 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>> { async whereMin<T2>(key: KeyOperator<T, T2>): Promise<Collection<T>> {
return this.where(key, '=', await this.min(key)) 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>> { async merge<T2>(merge_with: AsyncCollectionComparable<T2>): Promise<Collection<T|T2>> {
let items: T2[] let items: T2[]
if ( merge_with instanceof Collection ) items = await merge_with.all() 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 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>> { async nth(n: number): Promise<Collection<T>> {
const matches: CollectionItem<T>[] = [] const matches: CollectionItem<T>[] = []
let current = 1 let current = 1
@ -467,12 +731,21 @@ export class AsyncCollection<T> {
return new Collection<T>(matches) 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>> { async forPage(page: number, per_page: number): Promise<Collection<T>> {
const start = page * per_page - per_page const start = page * per_page - per_page
const end = page * per_page - 1 const end = page * per_page - 1
return this._items.from_range(start, end) 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 { pipe<T2>(func: AsyncCollectionFunction<T, T2>): any {
return func(this) return func(this)
} }
@ -484,6 +757,12 @@ export class AsyncCollection<T> {
} }
}*/ // TODO Fix this }*/ // 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>> { async random(n: number): Promise<Collection<T>> {
const random_items: CollectionItem<T>[] = [] const random_items: CollectionItem<T>[] = []
const fetched_indices: number[] = [] const fetched_indices: number[] = []
@ -505,6 +784,12 @@ export class AsyncCollection<T> {
return new Collection<T>(random_items) 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> { async reduce<T2>(reducer: KeyReducerFunction<T, T2>, initial_value?: T2): Promise<T2 | undefined> {
let current_value = initial_value let current_value = initial_value
let index = 0 let index = 0
@ -519,6 +804,11 @@ export class AsyncCollection<T> {
return current_value 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>> { async reject<T2>(truth_test: AsyncKeyFunction<T, T2>): Promise<Collection<T>> {
let rejected: CollectionItem<T>[] = [] let rejected: CollectionItem<T>[] = []
@ -531,10 +821,19 @@ export class AsyncCollection<T> {
return new Collection<T>(rejected) return new Collection<T>(rejected)
} }
/**
* Get a reversed collection of this collection's items.
* @return Promise<Collection>
*/
async reverse(): Promise<Collection<T>> { async reverse(): Promise<Collection<T>> {
return (await this.collect()).reverse() 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> { async search(item: CollectionItem<T>): Promise<MaybeCollectionIndex> {
let found_index let found_index
let index = 0 let index = 0
@ -554,6 +853,10 @@ export class AsyncCollection<T> {
return found_index return found_index
} }
/**
* Get the next item in the collection and remove it.
* @return Promise<MaybeCollectionItem>
*/
async shift(): Promise<MaybeCollectionItem<T>> { async shift(): Promise<MaybeCollectionItem<T>> {
const next_item = await this._items.next() const next_item = await this._items.next()
if ( !next_item.done ) { 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>> { async shuffle(): Promise<Collection<T>> {
return (await this.collect()).shuffle() 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>> { async slice(start: number, end: number): Promise<Collection<T>> {
return this._items.from_range(start, end - 1) 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>> { async sort(compare_func?: ComparisonFunction<T>): Promise<Collection<T>> {
return (await this.collect()).sort(compare_func) 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>> { async sortBy<T2>(key?: KeyOperator<T, T2>): Promise<Collection<T>> {
return (await this.collect()).sortBy(key) 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>> { async sortDesc(compare_func?: ComparisonFunction<T>): Promise<Collection<T>> {
return (await this.collect()).sortDesc(compare_func) 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>> { async sortByDesc<T2>(key?: KeyOperator<T, T2>): Promise<Collection<T>> {
return (await this.collect()).sortByDesc(key) 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>> { async splice(start: CollectionIndex, deleteCount?: number): Promise<Collection<T>> {
return (await this.collect()).splice(start, deleteCount) 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> { async sum<T2>(key?: KeyOperator<T, T2>): Promise<number> {
let running_sum: number = 0 let running_sum: number = 0
@ -606,6 +950,11 @@ export class AsyncCollection<T> {
return running_sum 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>> { async take(limit: number): Promise<Collection<T>> {
if ( limit === 0 ) return new Collection<T>() if ( limit === 0 ) return new Collection<T>()
else if ( limit > 0 ) { 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>> { async tap<T2>(func: AsyncCollectionFunction<T, T2>): Promise<AsyncCollection<T>> {
await func(this) await func(this)
return 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>> { async unique<T2>(key?: KeyOperator<T, T2>): Promise<Collection<T|T2>> {
const has: CollectionItem<T|T2>[] = [] const has: CollectionItem<T|T2>[] = []
@ -641,6 +1000,10 @@ export class AsyncCollection<T> {
return new Collection<T|T2>(has) return new Collection<T|T2>(has)
} }
/**
* Cast this collection to an array.
* @return Promise<array>
*/
async toArray(): Promise<any[]> { async toArray(): Promise<any[]> {
const returns: any = [] const returns: any = []
for ( const item of (await this.all()) ) { for ( const item of (await this.all()) ) {
@ -651,14 +1014,29 @@ export class AsyncCollection<T> {
return returns 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> { async toJSON(replacer = undefined, space = 4): Promise<string> {
return JSON.stringify(this.toArray(), replacer, space) return JSON.stringify(this.toArray(), replacer, space)
} }
/**
* Iterator to support async iteration:
*
* @example
* for await (const item of collection) {}
*/
[Symbol.asyncIterator]() { [Symbol.asyncIterator]() {
return this._items.clone() return this._items.clone()
} }
/**
* Get a clone of the underlying iterator of this collection.
* @return Iterable
*/
iterator() { iterator() {
return this._items.clone() return this._items.clone()
} }

View File

@ -5,14 +5,48 @@ export type ChunkCallback<T> = (items: Collection<T>) => any
export class StopIteration extends Error {} export class StopIteration extends Error {}
/**
* Abstract class representing an iterable, lazy-loaded dataset.
* @abstract
*/
export abstract class Iterable<T> { export abstract class Iterable<T> {
/**
* The current index of the iterable.
* @type number
*/
protected index = 0 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> 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>> 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> abstract async count(): Promise<number>
/**
* Get a copy of this iterable.
* @return Iterable
*/
abstract clone(): Iterable<T> abstract clone(): Iterable<T>
/**
* Advance to the next value of this iterable.
* @return Promise<MaybeIterationItem>
*/
public async next(): Promise<MaybeIterationItem<T>> { public async next(): Promise<MaybeIterationItem<T>> {
const i = this.index const i = this.index
@ -24,10 +58,22 @@ export abstract class Iterable<T> {
return { done: false, value: await this.at_index(i) } return { done: false, value: await this.at_index(i) }
} }
/**
* Function to enable async iteration.
*
* @example
* for await (const item of iterable) {}
*/
[Symbol.asyncIterator]() { [Symbol.asyncIterator]() {
return this 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>) { public async chunk(size: number, callback: ChunkCallback<T>) {
const total = await this.count() 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) { public async seek(index: number) {
if ( index < 0 ) throw new TypeError('Cannot seek to negative index.') 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.') else if ( index >= await this.count() ) throw new TypeError('Cannot seek past last item.')
this.index = index this.index = index
} }
/**
* Peek at the next value of the iterable, without advancing.
* @return Promise<any|undefined>
*/
public async peek(): Promise<T | undefined> { public async peek(): Promise<T | undefined> {
if ( this.index + 1 >= await this.count() ) return undefined if ( this.index + 1 >= await this.count() ) return undefined
else return this.at_index(this.index + 1) else return this.at_index(this.index + 1)
} }
/**
* Reset the iterable to the first index.
* @return Promise<any>
*/
public async reset() { public async reset() {
this.index = 0 this.index = 0
} }

View File

@ -1,20 +1,25 @@
/**
* Type representing a valid where operator.
*/
type WhereOperator = '&' | '>' | '>=' | '<' | '<=' | '!=' | '<=>' | '%' | '|' | '!' | '~' | '=' | '^' type WhereOperator = '&' | '>' | '>=' | '<' | '<=' | '!=' | '<=>' | '%' | '|' | '!' | '~' | '=' | '^'
/**
* Type associating search items with a key.
*/
type AssociatedSearchItem = { key: any, item: any } type AssociatedSearchItem = { key: any, item: any }
/**
* Type representing the result of a where.
*/
type WhereResult = any[] type WhereResult = any[]
// bitwise and /**
// greater than * Returns true if the given item satisfies the given where clause.
// greater than equal * @param {AssociatedSearchItem} item
// less than * @param {WhereOperator} operator
// less than equal * @param [operand]
// not equal * @return boolean
// null-safe equal */
// modulo
// bitwise or
// not
// bitwise negation
// equal
const whereMatch = (item: AssociatedSearchItem, operator: WhereOperator, operand?: any): boolean => { const whereMatch = (item: AssociatedSearchItem, operator: WhereOperator, operand?: any): boolean => {
switch ( operator ) { switch ( operator ) {
case '&': case '&':
@ -62,6 +67,12 @@ const whereMatch = (item: AssociatedSearchItem, operator: WhereOperator, operand
return false 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 applyWhere = (items: AssociatedSearchItem[], operator: WhereOperator, operand?: any): WhereResult => {
const matches: WhereResult = [] const matches: WhereResult = []
for ( const item of items ) { 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 { export enum HTTPStatus {
http100 = 100, http100 = 100,
http101 = 101, http101 = 101,
@ -110,6 +118,9 @@ export enum HTTPStatus {
NETWORK_AUTHENTICATION_REQUIRED = 511, NETWORK_AUTHENTICATION_REQUIRED = 511,
} }
/**
* Maps HTTP status code to default status message.
*/
export const Message = { export const Message = {
100: 'Continue', 100: 'Continue',
101: 'Switching Protocols', 101: 'Switching Protocols',

View File

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

View File

@ -1,8 +1,20 @@
import {HTTPStatus, Message} from '../const/http.ts' import {HTTPStatus, Message} from '../const/http.ts'
/**
* Error class representing an HTTP error.
* @extends Error
*/
export default class HTTPError extends Error { export default class HTTPError extends Error {
constructor( constructor(
/**
* The associated HTTP status code.
* @type HTTPStatus
*/
public readonly http_status: HTTPStatus, public readonly http_status: HTTPStatus,
/**
* The associated message.
* @type string
*/
public readonly http_message?: string public readonly http_message?: string
) { ) {
super(`HTTP ${http_status}: ${http_message || Message[http_status]}`) 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 { Service } from '../../../di/src/decorator/Service.ts'
import { Logging } from '../service/logging/Logging.ts' import { Logging } from '../service/logging/Logging.ts'
/**
* Service class for handling and displaying top-level errors.
*/
@Service() @Service()
export default class RunLevelErrorHandler { export default class RunLevelErrorHandler {
constructor( constructor(
protected logger: Logging, protected logger: Logging,
) {} ) {}
/**
* Get the error handler function.
* @type (e: Error) => void
*/
get handle(): (e: Error) => void { get handle(): (e: Error) => void {
return (e: Error) => { return (e: Error) => {
this.display(e) this.display(e)
@ -15,6 +22,10 @@ export default class RunLevelErrorHandler {
} }
} }
/**
* Log the error to the logger.
* @param {Error} e
*/
display(e: Error) { display(e: Error) {
try { try {
const error_string = `RunLevelErrorHandler invoked: 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 {Request} from '../Request.ts'
import {Logging} from '../../service/logging/Logging.ts' import {Logging} from '../../service/logging/Logging.ts'
/**
* Interface for fluently registering kernel modules into the kernel.
*/
export interface ModuleRegistrationFluency { export interface ModuleRegistrationFluency {
before: (other?: Instantiable<Module>) => Kernel, before: (other?: Instantiable<Module>) => Kernel,
after: (other?: Instantiable<Module>) => Kernel, after: (other?: Instantiable<Module>) => Kernel,
@ -14,18 +17,45 @@ export interface ModuleRegistrationFluency {
core: () => Kernel, 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 { export class KernelModuleNotFoundError extends Error {
constructor(mod_name: string) { constructor(mod_name: string) {
super(`The kernel module ${mod_name} is not registered with the kernel.`) 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() @Service()
export default class Kernel extends AppClass { export default class Kernel extends AppClass {
/**
* Collection of preflight modules to apply.
* @type Collection<Module>
*/
protected preflight: Collection<Module> = new Collection<Module>() protected preflight: Collection<Module> = new Collection<Module>()
/**
* Module considered to be the main handler.
* @type Module
*/
protected inflight?: Module protected inflight?: Module
/**
* Collection of postflight modules to apply.
* @type Collection<Module>
*/
protected postflight: Collection<Module> = new 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> { public async handle(request: Request): Promise<Request> {
const logger = this.make(Logging) const logger = this.make(Logging)
@ -47,6 +77,11 @@ export default class Kernel extends AppClass {
return request return request
} }
/**
* Get a fluent interface for registering the given kernel module.
* @param {Instantiable<Module>} module
* @return ModuleRegistrationFluency
*/
public register(module: Instantiable<Module>): ModuleRegistrationFluency { public register(module: Instantiable<Module>): ModuleRegistrationFluency {
this.make(Logging).verbose(`Registering HTTP kernel module: ${module.name}`) this.make(Logging).verbose(`Registering HTTP kernel module: ${module.name}`)
return { return {

View File

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

View File

@ -7,6 +7,10 @@ import ResponseFactory from '../../response/ResponseFactory.ts'
import {http, error} from '../../response/helpers.ts' import {http, error} from '../../response/helpers.ts'
import {HTTPStatus} from '../../../const/http.ts' import {HTTPStatus} from '../../../const/http.ts'
/**
* HTTP kernel module to apply route handlers to the incoming request.
* @extends Module
*/
@Injectable() @Injectable()
export default class ApplyRouteHandlers extends Module { export default class ApplyRouteHandlers extends Module {
public static register(kernel: Kernel) { public static register(kernel: Kernel) {
@ -19,6 +23,11 @@ export default class ApplyRouteHandlers extends Module {
super() super()
} }
/**
* Apply the route handler to the request.
* @param {Request} request
* @return Promise<Request>
*/
public async apply(request: Request): Promise<Request> { public async apply(request: Request): Promise<Request> {
if ( !request.route ) { // Route not found if ( !request.route ) { // Route not found
const factory = http(HTTPStatus.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 {Logging} from '../../../service/logging/Logging.ts'
import {Injectable} from '../../../../../di/src/decorator/Injection.ts' import {Injectable} from '../../../../../di/src/decorator/Injection.ts'
/**
* HTTP kernel module to retrieve and inject the session into the request.
* @extends Module
*/
@Injectable() @Injectable()
export default class InjectSession extends Module { export default class InjectSession extends Module {
public static register(kernel: Kernel) { public static register(kernel: Kernel) {
@ -19,6 +23,10 @@ export default class InjectSession extends Module {
super() super()
} }
/**
* Lookup or create the session object and inject it into the request.
* @param {Request} request
*/
public async apply(request: Request): Promise<Request> { public async apply(request: Request): Promise<Request> {
if ( request.session ) return 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 ActivatedRoute from '../../routing/ActivatedRoute.ts'
import {Injectable} from '../../../../../di/src/decorator/Injection.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() @Injectable()
export default class MountActivatedRoute extends Module { export default class MountActivatedRoute extends Module {
public static register(kernel: Kernel) { public static register(kernel: Kernel) {
@ -22,6 +26,10 @@ export default class MountActivatedRoute extends Module {
super() super()
} }
/**
* Parse and resolve the route and mount it into the request object.
* @param {Request} request
*/
public async apply(request: Request): Promise<Request> { public async apply(request: Request): Promise<Request> {
let incoming = this.routing.resolve([request.path]) let incoming = this.routing.resolve([request.path])
this.logger.info(`${request.method} ${incoming}`) this.logger.info(`${request.method} ${incoming}`)

View File

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

View File

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

View File

@ -4,6 +4,10 @@ import {Request} from '../../Request.ts'
import {Injectable} from '../../../../../di/src/decorator/Injection.ts' import {Injectable} from '../../../../../di/src/decorator/Injection.ts'
import Config from '../../../unit/Config.ts' import Config from '../../../unit/Config.ts'
/**
* Apply the default Daton headers to the outgoing response.
* @extends Module
*/
@Injectable() @Injectable()
export default class SetDatonHeaders extends Module { export default class SetDatonHeaders extends Module {
public static register(kernel: Kernel) { public static register(kernel: Kernel) {
@ -16,6 +20,10 @@ export default class SetDatonHeaders extends Module {
super() super()
} }
/**
* Apply the outgoing response headers.
* @param request
*/
public async apply(request: Request): Promise<Request> { public async apply(request: Request): Promise<Request> {
const text = this.config.get('server.powered_by.text', 'Daton') const text = this.config.get('server.powered_by.text', 'Daton')
request.response.headers.set('X-Powered-By', text) 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 {Injectable} from '../../../../../di/src/decorator/Injection.ts'
import {Logging} from '../../../service/logging/Logging.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() @Injectable()
export default class SetSessionCookie extends Module { export default class SetSessionCookie extends Module {
@ -19,6 +23,10 @@ export default class SetSessionCookie extends Module {
super() super()
} }
/**
* If one doesn't exist, generate and set the daton.session cookie.
* @param {Request} request
*/
public async apply(request: Request): Promise<Request> { public async apply(request: Request): Promise<Request> {
if ( !(await request.cookies.has('daton.session')) ) { if ( !(await request.cookies.has('daton.session')) ) {
const cookie = `${this.utility.uuid()}-${this.utility.uuid()}` const cookie = `${this.utility.uuid()}-${this.utility.uuid()}`