UNPKG

@snickerdoodlelabs/common-utils

Version:
283 lines (256 loc) 8.43 kB
import { CursorPagedResponse, PagedResponse, PagingRequest, JSONString, InvalidParametersError, BigNumberString, } from "@snickerdoodlelabs/objects"; import { errAsync, okAsync, ResultAsync } from "neverthrow"; import { ResultUtils } from "neverthrow-result-utils"; export class ObjectUtils { // Taken from https://stackoverflow.com/questions/27936772/how-to-deep-merge-instead-of-shallow-merge // eslint-disable-next-line @typescript-eslint/no-explicit-any static mergeDeep<T = unknown>(...objects: any[]): T { const isObject = (obj) => obj && typeof obj === "object"; return objects.reduce((prev, obj) => { Object.keys(obj).forEach((key) => { const pVal = prev[key]; const oVal = obj[key]; if (Array.isArray(pVal) && Array.isArray(oVal)) { prev[key] = pVal.concat(...oVal); } else if (isObject(pVal) && isObject(oVal)) { prev[key] = ObjectUtils.mergeDeep(pVal, oVal); } else { prev[key] = oVal; } }); return prev; }, {}); } static serialize(obj: unknown): JSONString { return JSONString( JSON.stringify(obj, (key, value) => { if (value instanceof Map) { return { dataType: "Map", value: Array.from(value.entries()), // or with spread: value: [...value] }; } else if (value instanceof Set) { return { dataType: "Set", value: [...value], }; } else if (value instanceof BigInt) { return { dataType: "BigInt", value: value.toString(), }; } else if (typeof value == "bigint") { return { dataType: "bigint", value: BigInt(value).toString(), }; } else { return value; } }), ); } static deserialize<T = Record<string, unknown>>(json: JSONString): T { return JSON.parse(json, (key, value) => { if (typeof value === "object" && value !== null) { if (value.dataType === "Map") { return new Map(value.value); } else if (value.dataType === "Set") { return new Set(value.value); } else if (value.dataType === "BigInt") { return BigInt(value.value); } else if (value.dataType === "bigint") { return BigInt(value.value); } } return value; }); } static toGenericObject(obj: unknown): Record<string, unknown> { return ObjectUtils.deserialize(ObjectUtils.serialize(obj)); } static iteratePages<T, TError, TError2>( readFunc: ( pagingRequest: PagingRequest, ) => ResultAsync<PagedResponse<T>, TError>, processFunc: (obj: T) => ResultAsync<void, TError2>, pageSize = 25, serialize = false, ): ResultAsync<void, TError | TError2> { // This is a recursive method. We just start it off. return ObjectUtils.fetchAndProcessPage( 1, readFunc, processFunc, pageSize, serialize, ); } private static fetchAndProcessPage<T, TError, TError2>( pageNumber: number, readFunc: ( pagingRequest: PagingRequest, ) => ResultAsync<PagedResponse<T>, TError>, processFunc: (obj: T) => ResultAsync<void, TError2>, pageSize = 25, serialize = false, ): ResultAsync<void, TError | TError2> { // Create the initial paging request const pagingRequest = new PagingRequest(pageNumber, pageSize); // Read the page return readFunc(pagingRequest).andThen((objectPage) => { // Iterate over the objects and run through the processor // We'll wait for all the processFuncs to complete before getting the next page let result: ResultAsync<void[], TError2>; if (serialize) { result = ResultUtils.executeSerially( objectPage.response.map((obj) => () => { return processFunc(obj); }), ); } else { result = ResultUtils.combine( objectPage.response.map((obj) => { return processFunc(obj); }), ); } return result.andThen(() => { // Done processing all of those results. Check if we need to recurse // See what result we're on and how it compares to the total results const maxResult = objectPage.page * objectPage.pageSize; if (maxResult < objectPage.totalResults) { return ObjectUtils.fetchAndProcessPage( objectPage.page + 1, readFunc, processFunc, pageSize, ); } return okAsync<void, TError | TError2>(undefined); }); }); } static iterateCursor<T, TCursor, TError>( readFunc: ( cursor: TCursor | null, ) => ResultAsync<CursorPagedResponse<T[], TCursor>, TError>, handler: (data: T[]) => ResultAsync<void, TError>, ): ResultAsync<void, TError> { // Do the first read return ObjectUtils.processNextBatch(null, readFunc, handler).map(() => {}); } static parseBoolean(val: string | number | boolean): boolean { switch (typeof val) { case "boolean": return val; case "number": return val !== 0; case "string": switch (val?.toLowerCase()?.trim()) { case "true": case "yes": case "1": return true; case "false": case "no": case "0": case null: case undefined: return false; default: console.log( `Don't have explicit check in parseBoolean for the string ${val}`, ); return false; } default: console.log( `Don't have check in parseBoolean for typeof ${typeof val} of value ${val}`, ); return false; } } private static processNextBatch<T, TCursor, TError>( cursor: TCursor | null, readFunc: ( cursor: TCursor | null, ) => ResultAsync<CursorPagedResponse<T[], TCursor>, TError>, handler: (data: T[]) => ResultAsync<void, TError>, ): ResultAsync<CursorPagedResponse<T[], TCursor>, TError> { return readFunc(cursor).andThen((resp) => { if (resp.response.length == 0) { return okAsync(resp); } return handler(resp.response).andThen(() => { return ObjectUtils.processNextBatch(resp.cursor, readFunc, handler); }); }); } static removeNullValues<T>(array: (T | null | undefined)[]): T[] { function notEmpty<T>(value: T | null | undefined): value is T { if (value === null || value === undefined) return false; const testConversion: T = value; return true; } return array.filter(notEmpty); } static progressiveFallback<TReturn, TProvider, TError>( method: (TProvider) => ResultAsync<TReturn, TError>, provider: TProvider[], ): ResultAsync<TReturn, TError | InvalidParametersError> { if (provider.length == 0) { return errAsync( new InvalidParametersError( "No providers provided to progressiveFallback()", ), ); } return method(provider[0]).orElse((err) => { // If we're on the last provider, just return the error if (provider.length == 1) { return errAsync(err); } // Otherwise, try the next provider return ObjectUtils.progressiveFallback(method, provider.slice(1)); }); } static verifyBigNumber( bigNumberString: BigNumberString, ): ResultAsync<bigint, InvalidParametersError> { try { const bigNumber = BigInt(bigNumberString); // will fail if bigNumberString is a float return okAsync(bigNumber); } catch (e) { return errAsync( new InvalidParametersError( `Can't convert BigNumberString ${bigNumberString} to BigInt. Received error ${e}`, ), ); } } static iterateThroughAllPages<T, TError>( readFunc: ( pagingRequest: PagingRequest, ) => ResultAsync<PagedResponse<T>, TError>, ): ResultAsync<T[], TError> { const data: T[] = []; const processFunc = (model: T) => { data.push(model); return okAsync(undefined); }; return readFunc(new PagingRequest(1, 1)) .andThen((firstPage) => { const pageSize = firstPage.totalResults; return ObjectUtils.iteratePages(readFunc, processFunc, pageSize); }) .map(() => data); } }