Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save deanlandolt/398771 to your computer and use it in GitHub Desktop.
Save deanlandolt/398771 to your computer and use it in GitHub Desktop.
/**
* This module provides querying functionality
*/
exports.jsonQueryCompatible = true;
var operatorMap = {
"=": "eq",
"==": "eq",
">": "gt",
">=": "ge",
"<": "lt",
"<=": "le",
"!=": "ne"
};
var parseQuery = exports.parseQuery = function(/*String|Object*/query, parameters){
var topTerms = [];
var term= {operator:"and", operands: topTerms};
if(typeof query === "object"){
for(var i in query){
topTerms.push({
property: i,
operator: "eq",
operands:[property[i]]
});
}
return topTerms;
}
if(exports.jsonQueryCompatible){
query = query.replace(/%3C=/g,"=le=").replace(/%3E=/g,"=ge=").replace(/%3C/g,"=lt=").replace(/%3E/g,"=gt=");
}
// convert FIQL to normalized call syntax form
query = query.replace(/([\+\*\-:\w%\._]+)(>|<|[<>!]?=([\w]*=)?)([\+\*\-:\w%\._]+|\([\+\*\-:\w%\._,]+\))/g, function(t, property, operator, operatorSuffix, value){
if(operator.length < 3){
if(!operatorMap[operator]){
throw new Error("Illegal operator " + operator);
}
operator = operatorMap[operator];
}
else{
operator = operator.substring(1, operator.length - 1);
}
return operator + '(' + property + "," + value + ")";
});
var leftoverCharacters = query.replace(/(\))|([&\|,])?([\+\*\$\-:\w%\._]*)(\(?)/g,
// |<-delim-- propertyOrValue -(> |<-closedParan->
function(t, closedParan, delim, propertyOrValue, openParan){
if(delim){
if(delim === "&"){
forceOperator("and");
}
if(delim === "|"){
forceOperator("or");
}
}
if(openParan){
var paths = propertyOrValue.split(".");
var newTerm = {
operator: paths[paths.length - 1],
parent: term
};
if(paths.length > 2){
newTerm.property = paths.slice(0, -1);
}
else if(paths.length === 2){
newTerm.property = paths[0];
}
call(newTerm);
}
else if(closedParan){
var isArray = !term.operator;
term = term.parent;
if(!term){
throw new URIError("Closing paranthesis without an opening paranthesis");
}
if(isArray){
term.operands.push(term.operands.pop().operands);
}
}
else if(propertyOrValue){
term.operands.push(stringToValue(propertyOrValue, parameters));
}
return "";
});
if(term.parent){
throw new URIError("Opening paranthesis without a closing paranthesis");
}
if(leftoverCharacters){
// any extra characters left over from the replace indicates invalid syntax
throw new URIError("Illegal character in query string encountered " + leftoverCharacters);
}
function call(newTerm, parent){
newTerm.operands = [];
term.operands.push(newTerm);
term = newTerm;
}
function forceOperator(operator){
if(!term.operator){
term.operator = operator;
}
else if(term.operator !== operator){
var last = term.operands.pop();
call({
operator:operator,
parent: term.parent
});
term.operands.push(last);
}
};
Object.defineProperty(topTerms, "toString", {
enumerable: false,
value: function() {
return "[Query object]"; // FIXME
var qs = "";
for (var i = 0; i < this.length; i++) {
// TODO
}
return qs;
}
});
function removeParentProperty(obj) {
if (!obj) return obj;
if (typeof obj.forEach === "function") {
obj.forEach(function(val) {
removeParentProperty(val);
});
}
else if (typeof obj === "object") {
if ("parent" in obj) delete obj.parent;
for (var key in obj) {
if (typeof obj[key] === "object") {
removeParentProperty(obj[key]);
}
}
}
return obj;
};
return removeParentProperty(topTerms);
}
exports.jsOperatorMap = {
"eq" : "===",
"ne" : "!==",
"le" : "<=",
"ge" : ">=",
"lt" : "<",
"gt" : ">"
}
exports.commonOperatorMap = {
"and" : "&",
"or" : "|",
"eq" : "=",
"ne" : "!=",
"le" : "<=",
"ge" : ">=",
"lt" : "<",
"gt" : ">"
}
exports.operators = {
sort: function(){
var terms = [];
for(var i = 0; i < arguments.length; i++){
var sortAttribute = arguments[i];
var firstChar = sortAttribute.charAt(0);
var term = {attribute: sortAttribute, ascending: true};
if (firstChar == "-" || firstChar == "+") {
if(firstChar == "-"){
term.ascending = false;
}
term.attribute = term.attribute.substring(1);
}
terms.push(term);
}
this.sort(function(a, b){
for (var i = 0; i < terms.length; i++) {
var term = terms[i];
if (a[term.attribute] != b[term.attribute]) {
return term.ascending == a[term.attribute] > b[term.attribute] ? 1 : -1;
}
}
return true; //undefined?
});
return this;
},
"in": filter(function(value, values){
return values.indexOf(value) > -1;
}),
contains: filter(function(array, value){
if(value instanceof Array){
return value.some.call(arguments, function(value){
return array.indexOf(value) > -1;
});
}
else{
return array.indexOf(value) > -1;
}
}),
or: function(){
var items = [];
//TODO: remove duplicates and use condition property
for(var i = 0; i < arguments.length; i++){
items = items.concat(arguments[i].call(this));
}
return items;
},
and: function(){
var items = this;
// TODO: use condition property
for(var i = 0; i < arguments.length; i++){
items = arguments[i].call(items);
}
return items;
},
select: function(first){
if(arguments.length == 1){
return this.map(function(object){
return object[first];
});
}
var args = arguments;
return this.map(function(object){
var selected = {};
for(var i = 0; i < args.length; i++){
var propertyName= args[i];
if(object.hasOwnProperty(propertyName)){
selected[propertyName] = object[propertyName];
}
}
return selected;
});
},
slice: function(){
return this.slice.apply(this, arguments);
}
};
exports.filter = filter;
function filter(condition){
var filter = function(property){
var args = arguments;
return this.filter(function(item){
args[0] = evaluateProperty(item, property);
return condition.apply(this, args);
}, this);
};
filter.condition = condition;
return filter;
};
exports.evaluateProperty = evaluateProperty;
function evaluateProperty(object, property){
if(property.indexOf(".") === -1){
return object[decodeURIComponent(property)];
}
else{
property.split(".").forEach(function(part){
object = object[decodeURIComponent(part)];
});
return object;
}
};
var conditionEvaluator = exports.conditionEvaluator = function(condition){
var jsOperator = exports.jsOperatorMap[term.operator];
if(jsOperator){
js += "(function(item){return item." + term[0] + jsOperator + "parameters[" + (index -1) + "][1];});";
}
else{
js += "operators['" + term.operator + "']";
}
return eval(js);
};
exports.executeQuery = function(query, options, target){
return exports.query(query, options, target);
}
exports.query = function(query, options, target){
if(typeof query === "string" || (typeof query === "object" && !(query instanceof Array))){
query = parseQuery(query, options && options.parameters);
}
function t(){}
t.prototype = exports.operators;
var operators = new t;
// inherit from exports.operators
for(var i in options.operators){
operators[i] = options.operators[i];
}
var parameters = options.parameters || [];
var js = "";
function queryToJS(value){
if(value && typeof value === "object" && !(value instanceof Array)){
var jsOperator = exports.jsOperatorMap[value.operator];
if(jsOperator){
return "(function(){return this.filter(function(item){return item." + value.operands[0] + jsOperator + queryToJS(value.operands[1]) + ";})})";
}else{
return "(function(){return operators['" + value.operator + "'].call(this, " +
value.operands.map(queryToJS).join(",") +
")})";
}
}else{
return JSON.stringify(value);
}
}
var evaluator = eval("(function(target){return " + queryToJS({operator:"and", operands: query}) + ".call(target);})");
if(options.start || options.end){
var totalCount = results.length;
results = results.slice(options.start || 0, (options.end || Infinity) + 1);
results.totalCount = totalCount;
}
return target ? evaluator(target) : evaluator;
}
function throwMaxIterations(){
throw new Error("Query has taken too much computation, and the user is not allowed to execute resource-intense queries. Increase maxIterations in your config file to allow longer running non-indexed queries to be processed.");
}
exports.maxIterations = 10000;
function stringToValue(string, parameters){
switch(string){
case "true": return true;
case "false": return false;
case "null": return null;
default:
// handle arrays
if (string.indexOf(',') > -1) {
var array = [];
string.split(',').forEach(function(x){
array.push(stringToValue(x, parameters));
});
return array;
}
// handle scalars
var number = parseFloat(string, 10);
if(isNaN(number)){
if(string.indexOf(":") > -1){
var date = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(string);
if (date) {
return new Date(Date.UTC(+date[1], +date[2] - 1, +date[3], +date[4],
+date[5], +date[6]));
}
var parts = string.split(":",2);
switch(parts[0]){
case "boolean" : return Boolean(parts[1]);
case "number" : return parseFloat(parts[1], 10);
case "string" : return decodeURIComponent(parts[1]);
case "date" : return new Date(stringToValue(parts[1]));
case "null" : return null;
default:
throw new URIError("Unknown type " + parts[0]);
}
}
if(string.charAt(0) == "$"){
return parameters[parseInt(string.substring(1)) - 1];
}
string = decodeURIComponent(string);
if(exports.jsonQueryCompatible){
if(string.charAt(0) == "'" && string.charAt(string.length-1) == "'"){
return JSON.parse('"' + string.substring(1,string.length-1) + '"');
}
}
return string;
}
return number;
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment