Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 795077a

Browse files
committedMar 17, 2023
Added new depth implementation, support for toJSON, support symbol property keys
1 parent 0ba2c45 commit 795077a

File tree

2 files changed

+236
-110
lines changed

2 files changed

+236
-110
lines changed
 

‎packages/core/src/Utils.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,12 @@ export function endsWith(input: string, suffix: string): boolean {
177177
* 3. If the value is an array, it will be pruned to the specified depth and truncated.
178178
* 4. If the value is an object, it will be pruned to the specified depth and
179179
* a. If the object is a Circular Reference it will return undefined.
180-
* b. If the object is a Map, it will be converted to an object.
180+
* b. If the object is a Map, it will be converted to an object. Some data loss might occur if map keys are object types as last in wins.
181181
* c. If the object is a Set, it will be converted to an array.
182182
* d. If the object contains prototype properties, they will be picked up.
183-
* e. If the object is is uniterable and not clonable (e.g., WeakMap, WeakSet, etc.), it will return undefined.
183+
* e. If the object contains a toJSON function, it will be called and it's value will be normalized.
184+
* f. If the object is is uniterable and not clonable (e.g., WeakMap, WeakSet, etc.), it will return undefined.
185+
* g. If a symbol property is encountered, it will be converted to a string representation and could overwrite existing object keys.
184186
* 5. If the value is an Error, we will treat it as an object.
185187
* 6. If the value is a primitive, it will be returned as is unless it is a string could be truncated.
186188
* 7. If the value is a Regexp, Symbol we will convert it to the string representation.
@@ -221,6 +223,10 @@ export function prune(value: unknown, depth: number = 10): unknown {
221223
}
222224

223225
function normalizeValue(value: unknown): unknown {
226+
function hasToJSONFunction(value: unknown): value is { toJSON: () => unknown } {
227+
return value !== null && typeof value === "object" && typeof (value as { toJSON?: unknown }).toJSON === "function";
228+
}
229+
224230
if (typeof value === "bigint") {
225231
return `${value.toString()}n`;
226232
}
@@ -248,6 +254,11 @@ export function prune(value: unknown, depth: number = 10): unknown {
248254
return Array.from(value as Iterable<unknown>);
249255
}
250256

257+
if (hasToJSONFunction(value)) {
258+
// NOTE: We are not checking for circular references or overflow
259+
return normalizeValue(value.toJSON());
260+
}
261+
251262
return value;
252263
}
253264

@@ -258,7 +269,7 @@ export function prune(value: unknown, depth: number = 10): unknown {
258269
return value;
259270
}
260271

261-
function pruneImpl(value: unknown, maxDepth: number, currentDepth: number = 10, seen: WeakSet<object> = new WeakSet()): unknown {
272+
function pruneImpl(value: unknown, maxDepth: number, currentDepth: number = 10, seen: WeakSet<object> = new WeakSet(), parentIsArray: boolean = false): unknown {
262273
if (value === null || value === undefined) {
263274
return value;
264275
}
@@ -277,8 +288,14 @@ export function prune(value: unknown, depth: number = 10): unknown {
277288
return normalizedValue;
278289
}
279290

291+
if (currentDepth == maxDepth) {
292+
return undefined;
293+
}
294+
280295
if (Array.isArray(normalizedValue)) {
281-
return normalizedValue.map(e => pruneImpl(e, maxDepth, currentDepth + 1, seen));
296+
// Treat an object inside of an array as a single level
297+
const depth: number = parentIsArray ? currentDepth + 1 : currentDepth;
298+
return normalizedValue.map(e => pruneImpl(e, maxDepth, depth, seen, true));
282299
}
283300

284301
// Check for circular references
@@ -291,9 +308,17 @@ export function prune(value: unknown, depth: number = 10): unknown {
291308
}
292309

293310
const result: Record<PropertyKey, unknown> = {};
294-
for (const key in value) {
295-
const val = (value as { [index: PropertyKey]: unknown })[key];
296-
result[key] = pruneImpl(val, maxDepth, currentDepth + 1, seen);
311+
for (const key in normalizedValue) {
312+
const objectValue = (normalizedValue as { [index: PropertyKey]: unknown })[key];
313+
result[key] = pruneImpl(objectValue, maxDepth, currentDepth + 1, seen);
314+
}
315+
316+
for (const symbolKey of Object.getOwnPropertySymbols(normalizedValue)) {
317+
// Normalize the key so Symbols are converted to strings.
318+
const normalizedKey = normalizeValue(symbolKey) as PropertyKey;
319+
320+
const objectValue = (normalizedValue as { [index: PropertyKey]: unknown })[symbolKey];
321+
result[normalizedKey] = pruneImpl(objectValue, maxDepth, currentDepth + 1, seen);
297322
}
298323

299324
return result;

‎packages/core/test/Utils.test.ts

Lines changed: 204 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@ import {
1212
} from "../src/Utils.js";
1313

1414
describe("Utils", () => {
15+
function getObjectWithInheritedProperties(): unknown {
16+
// @ts-expect-error TS2683
17+
const Foo = function () { this.a = "a"; };
18+
// @ts-expect-error TS2683
19+
const Bar = function () { this.b = "b"; };
20+
// @ts-expect-error TS7009
21+
Bar.prototype = new Foo();
22+
// @ts-expect-error TS7009
23+
return new Bar();
24+
}
25+
1526
describe("prune", () => {
1627
test("circular reference", () => {
1728
type Circular = { property: string, circularRef?: Circular };
@@ -110,6 +121,7 @@ describe("Utils", () => {
110121
});
111122
});
112123

124+
// NOTE: Buffer could be supported as it specifies a toJSON method
113125
const unsupportedValues = {
114126
"asyncGenerator": (async function* () { await Promise.resolve(1); yield 1; })(),
115127
"arrayBuffer": new ArrayBuffer(1),
@@ -152,20 +164,37 @@ describe("Utils", () => {
152164
});
153165

154166
test("for Object", () => {
155-
const expected = { a: {}, b: 1 };
167+
const expected = { a: undefined, b: 1 };
156168
const actual = prune({ a: { b: 2 }, b: 1 }, 1);
157169
expect(actual).toStrictEqual(expected);
158170
});
159171

160172
test("for Array", () => {
161-
const expected = [{}, 1];
162-
const actual = prune([{ a: { b: 2 } }, 1], 1);
173+
const expected = [{ a: undefined }, [undefined], 1];
174+
const actual = prune([{ a: { b: 2 } }, [[]], 1], 1);
163175
expect(actual).toStrictEqual(expected);
164176
});
165177

166178
test("for Map", () => {
167-
const expected = new Map([[{}, { a: {}, b: 1 }]]);
168-
const actual = prune(new Map([[{}, { a: { b: 2 }, b: 1 }]]), 2);
179+
const expected = {
180+
"123": 123,
181+
"[object Object]": {
182+
"a2": undefined,
183+
"b2": 1
184+
},
185+
"string key": "string key",
186+
"symbol": ["symbol key"]
187+
};
188+
189+
const actual = prune(new Map<unknown, unknown>([
190+
// NOTE: this value is lost due to being converted to ["[object Object]", { a: { b: 2 }, b: 1 }]
191+
[{ id: 1 }, { a: { b: 2 }, b: 1 }],
192+
[{ id: 2 }, { a2: { b2: 2 }, b2: 1 }],
193+
["string key", "string key"],
194+
[123, 123],
195+
[Symbol("symbol"), ["symbol key"]]
196+
]), 2);
197+
169198
expect(actual).toStrictEqual(expected);
170199
});
171200

@@ -175,15 +204,29 @@ describe("Utils", () => {
175204
});
176205

177206
test("for Set", () => {
178-
const expected = new Set([{ a: {}, b: 1 }]);
179-
const actual = prune(new Set([{ a: { b: 2 }, b: 1 }]), 2);
207+
const expected = [{ "a": undefined, "b": 1 }, 1];
208+
const actual = prune(new Set([{ a: { b: 2 }, b: 1 }, 1]), 1);
180209
expect(actual).toStrictEqual(expected);
181210
});
182211

183212
test("for WeakSet", () => {
184213
const actual = prune(new WeakSet([{ a: { b: 2 } }]), 2);
185214
expect(actual).toBeUndefined();
186215
});
216+
217+
test("should handle toJSON", () => {
218+
const expected = { test: "test" };
219+
const actual = prune({
220+
number: 1,
221+
toJSON() {
222+
return {
223+
test: "test"
224+
};
225+
}
226+
});
227+
228+
expect(actual).toStrictEqual(expected);
229+
});
187230
});
188231

189232
test("should respect maxDepth", () => {
@@ -208,47 +251,25 @@ describe("Utils", () => {
208251
};
209252

210253
expect(prune(value, 1)).toStrictEqual({ "ao": undefined });
211-
expect(prune(value, 2)).toStrictEqual({ "ao": { "bo": undefined, "ba": [], "bn": 1 } });
212-
expect(prune(value, 3)).toStrictEqual({ "ao": { "bo": { "cn": 1, "co": undefined }, "ba": [undefined], "bn": 1 } });
213-
expect(prune(value, 4)).toStrictEqual({ "ao": { "bo": { "cn": 1, "co": { "do": undefined } }, "ba": [{ "cn": 1, "co": undefined }], "bn": 1 } });
214-
expect(prune(value, 5)).toStrictEqual({ "ao": { "bo": { "cn": 1, "co": { "do": {} } }, "ba": [{ "cn": 1, "co": { "do": undefined } }], "bn": 1 } });
254+
expect(prune(value, 2)).toStrictEqual({ "ao": { "bo": undefined, "ba": undefined, "bn": 1 } });
255+
expect(prune(value, 3)).toStrictEqual({ "ao": { "bo": { "cn": 1, "co": undefined }, "ba": [{ "cn": 1, "co": undefined }], "bn": 1 } });
256+
expect(prune(value, 4)).toStrictEqual({ "ao": { "bo": { "cn": 1, "co": { "do": undefined } }, "ba": [{ "cn": 1, "co": { "do": undefined } }], "bn": 1 } });
257+
expect(prune(value, 5)).toStrictEqual({ "ao": { "bo": { "cn": 1, "co": { "do": {} } }, "ba": [{ "cn": 1, "co": { "do": {} } }], "bn": 1 } });
215258
});
216259

217260
test("should prune inherited properties", () => {
218-
// @ts-expect-error TS2683
219-
const Foo = function () { this.a = "a"; };
220-
// @ts-expect-error TS2683
221-
const Bar = function () { this.b = "b"; };
222-
// @ts-expect-error TS7009
223-
Bar.prototype = new Foo();
224-
// @ts-expect-error TS7009
225-
const bar = new Bar();
226-
227261
const expected = {
228262
a: "a",
229263
b: "b"
230264
};
231265

232-
const actual = prune(bar, 1);
266+
const actual = prune(getObjectWithInheritedProperties(), 1);
233267
expect(actual).toStrictEqual(expected);
234268
});
235269
});
236270

237271
describe("stringify", () => {
238-
const user = {
239-
id: 1,
240-
name: "Blake",
241-
password: "123456",
242-
passwordResetToken: "a reset token",
243-
myPassword: "123456",
244-
myPasswordValue: "123456",
245-
customValue: "Password",
246-
value: {
247-
Password: "123456"
248-
}
249-
};
250-
251-
test("error array", () => {
272+
test("event error", () => {
252273
const error = {
253274
type: "error",
254275
data: {
@@ -283,30 +304,16 @@ describe("Utils", () => {
283304
};
284305

285306
expect(stringify(error)).toBe(JSON.stringify(error));
286-
expect(stringify([error, error])).toBe(JSON.stringify([error, error]));
287307
});
288308

289309
test("circular reference", () => {
290310
type Circular = { property: string, circularRef?: Circular };
291311
const circular: Circular = { property: "string" };
292312
circular.circularRef = circular;
293313

294-
const expected = JSON.stringify({ "property": "string", "circularRef": {} });
314+
const expected = JSON.stringify({ "property": "string", "circularRef": undefined });
295315
const actual = stringify(circular);
296-
expect(actual).toBe(expected);
297-
});
298-
299-
test("deep circular object reference", () => {
300-
const a: { b?: unknown } = {};
301-
const b: { c?: unknown } = {};
302-
const c: { a?: unknown, d: string } = { d: "test" };
303-
304-
a.b = b;
305-
b.c = c;
306-
c.a = a;
307-
308-
const actual = stringify(a);
309-
expect(actual).toBe("{\"b\":{\"c\":{\"d\":\"test\",\"a\":\"{}\"}}}");
316+
expect(actual).toStrictEqual(expected);
310317
});
311318

312319
test("circular array reference", () => {
@@ -315,65 +322,153 @@ describe("Utils", () => {
315322
circular.circularRef = circular;
316323
circular.list = [circular];
317324

318-
const expected = JSON.stringify({ "property": "string", "circularRef": {}, "list": [{}] });
325+
const expected = JSON.stringify({ "property": "string", "circularRef": undefined, "list": [undefined] });
319326
const actual = stringify(circular);
320-
expect(actual).toBe(expected);
327+
expect(actual).toStrictEqual(expected);
321328
});
322329

323-
describe("should serialize all data types", () => {
324-
const value = {
330+
describe("should serialize data types", () => {
331+
const primitiveValues = {
325332
"undefined": undefined,
326333
"null": null,
327334
"string": "string",
328335
"number": 1,
329336
"boolean": true,
330-
"array": [1, 2, 3],
331-
"object": { "a": 1, "b": 2, "c": 3 },
332-
"date": new Date(),
333-
"function": () => { return undefined; },
334-
"error": new Error("error"),
335-
"map": new Map([["a", 1], ["b", 2], ["c", 3]]),
336-
"weakMap": new WeakMap([[{}, 1], [{}, 2], [{}, 3]]),
337-
"set": new Set([1, 2, 3]),
337+
"date": new Date()
338+
};
339+
340+
Object.entries(primitiveValues).forEach(([key, value]) => {
341+
test(`for ${key}`, () => {
342+
const actual = stringify(value, [], 1);
343+
const expected = JSON.stringify(value);
344+
expect(actual).toStrictEqual(expected);
345+
});
346+
});
347+
348+
const typedArrayValues = {
349+
"int8Array": new Int8Array([1]),
350+
"uint8Array": new Uint8Array([1]),
351+
"uint8ClampedArray": new Uint8ClampedArray([1]),
352+
"int16Array": new Int16Array([1]),
353+
"uint16Array": new Uint16Array([1]),
354+
"int32Array": new Int32Array([1]),
355+
"uint32Array": new Uint32Array([1]),
356+
"float32Array": new Float32Array([1]),
357+
"float64Array": new Float64Array([1])
358+
};
359+
360+
Object.entries(typedArrayValues).forEach(([key, value]) => {
361+
test(`for ${key}`, () => {
362+
const actual = stringify(value, [], 1);
363+
const expected = JSON.stringify([1]);
364+
expect(actual).toStrictEqual(expected);
365+
});
366+
});
367+
368+
const bigIntTypedArrayValues = {
369+
"bigint64Array": new BigInt64Array([1n]),
370+
"bigUint64Array": new BigUint64Array([1n])
371+
};
372+
373+
Object.entries(bigIntTypedArrayValues).forEach(([key, value]) => {
374+
test(`for ${key}`, () => {
375+
const actual = stringify(value, [], 1);
376+
const expected = JSON.stringify(["1n"]);
377+
expect(actual).toStrictEqual(expected);
378+
});
379+
});
380+
381+
// NOTE: Buffer could be supported as it specifies a toJSON method
382+
const unsupportedValues = {
383+
"asyncGenerator": (async function* () { await Promise.resolve(1); yield 1; })(),
338384
"arrayBuffer": new ArrayBuffer(1),
385+
"buffer": Buffer.from("buffer"),
339386
"dataView": new DataView(new ArrayBuffer(1)),
340-
"int8Array": new Int8Array(1),
341-
"uint8Array": new Uint8Array(1),
342-
"uint8ClampedArray": new Uint8ClampedArray(1),
343-
"int16Array": new Int16Array(1),
344-
"uint16Array": new Uint16Array(1),
345-
"int32Array": new Int32Array(1),
346-
"uint32Array": new Uint32Array(1),
347-
"float32Array": new Float32Array(1),
348-
"float64Array": new Float64Array(1),
349-
"promise": Promise.resolve(1),
387+
"function": () => { return undefined; },
350388
"generator": (function* () { yield 1; })(),
389+
"promise": Promise.resolve(1)
351390
};
352391

353-
Object.entries(value).forEach(([key, value]) => {
392+
Object.entries(unsupportedValues).forEach(([key, value]) => {
354393
test(`for ${key}`, () => {
355-
const expected = JSON.stringify(value);
356-
const actual = stringify(value);
357-
expect(actual).toBe(expected);
394+
const actual = stringify(value, [], 1);
395+
expect(actual).toBeUndefined();
358396
});
359397
});
360398

361-
test(`for bigint`, () => {
362-
expect(stringify(BigInt(1))).toBe(1);
363-
expect(stringify(new BigInt64Array(1))).toBe(1);
364-
expect(stringify(new BigUint64Array(1))).toBe(1)
399+
test("for BigInt", () => {
400+
const expected = JSON.stringify("1n");
401+
const actual = stringify(BigInt(1), [], 1);
402+
expect(actual).toStrictEqual(expected);
403+
});
404+
405+
test("for RegExp", () => {
406+
const expected = JSON.stringify("/regex/");
407+
const actual = stringify(/regex/, [], 1);
408+
expect(actual).toStrictEqual(expected);
409+
});
410+
411+
test("for Symbol", () => {
412+
const expected = JSON.stringify("symbol");
413+
const actual = stringify(Symbol("symbol"), [], 1);
414+
expect(actual).toStrictEqual(expected);
415+
});
416+
417+
test("for Error", () => {
418+
const expected = JSON.stringify({ "message": "error" });
419+
const actual = stringify(new Error("error"), [], 1);
420+
expect(actual).toStrictEqual(expected);
421+
});
422+
423+
test("for Object", () => {
424+
const expected = JSON.stringify({ a: undefined, b: 1 });
425+
const actual = stringify({ a: { b: 2 }, b: 1 }, [], 1);
426+
expect(actual).toStrictEqual(expected);
427+
});
428+
429+
test("for Array", () => {
430+
const expected = JSON.stringify([{ a: undefined }, [undefined], 1]);
431+
const actual = stringify([{ a: { b: 2 } }, [[]], 1], [], 1);
432+
expect(actual).toStrictEqual(expected);
433+
});
434+
435+
test("for Map", () => {
436+
const expected = JSON.stringify({
437+
"123": 123,
438+
"[object Object]": {
439+
"a2": undefined,
440+
"b2": 1
441+
},
442+
"string key": "string key",
443+
"symbol": ["symbol key"]
444+
});
445+
446+
const actual = stringify(new Map<unknown, unknown>([
447+
// NOTE: this value is lost due to being converted to ["[object Object]", { a: { b: 2 }, b: 1 }]
448+
[{ id: 1 }, { a: { b: 2 }, b: 1 }],
449+
[{ id: 2 }, { a2: { b2: 2 }, b2: 1 }],
450+
["string key", "string key"],
451+
[123, 123],
452+
[Symbol("symbol"), ["symbol key"]]
453+
]), [], 2);
454+
455+
expect(actual).toStrictEqual(expected);
365456
});
366457

367-
test(`for buffer`, () => {
368-
expect(stringify(Buffer.from("buffer"))).toBe("{\"type\":\"Buffer\",\"data\":[98,117,102,102,101,114]}");
458+
test("for WeakMap", () => {
459+
const actual = stringify(new WeakMap([[{}, { a: { b: 2 } }]]), [], 2);
460+
expect(actual).toBeUndefined();
369461
});
370462

371-
test(`for symbol`, () => {
372-
expect(stringify(Symbol("symbol"))).toBe("\"symbol\"");
463+
test("for Set", () => {
464+
const expected = JSON.stringify([{ "a": undefined, "b": 1 }, 1]);
465+
const actual = stringify(new Set([{ a: { b: 2 }, b: 1 }, 1]), [], 1);
466+
expect(actual).toStrictEqual(expected);
373467
});
374468

375-
test(`for regex`, () => {
376-
expect(stringify(/regex/)).toBe("\"/regex/\"");
469+
test("for WeakSet", () => {
470+
const actual = stringify(new WeakSet([{ a: { b: 2 } }]), [], 2);
471+
expect(actual).toBeUndefined();
377472
});
378473
});
379474

@@ -387,7 +482,9 @@ describe("Utils", () => {
387482
}
388483
};
389484

390-
expect(stringify(value)).toBe(JSON.stringify(value));
485+
const expected = JSON.stringify(value);
486+
const actual = stringify(value);
487+
expect(actual).toStrictEqual(expected);
391488
});
392489

393490
test("should respect maxDepth", () => {
@@ -411,33 +508,37 @@ describe("Utils", () => {
411508
}
412509
};
413510

414-
expect(stringify(value, undefined, 1)).toBe("{\"ao\":{}}");
415-
expect(stringify(value, undefined, 2)).toBe("{\"ao\":{\"bo\":{},\"ba\":[],\"bn\":1}}");
416-
expect(stringify(value, undefined, 3)).toBe("{\"ao\":{\"bo\":{\"cn\":1,\"co\":{}},\"ba\":[{}],\"bn\":1}}");
417-
expect(stringify(value, undefined, 4)).toBe("{\"ao\":{\"bo\":{\"cn\":1,\"co\":{\"do\":{}}},\"ba\":[{\"cn\":1,\"co\":{}}],\"bn\":1}}");
418-
expect(stringify(value, undefined, 5)).toBe("{\"ao\":{\"bo\":{\"cn\":1,\"co\":{\"do\":{}}},\"ba\":[{\"cn\":1,\"co\":{\"do\":{}}}],\"bn\":1}}");
511+
expect(stringify(value, [], 1)).toStrictEqual(JSON.stringify({ "ao": undefined }));
512+
expect(stringify(value, [], 2)).toStrictEqual(JSON.stringify({ "ao": { "bo": undefined, "ba": undefined, "bn": 1 } }));
513+
expect(stringify(value, [], 3)).toStrictEqual(JSON.stringify({ "ao": { "bo": { "cn": 1, "co": undefined }, "ba": [{ "cn": 1, "co": undefined }], "bn": 1 } }));
514+
expect(stringify(value, [], 4)).toStrictEqual(JSON.stringify({ "ao": { "bo": { "cn": 1, "co": { "do": undefined } }, "ba": [{ "cn": 1, "co": { "do": undefined } }], "bn": 1 } }));
515+
expect(stringify(value, [], 5)).toStrictEqual(JSON.stringify({ "ao": { "bo": { "cn": 1, "co": { "do": {} } }, "ba": [{ "cn": 1, "co": { "do": {} } }], "bn": 1 } }));
419516
});
420517

421518
test("should serialize inherited properties", () => {
422-
// @ts-expect-error TS2683
423-
const Foo = function () { this.a = "a"; };
424-
// @ts-expect-error TS2683
425-
const Bar = function () { this.b = "b"; };
426-
// @ts-expect-error TS7009
427-
Bar.prototype = new Foo();
428-
// @ts-expect-error TS7009
429-
const bar = new Bar();
430-
431519
const expected = {
432520
a: "a",
433521
b: "b"
434522
};
435523

436-
const actual = JSON.parse(stringify(bar) as string) as unknown;
524+
const actual = JSON.parse(stringify(getObjectWithInheritedProperties()) as string) as unknown;
437525
expect(actual).toEqual(expected);
438526
});
439527

440528
describe("with exclude pattern", () => {
529+
const user = {
530+
id: 1,
531+
name: "Blake",
532+
password: "123456",
533+
passwordResetToken: "a reset token",
534+
myPassword: "123456",
535+
myPasswordValue: "123456",
536+
customValue: "Password",
537+
value: {
538+
Password: "123456"
539+
}
540+
};
541+
441542
test("pAssword", () => {
442543
expect(stringify(user, ["pAssword"])).toBe(
443544
JSON.stringify({ "id": 1, "name": "Blake", "passwordResetToken": "a reset token", "myPassword": "123456", "myPasswordValue": "123456", "customValue": "Password", "value": {} })

0 commit comments

Comments
 (0)
Please sign in to comment.