import JSON5 from "json5"; import { constants, promises } from "fs"; import { MaybeLogger } from '../common/MaybeLogger.ts'; import { deepEqual } from 'fast-equals'; import type {CommonConfigPrimitives} from "../common/infrastructure/config/common.ts"; import { parseBoolStrict } from "../utils.ts"; import { removeUndefinedKeys } from '../../core/DataUtils.ts'; import { nonEmptyStringOrDefault } from "../../core/StringUtils.ts"; export const asArray = (data: T | T[]): T[] => { if (Array.isArray(data)) { return data; } return [data]; } const handler: ProxyHandler = { ownKeys(target) { return Reflect.ownKeys(target).map(x => typeof x === 'string' ? x.toLocaleLowerCase() : x); }, getOwnPropertyDescriptor(target, key) { return { ...Reflect.getOwnPropertyDescriptor(target, key), enumerable: true, configurable: true, }; }, get: function (target, key) { //console.log("key: " + key.toString()); if (typeof key == "string") { const uKey = key.toUpperCase(); if ((key != uKey) && (key in target)) return target[key]; return target[uKey]; } return target[key]; }, set: function (target, key, value) { if (typeof key == "string") { const uKey = key.toUpperCase(); if ((key != uKey) && (key in target)) target[key] = value; target[uKey] = value; return true; } else { target[key] = value; return true; } }, deleteProperty: function (target, key) { if (typeof key == "string") { const uKey = key.toUpperCase(); if ((key != uKey) && (key in target)) delete target[key]; if (uKey in target) delete target[uKey]; return true; } else { delete target[key]; return true; } }, }; const checkAtomic = (value) => { if (typeof value == "object") return noCasePropObj(value); // recursive call only for Objects return value; } export type toLowerCase = { [K in keyof T]: T[K] } export type LowercaseString = S extends `${infer Str}` ? `${Lowercase}` : S; /** https://medium.com/@vincent.dibon_78881/type-level-uppercase-and-lowercase-in-typescript-c18f574f4572 */ export type LowercaseKeys = { [K in keyof T as LowercaseString>]: T[K] }; /** Return a Proxy of an object where keys can be accessed case-insensitive * * Note: objects from rest operator in spread are *always* lowercase * * @see https://stackoverflow.com/a/50102779/1469797 */ export const noCasePropObj = (obj: T): LowercaseKeys => { const newObj = new Proxy>({} as LowercaseKeys, handler); // traverse the Original object converting string keys to upper case for (const key in obj) { if (typeof key == "string") { const objKey = key.toUpperCase(); if (!(key in newObj)) newObj[objKey] = checkAtomic(obj[key]); } } return newObj; // object with upper cased keys } export async function readJson(this: any, path: any, options: ReadJsonOptions = {}) { const { throwOnNotFound = true, interpolateEnvs = true, logger } = options; try { await promises.access(path, constants.R_OK); const data = (await promises.readFile(path)).toString(); if(interpolateEnvs) { const replaced = replaceInterpolatedValues(data, process.env, logger); return JSON5.parse(replaced); } return JSON5.parse(data); } catch (e) { const { code } = e; if (code === 'ENOENT') { if (throwOnNotFound) { throw new Error(`No file found at given path: ${path}`, { cause: e }); } else { return; } } throw new Error(`Encountered error while parsing file: ${path}`, { cause: e }); } }export interface ReadJsonOptions { throwOnNotFound?: boolean; interpolateEnvs?: boolean; logger?: MaybeLogger } export const replaceInterpolatedValues = (str: string, fromVals: Record, logger: MaybeLogger = new MaybeLogger()): string => { const cleanFromValKeys = noCasePropObj(fromVals); const matched = new Set(), unmatched = new Set(); const replaced = str.replaceAll(INTERPOLATION_WRAPPED_REGEX, (match, p1) => { //const fv = cleanFromValKeys[p1.toLocaleLowerCase().trim()]; const fv = cleanFromValKeys[p1.trim()]; if (fv !== undefined) { matched.add(p1); return fv; } unmatched.add(p1); return match; }); if (matched.size !== 0 || unmatched.size !== 0) { const logMsg = `Matched: ${matched.size === 0 ? 'None' : Array.from(matched.values()).join(', ')} | Unmatched: ${unmatched.size === 0 ? 'None' : Array.from(unmatched.values()).join(', ')}`; if(unmatched.size > 0) { logger.warn(logMsg); } else { logger.debug(logMsg); } } return replaced; }; export const INTERPOLATION_WRAPPED_REGEX: RegExp = new RegExp(/\[\[([^\r\n\[\]]+?)\]\]/g); export const objectIsEmpty = (obj: object, valueIsEmpty?: undefined | ((val: any) => boolean)): boolean => { if(Object.keys(obj).length === 0) { return true; } if(valueIsEmpty === undefined) { return false; } return Object.values(obj).every(x => valueIsEmpty(x)); } /** Shuffle array in-place * * https://stackoverflow.com/a/12646864/1469797 */ export const shuffleArray = (array: any[]): void => { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } } export const objectsEqual = (a: object, b: object) => { try { return deepEqual(a, b); } catch (e) { throw new Error('Could not compare objects', {cause: e}); } } export const getCommonComponentEnvConfig = (prefix: string): Partial => { const e = nonEmptyStringOrDefault(process.env[`${prefix}_ENABLE`], undefined); return removeUndefinedKeys({ id: nonEmptyStringOrDefault(process.env[`${prefix}_ID`], undefined), name: nonEmptyStringOrDefault(process.env[`${prefix}_NAME`], undefined), enable: e !== undefined ? parseBoolStrict(e) : undefined }, false); } const byteSizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'] /** * Convert bytes into human readable size * * @see https://stackoverflow.com/a/18650828/1469797 */ export const formatBytes = (bytes: number, decimals: number = 2): [string, number, string] => { if (!+bytes) return ['0 Bytes', 0, 'Byes']; const k = 1024 const dm = decimals < 0 ? 0 : decimals const i = Math.floor(Math.log(bytes) / Math.log(k)) const friendlySize = parseFloat((bytes / Math.pow(k, i)).toFixed(dm)); const friendlyUnit = byteSizes[i]; return [`${friendlySize} ${friendlyUnit}`, friendlySize, friendlyUnit]; }