diff --git a/modules/entity/spec/sorted_state_adapter.spec.ts b/modules/entity/spec/sorted_state_adapter.spec.ts index e88c12adc4..cc9116b6b0 100644 --- a/modules/entity/spec/sorted_state_adapter.spec.ts +++ b/modules/entity/spec/sorted_state_adapter.spec.ts @@ -1,4 +1,4 @@ -import { EntityStateAdapter, EntityState } from '../src/models'; +import { EntityStateAdapter, EntityState, Update } from '../src/models'; import { createEntityAdapter } from '../src/create_adapter'; import { BookModel, @@ -277,4 +277,72 @@ describe('Sorted State Adapter', () => { }, }); }); + + it('should let you add one entity to the state with upsert()', () => { + const withOneEntity = adapter.upsertOne( + { + id: TheGreatGatsby.id, + changes: TheGreatGatsby, + }, + state + ); + + expect(withOneEntity).toEqual({ + ids: [TheGreatGatsby.id], + entities: { + [TheGreatGatsby.id]: TheGreatGatsby, + }, + }); + }); + + it('should let you update an entity in the state with upsert()', () => { + const withOne = adapter.addOne(TheGreatGatsby, state); + const changes = { title: 'A New Hope' }; + + const withUpdates = adapter.upsertOne( + { + id: TheGreatGatsby.id, + changes, + }, + withOne + ); + + expect(withUpdates).toEqual({ + ids: [TheGreatGatsby.id], + entities: { + [TheGreatGatsby.id]: { + ...TheGreatGatsby, + ...changes, + }, + }, + }); + }); + + it('should let you upsert many entities in the state', () => { + const firstChange = { title: 'Zack' }; + const secondChange = { title: 'Aaron' }; + const withMany = adapter.addAll([TheGreatGatsby], state); + + const withUpserts = adapter.upsertMany( + [ + { id: TheGreatGatsby.id, changes: firstChange }, + { id: AClockworkOrange.id, changes: secondChange }, + ], + withMany + ); + + expect(withUpserts).toEqual({ + ids: [AClockworkOrange.id, TheGreatGatsby.id], + entities: { + [TheGreatGatsby.id]: { + ...TheGreatGatsby, + ...firstChange, + }, + [AClockworkOrange.id]: { + ...AClockworkOrange, + ...secondChange, + }, + }, + }); + }); }); diff --git a/modules/entity/spec/unsorted_state_adapter.spec.ts b/modules/entity/spec/unsorted_state_adapter.spec.ts index 0d94a28b2b..2a2655cc93 100644 --- a/modules/entity/spec/unsorted_state_adapter.spec.ts +++ b/modules/entity/spec/unsorted_state_adapter.spec.ts @@ -217,4 +217,72 @@ describe('Unsorted State Adapter', () => { }, }); }); + + it('should let you add one entity to the state with upsert()', () => { + const withOneEntity = adapter.upsertOne( + { + id: TheGreatGatsby.id, + changes: TheGreatGatsby, + }, + state + ); + + expect(withOneEntity).toEqual({ + ids: [TheGreatGatsby.id], + entities: { + [TheGreatGatsby.id]: TheGreatGatsby, + }, + }); + }); + + it('should let you update an entity in the state with upsert()', () => { + const withOne = adapter.addOne(TheGreatGatsby, state); + const changes = { title: 'A New Hope' }; + + const withUpdates = adapter.upsertOne( + { + id: TheGreatGatsby.id, + changes, + }, + withOne + ); + + expect(withUpdates).toEqual({ + ids: [TheGreatGatsby.id], + entities: { + [TheGreatGatsby.id]: { + ...TheGreatGatsby, + ...changes, + }, + }, + }); + }); + + it('should let you upsert many entities in the state', () => { + const firstChange = { title: 'First Change' }; + const secondChange = { title: 'Second Change' }; + const withMany = adapter.addAll([TheGreatGatsby], state); + + const withUpserts = adapter.upsertMany( + [ + { id: TheGreatGatsby.id, changes: firstChange }, + { id: AClockworkOrange.id, changes: secondChange }, + ], + withMany + ); + + expect(withUpserts).toEqual({ + ids: [TheGreatGatsby.id, AClockworkOrange.id], + entities: { + [TheGreatGatsby.id]: { + ...TheGreatGatsby, + ...firstChange, + }, + [AClockworkOrange.id]: { + ...AClockworkOrange, + ...secondChange, + }, + }, + }); + }); }); diff --git a/modules/entity/src/models.ts b/modules/entity/src/models.ts index 416bb6ae30..9f763391b9 100644 --- a/modules/entity/src/models.ts +++ b/modules/entity/src/models.ts @@ -63,6 +63,9 @@ export interface EntityStateAdapter { updateOne>(update: Update, state: S): S; updateMany>(updates: Update[], state: S): S; + + upsertOne>(update: Update, state: S): S; + upsertMany>(updates: Update[], state: S): S; } export type EntitySelectors = { diff --git a/modules/entity/src/sorted_state_adapter.ts b/modules/entity/src/sorted_state_adapter.ts index 0641234b65..15aaecbad0 100644 --- a/modules/entity/src/sorted_state_adapter.ts +++ b/modules/entity/src/sorted_state_adapter.ts @@ -106,6 +106,43 @@ export function createSortedStateAdapter(selectId: any, sort: any): any { } } + function upsertOneMutably(update: Update, state: R): DidMutate; + function upsertOneMutably(update: any, state: any): DidMutate { + return upsertManyMutably([update], state); + } + + function upsertManyMutably(updates: Update[], state: R): DidMutate; + function upsertManyMutably(updates: any[], state: any): DidMutate { + const added: T[] = []; + const updated: Update[] = []; + + for (let index in updates) { + const update = updates[index]; + if (update.id in state.entities) { + updated.push(update); + } else { + added.push({ + ...update.changes, + id: update.id, + }); + } + } + + const didMutateByUpdated = updateManyMutably(updated, state); + const didMutateByAdded = addManyMutably(added, state); + + switch (true) { + case didMutateByAdded === DidMutate.None && + didMutateByUpdated === DidMutate.None: + return DidMutate.None; + case didMutateByAdded === DidMutate.Both || + didMutateByUpdated === DidMutate.Both: + return DidMutate.Both; + default: + return DidMutate.EntitiesOnly; + } + } + function merge(models: T[], state: R): void; function merge(models: any[], state: any): void { models.sort(sort); @@ -147,8 +184,10 @@ export function createSortedStateAdapter(selectId: any, sort: any): any { removeAll, addOne: createStateOperator(addOneMutably), updateOne: createStateOperator(updateOneMutably), + upsertOne: createStateOperator(upsertOneMutably), addAll: createStateOperator(addAllMutably), addMany: createStateOperator(addManyMutably), updateMany: createStateOperator(updateManyMutably), + upsertMany: createStateOperator(upsertManyMutably), }; } diff --git a/modules/entity/src/unsorted_state_adapter.ts b/modules/entity/src/unsorted_state_adapter.ts index dcf1d53214..61ff410bdb 100644 --- a/modules/entity/src/unsorted_state_adapter.ts +++ b/modules/entity/src/unsorted_state_adapter.ts @@ -123,6 +123,43 @@ export function createUnsortedStateAdapter(selectId: IdSelector): any { return DidMutate.None; } + function upsertOneMutably(update: Update, state: R): DidMutate; + function upsertOneMutably(update: any, state: any): DidMutate { + return upsertManyMutably([update], state); + } + + function upsertManyMutably(updates: Update[], state: R): DidMutate; + function upsertManyMutably(updates: any[], state: any): DidMutate { + const added: T[] = []; + const updated: any[] = []; + + for (let index in updates) { + const update = updates[index]; + if (update.id in state.entities) { + updated.push(update); + } else { + added.push({ + ...update.changes, + id: update.id, + }); + } + } + + const didMutateByUpdated = updateManyMutably(updated, state); + const didMutateByAdded = addManyMutably(added, state); + + switch (true) { + case didMutateByAdded === DidMutate.None && + didMutateByUpdated === DidMutate.None: + return DidMutate.None; + case didMutateByAdded === DidMutate.Both || + didMutateByUpdated === DidMutate.Both: + return DidMutate.Both; + default: + return DidMutate.EntitiesOnly; + } + } + return { removeAll, addOne: createStateOperator(addOneMutably), @@ -130,6 +167,8 @@ export function createUnsortedStateAdapter(selectId: IdSelector): any { addAll: createStateOperator(addAllMutably), updateOne: createStateOperator(updateOneMutably), updateMany: createStateOperator(updateManyMutably), + upsertOne: createStateOperator(upsertOneMutably), + upsertMany: createStateOperator(upsertManyMutably), removeOne: createStateOperator(removeOneMutably), removeMany: createStateOperator(removeManyMutably), };