Skip to content

Commit afd13f6

Browse files
author
Thomas Hansen
committed
Add $exists, $type, $size and $mod. Fix code styling and update readme.
1 parent aaef9a8 commit afd13f6

File tree

6 files changed

+333
-194
lines changed

6 files changed

+333
-194
lines changed

.eslintrc.js

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
module.exports = {
2+
"env": {
3+
"node": true,
4+
"mocha": true
5+
},
6+
"extends": "eslint:recommended",
7+
"rules": {
8+
"indent": [
9+
"error",
10+
2
11+
],
12+
"linebreak-style": [
13+
"error",
14+
"unix"
15+
],
16+
"quotes": [
17+
"error",
18+
"single"
19+
],
20+
"semi": [
21+
"error",
22+
"never"
23+
]
24+
}
25+
};

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
node_modules
2+
.idea

README.md

+31-9
Original file line numberDiff line numberDiff line change
@@ -36,23 +36,45 @@ mongoToPostgres('data', query)
3636

3737
The first parameter is the name of your jsonb column in your postgres table. The second parameter is the Mongo style query.
3838

39+
## Match a Field Without Specifying Array Index
40+
41+
* [Mongo Docs](https://docs.mongodb.org/manual/tutorial/query-documents/#match-a-field-without-specifying-array-index)
42+
43+
You can have a document with an array of objects that you want to match when any one of the elements in the array matches.
44+
This is implemented in SQL using a subquery so it may not be the most efficient.
45+
46+
Example document.
47+
```javascript
48+
{
49+
"courses": [{
50+
"distance": "5K"
51+
}, {
52+
"distance": "10K"
53+
}]
54+
}
55+
```
56+
57+
Example query to match when there is a course with a distance of "5K".
58+
```javascript
59+
mongoToPostgres('data', { 'courses.distance': '5K' }, ['courses'])
60+
```
61+
3962
## Supported Features
4063
* $eq, $gt, $gte, $lt, $lte, $ne
41-
* $or, $not
64+
* $or, $not, $nin
4265
* [$in](https://docs.mongodb.org/manual/reference/operator/query/in/#use-the-in-operator-to-match-values-in-an-array), $nin
4366
* $elemMatch
4467
* [$regex](https://docs.mongodb.com/manual/reference/operator/query/regex/)
45-
68+
* [$type](https://docs.mongodb.org/manual/reference/operator/query/type/#op._S_type)
69+
* [$size](https://docs.mongodb.org/manual/reference/operator/query/size/#op._S_size)
70+
* [$exists](https://docs.mongodb.org/manual/reference/operator/query/exists/#op._S_exists)
71+
* [$mod](https://docs.mongodb.com/manual/reference/operator/query/mod/)
4672

4773
## Todo
48-
* $nor
49-
* [$exists](https://docs.mongodb.org/manual/reference/operator/query/exists/#op._S_exists)
50-
* [$size](https://docs.mongodb.org/manual/reference/operator/query/size/#op._S_size)
51-
* [$type](https://docs.mongodb.org/manual/reference/operator/query/type/#op._S_type)
5274
* [Match an array element](https://docs.mongodb.org/manual/tutorial/query-documents/#match-an-array-element)
53-
54-
## Can't support
55-
* [Match a Field Without Specifying Array Index](https://docs.mongodb.org/manual/tutorial/query-documents/#match-a-field-without-specifying-array-index)
75+
* [$all](https://docs.mongodb.com/manual/reference/operator/query/all/)
76+
* [$expr](https://docs.mongodb.com/manual/reference/operator/query/expr/)
77+
* [Bitwise Operators](https://docs.mongodb.com/manual/reference/operator/query-bitwise/)
5678

5779
## See also
5880
* [PostgreSQL json/jsonb functions and operators](http://www.postgresql.org/docs/9.4/static/functions-json.html)

index.js

+118-77
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,140 @@
11
var quote = function(data) {
2-
if (typeof data == 'string')
3-
return "'" + stringEscape(data) + "'";
4-
return "'"+JSON.stringify(data)+"'::jsonb"
2+
if (typeof data == 'string')
3+
return '\'' + stringEscape(data) + '\''
4+
return '\''+JSON.stringify(data)+'\'::jsonb'
55
}
66

77
var stringEscape = function(str) {
8-
return str.replace(/'/g, "''")
8+
return str.replace(/'/g, '\'\'')
99
}
1010

1111
var pathToText = function(path, isString) {
12-
var text = stringEscape(path[0]);
13-
for (var i = 1; i < path.length; i++) {
14-
text += (i == path.length-1 && isString ? '->>' : '->');
15-
if (/\d+/.test(path[i]))
16-
text += path[i]; //don't wrap numbers in quotes
17-
else
18-
text += '\'' + stringEscape(path[i]) + '\'';
19-
}
20-
return text;
12+
var text = stringEscape(path[0])
13+
for (var i = 1; i < path.length; i++) {
14+
text += (i == path.length-1 && isString ? '->>' : '->')
15+
if (/\d+/.test(path[i]))
16+
text += path[i] //don't wrap numbers in quotes
17+
else
18+
text += '\'' + stringEscape(path[i]) + '\''
19+
}
20+
return text
2121
}
2222
var convertDotNotation = function(path, pathDotNotation) {
23-
return pathToText([path].concat(pathDotNotation.split('.')), true);
23+
return pathToText([path].concat(pathDotNotation.split('.')), true)
2424
}
2525

2626
// These are the simple operators.
2727
var ops = {
28-
$eq: '=',
29-
$gt: '>',
30-
$gte: '>=',
31-
$lt: '<',
32-
$lte: '<=',
33-
$ne: '!=',
28+
$eq: '=',
29+
$gt: '>',
30+
$gte: '>=',
31+
$lt: '<',
32+
$lte: '<=',
33+
$ne: '!=',
3434
}
3535

3636
var otherOps = {
37-
$in: true, $nin: true, $not: true, $or: true, $and: true, $elemMatch: true, $regex: true
37+
$in: true, $nin: true, $not: true, $or: true, $and: true, $elemMatch: true, $regex: true, $type: true, $size: true, $exists: true, $mod: true
3838
}
3939

40-
var convert = function (path, query) {
41-
if (typeof query == 'object' && !Array.isArray(query)) {
42-
var specialKeys = Object.keys(query)
43-
if (path.length > 1) {
44-
specialKeys = specialKeys.filter(function(key) {
45-
return key in ops || key in otherOps;
46-
})
47-
}
48-
if (specialKeys.length > 0) {
49-
var conditions = specialKeys.map(function(key) {
50-
var value = query[key]
51-
52-
if (key == '$not') {
53-
return '(NOT ' + convert(path, query[key]) + ')';
54-
} else if (key == '$or' || key == '$and') {
55-
if (query[key].length == 0) {
56-
return key == '$or' ? 'FALSE' : 'TRUE'
57-
} else {
58-
return '('+query[key].map(function(subquery) {return convert(path, subquery) })
59-
.join(key == '$or' ? ' OR ' : ' AND ')+')';
60-
}
61-
} else if (key == '$regex') {
62-
var op = '~';
63-
if (query['$options'] && query['$options'].includes('i')) {
64-
op += '*';
65-
}
66-
return pathToText(path, true) + ' ' + op + ' \'' + stringEscape(value) + '\'';
67-
} else if (key == '$elemMatch') {
68-
return pathToText(path, false) + ' @> \'' + stringEscape(JSON.stringify(query[key])) + '\'::jsonb';
69-
} else if (key == '$in' || key == '$nin') {
70-
return pathToText(path, typeof query[key][0] == 'string') + (key == '$nin' ? ' NOT' : '') + ' IN (' + query[key].map(quote).join(', ') + ')';
71-
} else if (Object.keys(ops).indexOf(key) !== -1) {
72-
var text = pathToText(path, typeof query[key] == 'string')
73-
return text + ops[key] + quote(query[key])
74-
} else {
75-
return convert(path.concat(key.split('.')), query[key]);
76-
}
40+
function convertOp(path, op, value, parent, arrayPaths) {
41+
if (arrayPaths) {
42+
for (var arrPath of arrayPaths) {
43+
if (op.startsWith(arrPath)) {
44+
var subPath = op.split('.')
45+
var innerPath = ['value', subPath.pop()]
46+
var innerText = pathToText(innerPath, typeof value === 'string')
47+
path = path.concat(subPath)
48+
var text = pathToText(path, false)
49+
return 'EXISTS (SELECT * FROM jsonb_array_elements(' + text + ') WHERE ' + innerText + '=' + quote(value) + ')'
50+
}
51+
}
52+
}
53+
switch(op) {
54+
case '$not':
55+
return '(NOT ' + convert(path, value) + ')'
56+
case '$nor':
57+
var notted = value.map((e) => ({ $not: e }));
58+
return convertOp(path, '$and', notted, value, arrayPaths);
59+
case '$or':
60+
case '$and':
61+
if (!Array.isArray(value)) {
62+
throw new Error('$and or $or requires an array.')
63+
}
64+
if (value.length == 0) {
65+
return (op === '$or' ? 'FALSE' : 'TRUE')
66+
} else {
67+
return '(' + value.map((subquery) => convert(path, subquery)).join(op === '$or' ? ' OR ' : ' AND ') + ')'
68+
}
69+
case '$elemMatch':
70+
return pathToText(path, false) + ' @> \'' + stringEscape(JSON.stringify(value)) + '\'::jsonb'
71+
case '$in':
72+
case '$nin':
73+
return pathToText(path, typeof value[0] == 'string') + (op == '$nin' ? ' NOT' : '') + ' IN (' + value.map(quote).join(', ') + ')'
74+
case '$regex':
75+
var op = '~'
76+
if (parent['$options'] && parent['$options'].includes('i')) {
77+
op += '*'
78+
}
79+
return pathToText(path, true) + ' ' + op + ' \'' + stringEscape(value) + '\''
80+
case '$eq':
81+
case '$gt':
82+
case '$gte':
83+
case '$lt':
84+
case '$lte':
85+
case '$ne':
86+
var text = pathToText(path, typeof value == 'string')
87+
return text + ops[op] + quote(value)
88+
case '$type':
89+
var text = pathToText(path, false)
90+
return 'jsonb_typeof(' + text + ')=' + quote(value)
91+
case '$size':
92+
var text = pathToText(path, false)
93+
return 'jsonb_array_length(' + text + ')=' + value
94+
case '$exists':
95+
const key = path.pop();
96+
var text = pathToText(path, false)
97+
return text + ' ? ' + quote(key)
98+
case '$mod':
99+
var text = pathToText(path, true)
100+
if (typeof value[0] != 'number' || typeof value[1] != 'number') {
101+
throw new Error('$mod requires numeric inputs')
102+
}
103+
return 'cast(' + text + ' AS numeric) % ' + value[0] + '=' + value[1];
104+
default:
105+
return convert(path.concat(op.split('.')), value)
106+
}
107+
}
77108

78-
}).join(' and ');
79-
if (specialKeys.length == 1)
80-
return conditions;
81-
else
82-
return '(' + conditions + ')'
83-
} else {
84-
if (path.length == 1) {
85-
return 'TRUE';
86-
}
87-
var text = pathToText(path, typeof query == 'string')
88-
return text + '=' + quote(query);
89-
}
90-
} else {
91-
var text = pathToText(path, typeof query == 'string')
92-
return text + '=' + quote(query);
93-
}
109+
var convert = function (path, query, arrayPaths) {
110+
if (typeof query === 'string' || typeof query === 'boolean' || typeof query == 'number' || Array.isArray(query)) {
111+
var text = pathToText(path, typeof query == 'string')
112+
return text + '=' + quote(query)
113+
}
114+
if (typeof query == 'object') {
115+
// Check for an empty object
116+
if (Object.keys(query).length === 0) {
117+
return 'TRUE'
118+
}
119+
var specialKeys = Object.keys(query).filter(function (key) {
120+
return (path.length === 1) || key in ops || key in otherOps
121+
})
122+
switch (specialKeys.length) {
123+
case 0:
124+
var text = pathToText(path, typeof query == 'string')
125+
return text + '=' + quote(query)
126+
case 1:
127+
const key = specialKeys[0];
128+
return convertOp(path, key, query[key], query, arrayPaths);
129+
default:
130+
return '(' + specialKeys.map(function (key) {
131+
return convertOp(path, key, query[key], query, arrayPaths);
132+
}).join(' and ') + ')'
133+
}
134+
}
94135
}
95136

96-
module.exports = function (fieldName, query) {
97-
return convert([fieldName], query);
98-
};
137+
module.exports = function (fieldName, query, arrays) {
138+
return convert([fieldName], query, arrays || [])
139+
}
99140
module.exports.convertDotNotation = convertDotNotation

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
"directories": {
77
"test": "test"
88
},
9-
"dependencies": {
9+
"dependencies": {},
10+
"devDependencies": {
1011
"chai": "^3.5.0",
1112
"mocha": "^3.2.0"
1213
},
13-
"devDependencies": {},
1414
"repository": {
1515
"type": "git",
1616
"url": "https://github.com/thomas4019/mongo-query-to-postgres-jsonb.git"

0 commit comments

Comments
 (0)