diff --git a/di/src/Container.ts b/di/src/Container.ts index 75f6df5..0b288bd 100755 --- a/di/src/Container.ts +++ b/di/src/Container.ts @@ -11,17 +11,28 @@ type MaybeFactory = AbstractFactory | undefined type MaybeDependency = any | undefined type ResolvedDependency = { param_index: number, key: DependencyKey, resolved: any } +/** + * Interface used to keep track of singleton factory values, by their dependency key. + */ interface InstanceRef { key: DependencyKey, value: any, } +/** + * Error thrown when a factory is registered with a duplicate dependency key. + * @extends Error + */ class DuplicateFactoryKeyError extends Error { constructor(key: DependencyKey) { 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 { constructor(key: DependencyKey) { 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 { + /** + * Collection of factories registered with this container. + * @type Collection + */ private factories: Collection = new Collection() + + /** + * Collection of singleton instances produced by this container. + * @type Collection + */ private instances: Collection = new Collection() constructor() { @@ -43,6 +66,10 @@ class Container { this.register_singleton('injector', this) } + /** + * Register a basic instantiable class as a standard Factory with this container. + * @param {Instantiable} dependency + */ register(dependency: Instantiable) { if ( this.resolve(dependency) ) throw new DuplicateFactoryKeyError(dependency) @@ -51,6 +78,11 @@ class Container { 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) { if ( this.resolve(name) ) throw new DuplicateFactoryKeyError(name) @@ -59,6 +91,12 @@ class Container { 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) { if ( this.resolve(name) ) throw new DuplicateFactoryKeyError(name) @@ -67,6 +105,12 @@ class Container { 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) { if ( this.resolve(key) ) throw new DuplicateFactoryKeyError(key) @@ -74,29 +118,55 @@ class Container { this.factories.push(new SingletonFactory(value, key)) } + /** + * Register a given factory with the container. + * @param {AbstractFactory} factory + */ register_factory(factory: AbstractFactory) { if ( !this.factories.includes(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 { 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 { 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 { const instances = this.instances.where('key', '=', key) 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 { const factory = this.factories.firstWhere(item => item.match(key)) 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 { // If we've already instantiated this, just return that const instance = this.get_existing_instance(key) @@ -117,6 +187,12 @@ class Container { 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[]) { // Create the dependencies for the factory 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()) } + /** + * 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[]) { if ( this.has_key(target) ) return this.resolve_and_create(target, ...parameters) @@ -152,6 +237,10 @@ class Container { 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 { const factory = this.resolve(target) diff --git a/di/src/decorator/Injection.ts b/di/src/decorator/Injection.ts index cb062c2..9decad9 100755 --- a/di/src/decorator/Injection.ts +++ b/di/src/decorator/Injection.ts @@ -3,6 +3,11 @@ import {DEPENDENCY_KEYS_METADATA_KEY, DependencyKey} from '../type/DependencyKey import {collect, Collection} from '../../../lib/src/collection/Collection.ts' import {DependencyRequirement} from '../type/DependencyRequirement.ts' +/** + * Get a collection of dependency requirements for the given target object. + * @param {Object} target + * @return Collection + */ const initDependencyMetadata = (target: Object): Collection => { const param_types = Reflect.getMetadata('design:paramtypes', target) return collect(param_types).map((type, index) => { @@ -14,6 +19,11 @@ const initDependencyMetadata = (target: Object): Collection { return (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 => { return (target, property, param_index) => { if ( !Reflect.getMetadata(DEPENDENCY_KEYS_METADATA_KEY, target) ) { diff --git a/di/src/decorator/Service.ts b/di/src/decorator/Service.ts index f6316fa..6dd8696 100755 --- a/di/src/decorator/Service.ts +++ b/di/src/decorator/Service.ts @@ -4,6 +4,13 @@ import { Injectable } from './Injection.ts' 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 => { return (target) => { if ( isInstantiable(target) ) { diff --git a/di/src/factory/AbstractFactory.ts b/di/src/factory/AbstractFactory.ts index 1cf338f..ddd58f0 100644 --- a/di/src/factory/AbstractFactory.ts +++ b/di/src/factory/AbstractFactory.ts @@ -1,12 +1,38 @@ import {Collection} from '../../../lib/src/collection/Collection.ts' import {DependencyRequirement} from '../type/DependencyRequirement.ts' +/** + * Abstract base class for dependency container factories. + * @abstract + */ export default abstract class AbstractFactory { 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 ) {} + /** + * 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 + + /** + * Should return true if the given identifier matches the token for this factory. + * @param something + * @return boolean + */ abstract match(something: any): boolean + + /** + * Get the dependency requirements required by this factory's token. + * @return Collection + */ abstract get_dependency_keys(): Collection } diff --git a/di/src/factory/Factory.ts b/di/src/factory/Factory.ts index 55bc9cb..c1790fa 100755 --- a/di/src/factory/Factory.ts +++ b/di/src/factory/Factory.ts @@ -5,6 +5,10 @@ import { Collection } from '../../../lib/src/collection/Collection.ts' import { DependencyRequirement } from '../type/DependencyRequirement.ts' import AbstractFactory from './AbstractFactory.ts' +/** + * Standard factory that produces injected versions of instantiable classes. + * @extends AbstractFactory + */ export default class Factory extends AbstractFactory { constructor( protected token: Instantiable diff --git a/di/src/factory/FunctionFactory.ts b/di/src/factory/FunctionFactory.ts index 419f8c0..0377eee 100644 --- a/di/src/factory/FunctionFactory.ts +++ b/di/src/factory/FunctionFactory.ts @@ -2,9 +2,22 @@ import AbstractFactory from './AbstractFactory.ts' import {DependencyRequirement} from '../type/DependencyRequirement.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 { constructor( + /** + * The name identifying this factory in the container. + * @type {string} + */ protected name: string, + + /** + * The token, which is a function that returns the value of this factory. + * @type {function} + */ protected token: () => any, ) { super(token) diff --git a/di/src/factory/NamedFactory.ts b/di/src/factory/NamedFactory.ts index aad8d55..9633384 100644 --- a/di/src/factory/NamedFactory.ts +++ b/di/src/factory/NamedFactory.ts @@ -1,9 +1,23 @@ 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 { constructor( + /** + * The name identifying this factory in the container. + * @type {string} + */ protected name: string, + + /** + * The token to be instantiated. + * @type {Instantiable} + */ protected token: Instantiable, ) { super(token) diff --git a/di/src/factory/SingletonFactory.ts b/di/src/factory/SingletonFactory.ts index 28095d9..d54b387 100644 --- a/di/src/factory/SingletonFactory.ts +++ b/di/src/factory/SingletonFactory.ts @@ -2,9 +2,24 @@ import Factory from './Factory.ts' import {Collection} from '../../../lib/src/collection/Collection.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 { constructor( + /** + * Instantiated value of this factory. + * @type FunctionConstructor + */ protected token: FunctionConstructor, + + /** + * String name of this singleton identifying it in the container. + * @type string + */ protected key: string, ) { super(token) diff --git a/di/src/type/DependencyKey.ts b/di/src/type/DependencyKey.ts index 9e794e2..6f26a42 100644 --- a/di/src/type/DependencyKey.ts +++ b/di/src/type/DependencyKey.ts @@ -1,5 +1,10 @@ import Instantiable from './Instantiable.ts' import {StaticClass} from './StaticClass.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 | StaticClass | string + export { DependencyKey, DEPENDENCY_KEYS_METADATA_KEY } diff --git a/di/src/type/DependencyRequirement.ts b/di/src/type/DependencyRequirement.ts index ed090c9..8fbe517 100644 --- a/di/src/type/DependencyRequirement.ts +++ b/di/src/type/DependencyRequirement.ts @@ -1,5 +1,9 @@ import { DependencyKey } from './DependencyKey.ts' +/** + * Interface used to store dependency requirements by their place in the injectable + * target's constructor parameters. + */ interface DependencyRequirement { param_index: number, key: DependencyKey, diff --git a/di/src/type/Instantiable.ts b/di/src/type/Instantiable.ts index 0f75b87..f1a5bdd 100644 --- a/di/src/type/Instantiable.ts +++ b/di/src/type/Instantiable.ts @@ -1,7 +1,14 @@ +/** + * Interface that designates a particular value as able to be constructed. + */ export default interface Instantiable { new(...args: any[]): T } +/** + * Returns true if the given value is instantiable. + * @param what + */ const isInstantiable = (what: any): what is Instantiable => { return (typeof what === 'object' || typeof what === 'function') && 'constructor' in what && typeof what.constructor === 'function' } diff --git a/di/src/type/StaticClass.ts b/di/src/type/StaticClass.ts index 4ee0019..0b6989a 100644 --- a/di/src/type/StaticClass.ts +++ b/di/src/type/StaticClass.ts @@ -1,5 +1,12 @@ +/** + * Type that identifies a value as a static class, even if it is not instantiable. + */ export type StaticClass = Function & {prototype: T} & T2 +/** + * Returns true if the parameter is a static class. + * @param something + */ export function isStaticClass(something: any): something is StaticClass { return typeof something === 'function' && typeof something.prototype !== 'undefined' } diff --git a/lib/src/collection/ArrayIterable.ts b/lib/src/collection/ArrayIterable.ts index e2a6741..7ffd5b6 100644 --- a/lib/src/collection/ArrayIterable.ts +++ b/lib/src/collection/ArrayIterable.ts @@ -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 extends Iterable { constructor( + /** + * Items to use for this iterable. + */ protected items: T[], ) { super() diff --git a/lib/src/collection/AsyncCollection.ts b/lib/src/collection/AsyncCollection.ts index 5f79f42..72a6767 100644 --- a/lib/src/collection/AsyncCollection.ts +++ b/lib/src/collection/AsyncCollection.ts @@ -11,9 +11,21 @@ type AsyncCollectionComparable = CollectionItem[] | Collection | AsyncC type AsyncKeyFunction = (item: CollectionItem, index: number) => CollectionItem | Promise> type AsyncCollectionFunction = (items: AsyncCollection) => T2 +/** + * Like a collection, but asynchronous. + */ export class AsyncCollection { constructor( + /** + * Iterable of items to base this collction on. + * @type Iterable + */ private _items: Iterable, + + /** + * 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 { await this._items.reset() } + /** + * Get all items in this collection as an array. + * @return Promise + */ async all(): Promise[]> { return (await this._items.from_range(0, await this._items.count())).all() } + /** + * Get all items in this collection as a synchronous Collection + * @return Promise + */ async collect(): Promise> { 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 + */ async average(key?: KeyOperator): Promise { let running_total = 0 let running_items = 0 @@ -82,6 +107,11 @@ export class AsyncCollection { return running_total / running_items } + /** + * Get the median value of the collection or one of its keys. + * @param {KeyOperator} key + * @return Promise + */ async median(key?: KeyOperator): Promise { let items: number[] = [] @@ -100,6 +130,11 @@ export class AsyncCollection { 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 + */ async mode(key?: KeyOperator): Promise { let counts: any = {} @@ -118,11 +153,24 @@ export class AsyncCollection { 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 + */ async collapse(): Promise> { const items = await this.collect() return items.collapse() as Collection } + /** + * 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 + */ async contains(key: KeyOperator, operator: WhereOperator, operand?: any): Promise { let contains = false @@ -137,10 +185,19 @@ export class AsyncCollection { return contains } + /** + * Returns a clean instance of this collection pointing to the same result set of the iterable. + * @return Promise + */ async clone(): Promise> { return new AsyncCollection(await this._items.clone()) } - + + /** + * Returns the elements that are different between the two collections. + * @param {AsyncCollectionComparable} items + * @return Promise + */ async diff(items: AsyncCollectionComparable): Promise> { const matches: T[] = [] @@ -155,6 +212,13 @@ export class AsyncCollection { return new Collection(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 + */ async diffUsing(items: AsyncCollectionComparable, compare: DeterminesEquality): Promise> { const matches: T[] = [] @@ -168,6 +232,11 @@ export class AsyncCollection { return new Collection(matches) } + /** + * Returns true if the given item is present in the collection. + * @param item + * @return Promise + */ async includes(item: CollectionItem): Promise { let contains = false @@ -181,6 +250,11 @@ export class AsyncCollection { 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 + */ async some(operator: (item: T) => boolean): Promise { let contains = false @@ -195,7 +269,12 @@ export class AsyncCollection { return contains } - + + /** + * Applies a callback to each item in the collection. + * @param {AsyncKeyFunction} func + * @return Promise + */ async each(func: AsyncKeyFunction): Promise { let index = 0 @@ -207,6 +286,11 @@ export class AsyncCollection { }) } + /** + * Applies a callback to each item in the collection and returns the results as a collection. + * @param {AsyncKeyFunction} func + * @return Promise + */ async map(func: AsyncKeyFunction): Promise> { const new_items: CollectionItem[] = [] await this.each(async (item, index) => { @@ -215,6 +299,11 @@ export class AsyncCollection { return new Collection(new_items) } + /** + * Returns true if the given operator returns true for every item in the collection. + * @param {AsyncKeyFunction} func + * @return Promise + */ async every(func: AsyncKeyFunction): Promise { let pass = true let index = 0 @@ -233,6 +322,12 @@ export class AsyncCollection { 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(key: KeyOperator, operator: WhereOperator, operand?: any): Promise { let pass = true @@ -246,6 +341,11 @@ export class AsyncCollection { return pass } + /** + * Applies a filter to every item in the collection and returns the results that pass the filter. + * @param {KeyFunction} func + * @return Promise + */ async filter(func: KeyFunction): Promise> { let new_items: CollectionItem[] = [] @@ -256,16 +356,35 @@ export class AsyncCollection { return new Collection(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(bool: boolean, then: AsyncCollectionFunction): AsyncCollection { 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(bool: boolean, then: AsyncCollectionFunction): AsyncCollection { 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 + */ async where(key: KeyOperator, operator: WhereOperator, operand?: any): Promise> { let new_items: CollectionItem[] = [] await this._chunk(async items => { @@ -274,6 +393,14 @@ export class AsyncCollection { return new Collection(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 + */ async whereNot(key: KeyOperator, operator: WhereOperator, operand?: any): Promise> { let new_items: CollectionItem[] = [] await this._chunk(async items => { @@ -282,6 +409,12 @@ export class AsyncCollection { return new Collection(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 + */ async whereIn(key: KeyOperator, items: AsyncCollectionComparable): Promise> { let new_items: CollectionItem[] = [] await this._chunk_all_associate(key,async chunk => { @@ -294,6 +427,13 @@ export class AsyncCollection { return new Collection(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 + */ async whereNotIn(key: KeyOperator, items: AsyncCollectionComparable): Promise> { let new_items: CollectionItem[] = [] await this._chunk_all_associate(key,async chunk => { @@ -306,12 +446,23 @@ export class AsyncCollection { return new Collection(new_items) } + /** + * Returns the first item in the collection, if one exists. + * @return Promise + */ async first(): Promise> { 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 + */ async firstWhere(key: KeyOperator, operator: WhereOperator = '=', operand: any = true): Promise> { let item = undefined await this._chunk_all_associate(key, async items => { @@ -324,6 +475,12 @@ export class AsyncCollection { 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(key: KeyOperator, operator: WhereOperator = '=', operand: any = true): Promise> { let item: MaybeCollectionItem = undefined await this._chunk(async items => { @@ -336,31 +493,67 @@ export class AsyncCollection { return item } + /** + * Returns the number of elements in this collection. + * @return Promise + */ async count() { return this._items.count() } + /** + * Returns the number of elements in this collection. + * @return Promise + */ 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 + */ 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> { 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 + */ async groupBy(key: KeyOperator): Promise { return (await this.collect()).groupBy(key) } + /** + * Return an object mapping the given key value to items in this collection. + * @param {KeyOperator} key + * @return Promise + */ async associate(key: KeyOperator): Promise { 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 + */ async join(delimiter: string): Promise { let running_strings: string[] = [] @@ -371,33 +564,74 @@ export class AsyncCollection { 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 + */ async implode(delimiter: string): Promise { return this.join(delimiter) } // TODO intersect + /** + * Returns true if there are no items in this collection. + * @return Promise + */ async isEmpty(): Promise { return (await this._items.count()) < 1 } + /** + * Returns true if there is at least one item in this collection. + * @return Promise + */ async isNotEmpty(): Promise { return (await this._items.count()) > 0 } + /** + * Return the last item in this collection, if one exists. + * @return Promise + */ async last(): Promise> { 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 + */ async lastWhere(key: KeyOperator, operator: WhereOperator, operand?: any): Promise> { 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 + */ async lastWhereNot(key: KeyOperator, operator: WhereOperator, operand?: any): Promise> { 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 + */ async pluck(key: KeyOperator): Promise> { let new_items: CollectionItem[] = [] @@ -408,6 +642,11 @@ export class AsyncCollection { return new Collection(new_items) } + /** + * Return the max value of the given key. + * @param {KeyOperator} key + * @return Promise + */ async max(key: KeyOperator): Promise { let running_max: number @@ -421,10 +660,20 @@ export class AsyncCollection { return running_max } + /** + * Return a collection of items that have the max value of the given key. + * @param {KeyOperator} key + * @return Promise + */ async whereMax(key: KeyOperator): Promise> { return this.where(key, '=', await this.max(key)) } + /** + * Return the min value of the given key. + * @param {KeyOperator} key + * @return Promise + */ async min(key: KeyOperator): Promise { let running_min: number @@ -438,10 +687,20 @@ export class AsyncCollection { return running_min } + /** + * Return a collection of items that have the min value of the given key. + * @param {KeyOperator} key + * @return Promise + */ async whereMin(key: KeyOperator): Promise> { return this.where(key, '=', await this.min(key)) } + /** + * Merge the two collections. + * @param {AsyncCollectionComparable} merge_with + * @return Promise + */ async merge(merge_with: AsyncCollectionComparable): Promise> { let items: T2[] if ( merge_with instanceof Collection ) items = await merge_with.all() @@ -452,6 +711,11 @@ export class AsyncCollection { return new Collection([...items, ...await this.all()]) } + /** + * Return a collection of every nth item in this collection. + * @param {number} n + * @return Promise + */ async nth(n: number): Promise> { const matches: CollectionItem[] = [] let current = 1 @@ -467,12 +731,21 @@ export class AsyncCollection { return new Collection(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> { 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(func: AsyncCollectionFunction): any { return func(this) } @@ -484,6 +757,12 @@ export class AsyncCollection { } }*/ // 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 + */ async random(n: number): Promise> { const random_items: CollectionItem[] = [] const fetched_indices: number[] = [] @@ -505,6 +784,12 @@ export class AsyncCollection { return new Collection(random_items) } + /** + * Collapse the collection into a single value using a reducer function. + * @param {KeyReducerFunction} reducer + * @param [initial_value] + * @return Promise + */ async reduce(reducer: KeyReducerFunction, initial_value?: T2): Promise { let current_value = initial_value let index = 0 @@ -519,6 +804,11 @@ export class AsyncCollection { return current_value } + /** + * Returns a collection of items that fail the truth test. + * @param {AsyncKeyFunction} truth_test + * @return Promise + */ async reject(truth_test: AsyncKeyFunction): Promise> { let rejected: CollectionItem[] = [] @@ -531,10 +821,19 @@ export class AsyncCollection { return new Collection(rejected) } + /** + * Get a reversed collection of this collection's items. + * @return Promise + */ async reverse(): Promise> { return (await this.collect()).reverse() } + /** + * Search the collection and return the index of that item, if one exists. + * @param {CollectionItem} item + * @return Promise + */ async search(item: CollectionItem): Promise { let found_index let index = 0 @@ -554,6 +853,10 @@ export class AsyncCollection { return found_index } + /** + * Get the next item in the collection and remove it. + * @return Promise + */ async shift(): Promise> { const next_item = await this._items.next() if ( !next_item.done ) { @@ -561,34 +864,75 @@ export class AsyncCollection { } } + /** + * Shuffle the items in the collection to a random order. + * @return Promise + */ async shuffle(): Promise> { 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 + */ async slice(start: number, end: number): Promise> { return this._items.from_range(start, end - 1) } + /** + * Sort the collection, optionally with the given comparison function. + * @param {ComparisonFunction} compare_func + * @return Promise + */ async sort(compare_func?: ComparisonFunction): Promise> { return (await this.collect()).sort(compare_func) } + /** + * Sort the collection by the given key. + * @param {KeyOperator} key + * @return Promise + */ async sortBy(key?: KeyOperator): Promise> { return (await this.collect()).sortBy(key) } + /** + * Reverse sort the collection, optionally with the given comparison function. + * @param {ComparisonFunction} compare_func + * @return Promise + */ async sortDesc(compare_func?: ComparisonFunction): Promise> { return (await this.collect()).sortDesc(compare_func) } + /** + * Reverse sort the collection by the given key. + * @param {KeyOperator} key + * @return Promise + */ async sortByDesc(key?: KeyOperator): Promise> { 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 + */ async splice(start: CollectionIndex, deleteCount?: number): Promise> { 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 + */ async sum(key?: KeyOperator): Promise { let running_sum: number = 0 @@ -606,6 +950,11 @@ export class AsyncCollection { return running_sum } + /** + * Take the first n items from the front or back of the collection. + * @param {number} limit + * @return Promise + */ async take(limit: number): Promise> { if ( limit === 0 ) return new Collection() else if ( limit > 0 ) { @@ -616,11 +965,21 @@ export class AsyncCollection { } } + /** + * Call the given function, passing in this collection. Allows functional syntax. + * @param {AsyncCollectionFunction} func + * @return Promise + */ async tap(func: AsyncCollectionFunction): Promise> { 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 + */ async unique(key?: KeyOperator): Promise> { const has: CollectionItem[] = [] @@ -641,6 +1000,10 @@ export class AsyncCollection { return new Collection(has) } + /** + * Cast this collection to an array. + * @return Promise + */ async toArray(): Promise { const returns: any = [] for ( const item of (await this.all()) ) { @@ -651,14 +1014,29 @@ export class AsyncCollection { 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 { 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() } diff --git a/lib/src/collection/Iterable.ts b/lib/src/collection/Iterable.ts index 3fc9031..e8d51f1 100644 --- a/lib/src/collection/Iterable.ts +++ b/lib/src/collection/Iterable.ts @@ -5,14 +5,48 @@ export type ChunkCallback = (items: Collection) => any export class StopIteration extends Error {} +/** + * Abstract class representing an iterable, lazy-loaded dataset. + * @abstract + */ export abstract class Iterable { + /** + * 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 + */ abstract async at_index(i: number): Promise + + /** + * Get the collection of items in the given range of this iterable. + * @param {number} start + * @param {number} end + * @return Promise + */ abstract async from_range(start: number, end: number): Promise> + + /** + * Count the number of items in this collection. + * @return Promise + */ abstract async count(): Promise + + /** + * Get a copy of this iterable. + * @return Iterable + */ abstract clone(): Iterable + /** + * Advance to the next value of this iterable. + * @return Promise + */ public async next(): Promise> { const i = this.index @@ -24,10 +58,22 @@ export abstract class Iterable { 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 + */ public async chunk(size: number, callback: ChunkCallback) { const total = await this.count() @@ -45,17 +91,30 @@ export abstract class Iterable { } } + /** + * Advance the iterable to the given index. + * @param {number} index + * @return Promise + */ 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 + */ public async peek(): Promise { 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 + */ public async reset() { this.index = 0 } diff --git a/lib/src/collection/Where.ts b/lib/src/collection/Where.ts index 5d259ad..a70ea21 100755 --- a/lib/src/collection/Where.ts +++ b/lib/src/collection/Where.ts @@ -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 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 +/** + * Type representing the result of a where. + */ +type WhereResult = any[] +/** + * 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} items + * @param {WhereOperator} operator + * @param [operand] + */ const applyWhere = (items: AssociatedSearchItem[], operator: WhereOperator, operand?: any): WhereResult => { const matches: WhereResult = [] for ( const item of items ) { diff --git a/lib/src/const/http.ts b/lib/src/const/http.ts index 89bff0d..d909cd8 100644 --- a/lib/src/const/http.ts +++ b/lib/src/const/http.ts @@ -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', diff --git a/lib/src/const/status.ts b/lib/src/const/status.ts index 11b793a..3e1d71a 100644 --- a/lib/src/const/status.ts +++ b/lib/src/const/status.ts @@ -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, diff --git a/lib/src/error/HTTPError.ts b/lib/src/error/HTTPError.ts index 8a8aad7..a730746 100644 --- a/lib/src/error/HTTPError.ts +++ b/lib/src/error/HTTPError.ts @@ -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]}`) diff --git a/lib/src/error/RunLevelErrorHandler.ts b/lib/src/error/RunLevelErrorHandler.ts index 7a35cf0..8d2c6b7 100644 --- a/lib/src/error/RunLevelErrorHandler.ts +++ b/lib/src/error/RunLevelErrorHandler.ts @@ -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: diff --git a/lib/src/http/kernel/Kernel.ts b/lib/src/http/kernel/Kernel.ts index 6ce48f6..dc45d16 100644 --- a/lib/src/http/kernel/Kernel.ts +++ b/lib/src/http/kernel/Kernel.ts @@ -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) => Kernel, after: (other?: Instantiable) => 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 + */ protected preflight: Collection = new Collection() + + /** + * Module considered to be the main handler. + * @type Module + */ protected inflight?: Module + + /** + * Collection of postflight modules to apply. + * @type Collection + */ protected postflight: Collection = new Collection() + /** + * Handle the incoming request, applying the preflight modules, inflight module, then postflight modules. + * @param {Request} request + * @return Promise + */ public async handle(request: Request): Promise { 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 + * @return ModuleRegistrationFluency + */ public register(module: Instantiable): ModuleRegistrationFluency { this.make(Logging).verbose(`Registering HTTP kernel module: ${module.name}`) return { diff --git a/lib/src/http/kernel/Module.ts b/lib/src/http/kernel/Module.ts index 3740f03..984647e 100644 --- a/lib/src/http/kernel/Module.ts +++ b/lib/src/http/kernel/Module.ts @@ -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 + */ public async match(request: Request): Promise { return true } + /** + * Apply the module to the incoming request. + * @param {Request} request + * @return Promise + */ public async apply(request: Request): Promise { return request } + /** + * Register this module with the given HTTP kernel. + * @param {Kernel} kernel + */ public static register(kernel: Kernel) { kernel.register(this).before() } diff --git a/lib/src/http/kernel/module/ApplyRouteHandlers.ts b/lib/src/http/kernel/module/ApplyRouteHandlers.ts index 3aa6365..cfef163 100644 --- a/lib/src/http/kernel/module/ApplyRouteHandlers.ts +++ b/lib/src/http/kernel/module/ApplyRouteHandlers.ts @@ -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 + */ public async apply(request: Request): Promise { if ( !request.route ) { // Route not found const factory = http(HTTPStatus.NOT_FOUND) diff --git a/lib/src/http/kernel/module/InjectSession.ts b/lib/src/http/kernel/module/InjectSession.ts index f460eff..69777ac 100644 --- a/lib/src/http/kernel/module/InjectSession.ts +++ b/lib/src/http/kernel/module/InjectSession.ts @@ -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 { if ( request.session ) return request diff --git a/lib/src/http/kernel/module/MountActivatedRoute.ts b/lib/src/http/kernel/module/MountActivatedRoute.ts index b853049..88399f6 100644 --- a/lib/src/http/kernel/module/MountActivatedRoute.ts +++ b/lib/src/http/kernel/module/MountActivatedRoute.ts @@ -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 { let incoming = this.routing.resolve([request.path]) this.logger.info(`${request.method} ${incoming}`) diff --git a/lib/src/http/kernel/module/PersistSession.ts b/lib/src/http/kernel/module/PersistSession.ts index 0ee4517..ae7fa8b 100644 --- a/lib/src/http/kernel/module/PersistSession.ts +++ b/lib/src/http/kernel/module/PersistSession.ts @@ -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 { await request.session.persist() return request diff --git a/lib/src/http/kernel/module/PrepareRequest.ts b/lib/src/http/kernel/module/PrepareRequest.ts index dafcd81..66ab7e4 100644 --- a/lib/src/http/kernel/module/PrepareRequest.ts +++ b/lib/src/http/kernel/module/PrepareRequest.ts @@ -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 { await request.prepare() return request diff --git a/lib/src/http/kernel/module/SetDatonHeaders.ts b/lib/src/http/kernel/module/SetDatonHeaders.ts index 009906c..5ca0b57 100644 --- a/lib/src/http/kernel/module/SetDatonHeaders.ts +++ b/lib/src/http/kernel/module/SetDatonHeaders.ts @@ -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 { const text = this.config.get('server.powered_by.text', 'Daton') request.response.headers.set('X-Powered-By', text) diff --git a/lib/src/http/kernel/module/SetSessionCookie.ts b/lib/src/http/kernel/module/SetSessionCookie.ts index 8cccd0f..65025d3 100644 --- a/lib/src/http/kernel/module/SetSessionCookie.ts +++ b/lib/src/http/kernel/module/SetSessionCookie.ts @@ -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 { if ( !(await request.cookies.has('daton.session')) ) { const cookie = `${this.utility.uuid()}-${this.utility.uuid()}`