Repository: reduxjs/redux-toolkit (ts)

Issue:
removeAll method of entity is not working.
https://github.com/reduxjs/redux-toolkit/blob/c89d5e2c6d3a4c1cc14a96db94d823314c4624e4/src/entities/unsorted_state_adapter.ts#L67-L72

Hi, teams.
I tested removeAll method, but it didn't work.
It looks incorrect usage of Object.assign.

```javascript
function removeAll(state: R): any { 
   return Object.assign(state, { 
     ids: [], 
     entities: {} 
   }) 
 } 
```

`state` parameter should be placed at first of params

can you check it?

Pull request description:
fix #541: invoke removeAll mutably within produce
This should invoke removeAll as a mutable action when wrapped in createNextState/produce and close #541.
Also, lots of tests.

Patch:
diff --git a/src/entities/state_adapter.ts b/src/entities/state_adapter.ts
index a0008917..75895118 100644
--- a/src/entities/state_adapter.ts
+++ b/src/entities/state_adapter.ts
@@ -1,7 +1,21 @@
 import createNextState, { isDraft } from 'immer'
-import { EntityState } from './models'
+import { EntityState, PreventAny } from './models'
 import { PayloadAction, isFSA } from '../createAction'
 
+export function createSingleArgumentStateOperator<V>(
+  mutator: (state: EntityState<V>) => void
+) {
+  const operator = createStateOperator((_: undefined, state: EntityState<V>) =>
+    mutator(state)
+  )
+
+  return function operation<S extends EntityState<V>>(
+    state: PreventAny<S, V>
+  ): S {
+    return operator(state as S, undefined)
+  }
+}
+
 export function createStateOperator<V, R>(
   mutator: (arg: R, state: EntityState<V>) => void
 ) {
diff --git a/src/entities/unsorted_state_adapter.ts b/src/entities/unsorted_state_adapter.ts
index 8d41f242..13f20733 100644
--- a/src/entities/unsorted_state_adapter.ts
+++ b/src/entities/unsorted_state_adapter.ts
@@ -5,7 +5,10 @@ import {
   Update,
   EntityId
 } from './models'
-import { createStateOperator } from './state_adapter'
+import {
+  createStateOperator,
+  createSingleArgumentStateOperator
+} from './state_adapter'
 import { selectIdValue } from './utils'
 
 export function createUnsortedStateAdapter<T>(
@@ -64,8 +67,8 @@ export function createUnsortedStateAdapter<T>(
     }
   }
 
-  function removeAll(state: R): any {
-    return Object.assign({}, state, {
+  function removeAllMutably(state: R): void {
+    Object.assign(state, {
       ids: [],
       entities: {}
     })
@@ -156,7 +159,7 @@ export function createUnsortedStateAdapter<T>(
   }
 
   return {
-    removeAll,
+    removeAll: createSingleArgumentStateOperator(removeAllMutably),
     addOne: createStateOperator(addOneMutably),
     addMany: createStateOperator(addManyMutably),
     setAll: createStateOperator(setAllMutably),


Test patch:
diff --git a/src/entities/sorted_state_adapter.test.ts b/src/entities/sorted_state_adapter.test.ts
index 104375a3..daa45b7b 100644
--- a/src/entities/sorted_state_adapter.test.ts
+++ b/src/entities/sorted_state_adapter.test.ts
@@ -7,6 +7,7 @@ import {
   AClockworkOrange,
   AnimalFarm
 } from './fixtures/book'
+import { createNextState } from '..'
 
 describe('Sorted State Adapter', () => {
   let adapter: EntityStateAdapter<BookModel>
@@ -436,4 +437,263 @@ describe('Sorted State Adapter', () => {
       }
     })
   })
+
+  describe('can be used mutably when wrapped in createNextState', () => {
+    test('removeAll', () => {
+      const withTwo = adapter.addMany(state, [TheGreatGatsby, AnimalFarm])
+      const result = createNextState(withTwo, draft => {
+        adapter.removeAll(draft)
+      })
+      expect(result).toMatchInlineSnapshot(`
+        Object {
+          "entities": Object {},
+          "ids": Array [],
+        }
+      `)
+    })
+
+    test('addOne', () => {
+      const result = createNextState(state, draft => {
+        adapter.addOne(draft, TheGreatGatsby)
+      })
+
+      expect(result).toMatchInlineSnapshot(`
+        Object {
+          "entities": Object {
+            "tgg": Object {
+              "id": "tgg",
+              "title": "The Great Gatsby",
+            },
+          },
+          "ids": Array [
+            "tgg",
+          ],
+        }
+      `)
+    })
+
+    test('addMany', () => {
+      const result = createNextState(state, draft => {
+        adapter.addMany(draft, [TheGreatGatsby, AnimalFarm])
+      })
+
+      expect(result).toMatchInlineSnapshot(`
+        Object {
+          "entities": Object {
+            "af": Object {
+              "id": "af",
+              "title": "Animal Farm",
+            },
+            "tgg": Object {
+              "id": "tgg",
+              "title": "The Great Gatsby",
+            },
+          },
+          "ids": Array [
+            "af",
+            "tgg",
+          ],
+        }
+      `)
+    })
+
+    test('setAll', () => {
+      const result = createNextState(state, draft => {
+        adapter.setAll(draft, [TheGreatGatsby, AnimalFarm])
+      })
+
+      expect(result).toMatchInlineSnapshot(`
+        Object {
+          "entities": Object {
+            "af": Object {
+              "id": "af",
+              "title": "Animal Farm",
+            },
+            "tgg": Object {
+              "id": "tgg",
+              "title": "The Great Gatsby",
+            },
+          },
+          "ids": Array [
+            "af",
+            "tgg",
+          ],
+        }
+      `)
+    })
+
+    test('updateOne', () => {
+      const withOne = adapter.addOne(state, TheGreatGatsby)
+      const changes = { title: 'A New Hope' }
+      const result = createNextState(withOne, draft => {
+        adapter.updateOne(draft, {
+          id: TheGreatGatsby.id,
+          changes
+        })
+      })
+
+      expect(result).toMatchInlineSnapshot(`
+        Object {
+          "entities": Object {
+            "tgg": Object {
+              "id": "tgg",
+              "title": "A New Hope",
+            },
+          },
+          "ids": Array [
+            "tgg",
+          ],
+        }
+      `)
+    })
+
+    test('updateMany', () => {
+      const firstChange = { title: 'First Change' }
+      const secondChange = { title: 'Second Change' }
+      const withMany = adapter.setAll(state, [TheGreatGatsby, AClockworkOrange])
+
+      const result = createNextState(withMany, draft => {
+        adapter.updateMany(draft, [
+          { id: TheGreatGatsby.id, changes: firstChange },
+          { id: AClockworkOrange.id, changes: secondChange }
+        ])
+      })
+
+      expect(result).toMatchInlineSnapshot(`
+        Object {
+          "entities": Object {
+            "aco": Object {
+              "id": "aco",
+              "title": "Second Change",
+            },
+            "tgg": Object {
+              "id": "tgg",
+              "title": "First Change",
+            },
+          },
+          "ids": Array [
+            "tgg",
+            "aco",
+          ],
+        }
+      `)
+    })
+
+    test('upsertOne (insert)', () => {
+      const result = createNextState(state, draft => {
+        adapter.upsertOne(draft, TheGreatGatsby)
+      })
+      expect(result).toMatchInlineSnapshot(`
+        Object {
+          "entities": Object {
+            "tgg": Object {
+              "id": "tgg",
+              "title": "The Great Gatsby",
+            },
+          },
+          "ids": Array [
+            "tgg",
+          ],
+        }
+      `)
+    })
+
+    test('upsertOne (update)', () => {
+      const withOne = adapter.upsertOne(state, TheGreatGatsby)
+      const result = createNextState(withOne, draft => {
+        adapter.upsertOne(draft, {
+          id: TheGreatGatsby.id,
+          title: 'A New Hope'
+        })
+      })
+      expect(result).toMatchInlineSnapshot(`
+        Object {
+          "entities": Object {
+            "tgg": Object {
+              "id": "tgg",
+              "title": "A New Hope",
+            },
+          },
+          "ids": Array [
+            "tgg",
+          ],
+        }
+      `)
+    })
+
+    test('upsertMany', () => {
+      const withOne = adapter.upsertOne(state, TheGreatGatsby)
+      const result = createNextState(withOne, draft => {
+        adapter.upsertMany(draft, [
+          {
+            id: TheGreatGatsby.id,
+            title: 'A New Hope'
+          },
+          AnimalFarm
+        ])
+      })
+      expect(result).toMatchInlineSnapshot(`
+        Object {
+          "entities": Object {
+            "af": Object {
+              "id": "af",
+              "title": "Animal Farm",
+            },
+            "tgg": Object {
+              "id": "tgg",
+              "title": "A New Hope",
+            },
+          },
+          "ids": Array [
+            "tgg",
+            "af",
+          ],
+        }
+      `)
+    })
+
+    test('removeOne', () => {
+      const withTwo = adapter.addMany(state, [TheGreatGatsby, AnimalFarm])
+      const result = createNextState(withTwo, draft => {
+        adapter.removeOne(draft, TheGreatGatsby.id)
+      })
+      expect(result).toMatchInlineSnapshot(`
+        Object {
+          "entities": Object {
+            "af": Object {
+              "id": "af",
+              "title": "Animal Farm",
+            },
+          },
+          "ids": Array [
+            "af",
+          ],
+        }
+      `)
+    })
+
+    test('removeMany', () => {
+      const withThree = adapter.addMany(state, [
+        TheGreatGatsby,
+        AnimalFarm,
+        AClockworkOrange
+      ])
+      const result = createNextState(withThree, draft => {
+        adapter.removeMany(draft, [TheGreatGatsby.id, AnimalFarm.id])
+      })
+      expect(result).toMatchInlineSnapshot(`
+        Object {
+          "entities": Object {
+            "aco": Object {
+              "id": "aco",
+              "title": "A Clockwork Orange",
+            },
+          },
+          "ids": Array [
+            "aco",
+          ],
+        }
+      `)
+    })
+  })
 })
diff --git a/src/entities/unsorted_state_adapter.test.ts b/src/entities/unsorted_state_adapter.test.ts
index 87a4ad62..44f5d696 100644
--- a/src/entities/unsorted_state_adapter.test.ts
+++ b/src/entities/unsorted_state_adapter.test.ts
@@ -6,6 +6,7 @@ import {
   AClockworkOrange,
   AnimalFarm
 } from './fixtures/book'
+import { createNextState } from '..'
 
 describe('Unsorted State Adapter', () => {
   let adapter: EntityStateAdapter<BookModel>
@@ -352,4 +353,263 @@ describe('Unsorted State Adapter', () => {
       }
     })
   })
+
+  describe('can be used mutably when wrapped in createNextState', () => {
+    test('removeAll', () => {
+      const withTwo = adapter.addMany(state, [TheGreatGatsby, AnimalFarm])
+      const result = createNextState(withTwo, draft => {
+        adapter.removeAll(draft)
+      })
+      expect(result).toMatchInlineSnapshot(`
+        Object {
+          "entities": Object {},
+          "ids": Array [],
+        }
+      `)
+    })
+
+    test('addOne', () => {
+      const result = createNextState(state, draft => {
+        adapter.addOne(draft, TheGreatGatsby)
+      })
+
+      expect(result).toMatchInlineSnapshot(`
+        Object {
+          "entities": Object {
+            "tgg": Object {
+              "id": "tgg",
+              "title": "The Great Gatsby",
+            },
+          },
+          "ids": Array [
+            "tgg",
+          ],
+        }
+      `)
+    })
+
+    test('addMany', () => {
+      const result = createNextState(state, draft => {
+        adapter.addMany(draft, [TheGreatGatsby, AnimalFarm])
+      })
+
+      expect(result).toMatchInlineSnapshot(`
+        Object {
+          "entities": Object {
+            "af": Object {
+              "id": "af",
+              "title": "Animal Farm",
+            },
+            "tgg": Object {
+              "id": "tgg",
+              "title": "The Great Gatsby",
+            },
+          },
+          "ids": Array [
+            "tgg",
+            "af",
+          ],
+        }
+      `)
+    })
+
+    test('setAll', () => {
+      const result = createNextState(state, draft => {
+        adapter.setAll(draft, [TheGreatGatsby, AnimalFarm])
+      })
+
+      expect(result).toMatchInlineSnapshot(`
+        Object {
+          "entities": Object {
+            "af": Object {
+              "id": "af",
+              "title": "Animal Farm",
+            },
+            "tgg": Object {
+              "id": "tgg",
+              "title": "The Great Gatsby",
+            },
+          },
+          "ids": Array [
+            "tgg",
+            "af",
+          ],
+        }
+      `)
+    })
+
+    test('updateOne', () => {
+      const withOne = adapter.addOne(state, TheGreatGatsby)
+      const changes = { title: 'A New Hope' }
+      const result = createNextState(withOne, draft => {
+        adapter.updateOne(draft, {
+          id: TheGreatGatsby.id,
+          changes
+        })
+      })
+
+      expect(result).toMatchInlineSnapshot(`
+        Object {
+          "entities": Object {
+            "tgg": Object {
+              "id": "tgg",
+              "title": "A New Hope",
+            },
+          },
+          "ids": Array [
+            "tgg",
+          ],
+        }
+      `)
+    })
+
+    test('updateMany', () => {
+      const firstChange = { title: 'First Change' }
+      const secondChange = { title: 'Second Change' }
+      const withMany = adapter.setAll(state, [TheGreatGatsby, AClockworkOrange])
+
+      const result = createNextState(withMany, draft => {
+        adapter.updateMany(draft, [
+          { id: TheGreatGatsby.id, changes: firstChange },
+          { id: AClockworkOrange.id, changes: secondChange }
+        ])
+      })
+
+      expect(result).toMatchInlineSnapshot(`
+        Object {
+          "entities": Object {
+            "aco": Object {
+              "id": "aco",
+              "title": "Second Change",
+            },
+            "tgg": Object {
+              "id": "tgg",
+              "title": "First Change",
+            },
+          },
+          "ids": Array [
+            "tgg",
+            "aco",
+          ],
+        }
+      `)
+    })
+
+    test('upsertOne (insert)', () => {
+      const result = createNextState(state, draft => {
+        adapter.upsertOne(draft, TheGreatGatsby)
+      })
+      expect(result).toMatchInlineSnapshot(`
+        Object {
+          "entities": Object {
+            "tgg": Object {
+              "id": "tgg",
+              "title": "The Great Gatsby",
+            },
+          },
+          "ids": Array [
+            "tgg",
+          ],
+        }
+      `)
+    })
+
+    test('upsertOne (update)', () => {
+      const withOne = adapter.upsertOne(state, TheGreatGatsby)
+      const result = createNextState(withOne, draft => {
+        adapter.upsertOne(draft, {
+          id: TheGreatGatsby.id,
+          title: 'A New Hope'
+        })
+      })
+      expect(result).toMatchInlineSnapshot(`
+        Object {
+          "entities": Object {
+            "tgg": Object {
+              "id": "tgg",
+              "title": "A New Hope",
+            },
+          },
+          "ids": Array [
+            "tgg",
+          ],
+        }
+      `)
+    })
+
+    test('upsertMany', () => {
+      const withOne = adapter.upsertOne(state, TheGreatGatsby)
+      const result = createNextState(withOne, draft => {
+        adapter.upsertMany(draft, [
+          {
+            id: TheGreatGatsby.id,
+            title: 'A New Hope'
+          },
+          AnimalFarm
+        ])
+      })
+      expect(result).toMatchInlineSnapshot(`
+        Object {
+          "entities": Object {
+            "af": Object {
+              "id": "af",
+              "title": "Animal Farm",
+            },
+            "tgg": Object {
+              "id": "tgg",
+              "title": "A New Hope",
+            },
+          },
+          "ids": Array [
+            "tgg",
+            "af",
+          ],
+        }
+      `)
+    })
+
+    test('removeOne', () => {
+      const withTwo = adapter.addMany(state, [TheGreatGatsby, AnimalFarm])
+      const result = createNextState(withTwo, draft => {
+        adapter.removeOne(draft, TheGreatGatsby.id)
+      })
+      expect(result).toMatchInlineSnapshot(`
+        Object {
+          "entities": Object {
+            "af": Object {
+              "id": "af",
+              "title": "Animal Farm",
+            },
+          },
+          "ids": Array [
+            "af",
+          ],
+        }
+      `)
+    })
+
+    test('removeMany', () => {
+      const withThree = adapter.addMany(state, [
+        TheGreatGatsby,
+        AnimalFarm,
+        AClockworkOrange
+      ])
+      const result = createNextState(withThree, draft => {
+        adapter.removeMany(draft, [TheGreatGatsby.id, AnimalFarm.id])
+      })
+      expect(result).toMatchInlineSnapshot(`
+        Object {
+          "entities": Object {
+            "aco": Object {
+              "id": "aco",
+              "title": "A Clockwork Orange",
+            },
+          },
+          "ids": Array [
+            "aco",
+          ],
+        }
+      `)
+    })
+  })
 })