import { createHash } from 'node:crypto'; import type { InjectionToken } from 'graphql-modules'; import ms from 'ms'; import { UTCDate } from '@date-fns/utc'; import type { DateRangeInput } from '../__generated__/types'; import { DateRange } from './entities'; export type NullableAndPartial = { [P in keyof T]?: T[P] | undefined | null; }; export type NullableDictionary = { [P in keyof T]: T[P] | null }; export type Listify = Omit & { [_key in K]: T[K] | readonly T[K][]; }; export type MapToArray = Omit & { [_key in K]: readonly T[K][]; }; export function uuid(len = 13) { return Math.random().toString(16).substr(2, len); } export function stringifySelector< T extends { [key: string]: any; }, >(obj: T): string { return JSON.stringify( Object.keys(obj) .sort() .map(key => [key, obj[key]]), ); } function validateDateTime(dateTimeString?: string) { dateTimeString = dateTimeString === null || dateTimeString === void 0 ? void 0 : dateTimeString.toUpperCase(); if (!dateTimeString) { return false; } const RFC_3339_REGEX = /^(\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60))(\.\d{1,})?(([Z])|([+|-]([01][0-9]|2[0-3]):[0-5][0-9]))$/; // Validate the structure of the date-string if (!RFC_3339_REGEX.test(dateTimeString)) { return false; } // Check if it is a correct date using the javascript Date parse() method. const time = Date.parse(dateTimeString); if (time !== time) { return false; } // Split the date-time-string up into the string-date and time-string part. // and check whether these parts are RFC 3339 compliant. const index = dateTimeString.indexOf('T'); const dateString = dateTimeString.substr(0, index); const timeString = dateTimeString.substr(index + 1); return validateDate(dateString) && validateTime(timeString); } function validateTime(time?: string) { time = time === null || time === void 0 ? void 0 : time.toUpperCase(); if (!time) { return false; } const TIME_REGEX = /^([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])(\.\d{1,})?(([Z])|([+|-]([01][0-9]|2[0-3]):[0-5][0-9]))$/; return TIME_REGEX.test(time); } function validateDate(datestring: string) { const RFC_3339_REGEX = /^(\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01]))$/; if (!RFC_3339_REGEX.test(datestring)) { return false; } // Verify the correct number of days for // the month contained in the date-string. const year = Number(datestring.substr(0, 4)); const month = Number(datestring.substr(5, 2)); const day = Number(datestring.substr(8, 2)); switch (month) { case 2: // February if (leapYear(year) && day > 29) { return false; } if (!leapYear(year) && day > 28) { return false; } return true; case 4: // April case 6: // June case 9: // September case 11: // November if (day > 30) { return false; } break; } return true; } function leapYear(year: number) { return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; } export function parseDateTime(value: number | string | Date): Date { if (value instanceof Date) { return new UTCDate(value); } if (typeof value === 'string') { if (validateDateTime(value)) { return new UTCDate(value); } throw new TypeError(`DateTime cannot represent an invalid date-time-string ${value}.`); } if (typeof value === 'number') { try { return new UTCDate(value); } catch (e) { throw new TypeError('DateTime cannot represent an invalid Unix timestamp ' + value); } } throw new TypeError( 'DateTime cannot be serialized from a non string, ' + 'non numeric or non Date type ' + JSON.stringify(value), ); } export function parseDateRangeInput(period: DateRangeInput): DateRange { return { from: parseDateTime(period.from), to: parseDateTime(period.to), }; } export function createPeriod(period: string): DateRange { const to = new UTCDate(); const from = to.getTime() - ms(period); return { from: parseDateTime(from), to, }; } export type TypeOfToken = T extends InjectionToken ? R : unknown; export type Optional = Pick> & Partial>; export function hash(key: string): string { return createHash('md5').update(key).digest('hex'); } /** * A function that accepts two arrays and returns a difference */ export function diffArrays(left: readonly T[], right: readonly T[]): readonly T[] { return left.filter(val => !right.includes(val)).concat(right.filter(val => !left.includes(val))); } export function pushIfMissing(list: T[], item: T): void { if (!list.includes(item)) { list.push(item); } } export type PromiseOrValue = Promise | T; type BatchGroup = { args: TItem[]; callbacks: Array<{ resolve: (result: TResult) => void; reject: (error: Error) => void }>; }; /** * Batch processing of items based on a built key */ export function batchBy( /** Function to determine the batch group. */ buildBatchKey: (arg: TItem) => unknown, /** Loader for each batch group. */ loader: (args: TItem[]) => Promise[]>, /** Maximum amount of items per batch, if it is exceeded a new batch for a given batchKey is created. */ maxBatchSize = Infinity, ) { let batchGroups = new Map>(); let didSchedule = false; function startLoadingBatch(currentBatch: BatchGroup): void { const tickArgs = [...currentBatch.args]; const tickCallbacks = [...currentBatch.callbacks]; if (tickArgs.length !== tickCallbacks.length) { for (const cb of tickCallbacks) { cb.reject(new Error('Batch args and callbacks should have the same length.')); } throw new Error('Batch args and callbacks should have the same length.'); } loader(tickArgs).then( promises => { for (let i = 0; i < tickCallbacks.length; i++) { promises[i].then( result => { tickCallbacks[i].resolve(result); }, error => { tickCallbacks[i].reject(error); }, ); } }, error => { for (let i = 0; i < tickCallbacks.length; i++) { tickCallbacks[i].reject(error); } }, ); } function getBatchGroup(batchKey: unknown) { // get the batch collection for the batch key let currentBatch = batchGroups.get(batchKey); // if it does not exist or the batch is full, create a new batch if (currentBatch === undefined) { currentBatch = { args: [], callbacks: [], }; batchGroups.set(batchKey, currentBatch); } return currentBatch; } function scheduleExecutionOnNextTick() { if (didSchedule) { return; } didSchedule = true; process.nextTick(() => { for (const currentBatch of batchGroups.values()) { startLoadingBatch(currentBatch); } // reset the batch batchGroups = new Map(); didSchedule = false; }); } return (arg: TItem): Promise => { const batchKey = buildBatchKey(arg); const currentBatch = getBatchGroup(batchKey); scheduleExecutionOnNextTick(); currentBatch.args.push(arg); const d = Promise.withResolvers(); currentBatch.callbacks.push({ resolve: d.resolve, reject: d.reject }); // if the current batch is full, we already start loading it. if (currentBatch.callbacks.length >= maxBatchSize) { batchGroups.delete(batchKey); startLoadingBatch(currentBatch); } return d.promise; }; } export function assertOk( result: TOk | TNot, message: string, ): asserts result is TOk { if (!result.ok) { throw new Error(`${message}: ${result.message}`); } } export function batch(loader: (args: A[]) => Promise[]>): (arg: A) => Promise { let currentBatch: { args: A[]; callbacks: Array<{ resolve(r: R): void; reject(error: Error): void }>; } = { args: [], callbacks: [], }; return (arg: A) => { const schedule = currentBatch.args.length === 0; if (schedule) { process.nextTick(() => { const tickArgs = [...currentBatch.args]; const tickCallbacks = [...currentBatch.callbacks]; // reset the batch currentBatch = { args: [], callbacks: [], }; loader(tickArgs) .then(promises => { for (let i = 0; i < tickCallbacks.length; i++) { promises[i].then( result => { tickCallbacks[i].resolve(result); }, error => { tickCallbacks[i].reject(error); }, ); } }) .catch(err => { tickCallbacks.forEach(callback => callback.reject(err)); }); }); } currentBatch.args.push(arg); return new Promise((resolve, reject) => { currentBatch.callbacks.push({ resolve, reject }); }); }; } const atomicPromises = new Map>(); export function atomic(hashFn: (arg: A) => string) { return (_target: any, propertyKey: string, descriptor: PropertyDescriptor) => { const originalMethod = descriptor.value; descriptor.value = function (arg: A, ...rest: any[]) { if (rest.length) { console.warn(`@atomicPromise detected more than 1 argument in "${propertyKey}"`); } const key = `${propertyKey}_${hashFn(arg)}`; if (atomicPromises.has(key)) { return atomicPromises.get(key); } const promise = originalMethod.call(this, arg); atomicPromises.set(key, promise); return promise.finally(() => { atomicPromises.delete(key); }); }; }; } export function share(setter: () => Promise): () => Promise { let sharedPromise: Promise | null = null; return () => { if (!sharedPromise) { sharedPromise = Promise.resolve().then(setter); } return sharedPromise; }; } export function cache(cacheKeyFn: (arg: TInput) => string): MethodDecorator { return (_target, _propertyKey, descriptor) => { const cacheSymbol = Symbol('@cache'); const originalMethod = descriptor.value; function ensureCache(obj: any): Map { if (!obj[cacheSymbol]) { obj[cacheSymbol] = new Map(); } return obj[cacheSymbol]; } return { ...descriptor, value(this: any, arg: TInput) { const cacheMap = ensureCache(this); const key = cacheKeyFn(arg); const cachedValue = cacheMap.get(key); if (cachedValue !== null && typeof cachedValue !== 'undefined') { return cachedValue; } const result = (originalMethod as any).call(this, arg); cacheMap.set(key, result); return result; }, } as TypedPropertyDescriptor; }; } const NS_TO_MS = 1e6; export function nsToMs(ns: number) { return ns / NS_TO_MS; } export function msToNs(ms: number) { return ms * NS_TO_MS; } /** Typed Object.fromEntries */ export function objectFromEntries<$Key extends string, $Value>( entries: Array<[$Key, $Value]>, ): Record<$Key, $Value> { return Object.fromEntries(entries) as Record<$Key, $Value>; } export function objectEntries<$Key extends string, $Value>( object: Record<$Key, $Value>, ): Array<[$Key, $Value]> { return Object.entries(object) as Array<[$Key, $Value]>; }