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
JavaScript
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;
}