Skip to content

Commit

Permalink
Add setOne and setMany to entity adapter (#969)
Browse files Browse the repository at this point in the history
upsertOne and upsertMany merge the passed value with the existing item,
but there was no direct way to replace items.
setOne and setMany solve this and replace entirely the previous value
with the new one.
  • Loading branch information
HHK1 committed Apr 1, 2021
1 parent 4462647 commit 795af40
Show file tree
Hide file tree
Showing 6 changed files with 339 additions and 1 deletion.
8 changes: 8 additions & 0 deletions etc/redux-toolkit.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,14 @@ export interface EntityStateAdapter<T> {
// (undocumented)
setAll<S extends EntityState<T>>(state: PreventAny<S, T>, entities: PayloadAction<T[] | Record<EntityId, T>>): S;
// (undocumented)
setMany<S extends EntityState<T>>(state: PreventAny<S, T>, entities: T[] | Record<EntityId, T>): S;
// (undocumented)
setMany<S extends EntityState<T>>(state: PreventAny<S, T>, entities: PayloadAction<T[] | Record<EntityId, T>>): S;
// (undocumented)
setOne<S extends EntityState<T>>(state: PreventAny<S, T>, entity: T): S;
// (undocumented)
setOne<S extends EntityState<T>>(state: PreventAny<S, T>, action: PayloadAction<T>): S;
// (undocumented)
updateMany<S extends EntityState<T>>(state: PreventAny<S, T>, updates: Update<T>[]): S;
// (undocumented)
updateMany<S extends EntityState<T>>(state: PreventAny<S, T>, updates: PayloadAction<Update<T>[]>): S;
Expand Down
13 changes: 13 additions & 0 deletions src/entities/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,19 @@ export interface EntityStateAdapter<T> {
entities: PayloadAction<T[] | Record<EntityId, T>>
): S

setOne<S extends EntityState<T>>(state: PreventAny<S, T>, entity: T): S
setOne<S extends EntityState<T>>(
state: PreventAny<S, T>,
action: PayloadAction<T>
): S
setMany<S extends EntityState<T>>(
state: PreventAny<S, T>,
entities: T[] | Record<EntityId, T>
): S
setMany<S extends EntityState<T>>(
state: PreventAny<S, T>,
entities: PayloadAction<T[] | Record<EntityId, T>>
): S
setAll<S extends EntityState<T>>(
state: PreventAny<S, T>,
entities: T[] | Record<EntityId, T>
Expand Down
151 changes: 150 additions & 1 deletion src/entities/sorted_state_adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
BookModel,
TheGreatGatsby,
AClockworkOrange,
AnimalFarm
AnimalFarm,
TheHobbit
} from './fixtures/book'
import { createNextState } from '..'

Expand Down Expand Up @@ -464,6 +465,81 @@ describe('Sorted State Adapter', () => {
})
})

it('should let you add a new entity in the state with setOne() and keep the sorting', () => {
const withMany = adapter.setAll(state, [AnimalFarm, TheHobbit])
const withOneMore = adapter.setOne(withMany, TheGreatGatsby)
expect(withOneMore).toEqual({
ids: [AnimalFarm.id, TheGreatGatsby.id, TheHobbit.id],
entities: {
[AnimalFarm.id]: AnimalFarm,
[TheHobbit.id]: TheHobbit,
[TheGreatGatsby.id]: TheGreatGatsby
}
})
})

it('should let you replace an entity in the state with setOne()', () => {
let withOne = adapter.setOne(state, TheHobbit)
const changeWithoutAuthor = { id: TheHobbit.id, title: 'Silmarillion' }
withOne = adapter.setOne(withOne, changeWithoutAuthor)

expect(withOne).toEqual({
ids: [TheHobbit.id],
entities: {
[TheHobbit.id]: changeWithoutAuthor
}
})
})

it('should do nothing when setMany is given an empty array', () => {
const withMany = adapter.setAll(state, [TheGreatGatsby])

const withUpserts = adapter.setMany(withMany, [])

expect(withUpserts).toEqual({
ids: [TheGreatGatsby.id],
entities: {
[TheGreatGatsby.id]: TheGreatGatsby
}
})
})

