diff --git a/README.md b/README.md index 357f112..2ad10cb 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/src/index.js b/src/index.js index 6af651a..f01c582 100644 --- a/src/index.js +++ b/src/index.js @@ -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)}'`; @@ -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); @@ -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); } @@ -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) { @@ -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; diff --git a/tests/knex-flex-filter.test.js b/tests/knex-flex-filter.test.js index 2bda335..547e369 100644 --- a/tests/knex-flex-filter.test.js +++ b/tests/knex-flex-filter.test.js @@ -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'); @@ -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); + }); + }); });