Skip to content

Commit dece6ee

Browse files
committed
feat: Introduce safeNormalize util method to unify stored data
1 parent 16642e0 commit dece6ee

File tree

2 files changed

+319
-249
lines changed

2 files changed

+319
-249
lines changed

packages/utils/src/object.ts

Lines changed: 140 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -8,99 +8,6 @@ interface ExtendedError extends Error {
88
[key: string]: any;
99
}
1010

11-
/**
12-
* Transforms Error object into an object literal with all it's attributes
13-
* attached to it.
14-
*
15-
* Based on: https://github.com/ftlabs/js-abbreviate/blob/fa709e5f139e7770a71827b1893f22418097fbda/index.js#L95-L106
16-
*
17-
* @param error An Error containing all relevant information
18-
* @returns An object with all error properties
19-
*/
20-
function objectifyError(error: ExtendedError): object {
21-
// These properties are implemented as magical getters and don't show up in `for-in` loop
22-
const err: {
23-
stack: string | undefined;
24-
message: string;
25-
name: string;
26-
[key: string]: any;
27-
} = {
28-
message: error.message,
29-
name: error.name,
30-
stack: error.stack,
31-
};
32-
33-
for (const i in error) {
34-
if (Object.prototype.hasOwnProperty.call(error, i)) {
35-
err[i] = error[i];
36-
}
37-
}
38-
39-
return err;
40-
}
41-
42-
const NAN_VALUE = '[NaN]';
43-
const UNDEFINED_VALUE = '[undefined]';
44-
45-
/**
46-
* Serializer function used as 2nd argument to JSON.serialize in `serialize()` util function.
47-
*/
48-
function serializer(): (key: string, value: any) => any {
49-
const stack: any[] = [];
50-
const keys: string[] = [];
51-
const cycleReplacer = (_: string, value: any) => {
52-
if (stack[0] === value) {
53-
return '[Circular ~]';
54-
}
55-
return `[Circular ~.${keys.slice(0, stack.indexOf(value)).join('.')}]`;
56-
};
57-
58-
return function(this: any, key: string, value: any): any {
59-
let currentValue: any = value;
60-
61-
// NaN and undefined are not JSON.parseable, but we want to preserve this information
62-
if (isNaN(value)) {
63-
currentValue = NAN_VALUE;
64-
} else if (isUndefined(value)) {
65-
currentValue = UNDEFINED_VALUE;
66-
}
67-
68-
if (stack.length > 0) {
69-
const thisPos = stack.indexOf(this);
70-
71-
if (thisPos !== -1) {
72-
stack.splice(thisPos + 1);
73-
keys.splice(thisPos, Infinity, key);
74-
} else {
75-
stack.push(this);
76-
keys.push(key);
77-
}
78-
79-
if (stack.indexOf(currentValue) !== -1) {
80-
currentValue = cycleReplacer.call(this, key, currentValue);
81-
}
82-
} else {
83-
stack.push(currentValue);
84-
}
85-
86-
return currentValue instanceof Error ? objectifyError(currentValue) : currentValue;
87-
};
88-
}
89-
90-
/**
91-
* Reviver function used as 2nd argument to JSON.parse in `deserialize()` util function.
92-
*/
93-
function reviver(_key: string, value: any): any {
94-
// NaN and undefined are not JSON.parseable, but we want to preserve this information
95-
if (value === NAN_VALUE) {
96-
return NaN;
97-
}
98-
if (value === UNDEFINED_VALUE) {
99-
return undefined;
100-
}
101-
return value;
102-
}
103-
10411
/**
10512
* Serializes the given object into a string.
10613
* Like JSON.stringify, but doesn't throw on circular references.
@@ -114,7 +21,7 @@ function reviver(_key: string, value: any): any {
11421
* @returns A string containing the serialized object.
11522
*/
11623
export function serialize<T>(object: T): string {
117-
return JSON.stringify(object, serializer());
24+
return JSON.stringify(object);
11825
}
11926

12027
/**
@@ -125,7 +32,7 @@ export function serialize<T>(object: T): string {
12532
* @returns The deserialized object.
12633
*/
12734
export function deserialize<T>(str: string): T {
128-
return JSON.parse(str, reviver) as T;
35+
return JSON.parse(str) as T;
12936
}
13037

