From 1a220965e54251f9486d8128ae12384ee1e3a0b4 Mon Sep 17 00:00:00 2001 From: guylabs Date: Wed, 22 Apr 2015 07:47:58 +0200 Subject: [PATCH] #9 - Add possibility to pass URL template within resource name --- README.md | 11 ++++++ RELEASENOTES.md | 13 +++++++ dist/angular-spring-data-rest.js | 34 +++++++++++++++++-- dist/angular-spring-data-rest.min.js | 2 +- src/angular-spring-data-rest-provider.js | 34 +++++++++++++++++-- ...pring-data-rest-provider.resources.spec.js | 26 ++++++++++++++ 6 files changed, 115 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 30f589a..c975dc0 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,17 @@ This will call *Angular* `$resource` method by default (this is [exchangeable](# * `actions`: custom action that should extend the default set of the `$resource` actions. Read more [here](https://docs.angularjs.org/api/ngResource/service/$resource). * `options`: custom settings that should extend the default `$resourceProvider` behavior Read more [here](https://docs.angularjs.org/api/ngResource/service/$resource). +You are also able to add URL templates to the passed in `linkName` parameter. (This just works if you pass a string and not a resource object) The following example explains the usage: + +```javascript +SpringDataRestAdapter.process(response).then(function(processedResponse) { + var resources = processedResponse._resources("self/:id", {id: "@id"}); + var item = resources.get({id: 1}).then(function(data) { + item = data; + }; +}); +``` + The `_resources` method returns the *Angular* resource "class" object with methods for the default set of resource actions. Read more [here](https://docs.angularjs.org/api/ngResource/service/$resource). If no parameter is given the `_resources` method will return all available resources objects of the given object. When for example the following JSON response object is given: diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5fe43ce..0949982 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,18 @@ # Release notes of angular-spring-data-rest +## Version 0.4.2 + +* Tag: [0.4.2](https://github.com/guylabs/angular-spring-data-rest/tree/0.4.2) +* Release: [angular-spring-data-rest-0.4.2.zip](https://github.com/guylabs/angular-spring-data-rest/releases/download/0.4.2/angular-spring-data-rest-0.4.2.zip) + +### Changes + +* Fixed issue [#9](https://github.com/guylabs/angular-spring-data-rest/issues/9). + +### Migration notes + +* No migration needed. + ## Version 0.4.1 * Tag: [0.4.1](https://github.com/guylabs/angular-spring-data-rest/tree/0.4.1) diff --git a/dist/angular-spring-data-rest.js b/dist/angular-spring-data-rest.js index 4bc30f5..5aec83c 100644 --- a/dist/angular-spring-data-rest.js +++ b/dist/angular-spring-data-rest.js @@ -160,6 +160,15 @@ angular.module("spring-data-rest").provider("SpringDataRestAdapter", function () var resources = this[config.linksKey]; var parameters = paramDefaults; + var urlTemplates = ""; + + // split the resourceObject to extract the URL templates for the $resource method + if(hasUrlTemplate(resourceObject)) { + var extractedUrlTemplates = extractUrlTemplates(resourceObject); + resourceObject = extractedUrlTemplates[0]; + urlTemplates = extractedUrlTemplates[1]; + } + // if a resource object is given process it if (angular.isObject(resourceObject)) { if (!resourceObject.name) { @@ -193,8 +202,8 @@ angular.module("spring-data-rest").provider("SpringDataRestAdapter", function () return resourcesFunction(getProcessedUrl(data, resourceObject.name), parameters, actions, options); } else if (resourceObject in resources) { - // process the url and call the resources function with the given parameters - return resourcesFunction(getProcessedUrl(data, resourceObject), parameters, actions, options); + // process the url, add the url templates and call the resources function with the given parameters + return resourcesFunction(getProcessedUrl(data, resourceObject) + urlTemplates, parameters, actions, options); } // return the available resources as resource object array if the resource object parameter is not set @@ -325,6 +334,27 @@ angular.module("spring-data-rest").provider("SpringDataRestAdapter", function () // extract the template parameters of the raw URL return extractUrl(rawUrl, data[config.linksKey][resourceName].templated); } + + /** + * Returns true if the resource name has URL templates + * @param resourceName the resource name to parse + * @returns {boolean} true if the resource name has URL templates, false otherwise + */ + function hasUrlTemplate(resourceName) { + return typeof resourceName == "string" && resourceName.indexOf("/") > 0; + } + + /** + * Extracts the URL template and returns the resource name and the URL templates as an array. + * @param resourceName the resource name to parse + * @returns {[]} the first element is the raw resource name and the second is the extracted URL templates + */ + function extractUrlTemplates(resourceName) { + if(hasUrlTemplate(resourceName)) { + var indexOfSlash = resourceName.indexOf("/"); + return [resourceName.substr(0, indexOfSlash), resourceName.substr(indexOfSlash, resourceName.length)]; + } + } }; // return an object with the processData function diff --git a/dist/angular-spring-data-rest.min.js b/dist/angular-spring-data-rest.min.js index 30aaa42..10a7d98 100644 --- a/dist/angular-spring-data-rest.min.js +++ b/dist/angular-spring-data-rest.min.js @@ -3,4 +3,4 @@ * Copyright 2015 Guy Brand (@guy_labs) * https://github.com/guylabs/angular-spring-data-rest */ -!function(){"use strict";function a(b){return angular.forEach(arguments,function(c){c!==b&&angular.forEach(c,function(c,d){b[d]&&b[d].constructor&&b[d].constructor===Object?a(b[d],c):b[d]=c})}),angular.copy(b)}function b(a,b,c,d){var e=a[b];if(e){var f={};if(f[c]={},d===!0)angular.forEach(Object.keys(e),function(a){f[c][a]=e[a]});else{var g=Object.keys(e)[0];f[c]=e[g]}a=angular.extend(a,f),delete a[b]}return a}function c(a,b){return b&&(a=e(a)),a}function d(a,b,c){if(void 0==a||!a)throw new Error("The provided resource name '"+b+"' has no valid URL in the '"+c+"' property.");return a}function e(a){return a.replace(/{.*}/g,"")}function f(a){var b={},c=/{\?(.*)}/g,d=c.exec(a)[1].split(",");return angular.forEach(d,function(a){b[a]=""}),b}angular.module("spring-data-rest",["ngResource"]),angular.module("spring-data-rest").provider("SpringDataRestAdapter",function(){var e={linksKey:"_links",linksHrefKey:"href",linksSelfLinkName:"self",embeddedKey:"_embedded",embeddedNewKey:"_embeddedItems",embeddedNamedResources:!1,resourcesKey:"_resources",resourcesFunction:void 0,fetchFunction:void 0,fetchAllKey:"_allLinks"};return{config:function(b){if("undefined"!=typeof b){if(!angular.isObject(b))throw new Error("The given configuration '"+b+"' is not an object.");if(void 0!=b.resourcesFunction&&"function"!=typeof b.resourcesFunction)throw new Error("The given resource function '"+b.resourcesFunction+"' is not of type function.");if(void 0!=b.fetchFunction&&"function"!=typeof b.fetchFunction)throw new Error("The given fetch function '"+b.fetchFunction+"' is not of type function.");e=a(e,b)}return e},$get:["$injector",function(a){function g(b,c,d,f){return void 0==e.resourcesFunction?a.get("$resource")(b,c,d,f):e.resourcesFunction(b,c,d,f)}function h(b,c,d,f,g){if(void 0==e.fetchFunction){var h=[];return h.push(a.get("$http").get(b).then(function(a){return g?i(a.data,f,!0).then(function(a){d[c]=a}):i(a.data).then(function(a){d[c]=a})},function(b){return 404!=b.status?a.get("$q").reject(b):void 0})),a.get("$q").all(h)}return e.fetchFunction(b,c,d,f,g)}var i=function j(i,k,l){function m(a,b){var f=d(a[e.linksKey][b][e.linksHrefKey],b,e.linksHrefKey);return c(f,a[e.linksKey][b].templated)}return a.get("$q").when(i).then(function(c){var d=function(a,b,d,h){var i=this[e.linksKey],j=b;if(angular.isObject(a)){if(!a.name)throw new Error("The provided resource object must contain a name property.");var k=a.parameters;return b&&angular.isObject(b)?j=k&&angular.isObject(k)?angular.extend(angular.copy(b),angular.copy(k)):angular.copy(b):k&&angular.isObject(k)&&(j=angular.copy(k)),angular.forEach(j,function(a,b){""===a&&delete j[b]}),g(m(c,a.name),j,d,h)}if(a in i)return g(m(c,a),j,d,h);var l=[];return angular.forEach(i,function(a,b){if(a.templated){var c=f(a[e.linksHrefKey]);l.push({name:b,parameters:c})}else l.push({name:b})}),l};if(c&&c.data&&(c=c.data),!angular.isObject(c)||c instanceof Array)return a.get("$q").reject("Given data '"+c+"' is not of type object.");if(k&&!(k instanceof Array||"string"==typeof k))return a.get("$q").reject("Given fetch links '"+k+"' is not of type array or string.");var i=void 0,n=[];if(e.linksKey in c){var o={};o[e.resourcesKey]=d,i=angular.extend(angular.copy(c),o),void 0!=k&&angular.forEach(c[e.linksKey],function(a,b){b!=e.linksSelfLinkName&&(k==e.fetchAllKey||"string"==typeof k&&b==k||k instanceof Array&&k.indexOf(b)>=0)&&n.push(h(m(c,b),b,i,k,l))})}return e.embeddedKey in c&&(i||(i=angular.copy(c)),i=b(i,e.embeddedKey,e.embeddedNewKey,e.embeddedNamedResources),angular.forEach(i[e.embeddedNewKey],function(a,b){if(a instanceof Array&&a.length>0){var c,d=[];angular.forEach(a,function(a,b){c=j({data:a},k,l).then(function(a){d[b]=a}),n.push(c)}),c&&c.then(function(){i[e.embeddedNewKey][b]=d})}else n.push(j({data:a},k,l).then(function(a){i[e.embeddedNewKey][b]=a}))})),a.get("$q").all(n).then(function(){return i?i:c})})};return{process:i}}]}}),angular.module("spring-data-rest").provider("SpringDataRestInterceptor",["$httpProvider","SpringDataRestAdapterProvider",function(a){return{apply:function(){a.interceptors.push("SpringDataRestInterceptor")},$get:["SpringDataRestAdapter","$q",function(a){return{response:function(b){return a.process(b.data).then(function(a){return b.data=a,b})}}}]}}])}(); \ No newline at end of file +!function(){"use strict";function a(b){return angular.forEach(arguments,function(c){c!==b&&angular.forEach(c,function(c,d){b[d]&&b[d].constructor&&b[d].constructor===Object?a(b[d],c):b[d]=c})}),angular.copy(b)}function b(a,b,c,d){var e=a[b];if(e){var f={};if(f[c]={},d===!0)angular.forEach(Object.keys(e),function(a){f[c][a]=e[a]});else{var g=Object.keys(e)[0];f[c]=e[g]}a=angular.extend(a,f),delete a[b]}return a}function c(a,b){return b&&(a=e(a)),a}function d(a,b,c){if(void 0==a||!a)throw new Error("The provided resource name '"+b+"' has no valid URL in the '"+c+"' property.");return a}function e(a){return a.replace(/{.*}/g,"")}function f(a){var b={},c=/{\?(.*)}/g,d=c.exec(a)[1].split(",");return angular.forEach(d,function(a){b[a]=""}),b}angular.module("spring-data-rest",["ngResource"]),angular.module("spring-data-rest").provider("SpringDataRestAdapter",function(){var e={linksKey:"_links",linksHrefKey:"href",linksSelfLinkName:"self",embeddedKey:"_embedded",embeddedNewKey:"_embeddedItems",embeddedNamedResources:!1,resourcesKey:"_resources",resourcesFunction:void 0,fetchFunction:void 0,fetchAllKey:"_allLinks"};return{config:function(b){if("undefined"!=typeof b){if(!angular.isObject(b))throw new Error("The given configuration '"+b+"' is not an object.");if(void 0!=b.resourcesFunction&&"function"!=typeof b.resourcesFunction)throw new Error("The given resource function '"+b.resourcesFunction+"' is not of type function.");if(void 0!=b.fetchFunction&&"function"!=typeof b.fetchFunction)throw new Error("The given fetch function '"+b.fetchFunction+"' is not of type function.");e=a(e,b)}return e},$get:["$injector",function(a){function g(b,c,d,f){return void 0==e.resourcesFunction?a.get("$resource")(b,c,d,f):e.resourcesFunction(b,c,d,f)}function h(b,c,d,f,g){if(void 0==e.fetchFunction){var h=[];return h.push(a.get("$http").get(b).then(function(a){return g?i(a.data,f,!0).then(function(a){d[c]=a}):i(a.data).then(function(a){d[c]=a})},function(b){return 404!=b.status?a.get("$q").reject(b):void 0})),a.get("$q").all(h)}return e.fetchFunction(b,c,d,f,g)}var i=function j(i,k,l){function m(a,b){var f=d(a[e.linksKey][b][e.linksHrefKey],b,e.linksHrefKey);return c(f,a[e.linksKey][b].templated)}function n(a){return"string"==typeof a&&a.indexOf("/")>0}function o(a){if(n(a)){var b=a.indexOf("/");return[a.substr(0,b),a.substr(b,a.length)]}}return a.get("$q").when(i).then(function(c){var d=function(a,b,d,h){var i=this[e.linksKey],j=b,k="";if(n(a)){var l=o(a);a=l[0],k=l[1]}if(angular.isObject(a)){if(!a.name)throw new Error("The provided resource object must contain a name property.");var p=a.parameters;return b&&angular.isObject(b)?j=p&&angular.isObject(p)?angular.extend(angular.copy(b),angular.copy(p)):angular.copy(b):p&&angular.isObject(p)&&(j=angular.copy(p)),angular.forEach(j,function(a,b){""===a&&delete j[b]}),g(m(c,a.name),j,d,h)}if(a in i)return g(m(c,a)+k,j,d,h);var q=[];return angular.forEach(i,function(a,b){if(a.templated){var c=f(a[e.linksHrefKey]);q.push({name:b,parameters:c})}else q.push({name:b})}),q};if(c&&c.data&&(c=c.data),!angular.isObject(c)||c instanceof Array)return a.get("$q").reject("Given data '"+c+"' is not of type object.");if(k&&!(k instanceof Array||"string"==typeof k))return a.get("$q").reject("Given fetch links '"+k+"' is not of type array or string.");var i=void 0,p=[];if(e.linksKey in c){var q={};q[e.resourcesKey]=d,i=angular.extend(angular.copy(c),q),void 0!=k&&angular.forEach(c[e.linksKey],function(a,b){b!=e.linksSelfLinkName&&(k==e.fetchAllKey||"string"==typeof k&&b==k||k instanceof Array&&k.indexOf(b)>=0)&&p.push(h(m(c,b),b,i,k,l))})}return e.embeddedKey in c&&(i||(i=angular.copy(c)),i=b(i,e.embeddedKey,e.embeddedNewKey,e.embeddedNamedResources),angular.forEach(i[e.embeddedNewKey],function(a,b){if(a instanceof Array&&a.length>0){var c,d=[];angular.forEach(a,function(a,b){c=j({data:a},k,l).then(function(a){d[b]=a}),p.push(c)}),c&&c.then(function(){i[e.embeddedNewKey][b]=d})}else p.push(j({data:a},k,l).then(function(a){i[e.embeddedNewKey][b]=a}))})),a.get("$q").all(p).then(function(){return i?i:c})})};return{process:i}}]}}),angular.module("spring-data-rest").provider("SpringDataRestInterceptor",["$httpProvider","SpringDataRestAdapterProvider",function(a){return{apply:function(){a.interceptors.push("SpringDataRestInterceptor")},$get:["SpringDataRestAdapter","$q",function(a){return{response:function(b){return a.process(b.data).then(function(a){return b.data=a,b})}}}]}}])}(); \ No newline at end of file diff --git a/src/angular-spring-data-rest-provider.js b/src/angular-spring-data-rest-provider.js index dc64d46..983b396 100644 --- a/src/angular-spring-data-rest-provider.js +++ b/src/angular-spring-data-rest-provider.js @@ -148,6 +148,15 @@ angular.module("spring-data-rest").provider("SpringDataRestAdapter", function () var resources = this[config.linksKey]; var parameters = paramDefaults; + var urlTemplates = ""; + + // split the resourceObject to extract the URL templates for the $resource method + if(hasUrlTemplate(resourceObject)) { + var extractedUrlTemplates = extractUrlTemplates(resourceObject); + resourceObject = extractedUrlTemplates[0]; + urlTemplates = extractedUrlTemplates[1]; + } + // if a resource object is given process it if (angular.isObject(resourceObject)) { if (!resourceObject.name) { @@ -181,8 +190,8 @@ angular.module("spring-data-rest").provider("SpringDataRestAdapter", function () return resourcesFunction(getProcessedUrl(data, resourceObject.name), parameters, actions, options); } else if (resourceObject in resources) { - // process the url and call the resources function with the given parameters - return resourcesFunction(getProcessedUrl(data, resourceObject), parameters, actions, options); + // process the url, add the url templates and call the resources function with the given parameters + return resourcesFunction(getProcessedUrl(data, resourceObject) + urlTemplates, parameters, actions, options); } // return the available resources as resource object array if the resource object parameter is not set @@ -313,6 +322,27 @@ angular.module("spring-data-rest").provider("SpringDataRestAdapter", function () // extract the template parameters of the raw URL return extractUrl(rawUrl, data[config.linksKey][resourceName].templated); } + + /** + * Returns true if the resource name has URL templates + * @param resourceName the resource name to parse + * @returns {boolean} true if the resource name has URL templates, false otherwise + */ + function hasUrlTemplate(resourceName) { + return typeof resourceName == "string" && resourceName.indexOf("/") > 0; + } + + /** + * Extracts the URL template and returns the resource name and the URL templates as an array. + * @param resourceName the resource name to parse + * @returns {[]} the first element is the raw resource name and the second is the extracted URL templates + */ + function extractUrlTemplates(resourceName) { + if(hasUrlTemplate(resourceName)) { + var indexOfSlash = resourceName.indexOf("/"); + return [resourceName.substr(0, indexOfSlash), resourceName.substr(indexOfSlash, resourceName.length)]; + } + } }; // return an object with the processData function diff --git a/test/angular-spring-data-rest-provider.resources.spec.js b/test/angular-spring-data-rest-provider.resources.spec.js index 1933478..9776ca1 100644 --- a/test/angular-spring-data-rest-provider.resources.spec.js +++ b/test/angular-spring-data-rest-provider.resources.spec.js @@ -440,5 +440,31 @@ describe("the resources property", function () { this.rootScope.$apply(); }); + it("it must extract the URL templates and add it as suffix to the proper URL", function () { + var resourcesKey = this.config.resourcesKey; + var url = undefined; + var resourcesFunctionConfiguration = { + 'resourcesFunction': function (inUrl) { + url = inUrl; + return 'foo'; + } + }; + + // define the resource name and the correct resource href url + var resourceObject = "self/:id"; + var resourceHref = "http://localhost:8080/categories"; + + // set the new resource function with the given parameters + springDataRestAdapterProvider.config(resourcesFunctionConfiguration); + SpringDataRestAdapter.process(this.rawResponse).then(function (processedData) { + // call the new resource method and expect the response and the call to the method + var resourceResponse = processedData[resourcesKey](resourceObject); + expect(resourceResponse).toEqual('foo'); + expect(url).toEqual(resourceHref + '/:id'); + }); + + this.rootScope.$apply(); + }); + });