diff --git a/package-lock.json b/package-lock.json index a37a90b..072b314 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6154,10 +6154,9 @@ } }, "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", - "dev": true + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash.isequal": { "version": "4.5.0", diff --git a/package.json b/package.json index 556ffd3..7d1465e 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "webpack-cli": "^4.5.0" }, "dependencies": { - "babel-runtime": "^6.26.0" + "babel-runtime": "^6.26.0", + "lodash": "^4.17.21" }, "browserslist": "> 0.25%, not dead" } diff --git a/src/Collection.js b/src/Collection.js index 7ef21e8..e33f315 100644 --- a/src/Collection.js +++ b/src/Collection.js @@ -1,9 +1,78 @@ +import get from 'lodash/get'; + const every = (array, predicate) => array.reduce((acc, value) => acc && predicate(value), true); const some = (array, predicate) => array.reduce((acc, value) => acc || predicate(value), false); +const getArrayOfObjectsPaths = (keyParts, item) => + keyParts.reduce((acc, key, index) => { + // If we already found an array, we don't need to explore further + // For example with path `tags.name` when tags is an array of objects + if (acc != undefined) { + return acc; + } + + let keyToArray = keyParts.slice(0, index + 1).join('.'); + let keyToItem = keyParts.slice(index + 1).join('.') + let itemValue = get(item, keyToArray); + + // If the array is at the end of the key path, we will process it like we do normally with arrays + // For example with path `deep.tags` where tags is the array. In this case, we return undefined + return Array.isArray(itemValue) && index < keyParts.length - 1 + ? [keyToArray, keyToItem] + : undefined; + }, undefined); + +const getSimpleFilter = (key, value) => { + if (key.indexOf('_lte') !== -1) { + // less than or equal + let realKey = key.replace(/(_lte)$/, ''); + return item => get(item, realKey) <= value; + } + if (key.indexOf('_gte') !== -1) { + // less than or equal + let realKey = key.replace(/(_gte)$/, ''); + return item => get(item, realKey) >= value; + } + if (key.indexOf('_lt') !== -1) { + // less than or equal + let realKey = key.replace(/(_lt)$/, ''); + return item => get(item, realKey) < value; + } + if (key.indexOf('_gt') !== -1) { + // less than or equal + let realKey = key.replace(/(_gt)$/, ''); + return item => get(item, realKey) > value; + } + if (Array.isArray(value)) { + return item => { + if (Array.isArray(get(item, key))) { + // array filter and array item value: where all items in values + return every( + value, + v => some(get(item, key), itemValue => itemValue == v) + ); + } + // where item in values + return value.filter(v => v == get(item, key)).length > 0; + } + } + return item => { + if (Array.isArray(get(item, key)) && typeof value == 'string') { + // simple filter but array item value: where value in item + return get(item, key).indexOf(value) !== -1; + } + if (typeof get(item, key) == 'boolean' && typeof value == 'string') { + // simple filter but boolean item value: boolean where + return get(item, key) == (value === 'true' ? true : false); + } + // simple filter + return get(item, key) == value; + }; +} + function filterItems(items, filter) { if (typeof filter === 'function') { return items.filter(filter); @@ -21,52 +90,25 @@ function filterItems(items, filter) { return false; }; } + + let keyParts = key.split('.'); let value = filter[key]; - if (key.indexOf('_lte') !== -1) { - // less than or equal - let realKey = key.replace(/(_lte)$/, ''); - return item => item[realKey] <= value; - } - if (key.indexOf('_gte') !== -1) { - // less than or equal - let realKey = key.replace(/(_gte)$/, ''); - return item => item[realKey] >= value; - } - if (key.indexOf('_lt') !== -1) { - // less than or equal - let realKey = key.replace(/(_lt)$/, ''); - return item => item[realKey] < value; - } - if (key.indexOf('_gt') !== -1) { - // less than or equal - let realKey = key.replace(/(_gt)$/, ''); - return item => item[realKey] > value; - } - if (Array.isArray(value)) { + if (keyParts.length > 1) { return item => { - if (Array.isArray(item[key])) { - // array filter and array item value: where all items in values - return every( - value, - v => some(item[key], itemValue => itemValue == v) - ); + let arrayOfObjectsPaths = getArrayOfObjectsPaths(keyParts, item); + + if (arrayOfObjectsPaths) { + let [arrayPath, valuePath] = arrayOfObjectsPaths; + // Check wether any item in the array matches the filter + const filteredArrayItems = filterItems(get(item, arrayPath), {[valuePath]: value}); + return filteredArrayItems.length > 0; + } else { + return getSimpleFilter(key, value)(item); } - // where item in values - return value.filter(v => v == item[key]).length > 0; } } - return item => { - if (Array.isArray(item[key]) && typeof value == 'string') { - // simple filter but array item value: where value in item - return item[key].indexOf(value) !== -1; - } - if (typeof item[key] == 'boolean' && typeof value == 'string') { - // simple filter but boolean item value: boolean where - return item[key] == (value === 'true' ? true : false); - } - // simple filter - return item[key] == value; - }; + + return getSimpleFilter(key, value); }); // only the items matching all filters functions are in (AND logic) return items.filter(item => diff --git a/src/Collection.spec.js b/src/Collection.spec.js index 4a97ad9..b48457d 100644 --- a/src/Collection.spec.js +++ b/src/Collection.spec.js @@ -178,6 +178,16 @@ describe("Collection", () => { expect(collection.getAll({ filter: { name: "b" } })).toEqual(expected); }); + it("should filter values with deep paths", () => { + const collection = new Collection([ + { name: "c", deep: { value: "c" } }, + { name: "a", deep: { value: "a" } }, + { name: "b", deep: { value: "b" } }, + ]); + const expected = [{ name: "b", deep: { value: "b" }, id: 2 }]; + expect(collection.getAll({ filter: { "deep.value": "b" } })).toEqual(expected); + }); + it("should filter boolean values properly", () => { const collection = new Collection([ { name: "a", is: true }, @@ -217,6 +227,81 @@ describe("Collection", () => { expect(collection.getAll({ filter: { tags: "f" } })).toEqual([]); }); + it("should filter array values properly within deep paths", () => { + const collection = new Collection([ + { deep: { tags: ["a", "b", "c"] } }, + { deep: { tags: ["b", "c", "d"] } }, + { deep: { tags: ["c", "d", "e"] } }, + ]); + const expected = [ + { id: 0, deep: { tags: ["a", "b", "c"] } }, + { id: 1, deep: { tags: ["b", "c", "d"] } }, + ]; + expect(collection.getAll({ filter: { 'deep.tags': "b" } })).toEqual(expected); + expect(collection.getAll({ filter: { 'deep.tags': "f" } })).toEqual([]); + }); + + it("should filter array values properly inside deep paths", () => { + const collection = new Collection([ + { tags: { deep: ["a", "b", "c"] } }, + { tags: { deep: ["b", "c", "d"] } }, + { tags: { deep: ["c", "d", "e"] } }, + ]); + const expected = [ + { id: 0, tags: { deep: ["a", "b", "c"] } }, + { id: 1, tags: { deep: ["b", "c", "d"] } }, + ]; + expect(collection.getAll({ filter: { 'tags.deep': "b" } })).toEqual(expected); + expect(collection.getAll({ filter: { 'tags.deep': "f" } })).toEqual([]); + }); + + it("should filter array values properly with deep paths", () => { + const collection = new Collection([ + { tags: [{ name: "a" }, { name: "b" }, { name: "c" }] }, + { tags: [{ name: "b" }, { name: "c" }, { name: "d" }] }, + { tags: [{ name: "c" }, { name: "d" }, { name: "e" }] }, + ]); + const expected = [ + { id: 0, tags: [{ name: "a" }, { name: "b" }, { name: "c" }] }, + { id: 1, tags: [{ name: "b" }, { name: "c" }, { name: "d" }] }, + ]; + expect(collection.getAll({ filter: { 'tags.name': "b" } })).toEqual(expected); + expect(collection.getAll({ filter: { 'tags.name': "f" } })).toEqual([]); + }); + + it("should filter array values properly when receiving several values within deep paths", () => { + const collection = new Collection([ + { deep: { tags: ["a", "b", "c"] } }, + { deep: { tags: ["b", "c", "d"] } }, + { deep: { tags: ["c", "d", "e"] } }, + ]); + const expected = [{ id: 1, deep: { tags: ["b", "c", "d"] } }]; + expect(collection.getAll({ filter: { 'deep.tags': ["b", "d"] } })).toEqual( + expected + ); + expect( + collection.getAll({ filter: { 'deep.tags': ["a", "b", "e"] } }) + ).toEqual([]); + }); + + it("should filter array values properly when receiving several values with deep paths", () => { + const collection = new Collection([ + { tags: [{ name: "a" }, { name: "b" }, { name: "c" }] }, + { tags: [{ name: "c" }, { name: "d" }, { name: "e" }] }, + { tags: [{ name: "e" }, { name: "f" }, { name: "g" }] }, + ]); + const expected = [ + { id: 0, tags: [{ name: "a" }, { name: "b" }, { name: "c" }] }, + { id: 1, tags: [{ name: "c" }, { name: "d" }, { name: "e" }] } + ]; + expect(collection.getAll({ filter: { 'tags.name': ["c"] } })).toEqual( + expected + ); + expect( + collection.getAll({ filter: { 'tags.name': ["h", "i"] } }) + ).toEqual([]); + }); + it("should filter array values properly when receiving several values", () => { const collection = new Collection([ { tags: ["a", "b", "c"] },