Skip to content

Instantly share code, notes, and snippets.

@kriszyp
Created May 11, 2010 21:14
Show Gist options
  • Save kriszyp/397895 to your computer and use it in GitHub Desktop.
Save kriszyp/397895 to your computer and use it in GitHub Desktop.
/**
* 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