From f15af7d4a28f5dc3b2fd41ff91b9db1c2664433a Mon Sep 17 00:00:00 2001 From: Lucas Burruel Date: Mon, 19 Apr 2021 02:40:56 -0700 Subject: [PATCH] Add `maxDepth` option (#43) Co-authored-by: Sindre Sorhus --- index.d.ts | 27 +++++++++++++++++++++++-- index.js | 34 +++++++++++++++++++++++++------ index.test-d.ts | 5 +++-- readme.md | 28 ++++++++++++++++++++++++-- test.js | 53 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 135 insertions(+), 12 deletions(-) diff --git a/index.d.ts b/index.d.ts index 21eab41..1a3eb6b 100644 --- a/index.d.ts +++ b/index.d.ts @@ -7,6 +7,29 @@ export type ErrorObject = { code?: string; } & JsonObject; +export interface Options { + /** + The maximum depth of properties to preserve when serializing/deserializing. + + @default Number.POSITIVE_INFINITY + + @example + ``` + import {serializeError} from 'serialize-error'; + + const error = new Error('🦄'); + error.one = {two: {three: {}}}; + + console.log(serializeError(error, {maxDepth: 1})); + //=> {name: 'Error', message: '…', one: {}} + + console.log(serializeError(error, {maxDepth: 2})); + //=> {name: 'Error', message: '…', one: { two: {}}} + ``` + */ + readonly maxDepth?: number; +} + /** Serialize an `Error` object into a plain object. @@ -55,7 +78,7 @@ console.log(serializeError(error)); // => {date: '1970-01-01T00:00:00.000Z', message: '🦄', name, stack} ``` */ -export function serializeError(error: ErrorType): ErrorType extends Primitive +export function serializeError(error: ErrorType, options?: Options): ErrorType extends Primitive ? ErrorType : ErrorObject; @@ -83,4 +106,4 @@ console.log(error); // at :1:13 ``` */ -export function deserializeError(errorObject: ErrorObject | unknown): Error; +export function deserializeError(errorObject: ErrorObject | unknown, options?: Options): Error; diff --git a/index.js b/index.js index e8a2926..a6ca592 100644 --- a/index.js +++ b/index.js @@ -43,12 +43,18 @@ const destroyCircular = ({ from, seen, to_, - forceEnumerable + forceEnumerable, + maxDepth, + depth }) => { const to = to_ || (Array.isArray(from) ? [] : {}); seen.push(from); + if (depth >= maxDepth) { + return to; + } + if (typeof from.toJSON === 'function' && from[isCalled] !== true) { return toJSON(from); } @@ -69,10 +75,14 @@ const destroyCircular = ({ } if (!seen.includes(from[key])) { + depth++; + to[key] = destroyCircular({ from: from[key], seen: seen.slice(), - forceEnumerable + forceEnumerable, + maxDepth, + depth }); continue; } @@ -94,12 +104,16 @@ const destroyCircular = ({ return to; }; -const serializeError = value => { +const serializeError = (value, options = {}) => { + const {maxDepth = Number.POSITIVE_INFINITY} = options; + if (typeof value === 'object' && value !== null) { return destroyCircular({ from: value, seen: [], - forceEnumerable: true + forceEnumerable: true, + maxDepth, + depth: 0 }); } @@ -112,14 +126,22 @@ const serializeError = value => { return value; }; -const deserializeError = value => { +const deserializeError = (value, options = {}) => { + const {maxDepth = Number.POSITIVE_INFINITY} = options; + if (value instanceof Error) { return value; } if (typeof value === 'object' && value !== null && !Array.isArray(value)) { const newError = new Error(); // eslint-disable-line unicorn/error-message - destroyCircular({from: value, seen: [], to_: newError}); + destroyCircular({ + from: value, + seen: [], + to_: newError, + maxDepth, + depth: 0 + }); return newError; } diff --git a/index.test-d.ts b/index.test-d.ts index ad6ebd1..b9c544e 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -1,10 +1,11 @@ -import {expectType} from 'tsd'; -import {serializeError, deserializeError, ErrorObject} from './index.js'; +import {expectType, expectAssignable} from 'tsd'; +import {serializeError, deserializeError, ErrorObject, Options} from './index.js'; const error = new Error('unicorn'); expectType(serializeError(1)); expectType(serializeError(error)); +expectAssignable({maxDepth: 1}); expectType(deserializeError({ message: 'error message', diff --git a/readme.md b/readme.md index f2c0769..51a847e 100644 --- a/readme.md +++ b/readme.md @@ -33,7 +33,7 @@ console.log(deserialized); ## API -### serializeError(value) +### serializeError(value, options?) Type: `Error | unknown` @@ -76,7 +76,7 @@ console.log(serializeError(error)); // => {date: '1970-01-01T00:00:00.000Z', message: '🦄', name, stack} ``` -### deserializeError(value) +### deserializeError(value, options?) Type: `{[key: string]: unknown} | unknown` @@ -86,3 +86,27 @@ Deserialize a plain object or any value into an `Error` object. Non-error values are wrapped in a `NonError` error. Custom properties are preserved. Circular references are handled. + +### options + +Type: `object` + +#### maxDepth + +Type: `number`\ +Default: `Number.POSITIVE_INFINITY` + +The maximum depth of properties to preserve when serializing/deserializing. + +```js +const {serializeError} = require('serialize-error'); + +const error = new Error('🦄'); +error.one = {two: {three: {}}}; + +console.log(serializeError(error, {maxDepth: 1})); +//=> {name: 'Error', message: '…', one: {}} + +console.log(serializeError(error, {maxDepth: 2})); +//=> {name: 'Error', message: '…', one: { two: {}}} +``` diff --git a/test.js b/test.js index 38cd5a7..c1d8111 100644 --- a/test.js +++ b/test.js @@ -209,6 +209,41 @@ test('deserialized name, stack and message should not be enumerable, other props } }); +test('should deserialize properties up to `Options.maxDepth` levels deep', t => { + const error = new Error('errorMessage'); + const object = { + message: error.message, + name: error.name, + stack: error.stack, + one: { + two: { + three: {} + } + } + }; + + const levelZero = deserializeError(object, {maxDepth: 0}); + const emptyError = new Error('test'); + emptyError.message = ''; + t.is(levelZero instanceof Error, true); + t.deepEqual(levelZero, emptyError); + + const levelOne = deserializeError(object, {maxDepth: 1}); + error.one = {}; + t.is(levelOne instanceof Error, true); + t.deepEqual(levelOne, error); + + const levelTwo = deserializeError(object, {maxDepth: 2}); + error.one = {two: {}}; + t.is(levelTwo instanceof Error, true); + t.deepEqual(levelTwo, error); + + const levelThree = deserializeError(object, {maxDepth: 3}); + error.one = {two: {three: {}}}; + t.is(levelThree instanceof Error, true); + t.deepEqual(levelThree, error); +}); + test('should serialize Date as ISO string', t => { const date = {date: new Date(0)}; const serialized = serializeError(date); @@ -290,3 +325,21 @@ test('should serialize custom error with `.toJSON` defined with `serializeError` }); t.not(stack, undefined); }); + +test('should serialize properties up to `Options.maxDepth` levels deep', t => { + const error = new Error('errorMessage'); + error.one = {two: {three: {}}}; + const {message, name, stack} = error; + + const levelZero = serializeError(error, {maxDepth: 0}); + t.deepEqual(levelZero, {}); + + const levelOne = serializeError(error, {maxDepth: 1}); + t.deepEqual(levelOne, {message, name, stack, one: {}}); + + const levelTwo = serializeError(error, {maxDepth: 2}); + t.deepEqual(levelTwo, {message, name, stack, one: {two: {}}}); + + const levelThree = serializeError(error, {maxDepth: 3}); + t.deepEqual(levelThree, {message, name, stack, one: {two: {three: {}}}}); +});