Skip to content

Commit

Permalink
Merge pull request #337 from 0x0a0d/cylution/fix-fields-unsafe
Browse files Browse the repository at this point in the history
feat: allow filter object fields inside an array
  • Loading branch information
icebob authored Oct 22, 2023
2 parents 906f90a + 3a87421 commit 2702ad1
Show file tree
Hide file tree
Showing 3 changed files with 279 additions and 38 deletions.
13 changes: 6 additions & 7 deletions packages/moleculer-db/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -591,17 +593,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;
},

Expand Down
34 changes: 34 additions & 0 deletions packages/moleculer-db/src/utils.js
Original file line number Diff line number Diff line change
@@ -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;
270 changes: 239 additions & 31 deletions packages/moleculer-db/test/unit/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -761,49 +761,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);
});

it("should filter the fields", () => {
const res = service.filterFields(doc, ["name", "address"]);
expect(res).toEqual({
name: "Walter",
address: doc.address
});
name: "store"
});

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 excludeFields method", () => {
Expand Down

0 comments on commit 2702ad1

Please sign in to comment.