sluicebox
Version:
Quickly and elegantly add search, sort and pagination to your plural resource endpoints.
329 lines (316 loc) • 9.38 kB
JavaScript
(function() {
var Errors, isEmpty, numerify, ref, regify, trimStart;
ref = require('lodash'), isEmpty = ref.isEmpty, trimStart = ref.trimStart;
if (trimStart == null) {
trimStart = require('lodash').trimLeft;
}
Errors = require('./errors');
regify = function(it) {
return it.replace(/(\\|\[|\^|\$|\.|\||\?|\*|\+|\(|\))/g, '\\$1');
};
numerify = function(it) {
var num;
num = parseFloat(it, 10);
if (num.toString() === it) {
return num;
} else {
return it;
}
};
module.exports = function(field_for, value_for) {
var ParseContext, group_handler, handler_for, predicate_handler;
ParseContext = (function() {
function ParseContext(query1, idx) {
this.query = query1;
this.idx = idx != null ? idx : 0;
this.buffer = '';
this.state = ['normal'];
this.state.peek = (function(_this) {
return function() {
return _this.state[_this.state.length - 1];
};
})(this);
}
ParseContext.prototype.process_buffer = function() {
var value;
value = this.buffer;
this.buffer = '';
this.idx--;
return {
type: 'string',
value: value
};
};
ParseContext.prototype.next_token = function() {
var char;
while (this.idx < this.query.length) {
char = this.query[this.idx++];
switch (this.state.peek()) {
case 'escape':
this.state.pop();
this.buffer += char;
break;
case 'quote':
switch (char) {
case '\\':
this.state.push('escape');
break;
case '\'':
this.state.pop();
break;
default:
this.buffer += char;
}
break;
default:
switch (char) {
case '\\':
this.state.push('escape');
break;
case '\'':
this.state.push('quote');
break;
case '(':
if (this.buffer.length) {
return this.process_buffer();
}
return {
type: 'group_start'
};
case ')':
if (this.buffer.length) {
return this.process_buffer();
}
return {
type: 'group_end'
};
case ';':
if (this.buffer.length) {
return this.process_buffer();
}
return {
type: 'and'
};
case '|':
if (this.buffer.length) {
return this.process_buffer();
}
return {
type: 'or'
};
case ':':
if (this.buffer.length) {
return this.process_buffer();
}
return {
type: 'equal'
};
case '!':
if (this.buffer.length) {
return this.process_buffer();
}
return {
type: 'not'
};
case '*':
if (this.buffer.length) {
return this.process_buffer();
}
return {
type: 'contains'
};
case '^':
if (this.buffer.length) {
return this.process_buffer();
}
return {
type: 'starts_with'
};
case '$':
if (this.buffer.length) {
return this.process_buffer();
}
return {
type: 'ends_with'
};
case '<':
if (this.buffer.length) {
return this.process_buffer();
}
return {
type: 'less_than'
};
case '>':
if (this.buffer.length) {
return this.process_buffer();
}
return {
type: 'greater_than'
};
default:
this.buffer += char;
}
}
}
if (!this.buffer.length) {
return null;
}
return {
type: 'string',
value: this.buffer
};
};
return ParseContext;
})();
predicate_handler = function(token, context) {
var current, field, is_contains, is_ends_with, is_equal, is_greater_than, is_less_than, is_not, is_starts_with, obj, obj1, operator, pred, value;
field = field_for(token.value);
operator = context.next_token();
is_equal = false;
is_not = false;
is_contains = false;
is_starts_with = false;
is_ends_with = false;
is_less_than = false;
is_greater_than = false;
current = operator;
while (current.type !== 'string' && context.idx < context.query.length) {
switch (current.type) {
case 'equal':
is_equal = true;
break;
case 'not':
is_not = true;
break;
case 'contains':
is_contains = true;
break;
case 'starts_with':
is_starts_with = true;
break;
case 'ends_with':
is_ends_with = true;
break;
case 'less_than':
is_less_than = true;
break;
case 'greater_than':
is_greater_than = true;
}
current = context.next_token();
}
pred = {};
value = value_for(field, current.value);
if (is_contains) {
value = RegExp("" + (regify(value)));
if (is_not) {
value = {
$not: value
};
}
} else if (is_starts_with) {
value = RegExp("^" + (regify(value)));
if (is_not) {
value = {
$not: value
};
}
} else if (is_ends_with) {
value = RegExp((regify(value)) + "$");
if (is_not) {
value = {
$not: value
};
}
} else if (is_less_than) {
value = (
obj = {},
obj["" + (is_equal ? '$lte' : '$lt')] = numerify(value),
obj
);
if (is_not) {
value = {
$not: value
};
}
} else if (is_greater_than) {
value = (
obj1 = {},
obj1["" + (is_equal ? '$gte' : '$gt')] = numerify(value),
obj1
);
if (is_not) {
value = {
$not: value
};
}
} else if (is_not) {
value = {
'$ne': numerify(value)
};
} else {
value = numerify(value);
}
pred[field] = value;
return pred;
};
group_handler = function(token, context) {
var current, i, len, name, operator, predicate, predicates, result, value;
operator = 'and';
current = context.next_token();
predicates = [];
while (current !== null && current.type !== 'group_end' && context.idx < context.query.length) {
if (current.type === 'and') {
} else if (current.type === 'or') {
operator = '$or';
} else {
predicates.push(handler_for(current.type)(current, context));
}
current = context.next_token();
}
if (predicates.length === 1) {
return predicates[0];
}
result = {};
if (operator === 'and') {
for (i = 0, len = predicates.length; i < len; i++) {
predicate = predicates[i];
for (name in predicate) {
value = predicate[name];
result[name] = value;
}
}
} else {
result[operator] = predicates;
}
return result;
};
handler_for = function(type) {
switch (type) {
case 'string':
return predicate_handler;
case 'group_start':
return group_handler;
}
};
return function(query, required, valid, invalid) {
var context, err, ref1, result, token;
if (invalid == null) {
ref1 = [false, required, valid], required = ref1[0], valid = ref1[1], invalid = ref1[2];
}
query = "(" + (trimStart(query != null ? query : '')) + ")";
try {
context = new ParseContext(query);
token = context.next_token();
result = handler_for(token.type)(token, context);
if (required && isEmpty(result)) {
return invalid(Errors.wrap('query', 'A search query is required.'));
}
return valid(result);
} catch (_error) {
err = _error;
return invalid(Errors.wrap('query', 'The search query is invalid.'));
}
};
};
}).call(this);