@adaas/a-server 0.0.29 → 0.0.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/browser/index.d.mts +123 -69
  2. package/dist/browser/index.mjs +211 -69
  3. package/dist/browser/index.mjs.map +1 -1
  4. package/dist/node/controllers/A-EntityController/A-EntityController.component.d.mts +2 -5
  5. package/dist/node/controllers/A-EntityController/A-EntityController.component.d.ts +2 -5
  6. package/dist/node/controllers/A-EntityController/A-EntityController.component.js +66 -88
  7. package/dist/node/controllers/A-EntityController/A-EntityController.component.js.map +1 -1
  8. package/dist/node/controllers/A-EntityController/A-EntityController.component.mjs +67 -89
  9. package/dist/node/controllers/A-EntityController/A-EntityController.component.mjs.map +1 -1
  10. package/dist/node/controllers/A-ListingController/A-ListingController.component.js +20 -18
  11. package/dist/node/controllers/A-ListingController/A-ListingController.component.js.map +1 -1
  12. package/dist/node/controllers/A-ListingController/A-ListingController.component.mjs +20 -18
  13. package/dist/node/controllers/A-ListingController/A-ListingController.component.mjs.map +1 -1
  14. package/dist/node/controllers/A-ServerHealthMonitor/A-ServerHealthMonitor.component.d.mts +0 -2
  15. package/dist/node/controllers/A-ServerHealthMonitor/A-ServerHealthMonitor.component.d.ts +0 -2
  16. package/dist/node/controllers/A-ServerHealthMonitor/A-ServerHealthMonitor.component.js +10 -1
  17. package/dist/node/controllers/A-ServerHealthMonitor/A-ServerHealthMonitor.component.js.map +1 -1
  18. package/dist/node/controllers/A-ServerHealthMonitor/A-ServerHealthMonitor.component.mjs +10 -1
  19. package/dist/node/controllers/A-ServerHealthMonitor/A-ServerHealthMonitor.component.mjs.map +1 -1
  20. package/dist/node/index.d.mts +3 -1
  21. package/dist/node/index.d.ts +3 -1
  22. package/dist/node/index.js +14 -0
  23. package/dist/node/index.mjs +2 -0
  24. package/dist/node/lib/A-Server/A-HttpServer.container.d.mts +4 -6
  25. package/dist/node/lib/A-Server/A-HttpServer.container.d.ts +4 -6
  26. package/dist/node/lib/A-ServerController/A-ServerController.component.js +17 -4
  27. package/dist/node/lib/A-ServerController/A-ServerController.component.js.map +1 -1
  28. package/dist/node/lib/A-ServerController/A-ServerController.component.mjs +17 -4
  29. package/dist/node/lib/A-ServerController/A-ServerController.component.mjs.map +1 -1
  30. package/dist/node/lib/A-ServerEntityList/A-EntityList.entity.d.mts +52 -28
  31. package/dist/node/lib/A-ServerEntityList/A-EntityList.entity.d.ts +52 -28
  32. package/dist/node/lib/A-ServerEntityList/A-EntityList.entity.js +117 -44
  33. package/dist/node/lib/A-ServerEntityList/A-EntityList.entity.js.map +1 -1
  34. package/dist/node/lib/A-ServerEntityList/A-EntityList.entity.mjs +118 -45
  35. package/dist/node/lib/A-ServerEntityList/A-EntityList.entity.mjs.map +1 -1
  36. package/dist/node/lib/A-ServerEntityList/A-EntityList.types.d.mts +14 -6
  37. package/dist/node/lib/A-ServerEntityList/A-EntityList.types.d.ts +14 -6
  38. package/dist/node/lib/A-ServerEntityList/A-EntityList.types.js.map +1 -1
  39. package/dist/node/lib/A-ServerEntityList/A-EntityList.types.mjs.map +1 -1
  40. package/dist/node/lib/A-ServerEntityList/A-EntityListCacheState.context.d.mts +12 -0
  41. package/dist/node/lib/A-ServerEntityList/A-EntityListCacheState.context.d.ts +12 -0
  42. package/dist/node/lib/A-ServerEntityList/A-EntityListCacheState.context.js +25 -0
  43. package/dist/node/lib/A-ServerEntityList/A-EntityListCacheState.context.js.map +1 -0
  44. package/dist/node/lib/A-ServerEntityList/A-EntityListCacheState.context.mjs +24 -0
  45. package/dist/node/lib/A-ServerEntityList/A-EntityListCacheState.context.mjs.map +1 -0
  46. package/dist/node/lib/A-ServerEntityList/A-EntityListPagination.context.d.mts +18 -0
  47. package/dist/node/lib/A-ServerEntityList/A-EntityListPagination.context.d.ts +18 -0
  48. package/dist/node/lib/A-ServerEntityList/A-EntityListPagination.context.js +48 -0
  49. package/dist/node/lib/A-ServerEntityList/A-EntityListPagination.context.js.map +1 -0
  50. package/dist/node/lib/A-ServerEntityList/A-EntityListPagination.context.mjs +47 -0
  51. package/dist/node/lib/A-ServerEntityList/A-EntityListPagination.context.mjs.map +1 -0
  52. package/dist/node/lib/A-ServerLogger/A-ServerLogger.component.d.mts +6 -8
  53. package/dist/node/lib/A-ServerLogger/A-ServerLogger.component.d.ts +6 -8
  54. package/dist/node/lib/A-ServerLogger/A-ServerLogger.component.js +3 -4
  55. package/dist/node/lib/A-ServerLogger/A-ServerLogger.component.js.map +1 -1
  56. package/dist/node/lib/A-ServerLogger/A-ServerLogger.component.mjs +4 -5
  57. package/dist/node/lib/A-ServerLogger/A-ServerLogger.component.mjs.map +1 -1
  58. package/dist/node/lib/A-ServerRouter/A-ServerRouter.component.d.mts +0 -2
  59. package/dist/node/lib/A-ServerRouter/A-ServerRouter.component.d.ts +0 -2
  60. package/dist/node/middlewares/A-ServerCORS/A_ServerCORS.component.js +1 -1
  61. package/dist/node/middlewares/A-ServerCORS/A_ServerCORS.component.js.map +1 -1
  62. package/dist/node/middlewares/A-ServerCORS/A_ServerCORS.component.mjs +1 -1
  63. package/dist/node/middlewares/A-ServerCORS/A_ServerCORS.component.mjs.map +1 -1
  64. package/dist/node/repositories/A-EntityRepository/A-EntityRepository.component.d.mts +1 -0
  65. package/dist/node/repositories/A-EntityRepository/A-EntityRepository.component.d.ts +1 -0
  66. package/examples/simple-server/components/Users.repository.ts +2 -2
  67. package/jest.config.ts +1 -0
  68. package/package.json +5 -5
  69. package/src/controllers/A-EntityController/A-EntityController.component.ts +69 -109
  70. package/src/controllers/A-ListingController/A-ListingController.component.ts +22 -20
  71. package/src/controllers/A-ServerHealthMonitor/A-ServerHealthMonitor.component.ts +11 -1
  72. package/src/index.ts +2 -0
  73. package/src/lib/A-ServerController/A-ServerController.component.ts +17 -8
  74. package/src/lib/A-ServerEntityList/A-EntityList.entity.ts +159 -55
  75. package/src/lib/A-ServerEntityList/A-EntityList.types.ts +17 -7
  76. package/src/lib/A-ServerEntityList/A-EntityListCacheState.context.ts +27 -0
  77. package/src/lib/A-ServerEntityList/A-EntityListPagination.context.ts +48 -0
  78. package/src/lib/A-ServerLogger/A-ServerLogger.component.ts +3 -4
  79. package/src/middlewares/A-ServerCORS/A_ServerCORS.component.ts +1 -1
  80. package/tests/A-Server-CORS.test.ts +542 -0
  81. package/tests/A-Server-Entity.test.ts +205 -0
  82. package/tests/A-Server-Health.test.ts +89 -0
  83. package/tests/A-Server-Routes.test.ts +113 -0
  84. package/tests/A-ServerEntityList.test.ts +416 -0
