UNPKG

@thisisagile/easy

Version:

Straightforward library for building domain-driven microservice architectures

346 lines (266 loc) • 10.6 kB
import { ArrayLike, toArray } from './Array'; import { Constructor, on, use } from './Constructor'; import type { Json } from './Json'; import { isArray, isDefined, isEmpty } from './Is'; import { isA } from './IsA'; import { Get, GetProperty, ofGet, ofProperty } from './Get'; import type { Id } from './Id'; import { equals } from './Text'; import { Optional } from './Types'; import { ifDefined, ifTrue } from '../utils/If'; import { AnyKey } from './AnyKey'; import { traverse } from '../utils/Traverse'; import { Indexed } from './Indexed'; type Pred<T> = (value: T, index: number, obj: T[]) => unknown; export class List<T = unknown> extends Array<T> { get ids(): List<Id> { return this.mapDefined(i => (i as any).id as Id); } get indexed(): List<Indexed<T>> { return this.map((v, index) => ({ ...v, index })); } isSubSetOf(...items: ArrayLike<T>): boolean { return this.diff(items).length === 0; } isSuperSetOf(...items: ArrayLike<T>): boolean { return this.length > items.length && toList(...items).isSubSetOf(...this); } isIntersectingWith(...items: ArrayLike<T>): boolean { return this.intersect(items).length > 0; } isDisjointWith(...items: ArrayLike<T>): boolean { return !this.isIntersectingWith(...items); } areEqual(...items: ArrayLike<T>): boolean { return this.isSubSetOf(...items) && toList(...items).isSubSetOf(...this); } asc(p: GetProperty<T, any>): List<T> { return toList<T>(...this).sort((e1, e2) => (ofProperty(e1, p) > ofProperty(e2, p) ? 1 : -1)); } desc(p: GetProperty<T, any>): List<T> { return toList<T>(...this).sort((e1, e2) => (ofProperty(e1, p) < ofProperty(e2, p) ? 1 : -1)); } first(p?: Pred<T>, params?: unknown): T { return (p ? this.find(p, params) : this[0]) as T; } firstValue<V>(f: (t: T) => V, alt?: Get<V, T>): Optional<V> { return ifDefined( this.first(t => !!f(t)), f, v => ofGet(alt, v) ); } isFirst(value: T): boolean { return value === this.first(); } next(p?: Pred<T>, params?: unknown): T { return p ? this[this.findIndex(p, params) + 1] : this[0]; } prev(p?: Pred<T>, params?: unknown): T { return p ? this[this.findIndex(p, params) - 1] : this[0]; } last(p?: Pred<T>, params?: unknown): T { return p ? this.filter(p, params).last() : this[this.length - 1]; } isLast(value: T): boolean { return value === this.last(); } overlaps(...items: ArrayLike<T>): boolean { return toList<T>(...items).some(i => this.some(t => i === t)); } diff(others: ArrayLike<T>): List<T> { return this.filter(i => !others.includes(i)); } diffByKey<U = T>(others: ArrayLike<U>, key: keyof T & keyof U): List<T> { return this.filter((i: any) => !others.some((o: any) => o[key] === i[key])); } symmetricDiff(others: ArrayLike<T>): List<T> { return this.diff(others).concat(toList<T>(...others).diff(this)); } symmetricDiffByKey(others: ArrayLike<T>, key: keyof T): List<T> { return this.diffByKey(others, key).concat(toList<T>(...others).diffByKey(this, key)); } intersect(others: ArrayLike<T>): List<T> { return this.filter(i => others.includes(i)); } intersectByKey<U>(others: ArrayLike<U>, key: keyof T & keyof U): List<T> { return this.filter((i: any) => others.some((o: any) => o[key] === i[key])); } intersectBy<U>(others: ArrayLike<U>, f: (value: T, value2: U) => boolean): List<T> { return this.filter(i => others.some(o => f(i, o as U))); } accumulate(...keys: (keyof T)[]): List<T> { return this.map((d, i, arr) => { const acc = keys.reduce((acc, v) => { (acc as any)[v] = i === 0 ? d[v] : (arr[i - 1][v] as number) + (d[v] as number); return acc; }, d); arr[i] = acc; return acc; }); } toJSON(): Json[] { return this.reduce((a, i) => { a.push(JSON.parse(JSON.stringify(i ?? {}))); return a; }, new Array<Json>()); } map<U>(f: (value: T, index: number, array: T[]) => U, params?: unknown): List<U> { return toList<U>(super.map(f, params)); } flatMap<U, This = unknown>(f: (this: This, value: T, index: number, array: T[]) => ReadonlyArray<U> | U, params?: This): List<U> { return toList<U>(super.flatMap(f, params)); } mapDefined<U>(f: (value: T, index: number, array: T[]) => U, params?: unknown): List<NonNullable<U>> { return this.map(f, params).defined(); } mapAsync(f: (i: T) => Promise<T>): Promise<List<T>> { return Promise.all(super.map(e => f(e))).then(a => toList<T>(a)); } mapSerial<U>(f: (i: T) => Promise<U>): Promise<List<U>> { return super.reduce((p, item) => p.then(results => on(results, async r => r.push(await f(item)))), Promise.resolve(toList<U>())); } distinct(): List<T> { return this.filter((i, index) => this.indexOf(i) === index); } distinctByKey(key: keyof T): List<T> { const seen = new Set<T[keyof T]>(); return this.filter(item => !seen.has(item[key]) && seen.add(item[key])); } distinctByValue(): List<T> { const seen = new Set<string>(); return this.filter(item => !seen.has(JSON.stringify(item)) && seen.add(JSON.stringify(item))); } filter(p: Pred<T>, params?: unknown): List<T> { return toList<T>(super.filter(p, params)); } sum(p: (t: T) => number): number { return this.reduce((sum: number, i) => sum + p(i), 0); } max(p: (value: T) => any): T; max(key: keyof T): T; max(p: keyof T | ((value: T) => any)): T { return this.desc(p).first(); } min(key: keyof T): T; min(p: (value: T) => any): T; min(p: keyof T | ((value: T) => any)): T { return this.asc(p).first(); } byId(id: Id): T { return this.first(i => equals((i as any).id, id)); } byKey(key: keyof T, value: unknown): T { return this.first(i => i[key] == value); } add(...items: ArrayLike<T>): this { super.push(...toArray(...items)); return this; } concat(...items: ConcatArray<T>[]): List<T>; concat(...items: (T | ConcatArray<T>)[]): List<T>; concat(...items: (T | ConcatArray<T>)[]): List<T> { return toList<T>(super.concat(...items)); } reverse(): List<T> { return toList<T>(super.reverse()); } splice(start: number, deleteCount?: number): List<T>; splice(start: number, deleteCount: number, ...items: T[]): List<T>; splice(start: number, deleteCount: number, ...items: T[]): List<T> { return toList<T>(super.splice(start, deleteCount, ...items)); } remove(item: T): List<T> { const index = this.indexOf(item); if (index > -1) { this.splice(index, 1); } return this; } move(sourceId: Id, destinationId: Id): List<T> { return this.moveOn('id' as keyof T, sourceId, destinationId); } moveOn(key: keyof T, sourceId: Id, destinationId: Id): List<T> { const source = this.findIndex(i => equals(i[key], sourceId)); const dest = this.findIndex(i => equals(i[key], destinationId)); return on(toList<T>(...this), r => r.splice(dest, 0, ...r.splice(source, 1))); } replace(key: keyof T, item: T): List<T> { const index = this.findIndex(i => i[key] === item?.[key]); ifTrue(index != -1, () => this.splice(index, 1, item)); return this; } switch(item: T): List<T> { return this.includes(item) ? this.remove(item) : this.add(item); } switchOn<U = T>(item: U, on: AnyKey<U>): List<T>; switchOn<T>(item: T, on: Pred<T>): List<T>; switchOn<U = T>(item: T, on: AnyKey<U> | Pred<T>): List<T> { return use(typeof on === 'function' ? this.find(on) : use(traverse(item, on), v => this.find(i => traverse(i, on) === v)), i => i ? this.remove(i) : this.add(item) ); } defined(): List<NonNullable<T>> { return this.reduce((l, v) => (isDefined(v) ? l.add(v) : l), toList<NonNullable<T>>()); } toObject(key: keyof T, options: { deleteKey?: boolean } = {}): Record<string | number | symbol, T> { return this.reduce((o: any, i) => { o[i[key]] = i; if (options.deleteKey) delete o[i[key]][key]; return o; }, {}); } toObjectList(key: keyof T): Record<string | number | symbol, List<T>> { return this.reduce( (a, t) => { const k = t[key] as unknown as string | number | symbol; a[k] = a[k] ?? toList(); a[k].push(t); return a; }, {} as Record<string | number | symbol, List<T>> ); } orElse(...alt: ArrayLike<T>): Optional<List<T>> { return !isEmpty(this) ? this : !isEmpty(...alt) ? toList<T>(...alt) : undefined; } weave(insertFrom: T[], interval: number): this { for (let i = interval, n = 0; i <= this.length && n < insertFrom.length; i += interval + 1) { this.splice(i, 0, insertFrom[n++]); } return this; } slice(start?: number, end?: number): List<T> { return toList(super.slice(start, end)); } none(p: (t: T) => boolean): boolean { return !this.some(p); } chunk(chunkSize: number): List<List<T>> { return this.reduce((acc, _, index) => (index % chunkSize === 0 ? on(acc, a => a.push(this.slice(index, index + chunkSize))) : acc), toList<List<T>>()); } //we needed to add U because of a Typescript issue with generics update<U = T>(p: Pred<T>, val: T | ((v: U) => T)): List<T> { return this.map((v, i, a) => (p(v, i, a) ? ofGet<T>(val, v, i, a) : v)); } updateFirst<U = T>(p: Pred<T>, val: T | ((v: U) => T)) { const index = this.findIndex(p); return this.update((t, i) => p(t, i, this) && i == index, val); } updateFirstById<U = T>(id: Id, val: T | ((v: U) => T)) { return this.updateFirst(i => equals((i as any)?.id, id), val); } updateById<U = T>(id: Id, val: T | ((v: U) => T)) { return this.update(i => equals((i as any)?.id, id), val); } groupBy(by: Pred<T>): List<List<T>> { return toList( Array.from(this.reduce((m, v, i, a) => use(by(v, i, a), key => m.set(key, (m.get(key) ?? toList<T>()).add(v))), new Map<unknown, List<T>>()).values()) ); } } export const toList = <T = unknown>(...items: ArrayLike<T>): List<T> => new List<T>().add(...items); export const isList = <T>(l?: unknown): l is List<T> => isDefined(l) && isArray(l) && isA<List<T>>(l, 'first', 'last', 'asc', 'desc'); export const asList = <T>(c: Constructor<T>, items: unknown = []): List<T> => toList<T>(toArray(items).map(i => new c(i))); export const maxValue = <T>(l: List<T>, key: keyof T): T[keyof T] | undefined => l.desc(key).first()?.[key]; export const minValue = <T>(l: List<T>, key: keyof T): T[keyof T] | undefined => l.asc(key).first()?.[key];