Skip to content

Commit b989e4b

Browse files
bbaiabrandonroberts
authored andcommitted
fix(Entity): updateOne/updateMany should not change ids state on existing entity (#581)
Closes #571
1 parent aa9bf9d commit b989e4b

File tree

5 files changed

+171
-62
lines changed

5 files changed

+171
-62
lines changed

modules/entity/spec/sorted_state_adapter.spec.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,24 @@ describe('Sorted State Adapter', () => {
153153
expect(withUpdates).toBe(state);
154154
});
155155

156+
it('should not change ids state if you attempt to update an entity that does not impact sorting', () => {
157+
const withAll = adapter.addAll(
158+
[TheGreatGatsby, AClockworkOrange, AnimalFarm],
159+
state
160+
);
161+
const changes = { title: 'The Great Gatsby II' };
162+
163+
const withUpdates = adapter.updateOne(
164+
{
165+
id: TheGreatGatsby.id,
166+
changes,
167+
},
168+
withAll
169+
);
170+
171+
expect(withAll.ids).toBe(withUpdates.ids);
172+
});
173+
156174
it('should let you update the id of entity', () => {
157175
const withOne = adapter.addOne(TheGreatGatsby, state);
158176
const changes = { id: 'A New Id' };
@@ -176,6 +194,34 @@ describe('Sorted State Adapter', () => {
176194
});
177195
});
178196

197+
it('should resort correctly if same id but sort key update', () => {
198+
const withAll = adapter.addAll(
199+
[TheGreatGatsby, AnimalFarm, AClockworkOrange],
200+
state
201+
);
202+
const changes = { title: 'A New Hope' };
203+
204+
const withUpdates = adapter.updateOne(
205+
{
206+
id: TheGreatGatsby.id,
207+
changes,
208+
},
209+
withAll
210+
);
211+
212+
expect(withUpdates).toEqual({
213+
ids: [AClockworkOrange.id, TheGreatGatsby.id, AnimalFarm.id],
214+
entities: {
215+
[AClockworkOrange.id]: AClockworkOrange,
216+
[TheGreatGatsby.id]: {
217+
...TheGreatGatsby,
218+
...changes,
219+
},
220+
[AnimalFarm.id]: AnimalFarm,
221+
},
222+
});
223+
});
224+
179225
it('should resort correctly if the id and sort key update', () => {
180226
const withOne = adapter.addAll(
181227
[TheGreatGatsby, AnimalFarm, AClockworkOrange],

modules/entity/spec/unsorted_state_adapter.spec.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,21 @@ describe('Unsorted State Adapter', () => {
152152
expect(withUpdates).toBe(state);
153153
});
154154

155+
it('should not change ids state if you attempt to update an entity that has already been added', () => {
156+
const withOne = adapter.addOne(TheGreatGatsby, state);
157+
const changes = { title: 'A New Hope' };
158+
159+
const withUpdates = adapter.updateOne(
160+
{
161+
id: TheGreatGatsby.id,
162+
changes,
163+
},
164+
withOne
165+
);
166+
167+
expect(withOne.ids).toBe(withUpdates.ids);
168+
});
169+
155170
it('should let you update the id of entity', () => {
156171
const withOne = adapter.addOne(TheGreatGatsby, state);
157172
const changes = { id: 'A New Id' };

modules/entity/src/sorted_state_adapter.ts

Lines changed: 53 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
EntityStateAdapter,
77
Update,
88
} from './models';
9-
import { createStateOperator } from './state_adapter';
9+
import { createStateOperator, DidMutate } from './state_adapter';
1010
import { createUnsortedStateAdapter } from './unsorted_state_adapter';
1111

1212
export function createSortedStateAdapter<T>(
@@ -20,68 +20,94 @@ export function createSortedStateAdapter<T>(selectId: any, sort: any): any {
2020
selectId
2121
);
2222

23-
function addOneMutably(entity: T, state: R): boolean;
24-
function addOneMutably(entity: any, state: any): boolean {
23+
function addOneMutably(entity: T, state: R): DidMutate;
24+
function addOneMutably(entity: any, state: any): DidMutate {
2525
return addManyMutably([entity], state);
2626
}
2727

28-
function addManyMutably(newModels: T[], state: R): boolean;
29-
function addManyMutably(newModels: any[], state: any): boolean {
28+
function addManyMutably(newModels: T[], state: R): DidMutate;
29+
function addManyMutably(newModels: any[], state: any): DidMutate {
3030
const models = newModels.filter(
3131
model => !(selectId(model) in state.entities)
3232
);
3333

34-
return merge(models, state);
34+
if (models.length === 0) {
35+
return DidMutate.None;
36+
} else {
37+
merge(models, state);
38+
return DidMutate.Both;
39+
}
3540
}
3641

37-
function addAllMutably(models: T[], state: R): boolean;
38-
function addAllMutably(models: any[], state: any): boolean {
42+
function addAllMutably(models: T[], state: R): DidMutate;
43+
function addAllMutably(models: any[], state: any): DidMutate {
3944
state.entities = {};
4045
state.ids = [];
4146

4247
addManyMutably(models, state);
4348

44-
return true;
49+
return DidMutate.Both;
4550
}
4651

47-
function updateOneMutably(update: Update<T>, state: R): boolean;
48-
function updateOneMutably(update: any, state: any): boolean {
52+
function updateOneMutably(update: Update<T>, state: R): DidMutate;
53+
function updateOneMutably(update: any, state: any): DidMutate {
4954
return updateManyMutably([update], state);
5055
}
5156

52-
function takeUpdatedModel(models: T[], update: Update<T>, state: R): void;
53-
function takeUpdatedModel(models: any[], update: any, state: any): void {
57+
function takeUpdatedModel(models: T[], update: Update<T>, state: R): boolean;
58+
function takeUpdatedModel(models: any[], update: any, state: any): boolean {
5459
if (!(update.id in state.entities)) {
55-
return;
60+
return false;
5661
}
5762

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

6167
delete state.entities[update.id];
6268

6369
models.push(updated);
70+
71+
return newKey !== update.id;
6472
}
6573

66-
function updateManyMutably(updates: Update<T>[], state: R): boolean;
67-
function updateManyMutably(updates: any[], state: any): boolean {
74+
function updateManyMutably(updates: Update<T>[], state: R): DidMutate;
75+
function updateManyMutably(updates: any[], state: any): DidMutate {
6876
const models: T[] = [];
6977

70-
updates.forEach(update => takeUpdatedModel(models, update, state));
71-
72-
if (models.length) {
73-
state.ids = state.ids.filter((id: any) => id in state.entities);
74-
}
78+
const didMutateIds =
79+
updates.filter(update => takeUpdatedModel(models, update, state)).length >
80+
0;
7581

76-
return merge(models, state);
77-
}
78-
79-
function merge(models: T[], state: R): boolean;
80-
function merge(models: any[], state: any): boolean {
8182
if (models.length === 0) {
82-
return false;
83+
return DidMutate.None;
84+
} else {
85+
const originalIds = state.ids;
86+
const updatedIndexes: any[] = [];
87+
state.ids = state.ids.filter((id: any, index: number) => {
88+
if (id in state.entities) {
89+
return true;
90+
} else {
91+
updatedIndexes.push(index);
92+
return false;
93+
}
94+
});
95+
96+
merge(models, state);
97+
98+
if (
99+
!didMutateIds &&
100+
updatedIndexes.every((i: number) => state.ids[i] === originalIds[i])
101+
) {
102+
return DidMutate.EntitiesOnly;
103+
} else {
104+
return DidMutate.Both;
105+
}
83106
}
107+
}
84108

109+
function merge(models: T[], state: R): void;
110+
function merge(models: any[], state: any): void {
85111
models.sort(sort);
86112

87113
const ids: any[] = [];
@@ -113,8 +139,6 @@ export function createSortedStateAdapter<T>(selectId: any, sort: any): any {
113139
models.forEach((model, i) => {
114140
state.entities[selectId(model)] = model;
115141
});
116-
117-
return true;
118142
}
119143

120144
return {
Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import { EntityState, EntityStateAdapter } from './models';
22

3+
export enum DidMutate {
4+
EntitiesOnly,
5+
Both,
6+
None,
7+
}
8+
39
export function createStateOperator<V, R>(
4-
mutator: (arg: R, state: EntityState<V>) => boolean
10+
mutator: (arg: R, state: EntityState<V>) => DidMutate
511
): EntityState<V>;
612
export function createStateOperator<V, R>(
7-
mutator: (arg: any, state: any) => boolean
13+
mutator: (arg: any, state: any) => DidMutate
814
): any {
915
return function operation<S extends EntityState<V>>(arg: R, state: any): S {
1016
const clonedEntityState: EntityState<V> = {
@@ -14,10 +20,17 @@ export function createStateOperator<V, R>(
1420

1521
const didMutate = mutator(arg, clonedEntityState);
1622

17-
if (didMutate) {
23+
if (didMutate === DidMutate.Both) {
1824
return Object.assign({}, state, clonedEntityState);
1925
}
2026

27+
if (didMutate === DidMutate.EntitiesOnly) {
28+
return {
29+
...state,
30+
entities: clonedEntityState.entities,
31+
};
32+
}
33+
2134
return state;
2235
};
2336
}

0 commit comments

Comments
 (0)