/**
* Angular ResourceFactoryService
* Copyright 2016 Andreas Stocker
* MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
* OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
(function () {
'use strict';
var
module = angular.module('ngResourceFactory');
/**
* Factory that gives a constructor function for creating a resource service. The resource service is used to make
* calls to the REST-API via the "method" functions. These are the default method functions:
* ```javascript
* {
* // Undo all changes on the instance using the cache or by reloading the data from the server
* 'restore': {method:'GET', isArray:false},
* // Get a single instance from the API
* 'get': {method:'GET', isArray:false},
* // Get a single instance from the API bypassing the cache
* 'getNoCache': {method:'GET', isArray:false},
* // Get a list of instances from the API
* 'query': {method:'GET', isArray:true},
* // Get a list of instances from the API bypassing the cache
* 'queryNoCache': {method:'GET', isArray:true},
* // Save a new instance
* 'save': {method:'POST', isArray:false},
* // Update an existing instance
* 'update': {method:'PATCH', isArray:false},
* // Delete an instance
* 'remove': {method:'DELETE', isArray:false},
* }
* ```
*
* You can extend or override these methods by using the `options.extraMethods` option.
*
* Each of these methods is available as "static" version on the returned resource class, or as instance
* method on a resource instance. The instance methods have a `"$"` prefix. Each of these methods,
* static and instance versions, exists with a `"Bg"` postfix to make the call in the "background". This
* means you do not see a loading bar if you are using `angular-loading-bar` ({@link http://chieffancypants.github.io/angular-loading-bar/}).
*
* @name ResourceFactoryService
* @ngdoc service
* @param {String} name Name of the resource service
* @param {String} url URL to the resource
* @param {Object} options Options for the resource
* @param {Boolean} options.stripTrailingSlashes Strip trailing slashes from request URLs. Defaults to `false`.
* @param {Boolean} options.withCredentials Include credentials in CORS calls. Defaults to `true`.
* @param {Boolean} options.ignoreLoadingBar Ignore the resource for automatic loading bars. Defaults to `false`.
* @param {Boolean} options.generatePhantomIds Generate IDs for phantom records created via the `new` method. Defaults to `true`.
* @param {ResourcePhantomIdFactoryService} options.phantomIdGenerator Phantom ID generator instance used for generating phantom IDs. Defaults to `{@link ResourcePhantomIdNegativeInt}`.
* @param {String[]} options.dependent List of resource services to clean the cache for on modifying requests.
* @param {Object} options.extraMethods Extra request methods to put on the resource service. Works the same way as `actions` known from `ng-resource`. Defaults to `{}`.
* @param {Object} options.extraFunctions Extra functions to put on the resource service instance.
* @param {Object} options.extraParamDefaults Extra default values for `url` parameters. Works the same way as `defaultParams` known from `ng-resource`. Defaults to `{}`.
* @param {String} options.pkAttr Attribute name where to find the ID of objects. Defaults to `"pk"`.
* @param {String} options.urlAttr Attribute name where to find the URL of objects. Defaults to `"url"`.
* @param {String} options.dataAttr Attribute name where to find the data on call results. Defaults to `null`.
* @param {String} options.useDataAttrForList Use the `dataAttr` for list calls (e.g. `query`). Defaults to `true`.
* @param {String} options.useDataAttrForDetail Use the `dataAttr` for detail calls (e.g. `get`). Defaults to `false`.
* @param {String} options.queryDataAttr Deprecated: Attribute name where to find the data on call results. Note that this option does the same as setting `dataAttr`. Defaults to `null`.
* @param {String} options.queryTotalAttr Attribute name where to find the total amount of data on the query call (resource list calls). Defaults to `null`.
* @param {String} options.cacheClass Resource cache class constructor function to use (see `{@link ResourceCacheService}`). Defaults to `{@link ResourceCacheService}`.
* @param {Function} options.toInternal Function to post-process data coming from response. Gets `obj`, `headersGetter` and `status` and should return the processed `obj`.
* @param {Function} options.fromInternal Function to post-process data that is going to be sent. Gets `obj` and `headersGetter` and should return the processed `obj`.
* @class
*
* @example
* // Basic GET calls
* inject(function (ResourceFactoryService, $q) {
* var
* service = ResourceFactoryService('Test1ResourceService', 'http://test/:pk/');
*
* $httpBackend.expect('GET', 'http://test/').respond(200, [{pk: 1}, {pk: 2}]);
* $httpBackend.expect('GET', 'http://test/1/').respond(200, {pk: 1});
* $httpBackend.expect('GET', 'http://test/2/').respond(200, {pk: 2});
*
* $q.when()
* .then(function () {
* return service.query().$promise;
* })
* .then(function () {
* return service.get({pk: 1}).$promise;
* })
* .then(function () {
* return service.get({pk: 2}).$promise;
* })
* .then(done);
*
* $httpBackend.flush();
* $httpBackend.verifyNoOutstandingRequest();
* });
*
* @example
* // GET calls with query parameters
* inject(function (ResourceFactoryService, $q) {
* var
* service = ResourceFactoryService('Test2ResourceService', 'http://test/:pk/');
*
* $httpBackend.expect('GET', 'http://test/?filter=1').respond(200, [{pk: 1}, {pk: 2}]);
*
* $q.when()
* .then(function () {
* return service.query({filter: 1}).$promise;
* })
* .then(done);
*
* $httpBackend.flush();
* $httpBackend.verifyNoOutstandingRequest();
* });
*
* @example
* // Process list GET calls with data attribute
* inject(function (ResourceFactoryService, $q) {
* var
* service = ResourceFactoryService('TestResourceService', 'http://test/:pk/', {
* dataAttr: 'data'
* });
*
* expect(service.getDataAttr()).toBe('data');
*
* $httpBackend.expect('GET', 'http://test/').respond(200, {data: [{pk: 1}, {pk: 2}]});
* $httpBackend.expect('GET', 'http://test/1/').respond(200, {pk: 1});
*
* $q.when()
* .then(function () {
* return service.query().$promise;
* })
* .then(function (result) {
* expect(result.length).toBe(2);
* })
* .then(function () {
* return service.get({pk: 1}).$promise;
* })
* .then(function (result) {
* expect(result.pk).toBe(1);
* })
* .then(done);
*
* $httpBackend.flush();
* $httpBackend.verifyNoOutstandingRequest();
* });
*
* @example
* // Access attributes outside the data attribute
* inject(function (ResourceFactoryService, $q) {
* var
* service = ResourceFactoryService('TestResourceService', 'http://test/:pk/', {
* dataAttr: 'data'
* });
*
* $httpBackend.expect('GET', 'http://test/').respond(200, {prop1: 123, data: [{pk: 1}, {pk: 2}]});
*
* $q.when()
* .then(function () {
* return service.query().$promise;
* })
* .then(function (result) {
* expect(result.$meta.prop1).toBe(123);
* })
* .then(done);
*
* $httpBackend.flush();
* $httpBackend.verifyNoOutstandingRequest();
* });
*
* @example
* // Resource and instance level methods
* inject(function (ResourceFactoryService) {
* var
* service = ResourceFactoryService('TestResourceService', 'http://test/:pk/'),
* instance = service.new();
*
* expect(service.restore).toBeDefined();
* expect(service.get).toBeDefined();
* expect(service.getNoCache).toBeDefined();
* expect(service.query).toBeDefined();
* expect(service.queryNoCache).toBeDefined();
* expect(service.save).toBeDefined();
* expect(service.update).toBeDefined();
* expect(service.patch).toBeDefined();
* expect(service.remove).toBeDefined();
*
* expect(instance.$restore).toBeDefined();
* expect(instance.$get).toBeDefined();
* expect(instance.$getNoCache).toBeDefined();
* expect(instance.$query).toBeDefined();
* expect(instance.$queryNoCache).toBeDefined();
* expect(instance.$save).toBeDefined();
* expect(instance.$update).toBeDefined();
* expect(instance.$patch).toBeDefined();
* expect(instance.$remove).toBeDefined();
* });
*/
module.factory('ResourceFactoryService',
function ($q,
$resource,
ResourceCacheService,
ResourcePhantomIdNegativeInt) {
'ngInject';
return function (name, url, options) {
/**
* Options for the resource
* @type {Object}
*/
options = angular.extend({
/**
* Option to strip trailing slashes from request URLs
* @type {Boolean}
* @private
*/
stripTrailingSlashes: false,
/**
* Option to ignore the resource for automatic loading bars
* @type {Boolean}
* @private
*/
ignoreLoadingBar: false,
/**
* Include credentials in CORS calls
* @type {Boolean}
* @private
*/
withCredentials: true,
/**
* Generate IDs for phantom records created via the `new`
* method on the resource service
* @type {Boolean}
* @private
*/
generatePhantomIds: true,
/**
* Phantom ID generator instance to use
* @type {ResourcePhantomIdFactoryService}
* @private
*/
phantomIdGenerator: ResourcePhantomIdNegativeInt,
/**
* List of resource services to clean the cache for, on modifying requests
* @type {Array<String>}
* @private
*/
dependent: [],
/**
* Extra methods to put on the resource service
* @type {Object}
* @private
*/
extraMethods: {},
/**
* Extra functions to put on the resource instances
* @type {Object}
* @private
*/
extraFunctions: {},
/**
* Extra default values for `url` parameters
* @type {Object}
* @private
*/
extraParamDefaults: {},
/**
* Attribute name where to find the ID of objects
* @type {String}
* @private
*/
pkAttr: 'pk',
/**
* Attribute name where to find the URL of objects
* @type {String}
* @private
*/
urlAttr: 'url',
/**
* Attribute name where to find the data on call results
* @type {String|null}
* @private
*/
dataAttr: null,
/**
* Use the `dataAttr` for list calls (e.g. `query`)
* @type {Boolean}
* @private
*/
useDataAttrForList: true,
/**
* Use the `dataAttr` for detail calls (e.g. `get`)
* @type {Boolean}
* @private
*/
useDataAttrForDetail: false,
/**
* Deprecated: Attribute name where to find the data on the query call (Note that this option does the same as setting `dataAttr`)
* @type {String|null}
* @deprecated
* @private
*/
queryDataAttr: null,
/**
* Attribute name where to find the total amount of data on the query call
* @type {String|null}
* @private
*/
queryTotalAttr: null,
/**
* Storage for the query filters
* @type {Object}
* @private
*/
queryFilter: {},
/**
* Resource cache class constructor function to use (see `{@link ResourceCacheService}`)
* @type {Function}
* @private
*/
cacheClass: ResourceCacheService,
/**
* Function to post-process data coming from response
*
* @memberOf ResourceFactoryService
* @param obj
* @param headersGetter
* @param status
* @return {*}
* @private
*/
toInternal: function (obj, headersGetter, status) {
return obj;
},
/**
* Function to post-process data that is going to be sent
*
* @memberOf ResourceFactoryService
* @param obj
* @param headersGetter
* @return {*}
* @private
*/
fromInternal: function (obj, headersGetter) {
return obj;
}
}, options || {});
// TODO: remove `queryDataAttr` support
// Support deprecated `queryDataAttr` option, but warn about it
if (options.queryDataAttr) {
console.warn("ResourceFactoryService: The option 'queryDataAttr' is deprecated. Use 'dataAttr' instead.");
if (!options.dataAttr) {
options.dataAttr = options.queryDataAttr;
}
}
var
resource,
/**
* Error class for resource factory
* @type {*}
*/
minError = angular.$$minErr('ngResourceFactory'),
/**
* Default parameter configuration
* @type {{}}
*/
paramsDefaults = {},
/**
* Parameter configuration for save (insert). Used to
* disable the PK url template for save
* @type {{}}
*/
saveParams = {},
/**
* Parameter configuration for query. Used to
* disable the PK url template for query
* @type {{}}
*/
queryParams = {},
/**
* The cache instance for the resource.
* @type {ResourceCacheService}
*/
cache = new options.cacheClass(name, options.pkAttr, {
dataAttr: options.dataAttr,
wrapObjectsInDataAttr: !!(options.dataAttr && options.useDataAttrForDetail),
pkAttr: options.pkAttr,
urlAttr: options.urlAttr,
dependent: options.dependent,
ttl: 15 * 60
}),
/**
* Interceptor that puts the configured `queryTotalAttr` on the resulting array as attribute
* named `total`.
* @type {Object}
*/
queryInterceptor = {
response: function (response) {
var
data = response.data,
instance = response.resource;
// `data` is the object that went through the transformation chain. It should already
// have the `total` and `$meta` attribute set via the `queryTransformResponseData`
// transformation method.
instance.total = data.total;
instance.$meta = data.$meta;
return instance;
}
},
/**
* Interceptor that puts the returned object on the cache an invalidates the
* dependent resource services caches.
* @type {Object}
*/
insertingInterceptor = {
response: function (response) {
var
useDataAttr = options.useDataAttrForDetail && options.dataAttr,
instance = response.resource,
url = options.urlAttr ? instance[options.urlAttr] : null;
cache.removeAllRaw();
cache.removeAllLists();
cache.removeAllDependent();
/*
* Insert the cached object if we have an URL on the returned instance. Else we have
* to invalidate the whole object cache.
*/
if (url) {
cache.insert(url, instance, useDataAttr);
}
else {
cache.removeAllObjects();
}
return instance;
}
},
/**
* Interceptor that puts the returned object on the cache an invalidates the
* dependent resource services caches.
* @type {Object}
*/
modifyingInterceptor = {
response: function (response) {
var
useDataAttr = options.useDataAttrForDetail && options.dataAttr,
instance = response.resource,
url = options.urlAttr ? instance[options.urlAttr] : null;
cache.removeAllRaw();
cache.removeAllLists();
cache.removeAllDependent();
/*
* Update the cached object if we have an URL on the returned instance. Else we have
* to invalidate the whole object cache.
*/
if (url) {
cache.insert(url, instance, useDataAttr);
}
else {
cache.removeAllObjects();
}
return instance;
}
},
/**
* Interceptor that removes the cache for the deleted object, removes all list caches, and
* invalidates the dependent resource services caches.
* @type {Object}
*/
deletingInterceptor = {
response: function (response) {
var
instance = response.resource,
url = options.urlAttr ? instance[options.urlAttr] : null;
cache.removeAllRaw();
cache.removeAllLists();
cache.removeAllDependent();
/*
* Remove the cached object if we have an URL on the returned instance. Else we have
* to invalidate the whole object cache.
*/
if (url) {
cache.remove(url);
}
else {
cache.removeAllObjects();
}
return instance;
}
},
/**
* Parses the response text as JSON and returns it as object.
*
* @memberOf ResourceFactoryService
* @param responseText
* @param headersGetter
* @param status
* @return {Object|Array|string|number}
* @private
*/
transformResponseFromJson = function (responseText, headersGetter, status) {
console.log("ResourceFactoryService: Deserialize data.");
return responseText ? angular.fromJson(responseText) : null;
},
/**
* Calls the `toInternal` function on each object of the response array.
*
* @memberOf ResourceFactoryService
* @param responseData
* @param headersGetter
* @param status
* @return {*}
* @private
*/
queryTransformResponseToInternal = function (responseData, headersGetter, status) {
console.log("ResourceFactoryService: Post-process query data for internal use.");
// iterate over the response data, if it was an array
if (angular.isArray(responseData)) {
for (var i = 0; i < responseData.length; i++) {
responseData[i] = options.toInternal(responseData[i], headersGetter, status);
}
}
// else just call the `toInternal` function on the response object
else {
responseData = options.toInternal(responseData, headersGetter, status);
}
return responseData;
},
/**
* Calls the `toInternal` function on the response data object.
*
* @memberOf ResourceFactoryService
* @param responseData
* @param headersGetter
* @param status
* @return {*}
* @private
*/
singleTransformResponseToInternal = function (responseData, headersGetter, status) {
console.log("ResourceFactoryService: Post-process data for internal use.");
return options.toInternal(responseData, headersGetter, status);
},
/**
* Transforms detail responses to get the actual data from the `dataAttr` option, if
* configured.
*
* @memberOf ResourceFactoryService
* @param responseData
* @param headersGetter
* @param status
* @returns {Array|Object}
* @private
*/
singleTransformResponseData = function (responseData, headersGetter, status) {
var
meta = null,
result = null;
// get data on success status from `dataAttr`, if configured
if (status >= 200 && status < 300) {
// get the data from the `dataAttr`, if configured
if (options.useDataAttrForDetail && options.dataAttr) {
console.log("ResourceFactoryService: Get data from '" + options.dataAttr + "' attribute.");
// get the data from the configured `dataAttr` only if we have a response object.
// else we just want the result to be the response data.
if (responseData) {
meta = angular.copy(responseData);
delete meta[options.dataAttr];
result = responseData[options.dataAttr];
result.$meta = meta;
}
else {
result = responseData;
}
}
// if no data `dataAttr` is defined, use the response data directly
else {
result = responseData;
}
}
// on any other status just return the responded object
else {
result = responseData;
}
return result;
},
/**
* Transforms query responses to get the actual data from the `dataAttr` option, if
* configured. Also sets the `total` attribute on the list if `queryTotalAttr` is configured.
*
* @memberOf ResourceFactoryService
* @param responseData
* @param headersGetter
* @param status
* @returns {Array|Object}
* @private
*/
queryTransformResponseData = function (responseData, headersGetter, status) {
var
meta = null,
result = null;
// get data on success status from `dataAttr`, if configured
if (status >= 200 && status < 300) {
// get the data from the `dataAttr`, if configured
if (options.useDataAttrForList && options.dataAttr) {
console.log("ResourceFactoryService: Get data from '" + options.dataAttr + "' attribute.");
// get the data from the configured `dataAttr` only if we have a response object.
// else we just want the result to be the response data.
if (responseData) {
meta = angular.copy(responseData);
delete meta[options.dataAttr];
result = responseData[options.dataAttr];
result.$meta = meta;
}
else {
result = responseData;
}
}
// if no data `dataAttr` is defined, use the response data directly
else {
result = responseData;
}
// get the total from the `queryTotalAttr`, if configured
if (options.queryTotalAttr && responseData && responseData[options.queryTotalAttr]) {
console.log("ResourceFactoryService: Get total from '" + options.queryTotalAttr + "' attribute.");
result.total = responseData[options.queryTotalAttr];
}
}
// on any other status just return the responded object
else {
result = responseData;
}
return result;
},
/**
* Serializes the request data as JSON and returns it as string.
*
* @memberOf ResourceFactoryService
* @param requestData
* @param headersGetter
* @return {string}
* @private
*/
transformRequestToJson = function (requestData, headersGetter) {
console.log("ResourceFactoryService: Serialize data.");
var
filterPrivate = function (key) {
return String(key)[0] === '$';
},
keys = angular.isObject(requestData) ? Object.keys(requestData) : [],
privateKeys = keys.filter(filterPrivate);
for (var i = 0; i < privateKeys.length; i++) {
delete requestData[privateKeys[i]];
}
return angular.toJson(requestData);
},
/**
* Calls the `fromInternal` function on the request data object.
*
* @memberOf ResourceFactoryService
* @param requestData
* @param headersGetter
* @return {*}
* @private
*/
singleTransformRequestFromInternal = function (requestData, headersGetter) {
console.log("ResourceFactoryService: Post-process data for external use.");
return options.fromInternal(angular.copy(requestData), headersGetter);
},
/**
* Method configuration for the ng-resource
* @type {Object}
*/
methods = {
restore: {
method: 'GET',
isArray: false,
withCredentials: options.withCredentials,
cancellable: true,
ignoreLoadingBar: options.ignoreLoadingBar,
cache: options.useDataAttrForDetail ? cache.withDataAttrNoTtl : cache.withoutDataAttrNoTtl,
transformResponse: [
transformResponseFromJson,
singleTransformResponseData,
singleTransformResponseToInternal
],
transformRequest: [
singleTransformRequestFromInternal,
transformRequestToJson
]
},
get: {
method: 'GET',
isArray: false,
withCredentials: options.withCredentials,
cancellable: true,
ignoreLoadingBar: options.ignoreLoadingBar,
cache: options.useDataAttrForDetail ? cache.withDataAttr : cache.withoutDataAttr,
transformResponse: [
transformResponseFromJson,
singleTransformResponseData,
singleTransformResponseToInternal
],
transformRequest: [
singleTransformRequestFromInternal,
transformRequestToJson
]
},
getNoCache: {
method: 'GET',
isArray: false,
withCredentials: options.withCredentials,
cancellable: true,
ignoreLoadingBar: options.ignoreLoadingBar,
transformResponse: [
transformResponseFromJson,
singleTransformResponseData,
singleTransformResponseToInternal
],
transformRequest: [
singleTransformRequestFromInternal,
transformRequestToJson
]
},
query: {
method: 'GET',
isArray: true,
withCredentials: options.withCredentials,
cancellable: true,
ignoreLoadingBar: options.ignoreLoadingBar,
interceptor: queryInterceptor,
cache: options.useDataAttrForList ? cache.withDataAttr : cache.withoutDataAttr,
transformResponse: [
transformResponseFromJson,
queryTransformResponseData,
queryTransformResponseToInternal
],
transformRequest: [
singleTransformRequestFromInternal,
transformRequestToJson
]
},
queryNoCache: {
method: 'GET',
isArray: true,
withCredentials: options.withCredentials,
cancellable: true,
ignoreLoadingBar: options.ignoreLoadingBar,
interceptor: queryInterceptor,
transformResponse: [
transformResponseFromJson,
queryTransformResponseData,
queryTransformResponseToInternal
],
transformRequest: [
singleTransformRequestFromInternal,
transformRequestToJson
]
},
save: {
method: 'POST',
isArray: false,
withCredentials: options.withCredentials,
cancellable: false,
ignoreLoadingBar: options.ignoreLoadingBar,
interceptor: insertingInterceptor,
transformResponse: [
transformResponseFromJson,
singleTransformResponseData,
singleTransformResponseToInternal
],
transformRequest: [
singleTransformRequestFromInternal,
transformRequestToJson
]
},
update: {
method: 'PATCH',
isArray: false,
withCredentials: options.withCredentials,
cancellable: false,
ignoreLoadingBar: options.ignoreLoadingBar,
interceptor: modifyingInterceptor,
transformResponse: [
transformResponseFromJson,
singleTransformResponseData,
singleTransformResponseToInternal
],
transformRequest: [
singleTransformRequestFromInternal,
transformRequestToJson
]
},
patch: {
method: 'PATCH',
isArray: false,
withCredentials: options.withCredentials,
cancellable: false,
ignoreLoadingBar: options.ignoreLoadingBar,
interceptor: modifyingInterceptor,
transformResponse: [
transformResponseFromJson,
singleTransformResponseData,
singleTransformResponseToInternal
],
transformRequest: [
singleTransformRequestFromInternal,
transformRequestToJson
]
},
remove: {
method: 'DELETE',
isArray: false,
withCredentials: options.withCredentials,
cancellable: false,
ignoreLoadingBar: options.ignoreLoadingBar,
interceptor: deletingInterceptor,
transformResponse: [
transformResponseFromJson,
singleTransformResponseData,
singleTransformResponseToInternal
],
transformRequest: [
singleTransformRequestFromInternal,
transformRequestToJson
]
}
};
// extend methods by the extra methods. when extra methods contain a pre-defined method
// merge the properties with the pre-defined one.
for (var extraMethodName in options.extraMethods) {
if (options.extraMethods.hasOwnProperty(extraMethodName)) {
// if the defined extra method matches a pre-defined one
if (methods.hasOwnProperty(extraMethodName)) {
angular.extend(methods[extraMethodName], options.extraMethods[extraMethodName])
}
// if the extra method is a new one
else {
methods[extraMethodName] = options.extraMethods[extraMethodName];
}
}
}
// offer methods for querying without a loading bar (using a 'Bg' suffix)
for (var methodName in methods) {
if (methods.hasOwnProperty(methodName)) {
var
bgMethodName = methodName + 'Bg',
bgMethodConfig = angular.copy(methods[methodName]);
bgMethodConfig.ignoreLoadingBar = true;
methods[bgMethodName] = bgMethodConfig;
}
}
// build the default params configuration
paramsDefaults[options.pkAttr] = '@' + options.pkAttr;
angular.extend(paramsDefaults, options.extraParamDefaults);
// default params for save and query should be the same as the regular
// default params, except that the PK should not be included in the URLs
angular.extend(saveParams, paramsDefaults);
angular.extend(queryParams, paramsDefaults);
saveParams[options.pkAttr] = null;
queryParams[options.pkAttr] = null;
// save methods should not put PK in the URL
methods.save.params = saveParams;
methods.saveBg.params = saveParams;
// query methods should not put PK in the URL
methods.query.params = queryParams;
methods.queryBg.params = queryParams;
methods.queryNoCache.params = queryParams;
methods.queryNoCacheBg.params = queryParams;
// build the resource object
resource = $resource(url, paramsDefaults, methods, {
stripTrailingSlashes: options.stripTrailingSlashes
});
/**
* Gets the PK attribute name
*
* @memberOf ResourceFactoryService
* @function getPkAttr
* @return {String|null} PK attribute name
* @instance
*/
resource.getPkAttr = function () {
return options.pkAttr;
};
/**
* Gets the data attribute name
*
* @memberOf ResourceFactoryService
* @function getDataAttr
* @return {String|null} Data attribute name
* @instance
*/
resource.getDataAttr = function () {
return options.dataAttr;
};
/**
* Gets the query data attribute name
*
* @memberOf ResourceFactoryService
* @function getQueryDataAttr
* @return {String|null} Data attribute name
* @instance
* @deprecated
*/
resource.getQueryDataAttr = function () {
return options.dataAttr; // set by a compatibility code to the value of `queryDataAttr`
};
/**
* Gets the total attribute name
*
* @memberOf ResourceFactoryService
* @function getQueryTotalAttr
* @return {String|null} Total attribute name
* @instance
*/
resource.getQueryTotalAttr = function () {
return options.queryTotalAttr;
};
/**
* Gets the cache class
*
* @memberOf ResourceFactoryService
* @function getCacheClass
* @return {Function} Cache class
* @instance
*/
resource.getCacheClass = function () {
return options.cacheClass;
};
/**
* Returns an object holding the filter data for query request
*
* @memberOf ResourceFactoryService
* @function getQueryFilters
* @returns {Object} Filter object
* @instance
*/
resource.getQueryFilters = function () {
return options.queryFilter;
};
/**
* Sets a clone of the given object as the object holding the filter data for query request
*
* @memberOf ResourceFactoryService
* @function setQueryFilters
* @param {Object} filters Object holding filter attributes
* @returns {Object} Filter object
* @instance
*/
resource.setQueryFilters = function (filters) {
return angular.copy(filters, options.queryFilter);
};
/**
* Sets the given filter options if the aren't already set on the filter object
*
* @memberOf ResourceFactoryService
* @function setDefaultQueryFilters
* @param {Object} defaultFilters Object holding filter attributes
* @returns {Object} Filter object
* @instance
*/
resource.setDefaultQueryFilters = function (defaultFilters) {
var
filters = angular.extend({}, defaultFilters, options.queryFilter);
return angular.copy(filters, options.queryFilter);
};
/**
* Gets the first item from cache that matches the given PK value. If there is no item on the
* cache that matches, this method returns `undefined`. Note that the cache TTL is ignored.
*
* @memberOf ResourceFactoryService
* @function firstFromCacheByPk
* @param {String|int} pkValue PK value to search for
* @returns {ResourceInstance|undefined} Search result data
* @instance
*/
resource.firstFromCacheByPk = function (pkValue) {
var
data = cache.firstByPk(pkValue);
if (data) {
return resource.new(data);
}
return data;
};
/**
* Gets all items from cache that match the given PK value. If there is no item on the
* cache that matches, this method returns an empty array. Note that the cache TTL is ignored.
*
* @memberOf ResourceFactoryService
* @function findFromCacheByPk
* @param {String|int} pkValue PK value to search for
* @returns {ResourceInstance[]} Search results data
* @instance
*/
resource.findFromCacheByPk = function (pkValue) {
var
cacheResult = cache.findByPk(pkValue),
result = [];
for (var i = 0; i < cacheResult.length; i++) {
result.push(resource.new(cacheResult[i]));
}
return result;
};
/**
* Queries the resource with the configured filters.
*
* @memberOf ResourceFactoryService
* @function filter
* @param {Object} [filters] Object holding filter attributes
* @returns {Object} Query result object
* @instance
*/
resource.filter = function (filters) {
filters = angular.extend({}, resource.getQueryFilters(), filters);
return resource.query(filters);
};
/**
* Queries the resource with the configured filters without using the cache.
*
* @memberOf ResourceFactoryService
* @function filterNoCache
* @param {Object} [filters] Object holding filter attributes
* @returns {Object} Query result object
* @instance
*/
resource.filterNoCache = function (filters) {
filters = angular.extend({}, resource.getQueryFilters(), filters);
return resource.queryNoCache(filters);
};
/**
* Creates a new instance for the resource
*
* @memberOf ResourceFactoryService
* @function new
* @param {Object} [params] Object holding attributes to set on the resource instance
* @return {ResourceInstance} Resource instance
* @instance
*/
resource.new = function (params) {
var
phantomInstance = new resource(params);
// Generate phantom ID if desired
if (options.pkAttr && options.generatePhantomIds && options.phantomIdGenerator && !phantomInstance[options.pkAttr]) {
phantomInstance[options.pkAttr] = options.phantomIdGenerator.generate(phantomInstance);
}
return phantomInstance;
};
/**
* Checks if the given instance is a phantom instance (instance not persisted to the REST API yet)
*
* @memberOf ResourceFactoryService
* @function isPhantom
* @param {ResourceInstance} instance Instance to check
* @return {Boolean|undefined} If is phantom or not
* @instance
*/
resource.isPhantom = function (instance) {
var
pkValue = instance ? instance[options.pkAttr] : undefined;
// Check if phantom ID if all configured correctly
if (options.pkAttr && options.generatePhantomIds && options.phantomIdGenerator) {
return options.phantomIdGenerator.isPhantom(pkValue, instance);
}
return undefined;
};
/**
* Gets a list of instances from the given instances where the given attribute name matches
* the given attribute value.
*
* @memberOf ResourceFactoryService
* @function filterInstancesByAttr
* @param {ResourceInstance[]} instances List of instances to filter
* @param {String} attrName Attribute name to filter on
* @param {*} attrValue Value to filter for
* @return {ResourceInstance[]} Filtered list of instances
* @instance
*/
resource.filterInstancesByAttr = function (instances, attrName, attrValue) {
var
filterAttrValue = function (item) {
return item ? item[attrName] == attrValue : false; // use == here to match '123' to 123
};
return instances.filter(filterAttrValue);
};
/**
* Gets the first instance from the given instances where the given attribute name matches
* the given attribute value.
*
* @memberOf ResourceFactoryService
* @function getInstanceByAttr
* @param {ResourceInstance[]} instances List of instances to filter
* @param {String} attrName Attribute name to filter on
* @param {*} attrValue Value to filter for
* @return {ResourceInstance|null} First instances matching the filter
* @instance
*/
resource.getInstanceByAttr = function (instances, attrName, attrValue) {
var
result = null,
filteredInstances = resource.filterInstancesByAttr(instances, attrName, attrValue);
if (filteredInstances.length) {
if (filteredInstances.length > 1) {
console.warn("ResourceFactoryService: Found more than 1 instances where '" + attrName + "' is '" + attrValue + "' on given '" + name + "' instances.");
}
result = filteredInstances[0];
}
return result;
};
/**
* Gets the first instance from the given instances where the PK attribute has the given value.
*
* @memberOf ResourceFactoryService
* @function getInstanceByPk
* @param {ResourceInstance[]} instances List of instances to filter
* @param {String|int} pkValue PK value to filter for
* @return {ResourceInstance|null} Instance with the given PK
* @instance
*/
resource.getInstanceByPk = function (instances, pkValue) {
return resource.getInstanceByAttr(instances, options.pkAttr, pkValue);
};
/**
* Gets the name of the resource.
*
* @memberOf ResourceFactoryService
* @function getResourceName
* @return {String} Resource name
* @instance
*/
resource.getResourceName = function () {
return name;
};
/**
* Gets the cache instance.
*
* @memberOf ResourceFactoryService
* @function getCache
* @return {ResourceCacheService} Cache class instance
* @instance
*/
resource.getCache = function () {
return cache;
};
/**
* Creates a new store for the resource
*
* @memberOf ResourceFactoryService
* @function createStore
* @param [instances] List of instances that should be managed by the new store
* @return {ResourceStore} New store instance
* @instance
*/
resource.createStore = function (instances) {
instances = instances || [];
// Support for single instances by converting it to an array
if (!angular.isArray(instances)) {
instances = [instances];
}
return new ResourceStore(resource, instances, null);
};
/**
* Saves the given resource instance to the REST API. Uses the `$save` method if instance
* is phantom, else the `$update` method.
*
* @memberOf ResourceFactoryService
* @function persist
* @param {Object} [a1] Query params
* @param {ResourceInstance} [a2] Instance
* @param {Function} [a3] Success callback
* @param {Function} [a4] Error callback
* @return {ResourceInstance} Resource instance
* @instance
*/
resource.persist = function (a1, a2, a3, a4) {
var
params = {},
instance,
successFn,
errorFn,
saveFn;
/*
* Mimic the wired $resource action method signature, where the method can be
* called as follows:
* - params, instance, successFn, errorFn
* - instance, successFn, errorFn
* - params, instance, successFn
* - instance, successFn
* - params, instance
* - instance
*/
switch (arguments.length) {
// handle case: params, instance, successFn, errorFn
case 4:
params = a1;
instance = a2;
successFn = a3;
errorFn = a4;
break;
case 3:
// handle case: instance, successFn, errorFn
if (angular.isFunction(a2)) {
instance = a1;
successFn = a2;
errorFn = a3;
}
// handle case: params, instance, successFn
else {
params = a1;
instance = a2;
successFn = a3;
}
break;
case 2:
// handle case: instance, successFn
if (angular.isFunction(a2)) {
instance = a1;
successFn = a2;
}
// handle case: params, instance
else {
params = a1;
instance = a2;
}
break;
// handle case: instance
case 1:
instance = a1;
break;
// any other case is considered an error
default:
throw minError(
'badargs',
'Expected up to 4 arguments [params, instance, success, error], got {0} arguments',
arguments.length
);
}
saveFn = resource.isPhantom(instance) ? resource.save : resource.update;
return saveFn(params, instance, successFn, errorFn);
};
/*
* Add some of the resource methods as instance methods on the
* prototype of the resource.
*/
angular.extend(resource.prototype, {
/**
* Saves or updates the instance
* @param [a1] Query params
* @param [a2] Success callback
* @param [a3] Error callback
* @return {*}
* @private
*/
$persist: function (a1, a2, a3) {
var
instance = this;
/*
* Mimic the wired $resource action method signature, where the method can be
* called as follows:
* - params, successFn, errorFn
* - params, successFn
* - successFn, errorFn
* - params
* - successFn
* - no parameters
*/
if (angular.isFunction(a1)) {
a2 = a1;
a3 = a2;
a1 = {};
}
return resource.persist(a1, instance, a2, a3).$promise;
},
/**
* Checks if instance is a phantom record (not saved via the REST API yet)
* @return {*}
* @private
*/
$isPhantom: function () {
return resource.isPhantom(this);
}
});
/*
* Add extra functions as instance methods on the prototype of
* the resource.
*/
angular.extend(resource.prototype, options.extraFunctions);
return resource;
};
/**
* Constructor function for a resource store. A resource store manages inserts, updates and
* deletes of instances, can create sub-stores that commit changes to the parent store, and
* sets up relations between resource types (e.g. to update reference keys).
*
* @name ResourceStore
* @param {ResourceFactoryService} resource Resource service instance
* @param {ResourceInstance[]} managedInstances List of resource instances to manage
* @param {ResourceStore|null} parentStore The store of which the new store is a sub-store of
* @class
*
* @example
* // Basic usage of a resource store
* inject(function (ResourceFactoryService, $q) {
* var
* service = ResourceFactoryService('TestResourceService', 'http://test/:pk/'),
* instance1 = service.new(),
* instance2 = service.new(),
* instance3 = service.new(),
* store = service.createStore([instance1, instance2, instance3]);
*
* $httpBackend.expect('DELETE', 'http://test/2/').respond(204, '');
* $httpBackend.expect('PATCH', 'http://test/1/').respond(200, {pk: 1});
* $httpBackend.expect('POST', 'http://test/').respond(201, {pk: 2});
*
* instance1.pk = 1;
* instance2.pk = 2;
*
* store.remove(instance2);
* store.persist(instance1);
* store.persist(instance3);
*
* $q.when()
* .then(function () {
* return store.execute();
* })
* .then(done);
*
* $httpBackend.flush();
* });
*/
function ResourceStore (resource, managedInstances, parentStore) {
var
self = this,
/**
* Name of the resource service
* @type {String}
* @private
*/
resourceName = resource.getResourceName(),
/**
* Indicator for running execution (stops another execution from being issued)
* @type {boolean}
* @private
*/
executionRunning = false,
/**
* Contains relations to other stores (for updating references)
* @type {Array<ResourceStoreRelation>}
* @private
*/
relations = [],
/**
* Stores resource items that are visible for the user (not queued for remove)
* @type {Array}
* @private
*/
visibleQueue = [],
/**
* Stores resource items queued for persisting (save or update)
* @type {Array}
* @private
*/
persistQueue = [],
/**
* Stores resource items queued for deleting
* @type {Array}
* @private
*/
removeQueue = [],
/**
* Callbacks executed before each item persists
* @type {Array}
* @private
*/
beforePersistListeners = [],
/**
* Callbacks executed after each item persists
* @type {Array}
* @private
*/
afterPersistListeners = [],
/**
* Callbacks executed before each item removes
* @type {Array}
* @private
*/
beforeRemoveListeners = [],
/**
* Callbacks executed after each item removes
* @type {Array}
* @private
*/
afterRemoveListeners = [];
/**
* Manage given instances. The new instances object may be a ng-resource result,
* a promise, a list of instances or a single instance.
*
* @memberOf ResourceStore
* @function manage
* @param {ResourceInstance|ResourceInstance[]} newInstances List of instances to manage
* @return {Promise} Promise that resolves if all given instances are resolved
* @instance
*/
self.manage = function (newInstances) {
var
doManage = function (newInstances) {
console.log("ResourceStore: Manage given '" + resourceName + "' instances.");
// Support for single instances by converting it to an array
if (!angular.isArray(newInstances)) {
newInstances = [newInstances];
}
for (var i = 0; i < newInstances.length; i++) {
var
newInstance = newInstances[i];
// If the instance is not managed yet, manage it
if (!newInstance.$store) {
// Make the store available on the instance
newInstance.$store = self;
// Add the instance to the list of managed instances
addResourceInstance(managedInstances, newInstance);
addResourceInstance(visibleQueue, newInstance);
}
// If the instances is already managed by another store, print an error
else if (newInstance.$store !== self) {
console.error("ResourceStore: '" + resourceName + "' instance already managed by another store.");
}
// If the instance is already managed by this store, do nothing but logging
else {
console.log("ResourceStore: '" + resourceName + "' instance already managed by the store.");
}
}
};
// Support ng-resource objects and promises
if (isPromiseLike(newInstances) || isPromiseLike(newInstances.$promise)) {
var
promise = isPromiseLike(newInstances) ? newInstances : newInstances.$promise,
defer = $q.defer();
promise
.then(doManage)
.then(function () {
defer.resolve(newInstances);
});
return defer.promise;
}
// Synchronous if we have no promise
else {
doManage(newInstances);
return $q.resolve(newInstances);
}
};
/**
* Forget (un-manage) given instances. The instances object may be a ng-resource result,
* a promise, a list of instances or a single instance.
*
* @memberOf ResourceStore
* @function forget
* @param {ResourceInstance|ResourceInstance[]} oldInstances Instances the store should forget
* @return {Promise} Promise that resolves if all given instances are resolved
* @instance
*/
self.forget = function (oldInstances) {
var
doForget = function (oldInstances) {
console.log("ResourceStore: Forget given '" + resourceName + "' instances.");
// Support for single instances by converting it to an array
if (!angular.isArray(oldInstances)) {
oldInstances = [oldInstances];
}
for (var i = 0; i < oldInstances.length; i++) {
var
oldInstance = oldInstances[i],
oldInstancePkValue = oldInstance ? oldInstance[resource.getPkAttr()] : null;
// If the instance is not managed yet, manage it
if (oldInstance.$store === self) {
// Remove the store attribute from the instance
delete oldInstance.$store;
// Call the dispose handler on all relations for the disposed instance
for (var j = 0; j < relations.length; j++) {
relations[j].handleDispose(oldInstancePkValue);
}
// Remove the instance from the list of managed instances
removeResourceInstance(managedInstances, oldInstance);
removeResourceInstance(visibleQueue, oldInstance);
removeResourceInstance(persistQueue, oldInstance);
removeResourceInstance(removeQueue, oldInstance);
}
// If the instances is already managed by another store, print an error
else if (!!oldInstance.$store && oldInstance.$store !== self) {
console.error("ResourceStore: '" + resourceName + "' instance managed by another store.");
}
// If the instance is not managed by any store, print an error
else {
console.error("ResourceStore: '" + resourceName + "' instance is not managed.");
}
}
};
// Support ng-resource objects and promises
if (isPromiseLike(oldInstances) || isPromiseLike(oldInstances.$promise)) {
var
promise = isPromiseLike(oldInstances) ? oldInstances : oldInstances.$promise,
defer = $q.defer();
promise
.then(doForget)
.then(function () {
defer.resolve(oldInstances);
});
return defer.promise;
}
// Synchronous if we have no promise
else {
doForget(oldInstances);
return $q.resolve(oldInstances);
}
};
/**
* Returns a new instance managed by the store.
*
* @memberOf ResourceStore
* @function new
* @param {Object} [params] Object holding attributes to set on the resource instance
* @return {ResourceInstance} New resource instance that is managed by the store
* @instance
*/
self.new = function (params) {
var
newInstance = resource.new(params);
self.manage(newInstance);
return newInstance;
};
/**
* Queues given instance for persistence.
*
* @memberOf ResourceStore
* @function persist
* @param {ResourceInstance|ResourceInstance[]} instances Instances that should be queued for persistence
* @instance
*/
self.persist = function (instances) {
console.log("ResourceStore: Queue '" + resourceName + "' instances for persist.");
if (!angular.isArray(instances)) {
instances = [instances];
}
for (var i = 0; i < instances.length; i++) {
var
instance = instances[i];
if (instance.$store === self) {
addResourceInstance(persistQueue, instance);
addResourceInstance(visibleQueue, instance);
removeResourceInstance(removeQueue, instance);
}
else {
console.error("ResourceStore: '" + resourceName + "' instance is not managed by this store.");
}
}
};
/**
* Queues given instance for deletion.
*
* @memberOf ResourceStore
* @function remove
* @param {ResourceInstance|ResourceInstance[]} instances Instances that should be queued for removing
* @instance
*/
self.remove = function (instances) {
console.log("ResourceStore: Queue '" + resourceName + "' instances for remove.");
if (!angular.isArray(instances)) {
instances = [instances];
}
for (var i = 0; i < instances.length; i++) {
var
instance = instances[i],
instancePkValue = instance ? instance[resource.getPkAttr()] : null;
if (instance.$store === self) {
// Call the dispose handler on all relations for the disposed instance
for (var j = 0; j < relations.length; j++) {
relations[j].handleDispose(instancePkValue);
}
removeResourceInstance(persistQueue, instance);
removeResourceInstance(visibleQueue, instance);
addResourceInstance(removeQueue, instance);
}
else {
console.error("ResourceStore: '" + resourceName + "' instance is not managed by this store.");
}
}
};
/**
* Commits changes to the parent store
*
* @memberOf ResourceStore
* @function commit
* @instance
*/
self.commit = function () {
// Check if there is a parent store first. We cannot commit to a parent store
// if there is no parent store.
if (!parentStore) {
console.error("ResourceStore: Cannot commit '" + resourceName + "' instances as there is no parent store.");
return;
}
console.log("ResourceStore: Commit '" + resourceName + "' instance changes to parent store.");
// Commit the persist queue to the parent store
for (var i = 0; i < persistQueue.length; i++) {
var
childPersistInstance = copy(persistQueue[i]),
parentPersistInstance = parentStore.getByInstance(childPersistInstance);
delete childPersistInstance.$store;
if (!parentPersistInstance) {
parentPersistInstance = copy(childPersistInstance);
parentStore.manage(parentPersistInstance);
}
else {
merge(childPersistInstance, parentPersistInstance);
}
parentStore.persist(parentPersistInstance);
}
// Commit the remove queue to the parent store
for (var j = 0; j < removeQueue.length; j++) {
var
childRemoveInstance = copy(removeQueue[i]),
parentRemoveInstance = parentStore.getByInstance(childRemoveInstance);
delete childRemoveInstance.$store;
if (!parentRemoveInstance) {
parentRemoveInstance = copy(childRemoveInstance);
parentStore.manage(parentRemoveInstance);
}
else {
merge(childRemoveInstance, parentRemoveInstance);
}
parentStore.remove(parentRemoveInstance);
}
};
/**
* Executes the change queue on this an all related stores and clears the change queue if clearAfter is
* set to true.
*
* @memberOf ResourceStore
* @function executeAll
* @param [clearAfter] Clear change queue on all stores after executing (default: `true`)
* @return {Promise}
* @instance
*/
self.executeAll = function (clearAfter) {
// `clearAfter` should default to true
clearAfter = angular.isUndefined(clearAfter) || !!clearAfter;
var
defer = $q.defer(),
/**
* Executes the related stores
* @return {Promise}
*/
executeRelated = function () {
var
promises = [];
for (var i = 0; i < relations.length; i++) {
var
relation = relations[i],
relatedStore = relation.getRelatedStore();
// Add the execution of the related store to the list of
// promises to resolve
promises.push(relatedStore.executeAll(clearAfter));
}
return $q.all(promises);
};
// Execute the store itself, then execute the related stores. If everything
// went well, resolve the returned promise, else reject it.
self.execute(clearAfter)
.then(executeRelated)
.then(defer.resolve)
.catch(defer.reject);
return defer.promise;
};
/**
* Execute the change queue and clears the change queue if clearAfter is set to true (default).
*
* @memberOf ResourceStore
* @function execute
* @param [clearAfter] Clear change queue on the store after executing (default: `true`)
* @return {Promise}
* @instance
*/
self.execute = function (clearAfter) {
// `clearAfter` should default to true
clearAfter = angular.isUndefined(clearAfter) || !!clearAfter;
// Cannot execute when already executing
if (executionRunning) {
return $q.reject("Another execution is already running.");
}
// If there is a parent store raise an error
if (parentStore) {
throw "Executing the store is only possible on the topmost store";
}
// Execution started
executionRunning = true;
var
defer = $q.defer(),
/**
* Sets the running flag to false
* @param reason
* @return {*}
* @private
*/
handleError = function (reason) {
executionRunning = false;
defer.reject(reason);
},
/**
* Calls a list of listener functions with given item as parameter
* @param item
* @param listeners
* @private
*/
callListeners = function (item, listeners) {
for (var i = 0; i < listeners.length; i++) {
listeners[i](item);
}
},
/**
* Calls the remove handler on the registered relations.
* @param pkValue
* @private
*/
relationsRemove = function (pkValue) {
for (var i = 0; i < relations.length; i++) {
relations[i].handleRemove(pkValue);
}
},
/**
* Calls the update handler on the registered relations.
* @param oldPkValue
* @param newPkValue
* @private
*/
relationsUpdate = function (oldPkValue, newPkValue) {
for (var i = 0; i < relations.length; i++) {
relations[i].handleUpdate(oldPkValue, newPkValue);
}
},
/**
* Executes a single REST API call on the given item with the given function. Calls the given
* before and after listeners and resolves the given defer after all this is done.
* @param item
* @param execFn
* @param defer
* @param beforeListeners
* @param afterListeners
* @param isRemove
* @private
*/
executeSingle = function (item, execFn, beforeListeners, afterListeners, defer, isRemove) {
// Call the before listeners
callListeners(item, beforeListeners);
// Execute the REST API call
execFn({}, item).$promise
.then(function (response) {
// Forget referencing instances on related stores if this was a successful
// remove on the REST API
if (isRemove && item) {
relationsRemove(item[resource.getPkAttr()]);
}
// If the response contains the saved object (with the PK from the REST API) then
// set the new PK on the item.
if (response && response[resource.getPkAttr()]) {
var
oldPkValue = item ? item[resource.getPkAttr()] : null,
newPkValue = response ? response[resource.getPkAttr()] : null;
// Update the FK values on referencing instances on related stores if this
// was a successful insert or update on the REST API
if (!isRemove) {
relationsUpdate(oldPkValue, newPkValue);
}
item[resource.getPkAttr()] = newPkValue;
}
// Then call the after listeners
callListeners(item, afterListeners);
// And resolve the promise with the item
defer.resolve(item);
})
.catch(defer.reject);
},
/**
* Executes the remove queue. Returns a promise that resolves as soon as all
* REST API calls are done.
* @return {Promise}
* @private
*/
executeRemoves = function () {
var
promises = [],
queue = self.getRemoveQueue();
// Iterate over the queue
for (var i = 0; i < queue.length; i++) {
var
item = queue[i];
// Only non-phantom entries should be removed (phantoms don't exist anyway)
if (!item.$isPhantom()) {
var
defer = $q.defer();
promises.push(defer.promise);
// Execute the single REST API call
executeSingle(item, resource.remove, beforeRemoveListeners, afterRemoveListeners, defer, true);
}
}
return $q.all(promises);
},
/**
* Executes the update queue. Returns a promise that resolves as soon as all
* REST API calls are done.
* @return {Promise}
* @private
*/
executeUpdates = function () {
var
promises = [],
queue = self.getUpdateQueue();
// Iterate over the queue
for (var i = 0; i < queue.length; i++) {
var
item = queue[i],
defer = $q.defer();
promises.push(defer.promise);
// Execute the single REST API call
executeSingle(item, resource.update, beforePersistListeners, afterPersistListeners, defer, false);
}
return $q.all(promises);
},
/**
* Executes the save (insert) queue. Returns a promise that resolves as soon as all
* REST API calls are done.
* @return {Promise}
* @private
*/
executeSaves = function () {
var
promises = [],
queue = self.getSaveQueue();
// Iterate over the queue
for (var i = 0; i < queue.length; i++) {
var
item = queue[i],
defer = $q.defer();
promises.push(defer.promise);
// Execute the single REST API call
executeSingle(item, resource.save, beforePersistListeners, afterPersistListeners, defer, false);
}
return $q.all(promises);
},
/**
* Clears the change queues.
* @private
*/
clear = function () {
if (clearAfter) {
persistQueue.length = 0;
removeQueue.length = 0;
}
// Execution finished
executionRunning = false;
};
// Execute the REST API call queue
$q.when()
.then(executeRemoves)
.then(executeUpdates)
.then(executeSaves)
.then(clear)
.then(defer.resolve)
.catch(handleError);
return defer.promise;
};
/**
* Creates a new child store from the current store. This store can make changes
* to it's managed instances and it will not affect the current stores
* instances until the child store commits.
*
* @memberOf ResourceStore
* @function createChildStore
* @param {ResourceInstance|ResourceInstance[]} instances Instances to manage on the child store (default: all instances on the parent store)
* @return {ResourceStore} New child store
* @instance
*/
self.createChildStore = function (instances) {
instances = instances || managedInstances;
var
childStoreManagedInstances = copy(instances);
return new ResourceStore(resource, childStoreManagedInstances, self);
};
/**
* Adds a relation to another store. Relations do automatically update foreign key attributes on
* instances on a related store when certain events occur. These events include:
* - `remove`: An instance was removed on the server (e.g. by a DELETE call)
* - `dispose`: An instances was removed for forgot on the store (e.g. by `remove` or `forget` methods on the store)
* - `update`: An instance got a new PK from the server after saving
*
* You can either use pre-defined behaviours for these events, or hook in custom functions. The pre-defined
* behaviours are:
* - `"forget"`: Forget the referencing instance on the related store
* - `"null"`: Set the foreign key attribute on the referencing instance to `null`
* - `"update"`: Set the foreign key attribute on the referencing instance to the new PK
* - `"ignore"`: Do nothing
*
* @memberOf ResourceStore
* @function createRelation
* @param {Object} config Configuration object
* @param {ResourceStore} config.relatedStore Store instance this store has a relation to
* @param {String} config.fkAttr Foreign key attribute on instances on the related store
* @param {Function|"forget"|"null"|"ignore"} config.onRemove What to do on the related instances if instances on this store are removed (default: `"forget"`)
* @param {Function|"forget"|"null"|"ignore"} config.onDispose What to do on the related instances if instances on this store are disposed (default: `"forget"`)
* @param {Function|"update"|"null"|"ignore"} config.onUpdate What to do on the related instances if instances on this store are updated (default: `"update"`)
* @return {ResourceStoreRelation} New relation
* @instance
*/
self.createRelation = function (config) {
config = angular.extend({
relatedStore: null,
fkAttr: null,
onRemove: 'forget',
onDispose: 'forget',
onUpdate: 'update'
}, config);
var
relation = new ResourceStoreRelation(self, config.relatedStore, config.fkAttr, config.onUpdate, config.onRemove, config.onDispose);
relations.push(relation);
return relation;
};
/**
* Removes a relation from the store.
*
* @memberOf ResourceStore
* @function removeRelation
* @param {ResourceStoreRelation} relation Relation instance to remove
* @instance
*/
self.removeRelation = function (relation) {
var
relationIndex = relations.indexOf(relation),
relationFound = relationIndex !== -1;
if (relationFound) {
relations.splice(relationIndex, 1);
}
};
/**
* Gets the managed instance from the store that matches the given
* PK attribute value.
*
* @memberOf ResourceStore
* @function getByPk
* @param {String|int} pkValue
* @return {ResourceInstance|undefined} Resource instance managed by the store with the given PK value
* @instance
*/
self.getByPk = function (pkValue) {
return resource.getInstanceByPk(managedInstances, pkValue);
};
/**
* Gets the managed instance from the store that matches the given
* instance (which might by an copy that is not managed or managed by
* another store). The instances are matched by their PK attribute.
*
* @memberOf ResourceStore
* @function getByInstance
* @param {ResourceInstance} instance Instance to search for in the store
* @return {ResourceInstance|undefined} Resource instance managed by the store matching the given instance that might by not managed by the store
* @instance
*/
self.getByInstance = function (instance) {
var
pkValue = instance ? instance[resource.getPkAttr()] : undefined;
return self.getByPk(pkValue);
};
/**
* Gets a list of instances visible for the user.
*
* @memberOf ResourceStore
* @function getManagedInstances
* @return {ResourceInstance[]} Instances managed by the store
* @instance
*/
self.getManagedInstances = function () {
return managedInstances.slice();
};
/**
* Gets a list of instances visible for the user (e.g. that are not marked for removal).
*
* @memberOf ResourceStore
* @function getVisibleQueue
* @return {ResourceInstance[]} Instances visible for the user
* @instance
*/
self.getVisibleQueue = function () {
return visibleQueue.slice();
};
/**
* Gets a list of instances marked for persist.
*
* @memberOf ResourceStore
* @function getPersistQueue
* @return {ResourceInstance[]} Instances marked for persistence
* @instance
*/
self.getPersistQueue = function () {
return persistQueue.slice();
};
/**
* Gets a list of instances marked for remove.
*
* @memberOf ResourceStore
* @function getRemoveQueue
* @return {ResourceInstance[]} Instances marked for removal
* @instance
*/
self.getRemoveQueue = function () {
return removeQueue.slice();
};
/**
* Gets a list of instances marked for save (e.g. insert).
*
* @memberOf ResourceStore
* @function getSaveQueue
* @return {ResourceInstance[]} Instances marked for save
* @instance
*/
self.getSaveQueue = function () {
var
filterPhantom = function (instance) {
return instance.$isPhantom();
};
return persistQueue.filter(filterPhantom);
};
/**
* Gets a list of instances marked for update.
*
* @memberOf ResourceStore
* @function getUpdateQueue
* @return {ResourceInstance[]} Instances marked for update
* @instance
*/
self.getUpdateQueue = function () {
var
filterNonPhantom = function (instance) {
return !instance.$isPhantom();
};
return persistQueue.filter(filterNonPhantom);
};
/**
* Gets the managed resource service.
*
* @memberOf ResourceStore
* @function getResourceService
* @return {ResourceFactoryService} Resource service instance
* @instance
*/
self.getResourceService = function () {
return resource;
};
/**
* Adds a before-persist listener.
*
* @memberOf ResourceStore
* @function addBeforePersistListener
* @param {Function} fn Callback function
* @instance
*/
self.addBeforePersistListener = function (fn) {
beforePersistListeners.push(fn);
};
/**
* Removes a before-persist listener.
*
* @memberOf ResourceStore
* @function removeBeforePersistListener
* @param {Function} fn Callback function
* @instance
*/
self.removeBeforePersistListener = function (fn) {
var
fnIndex = beforePersistListeners.indexOf(fn),
fnFound = fnIndex !== -1;
if (fnFound) {
beforePersistListeners.splice(fnIndex, 1);
}
};
/**
* Adds a after-persist listener.
*
* @memberOf ResourceStore
* @function addAfterPersistListener
* @param {Function} fn Callback function
* @instance
*/
self.addAfterPersistListener = function (fn) {
afterPersistListeners.push(fn);
};
/**
* Removes a after-persist listener.
*
* @memberOf ResourceStore
* @function removeAfterPersistListener
* @param {Function} fn Callback function
* @instance
*/
self.removeAfterPersistListener = function (fn) {
var
fnIndex = afterPersistListeners.indexOf(fn),
fnFound = fnIndex !== -1;
if (fnFound) {
afterPersistListeners.splice(fnIndex, 1);
}
};
/**
* Adds a before-remove listener.
*
* @memberOf ResourceStore
* @function addBeforeRemoveListener
* @param {Function} fn Callback function
* @instance
*/
self.addBeforeRemoveListener = function (fn) {
beforeRemoveListeners.push(fn);
};
/**
* Removes a before-remove listener.
*
* @memberOf ResourceStore
* @function removeBeforeRemoveListener
* @param {Function} fn Callback function
* @instance
*/
self.removeBeforeRemoveListener = function (fn) {
var
fnIndex = beforeRemoveListeners.indexOf(fn),
fnFound = fnIndex !== -1;
if (fnFound) {
beforeRemoveListeners.splice(fnIndex, 1);
}
};
/**
* Adds a after-remove listener.
*
* @memberOf ResourceStore
* @function addAfterRemoveListener
* @param {Function} fn Callback function
* @instance
*/
self.addAfterRemoveListener = function (fn) {
afterRemoveListeners.push(fn);
};
/**
* Removes a after-remove listener.
*
* @memberOf ResourceStore
* @function removeAfterRemoveListener
* @param {Function} fn Callback function
* @instance
*/
self.removeAfterRemoveListener = function (fn) {
var
fnIndex = afterRemoveListeners.indexOf(fn),
fnFound = fnIndex !== -1;
if (fnFound) {
afterRemoveListeners.splice(fnIndex, 1);
}
};
/**
* Adds the given instance to the given list of instances. Does nothing if the instance
* is already in the list of instances.
*
* @memberOf ResourceStore
* @function addResourceInstance
* @param instances
* @param instance
* @private
*/
function addResourceInstance (instances, instance) {
var
matchingInstances = resource.filterInstancesByAttr(instances, resource.getPkAttr(), instance[resource.getPkAttr()]);
if (!!matchingInstances.length) {
for (var i = 0; i < matchingInstances.length; i++) {
var
matchingInstanceIndex = instances.indexOf(matchingInstances[i]),
matchingInstanceFound = matchingInstanceIndex !== -1;
if (matchingInstanceFound) {
instances.splice(matchingInstanceIndex, 1, instance);
}
}
}
else {
instances.push(instance);
}
}
/**
* Removes the given instance from the given list of instances. Does nothing if the instance
* is not in the list of instances.
*
* @memberOf ResourceStore
* @param instances
* @param instance
* @private
*/
function removeResourceInstance (instances, instance) {
var
matchingInstances = resource.filterInstancesByAttr(instances, resource.getPkAttr(), instance[resource.getPkAttr()]);
if (!!matchingInstances.length) {
for (var i = 0; i < matchingInstances.length; i++) {
var
matchingInstanceIndex = instances.indexOf(matchingInstances[i]),
matchingInstanceFound = matchingInstanceIndex !== -1;
if (matchingInstanceFound) {
instances.splice(matchingInstanceIndex, 1);
}
}
}
}
/**
* Internal function for checking if an object can be treated as an promise.
*
* @memberOf ResourceStore
* @param obj
* @return {*|boolean}
* @private
*/
function isPromiseLike (obj) {
return obj && angular.isFunction(obj.then);
}
/**
* Populates the destination object `dst` by copying the non-private data from `src` object. The data
* on the `dst` object will be a deep copy of the data on the `src`. This function will not copy
* attributes of the `src` whose names start with "$". These attributes are considered private. The
* method will also keep the private attributes of the `dst`.
*
* @memberOf ResourceStore
* @param dst {Undefined|Object|Array} Destination object
* @param src {Object|Array} Source object
* @param [keepMissing] boolean Keep attributes on dst that are not present on src
* @return {*}
* @private
*/
function populate (dst, src, keepMissing) {
// keepMissing defaults to true
keepMissing = angular.isUndefined(keepMissing) ? true : !!keepMissing;
dst = dst || undefined;
var
key,
preserve = !!dst,
preservedObjects = {};
/*
* As we do remove all "private" properties from the source, so they are not copied
* to the destination object, we make a copy of the source first. We do not want to
* modify the actual source object.
*/
src = angular.copy(src);
for (key in src) {
if (src.hasOwnProperty(key) && key[0] === '$') {
delete src[key];
}
}
/*
* Only preserve if we got a destination object. Save "private" object keys of destination before
* copying the source object over the destination object. We restore these properties afterwards.
*/
if (preserve) {
for (key in dst) {
if (dst.hasOwnProperty(key)) {
// keep private attributes
if (key[0] === '$') {
preservedObjects[key] = dst[key];
}
// keep attribute if not present on source
else if (keepMissing && !src.hasOwnProperty(key)) {
preservedObjects[key] = dst[key];
}
}
}
}
// do the actual copy
dst = angular.copy(src, dst);
/*
* Now we can restore the preserved data on the destination object again.
*/
if (preserve) {
for (key in preservedObjects) {
if (preservedObjects.hasOwnProperty(key)) {
dst[key] = preservedObjects[key];
}
}
}
return dst;
}
/**
* Copies the source object to the destination object (or array). Keeps private
* attributes on the `dst` object (attributes starting with $ are private).
*
* @memberOf ResourceStore
* @param src
* @param [dst]
* @return {*}
* @private
*/
function copy (src, dst) {
// if we are working on an array, copy each instance of the array to
// the dst.
if (angular.isArray(src)) {
dst = angular.isArray(dst) ? dst : [];
dst.length = 0;
for (var i = 0; i < src.length; i++) {
dst.push(populate(null, src[i], false));
}
}
// else we can just copy the src object.
else {
dst = populate(dst, src, false);
}
return dst;
}
/**
* Merges the source object to the destination object (or array). Keeps private
* attributes on the `dst` object (attributes starting with $ are private).
*
* @memberOf ResourceStore
* @param src
* @param [dst]
* @return {*}
* @private
*/
function merge (src, dst) {
// if we are working on an array, copy each instance of the array to
// the dst.
if (angular.isArray(src)) {
dst = angular.isArray(dst) ? dst : [];
dst.length = 0;
for (var i = 0; i < src.length; i++) {
dst.push(populate(null, src[i], true));
}
}
// else we can just copy the src object.
else {
dst = populate(dst, src, true);
}
return dst;
}
/**
* Initializes the store instance
*
* @memberOf ResourceStore
* @private
*/
function init () {
managedInstances = managedInstances || [];
parentStore = parentStore || null;
var
managed = self.manage(managedInstances),
/**
* Maps instances to a list of PKs
* @param instance
* @return {*|undefined}
* @private
*/
mapPk = function (instance) {
return instance ? String(instance[resource.getPkAttr()]) : undefined;
},
/**
* Filters instances to given list of PKs
* @param pks
* @return {Function}
* @private
*/
filterPks = function (pks) {
return function (instance) {
return instance ? pks.indexOf(String(instance[resource.getPkAttr()])) !== -1 : false;
}
};
// Initialize queues with the state of the parent store, if there is a parent store.
if (parentStore) {
managed.then(
function () {
console.log("ResourceStore: Copy state from parent store.");
var
parentVisibleQueuePks = parentStore.getVisibleQueue().map(mapPk),
parentPersistQueuePks = parentStore.getPersistQueue().map(mapPk),
parentRemoveQueuePks = parentStore.getRemoveQueue().map(mapPk);
// Initialize the visible, persist and remove queue with the state
// from the parent store.
visibleQueue = managedInstances.filter(filterPks(parentVisibleQueuePks));
persistQueue = managedInstances.filter(filterPks(parentPersistQueuePks));
removeQueue = managedInstances.filter(filterPks(parentRemoveQueuePks));
}
);
}
}
// Initialize the store
init();
}
/**
* Constructor class for a relation between two stores.
*
* @name ResourceStoreRelation
* @param {ResourceStore} store Store to create the relation on
* @param {ResourceStore} relatedStore Related store
* @param {String} fkAttr Name of foreign key attribute on instances on related store
* @param {Function|"update"|"null"|"ignore"} onUpdate What to do on the related instances if instances on the store are updated
* @param {Function|"forget"|"null"|"ignore"} onRemove What to do on the related instances if instances on the store are removed
* @param {Function|"forget"|"null"|"ignore"} onDispose What to do on the related instances if instances on the store are disposed
* @class
*
* @example
* // Basic usage of a ResourceStoreRelation
* inject(function (ResourceFactoryService, $q) {
* var
* service = ResourceFactoryService('TestResourceService', 'http://test/:pk/'),
* relatedService = ResourceFactoryService('RelatedTestResourceService', 'http://relatedtest/:pk/'),
* instance1 = service.new(),
* instance2 = service.new(),
* relatedInstance1 = relatedService.new({fk: instance1.pk}),
* relatedInstance2 = relatedService.new({fk: instance2.pk}),
* store = service.createStore([instance1, instance2]),
* relatedStore = relatedService.createStore([relatedInstance1, relatedInstance2]);
*
* $httpBackend.expect('POST', 'http://test/').respond(201, {pk: 1});
* $httpBackend.expect('POST', 'http://test/').respond(201, {pk: 2});
*
* store.createRelation({
* relatedStore: relatedStore,
* fkAttr: 'fk'
* });
*
* $q.when()
* .then(function () {
* expect(relatedInstance1.fk).toBe(-1);
* expect(relatedInstance2.fk).toBe(-2);
* })
* .then(function () {
* store.persist(instance1);
* store.persist(instance2);
* })
* .then(function () {
* return store.execute();
* })
* .then(function () {
* expect(relatedInstance1.fk).toBe(1);
* expect(relatedInstance2.fk).toBe(2);
* })
* .then(done);
*
* $httpBackend.flush();
* });
*/
function ResourceStoreRelation (store, relatedStore, fkAttr, onUpdate, onRemove, onDispose) {
var
self = this;
/*
* Implementation of pre-defined update behaviours
*/
switch (onUpdate) {
case 'update':
onUpdate = function (referencingStore, referencingInstance, oldReferencedInstancePk, newReferencedInstancePk, fkAttr) {
console.log("ResourceStoreRelation: Set reference to '" + relatedStore.getResourceService().getResourceName() + "' instance from '" + oldReferencedInstancePk + "' to '" + newReferencedInstancePk + "'.");
referencingInstance[fkAttr] = newReferencedInstancePk;
};
break;
case 'null':
onUpdate = function (referencingStore, referencingInstance, oldReferencedInstancePk, newReferencedInstancePk, fkAttr) {
console.log("ResourceStoreRelation: Set reference to '" + relatedStore.getResourceService().getResourceName() + "' instance from '" + oldReferencedInstancePk + "' to null.");
referencingInstance[fkAttr] = null;
};
break;
case 'ignore':
onUpdate = function (referencingStore, referencingInstance, oldReferencedInstancePk, newReferencedInstancePk, fkAttr) { };
break;
}
/*
* Implementation of pre-defined remove behaviours
*/
switch (onRemove) {
case 'forget':
onRemove = function (referencingStore, referencingInstance, oldReferencedInstancePk, fkAttr) {
console.log("ResourceStoreRelation: Forget '" + relatedStore.getResourceService().getResourceName() + "' instance '" + oldReferencedInstancePk + "' referencing instance.");
referencingStore.forget(referencingInstance);
};
break;
case 'null':
onRemove = function (referencingStore, referencingInstance, oldReferencedInstancePk, fkAttr) {
console.log("ResourceStoreRelation: Set reference to '" + relatedStore.getResourceService().getResourceName() + "' instance from '" + oldReferencedInstancePk + "' to null.");
referencingInstance[fkAttr] = null;
};
break;
case 'ignore':
onRemove = function (referencingStore, referencingInstance, oldReferencedInstancePk, fkAttr) { };
break;
}
/*
* Implementation of pre-defined dispose behaviours
*/
switch (onDispose) {
case 'forget':
onDispose = function (referencingStore, referencingInstance, oldReferencedInstancePk, fkAttr) {
console.log("ResourceStoreRelation: Forget '" + relatedStore.getResourceService().getResourceName() + "' instance '" + oldReferencedInstancePk + "' referencing instance.");
referencingStore.forget(referencingInstance);
};
break;
case 'null':
onDispose = function (referencingStore, referencingInstance, oldReferencedInstancePk, fkAttr) {
console.log("ResourceStoreRelation: Set reference to '" + relatedStore.getResourceService().getResourceName() + "' instance from '" + oldReferencedInstancePk + "' to null.");
referencingInstance[fkAttr] = null;
};
break;
case 'ignore':
onDispose = function (referencingStore, referencingInstance, oldReferencedInstancePk, fkAttr) { };
break;
}
/**
* Gets the store the relation is configured on.
*
* @memberOf ResourceStoreRelation
* @function getStore
* @return {ResourceStore} Resource store to work on
* @instance
*/
self.getStore = function () {
return store;
};
/**
* Gets the store the configured store is related on.
*
* @memberOf ResourceStoreRelation
* @function getRelatedStore
* @return {ResourceStore} Related resource store
* @instance
*/
self.getRelatedStore = function () {
return relatedStore;
};
/**
* Gets the FK attribute name.
*
* @memberOf ResourceStoreRelation
* @function getFkAttr
* @return {String} Name of the foreign key attribute on related instances
* @instance
*/
self.getFkAttr = function () {
return fkAttr;
};
/**
* Starts the configured update behaviour on the referencing instances on the related store. This is
* called when an instance was saved / updated on the server and got a new PK.
*
* @memberOf ResourceStoreRelation
* @function handleUpdate
* @param {String|int} oldPkValue PK of the instance on the store before the update
* @param {String|int} newPkValue PK of the instance on the store after the update
* @instance
*/
self.handleUpdate = function (oldPkValue, newPkValue) {
console.log("ResourceStoreRelation: Handle update of referenced instance on '" + relatedStore.getResourceService().getResourceName() + "' store.");
var
referencingInstances = relatedStore.getManagedInstances();
for (var i = 0; i < referencingInstances.length; i++) {
var
referencingInstance = referencingInstances[i];
if (referencingInstance && referencingInstance[fkAttr] == oldPkValue && oldPkValue != newPkValue) {
onUpdate(relatedStore, referencingInstance, oldPkValue, newPkValue, fkAttr);
}
}
};
/**
* Starts the configured remove behaviour on the referencing instances on the related store. This is
* called when an instance was deleted on the server.
*
* @memberOf ResourceStoreRelation
* @function handleRemove
* @param {String|int} pkValue PK of the instance on the store
* @instance
*/
self.handleRemove = function (pkValue) {
console.log("ResourceStoreRelation: Handle remove of referenced instance on '" + relatedStore.getResourceService().getResourceName() + "' store.");
var
referencingInstances = relatedStore.getManagedInstances();
for (var i = 0; i < referencingInstances.length; i++) {
var
referencingInstance = referencingInstances[i];
if (referencingInstance && referencingInstance[fkAttr] == pkValue) {
onRemove(relatedStore, referencingInstance, pkValue, fkAttr);
}
}
};
/**
* Starts the configured dispose behaviour on the referencing instances on the related store. This is
* called when an instance was removed or forgot on the store.
*
* @memberOf ResourceStoreRelation
* @function handleDispose
* @param {String|int} pkValue PK of the instance on the store
* @instance
*/
self.handleDispose = function (pkValue) {
console.log("ResourceStoreRelation: Handle dispose of referenced instance on '" + relatedStore.getResourceService().getResourceName() + "' store.");
var
referencingInstances = relatedStore.getManagedInstances();
for (var i = 0; i < referencingInstances.length; i++) {
var
referencingInstance = referencingInstances[i];
if (referencingInstance && referencingInstance[fkAttr] == pkValue) {
onDispose(relatedStore, referencingInstance, pkValue, fkAttr);
}
}
};
}
}
);
})();