Skip to content

Commit 5ee6587

Browse files
committed
Refactor, cleanup array matching multiple keys
1 parent 0d8bc98 commit 5ee6587

File tree

3 files changed

+36
-20
lines changed

3 files changed

+36
-20
lines changed

index.js

+14-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ const util = require('./util.js')
22

33
// These are the simple operators.
44
// Note that "is distinct from" needs to be used to ensure nulls are returned as expected, see https://modern-sql.com/feature/is-distinct-from
5-
var ops = {
5+
const OPS = {
66
$eq: '=',
77
$gt: '>',
88
$gte: '>=',
@@ -11,7 +11,7 @@ var ops = {
1111
$ne: ' IS DISTINCT FROM ',
1212
}
1313

14-
var otherOps = {
14+
const OTHER_OPS = {
1515
$all: true, $in: true, $nin: true, $not: true, $or: true, $and: true, $elemMatch: true, $regex: true, $type: true, $size: true, $exists: true, $mod: true, $text: true
1616
}
1717

@@ -47,6 +47,7 @@ function createElementOrArrayQuery(path, op, value, parent, arrayPathStr) {
4747
const safeArray = `jsonb_typeof(${text})='array' AND`
4848

4949
let arrayQuery = ''
50+
const specialKeys = getSpecialKeys(path, value, true)
5051
if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
5152
if (typeof value['$size'] !== 'undefined') {
5253
// size does not support array element based matching
@@ -63,6 +64,9 @@ function createElementOrArrayQuery(path, op, value, parent, arrayPathStr) {
6364
const sub = convert(innerPath, subquery, [], false)
6465
return `EXISTS (SELECT * FROM jsonb_array_elements(${text}) WHERE ${safeArray} ${sub})`
6566
}).join(' AND ') + ')'
67+
} else if (specialKeys.length === 0) {
68+
const sub = convert(innerPath, value, [], true)
69+
arrayQuery = `EXISTS (SELECT * FROM jsonb_array_elements(${text}) WHERE ${safeArray} ${sub})`
6670
} else {
6771
const params = value
6872
arrayQuery = '(' + Object.keys(params).map(function (subKey) {
@@ -172,7 +176,7 @@ function convertOp(path, op, value, parent, arrayPaths) {
172176
return `${op=='$ne' ? 'NOT ' : ''}${head} @> ` + util.pathToObject([...tail, value])
173177
} else {
174178
var text = util.pathToText(path, typeof value == 'string')
175-
return text + ops[op] + util.quote(value)
179+
return text + OPS[op] + util.quote(value)
176180
}
177181
}
178182
case '$type': {
@@ -209,6 +213,12 @@ function convertOp(path, op, value, parent, arrayPaths) {
209213
}
210214
}
211215

216+
function getSpecialKeys(path, query, forceExact) {
217+
return Object.keys(query).filter(function (key) {
218+
return (path.length === 1 && !forceExact) || key in OPS || key in OTHER_OPS
219+
})
220+
}
221+
212222
/**
213223
* Convert a filter expression to the corresponding PostgreSQL text.
214224
* @param path {Array} The current path
@@ -234,9 +244,7 @@ var convert = function (path, query, arrayPaths, forceExact=false) {
234244
if (Object.keys(query).length === 0) {
235245
return 'TRUE'
236246
}
237-
var specialKeys = Object.keys(query).filter(function (key) {
238-
return (path.length === 1 && !forceExact) || key in ops || key in otherOps
239-
})
247+
const specialKeys = getSpecialKeys(path, query, forceExact)
240248
switch (specialKeys.length) {
241249
case 0: {
242250
const text = util.pathToText(path, typeof query == 'string')

select.js

+16-14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
var util = require('./util.js')
22

3+
function convertRecur(fieldName, input) {
4+
if (typeof input === 'string') {
5+
return util.pathToText([fieldName].concat(input.split('.')), false)
6+
} else {
7+
var entries = []
8+
for (var key in input) {
9+
entries.push('\'' + key + '\'')
10+
entries.push(convertRecur(fieldName, input[key]))
11+
}
12+
return 'jsonb_build_object(' + entries.join(', ') + ')'
13+
}
14+
}
15+
316
var convert = function (fieldName, projection) {
417
// Empty projection returns full document
518
if (!projection) {
@@ -40,24 +53,13 @@ var convert = function (fieldName, projection) {
4053
if (removals.length > 0 && Object.keys(shellDoc).length > 0) {
4154
throw new Error('Projection cannot have a mix of inclusion and exclusion.')
4255
}
43-
function convertRecur(input) {
44-
if (typeof input === 'string') {
45-
return util.pathToText([fieldName].concat(input.split('.')), false)
46-
} else {
47-
var entries = []
48-
for (var key in input) {
49-
entries.push('\'' + key + '\'')
50-
entries.push(convertRecur(input[key]))
51-
}
52-
return 'jsonb_build_object(' + entries.join(', ') + ')'
53-
}
54-
}
55-
var out = Object.keys(shellDoc).length > 0 ? convertRecur(shellDoc) : fieldName
56+
57+
var out = Object.keys(shellDoc).length > 0 ? convertRecur(fieldName, shellDoc) : fieldName
5658
if (removals.length) {
5759
out += ' ' + removals.join(' ')
5860
}
5961
if (out === fieldName){
60-
return fieldName
62+
return out
6163
}
6264
return out + ' as ' + fieldName
6365
}

test/filter.js

+6
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,12 @@ describe('Match a Field Without Specifying Array Index', function () {
194194
'(data->\'courses\') WHERE jsonb_typeof(data->\'courses\')=\'array\' AND value @> \'{ "distance": "5K" }\'))',
195195
convert('data', { 'courses.distance': '5K' }, ['courses', 'other']))
196196
})
197+
it('basic case matching object', function() {
198+
assert.equal('(data->\'courses\'=\'{"distance":"5K","loop":true}\'::jsonb OR EXISTS (SELECT * FROM' +
199+
' jsonb_array_elements(data->\'courses\') WHERE jsonb_typeof(data->\'courses\')=\'array\' AND' +
200+
' value=\'{"distance":"5K","loop":true}\'::jsonb))',
201+
convert('data', { 'courses': { distance: '5K', loop: true } }, ['courses']))
202+
})
197203
it('basic deep case', function() {
198204
assert.equal('(data @> \'{ "courses": { "distance": "5K" } }\' OR EXISTS (SELECT * FROM jsonb_array_elements' +
199205
'(data->\'courses\'->\'distance\') WHERE jsonb_typeof(data->\'courses\'->\'distance\')=\'array\' AND value @> \'"5K"\'))',

0 commit comments

Comments
 (0)