feat(query): simple object predicate for scalar properties

This commit is contained in:
Ajai Shankar 2023-02-26 12:56:11 -06:00
parent b87cc7ccae
commit ee4509f037
3 changed files with 34 additions and 11 deletions

View File

@ -18,6 +18,10 @@ Array filtering [?] with corresponding filter function
```js
get(data, '..items[?].amount', i => i.amount > 20)
```
Array filtering [?] with simple object predicate, same as (i => i.id === 2 && i.amount === 20)
```js
get(data, '..items[?]', { id: 2, amount: 20 })
```
Array mapping [?] with corresponding mapper function
```js
get(data, '..items[?].amount', i => i.amount + 10)

View File

@ -19,11 +19,11 @@ function normalize(value: any) {
/**
* Gets value of a prop from source.
*
* If source is an array get value for each item.
* If source is an array get value from each item.
*
* If deep is true then recursively gets values for prop in nested objects.
*
* Once a value if found will not recurese further into that value.
* Once a value is found will not recurse further into that value.
*/
function getValue(source: any, prop: string, deep = false): any {
if (typeof source !== 'object') return;
@ -47,14 +47,27 @@ function getValue(source: any, prop: string, deep = false): any {
return normalize(value);
}
type PredicateOrMapper = (obj: any) => any;
type PredicateOrMapper = ((obj: any) => any) | Record<string, any>;
/**
* Make a predicate function that checks scalar properties for equality
*/
function objectPredicate(obj: Record<string, any>) {
return (item: any) => {
for (const [key, value] of Object.entries(obj)) {
if (item[key] !== value) return false;
}
return true;
};
}
/**
* Apply filter on source array or object
*
* If the filter returns a non boolean non null value it is treated as a mapped value
*/
function filterOrMap(source: any, fun: PredicateOrMapper) {
function filterOrMap(source: any, funOrObj: PredicateOrMapper) {
const fun = typeof funOrObj === 'object' ? objectPredicate(funOrObj) : funOrObj;
const isArray = Array.isArray(source);
const list = isArray ? source : [source];
const result = [] as any[];
@ -67,7 +80,7 @@ function filterOrMap(source: any, fun: PredicateOrMapper) {
result.push(value); // mapper
}
}
return isArray ? result : result[0];
return normalize(isArray ? result : result[0]);
}
/**
@ -89,7 +102,11 @@ function filterOrMap(source: any, fun: PredicateOrMapper) {
* ```js
* get(data, '..items[?].amount', i => i.amount > 20)
* ```
* 5. Array mapping [?] with corresponding mapper function
* 5. Array filtering [?] with simple object predicate, same as (i => i.id === 2 && i.amount === 20)
* ```js
* get(data, '..items[?]', { id: 2, amount: 20 })
* ```
* 6. Array mapping [?] with corresponding mapper function
* ```js
* get(data, '..items[?].amount', i => i.amount + 10)
* ```
@ -121,7 +138,7 @@ export function get(source: any, path: string, ...fns: PredicateOrMapper[]) {
source = filterOrMap(source, fun);
break;
case typeof token === 'number':
source = source[token];
source = normalize(source[token]);
break;
default:
source = getValue(source, token as string, lookbehind === "..");

View File

@ -22,7 +22,7 @@ const data = {
{ id: 4, amount: 40 }
]
}
]
],
},
};
@ -48,11 +48,13 @@ describe("get", () => {
// filter and map
it.each([
["..items[?].amount", [40], (i: any) => i.amount > 30], // [?] filter
["..items[?].amount", [40], { id: 4, amount: 40 }], // object filter
["..items[?].amount", undefined, { id: 5, amount: 40 }],
["..items..amount[?][0]", 40, (amt: number) => amt > 30],
["..items..amount[0][?]", undefined, (amt: number) => amt > 30], // filter on single value
["..items..amount[?]", [11, 21, 31, 41], (amt: number) => amt + 1], // [?] mapper
["..items..amount[0][?]", 11, (amt: number) => amt + 1], // [?] map on single value
])("%s should be %j %s", (expr, result, filter) => {
])("%s should be %j for %s", (expr, result, filter) => {
expect(get(data, expr, filter)).toEqual(result);
});