13138
/**
@@ -321,3 +228,141 @@ export function assign(target: any, ...args: any[]): object {
321228

322229
return to;
323230
}
231+
232+
/**
233+
* Transforms Error object into an object literal with all it's attributes
234+
* attached to it.
235+
*
236+
* Based on: https://github.com/ftlabs/js-abbreviate/blob/fa709e5f139e7770a71827b1893f22418097fbda/index.js#L95-L106
237+
*
238+
* @param error An Error containing all relevant information
239+
* @returns An object with all error properties
240+
*/
241+
function objectifyError(error: ExtendedError): object {
242+
// These properties are implemented as magical getters and don't show up in `for-in` loop
243+
const err: {
244+
stack: string | undefined;
245+
message: string;
246+
name: string;
247+
[key: string]: any;
248+
} = {
249+
message: error.message,
250+
name: error.name,
251+
stack: error.stack,
252+
};
253+
254+
for (const i in error) {
255+
if (Object.prototype.hasOwnProperty.call(error, i)) {
256+
err[i] = error[i];
257+
}
258+
}
259+
260+
return err;
261+
}
262+
263+
/**
264+
* standardizeValue()
265+
*
266+
* translates undefined/NaN values to "[undefined]"/"[NaN]" respectively,
267+
* serializes Error objects
268+
* filter global objects
269+
*/
270+
function standardizeValue(value: any, key: any): any {
271+
if (key === 'domain' && typeof value === 'object' && (value as { _events: any })._events) {
272+
return '[Domain]';
273+
}
274+
275+
if (key === 'domainEmitter') {
276+
return '[DomainEmitter]';
277+
}
278+
279+
if (typeof (global as any) !== 'undefined' && value === global) {
280+
return '[Global]';
281+
}
282+
283+
if (typeof (window as any) !== 'undefined' && value === window) {
284+
return '[Window]';
285+
}
286+
287+
if (typeof (document as any) !== 'undefined' && value === document) {
288+
return '[Document]';
289+
}
290+
291+
if (value instanceof Date) {
292+
return `[Date] ${value}`;
293+
}
294+
295+
if (value instanceof Error) {
296+
return objectifyError(value);
297+
}
298+
299+
if (isNaN(value)) {
300+
return '[NaN]';
301+
}
302+
303+
if (isUndefined(value)) {
304+
return '[undefined]';
305+
}
306+
307+
if (typeof value === 'function') {
308+
return `[Function] ${(value as () => void).name || '<unknown-function-name>'}`;
309+
}
310+
311+
return value;
312+
}
313+
314+
/**
315+
* standardizer()
316+
*
317+
* Remove circular references,
318+
* translates undefined/NaN values to "[undefined]"/"[NaN]" respectively,
319+
* and takes care of Error objects serialization
320+
*/
321+
function standardizer(): (key: string, value: any) => any {
322+
const stack: any[] = [];
323+
const keys: string[] = [];
324+
325+
/** recursive */
326+
function cycleStandardizer(_key: string, value: any): any {
327+
if (stack[0] === value) {
328+
return '[Circular ~]';
329+
}
330+
return `[Circular ~.${keys.slice(0, stack.indexOf(value)).join('.')}]`;
331+
}
332+
333+
return function(this: any, key: string, value: any): any {
334+
if (stack.length > 0) {
335+
const thisPos = stack.indexOf(this);
336+
337+
if (thisPos === -1) {
338+
stack.push(this);
339+
keys.push(key);
340+
} else {
341+
stack.splice(thisPos + 1);
342+
keys.splice(thisPos, Infinity, key);
343+
}
344+
345+
if (stack.indexOf(value) !== -1) {
346+
// tslint:disable-next-line:no-parameter-reassignment
347+
value = cycleStandardizer.call(this, key, value);
348+
}
349+
} else {
350+
stack.push(value);
351+
}
352+
353+
return standardizeValue(value, key);
354+
};
355+
}
356+
357+
/**
358+
* safeNormalize()
359+
*
360+
* Creates a copy of the input by applying standardizer function on it and parsing it back to unify the data
361+
*/
362+
export function safeNormalize(input: any): any {
363+
try {
364+
return JSON.parse(JSON.stringify(input, standardizer()));
365+
} catch (_oO) {
366+
return '**non-serializable**';
367+
}
368+
}

0 commit comments

Comments
 (0)