Skip to content

Commit 4864525

Browse files
authored
Merge pull request #9 from Terminal-Systems/feature/operator-mapper
Add ability to specify custom conditionMapper
2 parents 20a5b47 + aa84e0a commit 4864525

3 files changed

Lines changed: 76 additions & 6 deletions

File tree

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,29 @@ const query = knexFlexFilter(
165165

166166
Set to `true` if you want to use insensitive-case searches when using `contains` or `starts_with` filters. Defaults to false.
167167

168+
### conditionMapper
169+
Useful to transform/change the default operator or condition that a
170+
particular filter is being evaluated against. `conditionMapper` receives the column name, the condition being evaluated and the default/normal value that should returned for the condition.
171+
172+
For example, here we change the `contains` condition to use json containment operator `@>`:
173+
174+
```javascript
175+
import { knexFlexFilter, CONTAINS } from 'knex-flex-filter';
176+
...
177+
178+
const opts = {
179+
conditionMapper: (column, condition, defaultValue) => {
180+
if (condition === CONTAINS) {
181+
return '@> ?';
182+
}
183+
184+
return defaultValue;
185+
}
186+
}
187+
188+
knexFlexFilter(baseQuery, where, opts).then(console.log);
189+
```
190+
168191
## Contributing
169192

170193
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:

src/index.js

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,18 @@ export const dbTypes = [
7272

7373
const sanitize = identifier => identifier.replace(/([^A-Za-z0-9_]+)/g, '');
7474

75+
const getCondition = (conditionMapper, column, condition) => {
76+
let currCondition = conditionMap[condition];
77+
if (conditionMapper) {
78+
const mappedCondition = conditionMapper(column, condition, currCondition);
79+
if (mappedCondition) {
80+
currCondition = mappedCondition;
81+
}
82+
}
83+
84+
return currCondition;
85+
};
86+
7587
export const defaultPreprocessor = () => filterKey => `"${sanitize(filterKey)}"`;
7688

7789
export const jsonbPreprocessor = jsonbColumn => filterKey => `${sanitize(jsonbColumn)}->>'${sanitize(filterKey)}'`;
@@ -90,7 +102,7 @@ export const splitColumnAndCondition = (filterQS) => {
90102
return { column, condition };
91103
};
92104

93-
const processFilter = (filterQS, castFn, preprocessor) => {
105+
const processFilter = (filterQS, castFn, preprocessor, conditionMapper) => {
94106
const { column, condition } = splitColumnAndCondition(filterQS);
95107

96108
const preprocessed = preprocessor(column);
@@ -105,7 +117,7 @@ const processFilter = (filterQS, castFn, preprocessor) => {
105117
if (cast) query = `(${preprocessed})::${cast}`;
106118
}
107119

108-
const currCondition = conditionMap[condition];
120+
let currCondition = getCondition(conditionMapper, column, condition);
109121
if (currCondition.includes('??')) {
110122
return currCondition.replace('??', query);
111123
}
@@ -116,13 +128,13 @@ const processFilter = (filterQS, castFn, preprocessor) => {
116128

117129
export const knexFlexFilter = (originalQuery, where = {}, opts = {}) => {
118130
const {
119-
castFn, preprocessor = defaultPreprocessor(), isAggregateFn, caseInsensitiveSearch = false,
131+
castFn, preprocessor = defaultPreprocessor(), isAggregateFn, caseInsensitiveSearch = false, conditionMapper,
120132
} = opts;
121133

122134
let result = originalQuery;
123135

124136
Object.keys(where).forEach((key) => {
125-
let query = processFilter(key, castFn, preprocessor);
137+
let query = processFilter(key, castFn, preprocessor, conditionMapper);
126138
const { column, condition } = splitColumnAndCondition(key);
127139
let queryFn = 'whereRaw';
128140
if (isAggregateFn) {
@@ -133,7 +145,7 @@ export const knexFlexFilter = (originalQuery, where = {}, opts = {}) => {
133145
let value = where[key];
134146

135147
// Escape apostrophes correctly
136-
const matchEscape = conditionMap[condition].match(/'(.*)\?(.*)'/);
148+
const matchEscape = getCondition(conditionMapper, column, condition).match(/'(.*)\?(.*)'/);
137149
if (matchEscape) {
138150
// eslint-disable-next-line no-unused-vars
139151
const [_, pre, post] = matchEscape;

tests/knex-flex-filter.test.js

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
import seedsFn from './helpers/seeds';
33
import knex from './knex';
4-
import knexFlexFilter, { jsonbPreprocessor } from '../src';
4+
import knexFlexFilter, { jsonbPreprocessor, defaultPreprocessor, EQ } from '../src';
55

66
require('./helpers/database');
77

@@ -630,4 +630,39 @@ describe('knex-flex-filter', () => {
630630
done();
631631
});
632632
});
633+
634+
describe('when filtering using the condition mapper', () => {
635+
const BLOCK_NUMBER = 5000;
636+
it('should correctly use new condition operator @>', async () => {
637+
const query = knexFlexFilter(
638+
knex.table('entities'),
639+
{
640+
lastBuyBlockNumber_eq: { lastBuyBlockNumber: BLOCK_NUMBER },
641+
},
642+
{
643+
preprocessor: (column) => {
644+
switch (column) {
645+
case 'lastBuyBlockNumber':
646+
return 'data'; // match against data
647+
default:
648+
return defaultPreprocessor()(column);
649+
}
650+
},
651+
conditionMapper: (column, condition, defaultValue) => {
652+
if (column === 'lastBuyBlockNumber' && condition === EQ) {
653+
return '@> ?';
654+
}
655+
return defaultValue;
656+
},
657+
},
658+
);
659+
660+
expect(query._statements[0].value.sql).toEqual('data @> ?');
661+
expect(query._statements[0].value.bindings).toEqual([{ lastBuyBlockNumber: BLOCK_NUMBER }]);
662+
663+
const result = await query;
664+
665+
expect(parseInt(result[0].data.lastBuyBlockNumber, 10)).toEqual(BLOCK_NUMBER);
666+
});
667+
});
633668
});

0 commit comments

Comments
 (0)