From 92d8265b1b08a01872e7d350b8f3fcab078bc521 Mon Sep 17 00:00:00 2001 From: Michael Haynie Date: Tue, 31 May 2022 11:53:16 -0700 Subject: [PATCH] gh-10: test filter styles have equivalent output --- lib/ibm-filter/parse-ibm-filter.js | 7 ++ lib/mongo-filter/parse-mongo-filter.js | 7 +- test/ibm-filter/mongo-ibm-equivalence.test.js | 82 +++++++++++++++++++ test/ibm-filter/parse-ibm-filter.test.js | 17 +++- test/mongo-filter/parse-mongo-filter.test.js | 42 +++++----- 5 files changed, 127 insertions(+), 28 deletions(-) create mode 100644 test/ibm-filter/mongo-ibm-equivalence.test.js diff --git a/lib/ibm-filter/parse-ibm-filter.js b/lib/ibm-filter/parse-ibm-filter.js index b2ab2a3..13a8d78 100644 --- a/lib/ibm-filter/parse-ibm-filter.js +++ b/lib/ibm-filter/parse-ibm-filter.js @@ -58,6 +58,13 @@ const parseExpression = (expression) => { stack.push({[mapOperator(token)]: anyOperands }) } + // IS NULL - (unary operator) + else if (token === IbmOperator.EQUALS && isMongoNull(stack.at(-2))) { + const attributeRef = coerceValue(stack.pop(), token) + stack.pop() // null - not included in output + stack.push({[mapOperator(token, true)]: attributeRef }) + } + // NOT - (unary, higher order operator) else if (token === IbmOperator.NOT) { const objOperand = stack.pop() diff --git a/lib/mongo-filter/parse-mongo-filter.js b/lib/mongo-filter/parse-mongo-filter.js index c8f54c7..fc177b2 100644 --- a/lib/mongo-filter/parse-mongo-filter.js +++ b/lib/mongo-filter/parse-mongo-filter.js @@ -153,11 +153,6 @@ const parseMongoFilter = (querystring) => { return sqlOperator === SqlOperator.LIKE ? `%${val}%` : val })) - // unwrap single-value arrays (except for auto-wrappable operators) - const unwrapIsNeeded = sqlValues.length === 1 - && ![SqlOperator.IN, SqlOperator.NOT_IN].includes(sqlOperator) - const sqlValue = unwrapIsNeeded ? sqlValues[0] : sqlValues - // format like json-logic const attributeRef = `#${field}` const operatorIsUnary = [SqlOperator.IS_NULL, SqlOperator.IS_NOT_NULL] @@ -166,7 +161,7 @@ const parseMongoFilter = (querystring) => { if(operatorIsUnary){ fieldResults.push({ [sqlOperator]: attributeRef }) } else { - fieldResults.push({ [sqlOperator]: [attributeRef, sqlValue] }) + fieldResults.push({ [sqlOperator]: [attributeRef, ...sqlValues] }) } } diff --git a/test/ibm-filter/mongo-ibm-equivalence.test.js b/test/ibm-filter/mongo-ibm-equivalence.test.js new file mode 100644 index 0000000..78b19bf --- /dev/null +++ b/test/ibm-filter/mongo-ibm-equivalence.test.js @@ -0,0 +1,82 @@ +const { parseMongoFilter } = require('../../lib/mongo-filter/parse-mongo-filter') +const { parseIbmFilter } = require('../../lib/ibm-filter/parse-ibm-filter') + +function testEachCase(testCases) { + test.concurrent.each(testCases)('$title', ({ mongoQueryString, ibmQueryString}) => { + const { results: mongoResults } = parseMongoFilter(mongoQueryString) + const { results: ibmResults } = parseIbmFilter(ibmQueryString) + try { + expect(mongoResults).toEqual(ibmResults) + } catch (e) { + console.log('mongoResults: ', mongoResults) + console.log('ibmResults: ', ibmResults) + throw e + } + }) +} + +describe('MongoDB-style filtering vs IBM-style filtering Equivalence Tests', () => { + describe('value types', () => { + testEachCase([ + { + title: 'both styles should output string values the same way', + mongoQueryString: "filter[name][$eq]=michael", + ibmQueryString: "filter=equals(name,'michael')" + }, + { + title: 'both styles should output number values the same way', + mongoQueryString: "filter[age][$eq]=25", + ibmQueryString: "filter=equals(age,'25')" + }, + { + title: 'both styles should output date values the same way', + mongoQueryString: "filter[born][$eq]=2020-01-01", + ibmQueryString: "filter=equals(born,'2020-01-01')" + }, + { + title: 'both styles should output null values the same way', + mongoQueryString: "filter[born][$eq]=null", + ibmQueryString: "filter=equals(born,null)" + }, + ]) + }) + describe('operators', () => { + testEachCase([ + { + title: 'both styles should output the "=" sql operator the same way', + mongoQueryString: "filter[name][$eq]=michael", + ibmQueryString: "filter=equals(name,'michael')" + }, + { + title: 'both styles should output the ">" sql operator the same way', + mongoQueryString: "filter[name][$gt]=michael", + ibmQueryString: "filter=greaterThan(name,'michael')" + }, + { + title: 'both styles should output the ">=" sql operator the same way', + mongoQueryString: "filter[name][$gte]=michael", + ibmQueryString: "filter=greaterOrEqual(name,'michael')" + }, + { + title: 'both styles should output the "<" sql operator the same way', + mongoQueryString: "filter[name][$lt]=michael", + ibmQueryString: "filter=lessThan(name,'michael')" + }, + { + title: 'both styles should output the "<=" sql operator the same way', + mongoQueryString: "filter[name][$lte]=michael", + ibmQueryString: "filter=lessOrEqual(name,'michael')" + }, + { + title: 'both styles should output the "LIKE" sql operator the same way', + mongoQueryString: "filter[name][ilike]=ch", + ibmQueryString: "filter=contains(name,'ch')" + }, + { + title: 'both styles should output the "IN" sql operator the same way', + mongoQueryString: "filter[name][$in]=michael,brad", + ibmQueryString: "filter=any(name,'michael','brad')" + }, + ]) + }) +}) \ No newline at end of file diff --git a/test/ibm-filter/parse-ibm-filter.test.js b/test/ibm-filter/parse-ibm-filter.test.js index 790a685..ff71913 100644 --- a/test/ibm-filter/parse-ibm-filter.test.js +++ b/test/ibm-filter/parse-ibm-filter.test.js @@ -34,7 +34,7 @@ describe('parseIbmFilter() tests', () => { { title: 'the "equals" ibm operator should map to the "IS NULL" sql operator for null values', queryString: "filter=equals(age,null)", - expectedResults: { 'IS NULL': ['#age', null] } + expectedResults: { 'IS NULL': '#age' } }, ]) }) @@ -472,4 +472,19 @@ describe('parseIbmFilter() tests', () => { }, ]) }) + + describe('multiple filters', () => { + testEachCase([ + { + title: 'multiple filters should be join together in an OR fashion (ex: 2)', + queryString: "filter=equals(name,'michael')&filter=equals(age,'25')", + expectedResults: { 'OR': [{ '=': ['#name', 'michael'] }, { '=': ['#age', 25] }]} + }, + { + title: 'multiple filters should be join together in an OR fashion (ex: 3)', + queryString: "filter=equals(name,'michael')&filter=equals(age,'25')&filter=lessThan(born,'2020-01-01')", + expectedResults: {'OR': [{ 'OR': [{ '=': ['#name', 'michael'] }, { '=': ['#age', 25] }]}, { '<': ['#born', '2020-01-01']}]} + }, + ]) + }) }) \ No newline at end of file diff --git a/test/mongo-filter/parse-mongo-filter.test.js b/test/mongo-filter/parse-mongo-filter.test.js index 8843eb9..afabdac 100644 --- a/test/mongo-filter/parse-mongo-filter.test.js +++ b/test/mongo-filter/parse-mongo-filter.test.js @@ -224,37 +224,37 @@ describe('parseMongoFilter() tests', () => { { title: 'the "$in" mongo operator should map to the "IN" sql operator for multiple string value', queryString: 'filter[name][$in]=michael,brad', - expectedResults: { 'IN': ['#name', ['michael', 'brad']] } + expectedResults: { 'IN': ['#name', 'michael', 'brad'] } }, { title: 'the "$in" mongo operator should map to the "IN" sql operator for singular string values (auto-wrapping)', queryString: 'filter[name][$in]=michael', - expectedResults: { 'IN': ['#name', ['michael']] } + expectedResults: { 'IN': ['#name', 'michael'] } }, { title: 'the "$in" mongo operator should map to the "IN" sql operator for multiple number values', queryString: 'filter[age][$in]=24,25', - expectedResults: { 'IN': ['#age', [24, 25]] } + expectedResults: { 'IN': ['#age', 24, 25] } }, { title: 'the "$in" mongo operator should map to the "IN" sql operator for singular number values (auto-wrapping)', queryString: 'filter[age][$in]=25', - expectedResults: { 'IN': ['#age', [25]] } + expectedResults: { 'IN': ['#age', 25] } }, { title: 'the "$in" mongo operator should map to the "IN" sql operator for multiple date value', queryString: 'filter[born][$in]=2020-01-01,2021-01-01', - expectedResults: { 'IN': ['#born', ['2020-01-01', '2021-01-01']] } + expectedResults: { 'IN': ['#born', '2020-01-01', '2021-01-01'] } }, { title: 'the "$in" mongo operator should map to the "IN" sql operator for singular date values (auto-wrapping)', queryString: 'filter[born][$in]=2020-01-01', - expectedResults: { 'IN': ['#born', ['2020-01-01']] } + expectedResults: { 'IN': ['#born', '2020-01-01'] } }, { title: 'the "$in" mongo operator should map to the "IN" sql operator for singular null value (auto-wrapping)', queryString: 'filter[age][$in]=null', - expectedResults: { 'IN': ['#age', [null]] } + expectedResults: { 'IN': ['#age', null] } }, ]) }) @@ -264,37 +264,37 @@ describe('parseMongoFilter() tests', () => { { title: 'the "$nin" mongo operator should map to the "NOT IN" sql operator for multiple string values', queryString: 'filter[name][$nin]=michael,brad', - expectedResults: { 'NOT IN': ['#name', ['michael', 'brad']] } + expectedResults: { 'NOT IN': ['#name', 'michael', 'brad'] } }, { title: 'the "$nin" mongo operator should map to the "NOT IN" sql operator for singular string value (auto-wrapping)', queryString: 'filter[name][$nin]=michael', - expectedResults: { 'NOT IN': ['#name', ['michael']] } + expectedResults: { 'NOT IN': ['#name', 'michael'] } }, { title: 'the "$nin" mongo operator should map to the "NOT IN" sql operator for multiple number values', queryString: 'filter[age][$nin]=24,25', - expectedResults: { 'NOT IN': ['#age', [24, 25]] } + expectedResults: { 'NOT IN': ['#age', 24, 25] } }, { title: 'the "$nin" mongo operator should map to the "NOT IN" sql operator for singular number value (auto-wrapping)', queryString: 'filter[age][$nin]=25', - expectedResults: { 'NOT IN': ['#age', [25]] } + expectedResults: { 'NOT IN': ['#age', 25] } }, { title: 'the "$nin" mongo operator should map to the "NOT IN" sql operator for multiple date values', queryString: 'filter[born][$nin]=2020-01-01,2021-01-01', - expectedResults: { 'NOT IN': ['#born', ['2020-01-01', '2021-01-01']] } + expectedResults: { 'NOT IN': ['#born', '2020-01-01', '2021-01-01'] } }, { title: 'the "$nin" mongo operator should map to the "NOT IN" sql operator for singular date value (auto-wrapping)', queryString: 'filter[born][$nin]=2020-01-01', - expectedResults: { 'NOT IN': ['#born', ['2020-01-01']] } + expectedResults: { 'NOT IN': ['#born', '2020-01-01'] } }, { title: 'the "$nin" mongo operator should map to the "NOT IN" sql operator for singular null value (auto-wrapping)', queryString: 'filter[age][$nin]=null', - expectedResults: { 'NOT IN': ['#age', [null]] } + expectedResults: { 'NOT IN': ['#age', null] } }, ]) }) @@ -326,37 +326,37 @@ describe('parseMongoFilter() tests', () => { { title: 'the "IN" sql operator should be the default for string[] (string array) values', queryString: 'filter[name]=michael,brad', - expectedResults: { 'IN': ['#name', ['michael', 'brad']] } + expectedResults: { 'IN': ['#name', 'michael', 'brad'] } }, { title: 'the "IN" sql operator should be the default for string[] (string array) values (null included)', queryString: 'filter[name]=michael,null', - expectedResults: { 'IN': ['#name', ['michael', null]] } + expectedResults: { 'IN': ['#name', 'michael', null] } }, { title: 'the "IN" sql operator should be the default for number[] (number array) values', queryString: 'filter[age]=24,25', - expectedResults: { 'IN': ['#age', [24, 25]] } + expectedResults: { 'IN': ['#age', 24, 25] } }, { title: 'the "IN" sql operator should be the default for number[] (number array) values (null included)', queryString: 'filter[age]=24,null', - expectedResults: { 'IN': ['#age', [24, null]] } + expectedResults: { 'IN': ['#age', 24, null] } }, { title: 'the "IN" sql operator should be the default for date[] (date array) values', queryString: 'filter[born]=2020-01-01,2021-01-01', - expectedResults: { 'IN': ['#born', ['2020-01-01', '2021-01-01']] } + expectedResults: { 'IN': ['#born', '2020-01-01', '2021-01-01'] } }, { title: 'the "IN" sql operator should be the default for date[] (date array) values (null included)', queryString: 'filter[born]=2020-01-01,null', - expectedResults: { 'IN': ['#born', ['2020-01-01', null]] } + expectedResults: { 'IN': ['#born', '2020-01-01', null] } }, ]) }) - describe('compound filters', () => { + describe('multiple filters', () => { testEachCase([ { title: 'multiple filters should be join together in an AND fashion (ex: 2)',