diff --git a/README.md b/README.md index 46e0a25..d818305 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,9 @@ sentence:word # Search for 'word' in the object properties starting with 's' and any nested property, e.g., [{s: {a: 'word'}}] s*:word +# Search for 'word' in array e.g. [{ s: ['word','number'] }] +s:word + # Search for 'word' in the object by nested key 's.a' s.a:word diff --git a/src/tests/filter/filter.test.ts b/src/tests/filter/filter.test.ts index 8c9868d..098784d 100644 --- a/src/tests/filter/filter.test.ts +++ b/src/tests/filter/filter.test.ts @@ -122,4 +122,64 @@ describe(`filter`, () => { expect(result).toEqual(data.filter((d) => d._id == 13)); }); }); + + describe('filter array fields', () => { + const data = [ + { id: 1, tags: ['javascript', 'typescript'] }, + { id: 2, tags: ['python', 'typescript'] }, + { id: 3, tags: ['java'] }, + { id: 4, tags: [] }, + { id: 5 }, + ]; + + it('should filter by array value', () => { + const result = filter(new QueryParser('tags:typescript'), data); + expect(result).toEqual([ + { id: 1, tags: ['javascript', 'typescript'] }, + { id: 2, tags: ['python', 'typescript'] }, + ]); + }); + + it('should handle nested array fields', () => { + const nestedData = [ + { + id: 1, + nested: { + tags: ['javascript', 'typescript'], + }, + }, + { + id: 2, + nested: { + tags: ['python'], + }, + }, + { + id: 3, + nested: { + other: ['something'], + }, + }, + ]; + + const queries = [ + 'nested.tags:typescript', // explicit field path + '*.tags:typescript', // wildcard prefix + 'nested.tags*:typescript', // wildcard suffix + '*.tags*:typescript', // wildcard on both sides + ]; + + for (const query of queries) { + const result = filter(new QueryParser(query), nestedData); + expect(result).toEqual([ + { + id: 1, + nested: { + tags: ['javascript', 'typescript'], + }, + }, + ]); + } + }); + }); }); diff --git a/src/tests/utils/iterate.test.ts b/src/tests/utils/iterate.test.ts index c7a7fac..4a63f36 100644 --- a/src/tests/utils/iterate.test.ts +++ b/src/tests/utils/iterate.test.ts @@ -372,6 +372,25 @@ describe('iterate', () => { `); }); + it('should handle object with plain array', () => { + const someObj = { + tags: ['tagA', 'tagB'], + }; + const result = [...iterate(someObj, 'tags')]; + expect(result).toMatchInlineSnapshot(` + [ + [ + "tags.0", + "tagA", + ], + [ + "tags.1", + "tagB", + ], + ] + `); + }); + describe('iterate private fields feature', () => { const objWithPrivateFields = { private: { diff --git a/src/utils/iterate.ts b/src/utils/iterate.ts index 0b76bcf..cd7c25a 100644 --- a/src/utils/iterate.ts +++ b/src/utils/iterate.ts @@ -41,6 +41,14 @@ export default function* iterate( // Check if the object is iterable and not in the NOT_ITERABLE list if (typeof obj === 'object' && obj !== null && !NOT_ITERABLE.some((cls) => obj instanceof cls)) { + // Handle plain arrays directly if we're at the target field and not using a wildcard + if (Array.isArray(obj) && currentPath.length === splittedFields.length && !isTrailingWildcard && field) { + for (const [i, element] of obj.entries()) { + yield [[...currentPath, i].join('.'), element]; + } + return; + } + // Check if the object is an array with elements having the current field as a key const arrayWithInnerKey = Array.isArray(obj) && obj.some((o) => objectHasField(o, currentField, checkPrivate)); const objWithCurrentField = objectHasField(obj, currentField, checkPrivate);