it('should let you set many entities in the state', () => {
const firstChange = { id: TheHobbit.id, title: 'Silmarillion' }
const withMany = adapter.setAll(state, [TheHobbit])

const withSetMany = adapter.setMany(withMany, [
firstChange,
AClockworkOrange
])

expect(withSetMany).toEqual({
ids: [AClockworkOrange.id, TheHobbit.id],
entities: {
[TheHobbit.id]: firstChange,
[AClockworkOrange.id]: AClockworkOrange
}
})
})

it('should let you set many entities in the state when passing in a dictionary', () => {
const changeWithoutAuthor = { id: TheHobbit.id, title: 'Silmarillion' }
const withMany = adapter.setAll(state, [TheHobbit])

const withSetMany = adapter.setMany(withMany, {
[TheHobbit.id]: changeWithoutAuthor,
[AClockworkOrange.id]: AClockworkOrange
})

expect(withSetMany).toEqual({
ids: [AClockworkOrange.id, TheHobbit.id],
entities: {
[TheHobbit.id]: changeWithoutAuthor,
[AClockworkOrange.id]: AClockworkOrange
}
})
})

describe('can be used mutably when wrapped in createNextState', () => {
test('removeAll', () => {
const withTwo = adapter.addMany(state, [TheGreatGatsby, AnimalFarm])
Expand Down Expand Up @@ -678,6 +754,79 @@ describe('Sorted State Adapter', () => {
`)
})

test('setOne (insert)', () => {
const result = createNextState(state, draft => {
adapter.setOne(draft, TheGreatGatsby)
})
expect(result).toMatchInlineSnapshot(`
Object {
"entities": Object {
"tgg": Object {
"id": "tgg",
"title": "The Great Gatsby",
},
},
"ids": Array [
"tgg",
],
}
`)
})

test('setOne (update)', () => {
const withOne = adapter.setOne(state, TheHobbit)
const result = createNextState(withOne, draft => {
adapter.setOne(draft, {
id: TheHobbit.id,
title: 'Silmarillion'
})
})
expect(result).toMatchInlineSnapshot(`
Object {
"entities": Object {
"th": Object {
"id": "th",
"title": "Silmarillion",
},
},
"ids": Array [
"th",
],
}
`)
})

test('setMany', () => {
const withOne = adapter.setOne(state, TheHobbit)
const result = createNextState(withOne, draft => {
adapter.setMany(draft, [
{
id: TheHobbit.id,
title: 'Silmarillion'
},
AnimalFarm
])
})
expect(result).toMatchInlineSnapshot(`
Object {
"entities": Object {
"af": Object {
"id": "af",
"title": "Animal Farm",
},
"th": Object {
"id": "th",
"title": "Silmarillion",
},
},
"ids": Array [
"af",
"th",
],
}
`)
})

test('removeOne', () => {
const withTwo = adapter.addMany(state, [TheGreatGatsby, AnimalFarm])
const result = createNextState(withTwo, draft => {
Expand Down
16 changes: 16 additions & 0 deletions src/entities/sorted_state_adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@ export function createSortedStateAdapter<T>(
}
}

function setOneMutably(entity: T, state: R): void {
return setManyMutably([entity], state)
}

function setManyMutably(
newEntities: T[] | Record<EntityId, T>,
state: R
): void {
newEntities = ensureEntitiesArray(newEntities)
if (newEntities.length !== 0) {
merge(newEntities, state)
}
}

function setAllMutably(
newEntities: T[] | Record<EntityId, T>,
state: R
Expand Down Expand Up @@ -140,6 +154,8 @@ export function createSortedStateAdapter<T>(
addOne: createStateOperator(addOneMutably),
updateOne: createStateOperator(updateOneMutably),
upsertOne: createStateOperator(upsertOneMutably),
setOne: createStateOperator(setOneMutably),
setMany: createStateOperator(setManyMutably),
setAll: createStateOperator(setAllMutably),
addMany: createStateOperator(addManyMutably),
updateMany: createStateOperator(updateManyMutably),
Expand Down
132 changes: 132 additions & 0 deletions src/entities/unsorted_state_adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,65 @@ describe('Unsorted State Adapter', () => {
})
})

it('should let you add a new entity in the state with setOne()', () => {
const withOne = adapter.setOne(state, TheGreatGatsby)
expect(withOne).toEqual({
ids: [TheGreatGatsby.id],
entities: {
[TheGreatGatsby.id]: TheGreatGatsby
}
})
})

it('should let you replace an entity in the state with setOne()', () => {
let withOne = adapter.setOne(state, TheHobbit)
const changeWithoutAuthor = { id: TheHobbit.id, title: 'Silmarillion' }
withOne = adapter.setOne(withOne, changeWithoutAuthor)

expect(withOne).toEqual({
ids: [TheHobbit.id],
entities: {
[TheHobbit.id]: changeWithoutAuthor
}
})
})

it('should let you set many entities in the state', () => {
const changeWithoutAuthor = { id: TheHobbit.id, title: 'Silmarillion' }
const withMany = adapter.setAll(state, [TheHobbit])

const withSetMany = adapter.setMany(withMany, [
changeWithoutAuthor,
AClockworkOrange
])

expect(withSetMany).toEqual({
ids: [TheHobbit.id, AClockworkOrange.id],
entities: {
[TheHobbit.id]: changeWithoutAuthor,
[AClockworkOrange.id]: AClockworkOrange
}
})
})

it('should let you set many entities in the state when passing in a dictionary', () => {
const changeWithoutAuthor = { id: TheHobbit.id, title: 'Silmarillion' }
const withMany = adapter.setAll(state, [TheHobbit])

const withSetMany = adapter.setMany(withMany, {
[TheHobbit.id]: changeWithoutAuthor,
[AClockworkOrange.id]: AClockworkOrange
})

expect(withSetMany).toEqual({
ids: [TheHobbit.id, AClockworkOrange.id],
entities: {
[TheHobbit.id]: changeWithoutAuthor,
[AClockworkOrange.id]: AClockworkOrange
}
})
})

describe('can be used mutably when wrapped in createNextState', () => {
test('removeAll', () => {
const withTwo = adapter.addMany(state, [TheGreatGatsby, AnimalFarm])
Expand Down Expand Up @@ -583,6 +642,79 @@ describe('Unsorted State Adapter', () => {
`)
})

test('setOne (insert)', () => {
const result = createNextState(state, draft => {
adapter.setOne(draft, TheGreatGatsby)
})
expect(result).toMatchInlineSnapshot(`
Object {
"entities": Object {
"tgg": Object {
"id": "tgg",
"title": "The Great Gatsby",
},
},
"ids": Array [
"tgg",
],
}
`)
})

test('setOne (update)', () => {
const withOne = adapter.setOne(state, TheHobbit)
const result = createNextState(withOne, draft => {
adapter.setOne(draft, {
id: TheHobbit.id,
title: 'Silmarillion'
})
})
expect(result).toMatchInlineSnapshot(`
Object {
"entities": Object {
"th": Object {
"id": "th",
"title": "Silmarillion",
},
},
"ids": Array [
"th",
],
}
`)
})

test('setMany', () => {
const withOne = adapter.setOne(state, TheHobbit)
const result = createNextState(withOne, draft => {
adapter.setMany(draft, [
{
id: TheHobbit.id,
title: 'Silmarillion'
},
AnimalFarm
])
})
expect(result).toMatchInlineSnapshot(`
Object {
"entities": Object {
"af": Object {
"id": "af",
"title": "Animal Farm",
},
"th": Object {
"id": "th",
"title": "Silmarillion",
},
},
"ids": Array [
"th",
"af",
],
}
`)
})

test('removeOne', () => {
const withTwo = adapter.addMany(state, [TheGreatGatsby, AnimalFarm])
const result = createNextState(withTwo, draft => {
Expand Down

0 comments on commit 795af40

Please sign in to comment.