Skip to content

Commit f01bcd1

Browse files
feat(signals): add entities subpackage (#4090)
1 parent 73fda59 commit f01bcd1

35 files changed

+2663
-2
lines changed

modules/signals/.eslintrc.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,18 @@
1313
},
1414
"rules": {
1515
"@angular-eslint/directive-selector": "off",
16-
"@angular-eslint/component-selector": "off"
16+
"@angular-eslint/component-selector": "off",
17+
"@nx/enforce-module-boundaries": "off",
18+
"@typescript-eslint/ban-types": [
19+
"error",
20+
{
21+
"extendDefaults": true,
22+
"types": {
23+
"{}": false,
24+
"Function": false
25+
}
26+
}
27+
]
1728
},
1829
"plugins": ["@typescript-eslint"]
1930
},

modules/signals/entities/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './src/index';
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"lib": {
3+
"entryFile": "index.ts"
4+
}
5+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export type User = { id: number; firstName: string; lastName: string };
2+
export type Todo = { _id: string; text: string; completed: boolean };
3+
4+
export const user1: User = { id: 1, firstName: 'John', lastName: 'Doe' };
5+
export const user2: User = { id: 2, firstName: 'Jane', lastName: 'Smith' };
6+
export const user3: User = { id: 3, firstName: 'Joe', lastName: 'Johnson' };
7+
8+
export const todo1: Todo = { _id: 'x', text: 'Buy milk', completed: true };
9+
export const todo2: Todo = { _id: 'y', text: 'Buy eggs', completed: false };
10+
export const todo3: Todo = { _id: 'z', text: 'Make bread', completed: true };
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
import { patchState, signalStore, type } from '@ngrx/signals';
2+
import { addEntities, withEntities } from '../../src';
3+
import { Todo, todo1, todo2, todo3, User, user1, user2, user3 } from '../mocks';
4+
5+
describe('addEntities', () => {
6+
it('adds entities if they do not exist', () => {
7+
const Store = signalStore(withEntities<User>());
8+
const store = new Store();
9+
10+
patchState(store, addEntities([user1]));
11+
12+
expect(store.entityMap()).toEqual({ 1: user1 });
13+
expect(store.ids()).toEqual([1]);
14+
expect(store.entities()).toEqual([user1]);
15+
16+
patchState(store, addEntities([user2, user3]));
17+
18+
expect(store.entityMap()).toEqual({ 1: user1, 2: user2, 3: user3 });
19+
expect(store.ids()).toEqual([1, 2, 3]);
20+
expect(store.entities()).toEqual([user1, user2, user3]);
21+
});
22+
23+
it('does not add entities if they already exist', () => {
24+
const Store = signalStore(withEntities<User>());
25+
const store = new Store();
26+
27+
patchState(store, addEntities([user1, user2]));
28+
29+
const entityMap = store.entityMap();
30+
const ids = store.ids();
31+
const entities = store.entities();
32+
33+
patchState(
34+
store,
35+
addEntities([user2, { ...user2, firstName: 'Jack' }, user1]),
36+
addEntities([] as User[])
37+
);
38+
39+
expect(store.entityMap()).toBe(entityMap);
40+
expect(store.ids()).toBe(ids);
41+
expect(store.entities()).toBe(entities);
42+
43+
expect(store.entityMap()).toEqual({ 1: user1, 2: user2 });
44+
expect(store.ids()).toEqual([1, 2]);
45+
expect(store.entities()).toEqual([user1, user2]);
46+
47+
patchState(store, addEntities([user1, user3, user2]));
48+
49+
expect(store.entityMap()).toEqual({ 1: user1, 2: user2, 3: user3 });
50+
expect(store.ids()).toEqual([1, 2, 3]);
51+
expect(store.entities()).toEqual([user1, user2, user3]);
52+
});
53+
54+
it('adds entities to the specified collection if they do not exist', () => {
55+
const Store = signalStore(
56+
withEntities({
57+
entity: type<User>(),
58+
collection: 'user',
59+
})
60+
);
61+
const store = new Store();
62+
63+
patchState(
64+
store,
65+
addEntities([user1, user2], { collection: 'user' }),
66+
addEntities([user3], { collection: 'user' })
67+
);
68+
69+
expect(store.userEntityMap()).toEqual({ 1: user1, 2: user2, 3: user3 });
70+
expect(store.userIds()).toEqual([1, 2, 3]);
71+
expect(store.userEntities()).toEqual([user1, user2, user3]);
72+
});
73+
74+
it('does not add entities to the specified collection if they already exist', () => {
75+
const Store = signalStore(
76+
withEntities({
77+
entity: type<User>(),
78+
collection: 'user',
79+
})
80+
);
81+
const store = new Store();
82+
83+
patchState(
84+
store,
85+
addEntities([user1, { ...user1, lastName: 'Hendrix' }, user3, user1], {
86+
collection: 'user',
87+
})
88+
);
89+
90+
const userEntityMap = store.userEntityMap();
91+
const userIds = store.userIds();
92+
const userEntities = store.userEntities();
93+
94+
patchState(
95+
store,
96+
addEntities([] as User[], { collection: 'user' }),
97+
addEntities([user3, { ...user3, firstName: 'Jimmy' }, user1], {
98+
collection: 'user',
99+
})
100+
);
101+
102+
expect(store.userEntityMap()).toBe(userEntityMap);
103+
expect(store.userIds()).toBe(userIds);
104+
expect(store.userEntities()).toBe(userEntities);
105+
expect(store.userEntityMap()).toEqual({ 1: user1, 3: user3 });
106+
expect(store.userIds()).toEqual([1, 3]);
107+
expect(store.userEntities()).toEqual([user1, user3]);
108+
109+
patchState(
110+
store,
111+
addEntities([user1, user2, user3], { collection: 'user' })
112+
);
113+
expect(store.userEntityMap()).toEqual({ 1: user1, 3: user3, 2: user2 });
114+
expect(store.userIds()).toEqual([1, 3, 2]);
115+
expect(store.userEntities()).toEqual([user1, user3, user2]);
116+
});
117+
118+
it('adds entities with the specified idKey if they do not exist', () => {
119+
const Store = signalStore(withEntities<Todo>());
120+
const store = new Store();
121+
122+
patchState(store, addEntities([todo2, todo3], { idKey: '_id' }));
123+
124+
expect(store.entityMap()).toEqual({ y: todo2, z: todo3 });
125+
expect(store.ids()).toEqual(['y', 'z']);
126+
expect(store.entities()).toEqual([todo2, todo3]);
127+
128+
patchState(
129+
store,
130+
addEntities([todo1], { idKey: '_id' }),
131+
addEntities([] as Todo[], { idKey: '_id' })
132+
);
133+
134+
expect(store.entityMap()).toEqual({ y: todo2, z: todo3, x: todo1 });
135+
expect(store.ids()).toEqual(['y', 'z', 'x']);
136+
expect(store.entities()).toEqual([todo2, todo3, todo1]);
137+
});
138+
139+
it('does not add entities with the specified idKey if they already exist', () => {
140+
const Store = signalStore(withEntities<Todo>());
141+
const store = new Store();
142+
143+
patchState(
144+
store,
145+
addEntities([todo1], { idKey: '_id' }),
146+
addEntities([todo2, todo1], { idKey: '_id' }),
147+
addEntities([] as Todo[], { idKey: '_id' })
148+
);
149+
150+
const entityMap = store.entityMap();
151+
const ids = store.ids();
152+
const entities = store.entities();
153+
154+
patchState(
155+
store,
156+
addEntities([] as Todo[], { idKey: '_id' }),
157+
addEntities([todo2, { ...todo2, text: 'NgRx' }, todo1], { idKey: '_id' })
158+
);
159+
160+
expect(store.entityMap()).toBe(entityMap);
161+
expect(store.ids()).toBe(ids);
162+
expect(store.entities()).toBe(entities);
163+
expect(store.entityMap()).toEqual({ x: todo1, y: todo2 });
164+
expect(store.ids()).toEqual(['x', 'y']);
165+
expect(store.entities()).toEqual([todo1, todo2]);
166+
167+
patchState(store, addEntities([todo1, todo3, todo2], { idKey: '_id' }));
168+
169+
expect(store.entityMap()).toEqual({ x: todo1, y: todo2, z: todo3 });
170+
expect(store.ids()).toEqual(['x', 'y', 'z']);
171+
expect(store.entities()).toEqual([todo1, todo2, todo3]);
172+
});
173+
174+
it('adds entities with the specified idKey to the specified collection if they do not exist', () => {
175+
const Store = signalStore(
176+
withEntities({
177+
entity: type<Todo>(),
178+
collection: 'todo',
179+
})
180+
);
181+
const store = new Store();
182+
183+
patchState(
184+
store,
185+
addEntities([todo3, todo2], {
186+
collection: 'todo',
187+
idKey: '_id',
188+
})
189+
);
190+
191+
expect(store.todoEntityMap()).toEqual({ z: todo3, y: todo2 });
192+
expect(store.todoIds()).toEqual(['z', 'y']);
193+
expect(store.todoEntities()).toEqual([todo3, todo2]);
194+
195+
patchState(
196+
store,
197+
addEntities([todo1], { collection: 'todo', idKey: '_id' }),
198+
addEntities([] as Todo[], { collection: 'todo', idKey: '_id' })
199+
);
200+
201+
expect(store.todoEntityMap()).toEqual({ z: todo3, y: todo2, x: todo1 });
202+
expect(store.todoIds()).toEqual(['z', 'y', 'x']);
203+
expect(store.todoEntities()).toEqual([todo3, todo2, todo1]);
204+
});
205+
206+
it('does not add entities with the specified idKey to the specified collection if they already exist', () => {
207+
const todoMeta = {
208+
entity: type<Todo>(),
209+
collection: 'todo',
210+
idKey: '_id',
211+
} as const;
212+
213+
const Store = signalStore(withEntities(todoMeta));
214+
const store = new Store();
215+
216+
patchState(
217+
store,
218+
addEntities([todo2, { ...todo2, text: 'NgRx' }, todo3, todo2], todoMeta)
219+
);
220+
221+
const todoEntityMap = store.todoEntityMap();
222+
const todoIds = store.todoIds();
223+
const todoEntities = store.todoEntities();
224+
225+
patchState(
226+
store,
227+
addEntities([] as Todo[], todoMeta),
228+
addEntities([todo3, todo2, { ...todo3, text: 'NgRx' }], todoMeta)
229+
);
230+
231+
expect(store.todoEntityMap()).toBe(todoEntityMap);
232+
expect(store.todoIds()).toBe(todoIds);
233+
expect(store.todoEntities()).toBe(todoEntities);
234+
expect(store.todoEntityMap()).toEqual({ y: todo2, z: todo3 });
235+
expect(store.todoIds()).toEqual(['y', 'z']);
236+
expect(store.todoEntities()).toEqual([todo2, todo3]);
237+
238+
patchState(
239+
store,
240+
addEntities([todo1, todo2, todo3], todoMeta),
241+
addEntities([todo2, todo3, todo1], todoMeta)
242+
);
243+
244+
expect(store.todoEntityMap()).toEqual({ y: todo2, z: todo3, x: todo1 });
245+
expect(store.todoIds()).toEqual(['y', 'z', 'x']);
246+
expect(store.todoEntities()).toEqual([todo2, todo3, todo1]);
247+
});
248+
});

0 commit comments

Comments
 (0)