diff --git a/src/ngResource/resource.js b/src/ngResource/resource.js index c1d6ec8a818b..2796627d3443 100644 --- a/src/ngResource/resource.js +++ b/src/ngResource/resource.js @@ -195,6 +195,10 @@ function shallowClearAndCopy(src, dst) { * with `http response` object. See {@link ng.$http $http interceptors}. * - **`hasBody`** - `{boolean}` - allows to specify if a request body should be included or not. * If not specified only POST, PUT and PATCH requests will have a body. + * - **`arrayDecorate`** - `{boolean}` - Allows resource to be decorated by additional + * non-array-indice object properties. Useful where a collection contains meta-data. + * - **`errorDecorate`** - `{boolean}` - Allows resource to be decorated by properties of a + * rejected response. Useful where request error returns useful error messages. * * @param {Object} options Hash with custom settings that should extend the * default `$resourceProvider` behavior. The supported options are: @@ -744,39 +748,62 @@ angular.module('ngResource', ['ng']). extend({}, extractParams(data, action.params || {}), params), action.url); - var promise = $http(httpConfig).then(function(response) { - var data = response.data; - - if (data) { - // Need to convert action.isArray to boolean in case it is undefined - if (isArray(data) !== (!!action.isArray)) { - throw $resourceMinErr('badcfg', - 'Error in resource configuration for action `{0}`. Expected response to ' + - 'contain an {1} but got an {2} (Request: {3} {4})', name, action.isArray ? 'array' : 'object', - isArray(data) ? 'array' : 'object', httpConfig.method, httpConfig.url); - } - if (action.isArray) { - value.length = 0; - forEach(data, function(item) { - if (typeof item === 'object') { - value.push(new Resource(item)); - } 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); + var promise = $http(httpConfig).then( + function(response) { + var data = response.data; + + if (data) { + // Need to convert action.isArray to boolean in case it is undefined + if (isArray(data) !== (!!action.isArray)) { + throw $resourceMinErr('badcfg', + 'Error in resource configuration for action `{0}`. Expected response to ' + + 'contain an {1} but got an {2} (Request: {3} {4})', name, action.isArray ? 'array' : 'object', + isArray(data) ? 'array' : 'object', httpConfig.method, httpConfig.url); + } + if (action.isArray) { + value.length = 0; + // Decorate the array with the properties on the response data + if (action.arrayDecorate) { + for (var i in data) { + if (data.hasOwnProperty(i) && angular.isUndefined(value[i]) && !/^[0-9]+$/.test(i)) { + value[i] = data[i]; + } + } } - }); - } else { - var promise = value.$promise; // Save the promise - shallowClearAndCopy(data, value); - value.$promise = promise; // Restore the promise + forEach(data, function(item) { + if (typeof item === 'object') { + value.push(new Resource(item)); + } 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 { + var promise = value.$promise; // Save the promise + shallowClearAndCopy(data, value); + value.$promise = promise; // Restore the promise + } } - } - response.resource = value; + response.resource = value; - return response; - }); + return response; + }, + function(response) { + // Decorate the existing object with the properties on the response data + if (response && action.errorDecorate && (!action.isArray || action.arrayDecorate)) { + var data = response.data; + for (var i in data) { + if (data.hasOwnProperty(i) && !/^[0-9]+$/.test(i)) { + value[i] = data[i]; + } + } + response.resource = value; + } + return $q.reject(response); + } + ); promise = promise['finally'](function() { value.$resolved = true; diff --git a/test/ngResource/resourceSpec.js b/test/ngResource/resourceSpec.js index bc8bbd079642..0e30f746ffd5 100644 --- a/test/ngResource/resourceSpec.js +++ b/test/ngResource/resourceSpec.js @@ -1603,6 +1603,177 @@ describe('basic usage', function() { }); }); +describe('property decoration', function() { + var $httpBackend, $resource, + callback, errorCB; + + beforeEach(module('ngResource')); + + beforeEach(inject(function(_$httpBackend_, _$resource_) { + $httpBackend = _$httpBackend_; + $resource = _$resource_; + })); + + beforeEach(function() { + callback = jasmine.createSpy('callback'); + errorCB = jasmine.createSpy('error'); + }); + + it('should not decorate array', function() { + var result = [ + { thing: 'value1' }, + { thing: 'value2' } + ]; + result.meta = 'data'; + + $httpBackend.expectGET('URL').respond({}); + var models = $resource('URL', { }, { + query: { + method: 'GET', + isArray: true, + transformResponse: function() { + return result; + } + } + }).query(); + $httpBackend.flush(); + + // Should have array and no metadata + expect(models[0]).toEqual(jasmine.objectContaining(result[0])); + expect(models[1]).toEqual(jasmine.objectContaining(result[1])); + expect(models.meta).toBeUndefined(); + }); + + it('should decorate array', function() { + var result = [ + { thing: 'value1' }, + { thing: 'value2' } + ]; + result.meta = 'data'; + + $httpBackend.expectGET('URL').respond({}); + var models = $resource('URL', { }, { + query: { + method: 'GET', + isArray: true, + arrayDecorate: true, + transformResponse: function() { + return result; + } + } + }).query(); + $httpBackend.flush(); + + // Should have array and metadata + expect(models[0]).toEqual(jasmine.objectContaining(result[0])); + expect(models[1]).toEqual(jasmine.objectContaining(result[1])); + expect(models.meta).toBe('data'); + }); + + it('should not decorate on error', function() { + var model = { + untouched: true, + error: 'No Error' + }; + + model = new ($resource('URL', { }, { + save: { + method: 'POST' + } + }))(model); + + // Should have the config we passed the constructor + expect(model.untouched).toBe(true); + expect(model.error).toBe('No Error'); + + $httpBackend.expectPOST('URL').respond(500, { error: 'An error occurred' }); + model.$save(angular.noop, angular.noop); + $httpBackend.flush(); + + // Should have the same config as before; not decorated with props from response + expect(model.untouched).toBe(true); + expect(model.error).toBe('No Error'); + }); + + it('should decorate on error', function() { + var model = { + untouched: true, + error: 'No Error' + }; + + model = new ($resource('URL', { }, { + save: { + method: 'POST', + errorDecorate: true + } + }))(model); + + // Should have the config we passed the constructor + expect(model.untouched).toBe(true); + expect(model.error).toBe('No Error'); + + $httpBackend.expectPOST('URL').respond(500, { error: 'An error occurred' }); + + model.$save(callback, errorCB); + $httpBackend.flush(); + + // Should have the same config as before; decorated with props from response + expect(callback).not.toHaveBeenCalled(); + expect(errorCB).toHaveBeenCalledOnce(); + expect(model.untouched).toBe(true); + expect(model.error).toBe('An error occurred'); + }); + + it('should not decorate array on error', function() { + var result = []; + result.meta = 'data'; + + $httpBackend.expectGET('URL').respond(500, { error: 'An error occurred' }); + var models = $resource('URL', { }, { + query: { + method: 'GET', + isArray: true, + errorDecorate: true, + transformResponse: function() { + return result; + } + } + }).query(callback, errorCB); + $httpBackend.flush(); + + // Should have the same config as before; decorated with props from response + expect(callback).not.toHaveBeenCalled(); + expect(errorCB).toHaveBeenCalledOnce(); + expect(angular.isArray(models)).toBeTruthy(); + expect(models.meta).toBeUndefined(); + }); + + it('should decorate array on error', function() { + var result = []; + result.meta = 'data'; + + $httpBackend.expectGET('URL').respond(500, { error: 'An error occurred' }); + var models = $resource('URL', { }, { + query: { + method: 'GET', + isArray: true, + arrayDecorate: true, + errorDecorate: true, + transformResponse: function() { + return result; + } + } + }).query(callback, errorCB); + $httpBackend.flush(); + + // Should have the same config as before; decorated with props from response + expect(callback).not.toHaveBeenCalled(); + expect(errorCB).toHaveBeenCalledOnce(); + expect(angular.isArray(models)).toBeTruthy(); + expect(models.meta).toBe('data'); + }); +}); + describe('extra params', function() { var $http; var $httpBackend;