Skip to content

Commit

Permalink
fix: clear & custom state properties and custom state typing
Browse files Browse the repository at this point in the history
 * Updated clear reduction to spread existing state first to preserve custom state
 * Updated typings for all utility functions and types to ensure extra state types work with TS 3.x

Resolves: #85, #86, #88
  • Loading branch information
Jon Rista committed Feb 9, 2020
1 parent 6b5096c commit 685f348
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 50 deletions.
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
<a name="0.4.1"></a>

# [0.4.1](https://github.com/briebug/ngrx-auto-entity/compare/0.4.0...0.4.1) Beta (2020-02-09)

Introduces the ability to delete entities just by their key, or many entities by their keys. This allows
the deletion of entities without actually having the entity objects on hand.

Also resolves an issue with clearing state, which would also clear custom developer-defined extra state
included alongside auto-entity managed state.

### Features
- **actions**:** Add `DeleteByKey`, `DeleteManyByKeys` and related result actions (#85)
- **service:** Add support for `deleteByKey` and `deleteManyByKeys` methods in entity services (#85)
- **reducer:** Handles new delete by keys result actions to rmeove deleted entities and update deleting flags/timestamps (#85)
- **decorators:** Add support for new delete by keys actions in effect exclusion of `@Entity` decorator (#85)
- **facades:** Add `deleteByKey` and `deleteManyByKeys` methods to generated facades (#85)
- **effects:** Add operators and effects to handle delete by keys actions (#85)

### Bug Fix
- **reducer:** No longer removes custom state when clearing auto-entity managed state with `Clear` action (#86)
- **util:** Fix `buildState` and `buildFeatureState` and related types to support custom properties in extra state under TS 3.x (#88)


<a name="0.4.0"></a>

# [0.4.0](https://github.com/briebug/ngrx-auto-entity/compare/0.3.1...0.4.0) Beta (2020-01-13)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"start": "concurrently --prefix-colors white.bgBlue,white.bgRed --names angular,json-server --kill-others \"npm run serve-proxy\" \"npm run json-server\"",
"serve": "ng serve --source-map --vendorSourceMap",
"serve:prod": "ng serve --prod --source-map --vendorSourceMap",
"serve:test": "ng serve test-app --prod --source-map --vendorSourceMap",
"serve:test": "ng serve test-app --source-map --vendorSourceMap",
"serve:test:prod": "ng serve test-app --source-map --vendorSourceMap",
"serve-proxy": "ng serve --prod --source-map --vendorSourceMap --proxy-config proxy.conf.json",
"serve-proxy:test": "ng serve test-app --source-map --vendorSourceMap --proxy-config proxy.conf.json",
Expand Down
2 changes: 1 addition & 1 deletion projects/ngrx-auto-entity/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "0.4.0",
"version": "0.4.1",
"name": "@briebug/ngrx-auto-entity",
"description": "Automatic Entity State and Facades for NgRx. Simplifying reactive state!",
"keywords": [
Expand Down
49 changes: 49 additions & 0 deletions projects/ngrx-auto-entity/src/lib/reducer.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'jest-extended';

import {
Clear,
CreateManySuccess,
CreateSuccess,
DeleteByKeySuccess,
Expand Down Expand Up @@ -1080,5 +1081,53 @@ describe('NgRX Auto-Entity: Reducer', () => {
expect(newState.testEntity.currentEntitiesKeys).toBeUndefined();
});
// endregion

// region Clear
it('should reduce Clear and reset auto-entity managed state to default, empty state', () => {
const state = {
testEntity: {
entities: {
2: { identity: 2 },
1: { identity: 1 },
3: { identity: 3 }
},
ids: [1, 2, 3],
currentEntitiesKeys: [1, 2, 3]
}
};
const rootReducer = jest.fn();
const metaReducer = autoEntityMetaReducer(rootReducer);
const newState = metaReducer(state, new Clear(TestEntity));

expect(newState.testEntity).toEqual({
entities: {},
ids: []
});
});

it('should reduce Clear and leave custom user-defined properties alone while clearning auto-entity managed state', () => {
const state = {
testEntity: {
entities: {
2: { identity: 2 },
1: { identity: 1 },
3: { identity: 3 }
},
ids: [1, 2, 3],
currentEntitiesKeys: [1, 2, 3],
customProperty: 'hello'
}
};
const rootReducer = jest.fn();
const metaReducer = autoEntityMetaReducer(rootReducer);
const newState = metaReducer(state, new Clear(TestEntity));

expect(newState.testEntity).toEqual({
entities: {},
ids: [],
customProperty: 'hello'
});
});
// endregion
});
});
5 changes: 5 additions & 0 deletions projects/ngrx-auto-entity/src/lib/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,11 @@ export function autoEntityReducer(reducer: ActionReducer<any>, state, action: En

case EntityActionTypes.Clear: {
const newState = {
// If the developer has included their own extra state properties with buildState(Entity, { /* custom */ })
// then we don't want to mess with it. We want to leave any custom developer state as-is!
// Spread in the current state to ensure we KEEP custom developer-defined extra state properties:
...entityState,
// Now reset the auto-entity managed properties to their default states:
entities: {},
ids: [],
currentEntityKey: undefined,
Expand Down
87 changes: 45 additions & 42 deletions projects/ngrx-auto-entity/src/lib/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,12 @@ export interface IEntityState<TModel> {
/**
* Structure of the model state built by the buildState() function
*/
export interface IModelState<TParentState, TState, TModel> {
initialState: TState;
export interface IModelState<TParentState, TState, TModel, TExtra> {
initialState: TState & TExtra;
selectors: ISelectorMap<TParentState, TModel>;
reducer: (state: TState) => IEntityState<TModel>;
reducer: (state: TState & TExtra) => IEntityState<TModel> & TExtra;
facade: new (type: new () => TModel, store: Store<any>) => IEntityFacade<TModel>;
entityState: ((state: TParentState) => TState) | (MemoizedSelector<object, any>);
entityState: ((state: TParentState) => TState & TExtra) | (MemoizedSelector<object, any>);
}

/**
Expand Down Expand Up @@ -108,100 +108,103 @@ export interface ISelectorMap<TParentState, TModel> {
selectDeletedAt: MemoizedSelector<object | TParentState, Date>;
}

export const buildSelectorMap = <TParentState, TState extends IEntityState<TModel>, TModel>(
getState: Selector<TParentState, TState> | MemoizedSelector<object | TParentState, TState>,
export const buildSelectorMap = <TParentState, TState extends IEntityState<TModel>, TModel, TExtra>(
getState: Selector<TParentState, TState & TExtra> | MemoizedSelector<object | TParentState, TState & TExtra>,
compareFn?: (a, b) => number
): ISelectorMap<TParentState, TModel> =>
({
selectAll: createSelector(
getState,
(state: TState): TModel[] =>
(state: TState & TExtra): TModel[] =>
!state || !state.ids || !state.entities ? [] : state.ids.map(id => state.entities[id])
),
selectAllSorted: createSelector(
getState,
(state: TState): TModel[] =>
(state: TState & TExtra): TModel[] =>
(!state || !state.ids || !state.entities ? [] : state.ids.map(id => state.entities[id])).sort(compareFn)
),
selectEntities: createSelector(
getState,
(state: TState): IEntityDictionary<TModel> => (!state || !state.entities ? {} : state.entities)
(state: TState & TExtra): IEntityDictionary<TModel> => (!state || !state.entities ? {} : state.entities)
),
selectIds: createSelector(
getState,
(state: TState): EntityIdentity[] => (!state || !state.ids ? [] : state.ids)
(state: TState & TExtra): EntityIdentity[] => (!state || !state.ids ? [] : state.ids)
),
selectTotal: createSelector(
getState,
(state: TState): number => (!state || !state.ids ? 0 : state.ids.length)
(state: TState & TExtra): number => (!state || !state.ids ? 0 : state.ids.length)
),
selectCurrentEntity: createSelector(
getState,
(state: TState): TModel =>
(state: TState & TExtra): TModel =>
!state || !state.entities || !state.currentEntityKey ? null : state.entities[state.currentEntityKey]
),
selectCurrentEntityKey: createSelector(
getState,
(state: TState): EntityIdentity => (!state ? null : state.currentEntityKey)
(state: TState & TExtra): EntityIdentity => (!state ? null : state.currentEntityKey)
),
selectCurrentEntities: createSelector(
getState,
(state: TState): TModel[] =>
!state || !state.currentEntitiesKeys || !state.entities
(state: TState & TExtra): TModel[] =>
// prettier-ignore
(!state || !state.currentEntitiesKeys || !state.entities)
? []
: state.currentEntitiesKeys.map(key => state.entities[key])
),
selectCurrentEntitiesKeys: createSelector(
getState,
(state: TState): EntityIdentity[] => (!state || !state.currentEntitiesKeys ? [] : state.currentEntitiesKeys)
(state: TState & TExtra): EntityIdentity[] =>
// prettier-ignore
(!state || !state.currentEntitiesKeys) ? [] : state.currentEntitiesKeys
),
selectEditedEntity: createSelector(
getState,
(state: TState): Partial<TModel> => (!state ? null : state.editedEntity)
(state: TState & TExtra): Partial<TModel> => (!state ? null : state.editedEntity)
),
selectIsDirty: createSelector(
getState,
(state: TState): boolean => (!state ? false : !!state.isDirty)
(state: TState & TExtra): boolean => (!state ? false : !!state.isDirty)
),
selectCurrentPage: createSelector(
getState,
(state: TState): Page => (!state ? null : state.currentPage)
(state: TState & TExtra): Page => (!state ? null : state.currentPage)
),
selectCurrentRange: createSelector(
getState,
(state: TState): Range => (!state ? null : state.currentRange)
(state: TState & TExtra): Range => (!state ? null : state.currentRange)
),
selectTotalPageable: createSelector(
getState,
(state: TState): number => (!state ? 0 : state.totalPageableCount)
(state: TState & TExtra): number => (!state ? 0 : state.totalPageableCount)
),
selectIsLoading: createSelector(
getState,
(state: TState): boolean => (!state ? false : !!state.isLoading)
(state: TState & TExtra): boolean => (!state ? false : !!state.isLoading)
),
selectIsSaving: createSelector(
getState,
(state: TState): boolean => (!state ? false : !!state.isSaving)
(state: TState & TExtra): boolean => (!state ? false : !!state.isSaving)
),
selectIsDeleting: createSelector(
getState,
(state: TState): boolean => (!state ? false : !!state.isDeleting)
(state: TState & TExtra): boolean => (!state ? false : !!state.isDeleting)
),
selectLoadedAt: createSelector(
getState,
(state: TState): Date => (!state ? null : state.loadedAt)
(state: TState & TExtra): Date => (!state ? null : state.loadedAt)
),
selectSavedAt: createSelector(
getState,
(state: TState): Date => (!state ? null : state.savedAt)
(state: TState & TExtra): Date => (!state ? null : state.savedAt)
),
selectCreatedAt: createSelector(
getState,
(state: TState): Date => (!state ? null : state.createdAt)
(state: TState & TExtra): Date => (!state ? null : state.createdAt)
),
selectDeletedAt: createSelector(
getState,
(state: TState): Date => (!state ? null : state.deletedAt)
(state: TState & TExtra): Date => (!state ? null : state.deletedAt)
)
} as ISelectorMap<TParentState, TModel>);

Expand Down Expand Up @@ -525,10 +528,10 @@ export const buildFacade = <TModel, TParentState>(selectors: ISelectorMap<TParen
* @param type - the entity class
* @param extraInitialState - the (optional) initial state
*/
export const buildState = <TState extends IEntityState<TModel>, TParentState, TModel>(
export const buildState = <TState extends IEntityState<TModel>, TParentState, TModel, TExtra>(
type: IModelClass<TModel>,
extraInitialState?: any
): IModelState<TParentState, TState, TModel> => {
extraInitialState?: TExtra
): IModelState<TParentState, TState, TModel, TExtra> => {
const instance = new type();
const opts = type[ENTITY_OPTS_PROP] || {
modelName: instance.constructor.name,
Expand All @@ -537,7 +540,7 @@ export const buildState = <TState extends IEntityState<TModel>, TParentState, TM
const modelName = camelCase(opts.modelName);

console.log(`NGRX-AE: Building entity state for: ${modelName}; constructor name: ${instance.constructor.name}`);
const getState = (state: TParentState): TState => {
const getState = (state: TParentState): TState & TExtra => {
const modelState = state[modelName];
if (!modelState) {
console.error(`NGRX-AE: State for model ${modelName} could not be found!`);
Expand All @@ -552,16 +555,16 @@ export const buildState = <TState extends IEntityState<TModel>, TParentState, TM
entities: {},
ids: [],
...extraInitialState
} as TState;
} as TState & TExtra;

const selectors = buildSelectorMap<TParentState, TState, TModel>(getState, opts.comparer);
const selectors = buildSelectorMap<TParentState, TState, TModel, TExtra>(getState, opts.comparer);
const facade = buildFacade<TModel, TParentState>(selectors);
const reducer = (state = initialState): IEntityState<TModel> => {
const reducer = (state = initialState): IEntityState<TModel> & TExtra => {
// tslint:disable-line
return state;
};

const entityState = getState as (state: TParentState) => TState;
const entityState = getState as (state: TParentState) => TState & TExtra;

return {
initialState,
Expand All @@ -581,12 +584,12 @@ export const FEATURE_AFFINITY = '__ngrxae_feature_affinity';
* @param selectParentState a selector for the entity's parent state
* @param extraInitialState the (optional) initial feature state
*/
export const buildFeatureState = <TState extends IEntityState<TModel>, TParentState, TModel>(
export const buildFeatureState = <TState extends IEntityState<TModel>, TParentState, TModel, TExtra>(
type: IModelClass<TModel>,
featureStateName: NonNullable<string>,
selectParentState: MemoizedSelector<object, TParentState>,
extraInitialState?: any
): IModelState<TParentState, TState, TModel> => {
extraInitialState?: TExtra
): IModelState<TParentState, TState, TModel, TExtra> => {
const instance = new type();
const opts = type[ENTITY_OPTS_PROP] || {
modelName: instance.constructor.name
Expand Down Expand Up @@ -619,11 +622,11 @@ export const buildFeatureState = <TState extends IEntityState<TModel>, TParentSt
entities: {},
ids: [],
...extraInitialState
} as TState;
} as TState & TExtra;

const selectors = buildSelectorMap<TParentState, TState, TModel>(selectState);
const selectors = buildSelectorMap<TParentState, TState, TModel, TExtra>(selectState);
const facade = buildFacade<TModel, TParentState>(selectors);
const reducer = (state = initialState): IEntityState<TModel> => {
const reducer = (state = initialState): IEntityState<TModel> & TExtra => {
// tslint:disable-line
return state;
};
Expand Down
1 change: 1 addition & 0 deletions projects/test-app/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ export class AppComponent {

constructor(public customers: CustomerFacade) {
customers.loadAll();
setTimeout(() => customers.clear(), 3000);
}
}
6 changes: 2 additions & 4 deletions projects/test-app/src/app/state/app.state.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { IEntityState } from '@briebug/ngrx-auto-entity';
import { ActionReducerMap, MetaReducer } from '@ngrx/store';
import { storeFreeze } from 'ngrx-store-freeze';

import { customerReducer } from 'state/customer.state';
import { environment } from '../../environments/environment';
import { Customer } from '../models/customer.model';
import { customerReducer, ICustomerState } from './customer.state';

export interface IAppState {
customer: IEntityState<Customer>;
customer: ICustomerState;
}

export type AppState = IAppState;
Expand Down
11 changes: 9 additions & 2 deletions projects/test-app/src/app/state/customer.state.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { buildState, IEntityState } from '@briebug/ngrx-auto-entity';
import { Customer } from '../models/customer.model';

export const { initialState, facade: CustomerFacadeBase } = buildState(Customer);
export function customerReducer(state = initialState): IEntityState<Customer> {
export const { initialState, facade: CustomerFacadeBase } = buildState(Customer, {
customProperty: 'hello'
});

export interface ICustomerState extends IEntityState<Customer> {
customProperty: string;
}

export function customerReducer(state = initialState): ICustomerState {
return state;
}

0 comments on commit 685f348

Please sign in to comment.