diff --git a/packages/moleculer-db/src/index.js b/packages/moleculer-db/src/index.js index a9615a1c..6a5bf1b0 100644 --- a/packages/moleculer-db/src/index.js +++ b/packages/moleculer-db/src/index.js @@ -13,6 +13,8 @@ const { MoleculerClientError, ValidationError } = require("moleculer").Errors; const { EntityNotFoundError } = require("./errors"); const MemoryAdapter = require("./memory-adapter"); const pkg = require("../package.json"); +const { copyFieldValueByPath } = require("./utils"); +const stringToPath = require("lodash/_stringToPath"); /** * Service mixin to access database entities @@ -549,17 +551,14 @@ module.exports = { * @returns {Object} */ filterFields(doc, fields) { - // Apply field filter (support nested paths) if (Array.isArray(fields)) { - let res = {}; - fields.forEach(n => { - const v = _.get(doc, n); - if (v !== undefined) - _.set(res, n, v); + const res = {}; + fields.forEach(field => { + const paths = stringToPath(field); + copyFieldValueByPath(doc, paths, res); }); return res; } - return doc; }, diff --git a/packages/moleculer-db/src/utils.js b/packages/moleculer-db/src/utils.js new file mode 100644 index 00000000..8428ab3d --- /dev/null +++ b/packages/moleculer-db/src/utils.js @@ -0,0 +1,34 @@ +function copyFieldValueByPath(doc, paths, res, pathIndex = 0, cachePaths = []) { + if (pathIndex < paths.length) { + const path = paths[pathIndex]; + if (Array.isArray(doc)) { + cachePaths[cachePaths.length - 1].type = "array"; + if (path === "$") { + doc.forEach((item, itemIndex) => { + copyFieldValueByPath(item, paths, res, pathIndex + 1, cachePaths.concat({path: itemIndex})); + }); + } else if (Object.prototype.hasOwnProperty.call(doc, path)) { + copyFieldValueByPath(doc[path], paths, res, pathIndex + 1, cachePaths.concat({path: path})); + } + } else if (doc != null && Object.prototype.hasOwnProperty.call(doc, path)) { + cachePaths.push({ path }); + copyFieldValueByPath(doc[path], paths, res, pathIndex + 1, cachePaths); + } + } else { + let obj = res; + for (let i = 0; i < cachePaths.length - 1; i++) { + const cachePath = cachePaths[i]; + if (!Object.prototype.hasOwnProperty.call(obj, cachePath.path)) { + if (cachePath.type === "array") { + obj[cachePath.path] = []; + } else { + obj[cachePath.path] = {}; + } + } + obj = obj[cachePath.path]; + } + obj[cachePaths[cachePaths.length - 1].path] = doc; + } +} + +exports.copyFieldValueByPath = copyFieldValueByPath; diff --git a/packages/moleculer-db/test/unit/index.spec.js b/packages/moleculer-db/test/unit/index.spec.js index 1ed71106..db831149 100644 --- a/packages/moleculer-db/test/unit/index.spec.js +++ b/packages/moleculer-db/test/unit/index.spec.js @@ -684,49 +684,257 @@ describe("Test authorizeFields method", () => { }); describe("Test filterFields method", () => { - const doc = { - id : 1, - name: "Walter", - address: { - city: "Albuquerque", - state: "NM", - zip: 87111 - } - }; - const broker = new ServiceBroker({ logger: false, validation: false }); const service = broker.createService(DbService, { - name: "store", - adapter: mockAdapter, - settings: { - fields: "id name address" - } - }); - - it("should not touch the doc", () => { - const res = service.filterFields(doc); - expect(res).toBe(doc); + name: "store" }); - it("should filter the fields", () => { - const res = service.filterFields(doc, ["name", "address"]); - expect(res).toEqual({ - name: "Walter", - address: doc.address - }); - }); - - it("should filter with nested fields", () => { - const res = service.filterFields(doc, ["name", "address.city", "address.zip"]); - expect(res).toEqual({ + describe("Object test", () => { + const doc = { + id : 1, name: "Walter", address: { city: "Albuquerque", + state: "NM", zip: 87111 } + }; + + it("should not touch the doc", () => { + const res = service.filterFields(doc); + expect(res).toBe(doc); + }); + + it("should filter the fields", () => { + const res = service.filterFields(doc, ["name", "address"]); + expect(res).toEqual({ + name: "Walter", + address: doc.address + }); + }); + + it("should filter with nested fields", () => { + const res = service.filterFields(doc, ["name", "address.city", "address.zip"]); + expect(res).toEqual({ + name: "Walter", + address: { + city: "Albuquerque", + zip: 87111 + } + }); }); }); + describe("Array test", () => { + describe("common case", () => { + const doc = { + id : 1, + name: "Walter", + cars: [ + {id: 1, name: "BMW", model: "320i", wheels: [ + { placement: "front-left", id: 1}, + { placement: "front-right", id: 2}, + { placement: "behind-left", id: 3}, + { placement: "behind-right", id: 4}, + ]}, + {id: 2, name: "BMW", model: "520i", wheels: [ + { placement: "front-left", id: 1}, + { placement: "front-right", id: 2}, + { placement: "behind-left", id: 3}, + { placement: "behind-right", id: 4}, + ]}, + {id: 3, name: "AUDI", model: "Q7", wheels: [ + { placement: "front-left", id: 1, histories: []}, + { placement: "front-right", id: 2, histories: [ + {date: "11/11/2011", message: "replace new 2011"} + ]}, + { placement: "behind-left", id: 3, histories: []}, + { placement: "behind-right", id: 4, histories: [ + {date: "12/12/2012", message: "replace new 2012"} + ]}, + ]}, + ], + models: { + id: 1, + desc: "not an array", + items: [ + {id: 0, desc: "0 desc", name: "0 name"}, + {id: 1, desc: "1 desc", name: "1 name"}, + {id: 2, desc: "2 desc", name: "2 name"}, + ] + } + }; + it("should pass", () => { + const res = service.filterFields(doc, ["name", "cars.$.id", "cars.$.name", "cars.$.wheels.$.placement", "cars.$.wheels.$.histories.$.date", "cars.$.wheels.$.histories.$.non-existed"]); + expect(res).toEqual({ + name: "Walter", + cars: [ + {id: 1, name: "BMW", wheels: [ + { placement: "front-left" }, + { placement: "front-right" }, + { placement: "behind-left" }, + { placement: "behind-right" }, + ]}, + {id: 2, name: "BMW", wheels: [ + { placement: "front-left" }, + { placement: "front-right" }, + { placement: "behind-left" }, + { placement: "behind-right" }, + ]}, + {id: 3, name: "AUDI", wheels: [ + { placement: "front-left" }, + { placement: "front-right", histories: [{date: "11/11/2011"}] }, + { placement: "behind-left" }, + { placement: "behind-right", histories: [{date: "12/12/2012"}] }, + ]}, + ] + }); + }); + + it("test .$. with object", () => { + const res = service.filterFields(doc, ["models.$.desc", "name"]); + expect(res).toEqual({ + name: "Walter", + }); + }); + + describe("doc.models test", function () { + it("test .$", () => { + const res = service.filterFields(doc, ["name", "models.items.$.id"]); + expect(res).toEqual({ + name: "Walter", + models: { + items: [ + {id: 0}, + {id: 1}, + {id: 2}, + ] + }, + }); + }); + it("test .0", () => { + const res = service.filterFields(doc, ["name", "models.items.0.id"]); + expect(res).toEqual({ + name: "Walter", + models: { + items: [ + {id: 0}, + ] + }, + }); + }); + it("test .1", () => { + const res = service.filterFields(doc, ["name", "models.items.1.id"]); + expect(res).toEqual({ + name: "Walter", + models: { + items: [ + undefined, + {id: 1}, + ] + }, + }); + }); + it("test multiple array indexes", () => { + const res = service.filterFields(doc, ["name", "models.items.0.id", "models.items.1.desc", "models.items.2.name", "models.items.3.invalid"]); + expect(res).toEqual({ + name: "Walter", + models: { + items: [ + { id:0 }, + { desc: "1 desc" }, + { name: "2 name"}, + ] + }, + }); + }); + }); + }); + + describe("array-index vs object key", function () { + const doc = { + a: { + 2: { + b: [ + { + c: [ + { d: 3, e: 4 }, + { d: 5, e: 6 }, + ] + } + ] + } + } + }; + it("object key", () => { + const res = service.filterFields(doc, ["a.2.b.$.c.$.e"]); + expect(res).toEqual({a: { 2: { b: [{c: [{e: 4}, {e: 6}]}] } }}); + }); + it("asked array but got object key", () => { + const res = service.filterFields(doc, ["a.$.b.$.c.2.e"]); + expect(res).toEqual({}); + }); + it("array index", () => { + const res = service.filterFields(doc, ["a.2.b.$.c.1.e"]); + expect(res).toEqual({a: { 2: { b: [{c: [undefined, {e: 6}]}] } }}); + }); + describe("multiple fields", function () { + it("should overwrite", () => { + const res = service.filterFields(doc, ["a.2.b", "a.2.b.$.c.1.e"]); + expect(res).toEqual({a: { 2: { b: doc.a["2"].b } }}); + }); + it("should merge", () => { + const res = service.filterFields(doc, ["a.2.b.$.c.1.d", "a.2.b.$.c.1.e"]); + expect(res).toEqual({a: { 2: { b: [{c: [undefined, {d: 5, e: 6}]}] } }}); + }); + it("should pass", () => { + const res = service.filterFields(doc, ["a.2.b.$.c.$.d", "a.2.b.$.c.1.e"]); + expect(res).toEqual({a: { 2: { b: [{c: [{d: 3}, {d: 5, e: 6}]}] } }}); + }); + }); + }); + describe("Object with key '$'", function () { + const doc = { + $: [ + { + a: { + $: { + b: [ + {c: 1, d: 2}, + {c: 3, d: 4}, + ] + } + } + }, + { + a: { + $: { + b: [ + {c: 5, d: 6}, + {c: 7, d: 8}, + ] + } + } + }, + ] + }; + it("should handleable $", () => { + const res = service.filterFields(doc, ["$.$.a.$.b.$.c"]); + expect(res).toEqual({ + $: [ + {a: { $: { b: [ + { c: 1 }, + { c: 3 }, + ]}}}, + {a: { $: { b: [ + { c: 5 }, + { c: 7 }, + ]}}} + ] + }); + }); + }); + }); }); describe("Test populateDocs method", () => {