From dd2b7508f30424af2828c83be6d965e0e543ce3a Mon Sep 17 00:00:00 2001 From: Perfect Makanju Date: Wed, 17 Jul 2019 14:28:12 +0100 Subject: [PATCH 1/3] Add ability to specify custom conditionMapper --- src/index.js | 14 ++++++++---- tests/knex-flex-filter.test.js | 39 ++++++++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/index.js b/src/index.js index 6af651a..dff3128 100644 --- a/src/index.js +++ b/src/index.js @@ -90,7 +90,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 +105,13 @@ const processFilter = (filterQS, castFn, preprocessor) => { if (cast) query = `(${preprocessed})::${cast}`; } - const currCondition = conditionMap[condition]; + let currCondition = conditionMap[condition]; + if (conditionMapper) { + const mappedCondition = conditionMapper(column, condition, currCondition); + if (mappedCondition) { + currCondition = mappedCondition; + } + } if (currCondition.includes('??')) { return currCondition.replace('??', query); } @@ -116,13 +122,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) { diff --git a/tests/knex-flex-filter.test.js b/tests/knex-flex-filter.test.js index 2bda335..7116d4b 100644 --- a/tests/knex-flex-filter.test.js +++ b/tests/knex-flex-filter.test.js @@ -1,13 +1,13 @@ import seedsFn from './helpers/seeds'; import knex from './knex'; -import knexFlexFilter, { jsonbPreprocessor } from '../src'; +import knexFlexFilter, { jsonbPreprocessor, defaultPreprocessor, EQ } from '../src'; require('./helpers/database'); describe('knex-flex-filter', () => { - let castFn; + let castFn; beforeEach(async (done) => { await seedsFn(knex); @@ -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); + }); + }); }); From b3352c988e5ce36746a195fcff4dafaef2a543e2 Mon Sep 17 00:00:00 2001 From: Perfect Makanju Date: Wed, 17 Jul 2019 14:37:09 +0100 Subject: [PATCH 2/3] Update readme with conditionMapper documentation --- README.md | 23 +++++++++++++++++++++++ tests/knex-flex-filter.test.js | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) 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/tests/knex-flex-filter.test.js b/tests/knex-flex-filter.test.js index 7116d4b..547e369 100644 --- a/tests/knex-flex-filter.test.js +++ b/tests/knex-flex-filter.test.js @@ -7,7 +7,7 @@ require('./helpers/database'); describe('knex-flex-filter', () => { - let castFn; + let castFn; beforeEach(async (done) => { await seedsFn(knex); From aa84e0a082a372ed3df8cbb639d7b4552902d522 Mon Sep 17 00:00:00 2001 From: Perfect Makanju Date: Wed, 17 Jul 2019 16:09:55 +0100 Subject: [PATCH 3/3] Prevent escaping on custom condition --- src/index.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/index.js b/src/index.js index dff3128..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)}'`; @@ -105,13 +117,7 @@ const processFilter = (filterQS, castFn, preprocessor, conditionMapper) => { if (cast) query = `(${preprocessed})::${cast}`; } - let currCondition = conditionMap[condition]; - if (conditionMapper) { - const mappedCondition = conditionMapper(column, condition, currCondition); - if (mappedCondition) { - currCondition = mappedCondition; - } - } + let currCondition = getCondition(conditionMapper, column, condition); if (currCondition.includes('??')) { return currCondition.replace('??', query); } @@ -139,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;