-
Notifications
You must be signed in to change notification settings - Fork 133
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: Replace EntityActions with EntityAction operators - beta.6 (#157)
Removes the `EntityActions` class and replaces it with two _EntityAction_ operators, `ofEntityOp` and `ofEntityType`, because RxJS deprecates sub-classing of `Observable`. This is a *breaking change* for anyone who relied on `EntityActions`. See the ChangeLog for details.
- Loading branch information
Showing
24 changed files
with
1,226 additions
and
665 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Action>; | ||
|
||
const testActions = { | ||
foo: <Action>{ 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: <Action>(<any>{ type: 'Bar', payload: 'bar' }) | ||
}; | ||
|
||
function dispatchTestActions() { | ||
Object.keys(testActions).forEach(a => actions.next((<any>testActions)[a])); | ||
} | ||
|
||
beforeEach(() => { | ||
actions = new Subject<Action>(); | ||
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); | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<T extends EntityAction>( | ||
allowedOps: string[] | EntityOp[] | ||
): OperatorFunction<EntityAction, T>; | ||
export function ofEntityOp<T extends EntityAction>( | ||
...allowedOps: (string | EntityOp)[] | ||
): OperatorFunction<EntityAction, T>; | ||
export function ofEntityOp<T extends EntityAction>( | ||
...allowedEntityOps: any[] | ||
): OperatorFunction<EntityAction, T> { | ||
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<T extends EntityAction>( | ||
allowedEntityNames?: string[] | ||
): OperatorFunction<EntityAction, T>; | ||
export function ofEntityType<T extends EntityAction>( | ||
...allowedEntityNames: string[] | ||
): OperatorFunction<EntityAction, T>; | ||
export function ofEntityType<T extends EntityAction>( | ||
...allowedEntityNames: any[] | ||
): OperatorFunction<EntityAction, T> { | ||
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) | ||
); | ||
} | ||
} |
Oops, something went wrong.