AsyncPipe; table schemata; migrations; File logging
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2021-07-25 09:15:01 -05:00
parent e86cf420df
commit fcce28081b
42 changed files with 3139 additions and 56 deletions

View File

@@ -7,6 +7,7 @@ import {
} from './Collection'
import {Iterable, StopIteration} from './Iterable'
import {applyWhere, WhereOperator} from './where'
import {AsyncPipe, Pipe} from '../support/Pipe'
type AsyncCollectionComparable<T> = CollectionItem<T>[] | Collection<T> | AsyncCollection<T>
type AsyncKeyFunction<T, T2> = (item: CollectionItem<T>, index: number) => CollectionItem<T2> | Promise<CollectionItem<T2>>
type AsyncCollectionFunction<T, T2> = (items: AsyncCollection<T>) => T2
@@ -318,6 +319,24 @@ export class AsyncCollection<T> {
return new Collection<T2>(newItems)
}
/**
* Create a new collection by mapping the items in this collection using the given function,
* excluding any for which the function resolves undefined.
* @param func
*/
async partialMap<T2>(func: AsyncKeyFunction<T, T2 | undefined>): Promise<Collection<NonNullable<T2>>> {
const newItems: CollectionItem<NonNullable<T2>>[] = []
await this.each(async (item, index) => {
const result = await func(item, index)
if ( typeof result !== 'undefined' ) {
newItems.push(result as NonNullable<T2>)
}
})
return new Collection<NonNullable<T2>>(newItems)
}
/**
* Returns true if the given operator returns true for every item in the collection.
* @param {AsyncKeyFunction} func
@@ -783,10 +802,24 @@ export class AsyncCollection<T> {
* Return the value of the function, passing this collection to it.
* @param {AsyncCollectionFunction} func
*/
pipe<T2>(func: AsyncCollectionFunction<T, T2>): any {
pipeTo<T2>(func: AsyncCollectionFunction<T, T2>): any {
return func(this)
}
/**
* Return a new Pipe of this collection.
*/
pipe(): Pipe<AsyncCollection<T>> {
return Pipe.wrap(this)
}
/**
* Return a new AsyncPipe of this collection.
*/
asyncPipe(): AsyncPipe<AsyncCollection<T>> {
return AsyncPipe.wrap(this)
}
/* async pop(): Promise<MaybeCollectionItem<T>> {
const nextItem = await this.storedItems.next()
if ( !nextItem.done ) {

View File

@@ -1,11 +1,11 @@
import {Pipe} from '../support/Pipe'
import {AsyncPipe, Pipe} from '../support/Pipe'
type CollectionItem<T> = T
type MaybeCollectionItem<T> = CollectionItem<T> | undefined
type KeyFunction<T, T2> = (item: CollectionItem<T>, index: number) => CollectionItem<T2>
type KeyReducerFunction<T, T2> = (current: any, item: CollectionItem<T>, index: number) => T2
type CollectionFunction<T, T2> = (items: Collection<T>) => T2
type KeyOperator<T, T2> = string | KeyFunction<T, T2>
type KeyOperator<T, T2> = keyof T | KeyFunction<T, T2>
type AssociatedCollectionItem<T2, T> = { key: T2, item: CollectionItem<T> }
type CollectionComparable<T> = CollectionItem<T>[] | Collection<T>
type DeterminesEquality<T> = (item: CollectionItem<T>, other: any) => boolean
@@ -313,6 +313,24 @@ class Collection<T> {
return new Collection<T2>(newItems)
}
/**
* Create a new collection by mapping the items in this collection using the given function,
* excluding any for which the function returns undefined.
* @param func
*/
partialMap<T2>(func: KeyFunction<T, T2 | undefined>): Collection<NonNullable<T2>> {
const newItems: CollectionItem<NonNullable<T2>>[] = []
this.each(((item, index) => {
const result = func(item, index)
if ( typeof result !== 'undefined' ) {
newItems.push(result as NonNullable<T2>)
}
}))
return new Collection<NonNullable<T2>>(newItems)
}
/**
* Convert this collection to an object keyed by the given field.
*
@@ -354,10 +372,10 @@ class Collection<T> {
this.allAssociated(key).forEach(assoc => {
i += 1
if ( typeof value === 'string' ) {
obj[assoc.key] = (assoc.item as any)[value]
} else {
if ( typeof value === 'function' ) {
obj[assoc.key] = value(assoc.item, i)
} else {
obj[assoc.key] = (assoc.item[value] as any) as T2
}
})
@@ -805,6 +823,13 @@ class Collection<T> {
return Pipe.wrap(this)
}
/**
* Return a new AsyncPipe of this collection.
*/
asyncPipe(): AsyncPipe<Collection<T>> {
return AsyncPipe.wrap(this)
}
/**
* Remove the last item from this collection.
*/

View File

@@ -13,6 +13,7 @@ export * from './error/ErrorWithContext'
export * from './logging/Logger'
export * from './logging/StandardLogger'
export * from './logging/FileLogger'
export * from './logging/types'
export * from './support/BehaviorSubject'

View File

@@ -0,0 +1,44 @@
import {Logger} from './Logger'
import {LogMessage} from './types'
import {Injectable} from '../../di'
import {universalPath} from '../support/path'
import {appPath, env} from '../../lifecycle/Application'
import {Writable} from 'stream'
/**
* A Logger implementation that writes to a UniversalPath.
*/
@Injectable()
export class FileLogger extends Logger {
private resolvedPath?: Writable
/**
* Get the re-usable write stream to the log file.
* @protected
*/
protected async getWriteStream(): Promise<Writable> {
if ( !this.resolvedPath ) {
let basePath = env('EXTOLLO_LOGGING_FILE')
if ( basePath && !Array.isArray(basePath) ) {
basePath = [basePath]
}
const resolvedPath = basePath ? universalPath(...basePath) : appPath('..', '..', 'extollo.log')
if ( !(await resolvedPath.exists()) ) {
await resolvedPath.concat('..').mkdir()
await resolvedPath.write('')
}
this.resolvedPath = await resolvedPath.writeStream()
}
return this.resolvedPath
}
public async write(message: LogMessage): Promise<void> {
const text = `${message.level} ${this.formatDate(message.date)} (${message.callerName || 'Unknown'}) ${message.output}`
const stream = await this.getWriteStream()
stream.write(text + '\n')
}
}

View File

@@ -1,5 +1,6 @@
import {LoggingLevel, LogMessage} from './types'
import * as color from 'colors/safe'
import {Awaitable} from '../support/types'
/**
* Base class for an application logger.
@@ -10,7 +11,7 @@ export abstract class Logger {
* @param {LogMessage} message
* @return Promise<void>
*/
public abstract write(message: LogMessage): Promise<void> | void;
public abstract write(message: LogMessage): Awaitable<void>;
/**
* Format the date object to the string output format.

View File

@@ -1,12 +1,14 @@
/**
* A closure that maps a given pipe item to a different type.
*/
import {Awaitable} from './types'
export type PipeOperator<T, T2> = (subject: T) => T2
/**
* A closure that maps a given pipe item to an item of the same type.
*/
export type ReflexivePipeOperator<T> = (subject: T) => T
export type ReflexivePipeOperator<T> = (subject: T) => T|void
/**
* A condition or condition-resolving function for pipe methods.
@@ -77,6 +79,15 @@ export class Pipe<T> {
return new Pipe(op(this.subject))
}
/**
* Like tap, but always returns the original pipe.
* @param op
*/
peek<T2>(op: PipeOperator<T, T2>): this {
op(this.subject)
return this
}
/**
* If `check` is truthy, apply the given operator to the item in the pipe and return the result.
* Otherwise, just return the current pipe unchanged.
@@ -86,7 +97,7 @@ export class Pipe<T> {
*/
when(check: PipeCondition<T>, op: ReflexivePipeOperator<T>): Pipe<T> {
if ( (typeof check === 'function' && check(this.subject)) || check ) {
return Pipe.wrap(op(this.subject))
Pipe.wrap(op(this.subject))
}
return this
@@ -104,7 +115,8 @@ export class Pipe<T> {
return this
}
return Pipe.wrap(op(this.subject))
Pipe.wrap(op(this.subject))
return this
}
/**
@@ -127,4 +139,134 @@ export class Pipe<T> {
get(): T {
return this.subject
}
/**
* Get an AsyncPipe with the current item in the pipe.
*/
async(): AsyncPipe<T> {
return AsyncPipe.wrap<T>(this.subject)
}
}
/**
* A subject function that yields the value in the AsyncPipe.
*/
export type AsyncPipeResolver<T> = () => Awaitable<T>
/**
* A closure that maps a given pipe item to a different type.
*/
export type AsyncPipeOperator<T, T2> = (subject: T) => Awaitable<T2>
/**
* A closure that maps a given pipe item to an item of the same type.
*/
export type ReflexiveAsyncPipeOperator<T> = (subject: T) => Awaitable<T|void>
/**
* A condition or condition-resolving function for pipe methods.
*/
export type AsyncPipeCondition<T> = boolean | ((subject: T) => Awaitable<boolean>)
/**
* An asynchronous version of the Pipe helper.
*/
export class AsyncPipe<T> {
/**
* Get an AsyncPipe with the given value in it.
* @param subject
*/
static wrap<subjectType>(subject: subjectType): AsyncPipe<subjectType> {
return new AsyncPipe<subjectType>(() => subject)
}
constructor(
/** The current value resolver of the pipe. */
private subject: AsyncPipeResolver<T>,
) {}
/**
* Apply a transformative operator to the pipe.
* @param op
*/
tap<T2>(op: AsyncPipeOperator<T, T2>): AsyncPipe<T2> {
return new AsyncPipe<T2>(async () => op(await this.subject()))
}
/**
* Apply an operator to the pipe, but return the reference
* to the current pipe. The operator is resolved when the
* overall pipe is resolved.
* @param op
*/
peek<T2>(op: AsyncPipeOperator<T, T2>): AsyncPipe<T> {
return new AsyncPipe<T>(async () => {
const subject = await this.subject()
await op(subject)
return subject
})
}
/**
* Apply an operator to the pipe, if the check condition passes.
* @param check
* @param op
*/
when(check: AsyncPipeCondition<T>, op: ReflexiveAsyncPipeOperator<T>): AsyncPipe<T> {
return new AsyncPipe<T>(async () => {
let subject
if ( typeof check === 'function' ) {
check = await check(subject = await this.subject())
}
subject = subject ?? await this.subject()
if ( check ) {
return ((await op(subject)) ?? subject) as T
}
return subject as T
})
}
/**
* Apply an operator to the pipe, if the check condition fails.
* @param check
* @param op
*/
unless(check: AsyncPipeCondition<T>, op: ReflexiveAsyncPipeOperator<T>): AsyncPipe<T> {
if ( typeof check === 'function' ) {
return this.when(async (subject: T) => !(await check(subject)), op)
}
return this.when(!check, op)
}
/**
* Alias of `unless()`.
* @param check
* @param op
*/
whenNot(check: AsyncPipeCondition<T>, op: ReflexiveAsyncPipeOperator<T>): AsyncPipe<T> {
return this.unless(check, op)
}
/**
* Get the transformed value from the pipe.
*/
async resolve(): Promise<T> {
return this.subject()
}
/**
* Resolve the value and return it in a sync `Pipe` instance.
*/
async sync(): Promise<Pipe<T>> {
return Pipe.wrap<T>(await this.subject())
}
/** Get the transformed value from the pipe. Allows awaiting the pipe directly. */
then(): Promise<T> {
return this.resolve()
}
}

