diff --git a/src/behaviors/entity-store.js b/src/behaviors/entity-store.js index 7fa686f..5a5b6c1 100644 --- a/src/behaviors/entity-store.js +++ b/src/behaviors/entity-store.js @@ -1,7 +1,11 @@ import angular from 'angular'; import {Behavior} from './behavior'; -import {addBehavior} from '../utils'; import {Handler as handlerDecorator} from '../store'; +import { + addBehavior, + camelcase, + Inject as injectDecorator +} from '../utils'; export class EntityStoreBehavior extends Behavior { isLoading = false; @@ -21,11 +25,11 @@ export class EntityStoreBehavior extends Behavior { } reset() { - this.loadDeferred = null; - this.createDeferred = null; - this.readDeferred = null; - this.updateDeferred = null; - this.deleteDeferred = null; + this.loadDeferred = this.createNewDeferred(); + this.createDeferred = this.createNewDeferred(); + this.readDeferred = this.createNewDeferred(); + this.updateDeferred = this.createNewDeferred(); + this.deleteDeferred = this.createNewDeferred(); this.isSet = false; this.items.splice(0, this.items.length); @@ -37,31 +41,31 @@ export class EntityStoreBehavior extends Behavior { } get isBusy() { - return Boolean(this.loadPromise || - this.createPromise || - this.readPromise || - this.updatePromise || - this.deletePromise); + return Boolean(this.isLoading || + this.isCreating || + this.isReading || + this.isUpdating || + this.isDeleting); } get loadPromise() { - return this.loadDeferred && this.loadDeferred.promise; + return this.loadDeferred.promise; } get createPromise() { - return this.createDeferred && this.createDeferred.promise; + return this.createDeferred.promise; } get readPromise() { - return this.readDeferred && this.readDeferred.promise; + return this.readDeferred.promise; } get updatePromise() { - return this.updateDeferred && this.updateDeferred.promise; + return this.updateDeferred.promise; } get deletePromise() { - return this.deleteDeferred && this.deleteDeferred.promise; + return this.deleteDeferred.promise; } createNewDeferred() { @@ -72,6 +76,10 @@ export class EntityStoreBehavior extends Behavior { this.instance.emit('changed', ...arguments); } + onError() { + this.instance.emit('error', ...arguments); + } + getById(entityId) { return this.items.find(entity => entity[this.idProperty] === entityId) || null; } @@ -100,13 +108,14 @@ export class EntityStoreBehavior extends Behavior { this.onChanged('load', entities); this.loadDeferred.resolve(entities); - this.loadDeferred = null; } onLoadFailed(error) { this.isLoading = false; + + this.onError('load', error); + this.loadDeferred.reject(error); - this.loadDeferred = null; } onCreateStarted() { @@ -132,13 +141,14 @@ export class EntityStoreBehavior extends Behavior { this.onChanged('create', currentEntity); this.createDeferred.resolve(currentEntity); - this.createDeferred = null; } onCreateFailed(error) { this.isCreating = false; + + this.onError('create', error); + this.createDeferred.reject(error); - this.createDeferred = null; } onReadStarted() { @@ -164,13 +174,14 @@ export class EntityStoreBehavior extends Behavior { this.onChanged('read', currentEntity); this.readDeferred.resolve(entity); - this.readDeferred = null; } onReadFailed(error) { this.isReading = false; + + this.onError('read', error); + this.readDeferred.reject(error); - this.readDeferred = null; } onUpdateStarted() { @@ -186,7 +197,6 @@ export class EntityStoreBehavior extends Behavior { if (!currentEntity) { this.updateDeferred.reject('Updated entity that is not in this store...', entity); - this.updateDeferred = null; return; } @@ -196,13 +206,14 @@ export class EntityStoreBehavior extends Behavior { this.onChanged('update', currentEntity); this.updateDeferred.resolve(entity); - this.updateDeferred = null; } onUpdateFailed(error) { this.isUpdating = false; + + this.onError('update', error); + this.updateDeferred.reject(error); - this.updateDeferred = null; } onDeleteStarted() { @@ -218,7 +229,6 @@ export class EntityStoreBehavior extends Behavior { if (!currentEntity) { this.deleteDeferred.reject('Deleting entity that is not in this store...', entity); - this.deleteDeferred = null; return; } @@ -228,13 +238,14 @@ export class EntityStoreBehavior extends Behavior { this.onChanged('delete', currentEntity); this.deleteDeferred.resolve(entity); - this.deleteDeferred = null; } onDeleteFailed(error) { this.isDeleting = false; + + this.onError('delete', error); + this.deleteDeferred.reject(error); - this.deleteDeferred = null; } } @@ -253,11 +264,12 @@ export function EntityStore(config = {}) { }, preparedConfig); preparedConfig.entity = camelcase(preparedConfig.entity); + injectDecorator()(cls.prototype, '$q'); + const actionHandlers = []; for (const action of preparedConfig.actions) { const actionName = camelcase(action); const entityAction = `on${preparedConfig.entity}${actionName}`; - const startedAction = `${entityAction}Started`; const completedAction = `${entityAction}Completed`; const failedAction = `${entityAction}Failed`; @@ -266,10 +278,10 @@ export function EntityStore(config = {}) { actionHandlers.push(`${completedAction}:on${actionName}Completed`); actionHandlers.push(`${failedAction}:on${actionName}Failed`); - const decorate = handlerDecorator(null, false); - decorate(cls.prototype, startedAction); - decorate(cls.prototype, completedAction); - decorate(cls.prototype, failedAction); + const handlerDecorate = handlerDecorator(null, false); + handlerDecorate(cls.prototype, startedAction); + handlerDecorate(cls.prototype, completedAction); + handlerDecorate(cls.prototype, failedAction); } addBehavior(cls, 'entityStore', EntityStoreBehavior, preparedConfig, [ @@ -296,7 +308,3 @@ export function EntityStore(config = {}) { ].concat(actionHandlers)); }; } - -function camelcase(name) { - return `${name[0].toUpperCase()}${name.slice(1)}`; -} diff --git a/src/behaviors/entity-store.spec.js b/src/behaviors/entity-store.spec.js index e5a3c3c..15b2200 100644 --- a/src/behaviors/entity-store.spec.js +++ b/src/behaviors/entity-store.spec.js @@ -1,4 +1,5 @@ /*eslint-env node, jasmine*//*global module, inject*/ +/*eslint-disable max-statements, max-params*/ import angular from 'angular'; import 'angular-mocks'; import 'luxyflux/ng-luxyflux'; @@ -10,7 +11,7 @@ import { Handler } from 'anglue/anglue'; -describe('EventEmitter', () => { +describe('EntityStore', () => { describe('EntityStoreBehavior', () => { let mockInstance, behavior; beforeEach(() => { @@ -83,6 +84,11 @@ describe('EventEmitter', () => { expect(behavior.instance.emit).toHaveBeenCalledWith('changed', 'load', ['bar']); }); + it('should emit the error event on the store on LOAD_FAILED', () => { + behavior.onLoadFailed('foo error'); + expect(behavior.instance.emit).toHaveBeenCalledWith('error', 'load', 'foo error'); + }); + it('should reset the hasDetailSet', () => { behavior.hasDetailSet.add('foo'); behavior.onLoadCompleted(['bar']); @@ -172,6 +178,11 @@ describe('EventEmitter', () => { expect(behavior.instance.emit).toHaveBeenCalledWith('changed', 'create', entity); }); + it('should emit the error event on the store on CREATE_FAILED', () => { + behavior.onCreateFailed('foo error'); + expect(behavior.instance.emit).toHaveBeenCalledWith('error', 'create', 'foo error'); + }); + it('should define a createPromise on CREATE_STARTED', () => { expect(behavior.createPromise).toEqual(mockInstance.$q.defer().promise); }); @@ -254,6 +265,11 @@ describe('EventEmitter', () => { expect(behavior.instance.emit).toHaveBeenCalledWith('changed', 'read', entity); }); + it('should emit the error event on the store on READ_FAILED', () => { + behavior.onReadFailed('foo error'); + expect(behavior.instance.emit).toHaveBeenCalledWith('error', 'read', 'foo error'); + }); + it('should define a readPromise on READ_STARTED', () => { expect(behavior.readPromise).toEqual(mockInstance.$q.defer().promise); }); @@ -332,6 +348,11 @@ describe('EventEmitter', () => { expect(behavior.instance.emit).toHaveBeenCalledWith('changed', 'update', entity); }); + it('should emit the error event on the store on UPDATE_FAILED', () => { + behavior.onUpdateFailed('foo error'); + expect(behavior.instance.emit).toHaveBeenCalledWith('error', 'update', 'foo error'); + }); + it('should define a updatePromise on UPDATE_STARTED', () => { expect(behavior.updatePromise).toEqual(mockInstance.$q.defer().promise); }); @@ -389,6 +410,11 @@ describe('EventEmitter', () => { expect(behavior.instance.emit).toHaveBeenCalledWith('changed', 'delete', entity); }); + it('should emit the error event on the store on DELETE_FAILED', () => { + behavior.onDeleteFailed('foo error'); + expect(behavior.instance.emit).toHaveBeenCalledWith('error', 'delete', 'foo error'); + }); + it('should define a deletePromise on DELETE_STARTED', () => { expect(behavior.deletePromise).toEqual(mockInstance.$q.defer().promise); }); @@ -484,10 +510,52 @@ describe('EventEmitter', () => { }); describe('@EntityStore() decorator', () => { - it('should define the EntityStore API methods on the store', () => { - @EntityStore() class TestStore {} - const entityStore = new TestStore(); + @Store() @EntityStore() class TestStore {} + @Store() @EntityStore({idProperty: 'test'}) class IdPropertyStore {} + @Store() @EntityStore({entity: 'custom'}) class CustomEntityStore {} + @Store() @EntityStore('custom') class CustomEntityStringStore {} + @Store() @EntityStore({actions: ['read', 'update']}) class ActionsStore {} + @Store() @EntityStore({collection: 'foo'}) class CollectionStore {} + + let store, $q; + let idPropertyStore, customEntityStore, customEntityStringStore, actionsStore, collectionStore; + beforeEach(() => { + angular.module('test', [ + 'luxyflux', + TestStore.annotation.module.name, + IdPropertyStore.annotation.module.name, + CustomEntityStore.annotation.module.name, + CustomEntityStringStore.annotation.module.name, + ActionsStore.annotation.module.name, + CollectionStore.annotation.module.name + ]).service('ApplicationDispatcher', () => { + return { + register() {}, + dispatch() {} + }; + }); + + module('test'); + inject(( + _TestStore_, + _IdPropertyStore_, + _CustomEntityStore_, + _CustomEntityStringStore_, + _ActionsStore_, + _CollectionStore_, + _$q_ + ) => { + store = _TestStore_; + idPropertyStore = _IdPropertyStore_; + customEntityStore = _CustomEntityStore_; + customEntityStringStore = _CustomEntityStringStore_; + actionsStore = _ActionsStore_; + collectionStore = _CollectionStore_; + $q = _$q_; + }); + }); + it('should define the EntityStore API methods on the store', () => { [ 'entityStore', 'items', @@ -527,78 +595,59 @@ describe('EventEmitter', () => { 'onTestDeleteStarted', 'onTestDeleteCompleted', 'onTestDeleteFailed' - ].forEach(api => expect(entityStore[api]).toBeDefined()); + ].forEach(api => expect(store[api]).toBeDefined()); + }); + + it('should inject $q into the store', () => { + expect(store.$q).toBe($q); }); it('should have an instance of EntityStoreBehavior as the behavior property', () => { - @EntityStore() class TestStore {} - const store = new TestStore(); expect(store.entityStore).toEqual(jasmine.any(EntityStoreBehavior)); }); it('should use the class name to determine the crud entity by default', () => { - @EntityStore() class TestStore {} - const store = new TestStore(); expect(store.entityStore.config.entity).toEqual('Test'); }); it('should use id as the default the entity id property', () => { - @EntityStore() class TestStore {} - const store = new TestStore(); expect(store.entityStore.idProperty).toEqual('id'); }); it('should be possible to configure the entity id property', () => { - @EntityStore({idProperty: 'test'}) class TestStore {} - const store = new TestStore(); - expect(store.entityStore.idProperty).toEqual('test'); + expect(idPropertyStore.entityStore.idProperty).toEqual('test'); }); it('should be possible to configure the entity to manage', () => { - @EntityStore({entity: 'foo'}) class TestStore {} - const store = new TestStore(); - expect(store.entityStore.config.entity).toEqual('Foo'); + expect(customEntityStore.entityStore.config.entity).toEqual('Custom'); }); it('should be possible to pass the entity property as a string', () => { - @EntityStore('test') class TestStore {} - const store = new TestStore(); - expect(store.entityStore.config.entity).toEqual('Test'); + expect(customEntityStringStore.entityStore.config.entity).toEqual('Custom'); }); it('should create properly named handlers when configuring the entity', () => { - @EntityStore({entity: 'fooBar'}) class TestStore {} - const store = new TestStore(); - expect(store.onFooBarLoadCompleted).toBeDefined(); + expect(customEntityStore.onCustomLoadCompleted).toBeDefined(); }); it('should manage all actions by default', () => { - @EntityStore() class TestStore {} - const store = new TestStore(); expect(store.entityStore.config.actions) .toEqual(['load', 'create', 'read', 'update', 'delete']); }); it('should be possible to configure the actions the store manage', () => { - @EntityStore({actions: ['read', 'update']}) class TestStore {} - const store = new TestStore(); - expect(store.entityStore.config.actions).toEqual(['read', 'update']); + expect(actionsStore.entityStore.config.actions).toEqual(['read', 'update']); }); it('should use the items property to store entities in by default', () => { - @EntityStore() class TestStore {} - const store = new TestStore(); expect(store.items).toEqual(jasmine.any(Array)); }); it('should be possible to configure the collection property the entities are stored in', () => { - @EntityStore({collection: 'foo'}) class TestStore {} - const store = new TestStore(); - expect(store.foo).toEqual(jasmine.any(Array)); + expect(collectionStore.foo).toEqual(jasmine.any(Array)); }); it('should add handlers for actions', () => { - @EntityStore() class TestStore {} expect(TestStore.handlers) .toEqual(jasmine.objectContaining({ TEST_LOAD_STARTED: 'onTestLoadStarted', @@ -620,8 +669,8 @@ describe('EventEmitter', () => { }); it('should only add handlers for the chosen actions', () => { - @EntityStore({actions: ['read']}) class TestStore {} - expect(TestStore.handlers) + @EntityStore({actions: ['read']}) class CustomStore {} + expect(CustomStore.handlers) .not.toEqual(jasmine.objectContaining({ TEST_LOAD_STARTED: 'onTestLoadStarted' })); @@ -629,11 +678,10 @@ describe('EventEmitter', () => { it('should not override any handlers already defined on the store', () => { @Store() - @EntityStore() class TestStore { + @EntityStore() class CustomStore { @Handler('TEST_LOAD_FAILED') onCustomFailed() {} } - - expect(TestStore.handlers).toEqual(jasmine.objectContaining({ + expect(CustomStore.handlers).toEqual(jasmine.objectContaining({ TEST_LOAD_FAILED: 'onCustomFailed' })); }); diff --git a/src/utils.js b/src/utils.js index 166e8e6..cd41c98 100644 --- a/src/utils.js +++ b/src/utils.js @@ -111,7 +111,7 @@ export function Decorators(decorators) { }; } -function getCurrentDescriptorValue(propertyDescriptor) { +export function getCurrentDescriptorValue(propertyDescriptor) { if (propertyDescriptor === undefined) { return {}; } else if (propertyDescriptor.get) { @@ -119,3 +119,7 @@ function getCurrentDescriptorValue(propertyDescriptor) { } return propertyDescriptor.value; } + +export function camelcase(name) { + return `${name[0].toUpperCase()}${name.slice(1)}`; +}