Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(Entity): updateOne/updateMany should not change ids state on existing entity #581

Merged
merged 1 commit into from
Nov 28, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions modules/entity/spec/sorted_state_adapter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,24 @@ describe('Sorted State Adapter', () => {
expect(withUpdates).toBe(state);
});

it('should not change ids state if you attempt to update an entity that does not impact sorting', () => {
const withAll = adapter.addAll(
[TheGreatGatsby, AClockworkOrange, AnimalFarm],
state
);
const changes = { title: 'The Great Gatsby II' };

const withUpdates = adapter.updateOne(
{
id: TheGreatGatsby.id,
changes,
},
withAll
);

expect(withAll.ids).toBe(withUpdates.ids);
});

it('should let you update the id of entity', () => {
const withOne = adapter.addOne(TheGreatGatsby, state);
const changes = { id: 'A New Id' };
Expand All @@ -176,6 +194,34 @@ describe('Sorted State Adapter', () => {
});
});

it('should resort correctly if same id but sort key update', () => {
const withAll = adapter.addAll(
[TheGreatGatsby, AnimalFarm, AClockworkOrange],
state
);
const changes = { title: 'A New Hope' };

const withUpdates = adapter.updateOne(
{
id: TheGreatGatsby.id,
changes,
},
withAll
);

expect(withUpdates).toEqual({
ids: [AClockworkOrange.id, TheGreatGatsby.id, AnimalFarm.id],
entities: {
[AClockworkOrange.id]: AClockworkOrange,
[TheGreatGatsby.id]: {
...TheGreatGatsby,
...changes,
},
[AnimalFarm.id]: AnimalFarm,
},
});
});

it('should resort correctly if the id and sort key update', () => {
const withOne = adapter.addAll(
[TheGreatGatsby, AnimalFarm, AClockworkOrange],
Expand Down
15 changes: 15 additions & 0 deletions modules/entity/spec/unsorted_state_adapter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,21 @@ describe('Unsorted State Adapter', () => {
expect(withUpdates).toBe(state);
});

it('should not change ids state if you attempt to update an entity that has already been added', () => {
const withOne = adapter.addOne(TheGreatGatsby, state);
const changes = { title: 'A New Hope' };

const withUpdates = adapter.updateOne(
{
id: TheGreatGatsby.id,
changes,
},
withOne
);

expect(withOne.ids).toBe(withUpdates.ids);
});

it('should let you update the id of entity', () => {
const withOne = adapter.addOne(TheGreatGatsby, state);
const changes = { id: 'A New Id' };
Expand Down
82 changes: 53 additions & 29 deletions modules/entity/src/sorted_state_adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
EntityStateAdapter,
Update,
} from './models';
import { createStateOperator } from './state_adapter';
import { createStateOperator, DidMutate } from './state_adapter';
import { createUnsortedStateAdapter } from './unsorted_state_adapter';

export function createSortedStateAdapter<T>(
Expand All @@ -20,68 +20,94 @@ export function createSortedStateAdapter<T>(selectId: any, sort: any): any {
selectId
);

function addOneMutably(entity: T, state: R): boolean;
function addOneMutably(entity: any, state: any): boolean {
function addOneMutably(entity: T, state: R): DidMutate;
function addOneMutably(entity: any, state: any): DidMutate {
return addManyMutably([entity], state);
}

function addManyMutably(newModels: T[], state: R): boolean;
function addManyMutably(newModels: any[], state: any): boolean {
function addManyMutably(newModels: T[], state: R): DidMutate;
function addManyMutably(newModels: any[], state: any): DidMutate {
const models = newModels.filter(
model => !(selectId(model) in state.entities)
);

return merge(models, state);
if (models.length === 0) {
return DidMutate.None;
} else {
merge(models, state);
return DidMutate.Both;
}
}

function addAllMutably(models: T[], state: R): boolean;
function addAllMutably(models: any[], state: any): boolean {
function addAllMutably(models: T[], state: R): DidMutate;
function addAllMutably(models: any[], state: any): DidMutate {
state.entities = {};
state.ids = [];

addManyMutably(models, state);

return true;
return DidMutate.Both;
}

function updateOneMutably(update: Update<T>, state: R): boolean;
function updateOneMutably(update: any, state: any): boolean {
function updateOneMutably(update: Update<T>, state: R): DidMutate;
function updateOneMutably(update: any, state: any): DidMutate {
return updateManyMutably([update], state);
}

function takeUpdatedModel(models: T[], update: Update<T>, state: R): void;
function takeUpdatedModel(models: any[], update: any, state: any): void {
function takeUpdatedModel(models: T[], update: Update<T>, state: R): boolean;
function takeUpdatedModel(models: any[], update: any, state: any): boolean {
if (!(update.id in state.entities)) {
return;
return false;
}

const original = state.entities[update.id];
const updated = Object.assign({}, original, update.changes);
const newKey = selectId(updated);

delete state.entities[update.id];

models.push(updated);

return newKey !== update.id;
}

function updateManyMutably(updates: Update<T>[], state: R): boolean;
function updateManyMutably(updates: any[], state: any): boolean {
function updateManyMutably(updates: Update<T>[], state: R): DidMutate;
function updateManyMutably(updates: any[], state: any): DidMutate {
const models: T[] = [];

updates.forEach(update => takeUpdatedModel(models, update, state));

if (models.length) {
state.ids = state.ids.filter((id: any) => id in state.entities);
}
const didMutateIds =
updates.filter(update => takeUpdatedModel(models, update, state)).length >
0;

return merge(models, state);
}

function merge(models: T[], state: R): boolean;
function merge(models: any[], state: any): boolean {
if (models.length === 0) {
return false;
return DidMutate.None;
} else {
const originalIds = state.ids;
const updatedIndexes: any[] = [];
state.ids = state.ids.filter((id: any, index: number) => {
if (id in state.entities) {
return true;
} else {
updatedIndexes.push(index);
return false;
}
});

merge(models, state);

if (
!didMutateIds &&
updatedIndexes.every((i: number) => state.ids[i] === originalIds[i])
) {
return DidMutate.EntitiesOnly;
} else {
return DidMutate.Both;
}
}
}

function merge(models: T[], state: R): void;
function merge(models: any[], state: any): void {
models.sort(sort);

const ids: any[] = [];
Expand Down Expand Up @@ -113,8 +139,6 @@ export function createSortedStateAdapter<T>(selectId: any, sort: any): any {
models.forEach((model, i) => {
state.entities[selectId(model)] = model;
});

return true;
}

return {
Expand Down
19 changes: 16 additions & 3 deletions modules/entity/src/state_adapter.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { EntityState, EntityStateAdapter } from './models';

export enum DidMutate {
EntitiesOnly,
Both,
None,
}

export function createStateOperator<V, R>(
mutator: (arg: R, state: EntityState<V>) => boolean
mutator: (arg: R, state: EntityState<V>) => DidMutate
): EntityState<V>;
export function createStateOperator<V, R>(
mutator: (arg: any, state: any) => boolean
mutator: (arg: any, state: any) => DidMutate
): any {
return function operation<S extends EntityState<V>>(arg: R, state: any): S {
const clonedEntityState: EntityState<V> = {
Expand All @@ -14,10 +20,17 @@ export function createStateOperator<V, R>(

const didMutate = mutator(arg, clonedEntityState);

if (didMutate) {
if (didMutate === DidMutate.Both) {
return Object.assign({}, state, clonedEntityState);
}

if (didMutate === DidMutate.EntitiesOnly) {
return {
...state,
entities: clonedEntityState.entities,
};
}

return state;
};
}
Loading