UNPKG

@apollo/federation-internals

Version:
482 lines (432 loc) 12.3 kB
/** * For lack of a "home of federation utilities", this function is copy/pasted * verbatim across the federation, gateway, and query-planner packages. Any changes * made here should be reflected in the other two locations as well. * * @param condition * @param message * @throws */ export function assert(condition: any, message: string | (() => string)): asserts condition { if (!condition) { throw new Error(typeof message === 'string' ? message : message()); } } export function assertUnreachable(_: never): never { throw new Error("Didn't expect to get here"); } export class MultiMap<K, V> extends Map<K, V[]> { add(key: K, value: V): this { const values = this.get(key); if (values) { values.push(value); } else { this.set(key, [value]); } return this; } addAll(otherMap: MultiMap<K, V>): this { for (const [k, vs] of otherMap.entries()) { for (const v of vs) { this.add(k, v); } } return this; } } export class SetMultiMap<K, V> extends Map<K, Set<V>> { add(key: K, value: V): this { let values = this.get(key); if (!values) { values = new Set<V>(); this.set(key, values); } values.add(value); return this; } addAll(otherMap: SetMultiMap<K, V>): this { for (const [k, vs] of otherMap.entries()) { for (const v of vs) { this.add(k, v); } } return this; } } /** * Generic OrderedMap class that can sort keys based on an arbitrary sorting function * Insert time is O(log(N)) * Remove is not implemented, but the trivial implementation would be O(N) * Uses '<' '>' sorting by default * Collisions are fine, it will just overwrite the old value */ export class OrderedMap<K,V> { private _keys: K[] = []; private _values: Map<K,V> = new Map<K,V>(); private _compareFn: (a: K, b: K) => number; private static defaultCompareFn<K>(a: K, b: K) { if (a < b) { return -1; } else if (b < a) { return 1; } return 0; } constructor(compareFn: (a: K, b: K) => number = OrderedMap.defaultCompareFn) { this._compareFn = compareFn; } add(key: K, value: V) { if (!this._values.has(key)) { this.insertKeyInOrder(key); } this._values.set(key, value); } get(key: K): V | undefined { return this._values.get(key); } has(key: K): boolean { return this._values.has(key); } get size() { return this._keys.length; } keys(): K[] { return this._keys; } values(): V[] { return this._keys.map(key => { const v = this._values.get(key); assert(v, 'value for known key not found in OrderedMap'); return v; }); } // O(log(N)) - find location via middle finding private insertKeyInOrder(key: K) { let lower = 0; let upper = this._keys.length - 1; while (lower <= upper) { const middle = Math.floor((upper + lower) / 2); if (this._compareFn(this._keys[middle], key) < 0) { lower = middle + 1; } else { upper = middle - 1; } } this._keys = this._keys.slice(0, lower).concat(key).concat(this._keys.slice(lower)); } // remove(key: K): void - not implemented *[Symbol.iterator]() { for (let i = 0; i < this._keys.length; i += 1) { const v = this._values.get(this._keys[i]); assert(v, 'value for known key not found in OrderedMap'); yield v; } } } /** * Tests if the provided arrays have the same elements (using '===' equality or the provided * equality function). * This is _not_ a deep equality by default, though you can build one somewhat when passing * an equality function. */ export function arrayEquals<T>( a: readonly T[], b: readonly T[], equalFct?: (e1: T, e2: T) => boolean ): boolean { if (a === b) { return true; } if (a.length !== b.length) { return false; } for (let i = 0; i < a.length; ++i) { const eltEqual = equalFct ? equalFct(a[i], b[i]) : a[i] === b[i]; if (!eltEqual) { return false; } } return true; } /** * Whether the first set is a (non-strict) subset of the second set. */ export function isSubset<T>(superset: Set<T>, maybeSubset: Set<T>): boolean { if (superset === maybeSubset) { return true; } for (const elt of maybeSubset) { if (!superset.has(elt)) { return false; } } return true; } export function firstOf<T>(iterable: Iterable<T>): T | undefined { const res = iterable[Symbol.iterator]().next(); return res.done ? undefined : res.value; } export function mapValues<V>(map: ReadonlyMap<any, V>): V[] { const array = new Array(map.size); let i = 0; for (const v of map.values()) { array[i++] = v; } return array; } export function mapKeys<K>(map: ReadonlyMap<K, any>): K[] { const array = new Array(map.size); let i = 0; for (const k of map.keys()) { array[i++] = k; } return array; } export function mapEntries<K, V>(map: ReadonlyMap<K, V>): [K, V][] { const array = new Array(map.size); let i = 0; for (const entry of map.entries()) { array[i++] = entry; } return array; } export function setValues<V>(set: ReadonlySet<V>): V[] { const array = new Array(set.size); let i = 0; for (const v of set.values()) { array[i++] = v; } return array; } export class MapWithCachedArrays<K, V> { private readonly map = new Map<K, V>(); private cachedKeys?: readonly K[]; private cachedValues?: readonly V[]; private clearCaches() { this.cachedKeys = undefined; this.cachedValues = undefined; } get size(): number { return this.map.size; } has(key: K): boolean { return this.map.has(key); } get(key: K): V | undefined { return this.map.get(key); } set(key: K, value: V): this { this.map.set(key, value); this.clearCaches(); return this; } delete(key: K): boolean { const deleted = this.map.delete(key); if (deleted) { this.clearCaches(); } return deleted; } clear(): void { this.map.clear(); this.clearCaches(); } keys(): readonly K[] { if (!this.cachedKeys) { this.cachedKeys = mapKeys(this.map); } return this.cachedKeys; } values(): readonly V[] { if (!this.cachedValues) { this.cachedValues = mapValues(this.map); } return this.cachedValues; } } export function copyWitNewLength<T>(arr: T[], newLength: number): T[] { assert(newLength >= arr.length, () => `${newLength} < ${arr.length}`); const copy = new Array(newLength); for (let i = 0; i < arr.length; i++) { copy[i] = arr[i]; } return copy; } /** * Checks whether the provided string value is defined and represents a "boolean-ish" * value, returning that boolean value. * * @param str - the string to check. * @return the boolean value contains in `str` if `str` represents a boolean-ish value, * where "boolean-ish" is one of "true"/"false", "yes"/"no" or "0"/"1" (where the check * is case-insensitive). Otherwise, `undefined` is returned. */ export function validateStringContainsBoolean(str?: string) : boolean | undefined { if (!str) { return false; } switch (str.toLocaleLowerCase()) { case "true": case "yes": case "1": return true; case "false": case "no": case "0": return false; default: return undefined; } } /** * Joins an array of string, much like `Array.prototype.join`, but with the ability to use a specific different * separator for the first and/or last occurence. * * The goal is to make reading flow slightly better. For instance, if you have a list of subgraphs `s = ["A", "B", "C"]`, * then `"subgraphs " + joinString(s)` will yield "subgraphs A, B and C". */ export function joinStrings(toJoin: string[], sep: string = ', ', firstSep?: string, lastSep: string = ' and ') { if (toJoin.length == 0) { return ''; } const first = toJoin[0]; if (toJoin.length == 1) { return first; } const last = toJoin[toJoin.length - 1]; if (toJoin.length == 2) { return first + (firstSep ? firstSep : lastSep) + last; } return first + (firstSep ? firstSep : sep) + toJoin.slice(1, toJoin.length - 1).join(sep) + lastSep + last; } // When displaying a list of something in a human readable form, after what size (in // number of characters) we start displaying only a subset of the list. const DEFAULT_HUMAN_READABLE_LIST_CUTOFF_LENGTH = 100; /** * Like `joinStrings`, joins an array of string, but with a few twists, namely: * - If the resulting list to print is "too long", it only display a subset of the elements and use some elipsis (...). In other * words, this method is for case where, where the list ot print is too long, it is more useful to avoid flooding the output than * printing everything. * - it allows to prefix the whole list, and to use a different prefix for a single element than for > 1 elements. * - it forces the use of ',' as separator, but allow a different lastSeparator. */ export function printHumanReadableList( names: string[], { emptyValue, prefix, prefixPlural, lastSeparator, cutoff_output_length, } : { emptyValue?: string, prefix?: string, prefixPlural?: string, lastSeparator?: string, cutoff_output_length?: number, } ): string { if (names.length === 0) { return emptyValue ?? ''; } if (names.length == 1) { return prefix ? prefix + ' ' + names[0] : names[0]; } const cutoff = cutoff_output_length ?? DEFAULT_HUMAN_READABLE_LIST_CUTOFF_LENGTH; const { lastIdx } = names.reduce( ({ lastIdx, length }, name) => { if (length + name.length > cutoff) { return { lastIdx, length, }; } return { lastIdx: lastIdx + 1, length: length + name.length, }; }, { lastIdx: 0, length: 0} ); // In case the name we list have absurdly long names, we cut it off but ensure we at least display one. const toDisplay = names.slice(0, Math.max(1, lastIdx)); const actualPrefix = prefixPlural ? prefixPlural + ' ' : (prefix ? prefix + ' ' : ''); if (toDisplay.length === names.length) { return actualPrefix + joinStrings(toDisplay, ', ', undefined, lastSeparator); } else { return actualPrefix + joinStrings(toDisplay, ', ', undefined, ', ') + ', ...'; } } export type Concrete<Type> = { [Property in keyof Type]-?: Concrete<Type[Property]>; }; // for use with Array.filter // Example: // const x = [1,2,undefined]; // const y: number[] = x.filter(isDefined); export const isDefined = <T>(t: T | undefined): t is T => t === undefined ? false : true; /** * Removes the first occurrence of the provided element in the provided array, if said array contains said elements. * * @return whether the element was removed. */ export function removeArrayElement<T>(element: T, array: T[]): boolean { const index = array.indexOf(element); if (index >= 0) { array.splice(index, 1); return true; } else { return false; } } export type NonEmptyArray<T> = [T, ...T[]]; export function isNonEmptyArray<T>(array: T[]): array is NonEmptyArray<T> { return array.length > 0; } // We can switch to `Array.prototype.findLast` when we drop support for Node 16 export function findLast<T>(array: T[], predicate: (t: T) => boolean): T | undefined { for (let i = array.length - 1; i >= 0; i--) { const t = array[i]; if (predicate(t)) { return t; } } return undefined; } export function mergeMapOrNull<K,V>(m1: Map<K, V> | null, m2: Map<K, V> | null): Map<K, V> | null { if (!m1) { return m2; } if (!m2) { return m1; } return new Map<K, V>([...m1, ...m2]); } export function composeSets<T>(s1: Set<T> | null, s2: Set<T> | null): Set<T> | null { if (!s1 && !s2) { return null; } const result = new Set<T>(); s1?.forEach(v => result.add(v)); s2?.forEach(v => result.add(v)); return result; } export function setsEqual<T>(s1: Set<T> | null, s2: Set<T> | null): boolean { if (s1 === s2) { return true; } if (!s1 && !s2) { return true; } if (!s1 || !s2 || s1.size !== s2.size) { return false; } for (const key of s1) { if (!s2.has(key)) { return false; } } return true; }