UNPKG

@clickup/ent-framework

Version:

A PostgreSQL graph-database-alike library with microsharding and row-level security

431 lines (391 loc) 13.4 kB
import { createHash } from "crypto"; import { inspect } from "util"; import compact from "lodash/compact"; import objectHashModule from "object-hash"; /** * In some cases (e.g. when actively working with callbacks), TS is still weak * enough, so we are not always able to use generics/unknown/never types. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export type DesperateAny = any; /** * Removes constructor signature from a type. * https://github.com/microsoft/TypeScript/issues/40110#issuecomment-747142570 */ export type OmitNew<T extends new (...args: never[]) => unknown> = Pick< T, keyof T >; /** * Adds a type alternative to constructor signature's return value. This is * useful when we e.g. turn an instance of some Ent class into an Instance & Row * type where Row is dynamically inferred from the schema. */ export type AddNew< TClass extends new (...args: never[]) => unknown, TRet, > = OmitNew<TClass> & { new (): InstanceType<TClass> & TRet }; /** * Flattens the interface to make it more readable in IntelliSense. Can be used * when someone modifies (picks, omits, etc.) a huge type. */ export type Flatten<T> = {} & { [P in keyof T]: T[P] }; /** * Cancels "readonly" specifier on object's properties. */ export type Writeable<T> = { -readonly [P in keyof T]: T[P] }; /** * Returns a union type of all tuple strict prefixes: * ["a", "b", "c"] -> ["a", "b"] | ["a"] */ export type TuplePrefixes<T extends readonly unknown[]> = T extends [unknown] ? [] : T extends [infer First, ...infer Rest] ? [First, ...TuplePrefixes<Rest>] | [First] : []; /** * Picks only partial (optional) keys of an object. */ export type PickPartial<T> = { [K in keyof T as undefined extends T[K] ? K : never]: T[K]; }; /** * Denotes an option which can be dynamically configured at runtime. */ export type MaybeCallable<T> = T | (() => T); /** * Similar to MaybeCallable, but allows for async functions. */ export type MaybeAsyncCallable<T> = T | (() => T) | (() => Promise<T>); /** * Some Node APIs throw not an instance of Error object, but something looking * like an Error. So we can't do "instanceof Error" check in all cases, we can * only compare the shape of a variable received in a `catch (e: unknown)` block * and hope for best. */ export type MaybeError<TExtra extends {} = {}> = | ({ code?: string; message?: string; stack?: string } & Partial<TExtra>) | null | undefined; /** * Turns a list of Promises to a list of Promise resolution results. */ export async function join<TList extends readonly unknown[]>( promises: TList, ): Promise<{ -readonly [P in keyof TList]: Awaited<TList[P]> }>; /** * Turns an object where some values are Promises to an object with values as * Promise resolution results. */ export async function join<TRec extends Readonly<Record<string, unknown>>>( promises: TRec, ): Promise<{ -readonly [K in keyof TRec]: Awaited<TRec[K]> }>; /** * A safe replacement for Promise-all built-in method. * * Works the same way as Promise-all, but additionally guarantees that ALL OTHER * promises have settled in case one of them rejects. This is needed to ensure * that we never have unexpected "dangling" promises continuing running in * nowhere in case one of the promises rejects early (the behavior of * Promise.all is to reject eagerly and let the rest of stuff running whilst the * caller code unfreezes). * * The behavior of join() is similar to Promise.allSettled(), but it throws the * 1st exception occurred; this is what's expected in most of the cases, and * this is how promises are implemented in e.g. Hack. * * The benefits of ensuring everything is settled: * * 1. We never have surprising entries in our logs (e.g. imagine a request * aborted long time ago, and then some "dangling" promises continue running * and issue queries as if nothing happened). * 2. Predictable control flow: if we run `await join()`, we know that no side * effects from the spawned promises will appear after this await throws or * returns. * * "Join" is a term from parallel programming (e.g. "join threads"), it’s pretty * concrete and means that after the call, multiple parallel execution flows * “join” into one. It's a word to describe having "one" from "many". * * What’s interesting is that, besides Promise-all leaks execution flows, it * still doesn’t trigger unhandledRejection for them in case one of them throws * later, it just swallows all other exceptions. * * I.e. Promise-all means "run all in parallel, if one throws - throw * immediately and let the others continue running in nowhere; if some of THAT * others throws, swallow their exceptions". * * And join() means "run all in parallel, if one throws - wait until everyone * finishes, and then throw the 1st exception; if some of others throw, swallow * their exceptions". * * See also https://en.wikipedia.org/wiki/Fork%E2%80%93join_model */ export async function join(promises: unknown[] | object): Promise<unknown> { const promisesArray = promises instanceof Array ? promises : Object.values(promises); let firstError: unknown = undefined; let errorCount = 0; const resultsArray = await Promise["all"]( promisesArray.map(async (promise) => Promise.resolve(promise).catch((err) => { if (errorCount === 0) { firstError = err; } errorCount++; return undefined; }), ), ); if (errorCount > 0) { throw firstError; } return promises instanceof Array ? resultsArray : Object.fromEntries( Object.keys(promises).map((key, i) => [key, resultsArray[i]]), ); } /** * A shortcut for `await join(arr.map(async ...))`. */ export async function mapJoin<TElem, TRet>( arr: readonly TElem[] | Promise<readonly TElem[]>, func: (e: TElem, idx: number) => PromiseLike<TRet> | TRet, ): Promise<TRet[]> { return join((await arr).map((e, idx) => func(e, idx))); } /** * Returns a random value between 1 and 1+jitter. */ export function jitter(jitter: number): number { return 1 + jitter * Math.random(); } /** * Copies a stack-trace from fromErr error into toErr object. Useful for * lightweight exceptions wrapping. */ export function copyStack< TError extends Error, TFrom extends { stack?: unknown; message?: unknown } | null | undefined, >(toErr: TError, fromErr: TFrom): TError { if ( typeof fromErr?.stack !== "string" || typeof fromErr?.message !== "string" ) { return toErr; } // This is magic, the 1st line in stacktrace must be exactly "ExceptionType: // exception message\n", otherwise jest goes mad and prints the stacktrace // incorrectly (once from err.message and then once from err.stack). See also: // https://stackoverflow.com/questions/42754270/re-throwing-exception-in-nodejs-and-not-losing-stack-trace const fromMessageLines = fromErr.message.split("\n").length; toErr.stack = toErr.toString() + // original toErr message "\n" + fromErr.stack .split("\n") .slice(fromMessageLines) // skip prefix=fromErr.message in fromErr.stack .join("\n"); return toErr; } /** * Tries to minify a stacktrace by removing common parts of the paths. See unit * test with snapshot for examples. */ export function minifyStack(stack: string, framesToPop: number): string { return stack .replace(/^\w+:[ ]*\n/s, "") // remove "Error:" prefix .trim() .split("\n") .slice(framesToPop) .join("\n") .replace(/^\s+/gm, "") .replace(/^[^\n]+\(<anonymous>\)\n/gm, "") .replace(/(:\d+):\d+(?=[\n)])/gs, "$1") .replace(/^(at )\/.+\//gm, "$1") .replace(/^(at [^\n]+\()\/.+\//gm, "$1") .replace(/^(at )([^\n]+?) \((.+)\)/gm, "$1$3 ($2)"); } /** * A simple sequence generator which never returns the same value twice within * the same process. It's NOT random, NOT for cryptography, NOT stored (so * starts from scratch on a process restart) and is NOT shared with other * processes. */ export function localUniqueInt(): number { return sequenceValue++; } let sequenceValue = 1; /** * The quickest string hasher. Don't use for crypto purposes! * https://medium.com/@chris_72272/what-is-the-fastest-node-js-hashing-algorithm-c15c1a0e164e */ export function stringHash(s: string): string { return createHash("sha1").update(s).digest("hex"); } /** * Used to calculate stable hashes of e.g. unique keys. */ export function objectHash(obj: object): Buffer { return objectHashModule(obj, { algorithm: "sha1", encoding: "buffer", }); } /** * Similar to objectHash(), but uses JSON.stringify() under the hood, assuming * that it's faster than objectHash(). Also, doesn't throw when the object * contains bigint values (as opposed to JSON.stringify()). */ export function jsonHash(obj: unknown): string { return stringHash( JSON.stringify(obj, (_, value) => typeof value === "bigint" ? value.toString() : value, ), ); } /** * Indents each line of the text with 2 spaces. */ export function indent(message: string): string { return message.replace(/^/gm, " "); } /** * Adds text suffixes to the sentence (typically, to an error message). */ export function addSentenceSuffixes( sentence: string, ...suffixes: Array<string | undefined> ): string { const compacted = compact(suffixes); if (compacted.length === 0) { return sentence; } const suffix = compacted .filter((suffix) => !sentence.endsWith(suffix)) .join(""); return suffix.startsWith("\n") ? sentence + suffix : sentence.trimEnd().replace(/[.!?]+$/s, "") + suffix; } /** * Returns the 1st line of the message. */ export function firstLine<T extends string | undefined>(message: T): T { return ( typeof message === "string" ? message.replace(/\n.*/s, "") : message ) as T; } /** * A shorthand for inspect() in compact/no-break mode. */ export function inspectCompact(obj: unknown): string { return inspect(obj, { compact: true, breakLength: Infinity }).replace( /^([[])\s+|\s+([\]])$/gs, (_, $1, $2) => $1 || $2, ); } /** * Prepares something which is claimed to be an ID for debug printing in e.g. * exception messages. We replace all non-ASCII characters to their \u * representations. */ export function sanitizeIDForDebugPrinting(idIn: unknown): string { const MAX_LEN = 32; const id = "" + idIn; const value = id .substring(0, MAX_LEN) // We want to use control characters in this regex. // eslint-disable-next-line no-control-regex .replace(/[^\x1F-\x7F]/g, (v) => "\\u" + v.charCodeAt(0)) + (id.length > MAX_LEN ? "..." : ""); return value === "" ? '""' : value; } /** * Throws if the value passed is null or undefined. */ export function nullthrows<T>( x?: T | null, message?: (() => string | Error) | string | Error, ): T { if (x !== null && x !== undefined) { return x; } if (typeof message === "function") { message = message(); } const error = message instanceof Error ? message : Error(message ?? `Got unexpected ${x} in nullthrows()`); Error.captureStackTrace(error, nullthrows); throw error; } /** * Two modes: * 1. If an async (or sync) function is passed, spawns it in background and * doesn't await for its termination. * 2. If a Promise is passed, lets it continue executing, doesn't await on it. * * Useful when we want to launch a function "in the air", "hanging in nowhere", * and make no-misused-promises and no-floating-promises rules happy with it. An * example is some legacy callback-based API (e.g. chrome extension API) where * we want to pass an async function. * * It's like an analog of "async on intent" comment in the code. */ export function runInVoid( funcOrPromise: (() => Promise<unknown> | void) | Promise<unknown> | void, ): void { if (funcOrPromise instanceof Function) { // eslint-disable-next-line @typescript-eslint/no-floating-promises funcOrPromise(); } else { // do nothing, our Promise is already hanging in nowhere } } /** * A typesafe-way to invariant the object's key presence and being * non-undefined. It is not always working for union types: sometimes it asserts * the value of the key to be "any". It also doesn't remove "undefined" from the * type of the value. */ export function hasKey<K extends symbol | string>( k: K, o: unknown, ): o is { [_ in K]: DesperateAny } { return ( !!o && (typeof o === "object" || typeof o === "function") && k in o && (o as Record<K, unknown>)[k] !== undefined ); } /** * Same as Object.entries(), but returns strongly-typed entries. */ export function entries<K extends string, V>( obj: Partial<Record<K, V>>, ): Array<[K, V]> { return Object.entries(obj) as Array<[K, V]>; } /** * If the passed value is a function, calls it; otherwise, returns it intact. */ export function maybeCall<T>(valueOrFn: MaybeCallable<T>): T { return typeof valueOrFn === "function" || valueOrFn instanceof Function ? (valueOrFn as Function)() : valueOrFn; } /** * Same as maybeCall(), but for MaybeAsyncCallable. */ export async function maybeAsyncCall<T>( valueOrFn: MaybeAsyncCallable<T>, ): Promise<T> { return typeof valueOrFn === "function" || valueOrFn instanceof Function ? (valueOrFn as Function)() : valueOrFn; }