UNPKG

nhb-toolbox

Version:

A versatile collection of smart, efficient, and reusable utility functions and classes for everyday development needs.

221 lines (220 loc) 8.22 kB
import { isDateLike } from '../date/guards.js'; import { isCustomFile, isFileList, isFileOrBlob, isFileUpload, } from '../form/guards.js'; import { isArrayOfType, isNotEmptyObject, isObject, } from '../guards/non-primitives.js'; import { isString } from '../guards/primitives.js'; import { trimString } from '../string/basics.js'; /** * * Sanitizes a string, array of strings, an object or array of objects by ignoring specified keys and trimming string values. * * Also excludes nullish values (null, undefined) if specified. Always ignores empty nested object(s). * * @param input - The string, object or array of strings or objects to sanitize. * @param options - Options for processing. * @param _return - By default return type is as it is, passing this parameter `partial` makes the return type `Partial<T>`. * @returns A new string, object or array of strings or objects with the specified modifications. */ export function sanitizeData(input, options, _return) { const { keysToIgnore = [], requiredKeys = [], trimStrings = true, ignoreNullish = false, ignoreFalsy = false, ignoreEmpty = false, } = options || {}; // Flatten the object keys and use the keys for comparison const ignoreKeySet = new Set(keysToIgnore); /** * * Determines if a key is required * @param key The key to check. * @returns `true` if the key is required, otherwise `false`. */ const _isRequiredKey = (key) => { return Array.isArray(requiredKeys) ? requiredKeys?.some((path) => key === path || key.startsWith(`${path}.`)) : requiredKeys === '*'; }; /** * * Check if a value is an object and determines whether it should skip based on `ignoreEmpty` flag. * @param obj Object value to check. * @returns `true` if the object is skippable, otherwise `false`. */ const _skipObject = (obj) => { return ignoreEmpty && isObject(obj) && !isNotEmptyObject(obj); }; /** Determines if a value is file-like or date-like object */ const _shouldNotProcess = (value) => { return (isCustomFile(value) || isFileList(value) || isFileOrBlob(value) || isFileUpload(value) || isDateLike(value)); }; /** * * Recursively process an array and its nested content(s). * @param arr Array to process. * @param path Full path as dot notation if needed. * @returns Processed array. */ const _processArray = (arr, path) => { return arr ?.map((item) => { if (isString(item) && trimStrings) { return trimString(item); } if (Array.isArray(item)) { // Recursive sanitize return _processArray(item, path); } if (isObject(item)) { return _processObject(item, path); } return item; }) ?.filter((v) => { if (ignoreNullish && v == null) return false; if (ignoreFalsy && !v) return false; if (_skipObject(v) && !_isRequiredKey(path)) return false; return true; }); }; /** * * Helper function to process a single object. * * @param object The object to process. * @param parentPath The parent path of a key. * */ const _processObject = (object, parentPath = '') => Object.entries(object).reduce((acc, [key, value]) => { // Compute the full key path const fullKeyPath = parentPath ? `${parentPath}.${key}` : key; // Skip ignored keys if (ignoreKeySet.has(fullKeyPath)) { return acc; } // Exclude nullish values if specified if (ignoreNullish && !_isRequiredKey(fullKeyPath) && value == null) { return acc; } // Exclude falsy values `0`, `false`, `null` and `undefined` if (ignoreFalsy && !value && !_isRequiredKey(fullKeyPath)) { return acc; } if (isString(value) && trimStrings) { // Trim string values if enabled acc[key] = trimString(value); } else if (_shouldNotProcess(value)) { acc[key] = value; } else if (value && isObject(value)) { if (_shouldNotProcess(value)) { acc[key] = value; } else { // Recursively process nested objects const processedValue = _processObject(value, fullKeyPath); // Add the property conditionally if it's not an empty object if (!ignoreEmpty || _isRequiredKey(fullKeyPath) || isNotEmptyObject(processedValue)) { acc[key] = processedValue; } } } else if (value && Array.isArray(value)) { const processedArray = _processArray(value, fullKeyPath); if (!ignoreEmpty || _isRequiredKey(fullKeyPath) || processedArray?.length > 0) { acc[key] = processedArray; } } else { // Add other values untouched acc[key] = value; } return acc; }, {}); // Process strings if (isString(input)) { return trimString(input); } // Process array of strings and objects if (Array.isArray(input)) { // Process array of strings if (isArrayOfType(input, isString)) { return trimString(input); } // * Handle arrays with nested strings/arrays/objects return input ?.map((item) => sanitizeData(item, options, _return)) ?.filter((val) => { if (ignoreNullish && val == null) return false; if (ignoreFalsy && !val) return false; if (_skipObject(val)) return false; return true; }); } // Process object if (isObject(input)) { return _processObject(input); } return input; } /** * * Parse an object of stringified values into their appropriate primitive types. * * @description * - Attempts to convert string values into `boolean`, `number`, or JSON-parsed objects/arrays. * - Non-string values except arrays/objects are left unchanged. Nested arrays/objects are parsed recursively. * * @param object - The object with potentially stringified primitive values. * @param parseNested - Whether to convert stringified primitives in nested arrays/objects. (default: `true`). * @returns A new object with parsed values converted to their original types. */ export function parseObjectValues(object, parseNested = true) { function _deepParseValues(data) { if (Array.isArray(data)) { return data?.map(_deepParseValues); } else if (isNotEmptyObject(data)) { const result = {}; for (const [key, value] of Object.entries(data)) { result[key] = parseNested ? _deepParseValues(value) : value; } return result; } else if (isString(data)) { try { const parsed = JSON.parse(data); return _deepParseValues(parsed); } catch { if (data === 'true') return true; else if (data === 'false') { return false; } else if (data === 'null') { return null; } else if (data === 'undefined') { return undefined; } else if (!isNaN(Number(data))) { return Number(data); } else return data; } } return data; } const parsedBody = {}; if (isNotEmptyObject(object)) { Object.entries(object)?.forEach(([key, value]) => { parsedBody[key] = _deepParseValues(value); }); } return parsedBody; }