UNPKG

angular-odata-resources

Version:

Allows making fluent OData queries from angular resources

1,316 lines (1,139 loc) 58.8 kB
angular.module('ODataResources', ['ng']);; angular.module('ODataResources'). factory('$odataOperators', [function() { var rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g; var trim = function(value) { return value.replace(rtrim, ''); }; var filterOperators = { 'eq':['=','==','==='], 'ne':['!=','!==','<>'], 'gt':['>'], 'ge':['>=','>=='], 'lt':['<'], 'le':['<=','<=='], 'and':['&&'], 'or':['||'], 'not':['!'], 'add':['+'], 'sub':['-'], 'mul':['*'], 'div':['/'], 'mod':['%'], }; var convertOperator = function(from){ var input = trim(from).toLowerCase(); var key; for(key in filterOperators) { if(input === key) return key; var possibleValues = filterOperators[key]; for (var i = 0; i < possibleValues.length; i++) { if(input === possibleValues[i]){ return key; } } } throw "Operator "+ from+" not found"; }; return { operators : filterOperators, convert:convertOperator, }; }]); ;angular.module('ODataResources'). factory('$odataValue', [ function() { var illegalChars = { '%': '%25', '+': '%2B', '/': '%2F', '?': '%3F', '#': '%23', '&': '%26' }; var escapeIllegalChars = function(string) { for (var key in illegalChars) { string = string.replace(key, illegalChars[key]); } string = string.replace(/'/g, "''"); return string; }; var ODataValue = function(input, type) { this.value = input; this.type = type; }; var generateDate = function(date,isOdataV4){ if(!isOdataV4){ return "datetime'" + date.getFullYear() + "-" + ("0" + (date.getMonth() + 1)).slice(-2) + "-" + ("0" + date.getDate()).slice(-2) + "T" + ("0" + date.getHours()).slice(-2) + ":" + ("0" + date.getMinutes()).slice(-2)+':'+("0" + date.getSeconds()).slice(-2) + "'"; }else{ return date.getFullYear() + "-" + ("0" + (date.getMonth() + 1)).slice(-2) + "-" + ("0" + date.getDate()).slice(-2) + "T" + ("0" + date.getHours()).slice(-2) + ":" + ("0" + date.getMinutes()).slice(-2)+':'+("0" + date.getSeconds()).slice(-2) + "Z"; } }; var generateGuid = function(guidValue, isOdataV4){ if(!isOdataV4){ return "guid'"+guidValue+"'"; }else{ return guidValue; } }; var generateDateOffset = function (date, isOdataV4) { if (!isOdataV4) { return "datetimeoffset'" + date.getFullYear() + "-" + ("0" + (date.getMonth() + 1)).slice(-2) + "-" + ("0" + date.getDate()).slice(-2) + "T" + ("0" + date.getHours()).slice(-2) + ":" + ("0" + date.getMinutes()).slice(-2) + ':' + ("0" + date.getSeconds()).slice(-2) + "'"; } else { return date.getFullYear() + "-" + ("0" + (date.getMonth() + 1)).slice(-2) + "-" + ("0" + date.getDate()).slice(-2) + "T" + ("0" + date.getHours()).slice(-2) + ":" + ("0" + date.getMinutes()).slice(-2) + ':' + ("0" + date.getSeconds()).slice(-2) + "Z"; } }; ODataValue.prototype.executeWithUndefinedType = function(isOdataV4) { if (angular.isString(this.value)) { return "'" + escapeIllegalChars(this.value) + "'"; } else if (this.value === false) { return "false"; } else if (this.value === true) { return "true"; } else if (angular.isDate(this.value)) { return generateDate(this.value,isOdataV4); } else if (!isNaN(this.value)) { return this.value; } else { throw "Unrecognized type of " + this.value; } }; ODataValue.prototype.executeWithType = function(isOdataV4){ if(this.value === true || this.value === false){ if(this.type.toLowerCase() === "boolean"){ return !!this.value+""; }else if(this.type.toLowerCase() === "string"){ return "'"+!!this.value+"'"; }else { throw "Cannot convert bool ("+this.value+") into "+this.type; } } if(angular.isDate(this.value)){ if(this.type.toLowerCase() === "decimal"){ return this.value.getTime()+"M"; }else if(this.type.toLowerCase() === "int32"){ return this.value.getTime()+""; }else if(this.type.toLowerCase() === "single"){ return this.value.getTime()+"f"; }else if(this.type.toLowerCase() === "double"){ return this.value.getTime()+"d"; }else if(this.type.toLowerCase() === "datetime"){ return generateDate(this.value,isOdataV4); } else if (this.type.toLowerCase() === "datetimeoffset") { return generateDateOffset(new Date(this.value), isOdataV4); }else if(this.type.toLowerCase()==="string"){ return "'"+this.value.toISOString()+"'"; }else { throw "Cannot convert date ("+this.value+") into "+this.type; } } if(angular.isString(this.value)){ if(this.type.toLowerCase() === "guid"){ return generateGuid(this.value,isOdataV4); }else if(this.type.toLowerCase() === "datetime"){ return generateDate(new Date(this.value),isOdataV4); } else if (this.type.toLowerCase() === "datetimeoffset") { return generateDateOffset(new Date(this.value), isOdataV4); }else if(this.type.toLowerCase() === "single"){ return parseFloat(this.value)+"f"; }else if(this.type.toLowerCase() === "double"){ return parseFloat(this.value)+"d"; }else if(this.type.toLowerCase() === "decimal"){ return parseFloat(this.value)+"M"; }else if(this.type.toLowerCase() === "boolean"){ return this.value; }else if(this.type.toLowerCase() === "int32"){ return parseInt(this.value)+""; }else { throw "Cannot convert "+this.value+" into "+this.type; } }else if(!isNaN(this.value)){ if(this.type.toLowerCase() === "boolean"){ return !!this.value+""; }else if(this.type.toLowerCase() === "decimal"){ return this.value+"M"; }else if(this.type.toLowerCase() === "double"){ return this.value+"d"; }else if(this.type.toLowerCase() === "single"){ return this.value+"f"; }else if(this.type.toLowerCase() === "byte"){ return (this.value%255).toString(16); }else if(this.type.toLowerCase() === "datetime"){ return generateDate(new Date(this.value),isOdataV4); }else if(this.type.toLowerCase() === "string"){ return "'"+this.value+"'"; }else { throw "Cannot convert number ("+this.value+") into "+this.type; } } else{ throw "Source type of "+this.value+" to be conververted into "+this.type+"is not supported"; } }; ODataValue.prototype.execute = function(isOdataV4) { if(this.value === null){ return 'null'; } if (this.type === undefined) { return this.executeWithUndefinedType(isOdataV4); } else { return this.executeWithType(isOdataV4); } }; return ODataValue; } ]);;angular.module('ODataResources'). factory('$odataProperty', [function() { var ODataProperty = function(input){ this.value = input; }; ODataProperty.prototype.execute = function(){ return this.value; }; return ODataProperty; }]); ;angular.module('ODataResources'). factory('$odataBinaryOperation', ['$odataOperators','$odataProperty','$odataValue',function($odataOperators,ODataProperty,ODataValue) { var ODataBinaryOperation = function(a1,a2,a3){ if(a1===undefined){ throw "The property of a filter cannot be undefined"; } if(a2 === undefined){ throw "The value of a filter cannot be undefined"; } if(a3 === undefined){ //If strings are specified, we assume that the first one is the object property and the second one its value if(angular.isFunction(a1.execute)){ this.operandA = a1; }else{ this.operandA = new ODataProperty(a1); } if(a2!==null && angular.isFunction(a2.execute)){ this.operandB = a2; }else{ this.operandB = new ODataValue(a2); } this.filterOperator = 'eq'; } else{ if(angular.isFunction(a1.execute)){ this.operandA = a1; }else{ this.operandA = new ODataProperty(a1); } if(a3!==null && angular.isFunction(a3.execute)){ this.operandB = a3; }else{ this.operandB = new ODataValue(a3); } this.filterOperator = $odataOperators.convert(a2); } }; ODataBinaryOperation.prototype.execute = function(isODatav4,noParenthesis){ var result = this.operandA.execute(isODatav4)+" "+this.filterOperator+" " +this.operandB.execute(isODatav4); if(!noParenthesis) result = "("+result+")"; return result; }; ODataBinaryOperation.prototype.or = function(a1,a2,a3){ var other; if(a2!==undefined){ other = new ODataBinaryOperation(a1,a2,a3); } else if(angular.isFunction(a1.execute)){ other = a1; } else{ throw "The object " +a1 +" passed as a parameter of the or method is not valid"; } return new ODataBinaryOperation(this,"or",other); }; ODataBinaryOperation.prototype.and = function(a1,a2,a3){ var other; if(a2!==undefined){ other = new ODataBinaryOperation(a1,a2,a3); } else if(angular.isFunction(a1.execute)){ other = a1; } else{ throw "The object " +a1 +" passed as a parameter of the and method is not valid"; } return new ODataBinaryOperation(this,"and",other); }; return ODataBinaryOperation; } ]);;angular.module('ODataResources'). factory('$odataExpandPredicate', ['$odataPredicate', '$odataBinaryOperation', '$odataOrderByStatement', function (ODataPredicate, ODataBinaryOperation, ODataOrderByStatement) { var ODataExpandPredicate = function (tableName, context) { if (tableName === undefined) { throw "ExpandPredicate should be passed a table name but got undefined."; } if (context === undefined) { throw "ExpandPredicate should be passed a context but got undefined."; } this.name = tableName; this.expandables = []; // To maintain recursion compatibility with base OdataResourceProvider this.options = { select: [], filter: [], orderby: [], expand: this.expandables, }; this.context = context; }; ODataExpandPredicate.prototype.filter = function(operand1, operand2, operand3) { if (operand1 === undefined) throw "The first parameter is undefined. Did you forget to invoke the method as a constructor by adding the 'new' keyword?"; var predicate; if (angular.isFunction(operand1.execute) && operand2 === undefined) { predicate = operand1; } else { predicate = new ODataBinaryOperation(operand1, operand2, operand3); } this.options.filter.push(predicate); return this; }; ODataExpandPredicate.prototype.select = function (propertyName) { if (propertyName === undefined) { throw "ExpandPredicate.select should be passed a property name but got undefined."; } if (!angular.isArray(propertyName)) propertyName = propertyName.split(','); function checkArray(i, value) { return value === propertyName[i]; } for (var i = 0; i < propertyName.length; i++) { if (!this.options.select.some(checkArray.bind(this, i))) this.options.select.push(propertyName[i]); } return this; }; ODataExpandPredicate.prototype.orderBy = function (arg1, arg2) { this.options.orderby.push((new ODataOrderByStatement(arg1, arg2)).execute()); return this; }; ODataExpandPredicate.prototype.expand = function (tableName) { if (tableName === undefined) { throw "ExpandPredicate.expand should be passed a table name but got undefined."; } return new ODataExpandPredicate(tableName, this).finish(); }; ODataExpandPredicate.prototype.expandPredicate = function (tableName) { if (tableName === undefined) { throw "ExpandPredicate.expandPredicate should be passed a table name but got undefined."; } return new ODataExpandPredicate(tableName, this); }; ODataExpandPredicate.prototype.build = function () { var query = this.name; var sub = []; for (var option in this.options) { if (this.options[option].length) { if (option === 'filter') { sub.push("$filter=" + ODataPredicate.and(this.options.filter).execute(this.isv4, true)); } else { sub.push("$" + option + "=" + this.options[option].join(',')); } } } if (sub.length) { query += "(" + sub.join(';') + ")"; } return query; }; ODataExpandPredicate.prototype.finish = function () { var query = this.build(); this.context.expandables.push(query); return this.context; }; return ODataExpandPredicate; }]);;angular.module('ODataResources'). factory('$odataMethodCall', ['$odataProperty', '$odataValue', function(ODataProperty, ODataValue) { var ODataMethodCall = function(methodName) { if (methodName === undefined || methodName === "") throw "Method name should be defined"; this.params = []; if (arguments.length < 2) throw "Method should be invoked with arguments"; for (var i = 1; i < arguments.length; i++) { var value = arguments[i]; if (angular.isFunction(value.execute)) { this.params.push(value); } else { //We assume the first one is the object property; if (i == 1) { this.params.push(new ODataProperty(value)); } else { this.params.push(new ODataValue(value)); } } } this.methodName = methodName; }; ODataMethodCall.prototype.execute = function() { var lambdaOperators = ["any", "all"]; var invocation = ""; if(lambdaOperators.indexOf(this.methodName) > -1) { for (var i = 0; i < this.params.length; i++) { if (i === 0) { invocation += this.params[i].execute(); invocation += "/"; invocation += this.methodName; } else if(i === 1) { invocation += "("; invocation += this.params[i].value; invocation += ":"; } else { invocation += this.params[i].execute(); invocation += ")"; } } } else { invocation += this.methodName + "("; for (var j = 0; j < this.params.length; j++) { if (j > 0) invocation += ","; invocation += this.params[j].execute(); } invocation += ")"; } return invocation; }; return ODataMethodCall; } ]);;angular.module('ODataResources'). factory('$odataOrderByStatement', [function($odataOperators,ODataBinaryOperation,ODataPredicate) { var ODataOrderByStatement = function(propertyName, sortOrder){ if(propertyName===undefined){ throw "Orderby should be passed a property name but got undefined"; } this.propertyName = propertyName; this.direction = sortOrder || "asc"; }; ODataOrderByStatement.prototype.execute = function() { return this.propertyName+" "+this.direction; }; return ODataOrderByStatement; }]);;angular.module('ODataResources'). factory('$odataPredicate', ['$odataBinaryOperation',function(ODataBinaryOperation) { var ODataPredicate = function(a1,a2,a3){ if(angular.isFunction(a1.execute) && a2 === undefined){ return a1; } else{ return new ODataBinaryOperation(a1,a2,a3); } }; ODataPredicate.and = function(andStatements){ if(andStatements.length>0){ var finalOperation = andStatements[0]; for (var i = 1; i < andStatements.length; i++) { finalOperation = new ODataBinaryOperation(finalOperation,'and',andStatements[i]); } return finalOperation; } throw "No statements specified"; }; ODataPredicate.or = function(orStatements){ if(orStatements.length>0){ var finalOperation = orStatements[0]; for (var i = 1; i < orStatements.length; i++) { finalOperation = new ODataBinaryOperation(finalOperation,'or',orStatements[i]); } return finalOperation; } throw "No statements specified for OR predicate"; }; ODataPredicate.create = function(a1,a2,a3){ if(angular.isFunction(a1.execute) && a2 === undefined){ return a1; } else{ return new ODataBinaryOperation(a1,a2,a3); } }; return ODataPredicate; }]); ;angular.module('ODataResources'). factory('$odataProvider', ['$odataOperators', '$odataBinaryOperation', '$odataPredicate', '$odataOrderByStatement', '$odataExpandPredicate', function($odataOperators, ODataBinaryOperation, ODataPredicate, ODataOrderByStatement, ODataExpandPredicate) { var ODataProvider = function(callback, isv4, reusables) { this.$$callback = callback; this.filters = []; this.sortOrders = []; this.takeAmount = undefined; this.skipAmount = undefined; this.expandables = []; this.isv4 = isv4; this.hasInlineCount = false; this.selectables = []; this.transformUrls=[]; this.formatBy = undefined; if (reusables) this.$$reusables = reusables; }; ODataProvider.prototype.filter = function(operand1, operand2, operand3) { if (operand1 === undefined) throw "The first parameted is undefined. Did you forget to invoke the method as a constructor by adding the 'new' keyword?"; var predicate; if (angular.isFunction(operand1.execute) && operand2 === undefined) { predicate = operand1; } else { predicate = new ODataBinaryOperation(operand1, operand2, operand3); } this.filters.push(predicate); return this; }; ODataProvider.prototype.transformUrl = function(transformMethod) { this.transformUrls.push(transformMethod); return this; }; ODataProvider.prototype.orderBy = function(arg1, arg2) { this.sortOrders.push(new ODataOrderByStatement(arg1, arg2)); return this; }; ODataProvider.prototype.take = function(amount) { this.takeAmount = amount; return this; }; ODataProvider.prototype.skip = function(amount) { this.skipAmount = amount; return this; }; ODataProvider.prototype.format = function(format) { this.formatBy = format; return this; }; ODataProvider.prototype.execute = function() { var queryString = ''; var i; if (this.filters.length > 0) { queryString = "$filter=" + ODataPredicate.and(this.filters).execute(this.isv4,true); } if (this.sortOrders.length > 0) { if (queryString !== "") queryString += "&"; queryString += "$orderby="; for (i = 0; i < this.sortOrders.length; i++) { if (i > 0) { queryString += ","; } queryString += this.sortOrders[i].execute(); } } if (this.takeAmount) { if (queryString !== "") queryString += "&"; queryString += "$top=" + this.takeAmount; } if (this.skipAmount) { if (queryString !== "") queryString += "&"; queryString += "$skip=" + this.skipAmount; } if (this.expandables.length > 0) { if (queryString !== "") queryString += "&"; queryString += "$expand="+ this.expandables.join(','); } if(this.selectables.length>0){ if (queryString !== "") queryString += "&"; queryString += "$select=" + this.selectables.join(','); } if (this.hasInlineCount > 0) { if (queryString !== "") queryString += "&"; queryString += this.isv4 ? "$count=true" : "$inlinecount=allpages"; } if (this.formatBy) { if (queryString !== "") queryString += "&"; queryString += "$format=" + this.formatBy; } for (i = 0; i < this.transformUrls.length; i++) { var transform= this.transformUrls[i]; queryString = transform(queryString); } return queryString; }; ODataProvider.prototype.query = function(success, error) { if (!angular.isFunction(this.$$callback)) throw "Cannot execute query, no callback was specified"; success = success || angular.noop; error = error || angular.noop; return this.$$callback(this.execute(), success, error, false, false, getPersistence.bind(this, 'query')); }; ODataProvider.prototype.single = function(success, error) { if (!angular.isFunction(this.$$callback)) throw "Cannot execute single, no callback was specified"; success = success || angular.noop; error = error || angular.noop; return this.$$callback(this.execute(), success, error, true, true, getPersistence.bind(this, 'single')); }; ODataProvider.prototype.get = function(data, success, error) { if (!angular.isFunction(this.$$callback)) throw "Cannot execute get, no callback was specified"; success = success || angular.noop; error = error || angular.noop; // The query string from this.execute() should be included even // when fetching just a single element. var queryString = this.execute(); if (queryString.length > 0) { queryString = "?" + queryString; } return this.$$callback("(" + data + ")" + queryString, success, error, true, false, getPersistence.bind(this, 'get')); }; ODataProvider.prototype.count = function(success, error) { if (!angular.isFunction(this.$$callback)) throw "Cannot execute count, no callback was specified"; success = success || angular.noop; error = error || angular.noop; // The query string from this.execute() should be included even // when fetching just a single element. var queryString = this.execute(); if (queryString.length > 0) { queryString = "/?" + queryString; } return this.$$callback("/$count" + queryString, success, error, true, false, getPersistence.bind(this, 'count')); }; ODataProvider.prototype.withInlineCount = function() { this.hasInlineCount = true; return this; }; var expandOdatav4 = function(navigationProperties){ var first = navigationProperties.shift(); var current = first; if(navigationProperties.length>0){ current = current + "($expand="+expandOdatav4(navigationProperties)+")"; } return current; }; ODataProvider.prototype.expand = function(params) { if (!angular.isString(params) && !angular.isArray(params)) { throw "Invalid parameter passed to expand method (" + params + ")"; } if (params === "") { return; } var expandQuery = params; if (this.isv4) { //Make it an array if (!angular.isArray(params)) { params = Array.prototype.slice.call(arguments); } expandQuery = expandOdatav4(params); } else { if (angular.isArray(params)) { expandQuery = params.join('/'); } else { expandQuery = Array.prototype.slice.call(arguments).join('/'); } for (var i = 0; i < this.expandables.length; i++) { if (this.expandables[i] === expandQuery) return this; } } this.expandables.push(expandQuery); return this; }; ODataProvider.prototype.expandPredicate = function(tableName) { return new ODataExpandPredicate(tableName, this); }; ODataProvider.prototype.select = function(params) { if (!angular.isString(params) && !angular.isArray(params)) { throw "Invalid parameter passed to select method (" + params + ")"; } if (params === "") { return; } var selectQuery = params; if (!angular.isArray(params)) { params = Array.prototype.slice.call(arguments); } for (var i = params.length - 1; i >= 0; i--) { this.selectables.push(params[i]); } return this; }; function getPersistence(type, full) { var reusables = {}; // Set full persistence if type is count or single(?) because we'll want to pull in filters, etc to reproduce what we're refreshing. // Otherwise, let the factory decide if the persistence state should include the full (for a refresh on the array), or limited for // a single entity refresh. // Single is tricky... Should the refresh requery and take the first element based on full filtering, or just refresh the entity we // already have based on limited persistence? What's its purposed use case? Could set an option toggle for either or. if (!full && (type === 'count' || type === 'single')) full = true; Object.defineProperty(reusables, '$$type', { enumerble: false, writable: true, configurable: true, value: type }); if (full) { for (var key in this) { if (this.hasOwnProperty(key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) { reusables[key] = this[key]; } } return reusables; } else { if (this.selectables.length) reusables.selectables = this.selectables; if (this.expandables.length) reusables.expandables = this.expandables; if (this.formatBy) reusables.formatBy = this.formatBy; } return reusables; } ODataProvider.prototype.re = function (force) { if (this.$$reusables) { for (var option in this.$$reusables) { if (angular.isArray(this.$$reusables[option])) { for (var i = 0; i < this.$$reusables[option].length; i++) { if (this[option].indexOf(this.$$reusables[option][i]) === -1) this[option].push(this.$$reusables[option][i]); } } else this[option] = this.$$reusables[option]; } } return this; }; return ODataProvider; } ]);;/** * @license AngularJS v1.3.15 * (c) 2010-2014 Google, Inc. http://angularjs.org * License: MIT */ (function(window, angular, undefined) { 'use strict'; var $resourceMinErr = angular.$$minErr('$resource'); // Helper functions and regex to lookup a dotted path on an object // stopping at undefined/null. The path must be composed of ASCII // identifiers (just like $parse) var MEMBER_NAME_REGEX = /^(\.[a-zA-Z_$@][0-9a-zA-Z_$@]*)+$/; function isValidDottedPath(path) { return (path !== null && path !== '' && path !== 'hasOwnProperty' && MEMBER_NAME_REGEX.test('.' + path)); } function lookupDottedPath(obj, path) { if (!isValidDottedPath(path)) { throw $resourceMinErr('badmember', 'Dotted member path "@{0}" is invalid.', path); } var keys = path.split('.'); for (var i = 0, ii = keys.length; i < ii && obj !== undefined; i++) { var key = keys[i]; obj = (obj !== null) ? obj[key] : undefined; } return obj; } /** * Create a shallow copy of an object and clear other fields from the destination */ function shallowClearAndCopy(src, dst) { dst = dst || {}; angular.forEach(dst, function(value, key) { delete dst[key]; }); for (var key in src) { if (src.hasOwnProperty(key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) { dst[key] = src[key]; } } return dst; } angular.module('ODataResources'). provider('$odataresource', function() { var provider = this; this.defaults = { // Strip slashes by default stripTrailingSlashes: true, // Default actions configuration actions: { 'get': { method: 'GET' }, 'save': { method: 'POST' }, 'query': { method: 'GET', isArray: true }, 'remove': { method: 'DELETE' }, 'delete': { method: 'DELETE' }, 'update': { method: 'PUT' }, 'odata': { method: 'GET', isArray: true } } }; this.$get = ['$http', '$q', '$odata', function($http, $q, $odata) { var noop = angular.noop, forEach = angular.forEach, extend = angular.extend, copy = angular.copy, isFunction = angular.isFunction, resourceStore = []; /** * We need our custom method because encodeURIComponent is too aggressive and doesn't follow * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set * (pchar) allowed in path segments: * segment = *pchar * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" * pct-encoded = "%" HEXDIG HEXDIG * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" * / "*" / "+" / "," / ";" / "=" */ function encodeUriSegment(val) { return encodeUriQuery(val, true). replace(/%26/gi, '&'). replace(/%3D/gi, '='). replace(/%2B/gi, '+'); } /** * This method is intended for encoding *key* or *value* parts of query component. We need a * custom method because encodeURIComponent is too aggressive and encodes stuff that doesn't * have to be encoded per http://tools.ietf.org/html/rfc3986: * query = *( pchar / "/" / "?" ) * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" * pct-encoded = "%" HEXDIG HEXDIG * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" * / "*" / "+" / "," / ";" / "=" */ function encodeUriQuery(val, pctEncodeSpaces) { return encodeURIComponent(val). replace(/%40/gi, '@'). replace(/%3A/gi, ':'). replace(/%24/g, '$'). replace(/%2C/gi, ','). replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); } function Route(template, defaults) { this.template = template; this.defaults = extend({}, provider.defaults, defaults); this.urlParams = {}; } Route.prototype = { setUrlParams: function(config, params, actionUrl, data, isOData) { var self = this, url = actionUrl || self.template, val, encodedVal; if (url === self.template && (config.method === 'PUT' || config.method === 'DELETE' || (config.method == 'GET' && !isOData) || config.method == 'PATCH') && angular.isString(self.defaults.odatakey)) { // strip trailing slashes and set the url (unless this behavior is specifically disabled) if (self.defaults.stripTrailingSlashes) { url = url.replace(/\/+$/, '') || '/'; } var odatakeySplit = self.defaults.odatakey.split(','); var splitKey = odatakeySplit.map(function (key) { return odatakeySplit.length > 1 ? key + '=:' + key : ':' + key; }); url = url + '(' + splitKey.join(',') + ')'; if (data) { forEach(odatakeySplit, function (param) { params[param] = data[param]; }); } } var urlParams = self.urlParams = {}; forEach(url.split(/\W/), function(param) { if (param === 'hasOwnProperty') { throw $resourceMinErr('badname', "hasOwnProperty is not a valid parameter name."); } if (!(new RegExp("^\\d+$").test(param)) && param && (new RegExp("(^|[^\\\\]):" + param + "(\\W|$)").test(url))) { urlParams[param] = true; } }); url = url.replace(/\\:/g, ':'); params = params || {}; forEach(self.urlParams, function(_, urlParam) { val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam]; if (angular.isDefined(val) && val !== null) { encodedVal = encodeUriSegment(val); url = url.replace(new RegExp(":" + urlParam + "(\\W|$)", "g"), function(match, p1) { return encodedVal + p1; }); } else { url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W|$)", "g"), function(match, leadingSlashes, tail) { if (tail.charAt(0) == '/') { return tail; } else { return leadingSlashes + tail; } }); } }); // strip trailing slashes and set the url (unless this behavior is specifically disabled) if (self.defaults.stripTrailingSlashes) { url = url.replace(/\/+$/, '') || '/'; } // then replace collapse `/.` if found in the last URL path segment before the query // E.g. `http://url.com/id./format?q=x` becomes `http://url.com/id.format?q=x` url = url.replace(/\/\.(?=\w+($|\?))/, '.'); // replace escaped `/\.` with `/.` config.url = url.replace(/\/\\\./, '/.'); // set params - delegate param encoding to $http forEach(params, function(value, key) { if (!self.urlParams[key]) { config.params = config.params || {}; config.params[key] = value; } }); } }; function resourceFactory(url, paramDefaults, actions, options) { options = options || {}; if (angular.isString(paramDefaults)) { options.odatakey = paramDefaults; paramDefaults = {}; } var route = new Route(url, options); actions = extend({}, provider.defaults.actions, actions); function extractParams(data, actionParams) { var ids = {}; actionParams = extend({}, paramDefaults, actionParams); forEach(actionParams, function(value, key) { if (isFunction(value)) { value = value(); } ids[key] = value && value.charAt && value.charAt(0) == '@' ? lookupDottedPath(data, value.substr(1)) : value; }); return ids; } function defaultResponseInterceptor(response) { return response.resource; } function Resource(value) { shallowClearAndCopy(value || {}, this); } Resource.prototype.toJSON = function() { var data = extend({}, this); delete data.$promise; delete data.$resolved; return data; }; Resource.store = function (resource) { var exists = resourceStore.filter(function (filter) { return filter.resource === resource; }); if (exists.length) { Array.prototype.splice.call(arguments, 0, 1, exists[0]); return angular.extend.apply(angular, arguments); } Array.prototype.splice.call(arguments, 0, 1, { resource: resource }); exists = angular.extend.apply(angular, arguments); resourceStore.push(exists); return exists; }; Resource.isResource = function(target) { return resourceStore.filter(function(filter) { return filter.resource === target; }).length; }; Resource.store.get = function(target) { var hash = resourceStore.filter(function(filter) { return filter.resource === target; }); return hash.length ? hash[0] : null; }; Resource.store.getHeaders = function(target) { var stored = Resource.store.get(target); return stored ? stored.headers : null; }; Resource.store.copyHeaders = function(target, source) { return Resource.store(target, { headers: Resource.store.getHeaders(source) }); }; Resource.store.getConfig = function(target) { var stored = Resource.store.get(target); return stored ? stored.config : null; }; Resource.store.updateConfig = function (target) { if (arguments.length < 2) return false; var configPropNames = ['url', 'paramDefaults', 'action', 'options']; var settings = arguments.length > 2 && angular.isString(arguments[1]) ? Array.prototype.slice.call(arguments, 1, 5).reduce(function (prev, config, index) { prev[configPropNames[index]] = config; return prev; }, {}) : arguments[1]; if (Resource.isResource(settings)) settings = Resource.store.getConfig(settings); // If this library is updated to support 1.4+ change to use merge (not available in angular 1.3 which is what unit tests currently use). angular.extend(actions, settings.actions || {}); angular.extend(paramDefaults, settings.paramDefaults || {}); angular.extend(options, settings.options || {}); if (angular.isDefined(settings.url) && angular.isString(settings.url)) url = settings.url; route = new Route(url, options); Resource.store(target, { config: { url: url, paramDefaults: paramDefaults, actions: actions, options: options, }, pendingCorrection: false, }); return true; }; Resource.store.pendingCorrection = function (target) { var stored = Resource.store.get(target); return stored ? stored.pendingCorrection || false : false; }; Resource.store.getRefreshingResource = function(target) { var stored = resourceStore.filter(function(filter) { return filter.refreshedAs === target; }); return stored ? stored[0] : null; }; forEach(actions, function(action, name) { var hasBody = /^(POST|PUT|PATCH)$/i.test(action.method); Resource[name] = function(a1, a2, a3, a4, isOdata, odataQueryString, isSingleElement, forceSingleElement, persistence) { var params = {}, data, success, error; /* jshint -W086 */ /* (purposefully fall through case statements) */ switch (arguments.length) { case 9: case 8: case 7: case 6: case 4: error = a4; success = a3; //fallthrough case 3: case 2: if (isFunction(a2)) { if (isFunction(a1)) { success = a1; error = a2; break; } success = a2; error = a3; //fallthrough } else { params = a1; data = a2; success = a3; break; } case 1: if (isFunction(a1)) success = a1; else if (hasBody) data = a1; else params = a1; break; case 0: break; default: throw $resourceMinErr('badargs', "Expected up to 4 arguments [params, data, success, error], got {0} arguments", arguments.length); } /* jshint +W086 */ /* (purposefully fall through case statements) */ var isInstanceCall = this instanceof Resource; var value = isInstanceCall ? data : ((!isSingleElement && action.isArray) ? [] : new Resource(data)); var httpConfig = {}; var responseInterceptor = action.interceptor && action.interceptor.response || defaultResponseInterceptor; var responseErrorInterceptor = action.interceptor && action.interceptor.responseError || undefined; addRefreshMethod(value, persistence); forEach(action, function(value, key) { if (key != 'params' && key != 'isArray' && key != 'interceptor') { httpConfig[key] = copy(value); } }); if (hasBody) httpConfig.data = data; route.setUrlParams(httpConfig, extend({}, extractParams(data, action.params || {}), params), action.url, data, isOdata); //if (angular.isString(odataQueryString) && odataQueryString !== "" && !isOdata || (isOdata && (!isSingleElement || forceSingleElement))) { if (isOdata && odataQueryString !== "" && (!isSingleElement || forceSingleElement)) { httpConfig.url += "?" + odataQueryString; } else if (odataQueryString !== "" && isSingleElement) { httpConfig.url += odataQueryString; } //chieffancypants / angular-loading-bar //https://github.com/chieffancypants/angular-loading-bar if (options.ignoreLoadingBar) httpConfig.ignoreLoadingBar = true; function httpSuccessHandler(response) { var data = response.data, promise = value.$promise; if(data && angular.isNumber(data['@odata.count'])) { data.count = data['@odata.count']; } if (data && (angular.isString(data['@odata.context']) || angular.isString(data['odata.metadata']) ) && data.value && angular.isArray(data.value)) { var fullObject = data; data = data.value; for (var property in fullObject) { if (property !== "value") { value[property] = fullObject[property]; } } } if (data) { // Need to convert action.isArray to boolean in case it is undefined // jshint -W018 if (angular.isArray(data) !== (!isSingleElement && !! action.isArray) && !forceSingleElement) { throw $resourceMinErr('badcfg', 'Error in resource configuration for action `{0}`. Expected response to ' + 'contain an {1} but got an {2} (Request: {3} {4})', name, (!isSingleElement && action.isArray) ? 'array' : 'object', angular.isArray(data) ? 'array' : 'object', httpConfig.method, httpConfig.url); } if(angular.isArray(data) && forceSingleElement){ if(data.length>0){ data = data[0]; }else{ throw "The response returned no result"; } } // jshint +W018 if (!isSingleElement && action.isArray && isNaN(parseInt(data))) { value.length = 0; forEach(data, function(item) { if (typeof item === "object") { var newResource = new Resource(item); addRefreshMethod(newResource, persistence, false); value.push(newResource); } else { // Valid JSON values may be string literals, and these should not be converted // into objects. These items will not have access to the Resource prototype // methods, but unfortunately there value.push(item); } }); } else { shallowClearAndCopy(data, value); value.$promise = promise; } } if(angular.isNumber(data) && isSingleElement){ value.result = data; } else if(!isNaN(parseInt(data)) && isSingleElement){ value.result = parseInt(data); } value.$resolved = true; response.resource = value; Resource.store(value, { headers: response.headers() }); return responseInterceptor(response) || response; } // Input: httpResponse error object // Output: Rejection wrapped error object, rejected non promise response from errorInterceptor, or promise from errorInterceptor with errorCorrectionHandler appended to chain function httpErrorHandler(response) { value.$resolved = true; var refreshingResource = Resource.store.getRefreshingResource(value); if (refreshingResource && refreshingResource.preventErrorLooping) { Resource.store(refreshingResource.resource, { preventErrorLooping: false }); return $q.reject(response); } Resource.store(value, { headers: response.headers(), preventErrorLooping: true }); return chooseErrorResp