diff --git a/index.js b/index.js index 42e1ddc..503b7b5 100644 --- a/index.js +++ b/index.js @@ -1,157 +1,168 @@ -function isBuffer (obj) { - return obj && +function isBuffer(obj) { + return ( + obj && obj.constructor && - (typeof obj.constructor.isBuffer === 'function') && + typeof obj.constructor.isBuffer === "function" && obj.constructor.isBuffer(obj) + ); } -function keyIdentity (key) { - return key +function keyIdentity(key) { + return key; } -export function flatten (target, opts) { - opts = opts || {} +export function flatten(target, opts) { + opts = opts || {}; - const delimiter = opts.delimiter || '.' - const maxDepth = opts.maxDepth - const transformKey = opts.transformKey || keyIdentity - const output = {} + if (target === null || target === undefined) { + return target; + } + + const delimiter = opts.delimiter || "."; + const maxDepth = opts.maxDepth; + const transformKey = opts.transformKey || keyIdentity; + const output = {}; + + const seenObjects = new WeakSet(); + + function step(object, prev, currentDepth) { + currentDepth = currentDepth || 1; + + if (seenObjects.has(object)) { + throw new Error("Circular reference detected"); + } + + seenObjects.add(object); - function step (object, prev, currentDepth) { - currentDepth = currentDepth || 1 Object.keys(object).forEach(function (key) { - const value = object[key] - const isarray = opts.safe && Array.isArray(value) - const type = Object.prototype.toString.call(value) - const isbuffer = isBuffer(value) - const isobject = ( - type === '[object Object]' || - type === '[object Array]' - ) + const value = object[key]; + const isarray = opts.safe && Array.isArray(value); + const type = Object.prototype.toString.call(value); + const isbuffer = isBuffer(value); + const isobject = type === "[object Object]" || type === "[object Array]"; const newKey = prev ? prev + delimiter + transformKey(key) - : transformKey(key) - - if (!isarray && !isbuffer && isobject && Object.keys(value).length && - (!opts.maxDepth || currentDepth < maxDepth)) { - return step(value, newKey, currentDepth + 1) + : transformKey(key); + + if ( + !isarray && + !isbuffer && + isobject && + Object.keys(value).length && + (!opts.maxDepth || currentDepth < maxDepth) + ) { + return step(value, newKey, currentDepth + 1); } - output[newKey] = value - }) + output[newKey] = value; + }); + + const symbols = Object.getOwnPropertySymbols(object); + symbols.forEach((symbol) => { + const value = object[symbol]; + const newKey = prev ? prev + delimiter + String(symbol) : String(symbol); + output[newKey] = value; + }); } - step(target) + step(target); - return output + return output; } -export function unflatten (target, opts) { - opts = opts || {} +export function unflatten(target, opts) { + opts = opts || {}; - const delimiter = opts.delimiter || '.' - const overwrite = opts.overwrite || false - const transformKey = opts.transformKey || keyIdentity - const result = {} + const delimiter = opts.delimiter || "."; + const overwrite = opts.overwrite || false; + const transformKey = opts.transformKey || keyIdentity; + const result = {}; - const isbuffer = isBuffer(target) - if (isbuffer || Object.prototype.toString.call(target) !== '[object Object]') { - return target + const isbuffer = isBuffer(target); + if ( + isbuffer || + Object.prototype.toString.call(target) !== "[object Object]" + ) { + return target; } // safely ensure that the key is // an integer. - function getkey (key) { - const parsedKey = Number(key) - - return ( - isNaN(parsedKey) || - key.indexOf('.') !== -1 || - opts.object - ) + function getkey(key) { + const parsedKey = Number(key); + + return isNaN(parsedKey) || key.indexOf(".") !== -1 || opts.object ? key - : parsedKey + : parsedKey; } - function addKeys (keyPrefix, recipient, target) { + function addKeys(keyPrefix, recipient, target) { return Object.keys(target).reduce(function (result, key) { - result[keyPrefix + delimiter + key] = target[key] + result[keyPrefix + delimiter + key] = target[key]; - return result - }, recipient) + return result; + }, recipient); } - function isEmpty (val) { - const type = Object.prototype.toString.call(val) - const isArray = type === '[object Array]' - const isObject = type === '[object Object]' + function isEmpty(val) { + const type = Object.prototype.toString.call(val); + const isArray = type === "[object Array]"; + const isObject = type === "[object Object]"; if (!val) { - return true + return true; } else if (isArray) { - return !val.length + return !val.length; } else if (isObject) { - return !Object.keys(val).length + return !Object.keys(val).length; } } target = Object.keys(target).reduce(function (result, key) { - const type = Object.prototype.toString.call(target[key]) - const isObject = (type === '[object Object]' || type === '[object Array]') + const type = Object.prototype.toString.call(target[key]); + const isObject = type === "[object Object]" || type === "[object Array]"; if (!isObject || isEmpty(target[key])) { - result[key] = target[key] - return result + result[key] = target[key]; + return result; } else { - return addKeys( - key, - result, - flatten(target[key], opts) - ) + return addKeys(key, result, flatten(target[key], opts)); } - }, {}) + }, {}); Object.keys(target).forEach(function (key) { - const split = key.split(delimiter).map(transformKey) - let key1 = getkey(split.shift()) - let key2 = getkey(split[0]) - let recipient = result + const split = key.split(delimiter).map(transformKey); + let key1 = getkey(split.shift()); + let key2 = getkey(split[0]); + let recipient = result; while (key2 !== undefined) { - if (key1 === '__proto__') { - return + if (key1 === "__proto__") { + return; } - const type = Object.prototype.toString.call(recipient[key1]) - const isobject = ( - type === '[object Object]' || - type === '[object Array]' - ) + const type = Object.prototype.toString.call(recipient[key1]); + const isobject = type === "[object Object]" || type === "[object Array]"; // do not write over falsey, non-undefined values if overwrite is false - if (!overwrite && !isobject && typeof recipient[key1] !== 'undefined') { - return + if (!overwrite && !isobject && typeof recipient[key1] !== "undefined") { + return; } if ((overwrite && !isobject) || (!overwrite && recipient[key1] == null)) { - recipient[key1] = ( - typeof key2 === 'number' && - !opts.object - ? [] - : {} - ) + recipient[key1] = typeof key2 === "number" && !opts.object ? [] : {}; } - recipient = recipient[key1] + recipient = recipient[key1]; if (split.length > 0) { - key1 = getkey(split.shift()) - key2 = getkey(split[0]) + key1 = getkey(split.shift()); + key2 = getkey(split[0]); } } // unflatten again for 'messy objects' - recipient[key1] = unflatten(target[key], opts) - }) + recipient[key1] = unflatten(target[key], opts); + }); - return result + return result; } diff --git a/test/unhandledExceptionsTest.js b/test/unhandledExceptionsTest.js new file mode 100644 index 0000000..cc2cfb8 --- /dev/null +++ b/test/unhandledExceptionsTest.js @@ -0,0 +1,28 @@ +import assert from "node:assert"; +import { describe, test } from "node:test"; +import { flatten, unflatten } from "../index.js"; + +describe("flatten obj tests", function () { + test("Flatten null", function () { + assert.deepStrictEqual(flatten(null), null); + }); + + test("Flatten undefined", function () { + assert.deepStrictEqual(flatten(undefined), undefined); + }); + + test("Flatten object with Symbol keys", function () { + const sym = Symbol("test"); + const obj = { [sym]: "value", regularKey: "regularValue" }; + const flatObj = flatten(obj); + + assert.strictEqual(flatObj["Symbol(test)"], "value"); // Assuming stringification of symbol keys + }); + + test("Flatten circular reference", function () { + const obj = { name: "John" }; + obj.self = obj; // Circular reference + + assert.throws(() => flatten(obj), /Circular reference detected/); + }); +});