Skip to content
This repository was archived by the owner on May 1, 2025. It is now read-only.

Commit 3c8618d

Browse files
author
Hans Kristian Flaatten
committed
feat(parser): refactor array operator parsing
Refactor operator parsing logic into a separate function which is reused for arrays and normal key value parsing. This also allows multiple entries of operators in addition to `$in` and `$nin` operators inside arrays. The query `?count=>10&count<100` will evaluate to the following: ``` { "count": { "$gt": 10, "$lt": 100, } } ``` BREAKING CHANGE: the new parser will not discriminate agains having both `$in` and `$nin` values for the same key - which is redundant - but still a valid query. Close #20 Signed-off-by: Hans Kristian Flaatten <hans.kristian.flaatten@dnt.no>
1 parent 3e95baf commit 3c8618d

File tree

3 files changed

+276
-84
lines changed

3 files changed

+276
-84
lines changed

examples/test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,16 @@ describe('Example App', function() {
148148
})
149149
.end(done);
150150
});
151+
152+
it('returns places with visits > 40 and < 10,000', function(done) {
153+
app.get(url + '?visits=>40&visits=<10000')
154+
.expect(200)
155+
.expect(function(res) {
156+
assert.equal(res.body.length, 3);
157+
assert.equal(res.body[0].name, 'Norddalshytten');
158+
assert.equal(res.body[1].name, 'Vatnane');
159+
assert.equal(res.body[2].name, 'Selhamar');
160+
})
161+
.end(done);
162+
});
151163
});

index.js

