@directus/api
Version:
Directus is a real-time API and App dashboard for managing SQL database content
132 lines (131 loc) • 5.37 kB
JavaScript
import { isObject } from '@directus/utils';
/**
* Redact values in an object.
*
* @param input Input object in which values should be redacted.
* @param redact The key paths at which and values itself which should be redacted.
* @param redact.keys Nested array of key paths at which values should be redacted. (Supports `*` for shallow matching, `**` for deep matching.)
* @param redact.values Value names and the corresponding values that should be redacted.
* @param replacement Replacement function with which the values are redacted.
* @returns Redacted object.
*/
export function redactObject(input, redact, replacement) {
const wildcardChars = ['*', '**'];
const clone = JSON.parse(JSON.stringify(input, getReplacer(replacement, redact.values)));
if (redact.keys) {
traverse(clone, redact.keys);
}
return clone;
function traverse(object, checkKeyPaths) {
if (checkKeyPaths.length === 0) {
return;
}
const REDACTED_TEXT = replacement();
const globalCheckPaths = [];
for (const key of Object.keys(object)) {
const localCheckPaths = [];
for (const [index, path] of [...checkKeyPaths].entries()) {
const [current, ...remaining] = path;
const escapedKey = wildcardChars.includes(key) ? `\\${key}` : key;
switch (current) {
case escapedKey:
if (remaining.length > 0) {
localCheckPaths.push(remaining);
}
else {
object[key] = REDACTED_TEXT;
checkKeyPaths.splice(index, 1);
}
break;
case '*':
if (remaining.length > 0) {
globalCheckPaths.push(remaining);
checkKeyPaths.splice(index, 1);
}
else {
object[key] = REDACTED_TEXT;
}
break;
case '**':
if (remaining.length > 0) {
const [next, ...nextRemaining] = remaining;
if (next === escapedKey) {
if (nextRemaining.length === 0) {
object[key] = REDACTED_TEXT;
}
else {
localCheckPaths.push(nextRemaining);
}
}
else if (next !== undefined && wildcardChars.includes(next)) {
localCheckPaths.push(remaining);
}
else {
localCheckPaths.push(path);
}
}
else {
object[key] = REDACTED_TEXT;
}
break;
}
}
const value = object[key];
if (isObject(value)) {
traverse(value, [...globalCheckPaths, ...localCheckPaths]);
}
}
}
}
/**
* Replace values and extract Error objects for use with JSON.stringify()
*/
export function getReplacer(replacement, values) {
const filteredValues = values
? Object.entries(values).filter(([_k, v]) => typeof v === 'string' && v.length > 0)
: [];
const replacer = (seen) => {
return function (_key, value) {
if (value instanceof Error) {
return {
name: value.name,
message: value.message,
stack: value.stack,
cause: value.cause,
};
}
if (value !== null && typeof value === 'object') {
if (seen.has(value)) {
return '[Circular]';
}
seen.add(value);
const newValue = Array.isArray(value) ? [] : {};
for (const [key2, value2] of Object.entries(value)) {
if (typeof value2 === 'string') {
newValue[key2] = value2;
}
else {
newValue[key2] = replacer(seen)(key2, value2);
}
}
seen.delete(value);
return newValue;
}
if (!values || filteredValues.length === 0 || typeof value !== 'string')
return value;
let finalValue = value;
for (const [redactKey, valueToRedact] of filteredValues) {
if (finalValue.includes(valueToRedact)) {
const regexp = new RegExp(escapeRegexString(valueToRedact), 'g');
finalValue = finalValue.replace(regexp, replacement(redactKey));
}
}
return finalValue;
};
};
const seen = new WeakSet();
return replacer(seen);
}
function escapeRegexString(string) {
return string.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
}