Comparing version 3.2.1 to 3.3.0
@@ -376,5 +376,7 @@ (function() { | ||
/* @type {Array<configuration.model>|configuration.model} */ | ||
self.entityCache = configuration.collectionName ? [] : {}; | ||
self.entityCache = configuration.collectionName ? [] : {}; | ||
// Create the ID -> entityIndex lookup table. | ||
self.entityCache.__lookup = {}; | ||
// The raw cache is data that hasn't been deserialized and is used internally. | ||
self.__entityCacheRaw = null; | ||
self.__entityCacheRaw = null; | ||
@@ -490,3 +492,7 @@ // Should request caching be used at all? | ||
function deserializeCollectionEntry( rawEntity ) { | ||
self.entityCache.push( self.deserializer( rawEntity ) ); | ||
var entityToCache = self.deserializer( rawEntity ); | ||
self.entityCache.push( entityToCache ); | ||
if( self.entityCache.__lookup ) { | ||
self.entityCache.__lookup[ entityToCache.id ] = self.entityCache.length - 1; | ||
} | ||
} | ||
@@ -505,2 +511,12 @@ }; | ||
// Check if our raw entity cache was even initialized. | ||
// It's possible that it isn't, because websocket updates can be received before any manual requests | ||
// were made to the backend. | ||
if( !self.__entityCacheRaw || !self.__entityCacheRaw[ configuration.collectionName || configuration.entityName ] ) { | ||
// We ignore this update and just stack a new read request on top of any existing ones. | ||
// This makes sure that we load the freshest entity in an orderly fashion and lose the state we received | ||
// here, as we're getting the latest version of the entity. | ||
return self.read( _entityReceived.id ); | ||
} | ||
// Determine if the received record consists ONLY of an id property, | ||
@@ -510,2 +526,8 @@ // which would mean that this record was deleted from the backend. | ||
self.logInterface.info( self.logPrefix + "Entity was deleted from the server. Updating cache…" ); | ||
self.__cacheMaintain( self.__entityCacheRaw[ configuration.collectionName || configuration.entityName ], | ||
_entityReceived, | ||
"delete", | ||
false ); | ||
return self.__removeEntityFromCache( _entityReceived.id ); | ||
@@ -515,2 +537,8 @@ | ||
self.logInterface.debug( self.logPrefix + "Entity was updated on the server. Updating cache…" ); | ||
self.__cacheMaintain( self.__entityCacheRaw[ configuration.collectionName || configuration.entityName ], | ||
_entityReceived, | ||
"update", | ||
false ); | ||
return self.__updateCacheWithEntity( self.deserializer( _entityReceived ) ); | ||
@@ -677,2 +705,6 @@ } | ||
// Check if the entity is in the cache and return instantly if found. | ||
if( self.entityCache.__lookup ) { | ||
entityIndex = self.entityCache.__lookup[ id ] || self.entityCache.length; | ||
} | ||
for( var entityIndex = 0, entity = self.entityCache[ 0 ]; | ||
@@ -702,7 +734,16 @@ entityIndex < self.entityCache.length; | ||
var rawEntity = serverResponse.data[ configuration.entityName ]; | ||
// Put the raw entity into our raw entity cache. | ||
// We keep the raw copy to allow caching of the raw data. | ||
self.__cacheMaintain( self.__entityCacheRaw[ configuration.collectionName || configuration.entityName ], | ||
rawEntity, | ||
"update", | ||
false ); | ||
// Deserialize the object and place it into the cache. | ||
// We do not need to check here if the object already exists in the cache. | ||
// While it could be possible that the same entity is retrieved multiple times, __updateCacheWithEntity | ||
// will not insert duplicated into the cache. | ||
var deserialized = self.deserializer( serverResponse.data[ configuration.entityName ] ); | ||
// will not insert duplicates into the cache. | ||
var deserialized = self.deserializer( rawEntity ); | ||
self.__updateCacheWithEntity( deserialized ); | ||
@@ -741,2 +782,6 @@ return deserialized; | ||
// Make sure our raw entity cache exists. | ||
self.__entityCacheRaw = self.__entityCacheRaw || {}; | ||
self.__entityCacheRaw[ configuration.collectionName ] = self.__entityCacheRaw[ configuration.collectionName ] || []; | ||
var requestUri = configuration.entityUri + ( id ? ( "/" + id ) : "" ); | ||
@@ -886,6 +931,11 @@ | ||
/** | ||
* Invoked when the entity was deleted from the server. | ||
* Invoked when the entity was successfully deleted from the server. | ||
* @param {angular.IHttpPromiseCallbackArg|Object} serverResponse The reply sent from the server. | ||
*/ | ||
function onEntityDeleted( serverResponse ) { | ||
self.__cacheMaintain( self.__entityCacheRaw[ configuration.collectionName || configuration.entityName ], | ||
entity, | ||
"delete", | ||
false ); | ||
return self.__removeEntityFromCache( entityId ); | ||
@@ -919,80 +969,153 @@ } | ||
if( !Array.isArray( self.entityCache ) ) { | ||
// Allow the user to intervene in the update process, before updating the entity. | ||
self.scope.$broadcast( "beforeEntityUpdated", | ||
{ | ||
service : self, | ||
cache : self.entityCache, | ||
entity : self.entityCache, | ||
updated : entityToCache | ||
} ); | ||
return self.__cacheMaintain( self.entityCache, entityToCache, "update", true ); | ||
}; | ||
if( typeof self.entityCache.copyFrom === "function" ) { | ||
self.entityCache.copyFrom( entityToCache ); | ||
/** | ||
* Perform maintenance operations on a cache. | ||
* @param cache The cache to operate on. | ||
* @param entityToCache The entity that the operation is relating to. | ||
* @param {String} operation The operation to perform. | ||
* @param {Boolean} [emit=false] Should appropriate absync events be broadcast to notify other actors? | ||
* @private | ||
*/ | ||
CacheService.prototype.__cacheMaintain = function CacheService$cacheMaintain( cache, entityToCache, operation, emit ) { | ||
var self = this; | ||
} else { | ||
angular.extend( self.entityCache, entityToCache ); | ||
} | ||
var entityIndex = 0; | ||
var entity = cache[ entityIndex ]; | ||
// After updating the entity, send another event to allow the user to react. | ||
self.scope.$broadcast( "entityUpdated", | ||
{ | ||
service : self, | ||
cache : self.entityCache, | ||
entity : self.entityCache | ||
} ); | ||
return; | ||
if( cache.__lookup ) { | ||
entityIndex = cache.__lookup[ entityToCache.id ] || cache.length; | ||
} | ||
var found = false; | ||
for( var entityIndex = 0, entity = self.entityCache[ 0 ]; | ||
entityIndex < self.entityCache.length; | ||
++entityIndex, entity = self.entityCache[ entityIndex ] ) { | ||
if( entity.id == entityToCache.id ) { | ||
// Allow the user to intervene in the update process, before updating the entity. | ||
self.scope.$broadcast( "beforeEntityUpdated", | ||
{ | ||
service : self, | ||
cache : self.entityCache, | ||
entity : self.entityCache[ entityIndex ], | ||
updated : entityToCache | ||
} ); | ||
switch( operation ) { | ||
case "update": | ||
if( !Array.isArray( cache ) ) { | ||
if( emit ) { | ||
// Allow the user to intervene in the update process, before updating the entity. | ||
self.scope.$broadcast( "beforeEntityUpdated", | ||
{ | ||
service : self, | ||
cache : cache, | ||
entity : cache, | ||
updated : entityToCache | ||
} ); | ||
} | ||
// Use the "copyFrom" method on the entity, if it exists, otherwise use naive approach. | ||
var targetEntity = self.entityCache[ entityIndex ]; | ||
if( typeof targetEntity.copyFrom === "function" ) { | ||
targetEntity.copyFrom( entityToCache ); | ||
if( typeof cache.copyFrom === "function" ) { | ||
cache.copyFrom( entityToCache ); | ||
} else { | ||
angular.extend( targetEntity, entityToCache ); | ||
} else { | ||
angular.extend( cache, entityToCache ); | ||
} | ||
// After updating the entity, send another event to allow the user to react. | ||
self.scope.$broadcast( "entityUpdated", | ||
{ | ||
service : self, | ||
cache : cache, | ||
entity : cache | ||
} ); | ||
return; | ||
} | ||
found = true; | ||
var found = false; | ||
for( angular.noop; entityIndex < cache.length; ++entityIndex, entity = cache[ entityIndex ] ) { | ||
if( entity.id === entityToCache.id ) { | ||
if( emit ) { | ||
// Allow the user to intervene in the update process, before updating the entity. | ||
self.scope.$broadcast( "beforeEntityUpdated", | ||
{ | ||
service : self, | ||
cache : cache, | ||
entity : cache[ entityIndex ], | ||
updated : entityToCache | ||
} ); | ||
} | ||
// After updating the entity, send another event to allow the user to react. | ||
self.scope.$broadcast( "entityUpdated", | ||
{ | ||
service : self, | ||
cache : self.entityCache, | ||
entity : self.entityCache[ entityIndex ] | ||
} ); | ||
// Use the "copyFrom" method on the entity, if it exists, otherwise use naive approach. | ||
var targetEntity = cache[ entityIndex ]; | ||
if( typeof targetEntity.copyFrom === "function" ) { | ||
targetEntity.copyFrom( entityToCache ); | ||
} else { | ||
angular.extend( targetEntity, entityToCache ); | ||
} | ||
found = true; | ||
if( emit ) { | ||
// After updating the entity, send another event to allow the user to react. | ||
self.scope.$broadcast( "entityUpdated", | ||
{ | ||
service : self, | ||
cache : cache, | ||
entity : cache[ entityIndex ] | ||
} ); | ||
} | ||
break; | ||
} | ||
} | ||
// If the entity wasn't found in our records, it's a new entity. | ||
if( !found ) { | ||
if( emit ) { | ||
self.scope.$broadcast( "beforeEntityNew", { | ||
service : self, | ||
cache : cache, | ||
entity : entityToCache | ||
} ); | ||
} | ||
cache.push( entityToCache ); | ||
if( cache.__lookup ) { | ||
cache.__lookup[ entityToCache.id ] = cache.length - 1; | ||
} | ||
if( emit ) { | ||
self.scope.$broadcast( "entityNew", { | ||
service : self, | ||
cache : cache, | ||
entity : entityToCache | ||
} ); | ||
} | ||
} | ||
break; | ||
} | ||
} | ||
// If the entity wasn't found in our records, it's a new entity. | ||
if( !found ) { | ||
self.scope.$broadcast( "beforeEntityNew", { | ||
service : self, | ||
cache : self.entityCache, | ||
entity : entityToCache | ||
} ); | ||
case "delete": | ||
// The "delete" operation is not expected to happen for single cached entities. | ||
for( angular.noop; entityIndex < cache.length; ++entityIndex, entity = cache[ entityIndex ] ) { | ||
if( entity.id === entityToCache.id ) { | ||
if( emit ) { | ||
// Before removing the entity, allow the user to react. | ||
self.scope.$broadcast( "beforeEntityRemoved", { | ||
service : self, | ||
cache : cache, | ||
entity : entity | ||
} ); | ||
} | ||
self.entityCache.push( entityToCache ); | ||
// Remove the entity from the cache. | ||
cache.splice( entityIndex, 1 ); | ||
self.scope.$broadcast( "entityNew", { | ||
service : self, | ||
cache : self.entityCache, | ||
entity : entityToCache | ||
} ); | ||
if( cache.__lookup ) { | ||
for( var cacheEntry in cache.__lookup ) { | ||
if( entityIndex <= cache.__lookup[ cacheEntry ] ) { | ||
--cache.__lookup[ cacheEntry ]; | ||
} | ||
} | ||
} | ||
if( emit ) { | ||
// Send another event to allow the user to take note of the removal. | ||
self.scope.$broadcast( "entityRemoved", { | ||
service : self, | ||
cache : cache, | ||
entity : entity | ||
} ); | ||
} | ||
break; | ||
} | ||
} | ||
break; | ||
} | ||
@@ -1009,25 +1132,5 @@ }; | ||
for( var entityIndex = 0, entity = self.entityCache[ 0 ]; | ||
entityIndex < self.entityCache.length; | ||
++entityIndex, entity = self.entityCache[ entityIndex ] ) { | ||
if( entity.id == id ) { | ||
// Before removing the entity, allow the user to react. | ||
self.scope.$broadcast( "beforeEntityRemoved", { | ||
service : self, | ||
cache : self.entityCache, | ||
entity : entity | ||
} ); | ||
// Remove the entity from the cache. | ||
self.entityCache.splice( entityIndex, 1 ); | ||
// Send another event to allow the user to take note of the removal. | ||
self.scope.$broadcast( "entityRemoved", { | ||
service : self, | ||
cache : self.entityCache, | ||
entity : entity | ||
} ); | ||
break; | ||
} | ||
} | ||
return self.__cacheMaintain( self.entityCache, { | ||
id : id | ||
}, "delete", true ); | ||
}; | ||
@@ -1043,3 +1146,6 @@ | ||
// TODO: Keep a copy of the lookup table and only update it when the cached data updates | ||
if( self.entityCache.__lookup ) { | ||
return angular.copy( self.entityCache.__lookup ); | ||
} | ||
var lookupTable = []; | ||
@@ -1094,9 +1200,31 @@ for( var entityIndex = 0; | ||
* type instances which are being referenced in entity. | ||
* @param {Boolean} [force=false] If true, all complex types will be replaced with references to the | ||
* @param {Object|Boolean} [options] A hash with options relating to the population process. | ||
* @param {Boolean} [options.force=false] If true, all complex types will be replaced with references to the | ||
* instances in cache; otherwise, only properties that are string representations of complex type IDs will be replaced. | ||
* @param {Boolean} [options.crossLink=false] If true, the entity will also be put into a relating property in the | ||
* foreign entity. | ||
* @param {String} [options.crossLinkProperty] The name of the property in the foreign type into which the entity | ||
* should be cross-linked. | ||
* @returns {IPromise<TResult>|IPromise<any[]>|IPromise<{}>|angular.IPromise<TResult>} | ||
*/ | ||
CacheService.prototype.populateComplex = function CacheService$populateComplex( entity, propertyName, cache, force ) { | ||
CacheService.prototype.populateComplex = function CacheService$populateComplex( entity, propertyName, cache, options ) { | ||
var self = this; | ||
options = options || {}; | ||
if( typeof options === "boolean" ) { | ||
self.logInterface.warn( "Argument 'force' is deprecated. Provide an options hash instead." ); | ||
options = { | ||
force : options | ||
}; | ||
} | ||
options.force = options.force || false; | ||
options.crossLink = options.crossLink || false; | ||
options.crossLinkProperty = options.crossLinkProperty || ""; | ||
if( options.crossLink && !options.crossLinkProperty ) { | ||
self.logInterface.warn( | ||
"Option 'crossLink' given without 'crossLinkProperty'. Cross-linking will be disabled." ); | ||
options.crossLink = false; | ||
} | ||
// If the target property is an array, ... | ||
@@ -1113,3 +1241,3 @@ if( Array.isArray( entity[ propertyName ] ) ) { | ||
// If "force" is enabled, we check if this non-string property is an object and has an "id" member, which is a string. | ||
if( force && typeof entity[ propertyName ] === "object" && typeof entity[ propertyName ].id === "string" ) { | ||
if( options.force && typeof entity[ propertyName ] === "object" && typeof entity[ propertyName ].id === "string" ) { | ||
// If that is true, then we replace the whole object with the ID and continue as usual. | ||
@@ -1119,2 +1247,5 @@ entity[ propertyName ] = entity[ propertyName ].id; | ||
} else { | ||
if( self.throwFailures ) { | ||
throw new Error( "The referenced entity did not have an 'id' property that would be expected." ); | ||
} | ||
return self.q.when( false ); | ||
@@ -1133,3 +1264,3 @@ } | ||
// If "force" is enabled, we check if this non-string property is an object and has an "id" member, which is a string. | ||
if( force && typeof entity[ propertyName ][ index ] === "object" && typeof entity[ propertyName ][ index ].id === "string" ) { | ||
if( options.force && typeof entity[ propertyName ][ index ] === "object" && typeof entity[ propertyName ][ index ].id === "string" ) { | ||
// If that is true, then we replace the whole object with the ID and continue as usual. | ||
@@ -1139,2 +1270,6 @@ entity[ propertyName ][ index ] = entity[ propertyName ][ index ].id; | ||
} else { | ||
if( self.throwFailures ) { | ||
throw new Error( "The referenced entity did not have an 'id' property that would be expected." ); | ||
} | ||
return self.q.when( false ); | ||
@@ -1151,2 +1286,7 @@ } | ||
entity[ propertyName ][ index ] = complex; | ||
if( options.crossLink ) { | ||
crossLink( complex, entity ); | ||
} | ||
return entity; | ||
@@ -1159,3 +1299,34 @@ } | ||
entity[ propertyName ] = complex; | ||
if( options.crossLink ) { | ||
crossLink( complex, entity ); | ||
} | ||
return entity; | ||
} | ||
function crossLink( complex, entity ) { | ||
// If cross-linking is enabled, put our entity into the foreign complex. | ||
if( Array.isArray( complex[ options.crossLinkProperty ] ) ) { | ||
// Check if the entity is already linked into the array. | ||
var entityIndex = complex[ options.crossLinkProperty ].indexOf( entity ); | ||
if( -1 < entityIndex ) { | ||
return; | ||
} | ||
// Check if the ID exists in the array. | ||
var idIndex = complex[ options.crossLinkProperty ].indexOf( entity.id ); | ||
if( -1 < idIndex ) { | ||
// Replace the ID with the entity. | ||
complex[ options.crossLinkProperty ][ idIndex ] = entity; | ||
return; | ||
} | ||
// Just push the element into the array. | ||
complex[ options.crossLinkProperty ].push( entity ); | ||
return; | ||
} | ||
complex[ options.crossLinkProperty ] = entity; | ||
} | ||
}; | ||
@@ -1171,3 +1342,5 @@ | ||
self.entityCache = self.configuration.collectionName ? [] : {}; | ||
self.entityCache = self.configuration.collectionName ? [] : {}; | ||
self.entityCache.__lookup = self.entityCache.__lookup || {}; | ||
self.__entityCacheRaw = null; | ||
@@ -1250,2 +1423,3 @@ self.__requestCache = {}; | ||
info : angular.noop, | ||
warn : angular.noop, | ||
error : angular.noop | ||
@@ -1252,0 +1426,0 @@ } ); |
@@ -67,5 +67,7 @@ (function() { | ||
/* @type {Array<configuration.model>|configuration.model} */ | ||
self.entityCache = configuration.collectionName ? [] : {}; | ||
self.entityCache = configuration.collectionName ? [] : {}; | ||
// Create the ID -> entityIndex lookup table. | ||
self.entityCache.__lookup = {}; | ||
// The raw cache is data that hasn't been deserialized and is used internally. | ||
self.__entityCacheRaw = null; | ||
self.__entityCacheRaw = null; | ||
@@ -181,3 +183,7 @@ // Should request caching be used at all? | ||
function deserializeCollectionEntry( rawEntity ) { | ||
self.entityCache.push( self.deserializer( rawEntity ) ); | ||
var entityToCache = self.deserializer( rawEntity ); | ||
self.entityCache.push( entityToCache ); | ||
if( self.entityCache.__lookup ) { | ||
self.entityCache.__lookup[ entityToCache.id ] = self.entityCache.length - 1; | ||
} | ||
} | ||
@@ -196,2 +202,12 @@ }; | ||
// Check if our raw entity cache was even initialized. | ||
// It's possible that it isn't, because websocket updates can be received before any manual requests | ||
// were made to the backend. | ||
if( !self.__entityCacheRaw || !self.__entityCacheRaw[ configuration.collectionName || configuration.entityName ] ) { | ||
// We ignore this update and just stack a new read request on top of any existing ones. | ||
// This makes sure that we load the freshest entity in an orderly fashion and lose the state we received | ||
// here, as we're getting the latest version of the entity. | ||
return self.read( _entityReceived.id ); | ||
} | ||
// Determine if the received record consists ONLY of an id property, | ||
@@ -201,2 +217,8 @@ // which would mean that this record was deleted from the backend. | ||
self.logInterface.info( self.logPrefix + "Entity was deleted from the server. Updating cache…" ); | ||
self.__cacheMaintain( self.__entityCacheRaw[ configuration.collectionName || configuration.entityName ], | ||
_entityReceived, | ||
"delete", | ||
false ); | ||
return self.__removeEntityFromCache( _entityReceived.id ); | ||
@@ -206,2 +228,8 @@ | ||
self.logInterface.debug( self.logPrefix + "Entity was updated on the server. Updating cache…" ); | ||
self.__cacheMaintain( self.__entityCacheRaw[ configuration.collectionName || configuration.entityName ], | ||
_entityReceived, | ||
"update", | ||
false ); | ||
return self.__updateCacheWithEntity( self.deserializer( _entityReceived ) ); | ||
@@ -368,2 +396,6 @@ } | ||
// Check if the entity is in the cache and return instantly if found. | ||
if( self.entityCache.__lookup ) { | ||
entityIndex = self.entityCache.__lookup[ id ] || self.entityCache.length; | ||
} | ||
for( var entityIndex = 0, entity = self.entityCache[ 0 ]; | ||
@@ -393,7 +425,16 @@ entityIndex < self.entityCache.length; | ||
var rawEntity = serverResponse.data[ configuration.entityName ]; | ||
// Put the raw entity into our raw entity cache. | ||
// We keep the raw copy to allow caching of the raw data. | ||
self.__cacheMaintain( self.__entityCacheRaw[ configuration.collectionName || configuration.entityName ], | ||
rawEntity, | ||
"update", | ||
false ); | ||
// Deserialize the object and place it into the cache. | ||
// We do not need to check here if the object already exists in the cache. | ||
// While it could be possible that the same entity is retrieved multiple times, __updateCacheWithEntity | ||
// will not insert duplicated into the cache. | ||
var deserialized = self.deserializer( serverResponse.data[ configuration.entityName ] ); | ||
// will not insert duplicates into the cache. | ||
var deserialized = self.deserializer( rawEntity ); | ||
self.__updateCacheWithEntity( deserialized ); | ||
@@ -432,2 +473,6 @@ return deserialized; | ||
// Make sure our raw entity cache exists. | ||
self.__entityCacheRaw = self.__entityCacheRaw || {}; | ||
self.__entityCacheRaw[ configuration.collectionName ] = self.__entityCacheRaw[ configuration.collectionName ] || []; | ||
var requestUri = configuration.entityUri + ( id ? ( "/" + id ) : "" ); | ||
@@ -577,6 +622,11 @@ | ||
/** | ||
* Invoked when the entity was deleted from the server. | ||
* Invoked when the entity was successfully deleted from the server. | ||
* @param {angular.IHttpPromiseCallbackArg|Object} serverResponse The reply sent from the server. | ||
*/ | ||
function onEntityDeleted( serverResponse ) { | ||
self.__cacheMaintain( self.__entityCacheRaw[ configuration.collectionName || configuration.entityName ], | ||
entity, | ||
"delete", | ||
false ); | ||
return self.__removeEntityFromCache( entityId ); | ||
@@ -610,80 +660,153 @@ } | ||
if( !Array.isArray( self.entityCache ) ) { | ||
// Allow the user to intervene in the update process, before updating the entity. | ||
self.scope.$broadcast( "beforeEntityUpdated", | ||
{ | ||
service : self, | ||
cache : self.entityCache, | ||
entity : self.entityCache, | ||
updated : entityToCache | ||
} ); | ||
return self.__cacheMaintain( self.entityCache, entityToCache, "update", true ); | ||
}; | ||
if( typeof self.entityCache.copyFrom === "function" ) { | ||
self.entityCache.copyFrom( entityToCache ); | ||
/** | ||
* Perform maintenance operations on a cache. | ||
* @param cache The cache to operate on. | ||
* @param entityToCache The entity that the operation is relating to. | ||
* @param {String} operation The operation to perform. | ||
* @param {Boolean} [emit=false] Should appropriate absync events be broadcast to notify other actors? | ||
* @private | ||
*/ | ||
CacheService.prototype.__cacheMaintain = function CacheService$cacheMaintain( cache, entityToCache, operation, emit ) { | ||
var self = this; | ||
} else { | ||
angular.extend( self.entityCache, entityToCache ); | ||
} | ||
var entityIndex = 0; | ||
var entity = cache[ entityIndex ]; | ||
// After updating the entity, send another event to allow the user to react. | ||
self.scope.$broadcast( "entityUpdated", | ||
{ | ||
service : self, | ||
cache : self.entityCache, | ||
entity : self.entityCache | ||
} ); | ||
return; | ||
if( cache.__lookup ) { | ||
entityIndex = cache.__lookup[ entityToCache.id ] || cache.length; | ||
} | ||
var found = false; | ||
for( var entityIndex = 0, entity = self.entityCache[ 0 ]; | ||
entityIndex < self.entityCache.length; | ||
++entityIndex, entity = self.entityCache[ entityIndex ] ) { | ||
if( entity.id == entityToCache.id ) { | ||
// Allow the user to intervene in the update process, before updating the entity. | ||
self.scope.$broadcast( "beforeEntityUpdated", | ||
{ | ||
service : self, | ||
cache : self.entityCache, | ||
entity : self.entityCache[ entityIndex ], | ||
updated : entityToCache | ||
} ); | ||
switch( operation ) { | ||
case "update": | ||
if( !Array.isArray( cache ) ) { | ||
if( emit ) { | ||
// Allow the user to intervene in the update process, before updating the entity. | ||
self.scope.$broadcast( "beforeEntityUpdated", | ||
{ | ||
service : self, | ||
cache : cache, | ||
entity : cache, | ||
updated : entityToCache | ||
} ); | ||
} | ||
// Use the "copyFrom" method on the entity, if it exists, otherwise use naive approach. | ||
var targetEntity = self.entityCache[ entityIndex ]; | ||
if( typeof targetEntity.copyFrom === "function" ) { | ||
targetEntity.copyFrom( entityToCache ); | ||
if( typeof cache.copyFrom === "function" ) { | ||
cache.copyFrom( entityToCache ); | ||
} else { | ||
angular.extend( targetEntity, entityToCache ); | ||
} else { | ||
angular.extend( cache, entityToCache ); | ||
} | ||
// After updating the entity, send another event to allow the user to react. | ||
self.scope.$broadcast( "entityUpdated", | ||
{ | ||
service : self, | ||
cache : cache, | ||
entity : cache | ||
} ); | ||
return; | ||
} | ||
found = true; | ||
var found = false; | ||
for( angular.noop; entityIndex < cache.length; ++entityIndex, entity = cache[ entityIndex ] ) { | ||
if( entity.id === entityToCache.id ) { | ||
if( emit ) { | ||
// Allow the user to intervene in the update process, before updating the entity. | ||
self.scope.$broadcast( "beforeEntityUpdated", | ||
{ | ||
service : self, | ||
cache : cache, | ||
entity : cache[ entityIndex ], | ||
updated : entityToCache | ||
} ); | ||
} | ||
// After updating the entity, send another event to allow the user to react. | ||
self.scope.$broadcast( "entityUpdated", | ||
{ | ||
service : self, | ||
cache : self.entityCache, | ||
entity : self.entityCache[ entityIndex ] | ||
} ); | ||
// Use the "copyFrom" method on the entity, if it exists, otherwise use naive approach. | ||
var targetEntity = cache[ entityIndex ]; | ||
if( typeof targetEntity.copyFrom === "function" ) { | ||
targetEntity.copyFrom( entityToCache ); | ||
} else { | ||
angular.extend( targetEntity, entityToCache ); | ||
} | ||
found = true; | ||
if( emit ) { | ||
// After updating the entity, send another event to allow the user to react. | ||
self.scope.$broadcast( "entityUpdated", | ||
{ | ||
service : self, | ||
cache : cache, | ||
entity : cache[ entityIndex ] | ||
} ); | ||
} | ||
break; | ||
} | ||
} | ||
// If the entity wasn't found in our records, it's a new entity. | ||
if( !found ) { | ||
if( emit ) { | ||
self.scope.$broadcast( "beforeEntityNew", { | ||
service : self, | ||
cache : cache, | ||
entity : entityToCache | ||
} ); | ||
} | ||
cache.push( entityToCache ); | ||
if( cache.__lookup ) { | ||
cache.__lookup[ entityToCache.id ] = cache.length - 1; | ||
} | ||
if( emit ) { | ||
self.scope.$broadcast( "entityNew", { | ||
service : self, | ||
cache : cache, | ||
entity : entityToCache | ||
} ); | ||
} | ||
} | ||
break; | ||
} | ||
} | ||
// If the entity wasn't found in our records, it's a new entity. | ||
if( !found ) { | ||
self.scope.$broadcast( "beforeEntityNew", { | ||
service : self, | ||
cache : self.entityCache, | ||
entity : entityToCache | ||
} ); | ||
case "delete": | ||
// The "delete" operation is not expected to happen for single cached entities. | ||
for( angular.noop; entityIndex < cache.length; ++entityIndex, entity = cache[ entityIndex ] ) { | ||
if( entity.id === entityToCache.id ) { | ||
if( emit ) { | ||
// Before removing the entity, allow the user to react. | ||
self.scope.$broadcast( "beforeEntityRemoved", { | ||
service : self, | ||
cache : cache, | ||
entity : entity | ||
} ); | ||
} | ||
self.entityCache.push( entityToCache ); | ||
// Remove the entity from the cache. | ||
cache.splice( entityIndex, 1 ); | ||
self.scope.$broadcast( "entityNew", { | ||
service : self, | ||
cache : self.entityCache, | ||
entity : entityToCache | ||
} ); | ||
if( cache.__lookup ) { | ||
for( var cacheEntry in cache.__lookup ) { | ||
if( entityIndex <= cache.__lookup[ cacheEntry ] ) { | ||
--cache.__lookup[ cacheEntry ]; | ||
} | ||
} | ||
} | ||
if( emit ) { | ||
// Send another event to allow the user to take note of the removal. | ||
self.scope.$broadcast( "entityRemoved", { | ||
service : self, | ||
cache : cache, | ||
entity : entity | ||
} ); | ||
} | ||
break; | ||
} | ||
} | ||
break; | ||
} | ||
@@ -700,25 +823,5 @@ }; | ||
for( var entityIndex = 0, entity = self.entityCache[ 0 ]; | ||
entityIndex < self.entityCache.length; | ||
++entityIndex, entity = self.entityCache[ entityIndex ] ) { | ||
if( entity.id == id ) { | ||
// Before removing the entity, allow the user to react. | ||
self.scope.$broadcast( "beforeEntityRemoved", { | ||
service : self, | ||
cache : self.entityCache, | ||
entity : entity | ||
} ); | ||
// Remove the entity from the cache. | ||
self.entityCache.splice( entityIndex, 1 ); | ||
// Send another event to allow the user to take note of the removal. | ||
self.scope.$broadcast( "entityRemoved", { | ||
service : self, | ||
cache : self.entityCache, | ||
entity : entity | ||
} ); | ||
break; | ||
} | ||
} | ||
return self.__cacheMaintain( self.entityCache, { | ||
id : id | ||
}, "delete", true ); | ||
}; | ||
@@ -734,3 +837,6 @@ | ||
// TODO: Keep a copy of the lookup table and only update it when the cached data updates | ||
if( self.entityCache.__lookup ) { | ||
return angular.copy( self.entityCache.__lookup ); | ||
} | ||
var lookupTable = []; | ||
@@ -785,9 +891,31 @@ for( var entityIndex = 0; | ||
* type instances which are being referenced in entity. | ||
* @param {Boolean} [force=false] If true, all complex types will be replaced with references to the | ||
* @param {Object|Boolean} [options] A hash with options relating to the population process. | ||
* @param {Boolean} [options.force=false] If true, all complex types will be replaced with references to the | ||
* instances in cache; otherwise, only properties that are string representations of complex type IDs will be replaced. | ||
* @param {Boolean} [options.crossLink=false] If true, the entity will also be put into a relating property in the | ||
* foreign entity. | ||
* @param {String} [options.crossLinkProperty] The name of the property in the foreign type into which the entity | ||
* should be cross-linked. | ||
* @returns {IPromise<TResult>|IPromise<any[]>|IPromise<{}>|angular.IPromise<TResult>} | ||
*/ | ||
CacheService.prototype.populateComplex = function CacheService$populateComplex( entity, propertyName, cache, force ) { | ||
CacheService.prototype.populateComplex = function CacheService$populateComplex( entity, propertyName, cache, options ) { | ||
var self = this; | ||
options = options || {}; | ||
if( typeof options === "boolean" ) { | ||
self.logInterface.warn( "Argument 'force' is deprecated. Provide an options hash instead." ); | ||
options = { | ||
force : options | ||
}; | ||
} | ||
options.force = options.force || false; | ||
options.crossLink = options.crossLink || false; | ||
options.crossLinkProperty = options.crossLinkProperty || ""; | ||
if( options.crossLink && !options.crossLinkProperty ) { | ||
self.logInterface.warn( | ||
"Option 'crossLink' given without 'crossLinkProperty'. Cross-linking will be disabled." ); | ||
options.crossLink = false; | ||
} | ||
// If the target property is an array, ... | ||
@@ -804,3 +932,3 @@ if( Array.isArray( entity[ propertyName ] ) ) { | ||
// If "force" is enabled, we check if this non-string property is an object and has an "id" member, which is a string. | ||
if( force && typeof entity[ propertyName ] === "object" && typeof entity[ propertyName ].id === "string" ) { | ||
if( options.force && typeof entity[ propertyName ] === "object" && typeof entity[ propertyName ].id === "string" ) { | ||
// If that is true, then we replace the whole object with the ID and continue as usual. | ||
@@ -810,2 +938,5 @@ entity[ propertyName ] = entity[ propertyName ].id; | ||
} else { | ||
if( self.throwFailures ) { | ||
throw new Error( "The referenced entity did not have an 'id' property that would be expected." ); | ||
} | ||
return self.q.when( false ); | ||
@@ -824,3 +955,3 @@ } | ||
// If "force" is enabled, we check if this non-string property is an object and has an "id" member, which is a string. | ||
if( force && typeof entity[ propertyName ][ index ] === "object" && typeof entity[ propertyName ][ index ].id === "string" ) { | ||
if( options.force && typeof entity[ propertyName ][ index ] === "object" && typeof entity[ propertyName ][ index ].id === "string" ) { | ||
// If that is true, then we replace the whole object with the ID and continue as usual. | ||
@@ -830,2 +961,6 @@ entity[ propertyName ][ index ] = entity[ propertyName ][ index ].id; | ||
} else { | ||
if( self.throwFailures ) { | ||
throw new Error( "The referenced entity did not have an 'id' property that would be expected." ); | ||
} | ||
return self.q.when( false ); | ||
@@ -842,2 +977,7 @@ } | ||
entity[ propertyName ][ index ] = complex; | ||
if( options.crossLink ) { | ||
crossLink( complex, entity ); | ||
} | ||
return entity; | ||
@@ -850,3 +990,34 @@ } | ||
entity[ propertyName ] = complex; | ||
if( options.crossLink ) { | ||
crossLink( complex, entity ); | ||
} | ||
return entity; | ||
} | ||
function crossLink( complex, entity ) { | ||
// If cross-linking is enabled, put our entity into the foreign complex. | ||
if( Array.isArray( complex[ options.crossLinkProperty ] ) ) { | ||
// Check if the entity is already linked into the array. | ||
var entityIndex = complex[ options.crossLinkProperty ].indexOf( entity ); | ||
if( -1 < entityIndex ) { | ||
return; | ||
} | ||
// Check if the ID exists in the array. | ||
var idIndex = complex[ options.crossLinkProperty ].indexOf( entity.id ); | ||
if( -1 < idIndex ) { | ||
// Replace the ID with the entity. | ||
complex[ options.crossLinkProperty ][ idIndex ] = entity; | ||
return; | ||
} | ||
// Just push the element into the array. | ||
complex[ options.crossLinkProperty ].push( entity ); | ||
return; | ||
} | ||
complex[ options.crossLinkProperty ] = entity; | ||
} | ||
}; | ||
@@ -862,3 +1033,5 @@ | ||
self.entityCache = self.configuration.collectionName ? [] : {}; | ||
self.entityCache = self.configuration.collectionName ? [] : {}; | ||
self.entityCache.__lookup = self.entityCache.__lookup || {}; | ||
self.__entityCacheRaw = null; | ||
@@ -865,0 +1038,0 @@ self.__requestCache = {}; |
@@ -10,4 +10,5 @@ (function() { | ||
info : angular.noop, | ||
warn : angular.noop, | ||
error : angular.noop | ||
} ); | ||
}()); |
@@ -1,2 +0,2 @@ | ||
!function(){"use strict";angular.module("absync",[])}(),function(){"use strict";function e(e,n,r){return new t(e,n,r)}function t(e,t,n){var r=this;r.__injector=e,r.__provide=t,r.__absyncCache=n,r.__ioSocket=null,r.__registerLater=[],r.__listeners=[],r.__collections={},r.__entities={},r.debug=void 0}e.$inject=["$injector","$provide","absyncCache"],angular.module("absync").provider("absync",e),t.prototype.configure=function(e){var t=this;if("undefined"!=typeof e.socket){var n=e.socket,r="undefined"!=typeof io&&io.Socket&&n instanceof io.Socket;if("function"==typeof n)t.__ioSocket=new n;else{if(!r)throw new Error("configure() expects input to be a function or a socket.io Socket instance.");t.__ioSocket=n}t.__registerLater.length&&(angular.forEach(t.__registerLater,t.__registerListener.bind(t)),t.__registerLater=[])}"undefined"!=typeof e.debug&&(t.debug=e.debug||!1),t.debug&&(angular.forEach(t.__collections,function(e){e.configuration.debug=!0}),angular.forEach(t.__entities,function(e){e.configuration.debug=!0}))},t.prototype.disconnect=function(e){var t=this;e=e||!1,angular.forEach(t.__listeners,function(e){e.unregister(),delete e.unregister,t.__registerLater.push(e)}),t.__listeners=[],e&&(t.__ioSocket.disconnect(),t.__ioSocket=null)},t.prototype.__registerListener=function(e){var t=this;t.__listeners.push(e),e.unregister=t.__handleEntityEvent(e.eventName,e.callback)},t.prototype.collection=function(e,t){var n=this;if(n.__collections[e])throw new Error("A collection with the name '"+e+"' was already requested. Names for collections must be unique.");if(n.__entities[e])throw new Error("An entity with the name '"+e+"' was already requested. Names for collections must be unique and can't be shared with entities.");t.debug="undefined"==typeof t.debug?n.debug:t.debug,n.__collections[e]={constructor:n.__absyncCache(e,t),configuration:t},n.__provide.service(e,n.__collections[e].constructor)},t.prototype.entity=function(e,t){var n=this;if(n.__entities[e])throw new Error("An entity with the name '"+e+"' was already requested. Names for entities must be unique.");if(n.__collections[e])throw new Error("A collection with the name '"+e+"' was already requested. Names for entities must be unique and can't be shared with collections.");t.debug="undefined"==typeof t.debug?n.debug:t.debug,n.__entities[e]={constructor:n.__absyncCache(e,t),configuration:t},n.__provide.service(e,n.__entities[e].constructor)},t.prototype.on=function(e,t){var n=this;return n.__ioSocket?n.__registerListener({eventName:e,callback:t}):n.__registerLater.length>8192?null:(n.__registerLater.push({eventName:e,callback:t}),null)},t.prototype.__handleEntityEvent=function(e,t){var n=this;return n.__ioSocket.on(e,t),function(){n.__ioSocket.removeListener(e,t)}},t.prototype.emit=function(e,t,n){var r=this;if(!r.__ioSocket)throw new Error("socket.io is not initialized.");r.__ioSocket.emit(e,t,function(){n&&n.apply(r.__ioSocket,arguments)})},t.prototype.$get=function(){return this}}(),function(){"use strict";function e(e,n){function r(r,i,o,a,c,s,u,h){var l=this,d=n.injector||i,y=d.has(n.model);if(!y)throw new Error("Unable to construct the '"+e+"' service, because the referenced model '"+n.model+"' is not available for injection.");var f="string"==typeof n.model?d.get(n.model):n.model,_=f.serialize||n.serialize||t,p=f.deserialize||n.deserialize||t;l.name=e,l.configuration=n,l.entityCache=n.collectionName?[]:{},l.__entityCacheRaw=null,l.enableRequestCache=!0,l.__requestCache={},l.allowBrowserCache=(angular.merge||angular.extend)({},{sync:!0,request:!0},n.allowBrowserCache),l.__uncached=h,l.httpInterface=r,l.logInterface=n.debug?o:s,l.scope=c,l.q=a,l.logPrefix="absync:"+e.toLocaleUpperCase()+" ",l.forceEarlyCacheUpdate=!1,l.throwFailures=!0,l.serializer=_,l.deserializer=p,u.on(n.entityName,l.__onEntityOnWebsocket.bind(l)),n.collectionName&&u.on(n.collectionName,l.__onCollectionOnWebsocket.bind(l)),c.$on(n.entityName,l.__onEntityReceived.bind(l)),n.collectionName&&c.$on(n.collectionName,l.__onCollectionReceived.bind(l)),l.logInterface.info(l.logPrefix+"service was instantiated.")}function i(e,t){var r=this;if((e||r.forceEarlyCacheUpdate)&&t.data[n.entityName]){var i=t.data[n.entityName];if(r.forceEarlyCacheUpdate){var o=r.deserializer(i);if(r.__updateCacheWithEntity(o),e)return o}if(e)return i}}function o(e){var t=this;if(t.logInterface.error(t.logPrefix+"Unable to store entity on the server.",e),t.logInterface.error(e),t.scope.$emit("absyncError",e),t.throwFailures)throw e}return r.$inject=["$http","$injector","$log","$q","$rootScope","absyncNoopLog","absync","absyncUncachedFilter"],r.prototype.__onEntityOnWebsocket=function(e){var t=this;t.scope.$broadcast(n.entityName,e[n.entityName])},r.prototype.__onCollectionOnWebsocket=function(e){var t=this;t.scope.$broadcast(n.collectionName,e[n.collectionName])},r.prototype.__onDataAvailable=function(e){function t(e){r.entityCache.push(r.deserializer(e))}var r=this;if(Array.isArray(r.entityCache))angular.forEach(e[n.collectionName],t),r.scope.$broadcast("collectionNew",{service:r,cache:r.entityCache});else{var i=r.deserializer(e[n.entityName]);r.__updateCacheWithEntity(i)}return r.entityCache},r.prototype.__onEntityReceived=function(e,t){var n=this,r=t;return 1===Object.keys(r).length&&r.hasOwnProperty("id")?(n.logInterface.info(n.logPrefix+"Entity was deleted from the server. Updating cache…"),n.__removeEntityFromCache(r.id)):(n.logInterface.debug(n.logPrefix+"Entity was updated on the server. Updating cache…"),n.__updateCacheWithEntity(n.deserializer(r)))},r.prototype.__onCollectionReceived=function(e,t){function n(e){var t=r.deserializer(e);r.__updateCacheWithEntity(t)}var r=this,i=t;r.entityCache.length=0,angular.forEach(i,n)},r.prototype.ensureLoaded=function(e){function t(e){if(!e.data[n.collectionName])throw new Error("The response from the server was not in the expected format. It should have a member named '"+n.collectionName+"'.");return a.__entityCacheRaw=e.data,a.entityCache.splice(0,a.entityCache.length),a.__onDataAvailable(e.data)}function r(e){if(a.logInterface.error(a.logPrefix+"Unable to retrieve the collection from the server.",e),a.__entityCacheRaw=null,a.scope.$emit("absyncError",e),a.throwFailures)throw e}function i(e){if(!e.data[n.entityName])throw new Error("The response from the server was not in the expected format. It should have a member named '"+n.entityName+"'.");a.__entityCacheRaw=e.data,a.__onDataAvailable(e.data)}function o(e){if(a.logInterface.error(a.logPrefix+"Unable to retrieve the entity from the server.",e),a.__entityCacheRaw=null,a.scope.$emit("absyncError",e),a.throwFailures)throw e}var a=this;return e=e===!0,null===a.__entityCacheRaw||e?n.collectionName&&n.collectionUri?(a.logInterface.info(a.logPrefix+"Retrieving '"+n.collectionName+"' collection…"),a.httpInterface.get(a.allowBrowserCache.sync?n.collectionUri:a.__uncached(n.collectionUri)).then(t,r)):n.entityName&&n.entityUri?a.httpInterface.get(a.allowBrowserCache.sync?n.entityUri:a.__uncached(n.entityUri)).then(i,o):a.q.when([]):a.q.when(a.entityCache)},r.prototype.seed=function(e){var t=this;return t.__entityCacheRaw=e,t.__onDataAvailable(t.__entityCacheRaw)},r.prototype.sync=function(){var e=this;return e.__entityCacheRaw=null,e.ensureLoaded(!0)},r.prototype.read=function(e,t){function r(e){if(!e.data[n.entityName])throw new Error("The response from the server was not in the expected format. It should have a member named '"+n.entityName+"'.");var t=o.deserializer(e.data[n.entityName]);return o.__updateCacheWithEntity(t),t}function i(t){if(o.logInterface.error(o.logPrefix+"Unable to retrieve entity with ID '"+e+"' from the server.",t),o.scope.$emit("absyncError",t),o.throwFailures)throw t}var o=this;if(t=t===!0,o.logInterface.debug(o.logPrefix+"Requesting entity '"+e+"' (forceReload:"+t+")…"),!t)for(var a=0,c=o.entityCache[0];a<o.entityCache.length;++a,c=o.entityCache[a])if(c.id===e)return o.logInterface.debug(o.logPrefix+"Requested entity '"+e+"' is served from cache."),o.q.when(c);return o.logInterface.debug(o.logPrefix+"Requested entity '"+e+"' is fetched from backend."),o.__requestEntity(e).then(r,i)},r.prototype.__requestEntity=function(e){function t(e,t){return delete r.__requestCache[e],t}var r=this;if(r.enableRequestCache&&r.__requestCache&&r.__requestCache[e])return r.logInterface.debug(r.logPrefix+"Entity request '"+e+"' served from request cache."),r.__requestCache[e];var i=n.entityUri+(e?"/"+e:""),o=r.httpInterface.get(r.allowBrowserCache.request?i:r.__uncached(i)).then(t.bind(r,e));return r.enableRequestCache&&r.__requestCache&&(r.__requestCache[e]=o),o},r.prototype.update=function(e,t){var r=this;t=t||!1;var a=r.reduceComplex(e),c=r.serializer(a),s={};return s[n.entityName]=c,"undefined"!=typeof e.id?r.httpInterface.put(n.entityUri+"/"+e.id,s).then(i.bind(r,t),o.bind(r)):r.httpInterface.post(n.collectionUri,s).then(i.bind(r,t),o.bind(r))},r.prototype.patch=function(e){var t=this,r=t.reduceComplex(e),a=t.serializer(r),c={};if(c[n.entityName]=a,"undefined"!=typeof e.id)return t.httpInterface.patch(n.entityUri+"/"+e.id,c).then(i.bind(t),o.bind(t));throw new Error("Attempted to patch an entity that was never stored on the server.")},r.prototype.create=r.prototype.update,r.prototype["delete"]=function(e){function t(e){return i.__removeEntityFromCache(o)}function r(e){if(i.logInterface.error(e.data),i.scope.$emit("absyncError",e),i.throwFailures)throw e}var i=this,o=e.id;return i.httpInterface["delete"](n.entityUri+"/"+o).then(t)["catch"](r)},r.prototype.__updateCacheWithEntity=function(e){var t=this;if(t.logInterface.info(t.logPrefix+"Updating entity '"+(e.id||t.name)+"' in cache…",e),!Array.isArray(t.entityCache))return t.scope.$broadcast("beforeEntityUpdated",{service:t,cache:t.entityCache,entity:t.entityCache,updated:e}),"function"==typeof t.entityCache.copyFrom?t.entityCache.copyFrom(e):angular.extend(t.entityCache,e),void t.scope.$broadcast("entityUpdated",{service:t,cache:t.entityCache,entity:t.entityCache});for(var n=!1,r=0,i=t.entityCache[0];r<t.entityCache.length;++r,i=t.entityCache[r])if(i.id==e.id){t.scope.$broadcast("beforeEntityUpdated",{service:t,cache:t.entityCache,entity:t.entityCache[r],updated:e});var o=t.entityCache[r];"function"==typeof o.copyFrom?o.copyFrom(e):angular.extend(o,e),n=!0,t.scope.$broadcast("entityUpdated",{service:t,cache:t.entityCache,entity:t.entityCache[r]});break}n||(t.scope.$broadcast("beforeEntityNew",{service:t,cache:t.entityCache,entity:e}),t.entityCache.push(e),t.scope.$broadcast("entityNew",{service:t,cache:t.entityCache,entity:e}))},r.prototype.__removeEntityFromCache=function(e){for(var t=this,n=0,r=t.entityCache[0];n<t.entityCache.length;++n,r=t.entityCache[n])if(r.id==e){t.scope.$broadcast("beforeEntityRemoved",{service:t,cache:t.entityCache,entity:r}),t.entityCache.splice(n,1),t.scope.$broadcast("entityRemoved",{service:t,cache:t.entityCache,entity:r});break}},r.prototype.lookupTableById=function(){for(var e=this,t=[],n=0;n<e.entityCache.length;++n)t[e.entityCache[n].id]=e.entityCache[n];return t},r.prototype.reduceComplex=function(e,t){var n=this,r=t?[]:{};for(var i in e)e.hasOwnProperty(i)&&(Array.isArray(e[i])?r[i]=n.reduceComplex(e[i],!0):e[i]&&e[i].id?r[i]=e[i].id:r[i]=e[i]);return r},r.prototype.populateComplex=function(e,t,n,r){function i(i,o){function c(n){return e[t][o]=n,e}if("string"!=typeof e[t][o]){if(!r||"object"!=typeof e[t][o]||"string"!=typeof e[t][o].id)return a.q.when(!1);e[t][o]=e[t][o].id}return n.read(e[t][o]).then(c)}function o(n){e[t]=n}var a=this;if(Array.isArray(e[t])){var c=e[t].map(i);return a.q.all(c)}if("string"!=typeof e[t]){if(!r||"object"!=typeof e[t]||"string"!=typeof e[t].id)return a.q.when(!1);e[t]=e[t].id}return n.read(e[t]).then(o)},r.prototype.reset=function(){var e=this;e.entityCache=e.configuration.collectionName?[]:{},e.__entityCacheRaw=null,e.__requestCache={}},r}function t(e){return e}angular.module("absync").constant("absyncCache",e)}(),function(){"use strict";function e(){return t}function t(e,t,n,r,i,o,a,c,s,u){this.model=e,this.collectionUri=t,this.entityUri=n;var h=e.prototype.constructor.name.toLowerCase();this.collectionName=r||h+"s",this.entityName=i||h,this.deserialize=o||void 0,this.serialize=a||void 0,this.injector=c||void 0,this.debug=s||!1,this.allowBrowserCache=angular.merge({},{sync:!0,request:!0},u)}angular.module("absync").service("AbsyncServiceConfiguration",e)}(),function(){"use strict";angular.module("absync").constant("absyncNoopLog",{debug:angular.noop,info:angular.noop,error:angular.noop})}(),function(){"use strict";function e(){function e(e){if(!e)return e;var t=-1<e.indexOf("?")?"&":"?",n=(new Date).getTime();return e+t+"t="+n}return e}angular.module("absync").filter("absyncUncached",e)}(); | ||
!function(){"use strict";angular.module("absync",[])}(),function(){"use strict";function e(e,n,r){return new t(e,n,r)}function t(e,t,n){var r=this;r.__injector=e,r.__provide=t,r.__absyncCache=n,r.__ioSocket=null,r.__registerLater=[],r.__listeners=[],r.__collections={},r.__entities={},r.debug=void 0}e.$inject=["$injector","$provide","absyncCache"],angular.module("absync").provider("absync",e),t.prototype.configure=function(e){var t=this;if("undefined"!=typeof e.socket){var n=e.socket,r="undefined"!=typeof io&&io.Socket&&n instanceof io.Socket;if("function"==typeof n)t.__ioSocket=new n;else{if(!r)throw new Error("configure() expects input to be a function or a socket.io Socket instance.");t.__ioSocket=n}t.__registerLater.length&&(angular.forEach(t.__registerLater,t.__registerListener.bind(t)),t.__registerLater=[])}"undefined"!=typeof e.debug&&(t.debug=e.debug||!1),t.debug&&(angular.forEach(t.__collections,function(e){e.configuration.debug=!0}),angular.forEach(t.__entities,function(e){e.configuration.debug=!0}))},t.prototype.disconnect=function(e){var t=this;e=e||!1,angular.forEach(t.__listeners,function(e){e.unregister(),delete e.unregister,t.__registerLater.push(e)}),t.__listeners=[],e&&(t.__ioSocket.disconnect(),t.__ioSocket=null)},t.prototype.__registerListener=function(e){var t=this;t.__listeners.push(e),e.unregister=t.__handleEntityEvent(e.eventName,e.callback)},t.prototype.collection=function(e,t){var n=this;if(n.__collections[e])throw new Error("A collection with the name '"+e+"' was already requested. Names for collections must be unique.");if(n.__entities[e])throw new Error("An entity with the name '"+e+"' was already requested. Names for collections must be unique and can't be shared with entities.");t.debug="undefined"==typeof t.debug?n.debug:t.debug,n.__collections[e]={constructor:n.__absyncCache(e,t),configuration:t},n.__provide.service(e,n.__collections[e].constructor)},t.prototype.entity=function(e,t){var n=this;if(n.__entities[e])throw new Error("An entity with the name '"+e+"' was already requested. Names for entities must be unique.");if(n.__collections[e])throw new Error("A collection with the name '"+e+"' was already requested. Names for entities must be unique and can't be shared with collections.");t.debug="undefined"==typeof t.debug?n.debug:t.debug,n.__entities[e]={constructor:n.__absyncCache(e,t),configuration:t},n.__provide.service(e,n.__entities[e].constructor)},t.prototype.on=function(e,t){var n=this;return n.__ioSocket?n.__registerListener({eventName:e,callback:t}):n.__registerLater.length>8192?null:(n.__registerLater.push({eventName:e,callback:t}),null)},t.prototype.__handleEntityEvent=function(e,t){var n=this;return n.__ioSocket.on(e,t),function(){n.__ioSocket.removeListener(e,t)}},t.prototype.emit=function(e,t,n){var r=this;if(!r.__ioSocket)throw new Error("socket.io is not initialized.");r.__ioSocket.emit(e,t,function(){n&&n.apply(r.__ioSocket,arguments)})},t.prototype.$get=function(){return this}}(),function(){"use strict";function e(e,n){function r(r,o,i,a,c,s,l,u){var h=this,d=n.injector||o,_=d.has(n.model);if(!_)throw new Error("Unable to construct the '"+e+"' service, because the referenced model '"+n.model+"' is not available for injection.");var y="string"==typeof n.model?d.get(n.model):n.model,f=y.serialize||n.serialize||t,p=y.deserialize||n.deserialize||t;h.name=e,h.configuration=n,h.entityCache=n.collectionName?[]:{},h.entityCache.__lookup={},h.__entityCacheRaw=null,h.enableRequestCache=!0,h.__requestCache={},h.allowBrowserCache=(angular.merge||angular.extend)({},{sync:!0,request:!0},n.allowBrowserCache),h.__uncached=u,h.httpInterface=r,h.logInterface=n.debug?i:s,h.scope=c,h.q=a,h.logPrefix="absync:"+e.toLocaleUpperCase()+" ",h.forceEarlyCacheUpdate=!1,h.throwFailures=!0,h.serializer=f,h.deserializer=p,l.on(n.entityName,h.__onEntityOnWebsocket.bind(h)),n.collectionName&&l.on(n.collectionName,h.__onCollectionOnWebsocket.bind(h)),c.$on(n.entityName,h.__onEntityReceived.bind(h)),n.collectionName&&c.$on(n.collectionName,h.__onCollectionReceived.bind(h)),h.logInterface.info(h.logPrefix+"service was instantiated.")}function o(e,t){var r=this;if((e||r.forceEarlyCacheUpdate)&&t.data[n.entityName]){var o=t.data[n.entityName];if(r.forceEarlyCacheUpdate){var i=r.deserializer(o);if(r.__updateCacheWithEntity(i),e)return i}if(e)return o}}function i(e){var t=this;if(t.logInterface.error(t.logPrefix+"Unable to store entity on the server.",e),t.logInterface.error(e),t.scope.$emit("absyncError",e),t.throwFailures)throw e}return r.$inject=["$http","$injector","$log","$q","$rootScope","absyncNoopLog","absync","absyncUncachedFilter"],r.prototype.__onEntityOnWebsocket=function(e){var t=this;t.scope.$broadcast(n.entityName,e[n.entityName])},r.prototype.__onCollectionOnWebsocket=function(e){var t=this;t.scope.$broadcast(n.collectionName,e[n.collectionName])},r.prototype.__onDataAvailable=function(e){function t(e){var t=r.deserializer(e);r.entityCache.push(t),r.entityCache.__lookup&&(r.entityCache.__lookup[t.id]=r.entityCache.length-1)}var r=this;if(Array.isArray(r.entityCache))angular.forEach(e[n.collectionName],t),r.scope.$broadcast("collectionNew",{service:r,cache:r.entityCache});else{var o=r.deserializer(e[n.entityName]);r.__updateCacheWithEntity(o)}return r.entityCache},r.prototype.__onEntityReceived=function(e,t){var r=this,o=t;return r.__entityCacheRaw&&r.__entityCacheRaw[n.collectionName||n.entityName]?1===Object.keys(o).length&&o.hasOwnProperty("id")?(r.logInterface.info(r.logPrefix+"Entity was deleted from the server. Updating cache…"),r.__cacheMaintain(r.__entityCacheRaw[n.collectionName||n.entityName],o,"delete",!1),r.__removeEntityFromCache(o.id)):(r.logInterface.debug(r.logPrefix+"Entity was updated on the server. Updating cache…"),r.__cacheMaintain(r.__entityCacheRaw[n.collectionName||n.entityName],o,"update",!1),r.__updateCacheWithEntity(r.deserializer(o))):r.read(o.id)},r.prototype.__onCollectionReceived=function(e,t){function n(e){var t=r.deserializer(e);r.__updateCacheWithEntity(t)}var r=this,o=t;r.entityCache.length=0,angular.forEach(o,n)},r.prototype.ensureLoaded=function(e){function t(e){if(!e.data[n.collectionName])throw new Error("The response from the server was not in the expected format. It should have a member named '"+n.collectionName+"'.");return a.__entityCacheRaw=e.data,a.entityCache.splice(0,a.entityCache.length),a.__onDataAvailable(e.data)}function r(e){if(a.logInterface.error(a.logPrefix+"Unable to retrieve the collection from the server.",e),a.__entityCacheRaw=null,a.scope.$emit("absyncError",e),a.throwFailures)throw e}function o(e){if(!e.data[n.entityName])throw new Error("The response from the server was not in the expected format. It should have a member named '"+n.entityName+"'.");a.__entityCacheRaw=e.data,a.__onDataAvailable(e.data)}function i(e){if(a.logInterface.error(a.logPrefix+"Unable to retrieve the entity from the server.",e),a.__entityCacheRaw=null,a.scope.$emit("absyncError",e),a.throwFailures)throw e}var a=this;return e=e===!0,null===a.__entityCacheRaw||e?n.collectionName&&n.collectionUri?(a.logInterface.info(a.logPrefix+"Retrieving '"+n.collectionName+"' collection…"),a.httpInterface.get(a.allowBrowserCache.sync?n.collectionUri:a.__uncached(n.collectionUri)).then(t,r)):n.entityName&&n.entityUri?a.httpInterface.get(a.allowBrowserCache.sync?n.entityUri:a.__uncached(n.entityUri)).then(o,i):a.q.when([]):a.q.when(a.entityCache)},r.prototype.seed=function(e){var t=this;return t.__entityCacheRaw=e,t.__onDataAvailable(t.__entityCacheRaw)},r.prototype.sync=function(){var e=this;return e.__entityCacheRaw=null,e.ensureLoaded(!0)},r.prototype.read=function(e,t){function r(e){if(!e.data[n.entityName])throw new Error("The response from the server was not in the expected format. It should have a member named '"+n.entityName+"'.");var t=e.data[n.entityName];i.__cacheMaintain(i.__entityCacheRaw[n.collectionName||n.entityName],t,"update",!1);var r=i.deserializer(t);return i.__updateCacheWithEntity(r),r}function o(t){if(i.logInterface.error(i.logPrefix+"Unable to retrieve entity with ID '"+e+"' from the server.",t),i.scope.$emit("absyncError",t),i.throwFailures)throw t}var i=this;if(t=t===!0,i.logInterface.debug(i.logPrefix+"Requesting entity '"+e+"' (forceReload:"+t+")…"),!t){i.entityCache.__lookup&&(a=i.entityCache.__lookup[e]||i.entityCache.length);for(var a=0,c=i.entityCache[0];a<i.entityCache.length;++a,c=i.entityCache[a])if(c.id===e)return i.logInterface.debug(i.logPrefix+"Requested entity '"+e+"' is served from cache."),i.q.when(c)}return i.logInterface.debug(i.logPrefix+"Requested entity '"+e+"' is fetched from backend."),i.__requestEntity(e).then(r,o)},r.prototype.__requestEntity=function(e){function t(e,t){return delete r.__requestCache[e],t}var r=this;if(r.enableRequestCache&&r.__requestCache&&r.__requestCache[e])return r.logInterface.debug(r.logPrefix+"Entity request '"+e+"' served from request cache."),r.__requestCache[e];r.__entityCacheRaw=r.__entityCacheRaw||{},r.__entityCacheRaw[n.collectionName]=r.__entityCacheRaw[n.collectionName]||[];var o=n.entityUri+(e?"/"+e:""),i=r.httpInterface.get(r.allowBrowserCache.request?o:r.__uncached(o)).then(t.bind(r,e));return r.enableRequestCache&&r.__requestCache&&(r.__requestCache[e]=i),i},r.prototype.update=function(e,t){var r=this;t=t||!1;var a=r.reduceComplex(e),c=r.serializer(a),s={};return s[n.entityName]=c,"undefined"!=typeof e.id?r.httpInterface.put(n.entityUri+"/"+e.id,s).then(o.bind(r,t),i.bind(r)):r.httpInterface.post(n.collectionUri,s).then(o.bind(r,t),i.bind(r))},r.prototype.patch=function(e){var t=this,r=t.reduceComplex(e),a=t.serializer(r),c={};if(c[n.entityName]=a,"undefined"!=typeof e.id)return t.httpInterface.patch(n.entityUri+"/"+e.id,c).then(o.bind(t),i.bind(t));throw new Error("Attempted to patch an entity that was never stored on the server.")},r.prototype.create=r.prototype.update,r.prototype["delete"]=function(e){function t(t){return o.__cacheMaintain(o.__entityCacheRaw[n.collectionName||n.entityName],e,"delete",!1),o.__removeEntityFromCache(i)}function r(e){if(o.logInterface.error(e.data),o.scope.$emit("absyncError",e),o.throwFailures)throw e}var o=this,i=e.id;return o.httpInterface["delete"](n.entityUri+"/"+i).then(t)["catch"](r)},r.prototype.__updateCacheWithEntity=function(e){var t=this;return t.logInterface.info(t.logPrefix+"Updating entity '"+(e.id||t.name)+"' in cache…",e),t.__cacheMaintain(t.entityCache,e,"update",!0)},r.prototype.__cacheMaintain=function(e,t,n,r){var o=this,i=0,a=e[i];switch(e.__lookup&&(i=e.__lookup[t.id]||e.length),n){case"update":if(!Array.isArray(e))return r&&o.scope.$broadcast("beforeEntityUpdated",{service:o,cache:e,entity:e,updated:t}),"function"==typeof e.copyFrom?e.copyFrom(t):angular.extend(e,t),void o.scope.$broadcast("entityUpdated",{service:o,cache:e,entity:e});var c=!1;for(angular.noop;i<e.length;++i,a=e[i])if(a.id===t.id){r&&o.scope.$broadcast("beforeEntityUpdated",{service:o,cache:e,entity:e[i],updated:t});var s=e[i];"function"==typeof s.copyFrom?s.copyFrom(t):angular.extend(s,t),c=!0,r&&o.scope.$broadcast("entityUpdated",{service:o,cache:e,entity:e[i]});break}c||(r&&o.scope.$broadcast("beforeEntityNew",{service:o,cache:e,entity:t}),e.push(t),e.__lookup&&(e.__lookup[t.id]=e.length-1),r&&o.scope.$broadcast("entityNew",{service:o,cache:e,entity:t}));break;case"delete":for(angular.noop;i<e.length;++i,a=e[i])if(a.id===t.id){if(r&&o.scope.$broadcast("beforeEntityRemoved",{service:o,cache:e,entity:a}),e.splice(i,1),e.__lookup)for(var l in e.__lookup)i<=e.__lookup[l]&&--e.__lookup[l];r&&o.scope.$broadcast("entityRemoved",{service:o,cache:e,entity:a});break}}},r.prototype.__removeEntityFromCache=function(e){var t=this;return t.__cacheMaintain(t.entityCache,{id:e},"delete",!0)},r.prototype.lookupTableById=function(){var e=this;if(e.entityCache.__lookup)return angular.copy(e.entityCache.__lookup);for(var t=[],n=0;n<e.entityCache.length;++n)t[e.entityCache[n].id]=e.entityCache[n];return t},r.prototype.reduceComplex=function(e,t){var n=this,r=t?[]:{};for(var o in e)e.hasOwnProperty(o)&&(Array.isArray(e[o])?r[o]=n.reduceComplex(e[o],!0):e[o]&&e[o].id?r[o]=e[o].id:r[o]=e[o]);return r},r.prototype.populateComplex=function(e,t,n,r){function o(o,i){function s(n){return e[t][i]=n,r.crossLink&&a(n,e),e}if("string"!=typeof e[t][i]){if(!r.force||"object"!=typeof e[t][i]||"string"!=typeof e[t][i].id){if(c.throwFailures)throw new Error("The referenced entity did not have an 'id' property that would be expected.");return c.q.when(!1)}e[t][i]=e[t][i].id}return n.read(e[t][i]).then(s)}function i(n){return e[t]=n,r.crossLink&&a(n,e),e}function a(e,t){if(Array.isArray(e[r.crossLinkProperty])){var n=e[r.crossLinkProperty].indexOf(t);if(n>-1)return;var o=e[r.crossLinkProperty].indexOf(t.id);return o>-1?void(e[r.crossLinkProperty][o]=t):void e[r.crossLinkProperty].push(t)}e[r.crossLinkProperty]=t}var c=this;if(r=r||{},"boolean"==typeof r&&(c.logInterface.warn("Argument 'force' is deprecated. Provide an options hash instead."),r={force:r}),r.force=r.force||!1,r.crossLink=r.crossLink||!1,r.crossLinkProperty=r.crossLinkProperty||"",r.crossLink&&!r.crossLinkProperty&&(c.logInterface.warn("Option 'crossLink' given without 'crossLinkProperty'. Cross-linking will be disabled."),r.crossLink=!1),Array.isArray(e[t])){var s=e[t].map(o);return c.q.all(s)}if("string"!=typeof e[t]){if(!r.force||"object"!=typeof e[t]||"string"!=typeof e[t].id){if(c.throwFailures)throw new Error("The referenced entity did not have an 'id' property that would be expected.");return c.q.when(!1)}e[t]=e[t].id}return n.read(e[t]).then(i)},r.prototype.reset=function(){var e=this;e.entityCache=e.configuration.collectionName?[]:{},e.entityCache.__lookup=e.entityCache.__lookup||{},e.__entityCacheRaw=null,e.__requestCache={}},r}function t(e){return e}angular.module("absync").constant("absyncCache",e)}(),function(){"use strict";function e(){return t}function t(e,t,n,r,o,i,a,c,s,l){this.model=e,this.collectionUri=t,this.entityUri=n;var u=e.prototype.constructor.name.toLowerCase();this.collectionName=r||u+"s",this.entityName=o||u,this.deserialize=i||void 0,this.serialize=a||void 0,this.injector=c||void 0,this.debug=s||!1,this.allowBrowserCache=angular.merge({},{sync:!0,request:!0},l)}angular.module("absync").service("AbsyncServiceConfiguration",e)}(),function(){"use strict";angular.module("absync").constant("absyncNoopLog",{debug:angular.noop,info:angular.noop,warn:angular.noop,error:angular.noop})}(),function(){"use strict";function e(){function e(e){if(!e)return e;var t=-1<e.indexOf("?")?"&":"?",n=(new Date).getTime();return e+t+"t="+n}return e}angular.module("absync").filter("absyncUncached",e)}(); | ||
//# sourceMappingURL=maps/absync.concat.min.js.map |
{ | ||
"name": "absync", | ||
"version": "3.2.1", | ||
"version": "3.3.0", | ||
"description": "absync", | ||
@@ -5,0 +5,0 @@ "main": "dist/development/absync.concat.js", |
@@ -64,5 +64,7 @@ /* globals angular */ | ||
/* @type {Array<configuration.model>|configuration.model} */ | ||
self.entityCache = configuration.collectionName ? [] : {}; | ||
self.entityCache = configuration.collectionName ? [] : {}; | ||
// Create the ID -> entityIndex lookup table. | ||
self.entityCache.__lookup = {}; | ||
// The raw cache is data that hasn't been deserialized and is used internally. | ||
self.__entityCacheRaw = null; | ||
self.__entityCacheRaw = null; | ||
@@ -178,3 +180,7 @@ // Should request caching be used at all? | ||
function deserializeCollectionEntry( rawEntity ) { | ||
self.entityCache.push( self.deserializer( rawEntity ) ); | ||
var entityToCache = self.deserializer( rawEntity ); | ||
self.entityCache.push( entityToCache ); | ||
if( self.entityCache.__lookup ) { | ||
self.entityCache.__lookup[ entityToCache.id ] = self.entityCache.length - 1; | ||
} | ||
} | ||
@@ -193,2 +199,12 @@ }; | ||
// Check if our raw entity cache was even initialized. | ||
// It's possible that it isn't, because websocket updates can be received before any manual requests | ||
// were made to the backend. | ||
if( !self.__entityCacheRaw || !self.__entityCacheRaw[ configuration.collectionName || configuration.entityName ] ) { | ||
// We ignore this update and just stack a new read request on top of any existing ones. | ||
// This makes sure that we load the freshest entity in an orderly fashion and lose the state we received | ||
// here, as we're getting the latest version of the entity. | ||
return self.read( _entityReceived.id ); | ||
} | ||
// Determine if the received record consists ONLY of an id property, | ||
@@ -198,2 +214,8 @@ // which would mean that this record was deleted from the backend. | ||
self.logInterface.info( self.logPrefix + "Entity was deleted from the server. Updating cache…" ); | ||
self.__cacheMaintain( self.__entityCacheRaw[ configuration.collectionName || configuration.entityName ], | ||
_entityReceived, | ||
"delete", | ||
false ); | ||
return self.__removeEntityFromCache( _entityReceived.id ); | ||
@@ -203,2 +225,8 @@ | ||
self.logInterface.debug( self.logPrefix + "Entity was updated on the server. Updating cache…" ); | ||
self.__cacheMaintain( self.__entityCacheRaw[ configuration.collectionName || configuration.entityName ], | ||
_entityReceived, | ||
"update", | ||
false ); | ||
return self.__updateCacheWithEntity( self.deserializer( _entityReceived ) ); | ||
@@ -365,2 +393,6 @@ } | ||
// Check if the entity is in the cache and return instantly if found. | ||
if( self.entityCache.__lookup ) { | ||
entityIndex = self.entityCache.__lookup[ id ] || self.entityCache.length; | ||
} | ||
for( var entityIndex = 0, entity = self.entityCache[ 0 ]; | ||
@@ -390,7 +422,16 @@ entityIndex < self.entityCache.length; | ||
var rawEntity = serverResponse.data[ configuration.entityName ]; | ||
// Put the raw entity into our raw entity cache. | ||
// We keep the raw copy to allow caching of the raw data. | ||
self.__cacheMaintain( self.__entityCacheRaw[ configuration.collectionName || configuration.entityName ], | ||
rawEntity, | ||
"update", | ||
false ); | ||
// Deserialize the object and place it into the cache. | ||
// We do not need to check here if the object already exists in the cache. | ||
// While it could be possible that the same entity is retrieved multiple times, __updateCacheWithEntity | ||
// will not insert duplicated into the cache. | ||
var deserialized = self.deserializer( serverResponse.data[ configuration.entityName ] ); | ||
// will not insert duplicates into the cache. | ||
var deserialized = self.deserializer( rawEntity ); | ||
self.__updateCacheWithEntity( deserialized ); | ||
@@ -429,2 +470,6 @@ return deserialized; | ||
// Make sure our raw entity cache exists. | ||
self.__entityCacheRaw = self.__entityCacheRaw || {}; | ||
self.__entityCacheRaw[ configuration.collectionName ] = self.__entityCacheRaw[ configuration.collectionName ] || []; | ||
var requestUri = configuration.entityUri + ( id ? ( "/" + id ) : "" ); | ||
@@ -574,6 +619,11 @@ | ||
/** | ||
* Invoked when the entity was deleted from the server. | ||
* Invoked when the entity was successfully deleted from the server. | ||
* @param {angular.IHttpPromiseCallbackArg|Object} serverResponse The reply sent from the server. | ||
*/ | ||
function onEntityDeleted( serverResponse ) { | ||
self.__cacheMaintain( self.__entityCacheRaw[ configuration.collectionName || configuration.entityName ], | ||
entity, | ||
"delete", | ||
false ); | ||
return self.__removeEntityFromCache( entityId ); | ||
@@ -607,80 +657,153 @@ } | ||
if( !Array.isArray( self.entityCache ) ) { | ||
// Allow the user to intervene in the update process, before updating the entity. | ||
self.scope.$broadcast( "beforeEntityUpdated", | ||
{ | ||
service : self, | ||
cache : self.entityCache, | ||
entity : self.entityCache, | ||
updated : entityToCache | ||
} ); | ||
return self.__cacheMaintain( self.entityCache, entityToCache, "update", true ); | ||
}; | ||
if( typeof self.entityCache.copyFrom === "function" ) { | ||
self.entityCache.copyFrom( entityToCache ); | ||
/** | ||
* Perform maintenance operations on a cache. | ||
* @param cache The cache to operate on. | ||
* @param entityToCache The entity that the operation is relating to. | ||
* @param {String} operation The operation to perform. | ||
* @param {Boolean} [emit=false] Should appropriate absync events be broadcast to notify other actors? | ||
* @private | ||
*/ | ||
CacheService.prototype.__cacheMaintain = function CacheService$cacheMaintain( cache, entityToCache, operation, emit ) { | ||
var self = this; | ||
} else { | ||
angular.extend( self.entityCache, entityToCache ); | ||
} | ||
var entityIndex = 0; | ||
var entity = cache[ entityIndex ]; | ||
// After updating the entity, send another event to allow the user to react. | ||
self.scope.$broadcast( "entityUpdated", | ||
{ | ||
service : self, | ||
cache : self.entityCache, | ||
entity : self.entityCache | ||
} ); | ||
return; | ||
if( cache.__lookup ) { | ||
entityIndex = cache.__lookup[ entityToCache.id ] || cache.length; | ||
} | ||
var found = false; | ||
for( var entityIndex = 0, entity = self.entityCache[ 0 ]; | ||
entityIndex < self.entityCache.length; | ||
++entityIndex, entity = self.entityCache[ entityIndex ] ) { | ||
if( entity.id == entityToCache.id ) { | ||
// Allow the user to intervene in the update process, before updating the entity. | ||
self.scope.$broadcast( "beforeEntityUpdated", | ||
{ | ||
service : self, | ||
cache : self.entityCache, | ||
entity : self.entityCache[ entityIndex ], | ||
updated : entityToCache | ||
} ); | ||
switch( operation ) { | ||
case "update": | ||
if( !Array.isArray( cache ) ) { | ||
if( emit ) { | ||
// Allow the user to intervene in the update process, before updating the entity. | ||
self.scope.$broadcast( "beforeEntityUpdated", | ||
{ | ||
service : self, | ||
cache : cache, | ||
entity : cache, | ||
updated : entityToCache | ||
} ); | ||
} | ||
// Use the "copyFrom" method on the entity, if it exists, otherwise use naive approach. | ||
var targetEntity = self.entityCache[ entityIndex ]; | ||
if( typeof targetEntity.copyFrom === "function" ) { | ||
targetEntity.copyFrom( entityToCache ); | ||
if( typeof cache.copyFrom === "function" ) { | ||
cache.copyFrom( entityToCache ); | ||
} else { | ||
angular.extend( targetEntity, entityToCache ); | ||
} else { | ||
angular.extend( cache, entityToCache ); | ||
} | ||
// After updating the entity, send another event to allow the user to react. | ||
self.scope.$broadcast( "entityUpdated", | ||
{ | ||
service : self, | ||
cache : cache, | ||
entity : cache | ||
} ); | ||
return; | ||
} | ||
found = true; | ||
var found = false; | ||
for( angular.noop; entityIndex < cache.length; ++entityIndex, entity = cache[ entityIndex ] ) { | ||
if( entity.id === entityToCache.id ) { | ||
if( emit ) { | ||
// Allow the user to intervene in the update process, before updating the entity. | ||
self.scope.$broadcast( "beforeEntityUpdated", | ||
{ | ||
service : self, | ||
cache : cache, | ||
entity : cache[ entityIndex ], | ||
updated : entityToCache | ||
} ); | ||
} | ||
// After updating the entity, send another event to allow the user to react. | ||
self.scope.$broadcast( "entityUpdated", | ||
{ | ||
service : self, | ||
cache : self.entityCache, | ||
entity : self.entityCache[ entityIndex ] | ||
} ); | ||
// Use the "copyFrom" method on the entity, if it exists, otherwise use naive approach. | ||
var targetEntity = cache[ entityIndex ]; | ||
if( typeof targetEntity.copyFrom === "function" ) { | ||
targetEntity.copyFrom( entityToCache ); | ||
} else { | ||
angular.extend( targetEntity, entityToCache ); | ||
} | ||
found = true; | ||
if( emit ) { | ||
// After updating the entity, send another event to allow the user to react. | ||
self.scope.$broadcast( "entityUpdated", | ||
{ | ||
service : self, | ||
cache : cache, | ||
entity : cache[ entityIndex ] | ||
} ); | ||
} | ||
break; | ||
} | ||
} | ||
// If the entity wasn't found in our records, it's a new entity. | ||
if( !found ) { | ||
if( emit ) { | ||
self.scope.$broadcast( "beforeEntityNew", { | ||
service : self, | ||
cache : cache, | ||
entity : entityToCache | ||
} ); | ||
} | ||
cache.push( entityToCache ); | ||
if( cache.__lookup ) { | ||
cache.__lookup[ entityToCache.id ] = cache.length - 1; | ||
} | ||
if( emit ) { | ||
self.scope.$broadcast( "entityNew", { | ||
service : self, | ||
cache : cache, | ||
entity : entityToCache | ||
} ); | ||
} | ||
} | ||
break; | ||
} | ||
} | ||
// If the entity wasn't found in our records, it's a new entity. | ||
if( !found ) { | ||
self.scope.$broadcast( "beforeEntityNew", { | ||
service : self, | ||
cache : self.entityCache, | ||
entity : entityToCache | ||
} ); | ||
case "delete": | ||
// The "delete" operation is not expected to happen for single cached entities. | ||
for( angular.noop; entityIndex < cache.length; ++entityIndex, entity = cache[ entityIndex ] ) { | ||
if( entity.id === entityToCache.id ) { | ||
if( emit ) { | ||
// Before removing the entity, allow the user to react. | ||
self.scope.$broadcast( "beforeEntityRemoved", { | ||
service : self, | ||
cache : cache, | ||
entity : entity | ||
} ); | ||
} | ||
self.entityCache.push( entityToCache ); | ||
// Remove the entity from the cache. | ||
cache.splice( entityIndex, 1 ); | ||
self.scope.$broadcast( "entityNew", { | ||
service : self, | ||
cache : self.entityCache, | ||
entity : entityToCache | ||
} ); | ||
if( cache.__lookup ) { | ||
for( var cacheEntry in cache.__lookup ) { | ||
if( entityIndex <= cache.__lookup[ cacheEntry ] ) { | ||
--cache.__lookup[ cacheEntry ]; | ||
} | ||
} | ||
} | ||
if( emit ) { | ||
// Send another event to allow the user to take note of the removal. | ||
self.scope.$broadcast( "entityRemoved", { | ||
service : self, | ||
cache : cache, | ||
entity : entity | ||
} ); | ||
} | ||
break; | ||
} | ||
} | ||
break; | ||
} | ||
@@ -697,25 +820,5 @@ }; | ||
for( var entityIndex = 0, entity = self.entityCache[ 0 ]; | ||
entityIndex < self.entityCache.length; | ||
++entityIndex, entity = self.entityCache[ entityIndex ] ) { | ||
if( entity.id == id ) { | ||
// Before removing the entity, allow the user to react. | ||
self.scope.$broadcast( "beforeEntityRemoved", { | ||
service : self, | ||
cache : self.entityCache, | ||
entity : entity | ||
} ); | ||
// Remove the entity from the cache. | ||
self.entityCache.splice( entityIndex, 1 ); | ||
// Send another event to allow the user to take note of the removal. | ||
self.scope.$broadcast( "entityRemoved", { | ||
service : self, | ||
cache : self.entityCache, | ||
entity : entity | ||
} ); | ||
break; | ||
} | ||
} | ||
return self.__cacheMaintain( self.entityCache, { | ||
id : id | ||
}, "delete", true ); | ||
}; | ||
@@ -731,3 +834,6 @@ | ||
// TODO: Keep a copy of the lookup table and only update it when the cached data updates | ||
if( self.entityCache.__lookup ) { | ||
return angular.copy( self.entityCache.__lookup ); | ||
} | ||
var lookupTable = []; | ||
@@ -782,9 +888,31 @@ for( var entityIndex = 0; | ||
* type instances which are being referenced in entity. | ||
* @param {Boolean} [force=false] If true, all complex types will be replaced with references to the | ||
* @param {Object|Boolean} [options] A hash with options relating to the population process. | ||
* @param {Boolean} [options.force=false] If true, all complex types will be replaced with references to the | ||
* instances in cache; otherwise, only properties that are string representations of complex type IDs will be replaced. | ||
* @param {Boolean} [options.crossLink=false] If true, the entity will also be put into a relating property in the | ||
* foreign entity. | ||
* @param {String} [options.crossLinkProperty] The name of the property in the foreign type into which the entity | ||
* should be cross-linked. | ||
* @returns {IPromise<TResult>|IPromise<any[]>|IPromise<{}>|angular.IPromise<TResult>} | ||
*/ | ||
CacheService.prototype.populateComplex = function CacheService$populateComplex( entity, propertyName, cache, force ) { | ||
CacheService.prototype.populateComplex = function CacheService$populateComplex( entity, propertyName, cache, options ) { | ||
var self = this; | ||
options = options || {}; | ||
if( typeof options === "boolean" ) { | ||
self.logInterface.warn( "Argument 'force' is deprecated. Provide an options hash instead." ); | ||
options = { | ||
force : options | ||
}; | ||
} | ||
options.force = options.force || false; | ||
options.crossLink = options.crossLink || false; | ||
options.crossLinkProperty = options.crossLinkProperty || ""; | ||
if( options.crossLink && !options.crossLinkProperty ) { | ||
self.logInterface.warn( | ||
"Option 'crossLink' given without 'crossLinkProperty'. Cross-linking will be disabled." ); | ||
options.crossLink = false; | ||
} | ||
// If the target property is an array, ... | ||
@@ -801,3 +929,3 @@ if( Array.isArray( entity[ propertyName ] ) ) { | ||
// If "force" is enabled, we check if this non-string property is an object and has an "id" member, which is a string. | ||
if( force && typeof entity[ propertyName ] === "object" && typeof entity[ propertyName ].id === "string" ) { | ||
if( options.force && typeof entity[ propertyName ] === "object" && typeof entity[ propertyName ].id === "string" ) { | ||
// If that is true, then we replace the whole object with the ID and continue as usual. | ||
@@ -807,2 +935,5 @@ entity[ propertyName ] = entity[ propertyName ].id; | ||
} else { | ||
if( self.throwFailures ) { | ||
throw new Error( "The referenced entity did not have an 'id' property that would be expected." ); | ||
} | ||
return self.q.when( false ); | ||
@@ -821,3 +952,3 @@ } | ||
// If "force" is enabled, we check if this non-string property is an object and has an "id" member, which is a string. | ||
if( force && typeof entity[ propertyName ][ index ] === "object" && typeof entity[ propertyName ][ index ].id === "string" ) { | ||
if( options.force && typeof entity[ propertyName ][ index ] === "object" && typeof entity[ propertyName ][ index ].id === "string" ) { | ||
// If that is true, then we replace the whole object with the ID and continue as usual. | ||
@@ -827,2 +958,6 @@ entity[ propertyName ][ index ] = entity[ propertyName ][ index ].id; | ||
} else { | ||
if( self.throwFailures ) { | ||
throw new Error( "The referenced entity did not have an 'id' property that would be expected." ); | ||
} | ||
return self.q.when( false ); | ||
@@ -839,2 +974,7 @@ } | ||
entity[ propertyName ][ index ] = complex; | ||
if( options.crossLink ) { | ||
crossLink( complex, entity ); | ||
} | ||
return entity; | ||
@@ -847,3 +987,34 @@ } | ||
entity[ propertyName ] = complex; | ||
if( options.crossLink ) { | ||
crossLink( complex, entity ); | ||
} | ||
return entity; | ||
} | ||
function crossLink( complex, entity ) { | ||
// If cross-linking is enabled, put our entity into the foreign complex. | ||
if( Array.isArray( complex[ options.crossLinkProperty ] ) ) { | ||
// Check if the entity is already linked into the array. | ||
var entityIndex = complex[ options.crossLinkProperty ].indexOf( entity ); | ||
if( -1 < entityIndex ) { | ||
return; | ||
} | ||
// Check if the ID exists in the array. | ||
var idIndex = complex[ options.crossLinkProperty ].indexOf( entity.id ); | ||
if( -1 < idIndex ) { | ||
// Replace the ID with the entity. | ||
complex[ options.crossLinkProperty ][ idIndex ] = entity; | ||
return; | ||
} | ||
// Just push the element into the array. | ||
complex[ options.crossLinkProperty ].push( entity ); | ||
return; | ||
} | ||
complex[ options.crossLinkProperty ] = entity; | ||
} | ||
}; | ||
@@ -859,3 +1030,5 @@ | ||
self.entityCache = self.configuration.collectionName ? [] : {}; | ||
self.entityCache = self.configuration.collectionName ? [] : {}; | ||
self.entityCache.__lookup = self.entityCache.__lookup || {}; | ||
self.__entityCacheRaw = null; | ||
@@ -862,0 +1035,0 @@ self.__requestCache = {}; |
@@ -8,3 +8,4 @@ /* globals angular */ | ||
info : angular.noop, | ||
warn : angular.noop, | ||
error : angular.noop | ||
} ); |
@@ -7,3 +7,5 @@ // jscs:disable requireNamedUnassignedFunctions | ||
var $rootScope; | ||
var devices; | ||
var users; | ||
@@ -26,7 +28,30 @@ beforeEach( function() { | ||
} ) | ||
.constant( "Device", {} ); | ||
.config( function( _absyncProvider_ ) { | ||
var serviceDefinition = { | ||
model : "User", | ||
collectionName : "users", | ||
collectionUri : "/api/users", | ||
entityName : "user", | ||
entityUri : "/api/user", | ||
debug : true | ||
}; | ||
_absyncProvider_.configure( SockMock ); | ||
_absyncProvider_.collection( "users", serviceDefinition ); | ||
} ) | ||
.constant( "Device", { | ||
deserialize : function( data ) { | ||
return angular.copy( data ); | ||
} | ||
} ) | ||
.constant( "User", { | ||
deserialize : function( data ) { | ||
return angular.copy( data ); | ||
} | ||
} ); | ||
module( "absync", "test" ); | ||
} ); | ||
// Set up the devices API | ||
beforeEach( inject( function( _$httpBackend_, _$rootScope_ ) { | ||
@@ -40,4 +65,5 @@ $httpBackend = _$httpBackend_; | ||
devices : [ { | ||
id : 1, | ||
name : "My Device" | ||
id : "1", | ||
name : "My Device", | ||
owner : "1" | ||
} ] | ||
@@ -50,11 +76,64 @@ } ); | ||
device : { | ||
id : 1, | ||
name : "My Device" | ||
id : "1", | ||
name : "My Device", | ||
owner : "1" | ||
} | ||
} ); | ||
// This device is not served with the collection GET. | ||
$httpBackend | ||
.when( "GET", "/api/device/2" ) | ||
.respond( { | ||
device : { | ||
id : "2", | ||
name : "Another Device", | ||
owner : "2" | ||
} | ||
} ); | ||
$httpBackend | ||
.when( "DELETE", "/api/device/1" ) | ||
.respond( 200 ); | ||
} ) ); | ||
beforeEach( inject( function( _devices_ ) { | ||
// Set up the users API | ||
beforeEach( inject( function( _$httpBackend_, _$rootScope_ ) { | ||
$httpBackend = _$httpBackend_; | ||
$rootScope = _$rootScope_; | ||
$httpBackend | ||
.when( "GET", "/api/users" ) | ||
.respond( { | ||
users : [ { | ||
id : "1", | ||
name : "John Doe" | ||
} ] | ||
} ); | ||
$httpBackend | ||
.when( "GET", "/api/user/1" ) | ||
.respond( { | ||
user : { | ||
id : "1", | ||
name : "John Doe" | ||
} | ||
} ); | ||
// This user is not served with the collection GET. | ||
$httpBackend | ||
.when( "GET", "/api/user/2" ) | ||
.respond( { | ||
user : { | ||
id : "2", | ||
name : "Jane Smith" | ||
} | ||
} ); | ||
} ) ); | ||
beforeEach( inject( function( _devices_, _users_ ) { | ||
devices = _devices_; | ||
devices.reset(); | ||
users = _users_; | ||
users.reset(); | ||
} ) ); | ||
@@ -72,87 +151,204 @@ | ||
it( "should cached the loaded collection", function() { | ||
devices.ensureLoaded(); | ||
$httpBackend.flush(); | ||
expect( devices.entityCache ).to.be.an( "array" ).with.length( 1 ); | ||
describe( "caching", function() { | ||
it( "should cached the loaded collection", function() { | ||
devices.ensureLoaded(); | ||
$httpBackend.flush(); | ||
expect( devices.entityCache ).to.be.an( "array" ).with.length( 1 ); | ||
var entity = devices.entityCache[ 0 ]; | ||
var entity = devices.entityCache[ 0 ]; | ||
devices.ensureLoaded(); | ||
expect( devices.entityCache[ 0 ] ).to.equal( entity ); | ||
} ); | ||
devices.ensureLoaded(); | ||
expect( devices.entityCache[ 0 ] ).to.equal( entity ); | ||
} ); | ||
it( "should forget cached collections when reset", function() { | ||
devices.ensureLoaded(); | ||
$httpBackend.flush(); | ||
expect( devices.entityCache ).to.be.an( "array" ).with.length( 1 ); | ||
it( "should forget cached collections when reset", function() { | ||
devices.ensureLoaded(); | ||
$httpBackend.flush(); | ||
expect( devices.entityCache ).to.be.an( "array" ).with.length( 1 ); | ||
var entity = devices.entityCache[ 0 ]; | ||
devices.reset(); | ||
var entity = devices.entityCache[ 0 ]; | ||
devices.reset(); | ||
devices.ensureLoaded(); | ||
expect( devices.entityCache[ 0 ] ).to.not.equal( entity ); | ||
} ); | ||
devices.ensureLoaded(); | ||
expect( devices.entityCache[ 0 ] ).to.not.equal( entity ); | ||
} ); | ||
it( "should provide an entity", function( done ) { | ||
devices.ensureLoaded(); | ||
$httpBackend.flush(); | ||
devices.read( 1 ) | ||
.then( function( device ) { | ||
expect( devices.entityCache ).to.be.an( "array" ).with.length( 1 ); | ||
expect( device ).to.be.an( "object" ).with.property( "name" ).that.equals( "My Device" ); | ||
} ) | ||
.then( done ) | ||
.catch( done ); | ||
$rootScope.$digest(); | ||
it( "should maintain the raw entity cache for reads", function( done ) { | ||
devices.ensureLoaded(); | ||
$httpBackend.flush(); | ||
expect( devices.__entityCacheRaw.devices ).to.be.an( "array" ).with.length( 1 ); | ||
expect( devices.entityCache ).to.be.an( "array" ).with.length( 1 ); | ||
devices.read( 2 ) | ||
.then( function( device ) { | ||
expect( devices.__entityCacheRaw.devices ).to.be.an( "array" ).with.length( 2 ); | ||
expect( devices.entityCache ).to.be.an( "array" ).with.length( 2 ); | ||
} ) | ||
.then( done ) | ||
.catch( done ); | ||
$httpBackend.flush(); | ||
$rootScope.$digest(); | ||
} ); | ||
it( "should maintain the raw entity cache for deletes", function( done ) { | ||
devices.ensureLoaded(); | ||
$httpBackend.flush(); | ||
expect( devices.__entityCacheRaw.devices ).to.be.an( "array" ).with.length( 1 ); | ||
expect( devices.entityCache ).to.be.an( "array" ).with.length( 1 ); | ||
devices.delete( { | ||
id : "1" | ||
} ) | ||
.then( function() { | ||
expect( devices.__entityCacheRaw.devices ).to.be.an( "array" ).with.length( 0 ); | ||
expect( devices.entityCache ).to.be.an( "array" ).with.length( 0 ); | ||
} ) | ||
.then( done ) | ||
.catch( done ); | ||
$httpBackend.flush(); | ||
$rootScope.$digest(); | ||
} ); | ||
it( "should maintain the raw entity cache for updates over socket", function() { | ||
devices.ensureLoaded(); | ||
$httpBackend.flush(); | ||
expect( devices.__entityCacheRaw.devices ).to.be.an( "array" ).with.length( 1 ); | ||
expect( devices.entityCache ).to.be.an( "array" ).with.length( 1 ); | ||
var updated = { | ||
id : "1", | ||
name : "My Updated Device", | ||
owner : "1" | ||
}; | ||
devices.__onEntityReceived( null, updated ); | ||
expect( devices.__entityCacheRaw.devices ).to.be.an( "array" ).with.length( 1 ); | ||
expect( devices.entityCache ).to.be.an( "array" ).with.length( 1 ); | ||
expect( devices.__entityCacheRaw.devices[ 0 ] ).to.eql( updated ); | ||
expect( devices.entityCache[ 0 ] ).to.eql( updated ); | ||
} ); | ||
it( "should maintain the raw entity cache for deletes over socket", function() { | ||
devices.ensureLoaded(); | ||
$httpBackend.flush(); | ||
expect( devices.__entityCacheRaw.devices ).to.be.an( "array" ).with.length( 1 ); | ||
expect( devices.entityCache ).to.be.an( "array" ).with.length( 1 ); | ||
var deleted = { | ||
id : "1" | ||
}; | ||
devices.__onEntityReceived( null, deleted ); | ||
expect( devices.__entityCacheRaw.devices ).to.be.an( "array" ).with.length( 0 ); | ||
expect( devices.entityCache ).to.be.an( "array" ).with.length( 0 ); | ||
} ); | ||
} ); | ||
it( "should provide an entity when collection is not loaded", function( done ) { | ||
devices.read( 1 ) | ||
.then( function( device ) { | ||
expect( devices.entityCache ).to.be.an( "array" ).with.length( 1 ); | ||
expect( device ).to.be.an( "object" ).with.property( "name" ).that.equals( "My Device" ); | ||
} ) | ||
.then( done ) | ||
.catch( done ); | ||
$httpBackend.flush(); | ||
describe( "reads", function() { | ||
it( "should provide an entity", function( done ) { | ||
devices.ensureLoaded(); | ||
$httpBackend.flush(); | ||
devices.read( "1" ) | ||
.then( function( device ) { | ||
expect( devices.entityCache ).to.be.an( "array" ).with.length( 1 ); | ||
expect( device ).to.be.an( "object" ).with.property( "name" ).that.equals( "My Device" ); | ||
} ) | ||
.then( done ) | ||
.catch( done ); | ||
$rootScope.$digest(); | ||
} ); | ||
it( "should provide an entity when collection is not loaded", function( done ) { | ||
devices.read( 1 ) | ||
.then( function( device ) { | ||
expect( devices.entityCache ).to.be.an( "array" ).with.length( 1 ); | ||
expect( device ).to.be.an( "object" ).with.property( "name" ).that.equals( "My Device" ); | ||
} ) | ||
.then( done ) | ||
.catch( done ); | ||
$httpBackend.flush(); | ||
} ); | ||
} ); | ||
it( "should provide seeded content", function( done ) { | ||
devices.seed( { | ||
describe( "seeding", function() { | ||
it( "should provide seeded content", function( done ) { | ||
devices.seed( { | ||
devices : [ { | ||
id : "1", | ||
name : "My Device" | ||
} ] | ||
} | ||
); | ||
devices.read( "1" ) | ||
.then( function( device ) { | ||
expect( device ).to.be.an( "object" ).with.property( "name" ).that.equals( "My Device" ); | ||
} ) | ||
.then( done ) | ||
.catch( done ); | ||
$rootScope.$digest(); | ||
} ); | ||
it( "should provide updated content when syncing after seeding", function( done ) { | ||
var seed = { | ||
devices : [ { | ||
id : 1, | ||
id : "1", | ||
name : "My Device" | ||
} ] | ||
} | ||
); | ||
}; | ||
devices.read( 1 ) | ||
.then( function( device ) { | ||
expect( device ).to.be.an( "object" ).with.property( "name" ).that.equals( "My Device" ); | ||
} ) | ||
.then( done ) | ||
.catch( done ); | ||
$rootScope.$digest(); | ||
devices.seed( seed ); | ||
devices.sync(); | ||
$httpBackend.flush(); | ||
devices.read( "1" ) | ||
.then( function( device ) { | ||
expect( device ).to.not.equal( seed.devices[ 0 ] ); | ||
} ) | ||
.then( done ) | ||
.catch( done ); | ||
$rootScope.$digest(); | ||
} ); | ||
} ); | ||
it( "should provide updated content when syncing after seeding", function( done ) { | ||
var seed = { | ||
devices : [ { | ||
id : 1, | ||
name : "My Device" | ||
} ] | ||
}; | ||
describe( "populate complex", function() { | ||
it( "should populate referenced complex types", function( done ) { | ||
devices.read( "1" ); | ||
$httpBackend.flush(); | ||
devices.seed( seed ); | ||
devices.populateComplex( devices.entityCache[ 0 ], "owner", users ) | ||
.then( function() { | ||
devices.entityCache[ 0 ].owner.should.equal( users.entityCache[ 0 ] ); | ||
} ) | ||
.then( done ) | ||
.catch( done ); | ||
devices.sync(); | ||
$httpBackend.flush(); | ||
$httpBackend.flush(); | ||
} ); | ||
devices.read( 1 ) | ||
.then( function( device ) { | ||
expect( device ).to.not.equal( seed.devices[ 0 ] ); | ||
} ) | ||
.then( done ) | ||
.catch( done ); | ||
$rootScope.$digest(); | ||
it( "should populate referenced complex types and cross-link", function( done ) { | ||
users.ensureLoaded(); | ||
devices.read( "1" ); | ||
$httpBackend.flush(); | ||
users.entityCache[ 0 ].devices = []; | ||
devices.populateComplex( devices.entityCache[ 0 ], "owner", users, { | ||
crossLink : true, | ||
crossLinkProperty : "devices" | ||
} ) | ||
.then( function() { | ||
devices.entityCache[ 0 ].owner.should.equal( users.entityCache[ 0 ] ); | ||
users.entityCache[ 0 ].devices.should.be.an( "array" ).with.length( 1 ); | ||
users.entityCache[ 0 ].devices[ 0 ].should.equal( devices.entityCache[ 0 ] ); | ||
} ) | ||
.then( done ) | ||
.catch( done ); | ||
$httpBackend.flush(); | ||
} ); | ||
} ); | ||
@@ -159,0 +355,0 @@ } ); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
7721009
123
122161