Skip to content

Commit e12da85

Browse files
authored
Merge pull request #3 from Terminal-Systems/feature/aggregate-filters
Feature/aggregate filters
2 parents b92382e + 372f4f4 commit e12da85

5 files changed

Lines changed: 131 additions & 6 deletions

File tree

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,24 @@ knexFlexFilter(baseQuery, where, opts).then(result => console.log(result));
119119
// Will produce a query like whereRaw("myJsonbColumn->>'a' > ?")
120120
```
121121

122+
### isAggregateFn
123+
124+
Use this function when trying to filter over a query that has an aggregate function (sum, count, etc.). It receives the column name and must return true if it's an aggregate column and false otherwise. Must be used together with `preprocessor`, as aggregate functions are filtered using `having`, which takes the operation instead of the alias.
125+
126+
For example:
127+
128+
```javascript
129+
const baseQuery = knex.table('entities').sum('ownerId as ownerIdSum').groupBy('id');
130+
const isAggregateFn = column => column === 'ownerIdSum';
131+
const preprocessor = column => (column === 'ownerIdSum' ? 'sum("ownerId")' : column);
132+
133+
const query = knexFlexFilter(
134+
aggregatedQuery,
135+
{ ownerIdSum_eq: 1 },
136+
{ castFn, isAggregateFn, preprocessor }
137+
);
138+
```
139+
122140
## Contributing
123141

124142
Make sure all the tests pass before sending a PR. To run the test suite, run `yarn test`. Please note that the codebase is using `dotenv` package to connect to a test db, so, to connect to your own, add a `.env` file inside the `tests` folder with the following structure:

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "knex-flex-filter",
3-
"version": "0.2.0",
3+
"version": "0.2.1",
44
"description": "Flexible filtering and search for Knex queries",
55
"main": "dist/index.js",
66
"repository": "https://github.com/Terminal-Systems/knex-flex-filter",

src/index.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,20 @@ const processFilter = (filterQS, castFn, preprocessor) => {
8989

9090

9191
export const knexFlexFilter = (originalQuery, where = {}, opts = {}) => {
92-
const { castFn, preprocessor = defaultPreprocessor() } = opts;
92+
const { castFn, preprocessor = defaultPreprocessor(), isAggregateFn } = opts;
9393

9494
let result = originalQuery;
9595

9696
Object.keys(where).forEach((key) => {
9797
const query = processFilter(key, castFn, preprocessor);
98-
result = result.whereRaw(query, [where[key]]);
98+
const { column } = splitColumnAndCondition(key);
99+
let queryFn = 'whereRaw';
100+
if (isAggregateFn) {
101+
if (isAggregateFn(column)) {
102+
queryFn = 'havingRaw';
103+
}
104+
}
105+
result = result[queryFn](query, [where[key]]);
99106
});
100107

101108
return result;

tests/knex-flex-filter.test.js

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,8 @@ describe('knex-flex-filter', () => {
1717
return 'bigint';
1818
case 'lastBuyBlockNumber':
1919
return 'bigint';
20-
2120
default:
22-
return undefined;
21+
return '';
2322
}
2423
};
2524
done();
@@ -136,6 +135,107 @@ describe('knex-flex-filter', () => {
136135
});
137136
});
138137

138+
describe('when filtering an aggregate column', () => {
139+
let aggregatedQuery;
140+
let isAggregateFn;
141+
let preprocessor;
142+
beforeEach(() => {
143+
aggregatedQuery = knex.table('entities').sum('ownerId as ownerIdSum').groupBy('id');
144+
isAggregateFn = column => column === 'ownerIdSum';
145+
preprocessor = column => (column === 'ownerIdSum' ? 'sum("ownerId")' : column);
146+
});
147+
148+
it('correctly filters by _eq', async (done) => {
149+
const query = knexFlexFilter(aggregatedQuery, { ownerIdSum_eq: 1 }, { castFn, isAggregateFn, preprocessor });
150+
151+
expect(query._statements[2].value.sql).toEqual('sum("ownerId") = ?');
152+
expect(query._statements[2].value.bindings).toEqual([1]);
153+
154+
const result = await query;
155+
expect(parseInt(result[0].ownerIdSum, 10)).toEqual(1);
156+
done();
157+
});
158+
159+
it('correctly filters by _gt', async (done) => {
160+
const query = knexFlexFilter(aggregatedQuery, { ownerIdSum_gt: 0 }, { castFn, isAggregateFn, preprocessor });
161+
162+
expect(query._statements[2].value.sql).toEqual('sum("ownerId") > ?');
163+
expect(query._statements[2].value.bindings).toEqual([0]);
164+
165+
const result = await query;
166+
expect(parseInt(result[0].ownerIdSum, 10)).toBeGreaterThan(0);
167+
done();
168+
});
169+
170+
it('correctly filters by _lt', async (done) => {
171+
const query = knexFlexFilter(aggregatedQuery, { ownerIdSum_lt: 2 }, { castFn, isAggregateFn, preprocessor });
172+
173+
expect(query._statements[2].value.sql).toEqual('sum("ownerId") < ?');
174+
expect(query._statements[2].value.bindings).toEqual([2]);
175+
176+
const result = await query;
177+
expect(parseInt(result[0].ownerIdSum, 10)).toBeLessThan(2);
178+
done();
179+
});
180+
181+
it('correctly filters by _in', async (done) => {
182+
const query = knexFlexFilter(aggregatedQuery, { ownerIdSum_in: [1, 2] }, { castFn, isAggregateFn, preprocessor });
183+
184+
expect(query._statements[2].value.sql).toEqual('sum("ownerId") = ANY(?)');
185+
expect(query._statements[2].value.bindings).toEqual([[1, 2]]);
186+
187+
const result = await query;
188+
expect([1, 2]).toContain(parseInt(result[0].ownerIdSum, 10));
189+
done();
190+
});
191+
192+
it('correctly filters by _not', async (done) => {
193+
const query = knexFlexFilter(aggregatedQuery, { ownerIdSum_not: 2 }, { castFn, isAggregateFn, preprocessor });
194+
195+
expect(query._statements[2].value.sql).toEqual('sum("ownerId") <> ?');
196+
expect(query._statements[2].value.bindings).toEqual([2]);
197+
198+
const result = await query;
199+
expect(result.map(_schema => parseInt(_schema.ownerIdSum, 10))).not.toContain(2);
200+
done();
201+
});
202+
203+
it('correctly filters by _gte', async (done) => {
204+
const query = knexFlexFilter(aggregatedQuery, { ownerIdSum_gte: 1 }, { castFn, isAggregateFn, preprocessor });
205+
206+
expect(query._statements[2].value.sql).toEqual('sum("ownerId") >= ?');
207+
expect(query._statements[2].value.bindings).toEqual([1]);
208+
209+
const result = await query;
210+
expect(parseInt(result[0].ownerIdSum, 10)).toBeGreaterThanOrEqual(1);
211+
done();
212+
});
213+
214+
it('correctly filters by _lte', async (done) => {
215+
const query = knexFlexFilter(aggregatedQuery, { ownerIdSum_lte: 1 }, { castFn, isAggregateFn, preprocessor });
216+
217+
expect(query._statements[2].value.sql).toEqual('sum("ownerId") <= ?');
218+
expect(query._statements[2].value.bindings).toEqual([1]);
219+
220+
const result = await query;
221+
expect(parseInt(result[0].ownerIdSum, 10)).toBeLessThanOrEqual(1);
222+
done();
223+
});
224+
225+
// TODO @dmerrill6: Fix this test as it's not passing
226+
xit('correctly filters by _not_in', async (done) => {
227+
const query = knexFlexFilter(aggregatedQuery, { ownerIdSum_not_in: [2, 3] }, { castFn, isAggregateFn, preprocessor });
228+
console.log(query.toString());
229+
expect(query._statements[2].value.sql).toEqual('sum("ownerId") <> ANY(?)');
230+
expect(query._statements[2].value.bindings).toEqual([[2, 3]]);
231+
232+
const result = await query;
233+
console.log('result', result);
234+
expect([2, 3]).not.toContain(parseInt(result[0].ownerIdSum, 10));
235+
done();
236+
});
237+
});
238+
139239
describe('when filtering using the jsonb preprocessor', () => {
140240
const BLOCK_NUMBER = 5000;
141241

tests/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "knex-flex-filter-tests",
3-
"version": "0.1.0",
3+
"version": "0.2.0",
44
"description": "knex-flex-filter test suite",
55
"repository": "https://github.com/Terminal-Systems/knex-flex-filter",
66
"author": "Daniel Merrill <daniel@terminal.co>",

0 commit comments

Comments
 (0)