index.js 3.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. import set from "../utils/lodash/set.js";
  2. function extractStringNodes(data, options) {
  3. const parsedOptions = { ...options, maxDepth: options.maxDepth ?? 10 };
  4. const queue = [
  5. [data, 0, ""],
  6. ];
  7. const result = [];
  8. while (queue.length > 0) {
  9. const task = queue.shift();
  10. if (task == null)
  11. continue;
  12. const [value, depth, path] = task;
  13. if (typeof value === "object" && value != null) {
  14. if (depth >= parsedOptions.maxDepth)
  15. continue;
  16. for (const [key, nestedValue] of Object.entries(value)) {
  17. queue.push([nestedValue, depth + 1, path ? `${path}.${key}` : key]);
  18. }
  19. }
  20. else if (Array.isArray(value)) {
  21. if (depth >= parsedOptions.maxDepth)
  22. continue;
  23. for (let i = 0; i < value.length; i++) {
  24. queue.push([value[i], depth + 1, `${path}[${i}]`]);
  25. }
  26. }
  27. else if (typeof value === "string") {
  28. result.push({ value, path });
  29. }
  30. }
  31. return result;
  32. }
  33. function deepClone(data) {
  34. return JSON.parse(JSON.stringify(data));
  35. }
  36. export function createAnonymizer(replacer, options) {
  37. return (data) => {
  38. let mutateValue = deepClone(data);
  39. const nodes = extractStringNodes(mutateValue, {
  40. maxDepth: options?.maxDepth,
  41. });
  42. const processor = Array.isArray(replacer)
  43. ? (() => {
  44. const replacers = replacer.map(({ pattern, type, replace }) => {
  45. if (type != null && type !== "pattern")
  46. throw new Error("Invalid anonymizer type");
  47. return [
  48. typeof pattern === "string"
  49. ? new RegExp(pattern, "g")
  50. : pattern,
  51. replace ?? "[redacted]",
  52. ];
  53. });
  54. if (replacers.length === 0)
  55. throw new Error("No replacers provided");
  56. return {
  57. maskNodes: (nodes) => {
  58. return nodes.reduce((memo, item) => {
  59. const newValue = replacers.reduce((value, [regex, replace]) => {
  60. const result = value.replace(regex, replace);
  61. // make sure we reset the state of regex
  62. regex.lastIndex = 0;
  63. return result;
  64. }, item.value);
  65. if (newValue !== item.value) {
  66. memo.push({ value: newValue, path: item.path });
  67. }
  68. return memo;
  69. }, []);
  70. },
  71. };
  72. })()
  73. : typeof replacer === "function"
  74. ? {
  75. maskNodes: (nodes) => nodes.reduce((memo, item) => {
  76. const newValue = replacer(item.value, item.path);
  77. if (newValue !== item.value) {
  78. memo.push({ value: newValue, path: item.path });
  79. }
  80. return memo;
  81. }, []),
  82. }
  83. : replacer;
  84. const toUpdate = processor.maskNodes(nodes);
  85. for (const node of toUpdate) {
  86. if (node.path === "") {
  87. mutateValue = node.value;
  88. }
  89. else {
  90. set(mutateValue, node.path, node.value);
  91. }
  92. }
  93. return mutateValue;
  94. };
  95. }