beetle.js
Version:
Beetle is a data manager for Javascript. The goal is to be able to work with data as easy as Entity Framework and LINQ.
1,054 lines (999 loc) • 493 kB
JavaScript
/**
* Beetle module.
* Using UMD pattern.
* @module beetle
*/
(function (root, factory) {
var deps = {
jQuery: root.$,
angularjs: root.angular,
ko: root.ko,
Q: root.Q
};
if (typeof exports === "object") {
var node;
if (typeof window == 'undefined') {
try {
var http = require("http");
var https = require("https");
node = { http: http, https: https };
} catch (e) { }
}
var angular;
try {
var aCore = require("@angular/core");
var aHttp = require("@angular/http");
var http = aCore.ReflectiveInjector.resolveAndCreate([
aHttp.Http, aHttp.BrowserXhr,
{ provide: aHttp.ConnectionBackend, useClass: aHttp.XHRBackend },
{ provide: aHttp.RequestOptions, useClass: aHttp.BaseRequestOptions },
{ provide: aHttp.ResponseOptions, useClass: aHttp.BaseResponseOptions },
{ provide: aHttp.XSRFStrategy, useValue: new aHttp.CookieXSRFStrategy() }
]).get(aHttp.Http);
angular = { http: http, Request: aHttp.Request, Headers: aHttp.Headers };
} catch (e) { }
module.exports = factory(root, deps.jQuery, deps.angularjs, deps.ko, deps.Q, node, angular);
return module.exports;
}
else if (typeof define === "function" && define.amd) {
var modules = [];
for (var p in deps) {
if (require.specified(p)) {
modules.push(p);
}
}
define(modules, function () {
for (var i = 0; i < arguments.length; i++) {
var mdl = arguments[i];
if (mdl) deps[modules[i]] = mdl;
}
root.beetle = factory(root, deps.jQuery, deps.angularjs, deps.ko, deps.Q);
});
}
else {
root.beetle = factory(root, deps.jQuery, deps.angularjs, deps.ko, deps.Q);
return root.beetle;
}
})(this, function (root, $, angularjs, ko, Q, node, angular) {
"use strict";
/**
* Helper functions. We are trying not to use ECMA 5, so we polyfill some methods.
* @namespace helper
*/
var helper = {
/**
* Creates an assert instance to work with, a shortcut.
* @example
* helper.assertPrm(prm, 'prm').isArray().check()
* @param {any} value - The value of parameter.
* @param {string} name - The name of the parameter.
* @returns {Assert} Assert instance.
*/
assertPrm: function (value, name) {
return new Assert(value, name);
},
/**
* Combines first object's properties with second object's properties on a new object.
* @param {Object} obj1 - The first object.
* @param {Object} obj2 - The second object.
* @returns {Object} New object containing all properties from both objects.
*/
combine: function (obj1, obj2) {
if (obj1 == obj2) return obj1;
var obj;
if (obj1 != null) {
obj = {};
for (var p1 in obj1) {
obj[p1] = obj1[p1];
}
}
if (obj2 != null) {
obj = obj || {};
for (var p2 in obj2) {
var v1 = obj[p2];
var v2 = obj2[p2];
var v = Assert.isTypeOf(v1, 'object') && Assert.isTypeOf(v2, 'object') ? helper.combine(v1, v2) : v2;
obj[p2] = v;
}
}
return obj;
},
/**
* Extends objMain with objExt's properties.
* @param {Object} objMain - The main object.
* @param {Object} objExt - Object to extend with.
* @returns {Object} objMain is returned.
*/
extend: function (objMain, objExt) {
if (objMain != null && objExt != null) {
for (var p in objExt) {
if (!objMain.hasOwnProperty(p))
objMain[p] = objExt[p];
}
}
return objMain;
},
/**
* Checks if the given two are equal. if parameters are both objects, recursively controls their properties too.
* @param {Object} obj1 - The first object.
* @param {Object} obj2 - The second object.
* @returns {boolean} True when two objects are equal, otherwise false.
*/
objEquals: function (obj1, obj2) {
if (obj1 == obj2)
return true;
if (obj1 == null || obj2 == null)
return false;
if (Assert.isObject(obj1) && Assert.isObject(obj2)) {
var count1 = 0;
var count2 = 0;
for (var p in obj1) {
if (!(p in obj2)) return false;
if (helper.getValue(obj1, p) != helper.getValue(obj2, p)) return false;
count1++;
}
for (var p2 in obj2) count2++;
return count1 == count2;
}
return false;
},
/**
* Returns string case option for current operation context.
* @param {StringOptions} options - String options for the context.
* @returns {boolean} True when given options' isCaseSensitive is true (or null and global options' isCaseSensitive is true), otherwise false.
*/
isCaseSensitive: function (options) {
var isCaseSensitive = options && options.isCaseSensitive;
return isCaseSensitive == null ? settings.isCaseSensitive : isCaseSensitive;
},
/**
* Returns whitespace ignore option for current operation context.
* @param {StringOptions} options - String options for the context.
* @returns {boolean} True when given options' ignoreWhiteSpaces is true (or null and global options' ignoreWhiteSpaces is true), otherwise false.
*/
ignoreWhiteSpaces: function (options) {
var ignoreWhiteSpaces = options && options.ignoreWhiteSpaces;
return ignoreWhiteSpaces == null ? settings.ignoreWhiteSpaces : ignoreWhiteSpaces;
},
/**
* Applies current operation context string options to given parameter.
* @param {StringOptions} options - String options for the context.
* @returns {string} Modified string (using the options).
*/
handleStrOptions: function (str, options) {
if (str == null || typeof str !== "string") return str;
if (!helper.isCaseSensitive(options)) {
str = str.replace("İ", "i");
str = str.toLowerCase();
str = str.replace("ı", "i");
}
return helper.ignoreWhiteSpaces(options) ? str.trim() : str;
},
/**
* Compares two objects. Uses given options when necessary.
* @param {Object} obj1 - The first object.
* @param {Object} obj2 - The second object.
* @param {bool} isStrict - Use strict comparing (===).
* @param {StringOptions} options - String options for the context.
* @returns {boolean} True when two values are equal (options will be used for string values).
*/
equals: function (obj1, obj2, isStrict, options) {
if (typeof obj1 === 'string' && typeof obj2 === 'string') {
obj1 = helper.handleStrOptions(obj1, options);
obj2 = helper.handleStrOptions(obj2, options);
}
if (obj1 != null && obj2 != null && core.dataTypes.date.isValid(obj1) && core.dataTypes.date.isValid(obj2)) {
obj1 = obj1.valueOf();
obj2 = obj2.valueOf();
}
return isStrict ? obj1 === obj2 : obj1 == obj2;
},
/**
* Format string using given arguments. %1 and {1} format can be used for placeholders.
* @param {string} string - String to format.
* @param {...string} params - Values to replace.
* @returns {string} Formatted string.
*/
formatString: function (string, params) {
var args = arguments;
var pattern1 = RegExp("%([0-" + (arguments.length - 1) + "])", "g");
var pattern2 = RegExp("{([0-" + (arguments.length - 2) + "])}", "g");
return string
.replace(pattern1, function (match, index) {
return args[Number(index) + 1] || '';
})
.replace(pattern2, function (match, index) {
return args[Number(index) + 1] || '';
});
},
/**
* Finds the index of the given item in the array.
* @param {any[]} array - Array to search.
* @param {any} item - Item to find.
* @param {number=} index - Start index.
* @returns {number} Found index. If the item could not be found returns '-1'.
*/
indexOf: function (array, item, index) {
for (var i = index || 0; i < array.length; i++)
if (array[i] === item) return i;
return -1;
},
/**
* Calls given callback with item and current index parameters for each item in the array.
* @param {any[]} array - Array to iterate.
* @param {forEachCallback} callback - Method to call for each item.
*/
forEach: function (array, callback) {
for (var i = 0; i < array.length; i++) {
var obj = array[i];
callback.call(null, obj, i);
}
},
/**
* Iterate objects properties and skips ones starting with '$'.
* @param {Object} object - Object to iterate.
* @param {forEachPropertyCallback} callback - Method to call for each property.
*/
forEachProperty: function (object, callback) {
for (var p in object) {
if (p[0] == '$') continue;
var v = object[p];
if (Assert.isFunction(v)) continue;
callback(p, v);
}
},
/**
* Finds given item in the array.
* When property is given, looks item's property value, otherwise compares item's itself.
* @param {any[]} array - Array to search.
* @param {any} value - Value to find.
* @param {string=} property - Property to look for the value.
* @returns {any} When value is found; if property is provided, the array item containing the given value, otherwise value itself. When not found, null.
*/
findInArray: function (array, value, property) {
for (var i = 0; i < array.length; i++)
if (property) {
if (array[i][property] === value) return array[i];
} else if (array[i] === value) return value;
return null;
},
/**
* Copies array items that match the given conditions to another array and returns the new array.
* @param {any[]} array - The array to filter.
* @param {string|predicateFunction} predicate - A function to test each element for a condition (can be string expression).
* @returns {any[]} New array with filtered items.
*/
filterArray: function (array, predicate) {
var retVal = [];
for (var i = 0; i < array.length; i++) {
var item = array[i];
if (predicate(item) === true)
retVal.push(item);
}
return retVal;
},
/**
* Removes the item from given array.
* @param {any[]} array - The array to remove item from.
* @param {any} item - Item to remove.
* @param {string=} property - Property to look for the value.
* @returns {number[]} Removed item indexes.
*/
removeFromArray: function (array, item, property) {
var indexes = [];
this.forEach(array, function (current, index) {
if (property) {
if (current[property] === item) indexes.push(index);
} else if (current === item) indexes.push(index);
});
for (var i = indexes.length - 1; i >= 0; i--)
array.splice(indexes[i], 1);
return indexes.length;
},
/**
* Creates a new array with the results of calling the provided function on every element in the given array.
* @param {any[]} array - The array to map.
* @param {mapCallback} callback - Function that produces new element.
* @returns {any[]} New array with mapped values.
*/
mapArray: function (array, callback) {
var retVal = [];
for (var i = 0; i < array.length; i++) {
var item = array[i];
retVal.push(callback.call(item, item, i));
}
return retVal;
},
/**
* Creates a GUID string with "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" format.
* @returns {string} Newly generated GUID.
*/
createGuid: function () {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
},
/**
* Creates string representation of given function with arrow syntax.
* @param {function} func - The function.
* @returns {string} Arrow style code for given function.
*/
funcToLambda: function (func) {
helper.assertPrm(func, "func").isFunction().check();
var f = func.toString().replace(/function(.*?){/g, "$1=>").replace(/{|}|;|return /g, "").trim();
var l1 = f.indexOf("=>");
var p = f.substr(0, l1).trim();
f = f.substr(l1 + 2).trim();
var xs = f.split(",");
var s = [];
for (var i = 0; i < xs.length; i++) {
var x = xs[i];
var ps = x.split(":");
if (ps.length > 1) {
s.push(ps[1].trim() + " as " + ps[0].trim());
}
else s.push(x.trim());
}
f = s.join(", ");
return p ? (p + " => " + f) : f;
},
/**
* Finds and returns function name. Works for ES6 classes too.
* @param {function} func - The function (or class).
* @returns {string} Name of the given function.
*/
getFuncName: function (func) {
var funcNameRegex = /function (.*?)[\s|\(]|class (.*?)\s|\{/;
var results = funcNameRegex.exec(func.toString());
return results[1] || results[2];
},
/**
* Implements prototypal inheritance. Uses a "Function" instance to avoid unnecessary instantiations.
* @param {function} derivedClass - Deriving type.
* @param {function} baseClass - Base type.
*/
inherit: function (derivedClass, baseClass) {
var f = new Function();
f.prototype = baseClass.prototype;
derivedClass.prototype = new f();
derivedClass.prototype.constructor = derivedClass;
derivedClass.baseClass = baseClass.prototype;
},
/**
* Reads property of value, used when we are not sure if property is observable.
* @param {Object} object - Deriving type.
* @param {string} property - Property path. Can be a chain like "address.city.name".
* @returns {any} Value of property (undefined when a property cannot be found).
*/
getValue: function (object, propertyPath) {
if (object == null) return undefined;
var op = settings.getObservableProvider();
// split propertyPath path and read every items value
var paths = propertyPath.trim().split('.');
var retVal = object;
for (var i = 0; i < paths.length; i++) {
retVal = getValue(retVal, paths[i]);
if (retVal == null) break;
}
return retVal;
function getValue(o, p) {
// get tracker
var tracker = o.$tracker;
// if o is an entity, get value from its tracker
if (tracker)
return tracker.getValue(p);
if (op.isObservable(o, p)) // if o's p is observable get value from default observable provider
return op.getValue(o, p);
var value = o[p];
return value; // if its not observable get value of p
}
},
/**
* Gets localized value for given name using "settings.localizeFunc" function.
* @param {string} resourceName - Resource name.
* @param {string} altValue - Alternative value to use when resource cannot be found.
* @returns {string} Value for the given resource name.
*/
getResourceValue: function (resourceName, altValue) {
var localizeFunc = settings.getLocalizeFunction();
return (localizeFunc && resourceName && localizeFunc(resourceName)) || altValue;
},
/**
* Creates validation error object using given parameters.
* @param {any} entity - Entity containing invalid value.
* @param {any} value - Invalid value itself.
* @param {string} property - Property containing invalid value.
* @param {string} message - Validation message.
* @param {Validator} validator - Validator instance.
* @returns {Object} Validation error object.
*/
createValidationError: function (entity, value, property, message, validator) {
return { message: message, entity: entity, validator: validator, value: value, property: property };
},
/**
* Creates error object by formatting provided message and populates with given object's values.
* @param {string} message - Error message.
* @param {string[]} arg1 - Message format arguments.
* @param {Object=} arg2 - Extra informations, properties will be attached to error object.
* @returns {Error} Error object.
*/
createError: function (message, arg1, arg2) {
var args = null, obj = arg2;
if (Assert.isArray(arg1)) args = arg1;
else if (Assert.isObject(arg1)) obj = arg1;
if (args && args.length > 0) {
args.splice(0, 0, message);
message = helper.formatString.apply(null, args);
}
var retVal = new Error(message);
if (obj) {
for (var p in obj)
retVal[p] = obj[p];
}
events.error.notify(retVal);
return retVal;
},
/**
* Updates foreign keys of given navigation property with new values.
* @param {any} entity - The entity.
* @param {NavigationProperty} navProperty - The navigation property.
* @param {Object} newValue - Value of the navigation property.
*/
setForeignKeys: function (entity, navProperty, newValue) {
for (var i = 0; i < navProperty.foreignKeyNames.length; i++) {
// We get each related foreign key for this navigation property.
var fk = navProperty.foreignKeyNames[i];
var tracker = entity.$tracker;
if (newValue) {
// When foreign key is built with more than one column, we presume foreign key-primary key order is same
// Example:
// When we create an association between Product and Supplier using Name and Location fields
// we presume Supplier's corresponding primary keys are in exactly same order.
var k = newValue.$tracker.entityType.keys[i];
var v = newValue.$tracker.getValue(k.name);
tracker.setValue(fk, v);
} else {
var fkp = helper.findInArray(tracker.entityType.dataProperties, fk, 'name');
tracker.setValue(fk, fkp.getDefaultValue());
}
}
},
/**
* Creates an array and overrides methods to provide callbacks on array changes.
* @param {any[]} initial - Initial values for the array.
* @param {Object} object - Owner object of the array.
* @param {NavigationProperty} property - Navigation property metadata.
* @param {arrayChangeCallback} after - Array change callback.
* @returns {TrackableArray} Trackable array, an array with change events.
*/
createTrackableArray: function (initial, object, property, after) {
var array = initial || [];
array.object = object;
array.property = property;
array.after = after;
array.changing = new core.Event(property + "ArrayChanging", array);
array.changed = new core.Event(property + "ArrayChanged", array);
array.pop = function () {
var items = [this[this.length - 1]];
// call base method
this.changing.notify({ added: [], removed: items });
var retVal = Array.prototype.pop.call(this);
this.after(this.object, this.property, this, items, null);
this.changed.notify({ added: [], removed: items });
return retVal;
};
array.push = function (items) {
this.changing.notify({ added: arguments, removed: [] });
beforeAdd(arguments, this);
var retVal = Array.prototype.push.apply(this, arguments);
this.after(this.object, this.property, this, null, arguments);
this.changed.notify({ added: arguments, removed: [] });
return retVal;
};
array.unshift = function (items) {
this.changing.notify({ added: arguments, removed: [] });
beforeAdd(arguments, this);
var retVal = Array.prototype.unshift.apply(this, arguments);
this.after(this.object, this.property, this, null, arguments);
this.changed.notify({ added: arguments, removed: [] });
return retVal;
};
array.shift = function () {
var items = [this[0]];
this.changing.notify({ added: [], removed: items });
var retVal = Array.prototype.shift.call(this);
this.after(this.object, this.property, this, items, null);
this.changed.notify({ added: [], removed: items });
return retVal;
};
array.splice = function (start, count) {
count = count || this.length;
var addedItems = null;
if (arguments.length > 2)
addedItems = Array.prototype.slice.call(arguments).slice(2); // convert arguments to array then slice
var removedItems = this.slice(start, start + count);
this.changing.notify({ added: addedItems, removed: removedItems });
if (addedItems)
beforeAdd(addedItems, this);
var retVal = Array.prototype.splice.apply(this, arguments);
this.after(this.object, this.property, this, removedItems, addedItems);
this.changed.notify({ added: addedItems, removed: removedItems });
return retVal;
};
array.remove = function (items) {
var removed = [];
this.changing.notify({ added: [], removed: arguments });
var that = this;
helper.forEach(arguments, function (item) {
var index = helper.indexOf(that, item);
if (index >= 0) {
Array.prototype.splice.call(that, index, 1);
removed.push(item);
}
});
this.after(this.object, this.property, this, removed, null);
this.changed.notify({ added: [], removed: removed });
return removed;
};
/**
* Loads the navigation property using EntityManager.
* @param {string[]} expands - Expand navigations to apply when loading navigation property.
* @param {Object} resourceName - Resource name to query entities.
* @param {queryOptions} options - Query options.
* @param {successCallback=} successCallback - Success callback function.
* @param {errorCallback=} errorCallback - Error callback function.
* @returns {Promise} A Promise when available, otherwise return value of the AjaxProvider.
*/
array.load = function (expands, resourceName, options, successCallback, errorCallback) {
return this.object.$tracker.loadNavigationProperty(this.property.name, expands, resourceName, options, successCallback, errorCallback);
};
function beforeAdd(added, instance) {
var o = instance.object;
var p = instance.property;
if (p) {
var handleUnmappedProperties;
if (o.$tracker && o.$tracker.manager)
handleUnmappedProperties = o.$tracker.manager.handleUnmappedProperties;
if (handleUnmappedProperties == null) handleUnmappedProperties = settings.handleUnmappedProperties;
if (Assert.isInstanceOf(p, metadata.NavigationProperty))
helper.forEach(added, function (a) { p.checkAssign(a); });
else if (Assert.isInstanceOf(p, metadata.DataProperty))
helper.forEach(added, function (a, i) { added[i] = p.handle(a); });
else if (handleUnmappedProperties === true)
helper.forEach(added, function (a, i) { added[i] = core.dataTypes.handle(a); });
}
}
return array;
},
/**
* Creates a new array with the results of evaluating provided expression on every element in the given array.
* @param {any[]} array - Array to run projection on.
* @param {string} exp - Projection expression.
* @param {QueryContext} queryContext - Query execution context.
* @returns {any[]} Projected new object.
*/
runSelectExp: function (array, exp, queryContext) {
if (array.length == 0) return array;
exp = libs.jsep(exp);
var exps = exp.type === 'Compound' ? exp.body : [exp];
var projector = helper.jsepToProjector(exps, queryContext);
return helper.mapArray(array, projector);
},
/**
* Converts parsed javascript expression (jsep) to OData format query string.
* @param {Object} exp - Jsep expression (tokenized).
* @param {QueryContext} queryContext - Query execution context.
* @param {Object=} firstExp - First evaluated expression (for internal use only).
* @returns {string} OData query string.
*/
jsepToODataQuery: function (exp, queryContext, firstExp) {
firstExp = firstExp || exp;
if (!queryContext) queryContext = { aliases: [] };
else if (!queryContext.aliases) queryContext.aliases = [];
if (exp.type == 'LogicalExpression' || exp.type == 'BinaryExpression') {
if (exp.operator == '=>') {
if (exp != firstExp)
throw helper.createError(i18N.odataDoesNotSupportAlias);
queryContext.aliases.push({ alias: exp.left.name, value: null });
var r = helper.jsepToODataQuery(exp.right, queryContext, firstExp);
if (exp != firstExp)
queryContext.aliases.pop();
return r;
}
var op = enums.langOperators.find(exp.operator).oData;
if (!op) throw helper.createError(i18N.operatorNotSupportedForOData, [exp.operator], { expression: exp });
return '(' + helper.jsepToODataQuery(exp.left, queryContext, firstExp) + ' ' + op + ' ' + helper.jsepToODataQuery(exp.right, queryContext, firstExp) + ')';
}
else if (exp.type == 'UnaryExpression')
return exp.operator + helper.jsepToODataQuery(exp.argument, queryContext, firstExp);
else if (exp.type == 'Identifier') {
var n = exp.name;
var val = undefined;
var isPrm = false;
if (n[0] == '@') {
isPrm = true;
n = n.slice(1);
}
if (queryContext.expVarContext && queryContext.expVarContext[n] !== undefined)
val = queryContext.expVarContext[n];
else if (queryContext.varContext && queryContext.varContext[n] !== undefined)
val = queryContext.varContext[n];
if (val !== undefined)
return core.dataTypes.toODataValue(val);
if (isPrm)
throw helper.createError(i18N.unknownParameter, [n], { expression: exp, queryContext: queryContext });
var a = helper.findInArray(queryContext.aliases, n, 'alias');
if (a) return a.value;
return n;
}
else if (exp.type == 'Literal')
return core.dataTypes.toODataValue(exp.value);
else if (exp.type == 'MemberExpression') {
if (queryContext.currentAlias && exp.object.name == queryContext.currentAlias.alias)
return exp.property.name;
else {
var ali = helper.findInArray(queryContext.aliases, exp.object.name, 'alias'), o;
if (ali) o = ali.value;
else o = helper.jsepToODataQuery(exp.object, queryContext, firstExp);
if (o && o[exp.property.name] !== undefined)
return core.dataTypes.toODataValue(o[exp.property.name]);
return o ? o + '/' + exp.property.name : exp.property.name;
}
}
else if (exp.type == 'Compound') {
var sts = [];
for (var i = 0; i < exp.body.length; i++) {
var st = exp.body[i];
var s = helper.jsepToODataQuery(st, queryContext);
var ls = s.toLowerCase();
if (ls == 'desc' || ls == 'asc') {
if (sts.length == 0)
throw helper.createError(i18N.invalidStatement, { expression: exp, statement: st });
sts[sts.length - 1] += ' ' + s;
}
else if (ls == 'as') {
if (sts.length == 0 || exp.body.length < i + 1)
throw helper.createError(i18N.invalidStatement, { expression: exp, statement: st });
sts[sts.length - 1] += ' as ' + exp.body[i + 1].name;
i++;
}
else sts.push(s);
}
return sts.join(', ');
}
else if (exp.type == 'CallExpression') {
var argList = exp.arguments, args = [], alias = null;
if (argList.length == 1 && argList[0] && argList[0].type == 'BinaryExpression' && argList[0].operator == '=>') {
alias = { alias: argList[0].left.name };
alias.value = alias.alias;
argList = [argList[0].right];
}
if (alias) {
queryContext.currentAlias = alias;
queryContext.aliases.push(alias);
}
for (var j = 0; j < argList.length; j++) {
var arg = argList[j];
if (arg != null)
args.push(helper.jsepToODataQuery(arg, queryContext, firstExp));
}
var funcName;
if (exp.callee.type == 'MemberExpression') {
args.splice(0, 0, helper.jsepToODataQuery(exp.callee.object, queryContext, firstExp));
funcName = exp.callee.property.name;
}
else funcName = exp.callee.name;
var func = querying.queryFuncs.getFunc(funcName);
if (func.needsAlias == true) {
if (alias)
args.splice(0, 0, alias);
else throw helper.createError(i18N.functionNeedsAlias, [funcName], { expression: exp });
}
var retVal = func.toODataFunction.apply(func, args);
if (alias) {
queryContext.currentAlias = null;
queryContext.aliases.pop();
}
return retVal;
}
throw helper.createError(i18N.unknownExpression, { expression: exp });
},
/**
* Converts parsed javascript expression (jsep) to Beetle format query string.
* @param {Object} exp - Jsep expression (tokenized).
* @param {QueryContext} queryContext - Query execution context.
* @param {Object=} firstExp - First evaluated expression (for internal use only).
* @returns {string} OData query string.
*/
jsepToBeetleQuery: function (exp, queryContext, firstExp) {
firstExp = firstExp || exp;
if (!queryContext) queryContext = { aliases: [] };
else if (!queryContext.aliases) queryContext.aliases = [];
if (exp.type === 'LogicalExpression' || exp.type == 'BinaryExpression') {
if (exp.operator == '=>') {
queryContext.aliases.push({ alias: exp.left.name, value: 'it' });
var r = helper.jsepToBeetleQuery(exp.right, queryContext, firstExp);
if (exp != firstExp)
queryContext.aliases.pop();
return r;
}
var op = enums.langOperators.find(exp.operator).code;
return '(' + helper.jsepToBeetleQuery(exp.left, queryContext, firstExp) + ' ' + op + ' ' + helper.jsepToBeetleQuery(exp.right, queryContext, firstExp) + ')';
}
else if (exp.type === 'UnaryExpression')
return exp.operator + helper.jsepToBeetleQuery(exp.argument, queryContext, firstExp);
else if (exp.type === 'Identifier') {
var n = exp.name;
var val = undefined;
var isPrm = false;
if (n[0] == '@') {
isPrm = true;
n = n.slice(1);
}
if (queryContext.expVarContext && queryContext.expVarContext[n] !== undefined)
val = queryContext.expVarContext[n];
else if (queryContext.varContext && queryContext.varContext[n] !== undefined)
val = queryContext.varContext[n];
if (val !== undefined)
return core.dataTypes.toBeetleValue(val);
if (isPrm)
throw helper.createError(i18N.unknownParameter, [n], { expression: exp, queryContext: queryContext });
var a = helper.findInArray(queryContext.aliases, n, 'alias');
if (a) return a.value;
return n;
}
else if (exp.type === 'Literal')
return core.dataTypes.toBeetleValue(exp.value);
else if (exp.type === 'MemberExpression') {
if (queryContext.currentAlias && exp.object.name == queryContext.currentAlias.alias)
return exp.property.name;
else {
var ali = helper.findInArray(queryContext.aliases, exp.object.name, 'alias'), o;
if (ali) o = ali.value;
else o = helper.jsepToBeetleQuery(exp.object, queryContext, firstExp);
if (o[exp.property.name] !== undefined)
return core.dataTypes.toBeetleValue(o[exp.property.name]);
return o + '.' + exp.property.name;
}
}
else if (exp.type === 'Compound') {
var sts = [];
for (var i = 0; i < exp.body.length; i++) {
var st = exp.body[i];
var s = helper.jsepToBeetleQuery(st, queryContext);
var ls = s.toLowerCase();
if (ls == 'desc' || ls == 'asc') {
if (sts.length == 0)
throw helper.createError(i18N.invalidStatement, { expression: exp, statement: st });
sts[sts.length - 1] += ' ' + s;
}
else if (ls == 'as') {
if (sts.length == 0 || exp.body.length < i + 1)
throw helper.createError(i18N.invalidStatement, { expression: exp, statement: st });
sts[sts.length - 1] += ' as ' + exp.body[i + 1].name;
i++;
}
else sts.push(s);
}
return sts.join(', ');
}
else if (exp.type === 'CallExpression') {
var argList = exp.arguments, args = [], alias = null;
if (argList.length == 1 && argList[0] && argList[0].type == 'BinaryExpression' && argList[0].operator == '=>') {
alias = { alias: argList[0].left.name };
alias.value = alias.alias;
argList = [argList[0].right];
}
if (alias) {
queryContext.currentAlias = alias;
queryContext.aliases.push(alias);
}
for (var j = 0; j < argList.length; j++) {
var arg = argList[j];
if (arg != null)
args.push(helper.jsepToBeetleQuery(arg, queryContext, firstExp));
}
var funcName;
if (exp.callee.type == 'MemberExpression') {
args.splice(0, 0, helper.jsepToBeetleQuery(exp.callee.object, queryContext, firstExp));
funcName = exp.callee.property.name;
}
else funcName = exp.callee.name;
var func = querying.queryFuncs.getFunc(funcName);
var retVal = func.toBeetleFunction.apply(func, args);
if (alias) {
queryContext.currentAlias = null;
queryContext.aliases.pop();
}
return retVal;
}
throw helper.createError(i18N.unknownExpression, { expression: exp });
},
/**
* Converts parsed javascript expression (jsep) to Javascript function (not using evil "eval").
* @param {Object} exp - Jsep expression (tokenized).
* @param {QueryContext} queryContext - Query execution context.
* @returns {Function} Javascript function.
*/
jsepToFunction: function (exp, queryContext) {
return function (value) {
if (!queryContext) queryContext = { aliases: [] };
else if (!queryContext.aliases) queryContext.aliases = [];
if (queryContext.currentAlias)
queryContext.currentAlias.value = value;
if (exp.type === undefined) return value;
else if (exp.type == 'LogicalExpression' || exp.type == 'BinaryExpression') {
if (exp.operator == '=>') {
if (queryContext.currentAlias)
queryContext.aliases.push(queryContext.currentAlias);
queryContext.currentAlias = { alias: exp.left.name };
var r = helper.jsepToFunction(exp.right, queryContext)(value);
queryContext.currentAlias = queryContext.aliases.pop();
return r;
}
var op = enums.langOperators.find(exp.operator);
var varContext = queryContext.varContext;
var arg1 = function () { return helper.jsepToFunction(exp.left, queryContext)(value); };
var arg2 = function () { return helper.jsepToFunction(exp.right, queryContext)(value); };
return op.asFunc.call(varContext, arg1, arg2);
}
else if (exp.type == 'UnaryExpression') {
var arg = function () { return helper.jsepToFunction(exp.argument, queryContext)(value); };
var uop = enums.langOperators.find(exp.operator);
return uop.asFunc.call(varContext, arg);
}
else if (exp.type == 'Identifier') {
var n = exp.name;
if (n == 'null') return null;
if (n == 'true') return true;
if (n == 'false') return false;
if (n[0] == '@') {
var val = undefined;
var varName = n.slice(1);
if (queryContext.expVarContext && queryContext.expVarContext[varName] !== undefined)
val = queryContext.expVarContext[varName];
else if (queryContext.varContext)
val = queryContext.varContext[varName];
if (val === undefined) throw helper.createError(i18N.unknownParameter, [n], { expression: exp, queryContext: queryContext });
return val;
}
if (queryContext.currentAlias && queryContext.currentAlias.alias == n)
return value;
var a = helper.findInArray(queryContext.aliases, n, 'alias');
if (a) return a.value;
var v = helper.getValue(value, n);
if (v === undefined) return root[n];
return v;
}
else if (exp.type == 'Literal')
return exp.value;
else if (exp.type == 'MemberExpression') {
if (exp.object.name) {
if (queryContext.currentAlias && exp.object.name == queryContext.currentAlias.alias)
return helper.getValue(value, exp.property.name);
var ali = helper.findInArray(queryContext.aliases, exp.object.name, 'alias');
if (ali) return helper.getValue(ali.value, exp.property.name);
}
return helper.getValue(helper.jsepToFunction(exp.object, queryContext)(value), exp.property.name);
}
else if (exp.type == 'CallExpression') {
var argList = exp.arguments, args = [], alias = null;
if (argList.length == 1 && argList[0] && argList[0].type == 'BinaryExpression' && argList[0].operator == '=>') {
alias = argList[0].left.name;
argList = [argList[0].right];
}
if (alias) {
if (queryContext.currentAlias)
queryContext.aliases.push(queryContext.currentAlias);
queryContext.currentAlias = { alias: alias };
}
var funcName;
helper.forEach(argList, function (farg) {
if (farg != null)
args.push(helper.jsepToFunction(farg, queryContext));
});
funcName = exp.callee.type == 'MemberExpression' ? exp.callee.property.name : exp.callee.name;
var retVal;
var func = querying.queryFuncs.getFunc(funcName, false);
if (func) {
if (exp.callee.type == 'MemberExpression')
args.splice(0, 0, helper.jsepToFunction(exp.callee.object, queryContext));
args.splice(0, 0, value);
retVal = func.impl.apply(queryContext, args);
}
else {
if (funcName[0] == '@') {
var varFuncName = funcName.slice(1);
if (queryContext.expVarContext && queryContext.expVarContext[varFuncName])
func = queryContext.expVarContext[varFuncName];
else if (queryContext.varContext && queryContext.varContext[varFuncName])
func = queryContext.varContext[varFuncName];
else throw helper.createError(i18N.unknownParameter, [varFuncName], { expression: exp, queryContext: queryContext });
} else {
var obj;
if (exp.callee.type == 'MemberExpression')
obj = helper.jsepToFunction(exp.callee.object, queryContext)(value);
else
obj = root;
if (obj == null || (func = obj[funcName]) == null)
throw helper.createError(i18N.unknownFunction, [funcName]);
}
args = helper.mapArray(args, function () { return this(value); });
retVal = func.apply(obj, args);
}
if (alias)
queryContext.currentAlias = queryContext.aliases.pop();
return retVal;
}
else throw helper.createError(i18N.unknownExpression, { expression: exp });
};
},
/**
* Converts parsed javascript expression (jsep) to Javascript projection function (not using evil "eval").
* @param {Object} exp - Jsep expression (tokenized).
* @param {QueryContext} queryContext - Query execution context.
* @returns {Function} Javascript projector function.
*/
jsepToProjector: function (exps, queryContext) {
function getPropertyPath(e) {
switch (e.type) {
case 'Identifier':
return e.name;
case 'MemberExpression':
return getPropertyPath(e.object) + '-' + e.property.name;
}
}
var projectExps = [];
if (!Assert.isArray(exps)) exps = [exps];
for (var i = 0; i < exps.length; i++) {
var e = exps[i];
// list expression property names and value evaluators
var propertyName = getPropertyPath(e);
if (exps.length > i + 2 && exps[i + 1].name && exps[i + 1].name.toLowerCase() == 'as') {
i = i + 2;
var pExp = exps[i];
if (pExp.type != 'Identifier')
throw helper.createError(i18N.invalidPropertyAlias, { expressions: exps, aliasExpression: pExp });
propertyName = pExp.name;
}
if (exps.length > 1 && !propertyName)
throw helper.createError(i18N.projectionsMustHaveAlias, { expressions: exps, expression: e });
projectExps.push({ p: propertyName, func: helper.jsepToFunction(e, queryContext) });
}
return function (item) {
var projection = {};
for (var k = 0; k < projectExps.length; k++) {
var pe = projectExps[k];
var value = pe.func(item);
if (exps.length == 1) return value;
projection[pe.p] = value;
}
return projection;
};
}
};
/**
* Assertion methods. Two different usage possible, static methods and instance methods.
* Static methods returns true or false. Instance methods can be chained and they collect errors in an array.
* Check method throws error if there are any.
* @class
*/
var Assert = (function () {
/** @constructor
* @param {any} value - Value to check.
* @param {string=} name - Property name representing the value (will be used in error messages).
*/
var ctor = function (value, name) {
this.value = value;
this.name = name;
this.errors = [];
};
var proto = ctor.prototype;
/** Checks if value is not null or undefined. */
proto.hasValue = function () {
ctor.hasValue(this.value, this.errors, this.name);
return this;
};
/** Checks if value is object. */