diff --git a/CHANGELOG.md b/CHANGELOG.md index 52402cb9..29dab6ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,9 @@ Please look there. **_This_** Changelog covers changes to the repository and the demo applications. -# 0.6.1 (TBD) +# 0.6.1 (2018-05-25) +* Refactored for EntityAction operators as required by Beta 6 * Add example of extending EntityDataServices with custom `HeroDataService` as described in `entity-dataservice.md` (#151). diff --git a/lib/CHANGELOG.md b/lib/CHANGELOG.md index 5d275edd..d3dbf923 100644 --- a/lib/CHANGELOG.md +++ b/lib/CHANGELOG.md @@ -1,5 +1,60 @@ # Angular ngrx-data library ChangeLog + + +# 6.0.1-beta.6 (2018-05-24) + +## _EntityActions_ replaced by _EntityAction operators_ + +_BREAKING CHANGE_ + +Sub-classing of `Observable` is deprecated in v.6, in favor of custom pipeable operators. + +Accordingly, `EntityActions` has been removed. +Use the _EntityAction_ operators, `ofEntityOp` and `ofEntityType` instead. + +Before + +```typescript +// Select HeroActions +entityActions.ofEntityType('Hero).pipe(...); + +// Select QUERY_ALL operations +entityActions.ofOp(EntityOp.QUERY_ALL).pipe(...); +``` + +After + +```typescript +// Select HeroActions +entityActions.pipe(ofEntityType('Hero), ...); + +// Select QUERY_ALL operations +entityActions.pipe(ofEntityOp(EntityOp.QUERY_ALL, ...); +``` + +The `EntityActions.where` and `EntityActions.until` methods have not been replaced. +Use standard RxJS `filter` and `takeUntil` operators instead. + +## Other Features + +* `NgrxDataModuleWithoutEffects` is now public rather than internal. + Useful for devs who prefer to opt out of @ngrx/effects for entities and to + handle HTTP calls on their own. + +Import it instead of `NgrxDataModule`, like this. + +```typescript +@NgModule({ + imports: [ + NgrxDataModuleWithoutEffects.forRoot(appNgrxDataModuleConfig), + ... + ], + ... +}) +export class EntityAppModule {...} +``` + # 6.0.1-beta.5 (2018-05-23) @@ -247,7 +302,7 @@ Extends `DefaultDataServiceConfig` so you can specify them. For example, instead of setting the `PluralNames` for `Hero` you could fully specify the singular and plural resource URLS in the `DefaultDataServiceConfig` like this: -```javascript +```typescript // store/entity-metadata.ts // Not needed for data access when set Hero's HttpResourceUrls diff --git a/lib/package.json b/lib/package.json index 5a5854fa..982673de 100644 --- a/lib/package.json +++ b/lib/package.json @@ -1,6 +1,6 @@ { "name": "ngrx-data", - "version": "6.0.0-beta.5", + "version": "6.0.0-beta.6", "repository": "https://github.com/johnpapa/angular-ngrx-data.git", "license": "MIT", "peerDependencies": { diff --git a/lib/src/actions/entity-action-operators.spec.ts b/lib/src/actions/entity-action-operators.spec.ts new file mode 100644 index 00000000..3762475f --- /dev/null +++ b/lib/src/actions/entity-action-operators.spec.ts @@ -0,0 +1,162 @@ +import { Action } from '@ngrx/store'; +import { Actions } from '@ngrx/effects'; + +import { Subject } from 'rxjs'; + +import { EntityAction, EntityActionFactory } from './entity-action'; +import { EntityOp } from './entity-op'; + +import { ofEntityType, ofEntityOp } from './entity-action-operators'; + +class Hero { + id: number; + name: string; +} + +// Todo: consider marble testing +describe('EntityAction Operators', () => { + // factory never changes in these tests + const entityActionFactory = new EntityActionFactory(); + + let results: any[]; + let actions: Subject; + + const testActions = { + foo: { type: 'Foo' }, + hero_query_all: entityActionFactory.create('Hero', EntityOp.QUERY_ALL), + villain_query_many: entityActionFactory.create( + 'Villain', + EntityOp.QUERY_MANY + ), + hero_delete: entityActionFactory.create( + 'Hero', + EntityOp.SAVE_DELETE_ONE, + 42 + ), + bar: ({ type: 'Bar', payload: 'bar' }) + }; + + function dispatchTestActions() { + Object.keys(testActions).forEach(a => actions.next((testActions)[a])); + } + + beforeEach(() => { + actions = new Subject(); + results = []; + }); + + /////////////// + + it('#ofEntityType()', () => { + // EntityActions of any kind + actions.pipe(ofEntityType()).subscribe(ea => results.push(ea)); + + const expectedActions = [ + testActions.hero_query_all, + testActions.villain_query_many, + testActions.hero_delete + ]; + dispatchTestActions(); + expect(results).toEqual(expectedActions); + }); + + it(`#ofEntityType('SomeType')`, () => { + // EntityActions of one type + actions.pipe(ofEntityType('Hero')).subscribe(ea => results.push(ea)); + + const expectedActions = [ + testActions.hero_query_all, + testActions.hero_delete + ]; + dispatchTestActions(); + expect(results).toEqual(expectedActions); + }); + + it(`#ofEntityType('Type1', 'Type2', 'Type3')`, () => { + // n.b. 'Bar' is not an EntityType even though it is an action type + actions + .pipe(ofEntityType('Hero', 'Villain', 'Bar')) + .subscribe(ea => results.push(ea)); + + ofEntityTypeTest(); + }); + + it('#ofEntityType(...arrayOfTypeNames)', () => { + const types = ['Hero', 'Villain', 'Bar']; + + actions.pipe(ofEntityType(...types)).subscribe(ea => results.push(ea)); + ofEntityTypeTest(); + }); + + it('#ofEntityType(arrayOfTypeNames)', () => { + const types = ['Hero', 'Villain', 'Bar']; + + actions.pipe(ofEntityType(types)).subscribe(ea => results.push(ea)); + ofEntityTypeTest(); + }); + + function ofEntityTypeTest() { + const expectedActions = [ + testActions.hero_query_all, + testActions.villain_query_many, + testActions.hero_delete + // testActions.bar, // 'Bar' is not an EntityType + ]; + dispatchTestActions(); + expect(results).toEqual(expectedActions); + } + + it('#ofEntityType(...) is case sensitive', () => { + // EntityActions of the 'hero' type, but it's lowercase so shouldn't match + actions.pipe(ofEntityType('hero')).subscribe(ea => results.push(ea)); + + dispatchTestActions(); + expect(results).toEqual([], 'should not match anything'); + }); + + /////////////// + + it('#ofEntityOp with string args', () => { + actions + .pipe(ofEntityOp(EntityOp.QUERY_ALL, EntityOp.QUERY_MANY)) + .subscribe(ea => results.push(ea)); + + ofEntityOpTest(); + }); + + it('#ofEntityOp with ...rest args', () => { + const ops = [EntityOp.QUERY_ALL, EntityOp.QUERY_MANY]; + + actions.pipe(ofEntityOp(...ops)).subscribe(ea => results.push(ea)); + ofEntityOpTest(); + }); + + it('#ofEntityOp with array args', () => { + const ops = [EntityOp.QUERY_ALL, EntityOp.QUERY_MANY]; + + actions.pipe(ofEntityOp(ops)).subscribe(ea => results.push(ea)); + ofEntityOpTest(); + }); + + it('#ofEntityOp()', () => { + // EntityOps of any kind + actions.pipe(ofEntityOp()).subscribe(ea => results.push(ea)); + + const expectedActions = [ + testActions.hero_query_all, + testActions.villain_query_many, + testActions.hero_delete + ]; + dispatchTestActions(); + expect(results).toEqual(expectedActions); + }); + + function ofEntityOpTest() { + const expectedActions = [ + testActions.hero_query_all, + testActions.villain_query_many + ]; + dispatchTestActions(); + expect(results).toEqual(expectedActions); + } +}); diff --git a/lib/src/actions/entity-action-operators.ts b/lib/src/actions/entity-action-operators.ts new file mode 100644 index 00000000..063698f2 --- /dev/null +++ b/lib/src/actions/entity-action-operators.ts @@ -0,0 +1,80 @@ +import { Action } from '@ngrx/store'; +import { Actions } from '@ngrx/effects'; + +import { Observable, OperatorFunction } from 'rxjs'; +import { filter } from 'rxjs/operators'; + +import { EntityAction } from './entity-action'; +import { EntityOp } from './entity-op'; +import { flattenArgs } from '../utils/utilities'; + +/** + * Select actions concerning one of the allowed Entity operations + * @param allowedEntityOps Entity operations (e.g, EntityOp.QUERY_ALL) whose actions should be selected + * Example: + * ``` + * this.actions.pipe(ofEntityOp(EntityOp.QUERY_ALL, EntityOp.QUERY_MANY), ...) + * this.actions.pipe(ofEntityOp(...queryOps), ...) + * this.actions.pipe(ofEntityOp(queryOps), ...) + * this.actions.pipe(ofEntityOp(), ...) // any action with a defined `op` property + * ``` + */ +export function ofEntityOp( + allowedOps: string[] | EntityOp[] +): OperatorFunction; +export function ofEntityOp( + ...allowedOps: (string | EntityOp)[] +): OperatorFunction; +export function ofEntityOp( + ...allowedEntityOps: any[] +): OperatorFunction { + const ops: string[] = flattenArgs(allowedEntityOps); + switch (ops.length) { + case 0: + return filter((action: EntityAction): action is T => !!action.op); + case 1: + const op = ops[0]; + return filter((action: EntityAction): action is T => op === action.op); + default: + return filter((action: EntityAction): action is T => + ops.some(entityOp => entityOp === action.op) + ); + } +} + +/** + * Select actions concerning one of the allowed Entity types + * @param allowedEntityNames Entity-type names (e.g, 'Hero') whose actions should be selected + * Example: + * ``` + * this.actions.pipe(ofEntityType(), ...) // ayn EntityAction with a defined entity type property + * this.actions.pipe(ofEntityType('Hero'), ...) // EntityActions for the Hero entity + * this.actions.pipe(ofEntityType('Hero', 'Villain', 'Sidekick'), ...) + * this.actions.pipe(ofEntityType(...theChosen), ...) + * this.actions.pipe(ofEntityType(theChosen), ...) + * ``` + */ +export function ofEntityType( + allowedEntityNames?: string[] +): OperatorFunction; +export function ofEntityType( + ...allowedEntityNames: string[] +): OperatorFunction; +export function ofEntityType( + ...allowedEntityNames: any[] +): OperatorFunction { + const names: string[] = flattenArgs(allowedEntityNames); + switch (names.length) { + case 0: + return filter((action: EntityAction): action is T => !!action.entityName); + case 1: + const name = names[0]; + return filter( + (action: EntityAction): action is T => name === action.entityName + ); + default: + return filter((action: EntityAction): action is T => + names.some(entityName => entityName === action.entityName) + ); + } +} diff --git a/lib/src/actions/entity-actions.spec.ts b/lib/src/actions/entity-actions.spec.ts deleted file mode 100644 index 15b6a826..00000000 --- a/lib/src/actions/entity-actions.spec.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { Action } from '@ngrx/store'; -import { Actions } from '@ngrx/effects'; - -import { Subject } from 'rxjs'; - -import { EntityAction, EntityActionFactory } from './entity-action'; -import { EntityActions } from './entity-actions'; -import { EntityOp } from './entity-op'; - -class Hero { - id: number; - name: string; -} - -// Todo: consider marble testing -describe('EntityActions', () => { - // factory never changes in these tests - const entityActionFactory = new EntityActionFactory(); - - let eas: EntityActions; - let results: any[]; - let source: Subject; - - const testActions = { - foo: { type: 'Foo' }, - hero_query_all: entityActionFactory.create('Hero', EntityOp.QUERY_ALL), - villain_query_many: entityActionFactory.create( - 'Villain', - EntityOp.QUERY_MANY - ), - hero_delete: entityActionFactory.create( - 'Hero', - EntityOp.SAVE_DELETE_ONE, - 42 - ), - bar: ({ type: 'Bar', payload: 'bar' }) - }; - - function dispatchTestActions() { - Object.keys(testActions).forEach(a => source.next((testActions)[a])); - } - - beforeEach(() => { - source = new Subject(); - const actions = new Actions(source); - eas = new EntityActions(actions); - results = []; - }); - - it('#where', () => { - // Filter for the 'Hero' EntityAction with a payload - eas - .where(ea => ea.entityName === 'Hero' && ea.payload != null) - .subscribe(ea => results.push(ea)); - - // This is it - const expectedActions = [testActions.hero_delete]; - dispatchTestActions(); - expect(results).toEqual(expectedActions); - }); - - /////////////// - - it('#ofEntityType()', () => { - // EntityActions of any kind - eas.ofEntityType().subscribe(ea => results.push(ea)); - - const expectedActions = [ - testActions.hero_query_all, - testActions.villain_query_many, - testActions.hero_delete - ]; - dispatchTestActions(); - expect(results).toEqual(expectedActions); - }); - - it(`#ofEntityType('SomeType')`, () => { - // EntityActions of one type - eas.ofEntityType('Hero').subscribe(ea => results.push(ea)); - - const expectedActions = [ - testActions.hero_query_all, - testActions.hero_delete - ]; - dispatchTestActions(); - expect(results).toEqual(expectedActions); - }); - - it(`#ofEntityType('Type1', 'Type2', 'Type3')`, () => { - // n.b. 'Bar' is not an EntityType even though it is an action type - eas - .ofEntityType('Hero', 'Villain', 'Bar') - .subscribe(ea => results.push(ea)); - - ofEntityTypesTest(); - }); - - it('#ofEntityType(...arrayOfTypeNames)', () => { - const types = ['Hero', 'Villain', 'Bar']; - - eas.ofEntityType(...types).subscribe(ea => results.push(ea)); - ofEntityTypesTest(); - }); - - it('#ofEntityType(arrayOfTypeNames)', () => { - const types = ['Hero', 'Villain', 'Bar']; - - eas.ofEntityType(types).subscribe(ea => results.push(ea)); - ofEntityTypesTest(); - }); - - function ofEntityTypesTest() { - const expectedActions = [ - testActions.hero_query_all, - testActions.villain_query_many, - testActions.hero_delete - // testActions.bar, // 'Bar' is not an EntityType - ]; - dispatchTestActions(); - expect(results).toEqual(expectedActions); - } - - it('#ofEntityType(...) is case sensitive', () => { - // EntityActions of the 'hero' type, but it's lowercase so shouldn't match - eas.ofEntityType('hero').subscribe(ea => results.push(ea)); - - dispatchTestActions(); - expect(results).toEqual([], 'should not match anything'); - }); - - /////////////// - - it('#ofOp with string args', () => { - eas - .ofOp(EntityOp.QUERY_ALL, EntityOp.QUERY_MANY) - .subscribe(ea => results.push(ea)); - - ofOpTest(); - }); - - it('#ofOp with ...rest args', () => { - const ops = [EntityOp.QUERY_ALL, EntityOp.QUERY_MANY]; - - eas.ofOp(...ops).subscribe(ea => results.push(ea)); - ofOpTest(); - }); - - it('#ofOp with array args', () => { - const ops = [EntityOp.QUERY_ALL, EntityOp.QUERY_MANY]; - - eas.ofOp(ops).subscribe(ea => results.push(ea)); - ofOpTest(); - }); - - function ofOpTest() { - const expectedActions = [ - testActions.hero_query_all, - testActions.villain_query_many - ]; - dispatchTestActions(); - expect(results).toEqual(expectedActions); - } - - /////////////// - const testTypeNames = [ - entityActionFactory.formatActionType(EntityOp.QUERY_ALL, 'Hero'), - entityActionFactory.formatActionType(EntityOp.QUERY_MANY, 'Villain') - ]; - - it('#ofType with string args', () => { - eas.ofType(testTypeNames).subscribe(ea => results.push(ea)); - ofTypeTest(); - }); - - it('#ofType with ...rest args', () => { - eas.ofType(...testTypeNames).subscribe(ea => results.push(ea)); - ofTypeTest(); - }); - - it('#ofType with array args', () => { - eas.ofType(testTypeNames).subscribe(ea => results.push(ea)); - ofTypeTest(); - }); - - function ofTypeTest() { - const expectedActions = [ - testActions.hero_query_all, - testActions.villain_query_many - ]; - dispatchTestActions(); - expect(results).toEqual(expectedActions); - } - - /////////////// - - it('#until(notifier) completes the subscriber', () => { - const stop = new Subject(); - let completed = 0; - - const actions = eas.ofEntityType().until(stop); // completes and unsubscribes - - actions.subscribe(null, null, () => completed++); - actions.subscribe(ea => results.push(ea), null, () => completed++); - - const action = entityActionFactory.create( - 'Hero', - EntityOp.SAVE_DELETE_ONE, - 42 - ); - - source.next(action); - source.next(action); - source.next(action); - stop.next(); - - // The following should be ignored. - source.next(action); - source.next(action); - - expect(results.length).toBe(3, 'should have 3 results'); - expect(completed).toBe(2, 'should have completed both subscriptions'); - }); -}); diff --git a/lib/src/actions/entity-actions.ts b/lib/src/actions/entity-actions.ts deleted file mode 100644 index b5785a1e..00000000 --- a/lib/src/actions/entity-actions.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { Injectable } from '@angular/core'; -import { Action } from '@ngrx/store'; -import { Actions } from '@ngrx/effects'; - -import { Observable, Operator } from 'rxjs'; -import { filter, takeUntil } from 'rxjs/operators'; - -import { EntityAction } from './entity-action'; -import { EntityOp } from './entity-op'; -import { flattenArgs } from '../utils/utilities'; - -/** - * Observable of entity actions dispatched to the store. - * EntityAction-oriented filter operators for ease-of-use. - * Imitates `Actions.ofType()` in ngrx/entity. - */ -@Injectable() -export class EntityActions< - V extends EntityAction = EntityAction -> extends Observable { - // Inject the ngrx/effect Actions observable that watches dispatches to the store - constructor(source?: Actions) { - super(); - - if (source) { - // ONLY look at EntityActions (source is deprecated but no known substitute) - /* tslint:disable-next-line:deprecation */ - this.source = source.pipe( - filter((action: any) => action.op && action.entityName) - ); - } - } - - // Can't name it `filter` because exists in `import 'rxjs/operator';` (issue 97). - // 'where' was an alias for `filter` long ago but no import risk now. - - /** - * Filter EntityActions based on a predicate. - * @param predicate -returns true if EntityAction passes the test. - * Example: - * this.actions$.where(ea => ea.op.includes(EntityAction.OP_SUCCESS)) // Successful hero action - */ - where(predicate: (ea: EntityAction) => boolean) { - return filter(predicate)(this) as EntityActions; - } - - lift(operator: Operator): Observable { - const observable = new EntityActions(); - // source is deprecated but no known substitute - /* tslint:disable-next-line:deprecation */ - observable.source = this; - - // "Force-casts" below because can't change signature of Lift. - // operator is deprecated but no known substitute) - /* tslint:disable-next-line:deprecation */ - observable.operator = >(operator); - return >(observable); - } - - /** - * Entity actions concerning any of the given entity types - * @param allowedEntityNames - names of entities whose actions should pass through. - * Example: - * ``` - * this.actions$.ofEntityType() // an EntityAction of any entity type - * this.actions$.ofEntityType('Hero') // EntityActions for the Hero entity - * this.actions$.ofEntityType('Hero', 'Villain', 'Sidekick') - * this.actions$.ofEntityType(...theChosen) - * this.actions$.ofEntityType(theChosen) - * ``` - */ - ofEntityType(allowedEntityNames?: string[]): EntityActions; - ofEntityType(...allowedEntityNames: string[]): EntityActions; - ofEntityType(...allowedEntityNames: any[]): EntityActions { - const names: string[] = flattenArgs(allowedEntityNames); - switch (names.length) { - case 0: - return this.where(ea => !!ea.entityName); - case 1: - const name = names[0]; - return this.where(ea => name === ea.entityName); - default: - return this.where( - ea => ea.entityName && names.some(n => n === ea.entityName) - ); - } - } - - /** - * Entity actions concerning any of the given `EntityOp`s - * @param allowedOps - `EntityOp`s whose actions should pass through. - * Example: - * ``` - * this.actions$.ofOp(EntityOp.QUERY_ALL, EntityOp.QUERY_MANY) - * this.actions$.ofOp(...queryOps) - * this.actions$.ofOp(queryOps) - * ``` - */ - ofOp(allowedOps: string[] | EntityOp[]): EntityActions; - ofOp(...allowedOps: (string | EntityOp)[]): EntityActions; - ofOp(...allowedOps: any[]) { - // string is the runtime type of an EntityOp enum - const ops: string[] = flattenArgs(allowedOps); - return this.where(ea => ops.some(op => op === ea.op)); - } - - /** - * Entity actions of the given type(s) - * @param allowedTypes - `Action.type`s whose actions should pass through. - * Example: - * ``` - * this.actions$.ofTypes('GET_ALL_HEROES', 'GET_ALL_SIDEKICKS') - * this.actions$.ofTypes(...someTypes) - * this.actions$.ofTypes(someTypes) - * ``` - */ - ofType(allowedTypes: string[]): EntityActions; - ofType(...allowedTypes: string[]): EntityActions; - ofType(...allowedTypes: any[]): EntityActions { - const types: string[] = flattenArgs(allowedTypes); - return this.where(ea => types.some(type => type === ea.type)); - } - - /** - * Continue emitting actions until the `notifier` says stop. - * When the `notifier` emits a next value, this observable completes - * and subscribers are unsubscribed. - * Uses RxJS `takeUntil().` - * @param notifier - observable that stops the source with a `next()`. - * Example: - * this.actions$.ofEntityType('Hero').until(this.onDestroy); - */ - until(notifier: Observable): EntityActions { - return takeUntil(notifier)(this) as EntityActions; - } -} diff --git a/lib/src/effects/entity-effects.marbles.spec.ts b/lib/src/effects/entity-effects.marbles.spec.ts index bbe1933a..23f46909 100644 --- a/lib/src/effects/entity-effects.marbles.spec.ts +++ b/lib/src/effects/entity-effects.marbles.spec.ts @@ -2,10 +2,12 @@ import { TestBed } from '@angular/core/testing'; import { cold, hot } from 'jasmine-marbles'; -import { Observable } from 'rxjs'; +import { Observable, Subject } from 'rxjs'; + +import { Actions } from '@ngrx/effects'; +import { provideMockActions } from '@ngrx/effects/testing'; import { EntityAction, EntityActionFactory } from '../actions/entity-action'; -import { EntityActions } from '../actions/entity-actions'; import { EntityOp, OP_ERROR } from '../actions/entity-op'; import { @@ -26,19 +28,7 @@ import { EntityEffects } from './entity-effects'; import { Logger } from '../utils/interfaces'; import { Update } from '../utils/ngrx-entity-models'; - -export class TestEntityActions extends EntityActions { - set stream(source: Observable) { - // source is deprecated but no known substitute - /* tslint:disable-next-line:deprecation */ - this.source = source; - } -} - -// For AOT -export function getActions() { - return new TestEntityActions(); -} +import { TestHotObservable } from 'jasmine-marbles/src/test-observables'; export class TestEntityDataService { dataServiceSpy: any; @@ -70,7 +60,7 @@ describe('EntityEffects (marble testing)', () => { let effects: EntityEffects; let entityActionFactory: EntityActionFactory; let testEntityDataService: TestEntityDataService; - let actions$: TestEntityActions; + let actions: Observable; let logger: Logger; beforeEach(() => { @@ -79,8 +69,8 @@ describe('EntityEffects (marble testing)', () => { TestBed.configureTestingModule({ providers: [ EntityEffects, + provideMockActions(() => actions), EntityActionFactory, - { provide: EntityActions, useFactory: getActions }, { provide: EntityDataService, useFactory: getDataService }, { provide: Logger, useValue: logger }, { @@ -92,7 +82,7 @@ describe('EntityEffects (marble testing)', () => { entityActionFactory = TestBed.get(EntityActionFactory); effects = TestBed.get(EntityEffects); testEntityDataService = TestBed.get(EntityDataService); - actions$ = TestBed.get(EntityActions); + actions = TestBed.get(Actions); }); it('should return a QUERY_ALL_SUCCESS with the heroes on success', () => { @@ -107,7 +97,8 @@ describe('EntityEffects (marble testing)', () => { heroes ); - actions$.stream = hot('-a---', { a: action }); + const x = hot('-a---', { a: action }); + actions = hot('-a---', { a: action }); // delay the response 3 ticks const response = cold('---a|', { a: heroes }); const expected = cold('----b', { b: completion }); @@ -122,7 +113,7 @@ describe('EntityEffects (marble testing)', () => { const completion = makeEntityErrorCompletion(action, 'GET', httpError); const error = completion.payload.error; - actions$.stream = hot('-a---', { a: action }); + actions = hot('-a---', { a: action }); const response = cold('----#|', {}, error); const expected = cold('-----b', { b: completion }); testEntityDataService.dataServiceSpy.getAll.and.returnValue(response); @@ -142,7 +133,7 @@ describe('EntityEffects (marble testing)', () => { EntityOp.QUERY_BY_KEY_SUCCESS ); - actions$.stream = hot('-a---', { a: action }); + actions = hot('-a---', { a: action }); // delay the response 3 ticks const response = cold('---a|', { a: undefined }); const expected = cold('----b', { b: completion }); @@ -161,7 +152,7 @@ describe('EntityEffects (marble testing)', () => { const completion = makeEntityErrorCompletion(action, 'DELETE', httpError); const error = completion.payload.error; - actions$.stream = hot('-a---', { a: action }); + actions = hot('-a---', { a: action }); const response = cold('----#|', {}, error); const expected = cold('-----b', { b: completion }); testEntityDataService.dataServiceSpy.getById.and.returnValue(response); @@ -183,7 +174,7 @@ describe('EntityEffects (marble testing)', () => { heroes ); - actions$.stream = hot('-a---', { a: action }); + actions = hot('-a---', { a: action }); // delay the response 3 ticks const response = cold('---a|', { a: heroes }); const expected = cold('----b', { b: completion }); @@ -202,7 +193,7 @@ describe('EntityEffects (marble testing)', () => { }); const error = completion.payload.error; - actions$.stream = hot('-a---', { a: action }); + actions = hot('-a---', { a: action }); const response = cold('----#|', {}, error); const expected = cold('-----b', { b: completion }); testEntityDataService.dataServiceSpy.getWithQuery.and.returnValue(response); @@ -225,7 +216,7 @@ describe('EntityEffects (marble testing)', () => { hero ); - actions$.stream = hot('-a---', { a: action }); + actions = hot('-a---', { a: action }); // delay the response 3 ticks const response = cold('---a|', { a: hero }); const expected = cold('----b', { b: completion }); @@ -245,7 +236,7 @@ describe('EntityEffects (marble testing)', () => { const completion = makeEntityErrorCompletion(action, 'PUT', httpError); const error = completion.payload.error; - actions$.stream = hot('-a---', { a: action }); + actions = hot('-a---', { a: action }); const response = cold('----#|', {}, error); const expected = cold('-----b', { b: completion }); testEntityDataService.dataServiceSpy.add.and.returnValue(response); @@ -265,7 +256,7 @@ describe('EntityEffects (marble testing)', () => { 42 ); - actions$.stream = hot('-a---', { a: action }); + actions = hot('-a---', { a: action }); // delay the response 3 ticks const response = cold('---a|', { a: 42 }); const expected = cold('----b', { b: completion }); @@ -284,7 +275,7 @@ describe('EntityEffects (marble testing)', () => { const completion = makeEntityErrorCompletion(action, 'DELETE', httpError); const error = completion.payload.error; - actions$.stream = hot('-a---', { a: action }); + actions = hot('-a---', { a: action }); const response = cold('----#|', {}, error); const expected = cold('-----b', { b: completion }); testEntityDataService.dataServiceSpy.delete.and.returnValue(response); @@ -306,7 +297,7 @@ describe('EntityEffects (marble testing)', () => { update ); - actions$.stream = hot('-a---', { a: action }); + actions = hot('-a---', { a: action }); // delay the response 3 ticks const response = cold('---a|', { a: update }); const expected = cold('----b', { b: completion }); @@ -326,7 +317,7 @@ describe('EntityEffects (marble testing)', () => { const completion = makeEntityErrorCompletion(action, 'PUT', httpError); const error = completion.payload.error; - actions$.stream = hot('-a---', { a: action }); + actions = hot('-a---', { a: action }); const response = cold('----#|', {}, error); const expected = cold('-----b', { b: completion }); testEntityDataService.dataServiceSpy.update.and.returnValue(response); @@ -348,7 +339,7 @@ describe('EntityEffects (marble testing)', () => { hero ); - actions$.stream = hot('-a---', { a: action }); + actions = hot('-a---', { a: action }); // delay the response 3 ticks const response = cold('---a|', { a: hero }); const expected = cold('----b', { b: completion }); @@ -368,7 +359,7 @@ describe('EntityEffects (marble testing)', () => { const completion = makeEntityErrorCompletion(action, 'PUT', httpError); const error = completion.payload.error; - actions$.stream = hot('-a---', { a: action }); + actions = hot('-a---', { a: action }); const response = cold('----#|', {}, error); const expected = cold('-----b', { b: completion }); testEntityDataService.dataServiceSpy.add.and.returnValue(response); @@ -387,7 +378,7 @@ describe('EntityEffects (marble testing)', () => { EntityOp.SAVE_DELETE_ONE_OPTIMISTIC_SUCCESS ); - actions$.stream = hot('-a---', { a: action }); + actions = hot('-a---', { a: action }); // delay the response 3 ticks const response = cold('---a|', { a: undefined }); const expected = cold('----b', { b: completion }); @@ -406,7 +397,7 @@ describe('EntityEffects (marble testing)', () => { const completion = makeEntityErrorCompletion(action, 'DELETE', httpError); const error = completion.payload.error; - actions$.stream = hot('-a---', { a: action }); + actions = hot('-a---', { a: action }); const response = cold('----#|', {}, error); const expected = cold('-----b', { b: completion }); testEntityDataService.dataServiceSpy.delete.and.returnValue(response); @@ -428,7 +419,7 @@ describe('EntityEffects (marble testing)', () => { update ); - actions$.stream = hot('-a---', { a: action }); + actions = hot('-a---', { a: action }); // delay the response 3 ticks const response = cold('---a|', { a: update }); const expected = cold('----b', { b: completion }); @@ -448,7 +439,7 @@ describe('EntityEffects (marble testing)', () => { const completion = makeEntityErrorCompletion(action, 'PUT', httpError); const error = completion.payload.error; - actions$.stream = hot('-a---', { a: action }); + actions = hot('-a---', { a: action }); const response = cold('----#|', {}, error); const expected = cold('-----b', { b: completion }); testEntityDataService.dataServiceSpy.update.and.returnValue(response); @@ -460,7 +451,7 @@ describe('EntityEffects (marble testing)', () => { // Would clear the cached collection const action = entityActionFactory.create('Hero', EntityOp.REMOVE_ALL); - actions$.stream = hot('-a---', { a: action }); + actions = hot('-a---', { a: action }); const expected = cold('---'); expect(effects.persist$).toBeObservable(expected); diff --git a/lib/src/effects/entity-effects.spec.ts b/lib/src/effects/entity-effects.spec.ts index db173f22..8a359f9e 100644 --- a/lib/src/effects/entity-effects.spec.ts +++ b/lib/src/effects/entity-effects.spec.ts @@ -1,11 +1,12 @@ // Not using marble testing import { TestBed } from '@angular/core/testing'; +import { Action } from '@ngrx/store'; +import { Actions } from '@ngrx/effects'; -import { Observable, of, merge, throwError } from 'rxjs'; +import { Observable, of, merge, Subject, throwError } from 'rxjs'; import { delay, first } from 'rxjs/operators'; import { EntityAction, EntityActionFactory } from '../actions/entity-action'; -import { EntityActions } from '../actions/entity-actions'; import { EntityOp, OP_ERROR } from '../actions/entity-op'; import { @@ -27,18 +28,6 @@ import { EntityEffects } from './entity-effects'; import { Logger } from '../utils/interfaces'; import { Update } from '../utils/ngrx-entity-models'; -export class TestEntityActions extends EntityActions { - set stream(source: Observable) { - // source is deprecated but no known substitute - /* tslint:disable-next-line:deprecation */ - this.source = source; - } -} - -export function getActions() { - return new TestEntityActions(); -} - export class TestEntityDataService { dataServiceSpy: any; @@ -70,10 +59,10 @@ describe('EntityEffects (normal testing)', () => { // factory never changes in these tests const entityActionFactory = new EntityActionFactory(); + let actions$: Subject; let effects: EntityEffects; - let testEntityDataService: TestEntityDataService; - let actions$: TestEntityActions; let logger: Logger; + let testEntityDataService: TestEntityDataService; function expectCompletion(completion: EntityAction) { effects.persist$.subscribe( @@ -88,11 +77,12 @@ describe('EntityEffects (normal testing)', () => { beforeEach(() => { logger = jasmine.createSpyObj('Logger', ['error', 'log', 'warn']); + actions$ = new Subject(); TestBed.configureTestingModule({ providers: [ EntityEffects, - { provide: EntityActions, useFactory: getActions }, + { provide: Actions, useValue: actions$ }, { provide: EntityActionFactory, useValue: entityActionFactory }, { provide: EntityDataService, useFactory: getDataService }, { provide: Logger, useValue: logger }, @@ -103,9 +93,9 @@ describe('EntityEffects (normal testing)', () => { ] }); + actions$ = TestBed.get(Actions); effects = TestBed.get(EntityEffects); testEntityDataService = TestBed.get(EntityDataService); - actions$ = TestBed.get(EntityActions); }); it('should return a QUERY_ALL_SUCCESS, with the heroes, on success', () => { @@ -123,7 +113,7 @@ describe('EntityEffects (normal testing)', () => { heroes ); - actions$.stream = of(action); + actions$.next(action); expectCompletion(completion); }); @@ -148,7 +138,7 @@ describe('EntityEffects (normal testing)', () => { heroes ); - actions$.stream = of(action); + actions$.next(action); expectCompletion(completion); }); @@ -172,7 +162,7 @@ describe('EntityEffects (normal testing)', () => { heroes ); - actions$.stream = of(action); + actions$.next(action); expectCompletion(completion); }); @@ -182,7 +172,7 @@ describe('EntityEffects (normal testing)', () => { const completion = makeEntityErrorCompletion(action, 'GET', httpError); const error = completion.payload.error; - actions$.stream = of(action); + actions$.next(action); const response = throwError(error); testEntityDataService.dataServiceSpy.getAll.and.returnValue(response); @@ -201,7 +191,7 @@ describe('EntityEffects (normal testing)', () => { EntityOp.QUERY_BY_KEY_SUCCESS ); - actions$.stream = of(action); + actions$.next(action); const response = of(undefined); testEntityDataService.dataServiceSpy.getById.and.returnValue(response); @@ -218,7 +208,7 @@ describe('EntityEffects (normal testing)', () => { const completion = makeEntityErrorCompletion(action, 'DELETE', httpError); const error = completion.payload.error; - actions$.stream = of(action); + actions$.next(action); const response = throwError(error); testEntityDataService.dataServiceSpy.getById.and.returnValue(response); @@ -239,7 +229,7 @@ describe('EntityEffects (normal testing)', () => { heroes ); - actions$.stream = of(action); + actions$.next(action); const response = of(heroes); testEntityDataService.dataServiceSpy.getWithQuery.and.returnValue(response); @@ -256,7 +246,7 @@ describe('EntityEffects (normal testing)', () => { }); const error = completion.payload.error; - actions$.stream = of(action); + actions$.next(action); const response = throwError(error); testEntityDataService.dataServiceSpy.getWithQuery.and.returnValue(response); @@ -277,7 +267,7 @@ describe('EntityEffects (normal testing)', () => { hero ); - actions$.stream = of(action); + actions$.next(action); const response = of(hero); testEntityDataService.dataServiceSpy.add.and.returnValue(response); @@ -295,7 +285,7 @@ describe('EntityEffects (normal testing)', () => { const completion = makeEntityErrorCompletion(action, 'PUT', httpError); const error = completion.payload.error; - actions$.stream = of(action); + actions$.next(action); const response = throwError(error); testEntityDataService.dataServiceSpy.add.and.returnValue(response); @@ -314,7 +304,7 @@ describe('EntityEffects (normal testing)', () => { 42 ); - actions$.stream = of(action); + actions$.next(action); const response = of(42); // dataservice successful delete returns the deleted entity id testEntityDataService.dataServiceSpy.delete.and.returnValue(response); @@ -331,7 +321,7 @@ describe('EntityEffects (normal testing)', () => { const completion = makeEntityErrorCompletion(action, 'DELETE', httpError); const error = completion.payload.error; - actions$.stream = of(action); + actions$.next(action); const response = throwError(error); testEntityDataService.dataServiceSpy.delete.and.returnValue(response); @@ -352,7 +342,7 @@ describe('EntityEffects (normal testing)', () => { update ); - actions$.stream = of(action); + actions$.next(action); const response = of(update); testEntityDataService.dataServiceSpy.update.and.returnValue(response); @@ -370,7 +360,7 @@ describe('EntityEffects (normal testing)', () => { const completion = makeEntityErrorCompletion(action, 'PUT', httpError); const error = completion.payload.error; - actions$.stream = of(action); + actions$.next(action); const response = throwError(error); testEntityDataService.dataServiceSpy.update.and.returnValue(response); @@ -391,7 +381,7 @@ describe('EntityEffects (normal testing)', () => { hero ); - actions$.stream = of(action); + actions$.next(action); const response = of(hero); testEntityDataService.dataServiceSpy.add.and.returnValue(response); @@ -409,7 +399,7 @@ describe('EntityEffects (normal testing)', () => { const completion = makeEntityErrorCompletion(action, 'PUT', httpError); const error = completion.payload.error; - actions$.stream = of(action); + actions$.next(action); const response = throwError(error); testEntityDataService.dataServiceSpy.add.and.returnValue(response); @@ -427,7 +417,7 @@ describe('EntityEffects (normal testing)', () => { EntityOp.SAVE_DELETE_ONE_OPTIMISTIC_SUCCESS ); - actions$.stream = of(action); + actions$.next(action); const response = of(undefined); testEntityDataService.dataServiceSpy.delete.and.returnValue(response); @@ -444,7 +434,7 @@ describe('EntityEffects (normal testing)', () => { const completion = makeEntityErrorCompletion(action, 'DELETE', httpError); const error = completion.payload.error; - actions$.stream = of(action); + actions$.next(action); const response = throwError(error); testEntityDataService.dataServiceSpy.delete.and.returnValue(response); @@ -465,7 +455,7 @@ describe('EntityEffects (normal testing)', () => { update ); - actions$.stream = of(action); + actions$.next(action); const response = of(update); testEntityDataService.dataServiceSpy.update.and.returnValue(response); @@ -483,7 +473,7 @@ describe('EntityEffects (normal testing)', () => { const completion = makeEntityErrorCompletion(action, 'PUT', httpError); const error = completion.payload.error; - actions$.stream = of(action); + actions$.next(action); const response = throwError(error); testEntityDataService.dataServiceSpy.update.and.returnValue(response); @@ -494,7 +484,7 @@ describe('EntityEffects (normal testing)', () => { // Would clear the cached collection const action = entityActionFactory.create('Hero', EntityOp.REMOVE_ALL); - actions$.stream = of(action); + actions$.next(action); const sentinel = 'no persist$ effect'; merge( diff --git a/lib/src/effects/entity-effects.ts b/lib/src/effects/entity-effects.ts index 8080b513..32a87ef3 100644 --- a/lib/src/effects/entity-effects.ts +++ b/lib/src/effects/entity-effects.ts @@ -1,13 +1,13 @@ import { Injectable } from '@angular/core'; import { Action } from '@ngrx/store'; -import { Effect } from '@ngrx/effects'; +import { Effect, Actions } from '@ngrx/effects'; import { Observable, of } from 'rxjs'; import { concatMap, catchError, map } from 'rxjs/operators'; import { EntityAction, EntityActionFactory } from '../actions/entity-action'; -import { EntityActions } from '../actions/entity-actions'; import { EntityOp } from '../actions/entity-op'; +import { ofEntityOp } from '../actions/entity-action-operators'; import { EntityDataService } from '../dataservices/entity-data.service'; import { PersistenceResultHandler } from '../dataservices/persistence-result-handler.service'; @@ -29,12 +29,13 @@ export class EntityEffects { @Effect() // Concurrent persistence requests considered unsafe. // `concatMap` ensures each request must complete-or-fail before making the next request. - persist$: Observable = this.actions$ - .ofOp(persistOps) - .pipe(concatMap(action => this.persist(action))); + persist$: Observable = this.actions.pipe( + ofEntityOp(persistOps), + concatMap(action => this.persist(action)) + ); constructor( - private actions$: EntityActions, + private actions: Actions, private dataService: EntityDataService, private entityActionFactory: EntityActionFactory, private resultHandler: PersistenceResultHandler diff --git a/lib/src/entity-services/entity-collection-service-base.ts b/lib/src/entity-services/entity-collection-service-base.ts index 843dcf0d..092ddd54 100644 --- a/lib/src/entity-services/entity-collection-service-base.ts +++ b/lib/src/entity-services/entity-collection-service-base.ts @@ -1,11 +1,11 @@ import { Injectable } from '@angular/core'; import { Action, Store } from '@ngrx/store'; +import { Actions } from '@ngrx/effects'; import { Observable } from 'rxjs'; import { Dictionary, IdSelector, Update } from '../utils/ngrx-entity-models'; import { EntityAction } from '../actions/entity-action'; -import { EntityActions } from '../actions/entity-actions'; import { EntityOp } from '../actions/entity-op'; import { EntityActionGuard } from '../actions/entity-action-guard'; import { EntityCache } from '../reducers/entity-cache'; @@ -329,13 +329,13 @@ export class EntityCollectionServiceBase< entities$: Observable | Store; /** Observable of actions related to this entity type. */ - entityActions$: EntityActions; + entityActions$: Observable; /** Observable of the map of entity keys to entities */ entityMap$: Observable> | Store>; /** Observable of error actions related to this entity type. */ - errors$: EntityActions; + errors$: Observable; /** Observable of the filter pattern applied by the entity collection's filter function */ filter$: Observable | Store; diff --git a/lib/src/entity-services/entity-collection-service.spec.ts b/lib/src/entity-services/entity-collection-service.spec.ts index a964f89c..66f3cf29 100644 --- a/lib/src/entity-services/entity-collection-service.spec.ts +++ b/lib/src/entity-services/entity-collection-service.spec.ts @@ -1,11 +1,11 @@ /** TODO: much more testing */ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Action, StoreModule, Store } from '@ngrx/store'; +import { Actions } from '@ngrx/effects'; import { Subject } from 'rxjs'; import { EntityAction, EntityActionFactory } from '../actions/entity-action'; -import { EntityActions } from '../actions/entity-actions'; import { EntityOp } from '../actions/entity-op'; import { EntityCache } from '../reducers/entity-cache'; @@ -16,7 +16,7 @@ import { EntityCollectionServiceFactory } from './entity-services-interfaces'; -import { _NgrxDataModuleWithoutEffects } from '../ngrx-data.module'; +import { NgrxDataModuleWithoutEffects } from '../ngrx-data-without-effects.module'; import { commandDispatchTest } from '../dispatchers/entity-dispatcher.spec'; @@ -95,12 +95,11 @@ const heroMetadata = { function entityServiceFactorySetup() { const actions$ = new Subject(); - const entityActions = new EntityActions(actions$); TestBed.configureTestingModule({ - imports: [StoreModule.forRoot({}), _NgrxDataModuleWithoutEffects], + imports: [StoreModule.forRoot({}), NgrxDataModuleWithoutEffects], providers: [ - { provide: EntityActions, useValue: entityActions }, + { provide: Actions, useValue: actions$ }, { provide: ENTITY_METADATA_TOKEN, multi: true, @@ -123,7 +122,6 @@ function entityServiceFactorySetup() { return { actions$, - entityActions, entityActionFactory, entityCollectionServiceFactory, testStore diff --git a/lib/src/index.ts b/lib/src/index.ts index 1042365c..fd025a57 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -2,7 +2,7 @@ // NO BARRELS or else `ng build --aot` of any app using ngrx-data produces strange errors // actions export * from './actions/entity-action'; -export * from './actions/entity-actions'; +export * from './actions/entity-action-operators'; export * from './actions/entity-action-guard'; export * from './actions/entity-cache-actions'; export * from './actions/entity-op'; @@ -57,4 +57,9 @@ export * from './utils/default-logger'; export * from './utils/default-pluralizer'; export * from './utils/utilities'; -export { NgrxDataModule, NgrxDataModuleConfig } from './ngrx-data.module'; +// NgrxDataModule +export { NgrxDataModule } from './ngrx-data.module'; +export { + NgrxDataModuleWithoutEffects, + NgrxDataModuleConfig +} from './ngrx-data-without-effects.module'; diff --git a/lib/src/ngrx-data-without-effects.module.ts b/lib/src/ngrx-data-without-effects.module.ts new file mode 100644 index 00000000..51705fd8 --- /dev/null +++ b/lib/src/ngrx-data-without-effects.module.ts @@ -0,0 +1,191 @@ +import { + ModuleWithProviders, + NgModule, + Inject, + Injector, + InjectionToken, + Optional, + OnDestroy +} from '@angular/core'; + +import { + Action, + ActionReducer, + combineReducers, + MetaReducer, + ReducerManager, + StoreModule +} from '@ngrx/store'; + +import { EntityAction, EntityActionFactory } from './actions/entity-action'; + +import { EntityDispatcherFactory } from './dispatchers/entity-dispatcher-factory'; + +import { EntityDefinitionService } from './entity-metadata/entity-definition.service'; +import { + EntityMetadataMap, + ENTITY_METADATA_TOKEN +} from './entity-metadata/entity-metadata'; + +import { EntityCache } from './reducers/entity-cache'; +import { entityCacheSelectorProvider } from './selectors/entity-cache-selector'; +import { EntityCollectionServiceFactory } from './entity-services/entity-services-interfaces'; +import { DefaultEntityCollectionServiceFactory } from './entity-services/default-entity-collection-service-factory'; +import { EntityCollection } from './reducers/entity-collection'; +import { EntityCollectionCreator } from './reducers/entity-collection-creator'; +import { + EntityCollectionReducerFactory, + EntityCollectionReducerMethodsFactory +} from './reducers/entity-collection-reducer'; +import { EntityEffects } from './effects/entity-effects'; + +import { DefaultEntityCollectionReducerMethodsFactory } from './reducers/default-entity-collection-reducer-methods'; +import { + createEntityReducer, + EntityReducerFactory +} from './reducers/entity-reducer'; +import { + ENTITY_CACHE_NAME, + ENTITY_CACHE_NAME_TOKEN, + ENTITY_CACHE_META_REDUCERS, + ENTITY_COLLECTION_META_REDUCERS, + ENTITY_CACHE_REDUCER, + INITIAL_ENTITY_CACHE_STATE +} from './reducers/constants'; + +import { Logger, Pluralizer, PLURAL_NAMES_TOKEN } from './utils/interfaces'; + +import { EntitySelectors } from './selectors/entity-selectors'; +import { EntitySelectorsFactory } from './selectors/entity-selectors'; +import { EntitySelectors$Factory } from './selectors/entity-selectors$'; +import { EntityServices } from './entity-services/entity-services-interfaces'; +import { EntityServicesBase } from './entity-services/entity-services-base'; + +import { DefaultLogger } from './utils/default-logger'; +import { DefaultPluralizer } from './utils/default-pluralizer'; + +export interface NgrxDataModuleConfig { + entityMetadata?: EntityMetadataMap; + entityCacheMetaReducers?: ( + | MetaReducer + | InjectionToken>)[]; + entityCollectionMetaReducers?: MetaReducer[]; + // Initial EntityCache state or a function that returns that state + initialEntityCacheState?: EntityCache | (() => EntityCache); + pluralNames?: { [name: string]: string }; +} + +/** + * Module without effects or dataservices which means no HTTP calls + * This module helpful for internal testing. + * Also helpful for apps that handle server access on their own and + * therefore opt-out of @ngrx/effects for entities + */ +@NgModule({ + imports: [ + StoreModule // rely on Store feature providers rather than Store.forFeature() + ], + providers: [ + EntityActionFactory, + entityCacheSelectorProvider, + EntityCollectionCreator, + EntityCollectionReducerFactory, + EntityDefinitionService, + EntityDispatcherFactory, + EntityReducerFactory, + EntitySelectorsFactory, + EntitySelectors$Factory, + { + provide: EntityCollectionReducerMethodsFactory, + useClass: DefaultEntityCollectionReducerMethodsFactory + }, + { provide: ENTITY_CACHE_NAME_TOKEN, useValue: ENTITY_CACHE_NAME }, + { + provide: ENTITY_CACHE_REDUCER, + deps: [EntityReducerFactory], + useFactory: createEntityReducer + }, + { + provide: EntityCollectionServiceFactory, + useClass: DefaultEntityCollectionServiceFactory + }, + { + provide: EntityServices, + useClass: EntityServicesBase + }, + { provide: Logger, useClass: DefaultLogger } + ] +}) +export class NgrxDataModuleWithoutEffects implements OnDestroy { + private entityCacheFeature: any; + + static forRoot(config: NgrxDataModuleConfig): ModuleWithProviders { + return { + ngModule: NgrxDataModuleWithoutEffects, + providers: [ + { + provide: ENTITY_CACHE_META_REDUCERS, + useValue: config.entityCacheMetaReducers + ? config.entityCacheMetaReducers + : [] + }, + { + provide: ENTITY_COLLECTION_META_REDUCERS, + useValue: config.entityCollectionMetaReducers + ? config.entityCollectionMetaReducers + : [] + }, + { + provide: PLURAL_NAMES_TOKEN, + multi: true, + useValue: config.pluralNames ? config.pluralNames : {} + } + ] + }; + } + + constructor( + private reducerManager: ReducerManager, + @Inject(ENTITY_CACHE_REDUCER) + private entityCacheReducer: ActionReducer, + private injector: Injector, + // optional params + @Optional() + @Inject(ENTITY_CACHE_NAME_TOKEN) + private entityCacheName: string, + @Optional() + @Inject(INITIAL_ENTITY_CACHE_STATE) + private initialState: any, + @Optional() + @Inject(ENTITY_CACHE_META_REDUCERS) + private metaReducers: ( + | MetaReducer + | InjectionToken>)[] + ) { + // Add the ngrx-data feature to the Store's features + // as Store.forFeature does for StoreFeatureModule + const key = entityCacheName || ENTITY_CACHE_NAME; + + initialState = + typeof initialState === 'function' ? initialState() : initialState; + + const reducers: MetaReducer[] = ( + metaReducers || [] + ).map(mr => { + return mr instanceof InjectionToken ? injector.get(mr) : mr; + }); + + this.entityCacheFeature = { + key, + reducers: entityCacheReducer, + reducerFactory: combineReducers, + initialState: initialState || {}, + metaReducers: reducers + }; + reducerManager.addFeature(this.entityCacheFeature); + } + + ngOnDestroy() { + this.reducerManager.removeFeature(this.entityCacheFeature); + } +} diff --git a/lib/src/ngrx-data.module.spec.ts b/lib/src/ngrx-data.module.spec.ts index 78e09a05..958ca352 100644 --- a/lib/src/ngrx-data.module.spec.ts +++ b/lib/src/ngrx-data.module.spec.ts @@ -11,12 +11,12 @@ import { Actions, Effect, EffectsModule } from '@ngrx/effects'; // Not using marble testing import { TestBed } from '@angular/core/testing'; -import { Observable, of } from 'rxjs'; -import { map, skip } from 'rxjs/operators'; +import { Observable, of, Subject } from 'rxjs'; +import { map, skip, tap } from 'rxjs/operators'; import { EntityAction, EntityActionFactory } from './actions/entity-action'; -import { EntityActions } from './actions/entity-actions'; import { EntityOp, OP_ERROR } from './actions/entity-op'; +import { ofEntityOp } from './actions/entity-action-operators'; import { EntityCache } from './reducers/entity-cache'; import { EntityCollection } from './reducers/entity-collection'; @@ -25,14 +25,6 @@ import { EntityCollectionCreator } from './reducers/entity-collection-creator'; import { EntityEffects, persistOps } from './effects/entity-effects'; import { NgrxDataModule } from './ngrx-data.module'; -class TestEntityActions extends EntityActions { - set stream(source: Observable) { - // source is deprecated but no known substitute - /* tslint:disable-next-line:deprecation */ - this.source = source; - } -} - const TEST_ACTION = 'test/get-everything-succeeded'; const EC_METAREDUCER_TOKEN = new InjectionToken< MetaReducer @@ -41,9 +33,13 @@ const EC_METAREDUCER_TOKEN = new InjectionToken< @Injectable() class TestEntityEffects { @Effect() - test$: Observable = this.actions$ - .ofOp(persistOps) - .pipe(map(this.testHook)); + test$: Observable = this.actions.pipe( + // tap(action => { + // console.log('test$ effect', action); + // }), + ofEntityOp(persistOps), + map(this.testHook) + ); testHook(action: EntityAction) { return { @@ -53,7 +49,7 @@ class TestEntityEffects { }; } - constructor(private actions$: EntityActions) {} + constructor(private actions: Actions) {} } class Hero { @@ -80,7 +76,6 @@ describe('NgrxDataModule', () => { const entityActionFactory = new EntityActionFactory(); let actions$: Actions; - let entityAction$: TestEntityActions; let store: Store; let testEffects: TestEntityEffects; @@ -97,7 +92,6 @@ describe('NgrxDataModule', () => { }); actions$ = TestBed.get(Actions); - entityAction$ = TestBed.get(EntityActions); store = TestBed.get(Store); testEffects = TestBed.get(EntityEffects); @@ -105,12 +99,17 @@ describe('NgrxDataModule', () => { }); it('should invoke test effect with an EntityAction', () => { - const action = entityActionFactory.create('Hero', EntityOp.QUERY_ALL); const actions: Action[] = []; // listen for actions after the next dispatched action - actions$.pipe(skip(1)).subscribe(act => actions.push(act)); + actions$ + .pipe( + // tap(act => console.log('test action', act)), + skip(1) // Skip QUERY_ALL + ) + .subscribe(act => actions.push(act)); + const action = entityActionFactory.create('Hero', EntityOp.QUERY_ALL); store.dispatch(action); expect(actions.length).toBe(1, 'expect one effect action'); expect(actions[0].type).toBe('test-action'); @@ -122,7 +121,7 @@ describe('NgrxDataModule', () => { // listen for actions after the next dispatched action actions$.pipe(skip(1)).subscribe(act => actions.push(act)); - store.dispatch({ type: 'dummy-action' }); + store.dispatch({ type: 'not-an-entity-action' }); expect(actions.length).toBe(0); }); }); diff --git a/lib/src/ngrx-data.module.ts b/lib/src/ngrx-data.module.ts index 4dc44b88..59ed07fe 100644 --- a/lib/src/ngrx-data.module.ts +++ b/lib/src/ngrx-data.module.ts @@ -1,24 +1,6 @@ -import { - ModuleWithProviders, - NgModule, - Inject, - Injector, - InjectionToken, - Optional, - OnDestroy -} from '@angular/core'; -import { - Action, - ActionReducer, - combineReducers, - MetaReducer, - ReducerManager, - StoreModule -} from '@ngrx/store'; -import { EffectsModule, EffectSources } from '@ngrx/effects'; +import { ModuleWithProviders, NgModule } from '@angular/core'; -import { EntityAction, EntityActionFactory } from './actions/entity-action'; -import { EntityActions } from './actions/entity-actions'; +import { EffectsModule, EffectSources } from '@ngrx/effects'; import { DefaultDataServiceFactory } from './dataservices/default-data.service'; import { EntityDataService } from './dataservices/entity-data.service'; @@ -32,164 +14,34 @@ import { DefaultHttpUrlGenerator } from './dataservices/http-url-generator'; -import { EntityDispatcherFactory } from './dispatchers/entity-dispatcher-factory'; - -import { EntityDefinitionService } from './entity-metadata/entity-definition.service'; -import { - EntityMetadataMap, - ENTITY_METADATA_TOKEN -} from './entity-metadata/entity-metadata'; +import { ENTITY_METADATA_TOKEN } from './entity-metadata/entity-metadata'; -import { EntityCache } from './reducers/entity-cache'; -import { entityCacheSelectorProvider } from './selectors/entity-cache-selector'; -import { EntityCollectionServiceFactory } from './entity-services/entity-services-interfaces'; -import { DefaultEntityCollectionServiceFactory } from './entity-services/default-entity-collection-service-factory'; -import { EntityCollection } from './reducers/entity-collection'; -import { EntityCollectionCreator } from './reducers/entity-collection-creator'; -import { - EntityCollectionReducerFactory, - EntityCollectionReducerMethodsFactory -} from './reducers/entity-collection-reducer'; import { EntityEffects } from './effects/entity-effects'; -import { DefaultEntityCollectionReducerMethodsFactory } from './reducers/default-entity-collection-reducer-methods'; -import { - createEntityReducer, - EntityReducerFactory -} from './reducers/entity-reducer'; import { - ENTITY_CACHE_NAME, - ENTITY_CACHE_NAME_TOKEN, ENTITY_CACHE_META_REDUCERS, - ENTITY_COLLECTION_META_REDUCERS, - ENTITY_CACHE_REDUCER, - INITIAL_ENTITY_CACHE_STATE + ENTITY_COLLECTION_META_REDUCERS } from './reducers/constants'; - -import { Logger, Pluralizer, PLURAL_NAMES_TOKEN } from './utils/interfaces'; - -import { EntitySelectors } from './selectors/entity-selectors'; -import { EntitySelectorsFactory } from './selectors/entity-selectors'; -import { EntitySelectors$Factory } from './selectors/entity-selectors$'; -import { EntityServices } from './entity-services/entity-services-interfaces'; -import { EntityServicesBase } from './entity-services/entity-services-base'; - -import { DefaultLogger } from './utils/default-logger'; +import { Pluralizer, PLURAL_NAMES_TOKEN } from './utils/interfaces'; import { DefaultPluralizer } from './utils/default-pluralizer'; -export interface NgrxDataModuleConfig { - entityMetadata?: EntityMetadataMap; - entityCacheMetaReducers?: ( - | MetaReducer - | InjectionToken>)[]; - entityCollectionMetaReducers?: MetaReducer[]; - // Initial EntityCache state or a function that returns that state - initialEntityCacheState?: EntityCache | (() => EntityCache); - pluralNames?: { [name: string]: string }; -} - -/** - * Module without effects which means no HTTP calls - * It is helpful for internal testing but not for users - */ -@NgModule({ - imports: [ - StoreModule // rely on Store feature providers rather than Store.forFeature() - ], - providers: [ - EntityActionFactory, - entityCacheSelectorProvider, - EntityCollectionCreator, - EntityCollectionReducerFactory, - EntityDefinitionService, - EntityDispatcherFactory, - EntityReducerFactory, - EntitySelectorsFactory, - EntitySelectors$Factory, - { - provide: EntityCollectionReducerMethodsFactory, - useClass: DefaultEntityCollectionReducerMethodsFactory - }, - { provide: ENTITY_CACHE_NAME_TOKEN, useValue: ENTITY_CACHE_NAME }, - { - provide: ENTITY_CACHE_REDUCER, - deps: [EntityReducerFactory], - useFactory: createEntityReducer - }, - { - provide: EntityCollectionServiceFactory, - useClass: DefaultEntityCollectionServiceFactory - }, - { - provide: EntityServices, - useClass: EntityServicesBase - }, - { provide: Logger, useClass: DefaultLogger } - ] -}) -// tslint:disable-next-line:class-name -export class _NgrxDataModuleWithoutEffects implements OnDestroy { - private entityCacheFeature: any; - - constructor( - private reducerManager: ReducerManager, - @Inject(ENTITY_CACHE_REDUCER) - private entityCacheReducer: ActionReducer, - private injector: Injector, - // optional params - @Optional() - @Inject(ENTITY_CACHE_NAME_TOKEN) - private entityCacheName: string, - @Optional() - @Inject(INITIAL_ENTITY_CACHE_STATE) - private initialState: any, - @Optional() - @Inject(ENTITY_CACHE_META_REDUCERS) - private metaReducers: ( - | MetaReducer - | InjectionToken>)[] - ) { - // Add the ngrx-data feature to the Store's features - // as Store.forFeature does for StoreFeatureModule - const key = entityCacheName || ENTITY_CACHE_NAME; - - initialState = - typeof initialState === 'function' ? initialState() : initialState; - - const reducers: MetaReducer[] = ( - metaReducers || [] - ).map(mr => { - return mr instanceof InjectionToken ? injector.get(mr) : mr; - }); - - this.entityCacheFeature = { - key, - reducers: entityCacheReducer, - reducerFactory: combineReducers, - initialState: initialState || {}, - metaReducers: reducers - }; - reducerManager.addFeature(this.entityCacheFeature); - } - - ngOnDestroy() { - this.reducerManager.removeFeature(this.entityCacheFeature); - } -} +import { + NgrxDataModuleConfig, + NgrxDataModuleWithoutEffects +} from './ngrx-data-without-effects.module'; /** - * Ngrx-data main module + * Ngrx-data main module includes effects and HTTP data services * Configure with `forRoot`. * No `forFeature` yet. */ @NgModule({ imports: [ - _NgrxDataModuleWithoutEffects, + NgrxDataModuleWithoutEffects, EffectsModule // do not supply effects because can't replace later ], providers: [ DefaultDataServiceFactory, - EntityActions, EntityDataService, { provide: HttpUrlGenerator, useClass: DefaultHttpUrlGenerator }, { provide: Pluralizer, useClass: DefaultPluralizer }, diff --git a/lib/src/selectors/entity-selectors$.spec.ts b/lib/src/selectors/entity-selectors$.spec.ts index 3a97e4c7..69d66ab4 100644 --- a/lib/src/selectors/entity-selectors$.spec.ts +++ b/lib/src/selectors/entity-selectors$.spec.ts @@ -1,9 +1,9 @@ import { Action, createSelector, MemoizedSelector, Store } from '@ngrx/store'; +import { Actions } from '@ngrx/effects'; import { BehaviorSubject, Observable, Subject } from 'rxjs'; import { EntityAction, EntityActionFactory } from '../actions/entity-action'; -import { EntityActions } from '../actions/entity-actions'; import { EntityOp } from '../actions/entity-op'; import { EntityCache } from '../reducers/entity-cache'; @@ -72,7 +72,6 @@ describe('EntitySelectors$', () => { let state$: BehaviorSubject<{ entityCache: EntityCache }>; let actions$: Subject; - let entityActions: EntityActions; const nextCacheState = (cache: EntityCache) => state$.next({ entityCache: cache }); @@ -83,7 +82,6 @@ describe('EntitySelectors$', () => { beforeEach(() => { actions$ = new Subject(); - entityActions = new EntityActions(actions$); state$ = new BehaviorSubject({ entityCache: emptyCache }); store = new Store<{ entityCache: EntityCache }>(state$, null, null); @@ -103,7 +101,7 @@ describe('EntitySelectors$', () => { // EntitySelectorFactory factory = new EntitySelectors$Factory( store, - entityActions, + actions$ as any, createEntityCacheSelector(ENTITY_CACHE_NAME) ); diff --git a/lib/src/selectors/entity-selectors$.ts b/lib/src/selectors/entity-selectors$.ts index 1ef7bd69..5bb3c0ee 100644 --- a/lib/src/selectors/entity-selectors$.ts +++ b/lib/src/selectors/entity-selectors$.ts @@ -6,13 +6,15 @@ import { Selector, Store } from '@ngrx/store'; +import { Actions } from '@ngrx/effects'; import { Observable } from 'rxjs'; +import { filter } from 'rxjs/operators'; import { Dictionary } from '../utils/ngrx-entity-models'; import { EntityAction } from '../actions/entity-action'; -import { EntityActions } from '../actions/entity-actions'; import { OP_ERROR } from '../actions/entity-op'; +import { ofEntityType } from '../actions/entity-action-operators'; import { ENTITY_CACHE_SELECTOR_TOKEN, EntityCacheSelector @@ -40,13 +42,13 @@ export interface EntitySelectors$ { readonly entities$: Observable | Store; /** Observable of actions related to this entity type. */ - readonly entityActions$: EntityActions; + readonly entityActions$: Observable; /** Observable of the map of entity keys to entities */ readonly entityMap$: Observable> | Store>; /** Observable of error actions related to this entity type. */ - readonly errors$: EntityActions; + readonly errors$: Observable; /** Observable of the filter pattern applied by the entity collection's filter function */ readonly filter$: Observable | Store; @@ -74,7 +76,7 @@ export class EntitySelectors$Factory { constructor( private store: Store, - private entityActions$: EntityActions, + private actions: Actions, @Inject(ENTITY_CACHE_SELECTOR_TOKEN) private selectEntityCache: EntityCacheSelector ) { @@ -104,9 +106,9 @@ export class EntitySelectors$Factory { selectors$[name$] = this.store.select((selectors)[name]); } }); - selectors$.entityActions$ = this.entityActions$.ofEntityType(entityName); - selectors$.errors$ = selectors$.entityActions$.where((ea: EntityAction) => - ea.op.endsWith(OP_ERROR) + selectors$.entityActions$ = this.actions.pipe(ofEntityType(entityName)); + selectors$.errors$ = selectors$.entityActions$.pipe( + filter((ea: EntityAction) => ea.op.endsWith(OP_ERROR)) ); return selectors$ as S$; } diff --git a/lib/src/selectors/related-entity-selectors.spec.ts b/lib/src/selectors/related-entity-selectors.spec.ts index 0c72c20b..5b4fcdfe 100644 --- a/lib/src/selectors/related-entity-selectors.spec.ts +++ b/lib/src/selectors/related-entity-selectors.spec.ts @@ -6,13 +6,13 @@ import { StoreModule, Store } from '@ngrx/store'; +import { Actions } from '@ngrx/effects'; import { Observable, Subject } from 'rxjs'; import { skip } from 'rxjs/operators'; import { Dictionary, Update } from '../utils/ngrx-entity-models'; import { EntityAction, EntityActionFactory } from '../actions/entity-action'; -import { EntityActions } from '../actions/entity-actions'; import { EntityOp } from '../actions/entity-op'; import { EntityCache } from '../reducers/entity-cache'; @@ -26,7 +26,7 @@ import { import { EntitySelectorsFactory } from '../selectors/entity-selectors'; -import { _NgrxDataModuleWithoutEffects } from '../ngrx-data.module'; +import { NgrxDataModuleWithoutEffects } from '../ngrx-data-without-effects.module'; const entityMetadataMap: EntityMetadataMap = { Battle: {}, @@ -46,10 +46,10 @@ describe('Related-entity Selectors', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [StoreModule.forRoot({}), _NgrxDataModuleWithoutEffects], + imports: [StoreModule.forRoot({}), NgrxDataModuleWithoutEffects], providers: [ // required by NgrxData but not used in these tests - { provide: EntityActions, useValue: null }, + { provide: Actions, useValue: null }, { provide: ENTITY_METADATA_TOKEN, multi: true, diff --git a/lib/src/utils/utilities.ts b/lib/src/utils/utilities.ts index 66989455..b75ff129 100644 --- a/lib/src/utils/utilities.ts +++ b/lib/src/utils/utilities.ts @@ -15,10 +15,10 @@ export function defaultSelectId(entity: any) { * Allows fn with ...rest signature to be called with an array instead of spread * Example: * ``` - * // EntityActions.ofOp + * // See entity-action-operators.ts * const persistOps = [EntityOp.QUERY_ALL, EntityOp.ADD, ...]; - * ofOp(...persistOps) // works - * ofOp(persistOps) // also works + * actions.pipe(ofEntityOp(...persistOps)) // works + * actions.pipe(ofEntityOp(persistOps)) // also works * ``` * */ export function flattenArgs(args?: any[]): T[] { diff --git a/package-lock.json b/package-lock.json index 9212d77f..2e1383c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3479,6 +3479,13 @@ "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", "dev": true }, + "async-each-series": { + "version": "0.1.1", + "resolved": + "https://registry.npmjs.org/async-each-series/-/async-each-series-0.1.1.tgz", + "integrity": "sha1-dhfBkXQB/Yykooqtzj266Yr+tDI=", + "dev": true + }, "async-foreach": { "version": "0.1.3", "resolved": @@ -4129,6 +4136,282 @@ "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", "dev": true }, + "browser-sync": { + "version": "2.24.4", + "resolved": + "https://registry.npmjs.org/browser-sync/-/browser-sync-2.24.4.tgz", + "integrity": + "sha512-qfXv8vQA/Dctub2v44v/vPuvfC4XNd6bn+W5vWZVuhuy6w91lPsdY6qhalT2s2PjnJ3FR6kWq5wkTQgN26eKzA==", + "dev": true, + "requires": { + "browser-sync-ui": "1.0.1", + "bs-recipes": "1.3.4", + "chokidar": "1.7.0", + "connect": "3.5.0", + "connect-history-api-fallback": "1.5.0", + "dev-ip": "1.0.1", + "easy-extender": "2.3.2", + "eazy-logger": "3.0.2", + "etag": "1.8.1", + "fresh": "0.5.2", + "fs-extra": "3.0.1", + "http-proxy": "1.15.2", + "immutable": "3.8.2", + "localtunnel": "1.9.0", + "micromatch": "2.3.11", + "opn": "4.0.2", + "portscanner": "2.1.1", + "qs": "6.2.3", + "raw-body": "2.3.2", + "resp-modifier": "6.0.2", + "rx": "4.1.0", + "serve-index": "1.8.0", + "serve-static": "1.13.2", + "server-destroy": "1.0.1", + "socket.io": "2.0.4", + "ua-parser-js": "0.7.17", + "yargs": "6.4.0" + }, + "dependencies": { + "batch": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.5.3.tgz", + "integrity": "sha1-PzQU84AyF0O/wQQvmoP/HVgk1GQ=", + "dev": true + }, + "camelcase": { + "version": "3.0.0", + "resolved": + "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wrap-ansi": "2.1.0" + } + }, + "connect": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.5.0.tgz", + "integrity": "sha1-s1dSWgtMH1BZnNmD4dnv7qlncZg=", + "dev": true, + "requires": { + "debug": "2.2.0", + "finalhandler": "0.5.0", + "parseurl": "1.3.2", + "utils-merge": "1.0.0" + } + }, + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true, + "requires": { + "ms": "0.7.1" + } + }, + "eventemitter3": { + "version": "1.2.0", + "resolved": + "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz", + "integrity": "sha1-HIaZHYFq0eUEdQ5zh0Ik7PO+xQg=", + "dev": true + }, + "finalhandler": { + "version": "0.5.0", + "resolved": + "https://registry.npmjs.org/finalhandler/-/finalhandler-0.5.0.tgz", + "integrity": "sha1-6VCKvs6bbbqHGmlCodeRG5GRGsc=", + "dev": true, + "requires": { + "debug": "2.2.0", + "escape-html": "1.0.3", + "on-finished": "2.3.0", + "statuses": "1.3.1", + "unpipe": "1.0.0" + } + }, + "fs-extra": { + "version": "3.0.1", + "resolved": + "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", + "integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "3.0.1", + "universalify": "0.1.1" + } + }, + "http-errors": { + "version": "1.5.1", + "resolved": + "https://registry.npmjs.org/http-errors/-/http-errors-1.5.1.tgz", + "integrity": "sha1-eIwNLB3iyBuebowBhDtrl+uSB1A=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "setprototypeof": "1.0.2", + "statuses": "1.3.1" + } + }, + "http-proxy": { + "version": "1.15.2", + "resolved": + "https://registry.npmjs.org/http-proxy/-/http-proxy-1.15.2.tgz", + "integrity": "sha1-ZC/cr/5S00SNK9o7AHnpQJBk2jE=", + "dev": true, + "requires": { + "eventemitter3": "1.2.0", + "requires-port": "1.0.0" + } + }, + "jsonfile": { + "version": "3.0.1", + "resolved": + "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", + "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11" + } + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + }, + "opn": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/opn/-/opn-4.0.2.tgz", + "integrity": "sha1-erwi5kTf9jsKltWrfyeQwPAavJU=", + "dev": true, + "requires": { + "object-assign": "4.1.1", + "pinkie-promise": "2.0.1" + } + }, + "qs": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.2.3.tgz", + "integrity": "sha1-HPyyXBCpsrSDBT/zn138kjOQjP4=", + "dev": true + }, + "rx": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", + "integrity": "sha1-pfE/957zt0D+MKqAP7CfmIBdR4I=", + "dev": true + }, + "serve-index": { + "version": "1.8.0", + "resolved": + "https://registry.npmjs.org/serve-index/-/serve-index-1.8.0.tgz", + "integrity": "sha1-fF2WwT+xMRAfk8HFd0+FFqHnjTs=", + "dev": true, + "requires": { + "accepts": "1.3.5", + "batch": "0.5.3", + "debug": "2.2.0", + "escape-html": "1.0.3", + "http-errors": "1.5.1", + "mime-types": "2.1.18", + "parseurl": "1.3.2" + } + }, + "setprototypeof": { + "version": "1.0.2", + "resolved": + "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.2.tgz", + "integrity": "sha1-gaVSFB7BBLiOic44MQOtXGZWTQg=", + "dev": true + }, + "statuses": { + "version": "1.3.1", + "resolved": + "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=", + "dev": true + }, + "utils-merge": { + "version": "1.0.0", + "resolved": + "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz", + "integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg=", + "dev": true + }, + "window-size": { + "version": "0.2.0", + "resolved": + "https://registry.npmjs.org/window-size/-/window-size-0.2.0.tgz", + "integrity": "sha1-tDFbtCFKPXBY6+7okuE/ok2YsHU=", + "dev": true + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, + "yargs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.4.0.tgz", + "integrity": "sha1-gW4ahm1VmMzzTlWW3c4i2S2kkNQ=", + "dev": true, + "requires": { + "camelcase": "3.0.0", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "get-caller-file": "1.0.2", + "os-locale": "1.4.0", + "read-pkg-up": "1.0.1", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "1.0.2", + "which-module": "1.0.0", + "window-size": "0.2.0", + "y18n": "3.2.1", + "yargs-parser": "4.2.1" + } + }, + "yargs-parser": { + "version": "4.2.1", + "resolved": + "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", + "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=", + "dev": true, + "requires": { + "camelcase": "3.0.0" + } + } + } + }, + "browser-sync-ui": { + "version": "1.0.1", + "resolved": + "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-1.0.1.tgz", + "integrity": + "sha512-RIxmwVVcUFhRd1zxp7m2FfLnXHf59x4Gtj8HFwTA//3VgYI3AKkaQAuDL8KDJnE59XqCshxZa13JYuIWtZlKQg==", + "dev": true, + "requires": { + "async-each-series": "0.1.1", + "connect-history-api-fallback": "1.5.0", + "immutable": "3.8.2", + "server-destroy": "1.0.1", + "socket.io-client": "2.0.4", + "stream-throttle": "0.1.3" + } + }, "browserify-aes": { "version": "1.2.0", "resolved": @@ -4221,6 +4504,13 @@ "electron-to-chromium": "1.3.45" } }, + "bs-recipes": { + "version": "1.3.4", + "resolved": + "https://registry.npmjs.org/bs-recipes/-/bs-recipes-1.3.4.tgz", + "integrity": "sha1-DS1NSKcYyMBEdp/cT4lZLci2lYU=", + "dev": true + }, "bson": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.6.tgz", @@ -5020,6 +5310,16 @@ "integrity": "sha1-sGhzk0vF40T+9hGhlqb6rgruAVo=", "dev": true }, + "connect-logger": { + "version": "0.0.1", + "resolved": + "https://registry.npmjs.org/connect-logger/-/connect-logger-0.0.1.tgz", + "integrity": "sha1-TZmZeKHSC7RgjnzUNNdBZSJVF0s=", + "dev": true, + "requires": { + "moment": "2.22.1" + } + }, "console-browserify": { "version": "1.1.0", "resolved": @@ -5662,6 +5962,12 @@ "integrity": "sha1-ogM8CcyOFY03dI+951B4Mr1s4Sc=", "dev": true }, + "dev-ip": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dev-ip/-/dev-ip-1.0.1.tgz", + "integrity": "sha1-p2o+0YVb56ASu4rBbLgPPADcKPA=", + "dev": true + }, "di": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", @@ -5861,6 +6167,34 @@ "stream-shift": "1.0.0" } }, + "easy-extender": { + "version": "2.3.2", + "resolved": + "https://registry.npmjs.org/easy-extender/-/easy-extender-2.3.2.tgz", + "integrity": "sha1-PTJI/r4rFZYHMW2PnPSRwWZIIh0=", + "dev": true, + "requires": { + "lodash": "3.10.1" + }, + "dependencies": { + "lodash": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "dev": true + } + } + }, + "eazy-logger": { + "version": "3.0.2", + "resolved": + "https://registry.npmjs.org/eazy-logger/-/eazy-logger-3.0.2.tgz", + "integrity": "sha1-oyWqXlPROiIliJsqxBE7K5Y29Pw=", + "dev": true, + "requires": { + "tfunk": "3.1.0" + } + }, "ecc-jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", @@ -8961,6 +9295,12 @@ "dev": true, "optional": true }, + "immutable": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", + "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=", + "dev": true + }, "import-lazy": { "version": "2.1.0", "resolved": @@ -9366,6 +9706,17 @@ "kind-of": "3.2.2" } }, + "is-number-like": { + "version": "1.0.8", + "resolved": + "https://registry.npmjs.org/is-number-like/-/is-number-like-1.0.8.tgz", + "integrity": + "sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==", + "dev": true, + "requires": { + "lodash.isfinite": "3.3.2" + } + }, "is-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", @@ -10378,6 +10729,36 @@ "ejs": "2.5.9" } }, + "limiter": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.3.tgz", + "integrity": + "sha512-zrycnIMsLw/3ZxTbW7HCez56rcFGecWTx5OZNplzcXUUmJLmoYArC6qdJzmAN5BWiNXGcpjhF9RQ1HSv5zebEw==", + "dev": true + }, + "lite-server": { + "version": "2.3.0", + "resolved": + "https://registry.npmjs.org/lite-server/-/lite-server-2.3.0.tgz", + "integrity": "sha1-W0zI9dX9SDYQVICrKsSKOg3isMg=", + "dev": true, + "requires": { + "browser-sync": "2.24.4", + "connect-history-api-fallback": "1.5.0", + "connect-logger": "0.0.1", + "lodash": "4.17.5", + "minimist": "1.2.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": + "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, "load-json-file": { "version": "1.1.0", "resolved": @@ -10419,6 +10800,96 @@ "json5": "0.5.1" } }, + "localtunnel": { + "version": "1.9.0", + "resolved": + "https://registry.npmjs.org/localtunnel/-/localtunnel-1.9.0.tgz", + "integrity": + "sha512-wCIiIHJ8kKIcWkTQE3m1VRABvsH2ZuOkiOpZUofUCf6Q42v3VIZ+Q0YfX1Z4sYDRj0muiKL1bLvz1FeoxsPO0w==", + "dev": true, + "requires": { + "axios": "0.17.1", + "debug": "2.6.8", + "openurl": "1.1.1", + "yargs": "6.6.0" + }, + "dependencies": { + "axios": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.17.1.tgz", + "integrity": "sha1-LY4+XQvb1zJ/kbyBT1xXZg+Bgk0=", + "dev": true, + "requires": { + "follow-redirects": "1.4.1", + "is-buffer": "1.1.6" + } + }, + "camelcase": { + "version": "3.0.0", + "resolved": + "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "dev": true, + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wrap-ansi": "2.1.0" + } + }, + "debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, + "yargs": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", + "integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=", + "dev": true, + "requires": { + "camelcase": "3.0.0", + "cliui": "3.2.0", + "decamelize": "1.2.0", + "get-caller-file": "1.0.2", + "os-locale": "1.4.0", + "read-pkg-up": "1.0.1", + "require-directory": "2.1.1", + "require-main-filename": "1.0.1", + "set-blocking": "2.0.0", + "string-width": "1.0.2", + "which-module": "1.0.0", + "y18n": "3.2.1", + "yargs-parser": "4.2.1" + } + }, + "yargs-parser": { + "version": "4.2.1", + "resolved": + "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", + "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=", + "dev": true, + "requires": { + "camelcase": "3.0.0" + } + } + } + }, "locate-path": { "version": "2.0.0", "resolved": @@ -10470,6 +10941,13 @@ "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" }, + "lodash.isfinite": { + "version": "3.3.2", + "resolved": + "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz", + "integrity": "sha1-+4m2WpqAKBgz8LdHizpRBPiY67M=", + "dev": true + }, "lodash.mergewith": { "version": "4.6.1", "resolved": @@ -11123,6 +11601,13 @@ } } }, + "moment": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.1.tgz", + "integrity": + "sha512-shJkRTSebXvsVqk56I+lkb2latjBs8I+pc2TzWc545y2iFnSjm7Wg0QMh+ZWcdSLQyGEau5jI8ocnmkyTgr9YQ==", + "dev": true + }, "mongodb": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.0.7.tgz", @@ -12105,6 +12590,13 @@ "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=", "dev": true }, + "object-path": { + "version": "0.9.2", + "resolved": + "https://registry.npmjs.org/object-path/-/object-path-0.9.2.tgz", + "integrity": "sha1-D9mnT8X60a45aLWGvaXGMr1sBaU=", + "dev": true + }, "object-visit": { "version": "1.0.1", "resolved": @@ -12226,6 +12718,12 @@ "integrity": "sha1-XG2ixdflgx6P+jlklQ+NZnSskLg=", "dev": true }, + "openurl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/openurl/-/openurl-1.1.1.tgz", + "integrity": "sha1-OHW0sO96UsFW8NtB1GCduw+Us4c=", + "dev": true + }, "opn": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/opn/-/opn-5.1.0.tgz", @@ -12709,6 +13207,17 @@ "mkdirp": "0.5.1" } }, + "portscanner": { + "version": "2.1.1", + "resolved": + "https://registry.npmjs.org/portscanner/-/portscanner-2.1.1.tgz", + "integrity": "sha1-6rtAnk3iSVD1oqUW01rnaTQ/u5Y=", + "dev": true, + "requires": { + "async": "1.5.2", + "is-number-like": "1.0.8" + } + }, "posix-character-classes": { "version": "0.1.1", "resolved": @@ -13849,6 +14358,17 @@ "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", "dev": true }, + "resp-modifier": { + "version": "6.0.2", + "resolved": + "https://registry.npmjs.org/resp-modifier/-/resp-modifier-6.0.2.tgz", + "integrity": "sha1-sSTeXE+6/LpUH0j/pzlw9KpFa08=", + "dev": true, + "requires": { + "debug": "2.6.9", + "minimatch": "3.0.4" + } + }, "restore-cursor": { "version": "2.0.0", "resolved": @@ -14297,6 +14817,13 @@ "send": "0.16.2" } }, + "server-destroy": { + "version": "1.0.1", + "resolved": + "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", + "integrity": "sha1-8Tv5KOQrnD55OD5hzDmYtdFObN0=", + "dev": true + }, "set-blocking": { "version": "2.0.0", "resolved": @@ -15647,6 +16174,17 @@ "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", "dev": true }, + "stream-throttle": { + "version": "0.1.3", + "resolved": + "https://registry.npmjs.org/stream-throttle/-/stream-throttle-0.1.3.tgz", + "integrity": "sha1-rdV8jXzHOoFjDTHNVdOWHPr7qcM=", + "dev": true, + "requires": { + "commander": "2.14.1", + "limiter": "1.1.3" + } + }, "streamroller": { "version": "0.7.0", "resolved": @@ -15917,6 +16455,45 @@ "execa": "0.7.0" } }, + "tfunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tfunk/-/tfunk-3.1.0.tgz", + "integrity": "sha1-OORBT8ZJd9h6/apy+sttKfgve1s=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "object-path": "0.9.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": + "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": + "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, "then-fs": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/then-fs/-/then-fs-2.0.0.tgz", @@ -16358,6 +16935,14 @@ "sha512-p5TCYZDAO0m4G344hD+wx/LATebLWZNkkh2asWUFqSsD2OrDNhbAHuSjobrmsUmdzjJjEeZVU9g1h3O6vpstnw==", "dev": true }, + "ua-parser-js": { + "version": "0.7.17", + "resolved": + "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.17.tgz", + "integrity": + "sha512-uRdSdu1oA1rncCQL7sCj8vSyZkgtL7faaw9Tc9rZ3mGgraQ7+Pdx7w5mnOSF3gw9ZNG6oc+KXfkon3bKuROm0g==", + "dev": true + }, "uglify-js": { "version": "2.8.29", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", diff --git a/package.json b/package.json index da284b4f..0d5fb727 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "start": "ng serve -o", "build": "ng build", "build-prod": "ng build --prod --source-map", + "start-lite": "lite-server --baseDir=\"dist/app\"", "build-lib": "ng-packagr -p lib", "build-all": "rm -rf dist && npm run build-setup && npm run build-prod", "build-setup": @@ -82,6 +83,7 @@ "karma-coverage-istanbul-reporter": "^1.4.2", "karma-jasmine": "~1.1.0", "karma-jasmine-html-reporter": "^0.2.2", + "lite-server": "^2.3.0", "ng-packagr": "^3.0.0-rc.5", "prettier": "1.12.1", "pretty-quick": "^1.4.1", diff --git a/src/app/store/entity/ngrx-data-toast.service.ts b/src/app/store/entity/ngrx-data-toast.service.ts index d220d93e..0ea18e46 100644 --- a/src/app/store/entity/ngrx-data-toast.service.ts +++ b/src/app/store/entity/ngrx-data-toast.service.ts @@ -1,13 +1,22 @@ import { Injectable } from '@angular/core'; -import { EntityActions, OP_ERROR, OP_SUCCESS } from 'ngrx-data'; +import { Actions } from '@ngrx/effects'; + +import { filter } from 'rxjs/operators'; +import { EntityAction, ofEntityOp, OP_ERROR, OP_SUCCESS } from 'ngrx-data'; import { ToastService } from '../../core/toast.service'; /** Report ngrx-data success/error actions as toast messages **/ @Injectable() export class NgrxDataToastService { - constructor(actions$: EntityActions, toast: ToastService) { + constructor(actions$: Actions, toast: ToastService) { actions$ - .where(ea => ea.op.endsWith(OP_SUCCESS) || ea.op.endsWith(OP_ERROR)) + .pipe( + ofEntityOp(), + filter( + (ea: EntityAction) => + ea.op.endsWith(OP_SUCCESS) || ea.op.endsWith(OP_ERROR) + ) + ) // this service never dies so no need to unsubscribe .subscribe(action => toast.openSnackBar(`${action.entityName} action`, action.op) diff --git a/src/app/villains/villain-editor/villain-editor.component.ts b/src/app/villains/villain-editor/villain-editor.component.ts index 05ebe631..342a972b 100644 --- a/src/app/villains/villain-editor/villain-editor.component.ts +++ b/src/app/villains/villain-editor/villain-editor.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { EntityOp } from 'ngrx-data'; +import { EntityOp, ofEntityOp } from 'ngrx-data'; import { combineLatest, Observable, Subject } from 'rxjs'; import { delay, map, shareReplay, startWith, takeUntil } from 'rxjs/operators'; @@ -45,15 +45,14 @@ export class VillainEditorComponent implements OnInit, OnDestroy { shareReplay(1) ); - this.error$ = this.villainsService.errors$ - .ofOp(EntityOp.QUERY_BY_KEY_ERROR) - .pipe( - map(errorAction => errorAction.payload.error.message), - // delay guards against `ExpressionChangedAfterItHasBeenCheckedError` - delay(1), - // startWith(''), // prime it for loading$ - takeUntil(this.destroy$) - ); + this.error$ = this.villainsService.errors$.pipe( + ofEntityOp(EntityOp.QUERY_BY_KEY_ERROR), + map(errorAction => errorAction.payload.error.message), + // delay guards against `ExpressionChangedAfterItHasBeenCheckedError` + delay(1), + // startWith(''), // prime it for loading$ + takeUntil(this.destroy$) + ); this.loading$ = combineLatest(this.error$, this.villain$).pipe( map(([errorMsg, villain]) => !villain && !errorMsg)