|
|
|
@ -31,6 +31,44 @@ export function nullifyValuesInObjects<T>(aObjects: T[], keys: string[]): T[] { |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// Helper: Custom fast clone (faster than structuredClone for our use case)
|
|
|
|
function fastClone(obj: any, seen = new WeakMap()): any { |
|
|
|
// Handle primitives and null
|
|
|
|
if (obj === null || typeof obj !== 'object') { |
|
|
|
return obj; |
|
|
|
} |
|
|
|
|
|
|
|
// Don't clone Big.js instances
|
|
|
|
if (obj instanceof Big) { |
|
|
|
return obj; |
|
|
|
} |
|
|
|
|
|
|
|
// Handle circular references
|
|
|
|
if (seen.has(obj)) { |
|
|
|
return seen.get(obj); |
|
|
|
} |
|
|
|
|
|
|
|
// Handle arrays
|
|
|
|
if (isArray(obj)) { |
|
|
|
const arr: any[] = []; |
|
|
|
seen.set(obj, arr); |
|
|
|
for (let i = 0; i < obj.length; i++) { |
|
|
|
arr[i] = fastClone(obj[i], seen); |
|
|
|
} |
|
|
|
return arr; |
|
|
|
} |
|
|
|
|
|
|
|
// Handle objects
|
|
|
|
const cloned: any = {}; |
|
|
|
seen.set(obj, cloned); |
|
|
|
const keys = Object.keys(obj); |
|
|
|
for (let i = 0; i < keys.length; i++) { |
|
|
|
const key = keys[i]; |
|
|
|
cloned[key] = fastClone(obj[key], seen); |
|
|
|
} |
|
|
|
return cloned; |
|
|
|
} |
|
|
|
|
|
|
|
export function redactAttributes({ |
|
|
|
isFirstRun = true, |
|
|
|
object, |
|
|
|
@ -44,44 +82,75 @@ export function redactAttributes({ |
|
|
|
return object; |
|
|
|
} |
|
|
|
|
|
|
|
// Create deep clone
|
|
|
|
const redactedObject = isFirstRun |
|
|
|
? JSON.parse(JSON.stringify(object)) |
|
|
|
: object; |
|
|
|
|
|
|
|
for (const option of options) { |
|
|
|
if (redactedObject.hasOwnProperty(option.attribute)) { |
|
|
|
if (option.valueMap['*'] || option.valueMap['*'] === null) { |
|
|
|
redactedObject[option.attribute] = option.valueMap['*']; |
|
|
|
} else if (option.valueMap[redactedObject[option.attribute]]) { |
|
|
|
redactedObject[option.attribute] = |
|
|
|
option.valueMap[redactedObject[option.attribute]]; |
|
|
|
} |
|
|
|
// Optimization 1: Use custom fast clone instead of structuredClone
|
|
|
|
const redactedObject = isFirstRun ? fastClone(object) : object; |
|
|
|
|
|
|
|
// Optimization 2: Pre-process options and separate wildcards from conditional mappings
|
|
|
|
const wildcardAttrs = new Map<string, any>(); |
|
|
|
const conditionalAttrs = new Map<string, { [key: string]: any }>(); |
|
|
|
|
|
|
|
for (const opt of options) { |
|
|
|
if ('*' in opt.valueMap) { |
|
|
|
wildcardAttrs.set(opt.attribute, opt.valueMap['*']); |
|
|
|
} else { |
|
|
|
// If the attribute is not present on the current object,
|
|
|
|
// check if it exists on any nested objects
|
|
|
|
for (const property in redactedObject) { |
|
|
|
if (isArray(redactedObject[property])) { |
|
|
|
redactedObject[property] = redactedObject[property].map( |
|
|
|
(currentObject) => { |
|
|
|
return redactAttributes({ |
|
|
|
options, |
|
|
|
isFirstRun: false, |
|
|
|
object: currentObject |
|
|
|
}); |
|
|
|
conditionalAttrs.set(opt.attribute, opt.valueMap); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Optimization 3: Use iterative traversal with pointer-based queue
|
|
|
|
const workQueue: any[] = [redactedObject]; |
|
|
|
let queueIndex = 0; |
|
|
|
|
|
|
|
while (queueIndex < workQueue.length) { |
|
|
|
const current = workQueue[queueIndex++]; |
|
|
|
|
|
|
|
// Skip null/undefined
|
|
|
|
if (current == null) { |
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
// Process wildcard attributes first (most common case)
|
|
|
|
for (const [attribute, replacementValue] of wildcardAttrs) { |
|
|
|
if (attribute in current) { |
|
|
|
current[attribute] = replacementValue; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Process conditional attributes
|
|
|
|
for (const [attribute, valueMap] of conditionalAttrs) { |
|
|
|
if (attribute in current) { |
|
|
|
const currentValue = current[attribute]; |
|
|
|
if (currentValue in valueMap) { |
|
|
|
current[attribute] = valueMap[currentValue]; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Optimization 4: Use Object.keys() instead of for...in (faster, no inherited props)
|
|
|
|
const keys = Object.keys(current); |
|
|
|
for (let i = 0; i < keys.length; i++) { |
|
|
|
const key = keys[i]; |
|
|
|
const value = current[key]; |
|
|
|
|
|
|
|
// Optimization 5: Cache type check
|
|
|
|
const valueType = typeof value; |
|
|
|
if (valueType !== 'object' || value === null) { |
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
if (isArray(value)) { |
|
|
|
// Optimization 6: Batch array processing
|
|
|
|
if (value.length > 0) { |
|
|
|
for (let j = 0; j < value.length; j++) { |
|
|
|
const item = value[j]; |
|
|
|
if (item != null && typeof item === 'object') { |
|
|
|
workQueue.push(item); |
|
|
|
} |
|
|
|
); |
|
|
|
} else if ( |
|
|
|
isObject(redactedObject[property]) && |
|
|
|
!(redactedObject[property] instanceof Big) |
|
|
|
) { |
|
|
|
// Recursively call the function on the nested object
|
|
|
|
redactedObject[property] = redactAttributes({ |
|
|
|
options, |
|
|
|
isFirstRun: false, |
|
|
|
object: redactedObject[property] |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
} else if (!(value instanceof Big)) { |
|
|
|
// Push object to queue (Big.js instances excluded)
|
|
|
|
workQueue.push(value); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|