Created
May 11, 2010 21:14
-
-
Save kriszyp/397895 to your computer and use it in GitHub Desktop.
This file contains hidden or 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", | |
"!": "ne", | |
} | |
var parseQuery = exports.parseQuery = function(/*String*/query, parameters){ | |
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%\._]+)/g, function(t, operator, value){ | |
operator = operator ? operator.substring(0, operator.length - 1) : ""; | |
operator = operatorMap[operator] || operator; | |
return "." + operator + '(' + value + ")"; | |
}); | |
if(query.charAt(0)=="?"){ | |
query = query.substring(1); | |
} | |
var topTerms = []; | |
var term= {operator:"and", values: topTerms}; | |
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){ | |
term = term.parent; | |
if(!term){ | |
throw new URIError("Closing paranthesis without an opening paranthesis"); | |
} | |
} | |
else if(propertyOrValue){ | |
term.values.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.values = []; | |
term.values.push(newTerm); | |
term = newTerm; | |
} | |
function forceOperator(operator){ | |
if(!term.operator){ | |
term.operator = operator; | |
} | |
else if(term.operator !== operator){ | |
var last = term.values.pop(); | |
call({ | |
operator:operator, | |
parent: term.parent | |
}); | |
term.values.push(last); | |
} | |
} | |
Object.defineProperty(topTerms, "toString", { | |
enumerable: false, | |
value: function() { | |
var qs = "?"; | |
for (var i = 0; i < this.length; i++) { | |
var term = this[i]; | |
qs += term.conjunction || ""; | |
if (term.type == "comparison") { | |
qs += term.name + term.comparator + term.value; | |
} | |
else if (term.type == "call") { | |
qs += term.name + "(" + term.parameters + ")" | |
} | |
else { | |
// FIXME should we throw here? | |
} | |
} | |
if (qs == "?") return ""; | |
return qs; | |
} | |
}); | |
return topTerms; | |
} | |
exports.QueryFunctions = function(){ | |
} | |
exports.QueryFunctions.prototype = { | |
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": function(){ | |
return Array.prototype.indexOf.call(arguments, this.valueOf()) > -1; | |
}, | |
contains: function(value){ | |
if(arguments.length === 1){ | |
return this.indexOf(value) > -1; | |
} | |
else{ | |
var self = this; | |
return Array.prototype.some.call(arguments, function(value){ | |
return self.indexOf(value) > -1; | |
}); | |
} | |
}, | |
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.executeQuery = function(query, options, target){ | |
if(typeof query === "string"){ | |
query = parseQuery(query, options && options.parameters); | |
} | |
var functions = options.functions || exports.QueryFunctions.prototype; | |
var inComparision = false; | |
var js = ""; | |
query.forEach(function(term){ | |
if(term.type == "comparison"){ | |
if(!options){ | |
throw new Error("Values must be set as parameters on the options argument, which was not provided"); | |
} | |
if(!inComparision){ | |
inComparision = true; | |
js += "target = target.filter(function(item){return "; | |
} | |
else{ | |
js += term.conjunction + term.conjunction; | |
} | |
var index = (options.parameters = options.parameters || []).push(term.value); | |
if(term.comparator == "="){ | |
term.comparator = "=="; | |
} | |
js += "item." + (term.name instanceof Array ? term.name.join(".") : term.name) + term.comparator + "options.parameters[" + (index -1) + "]"; | |
} | |
else if(term.type == "call"){ | |
if(inComparision){ | |
js += "});"; | |
inComparision = false; | |
} | |
if(term.name instanceof Array){ | |
var path = term.name; | |
var index = (options.parameters = options.parameters || []).push(term.parameters); | |
js += "target = target.filter(function(item){return " + | |
"functions['" + path[path.length - 1] + "'].apply(" + "item." + path.slice(0, -1).join(".") + ",options.parameters[" + (index -1) + "]);});"; | |
} | |
else if(functions[term.name]){ | |
var index = (options.parameters = options.parameters || []).push(term.parameters); | |
js += "target = functions." + term.name + ".apply(target,options.parameters[" + (index -1) + "]);"; | |
} | |
else{ | |
throw new URIError("Invalid query syntax, " + term.name + " not implemented"); | |
} | |
} | |
else{ | |
throw new URIError("Invalid query syntax, unknown type"); | |
} | |
}); | |
if(inComparision){ | |
js += "});"; | |
first = false; | |
} | |
var results = eval(js + "target;"); | |
if(options.start || options.end){ | |
var totalCount = results.length; | |
results = results.slice(options.start || 0, (options.end || Infinity) + 1); | |
results.totalCount = totalCount; | |
} | |
return results; | |
} | |
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; | |
} | |
}; | |
function convertComparator(comparator){ | |
switch(comparator){ | |
case "=lt=" : return "<"; | |
case "=gt=" : return ">"; | |
case "=le=" : return "<="; | |
case "=ge=" : return ">="; | |
case "==" : return "="; | |
} | |
return comparator; | |
} | |
function convertPropertyName(property){ | |
if(property.indexOf(".") > -1){ | |
return property.split(".").map(function(part){ | |
return decodeURIComponent(part); | |
}); | |
} | |
return decodeURIComponent(property); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment