@@ -15,62 +15,81 @@ var otherOps = {
15
15
$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
16
16
}
17
17
18
- function convertOp ( path , op , value , parent , arrayPaths ) {
19
- if ( arrayPaths ) {
20
- for ( var arrPath of arrayPaths ) {
21
- if ( op . startsWith ( arrPath ) ) {
22
- const subPath = op . split ( '.' )
23
- const innerPath = subPath . length > 1 ? [ 'value' , subPath . pop ( ) ] : [ 'value' ]
24
- const singleElementQuery = convertOp ( path , op , value , parent , [ ] )
25
- path = path . concat ( subPath )
26
- const text = util . pathToText ( path , false )
27
- const safeArray = `jsonb_typeof(${ text } )='array' AND`
28
- let arrayQuery = ''
29
- if ( typeof value === 'object' && ! Array . isArray ( value ) && value !== null ) {
30
- if ( typeof value [ '$size' ] !== 'undefined' ) {
31
- // size does not support array element based matching
32
- } else if ( value [ '$elemMatch' ] ) {
33
- const sub = convert ( innerPath , value [ '$elemMatch' ] , [ ] , false )
34
- arrayQuery = `EXISTS (SELECT * FROM jsonb_array_elements(${ text } ) WHERE ${ safeArray } ${ sub } )`
35
- return arrayQuery
36
- } else if ( value [ '$in' ] ) {
37
- const sub = convert ( innerPath , value , [ ] , true )
38
- arrayQuery = `EXISTS (SELECT * FROM jsonb_array_elements(${ text } ) WHERE ${ safeArray } ${ sub } )`
39
- } else if ( value [ '$all' ] ) {
40
- const cleanedValue = value [ '$all' ] . filter ( ( v ) => ( v !== null && typeof v !== 'undefined' ) )
41
- arrayQuery = '(' + cleanedValue . map ( function ( subquery ) {
42
- const sub = convert ( innerPath , subquery , [ ] , false )
43
- return `EXISTS (SELECT * FROM jsonb_array_elements(${ text } ) WHERE ${ safeArray } ${ sub } )`
44
- } ) . join ( ' AND ' ) + ')'
45
- } else {
46
- const params = value
47
- arrayQuery = '(' + Object . keys ( params ) . map ( function ( subKey ) {
48
- const sub = convert ( innerPath , { [ subKey ] : params [ subKey ] } , [ ] , true )
49
- return `EXISTS (SELECT * FROM jsonb_array_elements(${ text } ) WHERE ${ safeArray } ${ sub } )`
50
- } ) . join ( ' AND ' ) + ')'
51
- }
52
- } else {
53
- const sub = convert ( innerPath , value , [ ] , true )
54
- arrayQuery = `EXISTS (SELECT * FROM jsonb_array_elements(${ text } ) WHERE ${ safeArray } ${ sub } )`
55
- }
56
- if ( ! arrayQuery || arrayQuery === '()' ) {
57
- return singleElementQuery
58
- }
59
- return `(${ singleElementQuery } OR ${ arrayQuery } )`
60
- }
18
+ function shouldQueryAsArray ( op , arrayPaths ) {
19
+ if ( arrayPaths === true ) {
20
+ // always assume array path if true is passed
21
+ return true
22
+ }
23
+ if ( ! arrayPaths || ! Array . isArray ( arrayPaths ) ) {
24
+ return false
25
+ }
26
+ return arrayPaths . some ( op => op . startsWith ( arrayPaths ) )
27
+ }
28
+
29
+ function createElementOrArrayQuery ( path , op , value , parent ) {
30
+ const subPath = op . split ( '.' )
31
+ const innerPath = subPath . length > 1 ? [ 'value' , subPath . pop ( ) ] : [ 'value' ]
32
+ const singleElementQuery = convertOp ( path , op , value , parent , [ ] )
33
+ path = path . concat ( subPath )
34
+ const text = util . pathToText ( path , false )
35
+ const safeArray = `jsonb_typeof(${ text } )='array' AND`
36
+ let arrayQuery = ''
37
+ if ( typeof value === 'object' && ! Array . isArray ( value ) && value !== null ) {
38
+ if ( typeof value [ '$size' ] !== 'undefined' ) {
39
+ // size does not support array element based matching
40
+ } else if ( value [ '$elemMatch' ] ) {
41
+ const sub = convert ( innerPath , value [ '$elemMatch' ] , [ ] , false )
42
+ arrayQuery = `EXISTS (SELECT * FROM jsonb_array_elements(${ text } ) WHERE ${ safeArray } ${ sub } )`
43
+ return arrayQuery
44
+ } else if ( value [ '$in' ] ) {
45
+ const sub = convert ( innerPath , value , [ ] , true )
46
+ arrayQuery = `EXISTS (SELECT * FROM jsonb_array_elements(${ text } ) WHERE ${ safeArray } ${ sub } )`
47
+ } else if ( value [ '$all' ] ) {
48
+ const cleanedValue = value [ '$all' ] . filter ( ( v ) => ( v !== null && typeof v !== 'undefined' ) )
49
+ arrayQuery = '(' + cleanedValue . map ( function ( subquery ) {
50
+ const sub = convert ( innerPath , subquery , [ ] , false )
51
+ return `EXISTS (SELECT * FROM jsonb_array_elements(${ text } ) WHERE ${ safeArray } ${ sub } )`
52
+ } ) . join ( ' AND ' ) + ')'
53
+ } else {
54
+ const params = value
55
+ arrayQuery = '(' + Object . keys ( params ) . map ( function ( subKey ) {
56
+ const sub = convert ( innerPath , { [ subKey ] : params [ subKey ] } , [ ] , true )
57
+ return `EXISTS (SELECT * FROM jsonb_array_elements(${ text } ) WHERE ${ safeArray } ${ sub } )`
58
+ } ) . join ( ' AND ' ) + ')'
61
59
}
60
+ } else {
61
+ const sub = convert ( innerPath , value , [ ] , true )
62
+ arrayQuery = `EXISTS (SELECT * FROM jsonb_array_elements(${ text } ) WHERE ${ safeArray } ${ sub } )`
63
+ }
64
+ if ( ! arrayQuery || arrayQuery === '()' ) {
65
+ return singleElementQuery
66
+ }
67
+ return `(${ singleElementQuery } OR ${ arrayQuery } )`
68
+ }
69
+
70
+ /**
71
+ * @param path {string} a dotted path
72
+ * @param op {string} sub path, especially the current operation to convert, e.g. $in
73
+ * @param value {mixed}
74
+ * @param parent {mixed} parent[path] = value
75
+ * @param arrayPaths {Array} List of dotted paths that possibly need to be handled as arrays.
76
+ */
77
+ function convertOp ( path , op , value , parent , arrayPaths ) {
78
+ if ( shouldQueryAsArray ( op , arrayPaths ) ) {
79
+ return createElementOrArrayQuery ( path , op , value , parent )
62
80
}
63
81
switch ( op ) {
64
82
case '$not' :
65
83
return '(NOT ' + convert ( path , value ) + ')'
66
- case '$nor' :
84
+ case '$nor' : {
67
85
for ( const v of value ) {
68
86
if ( typeof v !== 'object' ) {
69
87
throw new Error ( '$or/$and/$nor entries need to be full objects' )
70
88
}
71
89
}
72
- var notted = value . map ( ( e ) => ( { $not : e } ) )
90
+ const notted = value . map ( ( e ) => ( { $not : e } ) )
73
91
return convertOp ( path , '$and' , notted , value , arrayPaths )
92
+ }
74
93
case '$or' :
75
94
case '$and' :
76
95
if ( ! Array . isArray ( value ) ) {
@@ -91,7 +110,7 @@ function convertOp(path, op, value, parent, arrayPaths) {
91
110
return convert ( path , value , arrayPaths )
92
111
//return util.pathToText(path, false) + ' @> \'' + util.stringEscape(JSON.stringify(value)) + '\'::jsonb'
93
112
case '$in' :
94
- case '$nin' :
113
+ case '$nin' : {
95
114
if ( value . length === 0 ) {
96
115
return 'FALSE'
97
116
}
@@ -104,18 +123,16 @@ function convertOp(path, op, value, parent, arrayPaths) {
104
123
return ( op === '$in' ? '(' + partial + ' OR IS NULL)' : '(' + partial + ' AND IS NOT NULL)' )
105
124
}
106
125
return partial
107
- case '$text' :
108
- var op = '~'
109
- var op2 = ''
110
- if ( ! value [ '$caseSensitive' ] ) {
111
- op += '*'
112
- }
113
- return util . pathToText ( path , true ) + ' ' + op + ' \'' + op2 + util . stringEscape ( value [ '$search' ] ) + '\''
114
- case '$regex' :
115
- var op = '~'
126
+ }
127
+ case '$text' : {
128
+ const newOp = '~' + ( ! value [ '$caseSensitive' ] ? '*' : '' )
129
+ return util . pathToText ( path , true ) + ' ' + newOp + ' \'' + util . stringEscape ( value [ '$search' ] ) + '\''
130
+ }
131
+ case '$regex' : {
132
+ var regexOp = '~'
116
133
var op2 = ''
117
134
if ( parent [ '$options' ] && parent [ '$options' ] . includes ( 'i' ) ) {
118
- op += '*'
135
+ regexOp += '*'
119
136
}
120
137
if ( ! parent [ '$options' ] || ! parent [ '$options' ] . includes ( 's' ) ) {
121
138
// partial newline-sensitive matching
@@ -124,13 +141,14 @@ function convertOp(path, op, value, parent, arrayPaths) {
124
141
if ( value instanceof RegExp ) {
125
142
value = value . source
126
143
}
127
- return util . pathToText ( path , true ) + ' ' + op + ' \'' + op2 + util . stringEscape ( value ) + '\''
144
+ return util . pathToText ( path , true ) + ' ' + regexOp + ' \'' + op2 + util . stringEscape ( value ) + '\''
145
+ }
128
146
case '$gt' :
129
147
case '$gte' :
130
148
case '$lt' :
131
149
case '$lte' :
132
150
case '$ne' :
133
- case '$eq' :
151
+ case '$eq' : {
134
152
const isSimpleComparision = ( op === '$eq' || op === '$ne' )
135
153
const pathContainsArrayAccess = path . some ( ( key ) => / ^ \d + $ / . test ( key ) )
136
154
if ( isSimpleComparision && ! pathContainsArrayAccess ) {
@@ -142,31 +160,36 @@ function convertOp(path, op, value, parent, arrayPaths) {
142
160
var text = util . pathToText ( path , typeof value == 'string' )
143
161
return text + ops [ op ] + util . quote ( value )
144
162
}
145
- case '$type' :
146
- var text = util . pathToText ( path , false )
163
+ }
164
+ case '$type' : {
165
+ const text = util . pathToText ( path , false )
147
166
const type = util . getPostgresTypeName ( value )
148
167
return 'jsonb_typeof(' + text + ')=' + util . quote ( type )
149
- case '$size' :
168
+ }
169
+ case '$size' : {
150
170
if ( typeof value !== 'number' || value < 0 || ! Number . isInteger ( value ) ) {
151
171
throw new Error ( '$size only supports positive integer' )
152
172
}
153
- var text = util . pathToText ( path , false )
173
+ const text = util . pathToText ( path , false )
154
174
return 'jsonb_array_length(' + text + ')=' + value
155
- case '$exists' :
175
+ }
176
+ case '$exists' : {
156
177
if ( path . length > 1 ) {
157
178
const key = path . pop ( )
158
- var text = util . pathToText ( path , false )
179
+ const text = util . pathToText ( path , false )
159
180
return ( value ? '' : ' NOT ' ) + text + ' ? ' + util . quote ( key )
160
181
} else {
161
- var text = util . pathToText ( path , false )
182
+ const text = util . pathToText ( path , false )
162
183
return text + ' IS ' + ( value ? 'NOT ' : '' ) + 'NULL'
163
184
}
164
- case '$mod' :
165
- var text = util . pathToText ( path , true )
185
+ }
186
+ case '$mod' : {
187
+ const text = util . pathToText ( path , true )
166
188
if ( typeof value [ 0 ] != 'number' || typeof value [ 1 ] != 'number' ) {
167
189
throw new Error ( '$mod requires numeric inputs' )
168
190
}
169
191
return 'cast(' + text + ' AS numeric) % ' + value [ 0 ] + '=' + value [ 1 ]
192
+ }
170
193
default :
171
194
return convert ( path . concat ( op . split ( '.' ) ) , value )
172
195
}
@@ -185,7 +208,7 @@ var convert = function (path, query, arrayPaths, forceExact=false) {
185
208
return convertOp ( path , '$eq' , query , { } , arrayPaths )
186
209
}
187
210
if ( query === null ) {
188
- var text = util . pathToText ( path , false )
211
+ const text = util . pathToText ( path , false )
189
212
return '(' + text + ' IS NULL OR ' + text + ' = \'null\'::jsonb)'
190
213
}
191
214
if ( query instanceof RegExp ) {
@@ -201,12 +224,14 @@ var convert = function (path, query, arrayPaths, forceExact=false) {
201
224
return ( path . length === 1 && ! forceExact ) || key in ops || key in otherOps
202
225
} )
203
226
switch ( specialKeys . length ) {
204
- case 0 :
205
- var text = util . pathToText ( path , typeof query == 'string' )
227
+ case 0 : {
228
+ const text = util . pathToText ( path , typeof query == 'string' )
206
229
return text + '=' + util . quote ( query )
207
- case 1 :
230
+ }
231
+ case 1 : {
208
232
const key = specialKeys [ 0 ]
209
233
return convertOp ( path , key , query [ key ] , query , arrayPaths )
234
+ }
210
235
default :
211
236
return '(' + specialKeys . map ( function ( key ) {
212
237
return convertOp ( path , key , query [ key ] , query , arrayPaths )
0 commit comments