@@ -0,0 +1,416 @@
1
+ import http from 'http';
2
+ import { A_Concept, A_Scope, ASEID, A_Component, A_Entity, A_Feature, A_Inject, A_TYPES__EntityFeatures } from '@adaas/a-concept';
3
+ import { A_Config, ENVConfigReader } from '@adaas/a-utils/a-config';
4
+ import { A_Polyfill } from '@adaas/a-utils/a-polyfill';
5
+ import { A_HttpServer } from '@adaas/a-server/server/A-HttpServer.container';
6
+ import { A_ServerRouter } from '@adaas/a-server/router/A-ServerRouter.component';
7
+ import { A_ServerLogger } from '@adaas/a-server/logger/A-ServerLogger.component';
8
+ import { A_ServerController } from '@adaas/a-server/controller/A-ServerController.component';
9
+ import { A_ServerHealthMonitor } from '@adaas/a-server/controllers/A-ServerHealthMonitor/A-ServerHealthMonitor.component';
10
+ import { A_EntityController } from '@adaas/a-server/controllers/A-EntityController/A-EntityController.component';
11
+ import { A_ListingController } from '@adaas/a-server/controllers/A-ListingController/A-ListingController.component';
12
+ import { A_ServerEntityList } from '@adaas/a-server/entity-list/A-EntityList.entity';
13
+ import { A_SERVER_TYPES__A_EntityListPagination } from '@adaas/a-server/entity-list/A-EntityList.types';
14
+ import { A_ServerListQueryFilter } from '@adaas/a-server/list-query/A-ServerListQueryFilter.context';
15
+ import { A_HTTPChannel } from '@adaas/a-server/channels/A-Http/A-Http.channel';
16
+
17
+ jest.retryTimes(0);
18
+ jest.setTimeout(30_000);
19
+
20
+ const TEST_PORT = 3903;
21
+
22
+ // ── Shared entity types ───────────────────────────────────────────────────────
23
+
24
+ type NewUser = { id: number; email: string; name: string; };
25
+ type UserJSON = NewUser & { aseid: string; };
26
+
27
+ class User extends A_Entity<NewUser, UserJSON> {
28
+ static get entity() { return 'user'; }
29
+ static get concept() { return 'a-server'; }
30
+ static get scope() { return 'entity-list-test'; }
31
+
32
+ email!: string;
33
+ name!: string;
34
+
35
+ get id(): number { return Number(this.aseid.id); }
36
+
37
+ fromNew(newEntity: NewUser): void {
38
+ this.aseid = new ASEID({ concept: User.concept, scope: User.scope, entity: User.entity, id: newEntity.id });
39
+ this.email = newEntity.email;
40
+ this.name = newEntity.name;
41
+ }
42
+
43
+ fromJSON(serialized: UserJSON): void {
44
+ this.aseid = new ASEID(serialized.aseid);
45
+ this.email = serialized.email;
46
+ this.name = serialized.name;
47
+ }
48
+
49
+ toJSON(): UserJSON {
50
+ return { id: this.id, aseid: this.aseid.toString(), email: this.email, name: this.name };
51
+ }
52
+ }
53
+
54
+ // ── Server-side repository (in-memory) ────────────────────────────────────────
55
+
56
+ class UsersRepository extends A_Component {
57
+ private mockedUsers: UserJSON[] = [
58
+ new User({ id: 1, name: 'John Doe', email: 'joe@doe.com' }).toJSON(),
59
+ new User({ id: 2, name: 'mr Smith', email: 'mr.smith@doe.com' }).toJSON(),
60
+ ];
61
+
62
+ @A_Feature.Extend({ name: A_TYPES__EntityFeatures.LOAD, scope: { exclude: [A_ServerEntityList] } })
63
+ load(@A_Inject(User) user: User) {
64
+ const existedUser = this.mockedUsers.find(u => u.id === user.id);
65
+ if (!existedUser) throw new Error('User not found');
66
+ user.fromJSON(existedUser);
67
+ }
68
+
69
+ @A_Feature.Extend({ name: A_TYPES__EntityFeatures.LOAD, scope: [A_ServerEntityList] })
70
+ list(
71
+ @A_Inject(A_ServerListQueryFilter<['page', 'itemsPerPage']>) query: A_ServerListQueryFilter<['page', 'itemsPerPage']>,
72
+ @A_Inject(A_ServerEntityList<User>) list: A_ServerEntityList<User>
73
+ ) {
74
+ const page = parseInt(query.get('page', '1'), 10);
75
+ const itemsPerPage = parseInt(query.get('itemsPerPage', '10'), 10);
76
+ const items = this.mockedUsers.slice((page - 1) * itemsPerPage, page * itemsPerPage);
77
+ list.fromList(items, { total: this.mockedUsers.length, page, pageSize: itemsPerPage });
78
+ }
79
+ }
80
+
81
+ // ── Client-side components (HTTP-based) ───────────────────────────────────────
82
+
83
+ class UserListChannel extends A_HTTPChannel {
84
+ constructor() {
85
+ super();
86
+ this.baseUrl = `http://localhost:${TEST_PORT}`;
87
+ }
88
+ }
89
+
90
+ class ClientUsersRepository extends A_Component {
91
+ @A_Feature.Extend({ name: A_TYPES__EntityFeatures.LOAD, scope: [A_ServerEntityList] })
92
+ async list(
93
+ @A_Inject(A_ServerEntityList<User>) list: A_ServerEntityList<User>,
94
+ @A_Inject(UserListChannel) channel: UserListChannel
95
+ ) {
96
+ const { page, pageSize } = list.pagination;
97
+ const response = await channel.get<{ items: UserJSON[]; pagination: A_SERVER_TYPES__A_EntityListPagination }>(
98
+ '/a-list/v1/user',
99
+ { page: String(page), itemsPerPage: String(pageSize) },
100
+ );
101
+ list.fromList(response.data!.items, response.data!.pagination);
102
+ }
103
+ }
104
+
105
+ // ── Helpers ───────────────────────────────────────────────────────────────────
106
+
107
+ function httpGet(url: string): Promise<{ status: number; body: unknown }> {
108
+ return new Promise((resolve, reject) => {
109
+ http.get(url, (res) => {
110
+ let raw = '';
111
+ res.on('data', (chunk: Buffer) => { raw += chunk.toString(); });
112
+ res.on('end', () => {
113
+ try { resolve({ status: res.statusCode ?? 0, body: JSON.parse(raw) }); }
114
+ catch { resolve({ status: res.statusCode ?? 0, body: raw }); }
115
+ });
116
+ }).on('error', reject);
117
+ });
118
+ }
119
+
120
+ // ── Test suite ────────────────────────────────────────────────────────────────
121
+
122
+ describe('A-ServerEntityList', () => {
123
+ let serverConcept: A_Concept;
124
+ let clientScope: A_Scope;
125
+
126
+ beforeAll(async () => {
127
+ const server = new A_HttpServer({
128
+ name: 'entity-list-test-server',
129
+ components: [
130
+ A_Polyfill,
131
+ A_ServerLogger,
132
+ ENVConfigReader,
133
+ A_ServerRouter,
134
+ A_ServerController,
135
+ A_ServerHealthMonitor,
136
+ A_EntityController,
137
+ A_ListingController,
138
+ UsersRepository,
139
+ ],
140
+ entities: [User],
141
+ fragments: [
142
+ new A_Config({
143
+ variables: ['A_SERVER_PORT', 'A_ROUTER__PARSE_PARAMS_AUTOMATICALLY', 'CONFIG_VERBOSE'] as const,
144
+ defaults: {
145
+ A_SERVER_PORT: TEST_PORT,
146
+ A_ROUTER__PARSE_PARAMS_AUTOMATICALLY: true,
147
+ CONFIG_VERBOSE: false,
148
+ },
149
+ }),
150
+ ],
151
+ });
152
+
153
+ serverConcept = new A_Concept({
154
+ name: 'entity-list-test-concept',
155
+ containers: [server],
156
+ components: [],
157
+ fragments: [],
158
+ entities: [],
159
+ });
160
+
161
+ await serverConcept.load();
162
+ await serverConcept.start();
163
+ });
164
+
165
+ beforeEach(() => {
166
+ clientScope = new A_Scope({
167
+ components: [UserListChannel, ClientUsersRepository],
168
+ });
169
+ });
170
+
171
+ afterAll(async () => {
172
+ await serverConcept.stop();
173
+ });
174
+
175
+ // ── HTTP endpoint smoke tests ─────────────────────────────────────────────
176
+
177
+ describe('HTTP endpoint (smoke tests)', () => {
178
+ it('should return all users in the list', async () => {
179
+ const { status, body } = await httpGet(`http://localhost:${TEST_PORT}/a-list/v1/user`);
180
+
181
+ expect(status).toBe(200);
182
+ expect(body).toHaveProperty('items');
183
+ expect(Array.isArray((body as Record<string, unknown>).items)).toBe(true);
184
+ expect((body as Record<string, unknown[]>).items).toHaveLength(2);
185
+ });
186
+
187
+ it('should include pagination metadata in the response', async () => {
188
+ const { status, body } = await httpGet(`http://localhost:${TEST_PORT}/a-list/v1/user`);
189
+
190
+ expect(status).toBe(200);
191
+ expect(body).toHaveProperty('pagination');
192
+ expect((body as Record<string, Record<string, unknown>>).pagination).toMatchObject({ total: 2 });
193
+ });
194
+
195
+ it('should respect itemsPerPage=1 and return a single item', async () => {
196
+ const { status, body } = await httpGet(
197
+ `http://localhost:${TEST_PORT}/a-list/v1/user?page=1&itemsPerPage=1`
198
+ );
199
+
200
+ expect(status).toBe(200);
201
+ expect((body as Record<string, unknown[]>).items).toHaveLength(1);
202
+ expect((body as any).items[0]).toMatchObject({ name: 'John Doe' });
203
+ });
204
+
205
+ it('should return second page when page=2&itemsPerPage=1', async () => {
206
+ const { status, body } = await httpGet(
207
+ `http://localhost:${TEST_PORT}/a-list/v1/user?page=2&itemsPerPage=1`
208
+ );
209
+
210
+ expect(status).toBe(200);
211
+ expect((body as Record<string, unknown[]>).items).toHaveLength(1);
212
+ expect((body as any).items[0]).toMatchObject({ name: 'mr Smith' });
213
+ });
214
+
215
+ it('should return 404 for an unregistered entity type', async () => {
216
+ const { status } = await httpGet(`http://localhost:${TEST_PORT}/a-list/v1/unknown-entity`);
217
+
218
+ expect(status).toBe(404);
219
+ });
220
+ });
221
+
222
+ // ── Entity-first API tests ────────────────────────────────────────────────
223
+
224
+ describe('Entity-first API', () => {
225
+
226
+ it('should load all users via entity constructor', async () => {
227
+ const list = new A_ServerEntityList<User>({ entity: User, pagination: { page: 1, pageSize: 10 } });
228
+ clientScope.register(list);
229
+ await list.load();
230
+
231
+ expect(list.length).toBe(2);
232
+ expect(list.items[0]).toBeInstanceOf(User);
233
+ expect(list.items[0].name).toBe('John Doe');
234
+ });
235
+
236
+ it('should populate pagination after load', async () => {
237
+ const list = new A_ServerEntityList<User>({ entity: User, pagination: { page: 1, pageSize: 1 } });
238
+ clientScope.register(list);
239
+ await list.load();
240
+
241
+ expect(list.length).toBe(1);
242
+ expect(list.pagination.total).toBe(2);
243
+ expect(list.pagination.page).toBe(1);
244
+ expect(list.pagination.pageSize).toBe(1);
245
+ });
246
+
247
+ it('should return the second page', async () => {
248
+ const list = new A_ServerEntityList<User>({ entity: User, pagination: { page: 2, pageSize: 1 } });
249
+ clientScope.register(list);
250
+ await list.load();
251
+
252
+ expect(list.length).toBe(1);
253
+ expect(list.at(0)!.name).toBe('mr Smith');
254
+ });
255
+
256
+ // ── at() ───────────────────────────────────────────────────────────────
257
+
258
+ it('at() returns the item at the given index', async () => {
259
+ const list = new A_ServerEntityList<User>({ entity: User, pagination: { page: 1, pageSize: 10 } });
260
+ clientScope.register(list);
261
+ await list.load();
262
+
263
+ expect(list.at(0)).toBeInstanceOf(User);
264
+ expect(list.at(0)!.name).toBe('John Doe');
265
+ expect(list.at(1)!.name).toBe('mr Smith');
266
+ expect(list.at(99)).toBeUndefined();
267
+ });
268
+
269
+ // ── replace() ──────────────────────────────────────────────────────────
270
+
271
+ it('replace() swaps the item at the given index', async () => {
272
+ const list = new A_ServerEntityList<User>({ entity: User, pagination: { page: 1, pageSize: 10 } });
273
+ clientScope.register(list);
274
+ await list.load();
275
+
276
+ list.replace(0, new User({ id: 1, name: 'Updated Name', email: 'updated@example.com' }));
277
+
278
+ expect(list.length).toBe(2);
279
+ expect(list.at(0)!.name).toBe('Updated Name');
280
+ });
281
+
282
+ it('replace() accepts a plain serialised object', async () => {
283
+ const list = new A_ServerEntityList<User>({ entity: User, pagination: { page: 1, pageSize: 10 } });
284
+ clientScope.register(list);
285
+ await list.load();
286
+
287
+ const json = new User({ id: 1, name: 'From JSON', email: 'json@example.com' }).toJSON();
288
+ list.replace(0, json);
289
+
290
+ expect(list.at(0)!.name).toBe('From JSON');
291
+ });
292
+
293
+ // ── push() ─────────────────────────────────────────────────────────────
294
+
295
+ it('push() appends an item', async () => {
296
+ const list = new A_ServerEntityList<User>({ entity: User, pagination: { page: 1, pageSize: 10 } });
297
+ clientScope.register(list);
298
+ await list.load();
299
+
300
+ list.push(new User({ id: 3, name: 'Alice', email: 'alice@example.com' }));
301
+
302
+ expect(list.length).toBe(3);
303
+ expect(list.at(2)!.name).toBe('Alice');
304
+ });
305
+
306
+ // ── unshift() ──────────────────────────────────────────────────────────
307
+
308
+ it('unshift() prepends an item', async () => {
309
+ const list = new A_ServerEntityList<User>({ entity: User, pagination: { page: 1, pageSize: 10 } });
310
+ clientScope.register(list);
311
+ await list.load();
312
+
313
+ list.unshift(new User({ id: 99, name: 'First', email: 'first@example.com' }));
314
+
315
+ expect(list.length).toBe(3);
316
+ expect(list.at(0)!.name).toBe('First');
317
+ expect(list.at(1)!.name).toBe('John Doe');
318
+ });
319
+
320
+ // ── remove() ───────────────────────────────────────────────────────────
321
+
322
+ it('remove() deletes the item at the given index', async () => {
323
+ const list = new A_ServerEntityList<User>({ entity: User, pagination: { page: 1, pageSize: 10 } });
324
+ clientScope.register(list);
325
+ await list.load();
326
+
327
+ list.remove(0);
328
+
329
+ expect(list.length).toBe(1);
330
+ expect(list.at(0)!.name).toBe('mr Smith');
331
+ });
332
+
333
+ // ── find() ─────────────────────────────────────────────────────────────
334
+
335
+ it('find() returns the first matching item', async () => {
336
+ const list = new A_ServerEntityList<User>({ entity: User, pagination: { page: 1, pageSize: 10 } });
337
+ clientScope.register(list);
338
+ await list.load();
339
+
340
+ const found = list.find(u => u.name === 'mr Smith');
341
+ expect(found).toBeInstanceOf(User);
342
+ expect(found!.email).toBe('mr.smith@doe.com');
343
+ });
344
+
345
+ it('find() returns undefined when no item matches', async () => {
346
+ const list = new A_ServerEntityList<User>({ entity: User, pagination: { page: 1, pageSize: 10 } });
347
+ clientScope.register(list);
348
+ await list.load();
349
+
350
+ expect(list.find(u => u.name === 'Nobody')).toBeUndefined();
351
+ });
352
+
353
+ // ── filter() ───────────────────────────────────────────────────────────
354
+
355
+ it('filter() returns all matching items without mutating the list', async () => {
356
+ const list = new A_ServerEntityList<User>({ entity: User, pagination: { page: 1, pageSize: 10 } });
357
+ clientScope.register(list);
358
+ await list.load();
359
+
360
+ const filtered = list.filter(u => u.id > 0);
361
+
362
+ expect(filtered).toHaveLength(2);
363
+ expect(list.length).toBe(2); // original unchanged
364
+ });
365
+
366
+ it('filter() returns empty array when nothing matches', async () => {
367
+ const list = new A_ServerEntityList<User>({ entity: User, pagination: { page: 1, pageSize: 10 } });
368
+ clientScope.register(list);
369
+ await list.load();
370
+
371
+ expect(list.filter(u => u.id > 999)).toHaveLength(0);
372
+ });
373
+
374
+ // ── cache ──────────────────────────────────────────────────────────────
375
+
376
+ it('isCached() returns false before setCache is called', () => {
377
+ const list = new A_ServerEntityList<User>({ entity: User });
378
+ expect(list.isCached()).toBe(false);
379
+ });
380
+
381
+ it('setCache() / isCached() / invalidateCache() lifecycle', async () => {
382
+ const list = new A_ServerEntityList<User>({ entity: User, pagination: { page: 1, pageSize: 10 } });
383
+ clientScope.register(list);
384
+ await list.load();
385
+
386
+ expect(list.isCached()).toBe(false);
387
+
388
+ list.setCache(60_000);
389
+ expect(list.isCached()).toBe(true);
390
+
391
+ list.invalidateCache();
392
+ expect(list.isCached()).toBe(false);
393
+ });
394
+
395
+ it('setCache() with a negative ttl reports as stale immediately', () => {
396
+ const list = new A_ServerEntityList<User>({ entity: User });
397
+ list.setCache(-1);
398
+ expect(list.isCached()).toBe(false);
399
+ });
400
+
401
+ // ── chaining ───────────────────────────────────────────────────────────
402
+
403
+ it('mutation methods return `this` for chaining', async () => {
404
+ const list = new A_ServerEntityList<User>({ entity: User, pagination: { page: 1, pageSize: 10 } });
405
+ clientScope.register(list);
406
+ await list.load();
407
+
408
+ const newUser = new User({ id: 3, name: 'Chained', email: 'chain@example.com' });
409
+ const result = list.push(newUser).remove(0).setCache(5_000);
410
+
411
+ expect(result).toBe(list);
412
+ expect(list.length).toBe(2);
413
+ expect(list.isCached()).toBe(true);
414
+ });
415
+ });
416
+ });