From 16f86b732736d68baf6d5d78c755a7884cc93a3c Mon Sep 17 00:00:00 2001 From: Nicolas Polizzo Date: Tue, 4 Feb 2025 10:46:59 +0100 Subject: [PATCH 1/6] fix: use own object flattening function that supports ObjectIds --- packages/moleculer-db/package-lock.json | 5 -- packages/moleculer-db/package.json | 1 - packages/moleculer-db/src/index.js | 4 +- packages/moleculer-db/src/utils.js | 62 +++++++++++++++++++ packages/moleculer-db/test/unit/utils.spec.js | 46 ++++++++++++++ 5 files changed, 109 insertions(+), 9 deletions(-) create mode 100644 packages/moleculer-db/test/unit/utils.spec.js diff --git a/packages/moleculer-db/package-lock.json b/packages/moleculer-db/package-lock.json index 9517f567..782e1531 100644 --- a/packages/moleculer-db/package-lock.json +++ b/packages/moleculer-db/package-lock.json @@ -2708,11 +2708,6 @@ "path-exists": "^4.0.0" } }, - "flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==" - }, "flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", diff --git a/packages/moleculer-db/package.json b/packages/moleculer-db/package.json index d4ee12f7..c6bc30a2 100644 --- a/packages/moleculer-db/package.json +++ b/packages/moleculer-db/package.json @@ -51,7 +51,6 @@ }, "dependencies": { "@seald-io/nedb": "^3.0.0", - "flat": "^5.0.2", "lodash": "^4.17.21" } } diff --git a/packages/moleculer-db/src/index.js b/packages/moleculer-db/src/index.js index a6d9e826..7d033807 100644 --- a/packages/moleculer-db/src/index.js +++ b/packages/moleculer-db/src/index.js @@ -7,13 +7,11 @@ "use strict"; const _ = require("lodash"); -const { flatten } = require("flat"); const { MoleculerClientError, ValidationError } = require("moleculer").Errors; const { EntityNotFoundError } = require("./errors"); const MemoryAdapter = require("./memory-adapter"); const pkg = require("../package.json"); -const util = require("util"); -const { copyFieldValueByPath } = require("./utils"); +const { copyFieldValueByPath, flatten } = require("./utils"); const stringToPath = require("lodash/_stringToPath"); /** diff --git a/packages/moleculer-db/src/utils.js b/packages/moleculer-db/src/utils.js index 8428ab3d..c9fdb2d4 100644 --- a/packages/moleculer-db/src/utils.js +++ b/packages/moleculer-db/src/utils.js @@ -32,3 +32,65 @@ function copyFieldValueByPath(doc, paths, res, pathIndex = 0, cachePaths = []) { } exports.copyFieldValueByPath = copyFieldValueByPath; + +/** + * Flattens a JavaScript object using dot notation while preserving arrays and non-plain objects + * @param {Object} obj - The object to flatten + * @param {String} [prefix=""] - The prefix to use for nested keys (used internally for recursion) + * @returns {Object} A flattened object with dot notation keys + * + * @example + * const input = { + * name: "John", + * address: { + * street: "Main St", + * location: { + * city: "Boston", + * country: "USA" + * } + * }, + * account: { + * createdAt: new Date("2024-01-01"), + * settings: { + * theme: null, + * language: undefined + * } + * }, + * scores: [85, 90, 95], + * _id: ObjectId("507f1f77bcf86cd799439011") + * }; + * + * // Returns: + * // { + * // "name": "John", + * // "address.street": "Main St", + * // "address.location.city": "Boston", + * // "address.location.country": "USA", + * // "account.createdAt": Date("2024-01-01T00:00:00.000Z"), + * // "account.settings.theme": null, + * // "account.settings.language": undefined, + * // "scores": [85, 90, 95], + * // "_id": ObjectId("507f1f77bcf86cd799439011") + * // } + */ +function flatten(obj, prefix = "") { + return Object.keys(obj).reduce((acc, key) => { + const prefixedKey = prefix ? `${prefix}.${key}` : key; + const value = obj[key]; + + // Check if value is a plain object (not array, not null, and constructor is Object) + const isPlainObject = + typeof value === "object" && + value !== null && + !Array.isArray(value) && + value.constructor === Object; + + // If it's a plain object, flatten it recursively + // Otherwise, keep the value as is (handles primitives, null, undefined, arrays, dates, ObjectId, etc.) + return isPlainObject + ? { ...acc, ...flatten(value, prefixedKey) } + : { ...acc, [prefixedKey]: value }; + }, {}); +} + +exports.flatten = flatten; diff --git a/packages/moleculer-db/test/unit/utils.spec.js b/packages/moleculer-db/test/unit/utils.spec.js new file mode 100644 index 00000000..efdf5d24 --- /dev/null +++ b/packages/moleculer-db/test/unit/utils.spec.js @@ -0,0 +1,46 @@ +"use strict"; + +const { flatten } = require("../../src/utils"); +const { randomUUID } = require("crypto"); + +describe("Test Utils", () => { + describe("flatten", () => { + it("should properly flatten a given object", () => { + const uuid = randomUUID(); + const date = new Date("2024-01-01"); + const obj = { + name: "John", + address: { + street: "Main St", + location: { + city: "Boston", + country: "USA" + } + }, + account: { + createdAt: date, + identifier: uuid, + settings: { + theme: null, + language: undefined + } + }, + scores: [85, 90, 95], + }; + + const expected = { + "name": "John", + "address.street": "Main St", + "address.location.city": "Boston", + "address.location.country": "USA", + "account.createdAt": date, + "account.identifier": uuid, + "account.settings.theme": null, + "account.settings.language": undefined, + "scores": [85, 90, 95], + }; + + expect(flatten(obj)).toEqual(expected); + }); + }); +}); From 07f4b773a4655bf9c13970c61f22ddc1c58c5169 Mon Sep 17 00:00:00 2001 From: Nicolas Polizzo Date: Tue, 4 Feb 2025 11:10:12 +0100 Subject: [PATCH 2/6] remove flatten invalid second argument --- packages/moleculer-db/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/moleculer-db/src/index.js b/packages/moleculer-db/src/index.js index 7d033807..8b6c80cf 100644 --- a/packages/moleculer-db/src/index.js +++ b/packages/moleculer-db/src/index.js @@ -1026,7 +1026,7 @@ module.exports = { }); if (this.settings.useDotNotation) - sets = flatten(sets, { safe: true }); + sets = flatten(sets); return sets; }) From e749fcf3780fb1d0c3794fad20104ffe4d6a358b Mon Sep 17 00:00:00 2001 From: Nicolas Polizzo Date: Tue, 4 Feb 2025 11:15:40 +0100 Subject: [PATCH 3/6] skip test if node version is under 14 --- packages/moleculer-db/test/unit/utils.spec.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/moleculer-db/test/unit/utils.spec.js b/packages/moleculer-db/test/unit/utils.spec.js index efdf5d24..b9cb4e96 100644 --- a/packages/moleculer-db/test/unit/utils.spec.js +++ b/packages/moleculer-db/test/unit/utils.spec.js @@ -1,11 +1,16 @@ "use strict"; const { flatten } = require("../../src/utils"); -const { randomUUID } = require("crypto"); + +if (process.versions.node.split(".")[0] < 14) { + console.log("Skipping utils tests because node version is too low"); + it("Skipping utils tests because node version is too low", () => {}); +} describe("Test Utils", () => { describe("flatten", () => { it("should properly flatten a given object", () => { + const { randomUUID } = require("crypto"); const uuid = randomUUID(); const date = new Date("2024-01-01"); const obj = { From 41d815b1dbf0e039846b8c6cbf2e74e6b50e4a76 Mon Sep 17 00:00:00 2001 From: Nicolas Polizzo Date: Tue, 4 Feb 2025 11:25:08 +0100 Subject: [PATCH 4/6] try another fix for lower node version --- packages/moleculer-db/test/unit/utils.spec.js | 80 ++++++++++--------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/packages/moleculer-db/test/unit/utils.spec.js b/packages/moleculer-db/test/unit/utils.spec.js index b9cb4e96..9e53dd95 100644 --- a/packages/moleculer-db/test/unit/utils.spec.js +++ b/packages/moleculer-db/test/unit/utils.spec.js @@ -5,47 +5,49 @@ const { flatten } = require("../../src/utils"); if (process.versions.node.split(".")[0] < 14) { console.log("Skipping utils tests because node version is too low"); it("Skipping utils tests because node version is too low", () => {}); -} +} else { -describe("Test Utils", () => { - describe("flatten", () => { - it("should properly flatten a given object", () => { - const { randomUUID } = require("crypto"); - const uuid = randomUUID(); - const date = new Date("2024-01-01"); - const obj = { - name: "John", - address: { - street: "Main St", - location: { - city: "Boston", - country: "USA" - } - }, - account: { - createdAt: date, - identifier: uuid, - settings: { - theme: null, - language: undefined - } - }, - scores: [85, 90, 95], - }; + describe("Test Utils", () => { + describe("flatten", () => { + it("should properly flatten a given object", () => { + const {randomUUID} = require("crypto"); + const uuid = randomUUID(); + const date = new Date("2024-01-01"); + const obj = { + name: "John", + address: { + street: "Main St", + location: { + city: "Boston", + country: "USA" + } + }, + account: { + createdAt: date, + identifier: uuid, + settings: { + theme: null, + language: undefined + } + }, + scores: [85, 90, 95], + }; - const expected = { - "name": "John", - "address.street": "Main St", - "address.location.city": "Boston", - "address.location.country": "USA", - "account.createdAt": date, - "account.identifier": uuid, - "account.settings.theme": null, - "account.settings.language": undefined, - "scores": [85, 90, 95], - }; + const expected = { + "name": "John", + "address.street": "Main St", + "address.location.city": "Boston", + "address.location.country": "USA", + "account.createdAt": date, + "account.identifier": uuid, + "account.settings.theme": null, + "account.settings.language": undefined, + "scores": [85, 90, 95], + }; - expect(flatten(obj)).toEqual(expected); + expect(flatten(obj)).toEqual(expected); + }); }); }); -}); + +} From 4eb9a957999d36438fd53ad19bf44578b241f82c Mon Sep 17 00:00:00 2001 From: Nicolas Polizzo Date: Wed, 5 Feb 2025 19:11:27 +0100 Subject: [PATCH 5/6] simplify isPlainObject check --- packages/moleculer-db/src/utils.js | 6 +----- packages/moleculer-db/test/unit/utils.spec.js | 4 ++++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/moleculer-db/src/utils.js b/packages/moleculer-db/src/utils.js index c9fdb2d4..db295fcc 100644 --- a/packages/moleculer-db/src/utils.js +++ b/packages/moleculer-db/src/utils.js @@ -79,11 +79,7 @@ function flatten(obj, prefix = "") { const value = obj[key]; // Check if value is a plain object (not array, not null, and constructor is Object) - const isPlainObject = - typeof value === "object" && - value !== null && - !Array.isArray(value) && - value.constructor === Object; + const isPlainObject = typeof value === "object" && value && value.constructor === Object; // If it's a plain object, flatten it recursively // Otherwise, keep the value as is (handles primitives, null, undefined, arrays, dates, ObjectId, etc.) diff --git a/packages/moleculer-db/test/unit/utils.spec.js b/packages/moleculer-db/test/unit/utils.spec.js index 9e53dd95..61a90212 100644 --- a/packages/moleculer-db/test/unit/utils.spec.js +++ b/packages/moleculer-db/test/unit/utils.spec.js @@ -15,6 +15,7 @@ if (process.versions.node.split(".")[0] < 14) { const date = new Date("2024-01-01"); const obj = { name: "John", + active: true, address: { street: "Main St", location: { @@ -25,6 +26,7 @@ if (process.versions.node.split(".")[0] < 14) { account: { createdAt: date, identifier: uuid, + isSync: false, settings: { theme: null, language: undefined @@ -35,6 +37,7 @@ if (process.versions.node.split(".")[0] < 14) { const expected = { "name": "John", + "active": true, "address.street": "Main St", "address.location.city": "Boston", "address.location.country": "USA", @@ -42,6 +45,7 @@ if (process.versions.node.split(".")[0] < 14) { "account.identifier": uuid, "account.settings.theme": null, "account.settings.language": undefined, + "account.isSync": false, "scores": [85, 90, 95], }; From 2e5a734bbcab4d0d8156e0db175df5f4b819cd3b Mon Sep 17 00:00:00 2001 From: Nicolas Polizzo Date: Wed, 5 Feb 2025 21:48:22 +0100 Subject: [PATCH 6/6] Mimic ObjectId class in test --- packages/moleculer-db/test/unit/utils.spec.js | 105 ++++++++++-------- 1 file changed, 57 insertions(+), 48 deletions(-) diff --git a/packages/moleculer-db/test/unit/utils.spec.js b/packages/moleculer-db/test/unit/utils.spec.js index 61a90212..49a2bd57 100644 --- a/packages/moleculer-db/test/unit/utils.spec.js +++ b/packages/moleculer-db/test/unit/utils.spec.js @@ -2,56 +2,65 @@ const { flatten } = require("../../src/utils"); -if (process.versions.node.split(".")[0] < 14) { - console.log("Skipping utils tests because node version is too low"); - it("Skipping utils tests because node version is too low", () => {}); -} else { +class ObjectId { + constructor() { + this.timestamp = Math.floor(Date.now() / 1000).toString(16).padStart(8, "0"); + this.machineId = Math.floor(Math.random() * 16777216).toString(16).padStart(6, "0"); + this.processId = Math.floor(Math.random() * 65536).toString(16).padStart(4, "0"); + this.counter = Math.floor(Math.random() * 16777216).toString(16).padStart(6, "0"); + } - describe("Test Utils", () => { - describe("flatten", () => { - it("should properly flatten a given object", () => { - const {randomUUID} = require("crypto"); - const uuid = randomUUID(); - const date = new Date("2024-01-01"); - const obj = { - name: "John", - active: true, - address: { - street: "Main St", - location: { - city: "Boston", - country: "USA" - } - }, - account: { - createdAt: date, - identifier: uuid, - isSync: false, - settings: { - theme: null, - language: undefined - } - }, - scores: [85, 90, 95], - }; + toString() { + return this.timestamp + this.machineId + this.processId + this.counter; + } - const expected = { - "name": "John", - "active": true, - "address.street": "Main St", - "address.location.city": "Boston", - "address.location.country": "USA", - "account.createdAt": date, - "account.identifier": uuid, - "account.settings.theme": null, - "account.settings.language": undefined, - "account.isSync": false, - "scores": [85, 90, 95], - }; + getTimestamp() { + return new Date(parseInt(this.timestamp, 16) * 1000); + } +} + +describe("Test Utils", () => { + describe("flatten", () => { + it("should properly flatten a given object", () => { + const oid = new ObjectId(); + const date = new Date("2024-01-01"); + const obj = { + name: "John", + active: true, + address: { + street: "Main St", + location: { + city: "Boston", + country: "USA" + } + }, + account: { + createdAt: date, + identifier: oid, + isSync: false, + settings: { + theme: null, + language: undefined + } + }, + scores: [85, 90, 95], + }; + + const expected = { + "name": "John", + "active": true, + "address.street": "Main St", + "address.location.city": "Boston", + "address.location.country": "USA", + "account.createdAt": date, + "account.identifier": oid, + "account.settings.theme": null, + "account.settings.language": undefined, + "account.isSync": false, + "scores": [85, 90, 95], + }; - expect(flatten(obj)).toEqual(expected); - }); + expect(flatten(obj)).toEqual(expected); }); }); - -} +});