@@ -34,14 +34,14 @@ function getMatchingArrayPath(op, arrayPaths) {
34
34
* @param arrayPathStr
35
35
* @returns {string|string|* }
36
36
*/
37
- function createElementOrArrayQuery ( path , op , value , parent , arrayPathStr ) {
37
+ function createElementOrArrayQuery ( path , op , value , parent , arrayPathStr , options ) {
38
38
const arrayPath = arrayPathStr . split ( '.' )
39
39
const deeperPath = op . split ( '.' ) . slice ( arrayPath . length )
40
40
const innerPath = [ 'value' , ...deeperPath ]
41
41
const pathToMaybeArray = path . concat ( arrayPath )
42
42
43
43
// TODO: nested array paths are not yet supported.
44
- const singleElementQuery = convertOp ( path , op , value , parent , [ ] )
44
+ const singleElementQuery = convertOp ( path , op , value , parent , [ ] , options )
45
45
46
46
const text = util . pathToText ( pathToMaybeArray , false )
47
47
const safeArray = `jsonb_typeof(${ text } )='array' AND`
@@ -52,30 +52,30 @@ function createElementOrArrayQuery(path, op, value, parent, arrayPathStr) {
52
52
if ( typeof value [ '$size' ] !== 'undefined' ) {
53
53
// size does not support array element based matching
54
54
} else if ( value [ '$elemMatch' ] ) {
55
- const sub = convert ( innerPath , value [ '$elemMatch' ] , [ ] , false )
55
+ const sub = convert ( innerPath , value [ '$elemMatch' ] , [ ] , false , options )
56
56
arrayQuery = `EXISTS (SELECT * FROM jsonb_array_elements(${ text } ) WHERE ${ safeArray } ${ sub } )`
57
57
return arrayQuery
58
58
} else if ( value [ '$in' ] ) {
59
- const sub = convert ( innerPath , value , [ ] , true )
59
+ const sub = convert ( innerPath , value , [ ] , true , options )
60
60
arrayQuery = `EXISTS (SELECT * FROM jsonb_array_elements(${ text } ) WHERE ${ safeArray } ${ sub } )`
61
61
} else if ( value [ '$all' ] ) {
62
62
const cleanedValue = value [ '$all' ] . filter ( ( v ) => ( v !== null && typeof v !== 'undefined' ) )
63
63
arrayQuery = '(' + cleanedValue . map ( function ( subquery ) {
64
- const sub = convert ( innerPath , subquery , [ ] , false )
64
+ const sub = convert ( innerPath , subquery , [ ] , false , options )
65
65
return `EXISTS (SELECT * FROM jsonb_array_elements(${ text } ) WHERE ${ safeArray } ${ sub } )`
66
66
} ) . join ( ' AND ' ) + ')'
67
67
} else if ( specialKeys . length === 0 ) {
68
- const sub = convert ( innerPath , value , [ ] , true )
68
+ const sub = convert ( innerPath , value , [ ] , true , options )
69
69
arrayQuery = `EXISTS (SELECT * FROM jsonb_array_elements(${ text } ) WHERE ${ safeArray } ${ sub } )`
70
70
} else {
71
71
const params = value
72
72
arrayQuery = '(' + Object . keys ( params ) . map ( function ( subKey ) {
73
- const sub = convert ( innerPath , { [ subKey ] : params [ subKey ] } , [ ] , true )
73
+ const sub = convert ( innerPath , { [ subKey ] : params [ subKey ] } , [ ] , true , options )
74
74
return `EXISTS (SELECT * FROM jsonb_array_elements(${ text } ) WHERE ${ safeArray } ${ sub } )`
75
75
} ) . join ( ' AND ' ) + ')'
76
76
}
77
77
} else {
78
- const sub = convert ( innerPath , value , [ ] , true )
78
+ const sub = convert ( innerPath , value , [ ] , true , options )
79
79
arrayQuery = `EXISTS (SELECT * FROM jsonb_array_elements(${ text } ) WHERE ${ safeArray } ${ sub } )`
80
80
}
81
81
if ( ! arrayQuery || arrayQuery === '()' ) {
@@ -91,25 +91,25 @@ function createElementOrArrayQuery(path, op, value, parent, arrayPathStr) {
91
91
* @param parent {mixed} parent[path] = value
92
92
* @param arrayPaths {Array} List of dotted paths that possibly need to be handled as arrays.
93
93
*/
94
- function convertOp ( path , op , value , parent , arrayPaths ) {
94
+ function convertOp ( path , op , value , parent , arrayPaths , options ) {
95
95
const arrayPath = getMatchingArrayPath ( op , arrayPaths )
96
96
// It seems like direct matches shouldn't be array fields, but 2D arrays are possible in MongoDB
97
97
// I will need to do more testing to see if we should handle this case differently.
98
98
// const arrayDirectMatch = !isSpecialOp(op) && Array.isArray(value)
99
99
if ( arrayPath ) {
100
- return createElementOrArrayQuery ( path , op , value , parent , arrayPath )
100
+ return createElementOrArrayQuery ( path , op , value , parent , arrayPath , options )
101
101
}
102
102
switch ( op ) {
103
103
case '$not' :
104
- return '(NOT ' + convert ( path , value ) + ')'
104
+ return '(NOT ' + convert ( path , value , undefined , false , options ) + ')'
105
105
case '$nor' : {
106
106
for ( const v of value ) {
107
107
if ( typeof v !== 'object' ) {
108
108
throw new Error ( '$or/$and/$nor entries need to be full objects' )
109
109
}
110
110
}
111
111
const notted = value . map ( ( e ) => ( { $not : e } ) )
112
- return convertOp ( path , '$and' , notted , value , arrayPaths )
112
+ return convertOp ( path , '$and' , notted , value , arrayPaths , options )
113
113
}
114
114
case '$or' :
115
115
case '$and' :
@@ -124,19 +124,19 @@ function convertOp(path, op, value, parent, arrayPaths) {
124
124
throw new Error ( '$or/$and/$nor entries need to be full objects' )
125
125
}
126
126
}
127
- return '(' + value . map ( ( subquery ) => convert ( path , subquery , arrayPaths ) ) . join ( op === '$or' ? ' OR ' : ' AND ' ) + ')'
127
+ return '(' + value . map ( ( subquery ) => convert ( path , subquery , arrayPaths , false , options ) ) . join ( op === '$or' ? ' OR ' : ' AND ' ) + ')'
128
128
}
129
129
// TODO (make sure this handles multiple elements correctly)
130
130
case '$elemMatch' :
131
- return convert ( path , value , arrayPaths )
131
+ return convert ( path , value , arrayPaths , false , options )
132
132
//return util.pathToText(path, false) + ' @> \'' + util.stringEscape(JSON.stringify(value)) + '\'::jsonb'
133
133
case '$in' :
134
134
case '$nin' : {
135
135
if ( value . length === 0 ) {
136
136
return 'FALSE'
137
137
}
138
138
if ( value . length === 1 ) {
139
- return convert ( path , value [ 0 ] , arrayPaths )
139
+ return convert ( path , value [ 0 ] , arrayPaths , false , options )
140
140
}
141
141
const cleanedValue = value . filter ( ( v ) => ( v !== null && typeof v !== 'undefined' ) )
142
142
let partial = util . pathToText ( path , typeof value [ 0 ] == 'string' ) + ( op == '$nin' ? ' NOT' : '' ) + ' IN (' + cleanedValue . map ( util . quote ) . join ( ', ' ) + ')'
@@ -172,7 +172,7 @@ function convertOp(path, op, value, parent, arrayPaths) {
172
172
case '$eq' : {
173
173
const isSimpleComparision = ( op === '$eq' || op === '$ne' )
174
174
const pathContainsArrayAccess = path . some ( ( key ) => / ^ \d + $ / . test ( key ) )
175
- if ( isSimpleComparision && ! pathContainsArrayAccess ) {
175
+ if ( isSimpleComparision && ! pathContainsArrayAccess && ! options . disableContainmentQuery ) {
176
176
// create containment query since these can use GIN indexes
177
177
// See docs here, https://www.postgresql.org/docs/9.4/datatype-json.html#JSON-INDEXING
178
178
const [ head , ...tail ] = path
@@ -213,7 +213,7 @@ function convertOp(path, op, value, parent, arrayPaths) {
213
213
}
214
214
default :
215
215
// this is likely a top level field, recurse
216
- return convert ( path . concat ( op . split ( '.' ) ) , value )
216
+ return convert ( path . concat ( op . split ( '.' ) ) , value , undefined , false , options )
217
217
}
218
218
}
219
219
@@ -236,9 +236,9 @@ function getSpecialKeys(path, query, forceExact) {
236
236
* @param forceExact {Boolean} When true, an exact match will be required.
237
237
* @returns The corresponding PSQL expression
238
238
*/
239
- var convert = function ( path , query , arrayPaths , forceExact = false ) {
239
+ var convert = function ( path , query , arrayPaths , forceExact , options ) {
240
240
if ( typeof query === 'string' || typeof query === 'boolean' || typeof query == 'number' || Array . isArray ( query ) ) {
241
- return convertOp ( path , '$eq' , query , { } , arrayPaths )
241
+ return convertOp ( path , '$eq' , query , { } , arrayPaths , options )
242
242
}
243
243
if ( query === null ) {
244
244
const text = util . pathToText ( path , false )
@@ -261,18 +261,26 @@ var convert = function (path, query, arrayPaths, forceExact=false) {
261
261
}
262
262
case 1 : {
263
263
const key = specialKeys [ 0 ]
264
- return convertOp ( path , key , query [ key ] , query , arrayPaths )
264
+ return convertOp ( path , key , query [ key ] , query , arrayPaths , options )
265
265
}
266
266
default :
267
267
return '(' + specialKeys . map ( function ( key ) {
268
- return convertOp ( path , key , query [ key ] , query , arrayPaths )
268
+ return convertOp ( path , key , query [ key ] , query , arrayPaths , options )
269
269
} ) . join ( ' and ' ) + ')'
270
270
}
271
271
}
272
272
}
273
273
274
- module . exports = function ( fieldName , query , arrays ) {
275
- return convert ( [ fieldName ] , query , arrays || [ ] )
274
+ module . exports = function ( fieldName , query , arraysOrOptions ) {
275
+ let arrays
276
+ let options = { }
277
+ if ( arraysOrOptions && Array . isArray ( arraysOrOptions ) ) {
278
+ arrays = arraysOrOptions
279
+ } else if ( typeof arraysOrOptions === 'object' ) {
280
+ arrays = arraysOrOptions . arrays || [ ]
281
+ options = arraysOrOptions
282
+ }
283
+ return convert ( [ fieldName ] , query , arrays || [ ] , false , options )
276
284
}
277
285
module . exports . convertDotNotation = util . convertDotNotation
278
286
module . exports . pathToText = util . pathToText
0 commit comments