Start JSDocs
This commit is contained in:
parent
673fbc84f8
commit
c2a7c3f914
@ -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<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>()
|
||||
|
||||
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<any>) {
|
||||
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<any>) {
|
||||
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<DependencyKey> {
|
||||
const factory = this.resolve(target)
|
||||
|
||||
|
@ -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<DependencyRequirement>
|
||||
*/
|
||||
const initDependencyMetadata = (target: Object): Collection<DependencyRequirement> => {
|
||||
const param_types = Reflect.getMetadata('design:paramtypes', target)
|
||||
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 => {
|
||||
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) ) {
|
||||
|
@ -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) ) {
|
||||
|
@ -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<DependencyRequirement>
|
||||
*/
|
||||
abstract get_dependency_keys(): Collection<DependencyRequirement>
|
||||
}
|
||||
|
@ -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<any>
|
||||
|
@ -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)
|
||||
|
@ -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<any>,
|
||||
) {
|
||||
super(token)
|
||||
|
@ -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)
|
||||
|
@ -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<any> | StaticClass<any, any> | string
|
||||
|
||||
export { DependencyKey, DEPENDENCY_KEYS_METADATA_KEY }
|
||||
|
@ -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,
|
||||
|
@ -1,7 +1,14 @@
|
||||
/**
|
||||
* Interface that designates a particular value as able to be constructed.
|
||||
*/
|
||||
export default interface Instantiable<T> {
|
||||
new(...args: any[]): T
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given value is instantiable.
|
||||
* @param what
|
||||
*/
|
||||
const isInstantiable = <T>(what: any): what is Instantiable<T> => {
|
||||
return (typeof what === 'object' || typeof what === 'function') && 'constructor' in what && typeof what.constructor === 'function'
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
/**
|
||||
* Returns true if the parameter is a static class.
|
||||
* @param something
|
||||
*/
|
||||
export function isStaticClass<T, T2>(something: any): something is StaticClass<T, T2> {
|
||||
return typeof something === 'function' && typeof something.prototype !== 'undefined'
|
||||
}
|
||||
|
@ -1,8 +1,15 @@
|
||||
import { Iterable } from './Iterable.ts'
|
||||
import { collect } from './Collection.ts'
|
||||
|
||||
/**
|
||||
* A basic Iterable implementation that uses an array as a backend.
|
||||
* @extends Iterable
|
||||
*/
|
||||
export class ArrayIterable<T> extends Iterable<T> {
|
||||
constructor(
|
||||
/**
|
||||
* Items to use for this iterable.
|
||||
*/
|
||||
protected items: T[],
|
||||
) {
|
||||
super()
|
||||
|
@ -11,9 +11,21 @@ type AsyncCollectionComparable<T> = CollectionItem<T>[] | Collection<T> | AsyncC
|
||||
type AsyncKeyFunction<T, T2> = (item: CollectionItem<T>, index: number) => CollectionItem<T2> | Promise<CollectionItem<T2>>
|
||||
type AsyncCollectionFunction<T, T2> = (items: AsyncCollection<T>) => T2
|
||||
|
||||
/**
|
||||
* Like a collection, but asynchronous.
|
||||
*/
|
||||
export class AsyncCollection<T> {
|
||||
constructor(
|
||||
/**
|
||||
* Iterable of items to base this collction on.
|
||||
* @type Iterable
|
||||
*/
|
||||
private _items: Iterable<T>,
|
||||
|
||||
/**
|
||||
* Size to use when chunking results for memory-optimization.
|
||||
* @type number
|
||||
*/
|
||||
private _chunk_size: number = 1000, // TODO fix this. It's just for testing
|
||||
) {}
|
||||
|
||||
@ -57,14 +69,27 @@ export class AsyncCollection<T> {
|
||||
await this._items.reset()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all items in this collection as an array.
|
||||
* @return Promise<array>
|
||||
*/
|
||||
async all(): Promise<CollectionItem<T>[]> {
|
||||
return (await this._items.from_range(0, await this._items.count())).all()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all items in this collection as a synchronous Collection
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async collect(): Promise<Collection<T>> {
|
||||
return this._items.from_range(0, await this._items.count())
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the average value of the collection or one of its keys.
|
||||
* @param {KeyOperator} key
|
||||
* @return Promise<number>
|
||||
*/
|
||||
async average<T2>(key?: KeyOperator<T, T2>): Promise<number> {
|
||||
let running_total = 0
|
||||
let running_items = 0
|
||||
@ -82,6 +107,11 @@ export class AsyncCollection<T> {
|
||||
return running_total / running_items
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the median value of the collection or one of its keys.
|
||||
* @param {KeyOperator} key
|
||||
* @return Promise<number>
|
||||
*/
|
||||
async median<T2>(key?: KeyOperator<T, T2>): Promise<number> {
|
||||
let items: number[] = []
|
||||
|
||||
@ -100,6 +130,11 @@ export class AsyncCollection<T> {
|
||||
else return (items[middle] + items[middle + 1]) / 2
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mode value of the collection or one of its keys.
|
||||
* @param {KeyOperator} key
|
||||
* @return Promise<number>
|
||||
*/
|
||||
async mode<T2>(key?: KeyOperator<T, T2>): Promise<number> {
|
||||
let counts: any = {}
|
||||
|
||||
@ -118,11 +153,24 @@ export class AsyncCollection<T> {
|
||||
return Number(Object.keys(counts).reduce((a, b) => counts[a] > counts[b] ? a : b)[0])
|
||||
}
|
||||
|
||||
/**
|
||||
* If this collection contains nested collections, collapse them to a single level.
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async collapse(): Promise<Collection<any>> {
|
||||
const items = await this.collect()
|
||||
return items.collapse() as Collection<any>
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the collection contains an item satisfying the given collection.
|
||||
* @example
|
||||
* collection.contains('id', '>', 4)
|
||||
* @param {KeyOperator} key
|
||||
* @param {WhereOperator} operator
|
||||
* @param [operand]
|
||||
* @return Promise<boolean>
|
||||
*/
|
||||
async contains<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): Promise<boolean> {
|
||||
let contains = false
|
||||
|
||||
@ -137,10 +185,19 @@ export class AsyncCollection<T> {
|
||||
return contains
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a clean instance of this collection pointing to the same result set of the iterable.
|
||||
* @return Promise<AsyncCollection>
|
||||
*/
|
||||
async clone(): Promise<AsyncCollection<T>> {
|
||||
return new AsyncCollection<T>(await this._items.clone())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the elements that are different between the two collections.
|
||||
* @param {AsyncCollectionComparable} items
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async diff(items: AsyncCollectionComparable<T>): Promise<Collection<T>> {
|
||||
const matches: T[] = []
|
||||
|
||||
@ -155,6 +212,13 @@ export class AsyncCollection<T> {
|
||||
return new Collection<T>(matches)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the elements that are different between the two collections, using the given function
|
||||
* as a comparator for the elements.
|
||||
* @param {AsyncCollectionComparable} items
|
||||
* @param {DeterminesEquality} compare
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async diffUsing(items: AsyncCollectionComparable<T>, compare: DeterminesEquality<T>): Promise<Collection<T>> {
|
||||
const matches: T[] = []
|
||||
|
||||
@ -168,6 +232,11 @@ export class AsyncCollection<T> {
|
||||
return new Collection<T>(matches)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given item is present in the collection.
|
||||
* @param item
|
||||
* @return Promise<boolean>
|
||||
*/
|
||||
async includes(item: CollectionItem<T>): Promise<boolean> {
|
||||
let contains = false
|
||||
|
||||
@ -181,6 +250,11 @@ export class AsyncCollection<T> {
|
||||
return contains
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there is an item in the collection for which the given operator returns true.
|
||||
* @param {function} operator - item => boolean
|
||||
* @return Promise<boolean>
|
||||
*/
|
||||
async some(operator: (item: T) => boolean): Promise<boolean> {
|
||||
let contains = false
|
||||
|
||||
@ -196,6 +270,11 @@ export class AsyncCollection<T> {
|
||||
return contains
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a callback to each item in the collection.
|
||||
* @param {AsyncKeyFunction} func
|
||||
* @return Promise<void>
|
||||
*/
|
||||
async each<T2>(func: AsyncKeyFunction<T, T2>): Promise<void> {
|
||||
let index = 0
|
||||
|
||||
@ -207,6 +286,11 @@ export class AsyncCollection<T> {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a callback to each item in the collection and returns the results as a collection.
|
||||
* @param {AsyncKeyFunction} func
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async map<T2>(func: AsyncKeyFunction<T, T2>): Promise<Collection<T2>> {
|
||||
const new_items: CollectionItem<T2>[] = []
|
||||
await this.each(async (item, index) => {
|
||||
@ -215,6 +299,11 @@ export class AsyncCollection<T> {
|
||||
return new Collection<T2>(new_items)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given operator returns true for every item in the collection.
|
||||
* @param {AsyncKeyFunction} func
|
||||
* @return Promise<boolean>
|
||||
*/
|
||||
async every<T2>(func: AsyncKeyFunction<T, T2>): Promise<boolean> {
|
||||
let pass = true
|
||||
let index = 0
|
||||
@ -233,6 +322,12 @@ export class AsyncCollection<T> {
|
||||
return pass
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if every item in the collection satisfies the given where clause.
|
||||
* @param {KeyOperator} key
|
||||
* @param {WhereOperator} operator
|
||||
* @param [operand]
|
||||
*/
|
||||
async everyWhere<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): Promise<boolean> {
|
||||
let pass = true
|
||||
|
||||
@ -246,6 +341,11 @@ export class AsyncCollection<T> {
|
||||
return pass
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a filter to every item in the collection and returns the results that pass the filter.
|
||||
* @param {KeyFunction} func
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async filter<T2>(func: KeyFunction<T, T2>): Promise<Collection<T>> {
|
||||
let new_items: CollectionItem<T>[] = []
|
||||
|
||||
@ -256,16 +356,35 @@ export class AsyncCollection<T> {
|
||||
return new Collection<T>(new_items)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the passed in function if the boolean condition is true. Allows for functional syntax.
|
||||
* @param {boolean} bool
|
||||
* @param {AsyncCollectionFunction} then
|
||||
* @return AsyncCollection
|
||||
*/
|
||||
when<T2>(bool: boolean, then: AsyncCollectionFunction<T, T2>): AsyncCollection<T> {
|
||||
if ( bool ) then(this)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the passed in function if the boolean condition is false. Allows for functional syntax.
|
||||
* @param {boolean} bool
|
||||
* @param {AsyncCollectionFunction} then
|
||||
* @return AsyncCollection
|
||||
*/
|
||||
unless<T2>(bool: boolean, then: AsyncCollectionFunction<T, T2>): AsyncCollection<T> {
|
||||
if ( !bool ) then(this)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the given where condition to the collection and returns a new collection of the results.
|
||||
* @param {KeyOperator} key
|
||||
* @param {WhereOperator} operator
|
||||
* @param [operand]
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async where<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): Promise<Collection<T>> {
|
||||
let new_items: CollectionItem<T>[] = []
|
||||
await this._chunk(async items => {
|
||||
@ -274,6 +393,14 @@ export class AsyncCollection<T> {
|
||||
return new Collection<T>(new_items)
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the given where condition to the collection and returns a new collection of the items
|
||||
* that did not satisfy the condition.
|
||||
* @param {KeyOperator} key
|
||||
* @param {WhereOperator} operator
|
||||
* @param [operand]
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async whereNot<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): Promise<Collection<T>> {
|
||||
let new_items: CollectionItem<T>[] = []
|
||||
await this._chunk(async items => {
|
||||
@ -282,6 +409,12 @@ export class AsyncCollection<T> {
|
||||
return new Collection<T>(new_items)
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a WHERE ... IN ... condition to the collection an returns a new collection of the results.
|
||||
* @param {KeyOperator} key
|
||||
* @param {AsyncCollectionComparable} items
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async whereIn<T2>(key: KeyOperator<T, T2>, items: AsyncCollectionComparable<T2>): Promise<Collection<T>> {
|
||||
let new_items: CollectionItem<T>[] = []
|
||||
await this._chunk_all_associate(key,async chunk => {
|
||||
@ -294,6 +427,13 @@ export class AsyncCollection<T> {
|
||||
return new Collection<T>(new_items)
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a WHERE ... IN ... condition to the collection and returns a new collection of the items
|
||||
* that did not satisfy the condition.
|
||||
* @param {KeyOperator} key
|
||||
* @param {AsyncCollectionComparable} items
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async whereNotIn<T2>(key: KeyOperator<T, T2>, items: AsyncCollectionComparable<T2>): Promise<Collection<T>> {
|
||||
let new_items: CollectionItem<T>[] = []
|
||||
await this._chunk_all_associate(key,async chunk => {
|
||||
@ -306,12 +446,23 @@ export class AsyncCollection<T> {
|
||||
return new Collection<T>(new_items)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first item in the collection, if one exists.
|
||||
* @return Promise<MaybeCollectionItem>
|
||||
*/
|
||||
async first(): Promise<MaybeCollectionItem<T>> {
|
||||
if ( await this._items.count() > 0 ) {
|
||||
return this._items.at_index(0)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the first item in the collection that satisfies the given where condition, if one exists.
|
||||
* @param {KeyOperator} key
|
||||
* @param {WhereOperator} [operator = '=']
|
||||
* @param [operand = true]
|
||||
* @return Promise<MaybeCollectionItem>
|
||||
*/
|
||||
async firstWhere<T2>(key: KeyOperator<T, T2>, operator: WhereOperator = '=', operand: any = true): Promise<MaybeCollectionItem<T>> {
|
||||
let item = undefined
|
||||
await this._chunk_all_associate(key, async items => {
|
||||
@ -324,6 +475,12 @@ export class AsyncCollection<T> {
|
||||
return item
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the first item in the collection that does not satisfy the given where condition, if one exists.
|
||||
* @param {KeyOperator} key
|
||||
* @param {WhereOperator} [operator = '=']
|
||||
* @param [operand = true]
|
||||
*/
|
||||
async firstWhereNot<T2>(key: KeyOperator<T, T2>, operator: WhereOperator = '=', operand: any = true): Promise<MaybeCollectionItem<T>> {
|
||||
let item: MaybeCollectionItem<T> = undefined
|
||||
await this._chunk(async items => {
|
||||
@ -336,31 +493,67 @@ export class AsyncCollection<T> {
|
||||
return item
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of elements in this collection.
|
||||
* @return Promise<number>
|
||||
*/
|
||||
async count() {
|
||||
return this._items.count()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of elements in this collection.
|
||||
* @return Promise<number>
|
||||
*/
|
||||
async length() {
|
||||
return this._items.count()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the item at the given index of this collection, if one exists.
|
||||
* If none exists and a fallback value is provided, that value will be returned.
|
||||
* @param {number} index
|
||||
* @param [fallback]
|
||||
* @return Promise<any>
|
||||
*/
|
||||
async get(index: number, fallback?: any) {
|
||||
if ( (await this.count()) > index ) return this._items.at_index(index)
|
||||
else return fallback
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the item at the given index of this collection, if one exists.
|
||||
* @param {number} index
|
||||
*/
|
||||
async at(index: number): Promise<MaybeCollectionItem<T>> {
|
||||
return this.get(index)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an object which maps key values to arrays of items in the collection that satisfy that value.
|
||||
* @param {KeyOperator} key
|
||||
* @return Promise<object>
|
||||
*/
|
||||
async groupBy<T2>(key: KeyOperator<T, T2>): Promise<any> {
|
||||
return (await this.collect()).groupBy(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an object mapping the given key value to items in this collection.
|
||||
* @param {KeyOperator} key
|
||||
* @return Promise<object>
|
||||
*/
|
||||
async associate<T2>(key: KeyOperator<T, T2>): Promise<any> {
|
||||
return (await this.collect()).associate(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Join the items in this collection with the given delimiter.
|
||||
* @example
|
||||
* await collection.join(',') // => '1,2,3,4'
|
||||
* @param {string} delimiter
|
||||
* @return Promise<string>
|
||||
*/
|
||||
async join(delimiter: string): Promise<string> {
|
||||
let running_strings: string[] = []
|
||||
|
||||
@ -371,33 +564,74 @@ export class AsyncCollection<T> {
|
||||
return running_strings.join(delimiter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Join the items in this collection with the given delimiter.
|
||||
* @example
|
||||
* await collection.implode(',') // => '1,2,3,4'
|
||||
* @param {string} delimiter
|
||||
* @return Promise<string>
|
||||
*/
|
||||
async implode(delimiter: string): Promise<string> {
|
||||
return this.join(delimiter)
|
||||
}
|
||||
|
||||
// TODO intersect
|
||||
|
||||
/**
|
||||
* Returns true if there are no items in this collection.
|
||||
* @return Promise<boolean>
|
||||
*/
|
||||
async isEmpty(): Promise<boolean> {
|
||||
return (await this._items.count()) < 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there is at least one item in this collection.
|
||||
* @return Promise<boolean>
|
||||
*/
|
||||
async isNotEmpty(): Promise<boolean> {
|
||||
return (await this._items.count()) > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last item in this collection, if one exists.
|
||||
* @return Promise<MaybeCollectionItem>
|
||||
*/
|
||||
async last(): Promise<MaybeCollectionItem<T>> {
|
||||
const length = await this._items.count()
|
||||
if ( length > 0 ) return this._items.at_index(length - 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last item in this collection which satisfies the given where condition, if one exists.
|
||||
* @param {KeyOperator} key
|
||||
* @param {WhereOperator} operator
|
||||
* @param [operand]
|
||||
* @return Promise<MaybeCollectionItem>
|
||||
*/
|
||||
async lastWhere<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): Promise<MaybeCollectionItem<T>> {
|
||||
return (await this.where(key, operator, operand)).last()
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last item in this collection which does not satisfy the given condition, if one exists.
|
||||
* @param {KeyOperator} key
|
||||
* @param {WhereOperator} operator
|
||||
* @param [operand]
|
||||
* @return Promise<MaybeCollectionItem>
|
||||
*/
|
||||
async lastWhereNot<T2>(key: KeyOperator<T, T2>, operator: WhereOperator, operand?: any): Promise<MaybeCollectionItem<T>> {
|
||||
return (await this.whereNot(key, operator, operand)).last()
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a collection of the values of a given key for each item in this collection.
|
||||
* @example
|
||||
* // collection has { a: 1 }, { a: 2 }, { a: 3 }
|
||||
* await collection.pluck('a') // => [1, 2, 3]
|
||||
* @param {KeyOperator} key
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async pluck<T2>(key: KeyOperator<T, T2>): Promise<Collection<T2>> {
|
||||
let new_items: CollectionItem<T2>[] = []
|
||||
|
||||
@ -408,6 +642,11 @@ export class AsyncCollection<T> {
|
||||
return new Collection<T2>(new_items)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the max value of the given key.
|
||||
* @param {KeyOperator} key
|
||||
* @return Promise<number>
|
||||
*/
|
||||
async max<T2>(key: KeyOperator<T, T2>): Promise<number> {
|
||||
let running_max: number
|
||||
|
||||
@ -421,10 +660,20 @@ export class AsyncCollection<T> {
|
||||
return running_max
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a collection of items that have the max value of the given key.
|
||||
* @param {KeyOperator} key
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async whereMax<T2>(key: KeyOperator<T, T2>): Promise<Collection<T>> {
|
||||
return this.where(key, '=', await this.max(key))
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the min value of the given key.
|
||||
* @param {KeyOperator} key
|
||||
* @return Promise<number>
|
||||
*/
|
||||
async min<T2>(key: KeyOperator<T, T2>): Promise<number> {
|
||||
let running_min: number
|
||||
|
||||
@ -438,10 +687,20 @@ export class AsyncCollection<T> {
|
||||
return running_min
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a collection of items that have the min value of the given key.
|
||||
* @param {KeyOperator} key
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async whereMin<T2>(key: KeyOperator<T, T2>): Promise<Collection<T>> {
|
||||
return this.where(key, '=', await this.min(key))
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the two collections.
|
||||
* @param {AsyncCollectionComparable} merge_with
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async merge<T2>(merge_with: AsyncCollectionComparable<T2>): Promise<Collection<T|T2>> {
|
||||
let items: T2[]
|
||||
if ( merge_with instanceof Collection ) items = await merge_with.all()
|
||||
@ -452,6 +711,11 @@ export class AsyncCollection<T> {
|
||||
return new Collection<T|T2>([...items, ...await this.all()])
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a collection of every nth item in this collection.
|
||||
* @param {number} n
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async nth(n: number): Promise<Collection<T>> {
|
||||
const matches: CollectionItem<T>[] = []
|
||||
let current = 1
|
||||
@ -467,12 +731,21 @@ export class AsyncCollection<T> {
|
||||
return new Collection<T>(matches)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a collection containing the items that would be on the given page, with the given number of items per page.
|
||||
* @param {number} page
|
||||
* @param {number} per_page
|
||||
*/
|
||||
async forPage(page: number, per_page: number): Promise<Collection<T>> {
|
||||
const start = page * per_page - per_page
|
||||
const end = page * per_page - 1
|
||||
return this._items.from_range(start, end)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the value of the function, passing this collection to it.
|
||||
* @param {AsyncCollectionFunction} func
|
||||
*/
|
||||
pipe<T2>(func: AsyncCollectionFunction<T, T2>): any {
|
||||
return func(this)
|
||||
}
|
||||
@ -484,6 +757,12 @@ export class AsyncCollection<T> {
|
||||
}
|
||||
}*/ // TODO Fix this
|
||||
|
||||
/**
|
||||
* Get n random items from this collection.
|
||||
* @todo add safety check for it loop exceeds max number of items
|
||||
* @param {number} n
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async random(n: number): Promise<Collection<T>> {
|
||||
const random_items: CollectionItem<T>[] = []
|
||||
const fetched_indices: number[] = []
|
||||
@ -505,6 +784,12 @@ export class AsyncCollection<T> {
|
||||
return new Collection<T>(random_items)
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse the collection into a single value using a reducer function.
|
||||
* @param {KeyReducerFunction} reducer
|
||||
* @param [initial_value]
|
||||
* @return Promise<any>
|
||||
*/
|
||||
async reduce<T2>(reducer: KeyReducerFunction<T, T2>, initial_value?: T2): Promise<T2 | undefined> {
|
||||
let current_value = initial_value
|
||||
let index = 0
|
||||
@ -519,6 +804,11 @@ export class AsyncCollection<T> {
|
||||
return current_value
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a collection of items that fail the truth test.
|
||||
* @param {AsyncKeyFunction} truth_test
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async reject<T2>(truth_test: AsyncKeyFunction<T, T2>): Promise<Collection<T>> {
|
||||
let rejected: CollectionItem<T>[] = []
|
||||
|
||||
@ -531,10 +821,19 @@ export class AsyncCollection<T> {
|
||||
return new Collection<T>(rejected)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a reversed collection of this collection's items.
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async reverse(): Promise<Collection<T>> {
|
||||
return (await this.collect()).reverse()
|
||||
}
|
||||
|
||||
/**
|
||||
* Search the collection and return the index of that item, if one exists.
|
||||
* @param {CollectionItem} item
|
||||
* @return Promise<MaybeCollectionIndex>
|
||||
*/
|
||||
async search(item: CollectionItem<T>): Promise<MaybeCollectionIndex> {
|
||||
let found_index
|
||||
let index = 0
|
||||
@ -554,6 +853,10 @@ export class AsyncCollection<T> {
|
||||
return found_index
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next item in the collection and remove it.
|
||||
* @return Promise<MaybeCollectionItem>
|
||||
*/
|
||||
async shift(): Promise<MaybeCollectionItem<T>> {
|
||||
const next_item = await this._items.next()
|
||||
if ( !next_item.done ) {
|
||||
@ -561,34 +864,75 @@ export class AsyncCollection<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuffle the items in the collection to a random order.
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async shuffle(): Promise<Collection<T>> {
|
||||
return (await this.collect()).shuffle()
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a slice of this collection.
|
||||
* @param {number} start - the starting index
|
||||
* @param {number} end - the ending index
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async slice(start: number, end: number): Promise<Collection<T>> {
|
||||
return this._items.from_range(start, end - 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort the collection, optionally with the given comparison function.
|
||||
* @param {ComparisonFunction} compare_func
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async sort(compare_func?: ComparisonFunction<T>): Promise<Collection<T>> {
|
||||
return (await this.collect()).sort(compare_func)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort the collection by the given key.
|
||||
* @param {KeyOperator} key
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async sortBy<T2>(key?: KeyOperator<T, T2>): Promise<Collection<T>> {
|
||||
return (await this.collect()).sortBy(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse sort the collection, optionally with the given comparison function.
|
||||
* @param {ComparisonFunction} compare_func
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async sortDesc(compare_func?: ComparisonFunction<T>): Promise<Collection<T>> {
|
||||
return (await this.collect()).sortDesc(compare_func)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse sort the collection by the given key.
|
||||
* @param {KeyOperator} key
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async sortByDesc<T2>(key?: KeyOperator<T, T2>): Promise<Collection<T>> {
|
||||
return (await this.collect()).sortByDesc(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Splice the collection at the given index. Optionally, removing the given number of items.
|
||||
* @param {CollectionIndex} start
|
||||
* @param {number} [deleteCount]
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async splice(start: CollectionIndex, deleteCount?: number): Promise<Collection<T>> {
|
||||
return (await this.collect()).splice(start, deleteCount)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sum the items in the collection, or the values of the given key.
|
||||
* @param {KeyOperator} key
|
||||
* @return Promise<number>
|
||||
*/
|
||||
async sum<T2>(key?: KeyOperator<T, T2>): Promise<number> {
|
||||
let running_sum: number = 0
|
||||
|
||||
@ -606,6 +950,11 @@ export class AsyncCollection<T> {
|
||||
return running_sum
|
||||
}
|
||||
|
||||
/**
|
||||
* Take the first n items from the front or back of the collection.
|
||||
* @param {number} limit
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async take(limit: number): Promise<Collection<T>> {
|
||||
if ( limit === 0 ) return new Collection<T>()
|
||||
else if ( limit > 0 ) {
|
||||
@ -616,11 +965,21 @@ export class AsyncCollection<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the given function, passing in this collection. Allows functional syntax.
|
||||
* @param {AsyncCollectionFunction} func
|
||||
* @return Promise<AsyncCollection>
|
||||
*/
|
||||
async tap<T2>(func: AsyncCollectionFunction<T, T2>): Promise<AsyncCollection<T>> {
|
||||
await func(this)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all the unique values in the collection, or the unique values of the given key.
|
||||
* @param {KeyOperator} key
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
async unique<T2>(key?: KeyOperator<T, T2>): Promise<Collection<T|T2>> {
|
||||
const has: CollectionItem<T|T2>[] = []
|
||||
|
||||
@ -641,6 +1000,10 @@ export class AsyncCollection<T> {
|
||||
return new Collection<T|T2>(has)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast this collection to an array.
|
||||
* @return Promise<array>
|
||||
*/
|
||||
async toArray(): Promise<any[]> {
|
||||
const returns: any = []
|
||||
for ( const item of (await this.all()) ) {
|
||||
@ -651,14 +1014,29 @@ export class AsyncCollection<T> {
|
||||
return returns
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast this collection to a JSON string.
|
||||
* @param [replacer] - the replacer to use
|
||||
* @param {number} [space = 4] number of indentation spaces to use
|
||||
*/
|
||||
async toJSON(replacer = undefined, space = 4): Promise<string> {
|
||||
return JSON.stringify(this.toArray(), replacer, space)
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterator to support async iteration:
|
||||
*
|
||||
* @example
|
||||
* for await (const item of collection) {}
|
||||
*/
|
||||
[Symbol.asyncIterator]() {
|
||||
return this._items.clone()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a clone of the underlying iterator of this collection.
|
||||
* @return Iterable
|
||||
*/
|
||||
iterator() {
|
||||
return this._items.clone()
|
||||
}
|
||||
|
@ -5,14 +5,48 @@ export type ChunkCallback<T> = (items: Collection<T>) => any
|
||||
|
||||
export class StopIteration extends Error {}
|
||||
|
||||
/**
|
||||
* Abstract class representing an iterable, lazy-loaded dataset.
|
||||
* @abstract
|
||||
*/
|
||||
export abstract class Iterable<T> {
|
||||
/**
|
||||
* The current index of the iterable.
|
||||
* @type number
|
||||
*/
|
||||
protected index = 0
|
||||
|
||||
/**
|
||||
* Get the item of this iterable at the given index, if one exists.
|
||||
* @param {number} i
|
||||
* @return Promise<any|undefined>
|
||||
*/
|
||||
abstract async at_index(i: number): Promise<T | undefined>
|
||||
|
||||
/**
|
||||
* Get the collection of items in the given range of this iterable.
|
||||
* @param {number} start
|
||||
* @param {number} end
|
||||
* @return Promise<Collection>
|
||||
*/
|
||||
abstract async from_range(start: number, end: number): Promise<Collection<T>>
|
||||
|
||||
/**
|
||||
* Count the number of items in this collection.
|
||||
* @return Promise<number>
|
||||
*/
|
||||
abstract async count(): Promise<number>
|
||||
|
||||
/**
|
||||
* Get a copy of this iterable.
|
||||
* @return Iterable
|
||||
*/
|
||||
abstract clone(): Iterable<T>
|
||||
|
||||
/**
|
||||
* Advance to the next value of this iterable.
|
||||
* @return Promise<MaybeIterationItem>
|
||||
*/
|
||||
public async next(): Promise<MaybeIterationItem<T>> {
|
||||
const i = this.index
|
||||
|
||||
@ -24,10 +58,22 @@ export abstract class Iterable<T> {
|
||||
return { done: false, value: await this.at_index(i) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to enable async iteration.
|
||||
*
|
||||
* @example
|
||||
* for await (const item of iterable) {}
|
||||
*/
|
||||
[Symbol.asyncIterator]() {
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunk the iterable into the given size and call the callback passing the chunk along.
|
||||
* @param {number} size
|
||||
* @param {ChunkCallback} callback
|
||||
* @return Promise<void>
|
||||
*/
|
||||
public async chunk(size: number, callback: ChunkCallback<T>) {
|
||||
const total = await this.count()
|
||||
|
||||
@ -45,17 +91,30 @@ export abstract class Iterable<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Advance the iterable to the given index.
|
||||
* @param {number} index
|
||||
* @return Promise<void>
|
||||
*/
|
||||
public async seek(index: number) {
|
||||
if ( index < 0 ) throw new TypeError('Cannot seek to negative index.')
|
||||
else if ( index >= await this.count() ) throw new TypeError('Cannot seek past last item.')
|
||||
this.index = index
|
||||
}
|
||||
|
||||
/**
|
||||
* Peek at the next value of the iterable, without advancing.
|
||||
* @return Promise<any|undefined>
|
||||
*/
|
||||
public async peek(): Promise<T | undefined> {
|
||||
if ( this.index + 1 >= await this.count() ) return undefined
|
||||
else return this.at_index(this.index + 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the iterable to the first index.
|
||||
* @return Promise<any>
|
||||
*/
|
||||
public async reset() {
|
||||
this.index = 0
|
||||
}
|
||||
|
@ -1,20 +1,25 @@
|
||||
/**
|
||||
* Type representing a valid where operator.
|
||||
*/
|
||||
type WhereOperator = '&' | '>' | '>=' | '<' | '<=' | '!=' | '<=>' | '%' | '|' | '!' | '~' | '=' | '^'
|
||||
|
||||
/**
|
||||
* Type associating search items with a key.
|
||||
*/
|
||||
type AssociatedSearchItem = { key: any, item: any }
|
||||
|
||||
/**
|
||||
* Type representing the result of a where.
|
||||
*/
|
||||
type WhereResult = any[]
|
||||
|
||||
// bitwise and
|
||||
// greater than
|
||||
// greater than equal
|
||||
// less than
|
||||
// less than equal
|
||||
// not equal
|
||||
// null-safe equal
|
||||
// modulo
|
||||
// bitwise or
|
||||
// not
|
||||
// bitwise negation
|
||||
// equal
|
||||
|
||||
/**
|
||||
* Returns true if the given item satisfies the given where clause.
|
||||
* @param {AssociatedSearchItem} item
|
||||
* @param {WhereOperator} operator
|
||||
* @param [operand]
|
||||
* @return boolean
|
||||
*/
|
||||
const whereMatch = (item: AssociatedSearchItem, operator: WhereOperator, operand?: any): boolean => {
|
||||
switch ( operator ) {
|
||||
case '&':
|
||||
@ -62,6 +67,12 @@ const whereMatch = (item: AssociatedSearchItem, operator: WhereOperator, operand
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the given where clause to the items and return those that match.
|
||||
* @param {Array<AssociatedSearchItem>} items
|
||||
* @param {WhereOperator} operator
|
||||
* @param [operand]
|
||||
*/
|
||||
const applyWhere = (items: AssociatedSearchItem[], operator: WhereOperator, operand?: any): WhereResult => {
|
||||
const matches: WhereResult = []
|
||||
for ( const item of items ) {
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
|
@ -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]}`)
|
||||
|
@ -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:
|
||||
|
@ -6,6 +6,9 @@ import {Service} from '../../../../di/src/decorator/Service.ts'
|
||||
import {Request} from '../Request.ts'
|
||||
import {Logging} from '../../service/logging/Logging.ts'
|
||||
|
||||
/**
|
||||
* Interface for fluently registering kernel modules into the kernel.
|
||||
*/
|
||||
export interface ModuleRegistrationFluency {
|
||||
before: (other?: Instantiable<Module>) => Kernel,
|
||||
after: (other?: Instantiable<Module>) => Kernel,
|
||||
@ -14,18 +17,45 @@ export interface ModuleRegistrationFluency {
|
||||
core: () => Kernel,
|
||||
}
|
||||
|
||||
/**
|
||||
* Error thrown when a kernel module is requested that does not exist w/in the kernel.
|
||||
* @extends Error
|
||||
*/
|
||||
export class KernelModuleNotFoundError extends Error {
|
||||
constructor(mod_name: string) {
|
||||
super(`The kernel module ${mod_name} is not registered with the kernel.`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A basic HTTP kernel used to process incoming and outgoing middleware.
|
||||
* @extends AppClass
|
||||
*/
|
||||
@Service()
|
||||
export default class Kernel extends AppClass {
|
||||
/**
|
||||
* Collection of preflight modules to apply.
|
||||
* @type Collection<Module>
|
||||
*/
|
||||
protected preflight: Collection<Module> = new Collection<Module>()
|
||||
|
||||
/**
|
||||
* Module considered to be the main handler.
|
||||
* @type Module
|
||||
*/
|
||||
protected inflight?: Module
|
||||
|
||||
/**
|
||||
* Collection of postflight modules to apply.
|
||||
* @type Collection<Module>
|
||||
*/
|
||||
protected postflight: Collection<Module> = new Collection<Module>()
|
||||
|
||||
/**
|
||||
* Handle the incoming request, applying the preflight modules, inflight module, then postflight modules.
|
||||
* @param {Request} request
|
||||
* @return Promise<Request>
|
||||
*/
|
||||
public async handle(request: Request): Promise<Request> {
|
||||
const logger = this.make(Logging)
|
||||
|
||||
@ -47,6 +77,11 @@ export default class Kernel extends AppClass {
|
||||
return request
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a fluent interface for registering the given kernel module.
|
||||
* @param {Instantiable<Module>} module
|
||||
* @return ModuleRegistrationFluency
|
||||
*/
|
||||
public register(module: Instantiable<Module>): ModuleRegistrationFluency {
|
||||
this.make(Logging).verbose(`Registering HTTP kernel module: ${module.name}`)
|
||||
return {
|
||||
|
@ -2,15 +2,33 @@ import {Request} from '../Request.ts'
|
||||
import Kernel from './Kernel.ts'
|
||||
import AppClass from '../../lifecycle/AppClass.ts'
|
||||
|
||||
/**
|
||||
* Base class for HTTP kernel modules.
|
||||
* @extends AppClass
|
||||
*/
|
||||
export default class Module extends AppClass {
|
||||
/**
|
||||
* Returns true if the given module should be applied to the incoming request.
|
||||
* @param {Request} request
|
||||
* @return Promise<boolean>
|
||||
*/
|
||||
public async match(request: Request): Promise<boolean> {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the module to the incoming request.
|
||||
* @param {Request} request
|
||||
* @return Promise<Request>
|
||||
*/
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
return request
|
||||
}
|
||||
|
||||
/**
|
||||
* Register this module with the given HTTP kernel.
|
||||
* @param {Kernel} kernel
|
||||
*/
|
||||
public static register(kernel: Kernel) {
|
||||
kernel.register(this).before()
|
||||
}
|
||||
|
@ -7,6 +7,10 @@ import ResponseFactory from '../../response/ResponseFactory.ts'
|
||||
import {http, error} from '../../response/helpers.ts'
|
||||
import {HTTPStatus} from '../../../const/http.ts'
|
||||
|
||||
/**
|
||||
* HTTP kernel module to apply route handlers to the incoming request.
|
||||
* @extends Module
|
||||
*/
|
||||
@Injectable()
|
||||
export default class ApplyRouteHandlers extends Module {
|
||||
public static register(kernel: Kernel) {
|
||||
@ -19,6 +23,11 @@ export default class ApplyRouteHandlers extends Module {
|
||||
super()
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the route handler to the request.
|
||||
* @param {Request} request
|
||||
* @return Promise<Request>
|
||||
*/
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
if ( !request.route ) { // Route not found
|
||||
const factory = http(HTTPStatus.NOT_FOUND)
|
||||
|
@ -6,6 +6,10 @@ import SessionManager from '../../session/SessionManager.ts'
|
||||
import {Logging} from '../../../service/logging/Logging.ts'
|
||||
import {Injectable} from '../../../../../di/src/decorator/Injection.ts'
|
||||
|
||||
/**
|
||||
* HTTP kernel module to retrieve and inject the session into the request.
|
||||
* @extends Module
|
||||
*/
|
||||
@Injectable()
|
||||
export default class InjectSession extends Module {
|
||||
public static register(kernel: Kernel) {
|
||||
@ -19,6 +23,10 @@ export default class InjectSession extends Module {
|
||||
super()
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup or create the session object and inject it into the request.
|
||||
* @param {Request} request
|
||||
*/
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
if ( request.session ) return request
|
||||
|
||||
|
@ -8,6 +8,10 @@ import Config from '../../../unit/Config.ts'
|
||||
import ActivatedRoute from '../../routing/ActivatedRoute.ts'
|
||||
import {Injectable} from '../../../../../di/src/decorator/Injection.ts'
|
||||
|
||||
/**
|
||||
* HTTP kernel middleware to resolve and mount the registered route onto the request.
|
||||
* @extends Module
|
||||
*/
|
||||
@Injectable()
|
||||
export default class MountActivatedRoute extends Module {
|
||||
public static register(kernel: Kernel) {
|
||||
@ -22,6 +26,10 @@ export default class MountActivatedRoute extends Module {
|
||||
super()
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and resolve the route and mount it into the request object.
|
||||
* @param {Request} request
|
||||
*/
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
let incoming = this.routing.resolve([request.path])
|
||||
this.logger.info(`${request.method} ${incoming}`)
|
||||
|
@ -2,11 +2,19 @@ import Module from '../Module.ts'
|
||||
import Kernel from '../Kernel.ts'
|
||||
import {Request} from '../../Request.ts'
|
||||
|
||||
/**
|
||||
* Persist the session data before closing the request.
|
||||
* @extends Module
|
||||
*/
|
||||
export default class PersistSession extends Module {
|
||||
public static register(kernel: Kernel) {
|
||||
kernel.register(this).last()
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the session.
|
||||
* @param {Request} request
|
||||
*/
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
await request.session.persist()
|
||||
return request
|
||||
|
@ -2,12 +2,20 @@ import Module from '../Module.ts'
|
||||
import Kernel from '../Kernel.ts'
|
||||
import {Request} from '../../Request.ts'
|
||||
|
||||
/**
|
||||
* HTTP kernel module to call the request's prepare method.
|
||||
* @extends Module
|
||||
*/
|
||||
export default class PrepareRequest extends Module {
|
||||
|
||||
public static register(kernel: Kernel) {
|
||||
kernel.register(this).first()
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the request for Daton processing.
|
||||
* @param {Request} request
|
||||
*/
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
await request.prepare()
|
||||
return request
|
||||
|
@ -4,6 +4,10 @@ import {Request} from '../../Request.ts'
|
||||
import {Injectable} from '../../../../../di/src/decorator/Injection.ts'
|
||||
import Config from '../../../unit/Config.ts'
|
||||
|
||||
/**
|
||||
* Apply the default Daton headers to the outgoing response.
|
||||
* @extends Module
|
||||
*/
|
||||
@Injectable()
|
||||
export default class SetDatonHeaders extends Module {
|
||||
public static register(kernel: Kernel) {
|
||||
@ -16,6 +20,10 @@ export default class SetDatonHeaders extends Module {
|
||||
super()
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the outgoing response headers.
|
||||
* @param request
|
||||
*/
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
const text = this.config.get('server.powered_by.text', 'Daton')
|
||||
request.response.headers.set('X-Powered-By', text)
|
||||
|
@ -6,6 +6,10 @@ import Utility from '../../../service/utility/Utility.ts'
|
||||
import {Injectable} from '../../../../../di/src/decorator/Injection.ts'
|
||||
import {Logging} from '../../../service/logging/Logging.ts'
|
||||
|
||||
/**
|
||||
* HTTP kernel module to set the Daton session cookie, if it doesn't exist.
|
||||
* @extends Module
|
||||
*/
|
||||
@Injectable()
|
||||
export default class SetSessionCookie extends Module {
|
||||
|
||||
@ -19,6 +23,10 @@ export default class SetSessionCookie extends Module {
|
||||
super()
|
||||
}
|
||||
|
||||
/**
|
||||
* If one doesn't exist, generate and set the daton.session cookie.
|
||||
* @param {Request} request
|
||||
*/
|
||||
public async apply(request: Request): Promise<Request> {
|
||||
if ( !(await request.cookies.has('daton.session')) ) {
|
||||
const cookie = `${this.utility.uuid()}-${this.utility.uuid()}`
|
||||
|
Loading…
Reference in New Issue
Block a user