Skip to content

Commit

Permalink
Merge pull request #9 from Terminal-Systems/feature/operator-mapper
Browse files Browse the repository at this point in the history
Add ability to specify custom conditionMapper
  • Loading branch information
dmerrill6 authored Jul 17, 2019
2 parents 20a5b47 + aa84e0a commit 4864525
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 6 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,29 @@ const query = knexFlexFilter(

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

### conditionMapper
Useful to transform/change the default operator or condition that a
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.

For example, here we change the `contains` condition to use json containment operator `@>`:

```javascript
import { knexFlexFilter, CONTAINS } from 'knex-flex-filter';
...

const opts = {
conditionMapper: (column, condition, defaultValue) => {
if (condition === CONTAINS) {
return '@> ?';
}

return defaultValue;
}
}

knexFlexFilter(baseQuery, where, opts).then(console.log);
```

## Contributing

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:
Expand Down
22 changes: 17 additions & 5 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ export const dbTypes = [

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

const getCondition = (conditionMapper, column, condition) => {
let currCondition = conditionMap[condition];
if (conditionMapper) {
const mappedCondition = conditionMapper(column, condition, currCondition);
if (mappedCondition) {
currCondition = mappedCondition;
}
}

return currCondition;
};

export const defaultPreprocessor = () => filterKey => `"${sanitize(filterKey)}"`;

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

const processFilter = (filterQS, castFn, preprocessor) => {
const processFilter = (filterQS, castFn, preprocessor, conditionMapper) => {
const { column, condition } = splitColumnAndCondition(filterQS);

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

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

export const knexFlexFilter = (originalQuery, where = {}, opts = {}) => {
const {
castFn, preprocessor = defaultPreprocessor(), isAggregateFn, caseInsensitiveSearch = false,
castFn, preprocessor = defaultPreprocessor(), isAggregateFn, caseInsensitiveSearch = false, conditionMapper,
} = opts;

let result = originalQuery;

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

// Escape apostrophes correctly
const matchEscape = conditionMap[condition].match(/'(.*)\?(.*)'/);
const matchEscape = getCondition(conditionMapper, column, condition).match(/'(.*)\?(.*)'/);
if (matchEscape) {
// eslint-disable-next-line no-unused-vars
const [_, pre, post] = matchEscape;
Expand Down
37 changes: 36 additions & 1 deletion tests/knex-flex-filter.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

import seedsFn from './helpers/seeds';
import knex from './knex';
import knexFlexFilter, { jsonbPreprocessor } from '../src';
import knexFlexFilter, { jsonbPreprocessor, defaultPreprocessor, EQ } from '../src';

require('./helpers/database');

Expand Down Expand Up @@ -630,4 +630,39 @@ describe('knex-flex-filter', () => {
done();
});
});

describe('when filtering using the condition mapper', () => {
const BLOCK_NUMBER = 5000;
it('should correctly use new condition operator @>', async () => {
const query = knexFlexFilter(
knex.table('entities'),
{
lastBuyBlockNumber_eq: { lastBuyBlockNumber: BLOCK_NUMBER },
},
{
preprocessor: (column) => {
switch (column) {
case 'lastBuyBlockNumber':
return 'data'; // match against data
default:
return defaultPreprocessor()(column);
}
},
conditionMapper: (column, condition, defaultValue) => {
if (column === 'lastBuyBlockNumber' && condition === EQ) {
return '@> ?';
}
return defaultValue;
},
},
);

expect(query._statements[0].value.sql).toEqual('data @> ?');
expect(query._statements[0].value.bindings).toEqual([{ lastBuyBlockNumber: BLOCK_NUMBER }]);

const result = await query;

expect(parseInt(result[0].data.lastBuyBlockNumber, 10)).toEqual(BLOCK_NUMBER);
});
});
});

0 comments on commit 4864525

Please sign in to comment.