-
-
Save deanlandolt/398771 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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