├── CHANGELOG.md ├── bower.json ├── LICENSE ├── test └── NestedResrourceSpec.js ├── README.md └── src └── angular-nested-resource.js /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-nested-resource", 3 | "version": "2.0.0", 4 | "homepage": "https://github.com/roypeled/angular-nested-resource", 5 | "authors": [ 6 | "Roy Peled " 7 | ], 8 | "description": "A lightweight angular module that helps working with RESTful models. I was researching restangular and didn't like the fact that it was dependant on lodash and the dact that the models weren't configurable.", 9 | "main": "src/angular-nested-resource.js", 10 | "keywords": [ 11 | "REST", 12 | "Angular", 13 | "AngularJS", 14 | "resource", 15 | "nested" 16 | ], 17 | "license": "MIT", 18 | "ignore": [ 19 | "**/.*", 20 | "node_modules", 21 | "bower_components", 22 | "test", 23 | "tests" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Roy Peled 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /test/NestedResrourceSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var module = angular.module('module',['ngResource', 'ngNestedResource']); 4 | 5 | describe("Nested Resource", function(){ 6 | 7 | 8 | beforeEach(module('module')); 9 | 10 | var organizations, httpBackend; 11 | 12 | 13 | beforeEach(inject(function($httpBackend, nestedResource){ 14 | 15 | httpBackend = $httpBackend; 16 | 17 | organizations = nestedResource("/api/organizations/", { 18 | getAll: {method: "GET", isArray: true}, 19 | create: {method: "POST"}, 20 | get: { 21 | route: "@/", 22 | method: "GET", 23 | nested: { 24 | update: { method: "PUT" }, 25 | delete: { method: "DELETE" }, 26 | workers: { 27 | method: "GET", 28 | route: "workers/", 29 | isArray: true, 30 | nested: { 31 | update: { 32 | method: "PUT", 33 | route: "@/" 34 | } 35 | } 36 | } 37 | } 38 | } 39 | }); 40 | })); 41 | 42 | it("should verify all calls are handled correctly", function(){ 43 | 44 | organizations.getAll(); 45 | httpBackend.expectGET("/api/organizations").respond([{id:1, name:"Google.com"}]); 46 | httpBackend.flush(); 47 | 48 | organizations.create({name: "Google.com"}); 49 | httpBackend.expectPOST("/api/organizations").respond({id:1, name:"Google.com"}); 50 | httpBackend.flush(); 51 | 52 | var organization = organizations.get(1); 53 | httpBackend.expectGET("/api/organizations/1").respond({id:1, name:"Google.com"}); 54 | httpBackend.flush(); 55 | 56 | organization.$update({name: "Bing.com"}); 57 | httpBackend.expectPUT("/api/organizations/1", {name: "Bing.com"}).respond({id:1, name:"Bing.com"}); 58 | httpBackend.flush(); 59 | 60 | var workers = organization.$workers(); 61 | httpBackend.expectGET("/api/organizations/1/workers?").respond([{id:264, name:"No Name"}]); 62 | httpBackend.flush(); 63 | 64 | workers.$update(264, {name: "Steve Ballmer"}); 65 | httpBackend.expectPUT("/api/organizations/1/workers", {param:264, name: "Steve Ballmer"}) 66 | .respond({id:264, name:"Steve Ballmer"}); 67 | httpBackend.flush(); 68 | 69 | }) 70 | 71 | 72 | }) 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | angular-nested-resource 2 | ====================== 3 | 4 | A lightweight angular module that helps working with RESTful models. 5 | I was researching restangular and didn't like the fact that it was dependant on lodash and the dact that the models weren't configurable. 6 | 7 | I liked ng-resources approach, but the implementation is horrible with nested objects. 8 | 9 | So here is my solution. 10 | 11 | ### Version 2.0 12 | - Removed dependency with ngResource, now this code duplicates $resource's behavior. 13 | - Refactored the entire process, code is much more organized. 14 | - Added support to define a type for a resource which will be accesible on the $$type flag. 15 | - Added better promise behavior. 16 | - Added 'constructor' flag which will allow wrapping of objects in an inner actions. 17 | - Added 'iterator' function to manipulate wrapping of array results. 18 | 19 | 20 | ## Usage 21 | To use this module you must add the JS file to your HTML and add `ngNestedResource` as a dependency to your app: 22 | ```html 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ... 32 | 33 | ``` 34 | ```js 35 | angular.module("app", ["ngNestedResource"]); 36 | ``` 37 | 38 | ### Basic root object 39 | Nested resource is using angular's own resource module as a scaffolding. So you can use the original resource configuration: 40 | ```js 41 | 42 | var organizations = nestedResource("/api/organizations/", { 43 | getAll: {method: "GET", isArray: true}, 44 | create: {method: "POST"} 45 | }); 46 | 47 | organizations.getAll(); 48 | // GET: "/api/organizations/" 49 | organizations.create({name: "Google.com"}); 50 | // POST: "/api/organizations/", payload: "{name: 'Google.com'}" 51 | // returns { id:1, name: "Google.com"} 52 | 53 | ``` 54 | Each method will behave like a regular resource object. 55 | 56 | ### Creating nested objects 57 | ```js 58 | var organizations = nestedResource("/api/organizations/", { 59 | getAll: {method: "GET", isArray: true}, 60 | create: {method: "POST"}, 61 | get: { 62 | route: "@id/", 63 | method: "GET" 64 | } 65 | }); 66 | 67 | organizations.get(1); 68 | // GET: "/api/organizations/1/" 69 | // returns { id:1, name: "Google.com"} 70 | 71 | organizations.get({id: 1}); 72 | // GET: "/api/organizations/1/" 73 | // returns { id:1, name: "Google.com"} 74 | ``` 75 | When specifying a route to an action you can add a wildcard (@) which will be replaced with the parameter supplied to the action, or a field name (@id) which will be replaced from the same field name in a supplied object. 76 | 77 | ### Extending a nested object 78 | ```js 79 | var organizations = nestedResource("/api/organizations/", { 80 | get: { 81 | route: "@/", 82 | method: "GET", 83 | nested: { 84 | update: { method: "PUT" }, 85 | delete: { method: "DELETE" }, 86 | workers: { 87 | method: "GET", 88 | route: "workers/", 89 | isArray: true, 90 | nested: { 91 | update: { 92 | method: "PUT", 93 | route: "@/" 94 | } 95 | } 96 | } 97 | } 98 | } 99 | }); 100 | 101 | var organization = organizations.get(1); 102 | // GET: "/api/organizations/1/" 103 | // returns { id:1, name: "Google.com"} 104 | 105 | organization.$update({name: "Bing.com"}); 106 | // PUT: "/api/organizations/1/", payload: "{name: 'Bing.com'}" 107 | // returns { id:1, name: "Bing.com"} 108 | 109 | var workers = organization.$workers(); 110 | // GET: "/api/organizations/1/workers/" 111 | // returns [...] 112 | 113 | workers.$update(264, {name: "Steve Ballmer"}); 114 | // PUT: "/api/organizations/1/workers/264/", payload: "{name: 'Steve Ballmer'}" 115 | ``` 116 | You can nest objects using the 'nested' property, and just follow the same pattern inside. 117 | 118 | 119 | ### Creating a new resource with existing data 120 | 121 | ```js 122 | var Organizations = nestedResource("/api/organizations/", { 123 | get: { 124 | route: "@id/", 125 | method: "GET", 126 | construct: true, 127 | nested: { 128 | update: { method: "PUT" }, 129 | delete: { method: "DELETE" }, 130 | workers: { 131 | method: "GET", 132 | route: "workers/", 133 | isArray: true, 134 | nested: { 135 | update: { 136 | method: "PUT", 137 | route: "@/" 138 | } 139 | } 140 | } 141 | } 142 | } 143 | }); 144 | 145 | var org = { 146 | id: 294, 147 | name: "Google Inc." 148 | } 149 | 150 | var orgResource = new Organizations(org); 151 | orgResource.$delete() 152 | // DELETE: "/api/organizations/294/" 153 | ``` 154 | A constructor action will use its nested actions to wrap a new object. When defining a constructor action define which property name should be used in the route (@id -> id: 294). 155 | 156 | ### Defining type 157 | 158 | ```js 159 | var Organizations = nestedResource("/api/organizations/", { 160 | getAll: {method: "GET", isArray: true}, 161 | create: {method: "POST"}, 162 | get: { 163 | route: "@/", 164 | isArray: true, 165 | method: "GET" 166 | } 167 | }, "organization"); 168 | 169 | Organizations.$$type; 170 | // "organization" 171 | 172 | var org = Organizations.get(1); 173 | org[0].$$type; 174 | // "organization" 175 | 176 | var org = new Organizations(); 177 | org.$$type; 178 | // "organization" 179 | ``` 180 | A type is added for every nestedResource object. 181 | 182 | 183 | ### Async Handling 184 | 185 | This module uses $q's promises: 186 | 187 | ```js 188 | var Organizations = nestedResource("/api/organizations/", { 189 | getAll: {method: "GET", isArray: true}, 190 | create: {method: "POST"}, 191 | get: { 192 | route: "@/", 193 | isArray: true, 194 | method: "GET" 195 | } 196 | }); 197 | 198 | Organizations.get(1, onSuccess, onError); 199 | 200 | funciton onSuccess(org){ 201 | org.id; // 1; 202 | } 203 | 204 | function onError(e){ 205 | // Something went wrong... 206 | } 207 | ``` 208 | 209 | A nested action has a maximum of 4 parameters: 210 | ```js 211 | Organizations.get(routeParam (string|integer), payloadObject, onSuccess, onError); 212 | ``` 213 | All parameters are optional. 214 | -------------------------------------------------------------------------------- /src/angular-nested-resource.js: -------------------------------------------------------------------------------- 1 | /**! 2 | * AngularJS Nested Resource 3 | * @author Roy 4 | * @version 2.0.0 5 | */ 6 | (function() { 7 | var angularNested = angular.module('ngNestedResource', []); 8 | 9 | angularNested.factory('nestedResource', ['$http', '$q', function($http, $q) { 10 | 11 | 12 | var noop = angular.noop, 13 | forEach = angular.forEach, 14 | extend = angular.extend, 15 | copy = angular.copy, 16 | isFunction = angular.isFunction, 17 | isObject = angular.isObject; 18 | 19 | var KEYS_TO_REMOVE = { 20 | params: true, isArray: true, interceptor: true, construct: true, nested: true, route: true 21 | } 22 | 23 | 24 | function cleanParams(params){ 25 | var result = {}; 26 | forEach(params, function(value, key){ 27 | if(!/^\$/.test(key)) 28 | result[key] = value; 29 | }); 30 | return result; 31 | } 32 | 33 | function defaultResponseInterceptor(response) { 34 | return response; 35 | } 36 | 37 | function Route(template){ 38 | var builtRoute = template; 39 | 40 | return { 41 | build: function(param){ 42 | if(typeof param == "object") { 43 | builtRoute = template.replace(/@([^/]*)/, function(match, group){ 44 | return param[group]; 45 | }); 46 | } else 47 | builtRoute = template.replace(/@([^/]*)/, param); 48 | 49 | isBuilt = true; 50 | }, 51 | parameterize: function(obj){ 52 | if(!obj) 53 | return; 54 | 55 | builtRoute = builtRoute.replace(/:([^/]*)/gi, function(match, group){ 56 | return obj[group]; 57 | }); 58 | }, 59 | url: function(){ 60 | return builtRoute; 61 | }, 62 | template: function(){ 63 | return template; 64 | } 65 | 66 | } 67 | } 68 | 69 | return (function Factory(root, setup, type){ 70 | 71 | var constructObject, rootObject; 72 | 73 | type = type || "resource"; 74 | 75 | function NestedAction(parentRoute, setupParams){ 76 | 77 | var currentRoute, httpConfig = {}, parameters, FutureActions; 78 | 79 | var DEFAULT_SETTINGS = { 80 | method: "GET", 81 | route: "" 82 | } 83 | 84 | forEach(setupParams, function(value, key) { 85 | if (!KEYS_TO_REMOVE[key]) { 86 | httpConfig[key] = copy(value); 87 | } 88 | }); 89 | 90 | return function callNestedAction(param, payload, success, error){ 91 | /** 92 | * Fallback variables 93 | */ 94 | if(isObject(param)){ 95 | error = success; 96 | success = payload 97 | payload = param; 98 | param = null; 99 | } else if(isFunction(param)){ 100 | error = payload; 101 | success = param; 102 | payload = {}; 103 | param = null; 104 | } else if(isFunction(payload)){ 105 | error = success; 106 | success = payload; 107 | payload = {}; 108 | } 109 | 110 | parameters = copy(payload || {}); 111 | extend(parameters, this); 112 | 113 | currentRoute = new Route(parentRoute.url() + (setupParams.route || DEFAULT_SETTINGS.route)); 114 | FutureActions = innerStepFactory(currentRoute, setupParams.nested); 115 | 116 | currentRoute.build(param || parameters); 117 | currentRoute.parameterize(parameters); 118 | 119 | httpConfig.url = currentRoute.url(); 120 | 121 | // If the request if GET, send in querystring only the supplied arguments. Else (PUT, POST, DELETE), send the entire object: 122 | if(httpConfig.method == "GET") 123 | httpConfig.params = payload; 124 | else 125 | httpConfig.data = cleanParams(parameters); 126 | 127 | var responseInterceptor = setupParams.interceptor && setupParams.interceptor.response || 128 | defaultResponseInterceptor; 129 | var responseErrorInterceptor = setupParams.interceptor && setupParams.interceptor.responseError || 130 | undefined; 131 | 132 | var value; 133 | if(setupParams.isArray) 134 | value = []; 135 | else if(!setupParams.scoped) 136 | value = this.$$type ? this : new FutureActions(this); 137 | else 138 | value = new FutureActions(); 139 | 140 | var result = $http(httpConfig) 141 | .then(function(response){ 142 | var data = response.data; 143 | 144 | if (data) { 145 | if (setupParams.isArray) { 146 | if(setupParams.iterator){ 147 | var wrapped = setupParams.iterator(data, function(item){ 148 | return new FutureActions(item, true); 149 | }); 150 | value = value.concat(wrapped); 151 | } else { 152 | for (var i = 0; i < data.length; i++) { 153 | value.push(new FutureActions(data[i], true)); 154 | } 155 | } 156 | } else if(isObject(data)) { 157 | extend(value, data); 158 | } 159 | } 160 | 161 | response.resource = value; 162 | 163 | return response; 164 | 165 | }, function(response) { 166 | 167 | (error||noop)(response); 168 | 169 | return $q.reject(response); 170 | }); 171 | 172 | result = result.then( 173 | function(response) { 174 | var value = responseInterceptor(response.resource); 175 | (success||noop)(value, response.headers); 176 | return value; 177 | }, 178 | responseErrorInterceptor); 179 | 180 | return value; 181 | 182 | } 183 | 184 | } 185 | 186 | function rootStepFactory(baseRoute, setupActions){ 187 | var actions = {}; 188 | 189 | function NestedResource(data){ 190 | if(data && constructObject) 191 | data = new constructObject(data); 192 | else 193 | extend(this, actions); 194 | extend(this, data); 195 | } 196 | 197 | NestedResource.prototype["$$type"] = type; 198 | 199 | forEach(setupActions, function(action, name){ 200 | actions[name] = new NestedAction(baseRoute, action); 201 | }); 202 | 203 | extend(NestedResource, actions); 204 | 205 | return NestedResource; 206 | } 207 | 208 | function innerStepFactory(baseRoute, setupActions){ 209 | if(!setupActions) 210 | return constructObject || rootObject; 211 | 212 | var actions = {}; 213 | 214 | function NestedResource(data){ 215 | extend(this, data); 216 | extend(this, actions); 217 | } 218 | 219 | actions["$$type"] = type; 220 | 221 | forEach(setupActions, function(action, name){ 222 | actions["$" + name] = new NestedAction(baseRoute, action); 223 | }); 224 | 225 | return NestedResource; 226 | } 227 | 228 | function constructStepFactory(baseRoute, setupActions){ 229 | forEach(setupActions, function(action, name){ 230 | if(action.construct) { 231 | var newRoute = new Route(baseRoute.url() + action.route) 232 | constructObject = innerStepFactory(newRoute, action.nested); 233 | } 234 | }); 235 | } 236 | 237 | var root = new Route(root); 238 | 239 | constructStepFactory(root, setup); 240 | 241 | rootObject = rootStepFactory(root, setup); 242 | 243 | return rootObject; 244 | 245 | }) 246 | 247 | }]); 248 | })(); 249 | --------------------------------------------------------------------------------