Lines changed: 101 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ module.exports = function MongoQS(opts) {
1717
this.string.toNumber = opts.string.toNumber || true;
1818

1919
this.keyRegex = opts.keyRegex || /^[a-zæøå0-9-_.]+$/i;
20+
this.valRegex = opts.valRegex || /[^a-zæøå0-9-_.* ]/i;
2021
this.arrRegex = opts.arrRegex || /^[a-zæøå0-9-_.]+(\[\])?$/i;
2122

2223
for (var param in this.custom) {
@@ -117,20 +118,86 @@ module.exports.prototype.customAfter = function(field) {
117118
};
118119
};
119120

120-
module.exports.prototype.parseString = function(string) {
121+
module.exports.prototype.parseString = function(string, array) {
122+
var op = string[0] || '';
123+
var eq = string[1] === '=';
124+
var org = string.substr(eq ? 2 : 1) || '';
125+
var val = this.parseStringVal(org);
126+
127+
var ret = {op: op, org: org, value: val};
128+
129+
switch (op) {
130+
case '!':
131+
if (array) {
132+
ret.field = '$nin';
133+
} else if (org === '') {
134+
ret.field = '$exists';
135+
ret.value = false;
136+
} else {
137+
ret.field = '$ne';
138+
}
139+
break;
140+
case '>':
141+
ret.field = eq ? '$gte' : '$gt';
142+
break;
143+
case '<':
144+
ret.field = eq ? '$lte' : '$lt';
145+
break;
146+
case '^':
147+
case '$':
148+
case '~':
149+
ret.field = '$regex';
150+
ret.options = 'i';
151+
ret.value = org.replace(this.valReqex, '');
152+
153+
switch (op) {
154+
case '^':
155+
ret.value = '^' + val;
156+
break;
157+
case '$':
158+
ret.value = val + '$';
159+
break;
160+
}
161+
break;
162+
default:
163+
ret.org = org = op + org;
164+
ret.op = op = '';
165+
ret.value = this.parseStringVal(org);
166+
167+
if (array) {
168+
ret.field = '$in';
169+
} else if (org === '') {
170+
ret.field = '$exists';
171+
ret.value = true;
172+
} else {
173+
ret.field = '$eq';
174+
}
175+
}
176+
177+
ret.parsed = {};
178+
ret.parsed[ret.field] = ret.value;
179+
180+
if (ret.options) {
181+
ret.parsed.$options = ret.options;
182+
}
183+
184+
return ret;
185+
};
186+
187+
module.exports.prototype.parseStringVal = function(string) {
121188
if (this.string.toBoolean && string.toLowerCase() === 'true') {
122189
return true;
123190
} else if (this.string.toBoolean && string.toLowerCase() === 'false') {
124191
return false;
125-
} else if (this.string.toNumber && !isNaN(string)) {
192+
} else if (this.string.toNumber && !isNaN(parseFloat(string, 10))) {
126193
return parseFloat(string, 10);
127194
} else {
128195
return string;
129196
}
130197
};
131198

132199
module.exports.prototype.parse = function(query) {
133-
var op, val;
200+
var val;
134201
var res = {};
135202

136203
for (var key in query) {
@@ -161,71 +228,55 @@ module.exports.prototype.parse = function(query) {
161228
if (this.ops.indexOf('$in') >= 0 && val.length > 0) {
162229
// remove [] at end of key name (unless it has already been removed)
163230
key = key.replace(/\[\]$/, '');
164-
165-
// $in query
166-
if (val[0][0] !== '!') {
167-
res[key] = {$in: val.filter(function(element) {
168-
return element[0] !== '!';
169-
}).map(function(element) {
170-
return this.parseString(element);
171-
}.bind(this))};
172-
173-
// $nin query
174-
} else {
175-
res[key] = {$nin: val.filter(function(element) {
176-
return element[0] === '!';
177-
}).map(function(element) {
178-
return this.parseString(element.substr(1));
179-
}.bind(this))};
231+
res[key] = {};
232+
233+
for (var i = 0; i < val.length; i++) {
234+
if (this.ops.indexOf(val[i][0]) >= 0) {
235+
var parsed = this.parseString(val[i], true);
236+
237+
switch (parsed.field) {
238+
case '$in':
239+
case '$nin':
240+
res[key][parsed.field] = res[key][parsed.field] || [];
241+
res[key][parsed.field].push(parsed.value);
242+
break;
243+
case '$regex':
244+
res[key].$regex = parsed.value;
245+
res[key].$options = parsed.options;
246+
break;
247+
default:
248+
res[key][parsed.field] = parsed.value;
249+
}
250+
} else {
251+
res[key].$in = res[key].$in || [];
252+
res[key].$in.push(this.parseStringVal(val[i]));
253+
}
180254
}
181255
}
182256

183257
continue;
184258
}
185259

260+
// value must be a string
186261
if (typeof val !== 'string') {
187262
continue;
188263
}
189264

265+
// custom functions
190266
if (typeof this.custom[key] === 'function') {
191267
this.custom[key](res, val);
192268

269+
// field exists query
193270
} else if (!val) {
194271
res[key] = { $exists: true };
195272

273+
// query operators
196274
} else if (this.ops.indexOf(val[0]) >= 0) {
197-
op = val.charAt(0);
198-
val = val.substr(1);
199-
200-
res[key] = (function() {
201-
var hasEqual = (val.charAt(0) === '=');
202-
var output = parseFloat((hasEqual ? val.substr(1) : val), 10);
203-
switch (op) {
204-
case '!':
205-
if (val) {
206-
return { $ne: this.parseString(val) };
207-
} else {
208-
return { $exists: false };
209-
}
210-
break;
211-
case '>':
212-
return output ? hasEqual ? { $gte: output } : { $gt: output } : {};
213-
case '<':
214-
return output ? hasEqual ? { $lte: output } : { $lt: output } : {};
215-
default:
216-
val = val.replace(/[^a-zæøå0-9-_.* ]/i, '');
217-
switch (op) {
218-
case '^':
219-
return { $regex: '^' + val, $options: 'i' };
220-
case '$':
221-
return { $regex: val + '$', $options: 'i' };
222-
default:
223-
return { $regex: val, $options: 'i' };
224-
}
225-
}
226-
}.bind(this))();
275+
res[key] = this.parseString(val).parsed;
276+
277+
// equal operator (no operator)
227278
} else {
228-
res[key] = this.parseString(val);
279+
res[key] = this.parseStringVal(val);
229280
}
230281
}
231282
return res;

0 commit comments

Comments
 (0)