Skip to content

Commit

Permalink
gh-10: test filter styles have equivalent output
Browse files Browse the repository at this point in the history
  • Loading branch information
michael-dean-haynie committed May 31, 2022
1 parent faf98ca commit 92d8265
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 28 deletions.
7 changes: 7 additions & 0 deletions lib/ibm-filter/parse-ibm-filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
7 changes: 1 addition & 6 deletions lib/mongo-filter/parse-mongo-filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -166,7 +161,7 @@ const parseMongoFilter = (querystring) => {
if(operatorIsUnary){
fieldResults.push({ [sqlOperator]: attributeRef })
} else {
fieldResults.push({ [sqlOperator]: [attributeRef, sqlValue] })
fieldResults.push({ [sqlOperator]: [attributeRef, ...sqlValues] })
}
}

Expand Down
82 changes: 82 additions & 0 deletions test/ibm-filter/mongo-ibm-equivalence.test.js
Original file line number Diff line number Diff line change
@@ -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')"
},
])
})
})
17 changes: 16 additions & 1 deletion test/ibm-filter/parse-ibm-filter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
},
])
})
Expand Down Expand Up @@ -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']}]}
},
])
})
})
42 changes: 21 additions & 21 deletions test/mongo-filter/parse-mongo-filter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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] }
},
])
})
Expand All @@ -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] }
},
])
})
Expand Down Expand Up @@ -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)',
Expand Down

0 comments on commit 92d8265

Please sign in to comment.