From 67a5a93e70aef3db19bcbf76506e50bb2266bb0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Stanimirovi=C4=87?= Date: Wed, 19 Jun 2024 00:43:34 +0200 Subject: [PATCH] feat(signals): replace `idKey` with `selectId` when defining custom entity ID (#4396) Closes #4217, #4392 Co-authored-by: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> --- modules/signals/entities/spec/helpers.ts | 4 +++ .../spec/updaters/add-entities.spec.ts | 35 ++++++++++--------- .../entities/spec/updaters/add-entity.spec.ts | 31 ++++++++-------- .../spec/updaters/remove-all-entities.spec.ts | 3 +- .../spec/updaters/remove-entities.spec.ts | 5 +-- .../spec/updaters/remove-entity.spec.ts | 5 +-- .../spec/updaters/set-all-entities.spec.ts | 13 +++---- .../spec/updaters/set-entities.spec.ts | 33 ++++++++--------- .../entities/spec/updaters/set-entity.spec.ts | 25 ++++++------- .../spec/updaters/update-all-entities.spec.ts | 3 +- .../spec/updaters/update-entities.spec.ts | 8 +++-- .../spec/updaters/update-entity.spec.ts | 5 +-- .../entities/spec/with-entities.spec.ts | 3 +- modules/signals/entities/src/helpers.ts | 25 +++++++------ modules/signals/entities/src/index.ts | 8 ++++- modules/signals/entities/src/models.ts | 6 +--- .../entities/src/updaters/add-entities.ts | 14 ++++---- .../entities/src/updaters/add-entity.ts | 14 ++++---- .../entities/src/updaters/set-all-entities.ts | 14 ++++---- .../entities/src/updaters/set-entities.ts | 14 ++++---- .../entities/src/updaters/set-entity.ts | 14 ++++---- .../signals/signal-store/entity-management.md | 16 +++++---- 22 files changed, 164 insertions(+), 134 deletions(-) create mode 100644 modules/signals/entities/spec/helpers.ts diff --git a/modules/signals/entities/spec/helpers.ts b/modules/signals/entities/spec/helpers.ts new file mode 100644 index 0000000000..18caf68705 --- /dev/null +++ b/modules/signals/entities/spec/helpers.ts @@ -0,0 +1,4 @@ +import { SelectEntityId } from '../src'; +import { Todo } from './mocks'; + +export const selectTodoId: SelectEntityId = (todo) => todo._id; diff --git a/modules/signals/entities/spec/updaters/add-entities.spec.ts b/modules/signals/entities/spec/updaters/add-entities.spec.ts index 1a05d188b1..789130fdb6 100644 --- a/modules/signals/entities/spec/updaters/add-entities.spec.ts +++ b/modules/signals/entities/spec/updaters/add-entities.spec.ts @@ -1,6 +1,7 @@ import { patchState, signalStore, type } from '@ngrx/signals'; import { addEntities, withEntities } from '../../src'; import { Todo, todo1, todo2, todo3, User, user1, user2, user3 } from '../mocks'; +import { selectTodoId as selectId } from '../helpers'; describe('addEntities', () => { it('adds entities if they do not exist', () => { @@ -115,11 +116,11 @@ describe('addEntities', () => { expect(store.userEntities()).toEqual([user1, user3, user2]); }); - it('adds entities with the specified idKey if they do not exist', () => { + it('adds entities with a custom id if they do not exist', () => { const Store = signalStore(withEntities()); const store = new Store(); - patchState(store, addEntities([todo2, todo3], { idKey: '_id' })); + patchState(store, addEntities([todo2, todo3], { selectId })); expect(store.entityMap()).toEqual({ y: todo2, z: todo3 }); expect(store.ids()).toEqual(['y', 'z']); @@ -127,8 +128,8 @@ describe('addEntities', () => { patchState( store, - addEntities([todo1], { idKey: '_id' }), - addEntities([] as Todo[], { idKey: '_id' }) + addEntities([todo1], { selectId }), + addEntities([] as Todo[], { selectId }) ); expect(store.entityMap()).toEqual({ y: todo2, z: todo3, x: todo1 }); @@ -136,15 +137,15 @@ describe('addEntities', () => { expect(store.entities()).toEqual([todo2, todo3, todo1]); }); - it('does not add entities with the specified idKey if they already exist', () => { + it('does not add entities with a custom id if they already exist', () => { const Store = signalStore(withEntities()); const store = new Store(); patchState( store, - addEntities([todo1], { idKey: '_id' }), - addEntities([todo2, todo1], { idKey: '_id' }), - addEntities([] as Todo[], { idKey: '_id' }) + addEntities([todo1], { selectId }), + addEntities([todo2, todo1], { selectId }), + addEntities([] as Todo[], { selectId }) ); const entityMap = store.entityMap(); @@ -153,8 +154,8 @@ describe('addEntities', () => { patchState( store, - addEntities([] as Todo[], { idKey: '_id' }), - addEntities([todo2, { ...todo2, text: 'NgRx' }, todo1], { idKey: '_id' }) + addEntities([] as Todo[], { selectId }), + addEntities([todo2, { ...todo2, text: 'NgRx' }, todo1], { selectId }) ); expect(store.entityMap()).toBe(entityMap); @@ -164,14 +165,14 @@ describe('addEntities', () => { expect(store.ids()).toEqual(['x', 'y']); expect(store.entities()).toEqual([todo1, todo2]); - patchState(store, addEntities([todo1, todo3, todo2], { idKey: '_id' })); + patchState(store, addEntities([todo1, todo3, todo2], { selectId })); expect(store.entityMap()).toEqual({ x: todo1, y: todo2, z: todo3 }); expect(store.ids()).toEqual(['x', 'y', 'z']); expect(store.entities()).toEqual([todo1, todo2, todo3]); }); - it('adds entities with the specified idKey to the specified collection if they do not exist', () => { + it('adds entities with a custom id to the specified collection if they do not exist', () => { const Store = signalStore( withEntities({ entity: type(), @@ -184,7 +185,7 @@ describe('addEntities', () => { store, addEntities([todo3, todo2], { collection: 'todo', - idKey: '_id', + selectId, }) ); @@ -194,8 +195,8 @@ describe('addEntities', () => { patchState( store, - addEntities([todo1], { collection: 'todo', idKey: '_id' }), - addEntities([] as Todo[], { collection: 'todo', idKey: '_id' }) + addEntities([todo1], { collection: 'todo', selectId }), + addEntities([] as Todo[], { collection: 'todo', selectId }) ); expect(store.todoEntityMap()).toEqual({ z: todo3, y: todo2, x: todo1 }); @@ -203,11 +204,11 @@ describe('addEntities', () => { expect(store.todoEntities()).toEqual([todo3, todo2, todo1]); }); - it('does not add entities with the specified idKey to the specified collection if they already exist', () => { + it('does not add entities with a custom id to the specified collection if they already exist', () => { const todoMeta = { entity: type(), collection: 'todo', - idKey: '_id', + selectId, } as const; const Store = signalStore(withEntities(todoMeta)); diff --git a/modules/signals/entities/spec/updaters/add-entity.spec.ts b/modules/signals/entities/spec/updaters/add-entity.spec.ts index 26b2f70e9b..0807a141ce 100644 --- a/modules/signals/entities/spec/updaters/add-entity.spec.ts +++ b/modules/signals/entities/spec/updaters/add-entity.spec.ts @@ -1,6 +1,7 @@ import { patchState, signalStore, type } from '@ngrx/signals'; import { addEntity, withEntities } from '../../src'; import { Todo, todo1, todo2, User, user1, user2 } from '../mocks'; +import { selectTodoId as selectId } from '../helpers'; describe('addEntity', () => { it('adds entity if it does not exist', () => { @@ -96,31 +97,31 @@ describe('addEntity', () => { expect(store.userEntities()).toEqual([user1]); }); - it('adds entity with the specified idKey if it does not exist', () => { + it('adds entity with a custom id if it does not exist', () => { const Store = signalStore(withEntities()); const store = new Store(); - patchState(store, addEntity(todo1, { idKey: '_id' })); + patchState(store, addEntity(todo1, { selectId })); expect(store.entityMap()).toEqual({ x: todo1 }); expect(store.ids()).toEqual(['x']); expect(store.entities()).toEqual([todo1]); - patchState(store, addEntity(todo2, { idKey: '_id' })); + patchState(store, addEntity(todo2, { selectId })); expect(store.entityMap()).toEqual({ x: todo1, y: todo2 }); expect(store.ids()).toEqual(['x', 'y']); expect(store.entities()).toEqual([todo1, todo2]); }); - it('does not add entity with the specified idKey if it already exists', () => { + it('does not add entity with a custom id if it already exists', () => { const Store = signalStore(withEntities()); const store = new Store(); patchState( store, - addEntity(todo1, { idKey: '_id' }), - addEntity(todo2, { idKey: '_id' }) + addEntity(todo1, { selectId }), + addEntity(todo2, { selectId }) ); const entityMap = store.entityMap(); @@ -129,10 +130,10 @@ describe('addEntity', () => { patchState( store, - addEntity(todo1, { idKey: '_id' }), - addEntity({ ...todo1, text: 'NgRx' }, { idKey: '_id' }), - addEntity(todo2, { idKey: '_id' }), - addEntity(todo1, { idKey: '_id' }) + addEntity(todo1, { selectId }), + addEntity({ ...todo1, text: 'NgRx' }, { selectId }), + addEntity(todo2, { selectId }), + addEntity(todo1, { selectId }) ); expect(store.entityMap()).toBe(entityMap); @@ -143,7 +144,7 @@ describe('addEntity', () => { expect(store.entities()).toEqual([todo1, todo2]); }); - it('adds entity with the specified idKey to the specified collection if it does not exist', () => { + it('adds entity with a custom id to the specified collection if it does not exist', () => { const Store = signalStore( withEntities({ entity: type(), @@ -152,24 +153,24 @@ describe('addEntity', () => { ); const store = new Store(); - patchState(store, addEntity(todo1, { collection: 'todo', idKey: '_id' })); + patchState(store, addEntity(todo1, { collection: 'todo', selectId })); expect(store.todoEntityMap()).toEqual({ x: todo1 }); expect(store.todoIds()).toEqual(['x']); expect(store.todoEntities()).toEqual([todo1]); - patchState(store, addEntity(todo2, { collection: 'todo', idKey: '_id' })); + patchState(store, addEntity(todo2, { collection: 'todo', selectId })); expect(store.todoEntityMap()).toEqual({ x: todo1, y: todo2 }); expect(store.todoIds()).toEqual(['x', 'y']); expect(store.todoEntities()).toEqual([todo1, todo2]); }); - it('does not add entity with the specified idKey to the specified collection if it already exists', () => { + it('does not add entity with a custom id to the specified collection if it already exists', () => { const todoMeta = { entity: type(), collection: 'todo', - idKey: '_id', + selectId, } as const; const Store = signalStore(withEntities(todoMeta)); diff --git a/modules/signals/entities/spec/updaters/remove-all-entities.spec.ts b/modules/signals/entities/spec/updaters/remove-all-entities.spec.ts index 9978726f82..d8a8d8b160 100644 --- a/modules/signals/entities/spec/updaters/remove-all-entities.spec.ts +++ b/modules/signals/entities/spec/updaters/remove-all-entities.spec.ts @@ -1,6 +1,7 @@ import { patchState, signalStore, type } from '@ngrx/signals'; import { removeAllEntities, setAllEntities, withEntities } from '../../src'; import { Todo, todo1, todo2, User, user1, user2 } from '../mocks'; +import { selectTodoId } from '../helpers'; describe('removeAllEntities', () => { it('removes all entities', () => { @@ -27,7 +28,7 @@ describe('removeAllEntities', () => { store, setAllEntities([todo1, todo2], { collection: 'todo', - idKey: '_id', + selectId: selectTodoId, }) ); patchState(store, removeAllEntities({ collection: 'todo' })); diff --git a/modules/signals/entities/spec/updaters/remove-entities.spec.ts b/modules/signals/entities/spec/updaters/remove-entities.spec.ts index 5ea4b6aa7c..7a9cc9b054 100644 --- a/modules/signals/entities/spec/updaters/remove-entities.spec.ts +++ b/modules/signals/entities/spec/updaters/remove-entities.spec.ts @@ -1,6 +1,7 @@ import { patchState, signalStore, type } from '@ngrx/signals'; import { addEntities, removeEntities, withEntities } from '../../src'; import { Todo, todo1, todo2, todo3, User, user1, user2, user3 } from '../mocks'; +import { selectTodoId } from '../helpers'; describe('removeEntities', () => { it('removes entities by ids', () => { @@ -24,7 +25,7 @@ describe('removeEntities', () => { patchState( store, - addEntities([todo1, todo2, todo3], { idKey: '_id' }), + addEntities([todo1, todo2, todo3], { selectId: selectTodoId }), removeEntities((todo) => todo.completed) ); @@ -99,7 +100,7 @@ describe('removeEntities', () => { const todoMeta = { entity: type(), collection: 'todo', - idKey: '_id', + selectId: selectTodoId, } as const; const Store = signalStore(withEntities(todoMeta)); diff --git a/modules/signals/entities/spec/updaters/remove-entity.spec.ts b/modules/signals/entities/spec/updaters/remove-entity.spec.ts index 1b5ab3e1e1..ae7dea8e76 100644 --- a/modules/signals/entities/spec/updaters/remove-entity.spec.ts +++ b/modules/signals/entities/spec/updaters/remove-entity.spec.ts @@ -1,6 +1,7 @@ import { patchState, signalStore, type } from '@ngrx/signals'; import { addEntities, removeEntity, withEntities } from '../../src'; import { Todo, todo1, todo2, todo3, User, user1, user2 } from '../mocks'; +import { selectTodoId } from '../helpers'; describe('removeEntity', () => { it('removes entity', () => { @@ -18,7 +19,7 @@ describe('removeEntity', () => { const Store = signalStore(withEntities()); const store = new Store(); - patchState(store, addEntities([todo2, todo3], { idKey: '_id' })); + patchState(store, addEntities([todo2, todo3], { selectId: selectTodoId })); const entityMap = store.entityMap(); const ids = store.ids(); @@ -39,7 +40,7 @@ describe('removeEntity', () => { const todoMeta = { entity: type(), collection: 'todo', - idKey: '_id', + selectId: selectTodoId, } as const; const Store = signalStore(withEntities(todoMeta)); diff --git a/modules/signals/entities/spec/updaters/set-all-entities.spec.ts b/modules/signals/entities/spec/updaters/set-all-entities.spec.ts index a7da16804c..e7dd188313 100644 --- a/modules/signals/entities/spec/updaters/set-all-entities.spec.ts +++ b/modules/signals/entities/spec/updaters/set-all-entities.spec.ts @@ -1,6 +1,7 @@ import { patchState, signalStore, type } from '@ngrx/signals'; import { setAllEntities, withEntities } from '../../src'; import { Todo, todo1, todo2, todo3, User, user1, user2, user3 } from '../mocks'; +import { selectTodoId as selectId } from '../helpers'; describe('setAllEntities', () => { it('replaces entity collection with provided entities', () => { @@ -57,34 +58,34 @@ describe('setAllEntities', () => { expect(store.userEntities()).toEqual([]); }); - it('replaces entity collection with provided entities with the specified idKey', () => { + it('replaces entity collection with provided entities with a custom id', () => { const Store = signalStore(withEntities()); const store = new Store(); - patchState(store, setAllEntities([todo2, todo3], { idKey: '_id' })); + patchState(store, setAllEntities([todo2, todo3], { selectId })); expect(store.entityMap()).toEqual({ y: todo2, z: todo3 }); expect(store.ids()).toEqual(['y', 'z']); expect(store.entities()).toEqual([todo2, todo3]); - patchState(store, setAllEntities([todo3, todo2, todo1], { idKey: '_id' })); + patchState(store, setAllEntities([todo3, todo2, todo1], { selectId })); expect(store.entityMap()).toEqual({ z: todo3, y: todo2, x: todo1 }); expect(store.ids()).toEqual(['z', 'y', 'x']); expect(store.entities()).toEqual([todo3, todo2, todo1]); - patchState(store, setAllEntities([] as Todo[], { idKey: '_id' })); + patchState(store, setAllEntities([] as Todo[], { selectId })); expect(store.entityMap()).toEqual({}); expect(store.ids()).toEqual([]); expect(store.entities()).toEqual([]); }); - it('replaces specified entity collection with provided entities with the specified idKey', () => { + it('replaces specified entity collection with provided entities with a custom id', () => { const todoMeta = { entity: type(), collection: 'todo', - idKey: '_id', + selectId, } as const; const Store = signalStore(withEntities(todoMeta)); diff --git a/modules/signals/entities/spec/updaters/set-entities.spec.ts b/modules/signals/entities/spec/updaters/set-entities.spec.ts index ef96d472d7..ed694ee669 100644 --- a/modules/signals/entities/spec/updaters/set-entities.spec.ts +++ b/modules/signals/entities/spec/updaters/set-entities.spec.ts @@ -1,6 +1,7 @@ import { patchState, signalStore, type } from '@ngrx/signals'; import { setEntities, withEntities } from '../../src'; import { Todo, todo1, todo2, todo3, User, user1, user2, user3 } from '../mocks'; +import { selectTodoId as selectId } from '../helpers'; describe('setEntities', () => { it('adds entities if they do not exist', () => { @@ -115,11 +116,11 @@ describe('setEntities', () => { ]); }); - it('adds entities with the specified idKey if they do not exist', () => { + it('adds entities with a custom id if they do not exist', () => { const Store = signalStore(withEntities()); const store = new Store(); - patchState(store, setEntities([todo2, todo3], { idKey: '_id' })); + patchState(store, setEntities([todo2, todo3], { selectId })); expect(store.entityMap()).toEqual({ y: todo2, z: todo3 }); expect(store.ids()).toEqual(['y', 'z']); @@ -127,8 +128,8 @@ describe('setEntities', () => { patchState( store, - setEntities([todo1], { idKey: '_id' }), - setEntities([] as Todo[], { idKey: '_id' }) + setEntities([todo1], { selectId }), + setEntities([] as Todo[], { selectId }) ); expect(store.entityMap()).toEqual({ y: todo2, z: todo3, x: todo1 }); @@ -136,22 +137,22 @@ describe('setEntities', () => { expect(store.entities()).toEqual([todo2, todo3, todo1]); }); - it('replaces entities with the specified idKey if they already exist', () => { + it('replaces entities with a custom id if they already exist', () => { const Store = signalStore(withEntities()); const store = new Store(); patchState( store, - setEntities([todo1], { idKey: '_id' }), - setEntities([todo2, { ...todo1, text: 'Signals' }], { idKey: '_id' }), - setEntities([] as Todo[], { idKey: '_id' }) + setEntities([todo1], { selectId }), + setEntities([todo2, { ...todo1, text: 'Signals' }], { selectId }), + setEntities([] as Todo[], { selectId }) ); patchState( store, - setEntities([] as Todo[], { idKey: '_id' }), + setEntities([] as Todo[], { selectId }), setEntities([todo3, todo2, { ...todo2, text: 'NgRx' }, todo1], { - idKey: '_id', + selectId, }) ); @@ -168,7 +169,7 @@ describe('setEntities', () => { ]); }); - it('adds entities with the specified idKey to the specified collection if they do not exist', () => { + it('adds entities with a custom id to the specified collection if they do not exist', () => { const Store = signalStore( withEntities({ entity: type(), @@ -181,7 +182,7 @@ describe('setEntities', () => { store, setEntities([todo3, todo2], { collection: 'todo', - idKey: '_id', + selectId, }) ); @@ -191,8 +192,8 @@ describe('setEntities', () => { patchState( store, - setEntities([todo1], { collection: 'todo', idKey: '_id' }), - setEntities([] as Todo[], { collection: 'todo', idKey: '_id' }) + setEntities([todo1], { collection: 'todo', selectId }), + setEntities([] as Todo[], { collection: 'todo', selectId }) ); expect(store.todoEntityMap()).toEqual({ z: todo3, y: todo2, x: todo1 }); @@ -200,11 +201,11 @@ describe('setEntities', () => { expect(store.todoEntities()).toEqual([todo3, todo2, todo1]); }); - it('replaces entities with the specified idKey to the specified collection if they already exist', () => { + it('replaces entities with a custom id to the specified collection if they already exist', () => { const todoMeta = { entity: type(), collection: 'todo', - idKey: '_id', + selectId, } as const; const Store = signalStore(withEntities(todoMeta)); diff --git a/modules/signals/entities/spec/updaters/set-entity.spec.ts b/modules/signals/entities/spec/updaters/set-entity.spec.ts index 495b7dac7a..30347622a3 100644 --- a/modules/signals/entities/spec/updaters/set-entity.spec.ts +++ b/modules/signals/entities/spec/updaters/set-entity.spec.ts @@ -1,6 +1,7 @@ import { patchState, signalStore, type } from '@ngrx/signals'; import { setEntity, withEntities } from '../../src'; import { Todo, todo1, todo2, User, user1, user2, user3 } from '../mocks'; +import { selectTodoId as selectId } from '../helpers'; describe('setEntity', () => { it('adds entity if it does not exist', () => { @@ -86,33 +87,33 @@ describe('setEntity', () => { ]); }); - it('adds entity with the specified idKey if it does not exist', () => { + it('adds entity with a custom id if it does not exist', () => { const Store = signalStore(withEntities()); const store = new Store(); - patchState(store, setEntity(todo1, { idKey: '_id' })); + patchState(store, setEntity(todo1, { selectId })); expect(store.entityMap()).toEqual({ x: todo1 }); expect(store.ids()).toEqual(['x']); expect(store.entities()).toEqual([todo1]); - patchState(store, setEntity(todo2, { idKey: '_id' })); + patchState(store, setEntity(todo2, { selectId })); expect(store.entityMap()).toEqual({ x: todo1, y: todo2 }); expect(store.ids()).toEqual(['x', 'y']); expect(store.entities()).toEqual([todo1, todo2]); }); - it('replaces entity with the specified idKey if it already exists', () => { + it('replaces entity with a custom id if it already exists', () => { const Store = signalStore(withEntities()); const store = new Store(); patchState( store, - setEntity(todo1, { idKey: '_id' }), - setEntity(todo2, { idKey: '_id' }) + setEntity(todo1, { selectId }), + setEntity(todo2, { selectId }) ); - patchState(store, setEntity({ ...todo2, text: 'NgRx' }, { idKey: '_id' })); + patchState(store, setEntity({ ...todo2, text: 'NgRx' }, { selectId })); expect(store.entityMap()).toEqual({ x: todo1, @@ -122,7 +123,7 @@ describe('setEntity', () => { expect(store.entities()).toEqual([todo1, { ...todo2, text: 'NgRx' }]); }); - it('adds entity with the specified idKey to the specified collection if it does not exist', () => { + it('adds entity with a custom id to the specified collection if it does not exist', () => { const Store = signalStore( withEntities({ entity: type(), @@ -131,24 +132,24 @@ describe('setEntity', () => { ); const store = new Store(); - patchState(store, setEntity(todo1, { collection: 'todo', idKey: '_id' })); + patchState(store, setEntity(todo1, { collection: 'todo', selectId })); expect(store.todoEntityMap()).toEqual({ x: todo1 }); expect(store.todoIds()).toEqual(['x']); expect(store.todoEntities()).toEqual([todo1]); - patchState(store, setEntity(todo2, { collection: 'todo', idKey: '_id' })); + patchState(store, setEntity(todo2, { collection: 'todo', selectId })); expect(store.todoEntityMap()).toEqual({ x: todo1, y: todo2 }); expect(store.todoIds()).toEqual(['x', 'y']); expect(store.todoEntities()).toEqual([todo1, todo2]); }); - it('replaces entity with the specified idKey to the specified collection if it already exists', () => { + it('replaces entity with a custom id to the specified collection if it already exists', () => { const todoMeta = { entity: type(), collection: 'todo', - idKey: '_id', + selectId, } as const; const Store = signalStore(withEntities(todoMeta)); diff --git a/modules/signals/entities/spec/updaters/update-all-entities.spec.ts b/modules/signals/entities/spec/updaters/update-all-entities.spec.ts index 6da99351bd..1324147461 100644 --- a/modules/signals/entities/spec/updaters/update-all-entities.spec.ts +++ b/modules/signals/entities/spec/updaters/update-all-entities.spec.ts @@ -1,6 +1,7 @@ import { patchState, signalStore, type } from '@ngrx/signals'; import { addEntities, updateAllEntities, withEntities } from '../../src'; import { Todo, todo1, todo2, todo3, User, user1, user2, user3 } from '../mocks'; +import { selectTodoId } from '../helpers'; describe('updateAllEntities', () => { it('updates all entities', () => { @@ -76,7 +77,7 @@ describe('updateAllEntities', () => { store, addEntities([todo1, todo2, todo3], { collection: 'todo', - idKey: '_id', + selectId: selectTodoId, }), updateAllEntities({ completed: false }, { collection: 'todo' }) ); diff --git a/modules/signals/entities/spec/updaters/update-entities.spec.ts b/modules/signals/entities/spec/updaters/update-entities.spec.ts index b1f0897c8a..265bafe1f9 100644 --- a/modules/signals/entities/spec/updaters/update-entities.spec.ts +++ b/modules/signals/entities/spec/updaters/update-entities.spec.ts @@ -1,6 +1,7 @@ import { patchState, signalStore, type } from '@ngrx/signals'; import { addEntities, updateEntities, withEntities } from '../../src'; import { Todo, todo1, todo2, todo3, User, user1, user2, user3 } from '../mocks'; +import { selectTodoId } from '../helpers'; describe('updateEntities', () => { it('updates entities by ids', () => { @@ -37,7 +38,10 @@ describe('updateEntities', () => { const Store = signalStore(withEntities()); const store = new Store(); - patchState(store, addEntities([todo1, todo2, todo3], { idKey: '_id' })); + patchState( + store, + addEntities([todo1, todo2, todo3], { selectId: selectTodoId }) + ); patchState( store, @@ -181,7 +185,7 @@ describe('updateEntities', () => { const todoMeta = { entity: type(), collection: 'todo', - idKey: '_id', + selectId: selectTodoId, } as const; const Store = signalStore(withEntities(todoMeta)); diff --git a/modules/signals/entities/spec/updaters/update-entity.spec.ts b/modules/signals/entities/spec/updaters/update-entity.spec.ts index 6a40adf382..395facd930 100644 --- a/modules/signals/entities/spec/updaters/update-entity.spec.ts +++ b/modules/signals/entities/spec/updaters/update-entity.spec.ts @@ -1,6 +1,7 @@ import { patchState, signalStore, type } from '@ngrx/signals'; import { addEntities, updateEntity, withEntities } from '../../src'; import { Todo, todo1, todo2, todo3, User, user1, user2, user3 } from '../mocks'; +import { selectTodoId } from '../helpers'; describe('updateEntity', () => { it('updates entity', () => { @@ -56,7 +57,7 @@ describe('updateEntity', () => { it('does not modify entity state if entity do not exist', () => { const todoMeta = { entity: type(), - idKey: '_id', + selectId: selectTodoId, } as const; const Store = signalStore(withEntities(todoMeta)); @@ -102,7 +103,7 @@ describe('updateEntity', () => { store, addEntities([todo1, todo2, todo3], { collection: 'todo', - idKey: '_id', + selectId: selectTodoId, }), updateEntity( { id: todo1._id, changes: { text: '' } }, diff --git a/modules/signals/entities/spec/with-entities.spec.ts b/modules/signals/entities/spec/with-entities.spec.ts index 50b3178a13..da12e530fc 100644 --- a/modules/signals/entities/spec/with-entities.spec.ts +++ b/modules/signals/entities/spec/with-entities.spec.ts @@ -2,6 +2,7 @@ import { isSignal } from '@angular/core'; import { patchState, signalStore, type } from '@ngrx/signals'; import { addEntities, withEntities } from '../src'; import { Todo, todo2, todo3, User, user1, user2 } from './mocks'; +import { selectTodoId } from './helpers'; describe('withEntities', () => { it('adds entity feature to the store', () => { @@ -50,7 +51,7 @@ describe('withEntities', () => { const todoMeta = { entity: type(), collection: 'todo', - idKey: '_id', + selectId: selectTodoId, } as const; const Store = signalStore(withEntities(), withEntities(todoMeta)); diff --git a/modules/signals/entities/src/helpers.ts b/modules/signals/entities/src/helpers.ts index 13cb6c48ce..5fdb744300 100644 --- a/modules/signals/entities/src/helpers.ts +++ b/modules/signals/entities/src/helpers.ts @@ -4,10 +4,15 @@ import { EntityId, EntityPredicate, EntityState, + SelectEntityId, } from './models'; -export function getEntityIdKey(config?: { idKey?: string }): string { - return config?.idKey ?? 'id'; +const defaultSelectId: SelectEntityId<{ id: EntityId }> = (entity) => entity.id; + +export function getEntityIdSelector(config?: { + selectId?: SelectEntityId; +}): SelectEntityId { + return config?.selectId ?? defaultSelectId; } export function getEntityStateKeys(config?: { collection?: string }): { @@ -65,9 +70,9 @@ export function getEntityUpdaterResult( export function addEntityMutably( state: EntityState, entity: any, - idKey: string + selectId: SelectEntityId ): DidMutate { - const id = entity[idKey]; + const id = selectId(entity); if (state.entityMap[id]) { return DidMutate.None; @@ -82,12 +87,12 @@ export function addEntityMutably( export function addEntitiesMutably( state: EntityState, entities: any[], - idKey: string + selectId: SelectEntityId ): DidMutate { let didMutate = DidMutate.None; for (const entity of entities) { - const result = addEntityMutably(state, entity, idKey); + const result = addEntityMutably(state, entity, selectId); if (result === DidMutate.Both) { didMutate = result; @@ -100,9 +105,9 @@ export function addEntitiesMutably( export function setEntityMutably( state: EntityState, entity: any, - idKey: string + selectId: SelectEntityId ): DidMutate { - const id = entity[idKey]; + const id = selectId(entity); if (state.entityMap[id]) { state.entityMap[id] = entity; @@ -118,12 +123,12 @@ export function setEntityMutably( export function setEntitiesMutably( state: EntityState, entities: any[], - idKey: string + selectId: SelectEntityId ): DidMutate { let didMutate = DidMutate.None; for (const entity of entities) { - const result = setEntityMutably(state, entity, idKey); + const result = setEntityMutably(state, entity, selectId); if (didMutate === DidMutate.Both) { continue; diff --git a/modules/signals/entities/src/index.ts b/modules/signals/entities/src/index.ts index 33b6d53b01..12cbb7659d 100644 --- a/modules/signals/entities/src/index.ts +++ b/modules/signals/entities/src/index.ts @@ -10,5 +10,11 @@ export { updateEntity } from './updaters/update-entity'; export { updateEntities } from './updaters/update-entities'; export { updateAllEntities } from './updaters/update-all-entities'; -export { EntityId, EntityMap, EntityState, NamedEntityState } from './models'; +export { + EntityId, + EntityMap, + EntityState, + NamedEntityState, + SelectEntityId, +} from './models'; export { withEntities } from './with-entities'; diff --git a/modules/signals/entities/src/models.ts b/modules/signals/entities/src/models.ts index 1f89ad6c30..ae23bb830c 100644 --- a/modules/signals/entities/src/models.ts +++ b/modules/signals/entities/src/models.ts @@ -21,11 +21,7 @@ export type NamedEntityComputed = { [K in keyof EntityComputed as `${Collection}${Capitalize}`]: EntityComputed[K]; }; -export type EntityIdProps = { - [K in keyof Entity as Entity[K] extends EntityId ? K : never]: Entity[K]; -}; - -export type EntityIdKey = keyof EntityIdProps & string; +export type SelectEntityId = (entity: Entity) => EntityId; export type EntityPredicate = (entity: Entity) => boolean; diff --git a/modules/signals/entities/src/updaters/add-entities.ts b/modules/signals/entities/src/updaters/add-entities.ts index a79598f2c9..dc26260ee7 100644 --- a/modules/signals/entities/src/updaters/add-entities.ts +++ b/modules/signals/entities/src/updaters/add-entities.ts @@ -1,14 +1,14 @@ import { PartialStateUpdater } from '@ngrx/signals'; import { EntityId, - EntityIdKey, EntityState, NamedEntityState, + SelectEntityId, } from '../models'; import { addEntitiesMutably, cloneEntityState, - getEntityIdKey, + getEntityIdSelector, getEntityStateKeys, getEntityUpdaterResult, } from '../helpers'; @@ -18,7 +18,7 @@ export function addEntities( ): PartialStateUpdater>; export function addEntities( entities: Entity[], - config: { collection: Collection; idKey: EntityIdKey } + config: { collection: Collection; selectId: SelectEntityId> } ): PartialStateUpdater>; export function addEntities< Entity extends { id: EntityId }, @@ -29,18 +29,18 @@ export function addEntities< ): PartialStateUpdater>; export function addEntities( entities: Entity[], - config: { idKey: EntityIdKey } + config: { selectId: SelectEntityId> } ): PartialStateUpdater>; export function addEntities( entities: any[], - config?: { collection?: string; idKey?: string } + config?: { collection?: string; selectId?: SelectEntityId } ): PartialStateUpdater | NamedEntityState> { - const idKey = getEntityIdKey(config); + const selectId = getEntityIdSelector(config); const stateKeys = getEntityStateKeys(config); return (state) => { const clonedState = cloneEntityState(state, stateKeys); - const didMutate = addEntitiesMutably(clonedState, entities, idKey); + const didMutate = addEntitiesMutably(clonedState, entities, selectId); return getEntityUpdaterResult(clonedState, stateKeys, didMutate); }; diff --git a/modules/signals/entities/src/updaters/add-entity.ts b/modules/signals/entities/src/updaters/add-entity.ts index 9e1cc59359..cdd4ee4b5a 100644 --- a/modules/signals/entities/src/updaters/add-entity.ts +++ b/modules/signals/entities/src/updaters/add-entity.ts @@ -1,14 +1,14 @@ import { PartialStateUpdater } from '@ngrx/signals'; import { EntityId, - EntityIdKey, EntityState, NamedEntityState, + SelectEntityId, } from '../models'; import { addEntityMutably, cloneEntityState, - getEntityIdKey, + getEntityIdSelector, getEntityStateKeys, getEntityUpdaterResult, } from '../helpers'; @@ -18,7 +18,7 @@ export function addEntity( ): PartialStateUpdater>; export function addEntity( entity: Entity, - config: { collection: Collection; idKey: EntityIdKey } + config: { collection: Collection; selectId: SelectEntityId> } ): PartialStateUpdater>; export function addEntity< Entity extends { id: EntityId }, @@ -29,18 +29,18 @@ export function addEntity< ): PartialStateUpdater>; export function addEntity( entity: Entity, - config: { idKey: EntityIdKey } + config: { selectId: SelectEntityId> } ): PartialStateUpdater>; export function addEntity( entity: any, - config?: { collection?: string; idKey?: string } + config?: { collection?: string; selectId?: SelectEntityId } ): PartialStateUpdater | NamedEntityState> { - const idKey = getEntityIdKey(config); + const selectId = getEntityIdSelector(config); const stateKeys = getEntityStateKeys(config); return (state) => { const clonedState = cloneEntityState(state, stateKeys); - const didMutate = addEntityMutably(clonedState, entity, idKey); + const didMutate = addEntityMutably(clonedState, entity, selectId); return getEntityUpdaterResult(clonedState, stateKeys, didMutate); }; diff --git a/modules/signals/entities/src/updaters/set-all-entities.ts b/modules/signals/entities/src/updaters/set-all-entities.ts index afdaa3f8b5..3a2c56b799 100644 --- a/modules/signals/entities/src/updaters/set-all-entities.ts +++ b/modules/signals/entities/src/updaters/set-all-entities.ts @@ -1,12 +1,12 @@ import { PartialStateUpdater } from '@ngrx/signals'; import { EntityId, - EntityIdKey, EntityState, NamedEntityState, + SelectEntityId, } from '../models'; import { - getEntityIdKey, + getEntityIdSelector, getEntityStateKeys, setEntitiesMutably, } from '../helpers'; @@ -16,7 +16,7 @@ export function setAllEntities( ): PartialStateUpdater>; export function setAllEntities( entities: Entity[], - config: { collection: Collection; idKey: EntityIdKey } + config: { collection: Collection; selectId: SelectEntityId> } ): PartialStateUpdater>; export function setAllEntities< Entity extends { id: EntityId }, @@ -27,18 +27,18 @@ export function setAllEntities< ): PartialStateUpdater>; export function setAllEntities( entities: Entity[], - config: { idKey: EntityIdKey } + config: { selectId: SelectEntityId> } ): PartialStateUpdater>; export function setAllEntities( entities: any[], - config?: { collection?: string; idKey?: string } + config?: { collection?: string; selectId?: SelectEntityId } ): PartialStateUpdater | NamedEntityState> { - const idKey = getEntityIdKey(config); + const selectId = getEntityIdSelector(config); const stateKeys = getEntityStateKeys(config); return () => { const state: EntityState = { entityMap: {}, ids: [] }; - setEntitiesMutably(state, entities, idKey); + setEntitiesMutably(state, entities, selectId); return { [stateKeys.entityMapKey]: state.entityMap, diff --git a/modules/signals/entities/src/updaters/set-entities.ts b/modules/signals/entities/src/updaters/set-entities.ts index ea147d67b5..c6339a0cb6 100644 --- a/modules/signals/entities/src/updaters/set-entities.ts +++ b/modules/signals/entities/src/updaters/set-entities.ts @@ -1,13 +1,13 @@ import { PartialStateUpdater } from '@ngrx/signals'; import { EntityId, - EntityIdKey, EntityState, NamedEntityState, + SelectEntityId, } from '../models'; import { cloneEntityState, - getEntityIdKey, + getEntityIdSelector, getEntityStateKeys, getEntityUpdaterResult, setEntitiesMutably, @@ -18,7 +18,7 @@ export function setEntities( ): PartialStateUpdater>; export function setEntities( entities: Entity[], - config: { collection: Collection; idKey: EntityIdKey } + config: { collection: Collection; selectId: SelectEntityId> } ): PartialStateUpdater>; export function setEntities< Entity extends { id: EntityId }, @@ -29,18 +29,18 @@ export function setEntities< ): PartialStateUpdater>; export function setEntities( entities: Entity[], - config: { idKey: EntityIdKey } + config: { selectId: SelectEntityId> } ): PartialStateUpdater>; export function setEntities( entities: any[], - config?: { collection?: string; idKey?: string } + config?: { collection?: string; selectId?: SelectEntityId } ): PartialStateUpdater | NamedEntityState> { - const idKey = getEntityIdKey(config); + const selectId = getEntityIdSelector(config); const stateKeys = getEntityStateKeys(config); return (state) => { const clonedState = cloneEntityState(state, stateKeys); - const didMutate = setEntitiesMutably(clonedState, entities, idKey); + const didMutate = setEntitiesMutably(clonedState, entities, selectId); return getEntityUpdaterResult(clonedState, stateKeys, didMutate); }; diff --git a/modules/signals/entities/src/updaters/set-entity.ts b/modules/signals/entities/src/updaters/set-entity.ts index e691114281..d9dd1f1b98 100644 --- a/modules/signals/entities/src/updaters/set-entity.ts +++ b/modules/signals/entities/src/updaters/set-entity.ts @@ -1,13 +1,13 @@ import { PartialStateUpdater } from '@ngrx/signals'; import { EntityId, - EntityIdKey, EntityState, NamedEntityState, + SelectEntityId, } from '../models'; import { cloneEntityState, - getEntityIdKey, + getEntityIdSelector, getEntityStateKeys, getEntityUpdaterResult, setEntityMutably, @@ -18,7 +18,7 @@ export function setEntity( ): PartialStateUpdater>; export function setEntity( entity: Entity, - config: { collection: Collection; idKey: EntityIdKey } + config: { collection: Collection; selectId: SelectEntityId> } ): PartialStateUpdater>; export function setEntity< Entity extends { id: EntityId }, @@ -29,18 +29,18 @@ export function setEntity< ): PartialStateUpdater>; export function setEntity( entity: Entity, - config: { idKey: EntityIdKey } + config: { selectId: SelectEntityId> } ): PartialStateUpdater>; export function setEntity( entity: any, - config?: { collection?: string; idKey?: string } + config?: { collection?: string; selectId?: SelectEntityId } ): PartialStateUpdater | NamedEntityState> { - const idKey = getEntityIdKey(config); + const selectId = getEntityIdSelector(config); const stateKeys = getEntityStateKeys(config); return (state) => { const clonedState = cloneEntityState(state, stateKeys); - const didMutate = setEntityMutably(clonedState, entity, idKey); + const didMutate = setEntityMutably(clonedState, entity, selectId); return getEntityUpdaterResult(clonedState, stateKeys, didMutate); }; diff --git a/projects/ngrx.io/content/guide/signals/signal-store/entity-management.md b/projects/ngrx.io/content/guide/signals/signal-store/entity-management.md index 5bb049ddf5..e0b2cc076d 100644 --- a/projects/ngrx.io/content/guide/signals/signal-store/entity-management.md +++ b/projects/ngrx.io/content/guide/signals/signal-store/entity-management.md @@ -217,9 +217,9 @@ patchState(this.todoStore, removeEntities([2, 4])); The default property name for an identifier is `id` and is of type `string` or `number`. -It is possible to specify a different name, but the type must still be `string` or `number`. You can specify the id only when adding or setting an entity. It is not possible to define it via `withEntities`. +It is possible to specify a custom ID selector, but the return type must still be a `string` or `number`. Custom ID selectors should be provided when adding or setting an entity. It is not possible to define it via `withEntities`. -Therefore, all variations of the `add*` and `set*` functions have an optional (last) parameter, which is an object literal and allows to define the id property via `idKey`. +Therefore, all variations of the `add*` and `set*` functions have an optional (last) parameter, which is an object literal that allows to specify the `selectId` function. For example: @@ -230,6 +230,8 @@ interface Todo { finished: boolean; } +const selectId: SelectEntityId = (todo) => todo.key; + patchState( this.todoStore, addEntities( @@ -237,11 +239,11 @@ patchState( { key: 2, name: 'Car Washing', finished: false }, { key: 3, name: 'Cat Feeding', finished: false }, ], - { idKey: 'key' } + { selectId } ) ); -patchState(this.todoStore, setEntity({ key: 4, name: 'Dog Feeding', finished: false }, { idKey: 'key' })); +patchState(this.todoStore, setEntity({ key: 4, name: 'Dog Feeding', finished: false }, { selectId })); ``` The `update*` and `remove*` methods, which expect an id value, automatically pick the right one. That is possible because every entity belongs to a map with its id as the key. @@ -306,9 +308,11 @@ The names of the state properties changed from: All functions that operate on entities require a collection parameter. Those are `add*`, `set*`, `update*`, and `remove*`. They are type-safe because you need to provide the collection to avoid getting a compilation error. -If you have a customized id property, you need to include the `idKey` parameter in the object literal, too: +If you have a customized id property, you need to include the `selectId` function in the object literal, too: ```typescript +const selectId: SelectEntityId = (todo) => todo.key; + patchState( this.todoStore, addEntities( @@ -316,7 +320,7 @@ patchState( { key: 2, name: 'Car Washing', finished: false }, { key: 3, name: 'Cat Feeding', finished: false }, ], - { idKey: 'key', collection: 'todo' } + { selectId, collection: 'todo' } ) ); ```