Skip to content

Commit 67a5a93

Browse files
markostanimirovictimdeschryver
andauthoredJun 18, 2024··
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>
1 parent 05f0940 commit 67a5a93

22 files changed

+164
-134
lines changed
 
+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { SelectEntityId } from '../src';
2+
import { Todo } from './mocks';
3+
4+
export const selectTodoId: SelectEntityId<Todo> = (todo) => todo._id;

‎modules/signals/entities/spec/updaters/add-entities.spec.ts

+18-17
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { patchState, signalStore, type } from '@ngrx/signals';
22
import { addEntities, withEntities } from '../../src';
33
import { Todo, todo1, todo2, todo3, User, user1, user2, user3 } from '../mocks';
4+
import { selectTodoId as selectId } from '../helpers';
45

56
describe('addEntities', () => {
67
it('adds entities if they do not exist', () => {
@@ -115,36 +116,36 @@ describe('addEntities', () => {
115116
expect(store.userEntities()).toEqual([user1, user3, user2]);
116117
});
117118

118-
it('adds entities with the specified idKey if they do not exist', () => {
119+
it('adds entities with a custom id if they do not exist', () => {
119120
const Store = signalStore(withEntities<Todo>());
120121
const store = new Store();
121122

122-
patchState(store, addEntities([todo2, todo3], { idKey: '_id' }));
123+
patchState(store, addEntities([todo2, todo3], { selectId }));
123124

124125
expect(store.entityMap()).toEqual({ y: todo2, z: todo3 });
125126
expect(store.ids()).toEqual(['y', 'z']);
126127
expect(store.entities()).toEqual([todo2, todo3]);
127128

128129
patchState(
129130
store,
130-
addEntities([todo1], { idKey: '_id' }),
131-
addEntities([] as Todo[], { idKey: '_id' })
131+
addEntities([todo1], { selectId }),
132+
addEntities([] as Todo[], { selectId })
132133
);
133134

134135
expect(store.entityMap()).toEqual({ y: todo2, z: todo3, x: todo1 });
135136
expect(store.ids()).toEqual(['y', 'z', 'x']);
136137
expect(store.entities()).toEqual([todo2, todo3, todo1]);
137138
});
138139

139-
it('does not add entities with the specified idKey if they already exist', () => {
140+
it('does not add entities with a custom id if they already exist', () => {
140141
const Store = signalStore(withEntities<Todo>());
141142
const store = new Store();
142143

143144
patchState(
144145
store,
145-
addEntities([todo1], { idKey: '_id' }),
146-
addEntities([todo2, todo1], { idKey: '_id' }),
147-
addEntities([] as Todo[], { idKey: '_id' })
146+
addEntities([todo1], { selectId }),
147+
addEntities([todo2, todo1], { selectId }),
148+
addEntities([] as Todo[], { selectId })
148149
);
149150

150151
const entityMap = store.entityMap();
@@ -153,8 +154,8 @@ describe('addEntities', () => {
153154

154155
patchState(
155156
store,
156-
addEntities([] as Todo[], { idKey: '_id' }),
157-
addEntities([todo2, { ...todo2, text: 'NgRx' }, todo1], { idKey: '_id' })
157+
addEntities([] as Todo[], { selectId }),
158+
addEntities([todo2, { ...todo2, text: 'NgRx' }, todo1], { selectId })
158159
);
159160

160161
expect(store.entityMap()).toBe(entityMap);
@@ -164,14 +165,14 @@ describe('addEntities', () => {
164165
expect(store.ids()).toEqual(['x', 'y']);
165166
expect(store.entities()).toEqual([todo1, todo2]);
166167

167-
patchState(store, addEntities([todo1, todo3, todo2], { idKey: '_id' }));
168+
patchState(store, addEntities([todo1, todo3, todo2], { selectId }));
168169

169170
expect(store.entityMap()).toEqual({ x: todo1, y: todo2, z: todo3 });
170171
expect(store.ids()).toEqual(['x', 'y', 'z']);
171172
expect(store.entities()).toEqual([todo1, todo2, todo3]);
172173
});
173174

174-
it('adds entities with the specified idKey to the specified collection if they do not exist', () => {
175+
it('adds entities with a custom id to the specified collection if they do not exist', () => {
175176
const Store = signalStore(
176177
withEntities({
177178
entity: type<Todo>(),
@@ -184,7 +185,7 @@ describe('addEntities', () => {
184185
store,
185186
addEntities([todo3, todo2], {
186187
collection: 'todo',
187-
idKey: '_id',
188+
selectId,
188189
})
189190
);
190191

@@ -194,20 +195,20 @@ describe('addEntities', () => {
194195

195196
patchState(
196197
store,
197-
addEntities([todo1], { collection: 'todo', idKey: '_id' }),
198-
addEntities([] as Todo[], { collection: 'todo', idKey: '_id' })
198+
addEntities([todo1], { collection: 'todo', selectId }),
199+
addEntities([] as Todo[], { collection: 'todo', selectId })
199200
);
200201

201202
expect(store.todoEntityMap()).toEqual({ z: todo3, y: todo2, x: todo1 });
202203
expect(store.todoIds()).toEqual(['z', 'y', 'x']);
203204
expect(store.todoEntities()).toEqual([todo3, todo2, todo1]);
204205
});
205206

206-
it('does not add entities with the specified idKey to the specified collection if they already exist', () => {
207+
it('does not add entities with a custom id to the specified collection if they already exist', () => {
207208
const todoMeta = {
208209
entity: type<Todo>(),
209210
collection: 'todo',
210-
idKey: '_id',
211+
selectId,
211212
} as const;
212213

213214
const Store = signalStore(withEntities(todoMeta));

‎modules/signals/entities/spec/updaters/add-entity.spec.ts

+16-15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { patchState, signalStore, type } from '@ngrx/signals';
22
import { addEntity, withEntities } from '../../src';
33
import { Todo, todo1, todo2, User, user1, user2 } from '../mocks';
4+
import { selectTodoId as selectId } from '../helpers';
45

56
describe('addEntity', () => {
67
it('adds entity if it does not exist', () => {
@@ -96,31 +97,31 @@ describe('addEntity', () => {
9697
expect(store.userEntities()).toEqual([user1]);
9798
});
9899

99-
it('adds entity with the specified idKey if it does not exist', () => {
100+
it('adds entity with a custom id if it does not exist', () => {
100101
const Store = signalStore(withEntities<Todo>());
101102
const store = new Store();
102103

103-
patchState(store, addEntity(todo1, { idKey: '_id' }));
104+
patchState(store, addEntity(todo1, { selectId }));
104105

105106
expect(store.entityMap()).toEqual({ x: todo1 });
106107
expect(store.ids()).toEqual(['x']);
107108
expect(store.entities()).toEqual([todo1]);
108109

109-
patchState(store, addEntity(todo2, { idKey: '_id' }));
110+
patchState(store, addEntity(todo2, { selectId }));
110111

111112
expect(store.entityMap()).toEqual({ x: todo1, y: todo2 });
112113
expect(store.ids()).toEqual(['x', 'y']);
113114
expect(store.entities()).toEqual([todo1, todo2]);
114115
});
115116

116-
it('does not add entity with the specified idKey if it already exists', () => {
117+
it('does not add entity with a custom id if it already exists', () => {
117118
const Store = signalStore(withEntities<Todo>());
118119
const store = new Store();
119120

120121
patchState(
121122
store,
122-
addEntity(todo1, { idKey: '_id' }),
123-
addEntity(todo2, { idKey: '_id' })
123+
addEntity(todo1, { selectId }),
124+
addEntity(todo2, { selectId })
124125
);
125126

126127
const entityMap = store.entityMap();
@@ -129,10 +130,10 @@ describe('addEntity', () => {
129130

130131
patchState(
131132
store,
132-
addEntity(todo1, { idKey: '_id' }),
133-
addEntity({ ...todo1, text: 'NgRx' }, { idKey: '_id' }),
134-
addEntity(todo2, { idKey: '_id' }),
135-
addEntity(todo1, { idKey: '_id' })
133+
addEntity(todo1, { selectId }),
134+
addEntity({ ...todo1, text: 'NgRx' }, { selectId }),
135+
addEntity(todo2, { selectId }),
136+
addEntity(todo1, { selectId })
136137
);
137138

138139
expect(store.entityMap()).toBe(entityMap);
@@ -143,7 +144,7 @@ describe('addEntity', () => {
143144
expect(store.entities()).toEqual([todo1, todo2]);
144145
});
145146

146-
it('adds entity with the specified idKey to the specified collection if it does not exist', () => {
147+
it('adds entity with a custom id to the specified collection if it does not exist', () => {
147148
const Store = signalStore(
148149
withEntities({
149150
entity: type<Todo>(),
@@ -152,24 +153,24 @@ describe('addEntity', () => {
152153
);
153154
const store = new Store();
154155

155-
patchState(store, addEntity(todo1, { collection: 'todo', idKey: '_id' }));
156+
patchState(store, addEntity(todo1, { collection: 'todo', selectId }));
156157

157158
expect(store.todoEntityMap()).toEqual({ x: todo1 });
158159
expect(store.todoIds()).toEqual(['x']);
159160
expect(store.todoEntities()).toEqual([todo1]);
160161

161-
patchState(store, addEntity(todo2, { collection: 'todo', idKey: '_id' }));
162+
patchState(store, addEntity(todo2, { collection: 'todo', selectId }));
162163

163164
expect(store.todoEntityMap()).toEqual({ x: todo1, y: todo2 });
164165
expect(store.todoIds()).toEqual(['x', 'y']);
165166
expect(store.todoEntities()).toEqual([todo1, todo2]);
166167
});
167168

168-
it('does not add entity with the specified idKey to the specified collection if it already exists', () => {
169+
it('does not add entity with a custom id to the specified collection if it already exists', () => {
169170
const todoMeta = {
170171
entity: type<Todo>(),
171172
collection: 'todo',
172-
idKey: '_id',
173+
selectId,
173174
} as const;
174175

175176
const Store = signalStore(withEntities(todoMeta));

‎modules/signals/entities/spec/updaters/remove-all-entities.spec.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { patchState, signalStore, type } from '@ngrx/signals';
22
import { removeAllEntities, setAllEntities, withEntities } from '../../src';
33
import { Todo, todo1, todo2, User, user1, user2 } from '../mocks';
4+
import { selectTodoId } from '../helpers';
45

56
describe('removeAllEntities', () => {
67
it('removes all entities', () => {
@@ -27,7 +28,7 @@ describe('removeAllEntities', () => {
2728
store,
2829
setAllEntities([todo1, todo2], {
2930
collection: 'todo',
30-
idKey: '_id',
31+
selectId: selectTodoId,
3132
})
3233
);
3334
patchState(store, removeAllEntities({ collection: 'todo' }));

‎modules/signals/entities/spec/updaters/remove-entities.spec.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { patchState, signalStore, type } from '@ngrx/signals';
22
import { addEntities, removeEntities, withEntities } from '../../src';
33
import { Todo, todo1, todo2, todo3, User, user1, user2, user3 } from '../mocks';
4+
import { selectTodoId } from '../helpers';
45

56
describe('removeEntities', () => {
67
it('removes entities by ids', () => {
@@ -24,7 +25,7 @@ describe('removeEntities', () => {
2425

2526
patchState(
2627
store,
27-
addEntities([todo1, todo2, todo3], { idKey: '_id' }),
28+
addEntities([todo1, todo2, todo3], { selectId: selectTodoId }),
2829
removeEntities((todo) => todo.completed)
2930
);
3031

@@ -99,7 +100,7 @@ describe('removeEntities', () => {
99100
const todoMeta = {
100101
entity: type<Todo>(),
101102
collection: 'todo',
102-
idKey: '_id',
103+
selectId: selectTodoId,
103104
} as const;
104105

105106
const Store = signalStore(withEntities(todoMeta));

‎modules/signals/entities/spec/updaters/remove-entity.spec.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { patchState, signalStore, type } from '@ngrx/signals';
22
import { addEntities, removeEntity, withEntities } from '../../src';
33
import { Todo, todo1, todo2, todo3, User, user1, user2 } from '../mocks';
4+
import { selectTodoId } from '../helpers';
45

56
describe('removeEntity', () => {
67
it('removes entity', () => {
@@ -18,7 +19,7 @@ describe('removeEntity', () => {
1819
const Store = signalStore(withEntities<Todo>());
1920
const store = new Store();
2021

21-
patchState(store, addEntities([todo2, todo3], { idKey: '_id' }));
22+
patchState(store, addEntities([todo2, todo3], { selectId: selectTodoId }));
2223

2324
const entityMap = store.entityMap();
2425
const ids = store.ids();
@@ -39,7 +40,7 @@ describe('removeEntity', () => {
3940
const todoMeta = {
4041
entity: type<Todo>(),
4142
collection: 'todo',
42-
idKey: '_id',
43+
selectId: selectTodoId,
4344
} as const;
4445

4546
const Store = signalStore(withEntities(todoMeta));

‎modules/signals/entities/spec/updaters/set-all-entities.spec.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { patchState, signalStore, type } from '@ngrx/signals';
22
import { setAllEntities, withEntities } from '../../src';
33
import { Todo, todo1, todo2, todo3, User, user1, user2, user3 } from '../mocks';
4+
import { selectTodoId as selectId } from '../helpers';
45

56
describe('setAllEntities', () => {
67
it('replaces entity collection with provided entities', () => {
@@ -57,34 +58,34 @@ describe('setAllEntities', () => {
5758
expect(store.userEntities()).toEqual([]);
5859
});
5960

60-
it('replaces entity collection with provided entities with the specified idKey', () => {
61+
it('replaces entity collection with provided entities with a custom id', () => {
6162
const Store = signalStore(withEntities<Todo>());
6263
const store = new Store();
6364

64-
patchState(store, setAllEntities([todo2, todo3], { idKey: '_id' }));
65+
patchState(store, setAllEntities([todo2, todo3], { selectId }));
6566

6667
expect(store.entityMap()).toEqual({ y: todo2, z: todo3 });
6768
expect(store.ids()).toEqual(['y', 'z']);
6869
expect(store.entities()).toEqual([todo2, todo3]);
6970

70-
patchState(store, setAllEntities([todo3, todo2, todo1], { idKey: '_id' }));
71+
patchState(store, setAllEntities([todo3, todo2, todo1], { selectId }));
7172

7273
expect(store.entityMap()).toEqual({ z: todo3, y: todo2, x: todo1 });
7374
expect(store.ids()).toEqual(['z', 'y', 'x']);
7475
expect(store.entities()).toEqual([todo3, todo2, todo1]);
7576

76-
patchState(store, setAllEntities([] as Todo[], { idKey: '_id' }));
77+
patchState(store, setAllEntities([] as Todo[], { selectId }));
7778

7879
expect(store.entityMap()).toEqual({});
7980
expect(store.ids()).toEqual([]);
8081
expect(store.entities()).toEqual([]);
8182
});
8283

83-
it('replaces specified entity collection with provided entities with the specified idKey', () => {
84+
it('replaces specified entity collection with provided entities with a custom id', () => {
8485
const todoMeta = {
8586
entity: type<Todo>(),
8687
collection: 'todo',
87-
idKey: '_id',
88+
selectId,
8889
} as const;
8990

9091
const Store = signalStore(withEntities(todoMeta));

‎modules/signals/entities/spec/updaters/set-entities.spec.ts

+17-16
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { patchState, signalStore, type } from '@ngrx/signals';
22
import { setEntities, withEntities } from '../../src';
33
import { Todo, todo1, todo2, todo3, User, user1, user2, user3 } from '../mocks';
4+
import { selectTodoId as selectId } from '../helpers';
45

56
describe('setEntities', () => {
67
it('adds entities if they do not exist', () => {
@@ -115,43 +116,43 @@ describe('setEntities', () => {
115116
]);
116117
});
117118

118-
it('adds entities with the specified idKey if they do not exist', () => {
119+
it('adds entities with a custom id if they do not exist', () => {
119120
const Store = signalStore(withEntities<Todo>());
120121
const store = new Store();
121122

122-
patchState(store, setEntities([todo2, todo3], { idKey: '_id' }));
123+
patchState(store, setEntities([todo2, todo3], { selectId }));
123124

124125
expect(store.entityMap()).toEqual({ y: todo2, z: todo3 });
125126
expect(store.ids()).toEqual(['y', 'z']);
126127
expect(store.entities()).toEqual([todo2, todo3]);
127128

128129
patchState(
129130
store,
130-
setEntities([todo1], { idKey: '_id' }),
131-
setEntities([] as Todo[], { idKey: '_id' })
131+
setEntities([todo1], { selectId }),
132+
setEntities([] as Todo[], { selectId })
132133
);
133134

134135
expect(store.entityMap()).toEqual({ y: todo2, z: todo3, x: todo1 });
135136
expect(store.ids()).toEqual(['y', 'z', 'x']);
136137
expect(store.entities()).toEqual([todo2, todo3, todo1]);
137138
});
138139

139-
it('replaces entities with the specified idKey if they already exist', () => {
140+
it('replaces entities with a custom id if they already exist', () => {
140141
const Store = signalStore(withEntities<Todo>());
141142
const store = new Store();
142143

143144
patchState(
144145
store,
145-
setEntities([todo1], { idKey: '_id' }),
146-
setEntities([todo2, { ...todo1, text: 'Signals' }], { idKey: '_id' }),
147-
setEntities([] as Todo[], { idKey: '_id' })
146+
setEntities([todo1], { selectId }),
147+
setEntities([todo2, { ...todo1, text: 'Signals' }], { selectId }),
148+
setEntities([] as Todo[], { selectId })
148149
);
149150

150151
patchState(
151152
store,
152-
setEntities([] as Todo[], { idKey: '_id' }),
153+
setEntities([] as Todo[], { selectId }),
153154
setEntities([todo3, todo2, { ...todo2, text: 'NgRx' }, todo1], {
154-
idKey: '_id',
155+
selectId,
155156
})
156157
);
157158

@@ -168,7 +169,7 @@ describe('setEntities', () => {
168169
]);
169170
});
170171

171-
it('adds entities with the specified idKey to the specified collection if they do not exist', () => {
172+
it('adds entities with a custom id to the specified collection if they do not exist', () => {
172173
const Store = signalStore(
173174
withEntities({
174175
entity: type<Todo>(),
@@ -181,7 +182,7 @@ describe('setEntities', () => {
181182
store,
182183
setEntities([todo3, todo2], {
183184
collection: 'todo',
184-
idKey: '_id',
185+
selectId,
185186
})
186187
);
187188

@@ -191,20 +192,20 @@ describe('setEntities', () => {
191192

192193
patchState(
193194
store,
194-
setEntities([todo1], { collection: 'todo', idKey: '_id' }),
195-
setEntities([] as Todo[], { collection: 'todo', idKey: '_id' })
195+
setEntities([todo1], { collection: 'todo', selectId }),
196+
setEntities([] as Todo[], { collection: 'todo', selectId })
196197
);
197198

198199
expect(store.todoEntityMap()).toEqual({ z: todo3, y: todo2, x: todo1 });
199200
expect(store.todoIds()).toEqual(['z', 'y', 'x']);
200201
expect(store.todoEntities()).toEqual([todo3, todo2, todo1]);
201202
});
202203

203-
it('replaces entities with the specified idKey to the specified collection if they already exist', () => {
204+
it('replaces entities with a custom id to the specified collection if they already exist', () => {
204205
const todoMeta = {
205206
entity: type<Todo>(),
206207
collection: 'todo',
207-
idKey: '_id',
208+
selectId,
208209
} as const;
209210

210211
const Store = signalStore(withEntities(todoMeta));

‎modules/signals/entities/spec/updaters/set-entity.spec.ts

+13-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { patchState, signalStore, type } from '@ngrx/signals';
22
import { setEntity, withEntities } from '../../src';
33
import { Todo, todo1, todo2, User, user1, user2, user3 } from '../mocks';
4+
import { selectTodoId as selectId } from '../helpers';
45

56
describe('setEntity', () => {
67
it('adds entity if it does not exist', () => {
@@ -86,33 +87,33 @@ describe('setEntity', () => {
8687
]);
8788
});
8889

89-
it('adds entity with the specified idKey if it does not exist', () => {
90+
it('adds entity with a custom id if it does not exist', () => {
9091
const Store = signalStore(withEntities<Todo>());
9192
const store = new Store();
9293

93-
patchState(store, setEntity(todo1, { idKey: '_id' }));
94+
patchState(store, setEntity(todo1, { selectId }));
9495

9596
expect(store.entityMap()).toEqual({ x: todo1 });
9697
expect(store.ids()).toEqual(['x']);
9798
expect(store.entities()).toEqual([todo1]);
9899

99-
patchState(store, setEntity(todo2, { idKey: '_id' }));
100+
patchState(store, setEntity(todo2, { selectId }));
100101

101102
expect(store.entityMap()).toEqual({ x: todo1, y: todo2 });
102103
expect(store.ids()).toEqual(['x', 'y']);
103104
expect(store.entities()).toEqual([todo1, todo2]);
104105
});
105106

106-
it('replaces entity with the specified idKey if it already exists', () => {
107+
it('replaces entity with a custom id if it already exists', () => {
107108
const Store = signalStore(withEntities<Todo>());
108109
const store = new Store();
109110

110111
patchState(
111112
store,
112-
setEntity(todo1, { idKey: '_id' }),
113-
setEntity(todo2, { idKey: '_id' })
113+
setEntity(todo1, { selectId }),
114+
setEntity(todo2, { selectId })
114115
);
115-
patchState(store, setEntity({ ...todo2, text: 'NgRx' }, { idKey: '_id' }));
116+
patchState(store, setEntity({ ...todo2, text: 'NgRx' }, { selectId }));
116117

117118
expect(store.entityMap()).toEqual({
118119
x: todo1,
@@ -122,7 +123,7 @@ describe('setEntity', () => {
122123
expect(store.entities()).toEqual([todo1, { ...todo2, text: 'NgRx' }]);
123124
});
124125

125-
it('adds entity with the specified idKey to the specified collection if it does not exist', () => {
126+
it('adds entity with a custom id to the specified collection if it does not exist', () => {
126127
const Store = signalStore(
127128
withEntities({
128129
entity: type<Todo>(),
@@ -131,24 +132,24 @@ describe('setEntity', () => {
131132
);
132133
const store = new Store();
133134

134-
patchState(store, setEntity(todo1, { collection: 'todo', idKey: '_id' }));
135+
patchState(store, setEntity(todo1, { collection: 'todo', selectId }));
135136

136137
expect(store.todoEntityMap()).toEqual({ x: todo1 });
137138
expect(store.todoIds()).toEqual(['x']);
138139
expect(store.todoEntities()).toEqual([todo1]);
139140

140-
patchState(store, setEntity(todo2, { collection: 'todo', idKey: '_id' }));
141+
patchState(store, setEntity(todo2, { collection: 'todo', selectId }));
141142

142143
expect(store.todoEntityMap()).toEqual({ x: todo1, y: todo2 });
143144
expect(store.todoIds()).toEqual(['x', 'y']);
144145
expect(store.todoEntities()).toEqual([todo1, todo2]);
145146
});
146147

147-
it('replaces entity with the specified idKey to the specified collection if it already exists', () => {
148+
it('replaces entity with a custom id to the specified collection if it already exists', () => {
148149
const todoMeta = {
149150
entity: type<Todo>(),
150151
collection: 'todo',
151-
idKey: '_id',
152+
selectId,
152153
} as const;
153154

154155
const Store = signalStore(withEntities(todoMeta));

‎modules/signals/entities/spec/updaters/update-all-entities.spec.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { patchState, signalStore, type } from '@ngrx/signals';
22
import { addEntities, updateAllEntities, withEntities } from '../../src';
33
import { Todo, todo1, todo2, todo3, User, user1, user2, user3 } from '../mocks';
4+
import { selectTodoId } from '../helpers';
45

56
describe('updateAllEntities', () => {
67
it('updates all entities', () => {
@@ -76,7 +77,7 @@ describe('updateAllEntities', () => {
7677
store,
7778
addEntities([todo1, todo2, todo3], {
7879
collection: 'todo',
79-
idKey: '_id',
80+
selectId: selectTodoId,
8081
}),
8182
updateAllEntities({ completed: false }, { collection: 'todo' })
8283
);

‎modules/signals/entities/spec/updaters/update-entities.spec.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { patchState, signalStore, type } from '@ngrx/signals';
22
import { addEntities, updateEntities, withEntities } from '../../src';
33
import { Todo, todo1, todo2, todo3, User, user1, user2, user3 } from '../mocks';
4+
import { selectTodoId } from '../helpers';
45

56
describe('updateEntities', () => {
67
it('updates entities by ids', () => {
@@ -37,7 +38,10 @@ describe('updateEntities', () => {
3738
const Store = signalStore(withEntities<Todo>());
3839
const store = new Store();
3940

40-
patchState(store, addEntities([todo1, todo2, todo3], { idKey: '_id' }));
41+
patchState(
42+
store,
43+
addEntities([todo1, todo2, todo3], { selectId: selectTodoId })
44+
);
4145

4246
patchState(
4347
store,
@@ -181,7 +185,7 @@ describe('updateEntities', () => {
181185
const todoMeta = {
182186
entity: type<Todo>(),
183187
collection: 'todo',
184-
idKey: '_id',
188+
selectId: selectTodoId,
185189
} as const;
186190

187191
const Store = signalStore(withEntities(todoMeta));

‎modules/signals/entities/spec/updaters/update-entity.spec.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { patchState, signalStore, type } from '@ngrx/signals';
22
import { addEntities, updateEntity, withEntities } from '../../src';
33
import { Todo, todo1, todo2, todo3, User, user1, user2, user3 } from '../mocks';
4+
import { selectTodoId } from '../helpers';
45

56
describe('updateEntity', () => {
67
it('updates entity', () => {
@@ -56,7 +57,7 @@ describe('updateEntity', () => {
5657
it('does not modify entity state if entity do not exist', () => {
5758
const todoMeta = {
5859
entity: type<Todo>(),
59-
idKey: '_id',
60+
selectId: selectTodoId,
6061
} as const;
6162

6263
const Store = signalStore(withEntities(todoMeta));
@@ -102,7 +103,7 @@ describe('updateEntity', () => {
102103
store,
103104
addEntities([todo1, todo2, todo3], {
104105
collection: 'todo',
105-
idKey: '_id',
106+
selectId: selectTodoId,
106107
}),
107108
updateEntity(
108109
{ id: todo1._id, changes: { text: '' } },

‎modules/signals/entities/spec/with-entities.spec.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { isSignal } from '@angular/core';
22
import { patchState, signalStore, type } from '@ngrx/signals';
33
import { addEntities, withEntities } from '../src';
44
import { Todo, todo2, todo3, User, user1, user2 } from './mocks';
5+
import { selectTodoId } from './helpers';
56

67
describe('withEntities', () => {
78
it('adds entity feature to the store', () => {
@@ -50,7 +51,7 @@ describe('withEntities', () => {
5051
const todoMeta = {
5152
entity: type<Todo>(),
5253
collection: 'todo',
53-
idKey: '_id',
54+
selectId: selectTodoId,
5455
} as const;
5556

5657
const Store = signalStore(withEntities<User>(), withEntities(todoMeta));

‎modules/signals/entities/src/helpers.ts

+15-10
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,15 @@ import {
44
EntityId,
55
EntityPredicate,
66
EntityState,
7+
SelectEntityId,
78
} from './models';
89

9-
export function getEntityIdKey(config?: { idKey?: string }): string {
10-
return config?.idKey ?? 'id';
10+
const defaultSelectId: SelectEntityId<{ id: EntityId }> = (entity) => entity.id;
11+
12+
export function getEntityIdSelector(config?: {
13+
selectId?: SelectEntityId<any>;
14+
}): SelectEntityId<any> {
15+
return config?.selectId ?? defaultSelectId;
1116
}
1217

1318
export function getEntityStateKeys(config?: { collection?: string }): {
@@ -65,9 +70,9 @@ export function getEntityUpdaterResult(
6570
export function addEntityMutably(
6671
state: EntityState<any>,
6772
entity: any,
68-
idKey: string
73+
selectId: SelectEntityId<any>
6974
): DidMutate {
70-
const id = entity[idKey];
75+
const id = selectId(entity);
7176

7277
if (state.entityMap[id]) {
7378
return DidMutate.None;
@@ -82,12 +87,12 @@ export function addEntityMutably(
8287
export function addEntitiesMutably(
8388
state: EntityState<any>,
8489
entities: any[],
85-
idKey: string
90+
selectId: SelectEntityId<any>
8691
): DidMutate {
8792
let didMutate = DidMutate.None;
8893

8994
for (const entity of entities) {
90-
const result = addEntityMutably(state, entity, idKey);
95+
const result = addEntityMutably(state, entity, selectId);
9196

9297
if (result === DidMutate.Both) {
9398
didMutate = result;
@@ -100,9 +105,9 @@ export function addEntitiesMutably(
100105
export function setEntityMutably(
101106
state: EntityState<any>,
102107
entity: any,
103-
idKey: string
108+
selectId: SelectEntityId<any>
104109
): DidMutate {
105-
const id = entity[idKey];
110+
const id = selectId(entity);
106111

107112
if (state.entityMap[id]) {
108113
state.entityMap[id] = entity;
@@ -118,12 +123,12 @@ export function setEntityMutably(
118123
export function setEntitiesMutably(
119124
state: EntityState<any>,
120125
entities: any[],
121-
idKey: string
126+
selectId: SelectEntityId<any>
122127
): DidMutate {
123128
let didMutate = DidMutate.None;
124129

125130
for (const entity of entities) {
126-
const result = setEntityMutably(state, entity, idKey);
131+
const result = setEntityMutably(state, entity, selectId);
127132

128133
if (didMutate === DidMutate.Both) {
129134
continue;

‎modules/signals/entities/src/index.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,11 @@ export { updateEntity } from './updaters/update-entity';
1010
export { updateEntities } from './updaters/update-entities';
1111
export { updateAllEntities } from './updaters/update-all-entities';
1212

13-
export { EntityId, EntityMap, EntityState, NamedEntityState } from './models';
13+
export {
14+
EntityId,
15+
EntityMap,
16+
EntityState,
17+
NamedEntityState,
18+
SelectEntityId,
19+
} from './models';
1420
export { withEntities } from './with-entities';

‎modules/signals/entities/src/models.ts

+1-5
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,7 @@ export type NamedEntityComputed<Entity, Collection extends string> = {
2121
[K in keyof EntityComputed<Entity> as `${Collection}${Capitalize<K>}`]: EntityComputed<Entity>[K];
2222
};
2323

24-
export type EntityIdProps<Entity> = {
25-
[K in keyof Entity as Entity[K] extends EntityId ? K : never]: Entity[K];
26-
};
27-
28-
export type EntityIdKey<Entity> = keyof EntityIdProps<Entity> & string;
24+
export type SelectEntityId<Entity> = (entity: Entity) => EntityId;
2925

3026
export type EntityPredicate<Entity> = (entity: Entity) => boolean;
3127

‎modules/signals/entities/src/updaters/add-entities.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { PartialStateUpdater } from '@ngrx/signals';
22
import {
33
EntityId,
4-
EntityIdKey,
54
EntityState,
65
NamedEntityState,
6+
SelectEntityId,
77
} from '../models';
88
import {
99
addEntitiesMutably,
1010
cloneEntityState,
11-
getEntityIdKey,
11+
getEntityIdSelector,
1212
getEntityStateKeys,
1313
getEntityUpdaterResult,
1414
} from '../helpers';
@@ -18,7 +18,7 @@ export function addEntities<Entity extends { id: EntityId }>(
1818
): PartialStateUpdater<EntityState<Entity>>;
1919
export function addEntities<Entity, Collection extends string>(
2020
entities: Entity[],
21-
config: { collection: Collection; idKey: EntityIdKey<Entity> }
21+
config: { collection: Collection; selectId: SelectEntityId<NoInfer<Entity>> }
2222
): PartialStateUpdater<NamedEntityState<Entity, Collection>>;
2323
export function addEntities<
2424
Entity extends { id: EntityId },
@@ -29,18 +29,18 @@ export function addEntities<
2929
): PartialStateUpdater<NamedEntityState<Entity, Collection>>;
3030
export function addEntities<Entity>(
3131
entities: Entity[],
32-
config: { idKey: EntityIdKey<Entity> }
32+
config: { selectId: SelectEntityId<NoInfer<Entity>> }
3333
): PartialStateUpdater<EntityState<Entity>>;
3434
export function addEntities(
3535
entities: any[],
36-
config?: { collection?: string; idKey?: string }
36+
config?: { collection?: string; selectId?: SelectEntityId<any> }
3737
): PartialStateUpdater<EntityState<any> | NamedEntityState<any, string>> {
38-
const idKey = getEntityIdKey(config);
38+
const selectId = getEntityIdSelector(config);
3939
const stateKeys = getEntityStateKeys(config);
4040

4141
return (state) => {
4242
const clonedState = cloneEntityState(state, stateKeys);
43-
const didMutate = addEntitiesMutably(clonedState, entities, idKey);
43+
const didMutate = addEntitiesMutably(clonedState, entities, selectId);
4444

4545
return getEntityUpdaterResult(clonedState, stateKeys, didMutate);
4646
};

‎modules/signals/entities/src/updaters/add-entity.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { PartialStateUpdater } from '@ngrx/signals';
22
import {
33
EntityId,
4-
EntityIdKey,
54
EntityState,
65
NamedEntityState,
6+
SelectEntityId,
77
} from '../models';
88
import {
99
addEntityMutably,
1010
cloneEntityState,
11-
getEntityIdKey,
11+
getEntityIdSelector,
1212
getEntityStateKeys,
1313
getEntityUpdaterResult,
1414
} from '../helpers';
@@ -18,7 +18,7 @@ export function addEntity<Entity extends { id: EntityId }>(
1818
): PartialStateUpdater<EntityState<Entity>>;
1919
export function addEntity<Entity, Collection extends string>(
2020
entity: Entity,
21-
config: { collection: Collection; idKey: EntityIdKey<Entity> }
21+
config: { collection: Collection; selectId: SelectEntityId<NoInfer<Entity>> }
2222
): PartialStateUpdater<NamedEntityState<Entity, Collection>>;
2323
export function addEntity<
2424
Entity extends { id: EntityId },
@@ -29,18 +29,18 @@ export function addEntity<
2929
): PartialStateUpdater<NamedEntityState<Entity, Collection>>;
3030
export function addEntity<Entity>(
3131
entity: Entity,
32-
config: { idKey: EntityIdKey<Entity> }
32+
config: { selectId: SelectEntityId<NoInfer<Entity>> }
3333
): PartialStateUpdater<EntityState<Entity>>;
3434
export function addEntity(
3535
entity: any,
36-
config?: { collection?: string; idKey?: string }
36+
config?: { collection?: string; selectId?: SelectEntityId<any> }
3737
): PartialStateUpdater<EntityState<any> | NamedEntityState<any, string>> {
38-
const idKey = getEntityIdKey(config);
38+
const selectId = getEntityIdSelector(config);
3939
const stateKeys = getEntityStateKeys(config);
4040

4141
return (state) => {
4242
const clonedState = cloneEntityState(state, stateKeys);
43-
const didMutate = addEntityMutably(clonedState, entity, idKey);
43+
const didMutate = addEntityMutably(clonedState, entity, selectId);
4444

4545
return getEntityUpdaterResult(clonedState, stateKeys, didMutate);
4646
};

‎modules/signals/entities/src/updaters/set-all-entities.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { PartialStateUpdater } from '@ngrx/signals';
22
import {
33
EntityId,
4-
EntityIdKey,
54
EntityState,
65
NamedEntityState,
6+
SelectEntityId,
77
} from '../models';
88
import {
9-
getEntityIdKey,
9+
getEntityIdSelector,
1010
getEntityStateKeys,
1111
setEntitiesMutably,
1212
} from '../helpers';
@@ -16,7 +16,7 @@ export function setAllEntities<Entity extends { id: EntityId }>(
1616
): PartialStateUpdater<EntityState<Entity>>;
1717
export function setAllEntities<Entity, Collection extends string>(
1818
entities: Entity[],
19-
config: { collection: Collection; idKey: EntityIdKey<Entity> }
19+
config: { collection: Collection; selectId: SelectEntityId<NoInfer<Entity>> }
2020
): PartialStateUpdater<NamedEntityState<Entity, Collection>>;
2121
export function setAllEntities<
2222
Entity extends { id: EntityId },
@@ -27,18 +27,18 @@ export function setAllEntities<
2727
): PartialStateUpdater<NamedEntityState<Entity, Collection>>;
2828
export function setAllEntities<Entity>(
2929
entities: Entity[],
30-
config: { idKey: EntityIdKey<Entity> }
30+
config: { selectId: SelectEntityId<NoInfer<Entity>> }
3131
): PartialStateUpdater<EntityState<Entity>>;
3232
export function setAllEntities(
3333
entities: any[],
34-
config?: { collection?: string; idKey?: string }
34+
config?: { collection?: string; selectId?: SelectEntityId<any> }
3535
): PartialStateUpdater<EntityState<any> | NamedEntityState<any, string>> {
36-
const idKey = getEntityIdKey(config);
36+
const selectId = getEntityIdSelector(config);
3737
const stateKeys = getEntityStateKeys(config);
3838

3939
return () => {
4040
const state: EntityState<any> = { entityMap: {}, ids: [] };
41-
setEntitiesMutably(state, entities, idKey);
41+
setEntitiesMutably(state, entities, selectId);
4242

4343
return {
4444
[stateKeys.entityMapKey]: state.entityMap,

‎modules/signals/entities/src/updaters/set-entities.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { PartialStateUpdater } from '@ngrx/signals';
22
import {
33
EntityId,
4-
EntityIdKey,
54
EntityState,
65
NamedEntityState,
6+
SelectEntityId,
77
} from '../models';
88
import {
99
cloneEntityState,
10-
getEntityIdKey,
10+
getEntityIdSelector,
1111
getEntityStateKeys,
1212
getEntityUpdaterResult,
1313
setEntitiesMutably,
@@ -18,7 +18,7 @@ export function setEntities<Entity extends { id: EntityId }>(
1818
): PartialStateUpdater<EntityState<Entity>>;
1919
export function setEntities<Entity, Collection extends string>(
2020
entities: Entity[],
21-
config: { collection: Collection; idKey: EntityIdKey<Entity> }
21+
config: { collection: Collection; selectId: SelectEntityId<NoInfer<Entity>> }
2222
): PartialStateUpdater<NamedEntityState<Entity, Collection>>;
2323
export function setEntities<
2424
Entity extends { id: EntityId },
@@ -29,18 +29,18 @@ export function setEntities<
2929
): PartialStateUpdater<NamedEntityState<Entity, Collection>>;
3030
export function setEntities<Entity>(
3131
entities: Entity[],
32-
config: { idKey: EntityIdKey<Entity> }
32+
config: { selectId: SelectEntityId<NoInfer<Entity>> }
3333
): PartialStateUpdater<EntityState<Entity>>;
3434
export function setEntities(
3535
entities: any[],
36-
config?: { collection?: string; idKey?: string }
36+
config?: { collection?: string; selectId?: SelectEntityId<any> }
3737
): PartialStateUpdater<EntityState<any> | NamedEntityState<any, string>> {
38-
const idKey = getEntityIdKey(config);
38+
const selectId = getEntityIdSelector(config);
3939
const stateKeys = getEntityStateKeys(config);
4040

4141
return (state) => {
4242
const clonedState = cloneEntityState(state, stateKeys);
43-
const didMutate = setEntitiesMutably(clonedState, entities, idKey);
43+
const didMutate = setEntitiesMutably(clonedState, entities, selectId);
4444

4545
return getEntityUpdaterResult(clonedState, stateKeys, didMutate);
4646
};

‎modules/signals/entities/src/updaters/set-entity.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { PartialStateUpdater } from '@ngrx/signals';
22
import {
33
EntityId,
4-
EntityIdKey,
54
EntityState,
65
NamedEntityState,
6+
SelectEntityId,
77
} from '../models';
88
import {
99
cloneEntityState,
10-
getEntityIdKey,
10+
getEntityIdSelector,
1111
getEntityStateKeys,
1212
getEntityUpdaterResult,
1313
setEntityMutably,
@@ -18,7 +18,7 @@ export function setEntity<Entity extends { id: EntityId }>(
1818
): PartialStateUpdater<EntityState<Entity>>;
1919
export function setEntity<Entity, Collection extends string>(
2020
entity: Entity,
21-
config: { collection: Collection; idKey: EntityIdKey<Entity> }
21+
config: { collection: Collection; selectId: SelectEntityId<NoInfer<Entity>> }
2222
): PartialStateUpdater<NamedEntityState<Entity, Collection>>;
2323
export function setEntity<
2424
Entity extends { id: EntityId },
@@ -29,18 +29,18 @@ export function setEntity<
2929
): PartialStateUpdater<NamedEntityState<Entity, Collection>>;
3030
export function setEntity<Entity>(
3131
entity: Entity,
32-
config: { idKey: EntityIdKey<Entity> }
32+
config: { selectId: SelectEntityId<NoInfer<Entity>> }
3333
): PartialStateUpdater<EntityState<Entity>>;
3434
export function setEntity(
3535
entity: any,
36-
config?: { collection?: string; idKey?: string }
36+
config?: { collection?: string; selectId?: SelectEntityId<any> }
3737
): PartialStateUpdater<EntityState<any> | NamedEntityState<any, string>> {
38-
const idKey = getEntityIdKey(config);
38+
const selectId = getEntityIdSelector(config);
3939
const stateKeys = getEntityStateKeys(config);
4040

4141
return (state) => {
4242
const clonedState = cloneEntityState(state, stateKeys);
43-
const didMutate = setEntityMutably(clonedState, entity, idKey);
43+
const didMutate = setEntityMutably(clonedState, entity, selectId);
4444

4545
return getEntityUpdaterResult(clonedState, stateKeys, didMutate);
4646
};

‎projects/ngrx.io/content/guide/signals/signal-store/entity-management.md

+10-6
Original file line numberDiff line numberDiff line change
@@ -217,9 +217,9 @@ patchState(this.todoStore, removeEntities([2, 4]));
217217

218218
The default property name for an identifier is `id` and is of type `string` or `number`.
219219

220-
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`.
220+
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`.
221221

222-
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`.
222+
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.
223223

224224
For example:
225225

@@ -230,18 +230,20 @@ interface Todo {
230230
finished: boolean;
231231
}
232232

233+
const selectId: SelectEntityId<Todo> = (todo) => todo.key;
234+
233235
patchState(
234236
this.todoStore,
235237
addEntities(
236238
[
237239
{ key: 2, name: 'Car Washing', finished: false },
238240
{ key: 3, name: 'Cat Feeding', finished: false },
239241
],
240-
{ idKey: 'key' }
242+
{ selectId }
241243
)
242244
);
243245

244-
patchState(this.todoStore, setEntity({ key: 4, name: 'Dog Feeding', finished: false }, { idKey: 'key' }));
246+
patchState(this.todoStore, setEntity({ key: 4, name: 'Dog Feeding', finished: false }, { selectId }));
245247
```
246248

247249
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,17 +308,19 @@ The names of the state properties changed from:
306308

307309
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.
308310

309-
If you have a customized id property, you need to include the `idKey` parameter in the object literal, too:
311+
If you have a customized id property, you need to include the `selectId` function in the object literal, too:
310312

311313
```typescript
314+
const selectId: SelectEntityId<Todo> = (todo) => todo.key;
315+
312316
patchState(
313317
this.todoStore,
314318
addEntities(
315319
[
316320
{ key: 2, name: 'Car Washing', finished: false },
317321
{ key: 3, name: 'Cat Feeding', finished: false },
318322
],
319-
{ idKey: 'key', collection: 'todo' }
323+
{ selectId, collection: 'todo' }
320324
)
321325
);
322326
```

0 commit comments

Comments
 (0)
Please sign in to comment.