View File

@@ -1,6 +1,28 @@
import * as nodeUUID from 'uuid'
import {ErrorWithContext} from '../error/ErrorWithContext'
import {JSONState} from './Rehydratable'
import {KeyValue} from './types'
/**
* Create an array of key-value pairs for the keys in a uniform object.
* @param obj
*/
export function objectToKeyValue<T>(obj: {[key: string]: T}): KeyValue<T>[] {
const values: KeyValue<T>[] = []
for ( const key in obj ) {
if ( !Object.prototype.hasOwnProperty.call(obj, key) ) {
continue
}
values.push({
key,
value: obj[key],
})
}
return values
}
/**
* Make a deep copy of an object.

View File

@@ -47,3 +47,17 @@ export function padCenter(string: string, length: number, padWith = ' '): string
return string
}
/**
* Convert a string to PascalCase.
* @param input
*/
export function stringToPascal(input: string): string {
return input.split(/[\s_]+/i)
.map(part => {
return part[0].toUpperCase() + part.substr(1)
})
.join('')
.split(/\W+/i)
.join('')
}

View File

@@ -3,3 +3,9 @@ export type Awaitable<T> = T | Promise<T>
/** Type alias for something that may be undefined. */
export type Maybe<T> = T | undefined
/** Type alias for a callback that accepts a typed argument. */
export type ParameterizedCallback<T> = ((arg: T) => any)
/** A key-value form of a given type. */
export type KeyValue<T> = {key: string, value: T}