Skip to content

Commit

Permalink
fix: Replace EntityActions with EntityAction operators - beta.6 (#157)
Browse files Browse the repository at this point in the history
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
wardbell authored May 25, 2018
1 parent 6283987 commit 05cdc04
Show file tree
Hide file tree
Showing 24 changed files with 1,226 additions and 665 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ Please look there.
**_This_** Changelog covers changes to the repository and the demo applications.

<a name="0.6.1">
# 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).

<a name="0.6.0"></a>
Expand Down
57 changes: 56 additions & 1 deletion lib/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,60 @@
# Angular ngrx-data library ChangeLog

<a name="6.0.1-beta.6"></a>

# 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 {...}
```
<a name="6.0.1-beta.5"></a>
# 6.0.1-beta.5 (2018-05-23)
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
162 changes: 162 additions & 0 deletions lib/src/actions/entity-action-operators.spec.ts
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);
}
});
80 changes: 80 additions & 0 deletions lib/src/actions/entity-action-operators.ts
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)
);
}
}
Loading

0 comments on commit 05cdc04

Please sign in to comment.