@signalium/query 0.0.0
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.
- package/ENTITY_STORE_DESIGN.md +386 -0
- package/package.json +71 -0
- package/src/EntityMap.ts +63 -0
- package/src/QueryClient.ts +266 -0
- package/src/QueryStore.ts +314 -0
- package/src/__tests__/caching-persistence.test.ts +954 -0
- package/src/__tests__/entity-system.test.ts +552 -0
- package/src/__tests__/mock-fetch.test.ts +182 -0
- package/src/__tests__/parse-entities.test.ts +421 -0
- package/src/__tests__/path-interpolation.test.ts +225 -0
- package/src/__tests__/reactivity.test.ts +420 -0
- package/src/__tests__/rest-query-api.test.ts +564 -0
- package/src/__tests__/type-to-string.test.ts +129 -0
- package/src/__tests__/utils.ts +242 -0
- package/src/__tests__/validation-edge-cases.test.ts +820 -0
- package/src/errors.ts +124 -0
- package/src/index.ts +7 -0
- package/src/parseEntities.ts +213 -0
- package/src/pathInterpolator.ts +74 -0
- package/src/proxy.ts +257 -0
- package/src/query.ts +163 -0
- package/src/react/__tests__/basic.test.tsx +921 -0
- package/src/react/__tests__/component.test.tsx +977 -0
- package/src/react/__tests__/utils.tsx +71 -0
- package/src/typeDefs.ts +351 -0
- package/src/types.ts +121 -0
- package/src/utils.ts +66 -0
- package/tsconfig.cjs.json +14 -0
- package/tsconfig.esm.json +13 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +71 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { SyncQueryStore, MemoryPersistentStore, refIdsKeyFor, refCountKeyFor } from '../QueryStore.js';
|
|
3
|
+
import { QueryClient } from '../QueryClient.js';
|
|
4
|
+
import { entity, t } from '../typeDefs.js';
|
|
5
|
+
import { query, ExtractType } from '../query.js';
|
|
6
|
+
import { parseObjectEntities, parseArrayEntities, parseEntities } from '../parseEntities.js';
|
|
7
|
+
import { createMockFetch, getClientEntityMap, getEntityMapSize, testWithClient } from './utils.js';
|
|
8
|
+
import { hashValue } from 'signalium/utils';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Entity System Tests
|
|
12
|
+
*
|
|
13
|
+
* Tests entity parsing, deduplication, caching, proxy behavior, and reactivity.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
describe('Entity System', () => {
|
|
17
|
+
let client: QueryClient;
|
|
18
|
+
let mockFetch: ReturnType<typeof createMockFetch>;
|
|
19
|
+
let kv: any;
|
|
20
|
+
let store: any;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
kv = new MemoryPersistentStore();
|
|
24
|
+
store = new SyncQueryStore(kv);
|
|
25
|
+
mockFetch = createMockFetch();
|
|
26
|
+
client = new QueryClient(store, { fetch: mockFetch as any });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('Entity Proxies', () => {
|
|
30
|
+
it('should create reactive entity proxies', async () => {
|
|
31
|
+
const User = entity(() => ({
|
|
32
|
+
__typename: t.typename('User'),
|
|
33
|
+
id: t.id,
|
|
34
|
+
name: t.string,
|
|
35
|
+
email: t.string,
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
mockFetch.get('/users/[id]', {
|
|
39
|
+
user: {
|
|
40
|
+
__typename: 'User',
|
|
41
|
+
id: 1,
|
|
42
|
+
name: 'Alice',
|
|
43
|
+
email: 'alice@example.com',
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await testWithClient(client, async () => {
|
|
48
|
+
const getUser = query(t => ({
|
|
49
|
+
path: '/users/[id]',
|
|
50
|
+
response: { user: User },
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
const relay = getUser({ id: '1' });
|
|
54
|
+
const result = await relay;
|
|
55
|
+
|
|
56
|
+
// Verify proxy provides reactive access
|
|
57
|
+
expect(result.user.name).toBe('Alice');
|
|
58
|
+
expect(result.user.email).toBe('alice@example.com');
|
|
59
|
+
|
|
60
|
+
// Verify entity is in the entity map
|
|
61
|
+
const entityMap = getClientEntityMap(client);
|
|
62
|
+
const userKey = hashValue('User:1');
|
|
63
|
+
const entityRecord = entityMap.getEntity(userKey);
|
|
64
|
+
|
|
65
|
+
expect(entityRecord).toBeDefined();
|
|
66
|
+
expect(entityRecord!.proxy).toBe(result.user);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should cache property access in entity proxies', async () => {
|
|
71
|
+
const User = entity(() => ({
|
|
72
|
+
__typename: t.typename('User'),
|
|
73
|
+
id: t.id,
|
|
74
|
+
name: t.string,
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
mockFetch.get('/users/[id]', {
|
|
78
|
+
user: { __typename: 'User', id: 1, name: 'Alice' },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await testWithClient(client, async () => {
|
|
82
|
+
const getUser = query(t => ({
|
|
83
|
+
path: '/users/[id]',
|
|
84
|
+
response: { user: User },
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
const relay = getUser({ id: '1' });
|
|
88
|
+
const result = await relay;
|
|
89
|
+
|
|
90
|
+
const user = result.user;
|
|
91
|
+
|
|
92
|
+
// Access same property multiple times
|
|
93
|
+
const name1 = user.name;
|
|
94
|
+
const name2 = user.name;
|
|
95
|
+
const name3 = user.name;
|
|
96
|
+
|
|
97
|
+
// All should return the same value
|
|
98
|
+
expect(name1).toBe('Alice');
|
|
99
|
+
expect(name2).toBe('Alice');
|
|
100
|
+
expect(name3).toBe('Alice');
|
|
101
|
+
|
|
102
|
+
// Verify caching by checking the entity's cache
|
|
103
|
+
const entityMap = getClientEntityMap(client);
|
|
104
|
+
const userKey = hashValue('User:1');
|
|
105
|
+
const entityRecord = entityMap.getEntity(userKey);
|
|
106
|
+
|
|
107
|
+
expect(entityRecord!.cache.has('name')).toBe(true);
|
|
108
|
+
expect(entityRecord!.cache.get('name')).toBe('Alice');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should return updated entity data when refetched', async () => {
|
|
113
|
+
const User = entity(() => ({
|
|
114
|
+
__typename: t.typename('User'),
|
|
115
|
+
id: t.id,
|
|
116
|
+
name: t.string,
|
|
117
|
+
}));
|
|
118
|
+
|
|
119
|
+
mockFetch.get('/users/[id]', {
|
|
120
|
+
user: { __typename: 'User', id: 1, name: 'Alice' },
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await testWithClient(client, async () => {
|
|
124
|
+
const getUser = query(t => ({
|
|
125
|
+
path: '/users/[id]',
|
|
126
|
+
response: { user: User },
|
|
127
|
+
}));
|
|
128
|
+
|
|
129
|
+
const relay = getUser({ id: '1' });
|
|
130
|
+
const initialResult = await relay;
|
|
131
|
+
expect(initialResult.user.name).toBe('Alice');
|
|
132
|
+
|
|
133
|
+
// Set up updated response after initial fetch
|
|
134
|
+
mockFetch.get('/users/[id]', {
|
|
135
|
+
user: { __typename: 'User', id: 1, name: 'Alice Updated' },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Refetch to get updated data
|
|
139
|
+
const refetchedResult = await relay.refetch();
|
|
140
|
+
|
|
141
|
+
// Refetch should return the new data
|
|
142
|
+
expect(refetchedResult.user.name).toBe('Alice Updated');
|
|
143
|
+
|
|
144
|
+
// The relay value should also be updated
|
|
145
|
+
expect(relay.value!.user.name).toBe('Alice Updated');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('Entity Deduplication', () => {
|
|
151
|
+
it('should deduplicate entities within same response', async () => {
|
|
152
|
+
const User = entity(() => ({
|
|
153
|
+
__typename: t.typename('User'),
|
|
154
|
+
id: t.id,
|
|
155
|
+
name: t.string,
|
|
156
|
+
}));
|
|
157
|
+
|
|
158
|
+
mockFetch.get('/users', {
|
|
159
|
+
users: [
|
|
160
|
+
{ __typename: 'User', id: 1, name: 'Alice' },
|
|
161
|
+
{ __typename: 'User', id: 2, name: 'Bob' },
|
|
162
|
+
{ __typename: 'User', id: 1, name: 'Alice' }, // Duplicate
|
|
163
|
+
{ __typename: 'User', id: 3, name: 'Charlie' },
|
|
164
|
+
{ __typename: 'User', id: 2, name: 'Bob' }, // Another duplicate
|
|
165
|
+
],
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
await testWithClient(client, async () => {
|
|
169
|
+
const getUsers = query(t => ({
|
|
170
|
+
path: '/users',
|
|
171
|
+
response: {
|
|
172
|
+
users: t.array(User),
|
|
173
|
+
},
|
|
174
|
+
}));
|
|
175
|
+
|
|
176
|
+
const relay = getUsers();
|
|
177
|
+
const result = await relay;
|
|
178
|
+
|
|
179
|
+
// Verify array length
|
|
180
|
+
expect(result.users).toHaveLength(5);
|
|
181
|
+
|
|
182
|
+
// Should only have 3 unique entities in array (deduplication works)
|
|
183
|
+
expect(new Set(result.users).size).toBe(3);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should share entities across multiple queries', async () => {
|
|
188
|
+
const User = entity(() => ({
|
|
189
|
+
__typename: t.typename('User'),
|
|
190
|
+
id: t.id,
|
|
191
|
+
name: t.string,
|
|
192
|
+
}));
|
|
193
|
+
|
|
194
|
+
mockFetch.get('/users/[id]', {
|
|
195
|
+
user: { __typename: 'User', id: 1, name: 'Alice' },
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
mockFetch.get('/users', {
|
|
199
|
+
users: [
|
|
200
|
+
{ __typename: 'User', id: 1, name: 'Alice' },
|
|
201
|
+
{ __typename: 'User', id: 2, name: 'Bob' },
|
|
202
|
+
],
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
mockFetch.get('/author', {
|
|
206
|
+
author: { __typename: 'User', id: 1, name: 'Alice' },
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
await testWithClient(client, async () => {
|
|
210
|
+
const getUser = query(t => ({
|
|
211
|
+
path: '/users/[id]',
|
|
212
|
+
response: { user: User },
|
|
213
|
+
}));
|
|
214
|
+
|
|
215
|
+
const listUsers = query(t => ({
|
|
216
|
+
path: '/users',
|
|
217
|
+
response: { users: t.array(User) },
|
|
218
|
+
}));
|
|
219
|
+
|
|
220
|
+
const getAuthor = query(t => ({
|
|
221
|
+
path: '/author',
|
|
222
|
+
response: { author: User },
|
|
223
|
+
}));
|
|
224
|
+
|
|
225
|
+
const relay1 = getUser({ id: '1' });
|
|
226
|
+
const result1 = await relay1;
|
|
227
|
+
|
|
228
|
+
const relay2 = listUsers();
|
|
229
|
+
const result2 = await relay2;
|
|
230
|
+
|
|
231
|
+
const relay3 = getAuthor();
|
|
232
|
+
const result3 = await relay3;
|
|
233
|
+
|
|
234
|
+
// All three should reference the same User entity (id: 1)
|
|
235
|
+
expect(result1.user).toBe(result2.users[0]);
|
|
236
|
+
expect(result1.user).toBe(result3.author);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('Nested Entities', () => {
|
|
242
|
+
it('should parse deeply nested entities', async () => {
|
|
243
|
+
const Address = entity(() => ({
|
|
244
|
+
__typename: t.typename('Address'),
|
|
245
|
+
id: t.id,
|
|
246
|
+
city: t.string,
|
|
247
|
+
country: t.string,
|
|
248
|
+
}));
|
|
249
|
+
|
|
250
|
+
const Company = entity(() => ({
|
|
251
|
+
__typename: t.typename('Company'),
|
|
252
|
+
id: t.id,
|
|
253
|
+
name: t.string,
|
|
254
|
+
address: Address,
|
|
255
|
+
}));
|
|
256
|
+
|
|
257
|
+
const User = entity(() => ({
|
|
258
|
+
__typename: t.typename('User'),
|
|
259
|
+
id: t.id,
|
|
260
|
+
name: t.string,
|
|
261
|
+
company: Company,
|
|
262
|
+
}));
|
|
263
|
+
|
|
264
|
+
mockFetch.get('/users/[id]', {
|
|
265
|
+
user: {
|
|
266
|
+
__typename: 'User',
|
|
267
|
+
id: 1,
|
|
268
|
+
name: 'Alice',
|
|
269
|
+
company: {
|
|
270
|
+
__typename: 'Company',
|
|
271
|
+
id: 1,
|
|
272
|
+
name: 'Tech Corp',
|
|
273
|
+
address: {
|
|
274
|
+
__typename: 'Address',
|
|
275
|
+
id: 1,
|
|
276
|
+
city: 'San Francisco',
|
|
277
|
+
country: 'USA',
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
await testWithClient(client, async () => {
|
|
284
|
+
const getUser = query(t => ({
|
|
285
|
+
path: '/users/[id]',
|
|
286
|
+
response: { user: User },
|
|
287
|
+
}));
|
|
288
|
+
|
|
289
|
+
const relay = getUser({ id: '1' });
|
|
290
|
+
const result = await relay;
|
|
291
|
+
|
|
292
|
+
// Access deeply nested property
|
|
293
|
+
expect(result.user.company.address.city).toBe('San Francisco');
|
|
294
|
+
|
|
295
|
+
// All three entities should be in the map
|
|
296
|
+
expect(getEntityMapSize(client)).toBe(3);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('should handle entities with multiple nested arrays', async () => {
|
|
301
|
+
const Post = entity(() => ({
|
|
302
|
+
__typename: t.typename('Post'),
|
|
303
|
+
id: t.id,
|
|
304
|
+
title: t.string,
|
|
305
|
+
}));
|
|
306
|
+
|
|
307
|
+
const User = entity(() => ({
|
|
308
|
+
__typename: t.typename('User'),
|
|
309
|
+
id: t.id,
|
|
310
|
+
name: t.string,
|
|
311
|
+
posts: t.array(Post),
|
|
312
|
+
}));
|
|
313
|
+
|
|
314
|
+
mockFetch.get('/users/[id]', {
|
|
315
|
+
user: {
|
|
316
|
+
__typename: 'User',
|
|
317
|
+
id: 1,
|
|
318
|
+
name: 'Alice',
|
|
319
|
+
posts: [
|
|
320
|
+
{
|
|
321
|
+
__typename: 'Post',
|
|
322
|
+
id: 1,
|
|
323
|
+
title: 'First Post',
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
__typename: 'Post',
|
|
327
|
+
id: 2,
|
|
328
|
+
title: 'Second Post',
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
await testWithClient(client, async () => {
|
|
335
|
+
const getUser = query(t => ({
|
|
336
|
+
path: '/users/[id]',
|
|
337
|
+
response: { user: User },
|
|
338
|
+
}));
|
|
339
|
+
|
|
340
|
+
const relay = getUser({ id: '1' });
|
|
341
|
+
const result = await relay;
|
|
342
|
+
|
|
343
|
+
// Verify user entity
|
|
344
|
+
expect(result.user.name).toBe('Alice');
|
|
345
|
+
|
|
346
|
+
// Verify posts array
|
|
347
|
+
expect(result.user.posts).toHaveLength(2);
|
|
348
|
+
expect(result.user.posts[0].title).toBe('First Post');
|
|
349
|
+
expect(result.user.posts[1].title).toBe('Second Post');
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
describe('Entity Parsing Functions', () => {
|
|
355
|
+
it('should parse object entities correctly', async () => {
|
|
356
|
+
const User = entity(() => ({
|
|
357
|
+
__typename: t.typename('User'),
|
|
358
|
+
id: t.id,
|
|
359
|
+
name: t.string,
|
|
360
|
+
}));
|
|
361
|
+
|
|
362
|
+
const entityRefs = new Set<number>();
|
|
363
|
+
const data = {
|
|
364
|
+
__typename: 'User',
|
|
365
|
+
id: 1,
|
|
366
|
+
name: 'Test',
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const result = await parseObjectEntities(data, User, client, entityRefs);
|
|
370
|
+
|
|
371
|
+
// Should return proxy
|
|
372
|
+
expect(getEntityMapSize(client)).toBe(1);
|
|
373
|
+
expect(entityRefs.size).toBe(1);
|
|
374
|
+
|
|
375
|
+
// Result should be a proxy
|
|
376
|
+
const userKey = hashValue('User:1');
|
|
377
|
+
expect(entityRefs.has(userKey)).toBe(true);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('should parse array entities correctly', async () => {
|
|
381
|
+
const User = entity(() => ({
|
|
382
|
+
__typename: t.typename('User'),
|
|
383
|
+
id: t.id,
|
|
384
|
+
name: t.string,
|
|
385
|
+
}));
|
|
386
|
+
|
|
387
|
+
const entityRefs = new Set<number>();
|
|
388
|
+
const data = [
|
|
389
|
+
{ __typename: 'User', id: 1, name: 'Alice' },
|
|
390
|
+
{ __typename: 'User', id: 2, name: 'Bob' },
|
|
391
|
+
];
|
|
392
|
+
|
|
393
|
+
const result = await parseArrayEntities(data, User, client, entityRefs);
|
|
394
|
+
|
|
395
|
+
// Should have parsed 2 entities
|
|
396
|
+
expect(getEntityMapSize(client)).toBe(2);
|
|
397
|
+
expect(entityRefs.size).toBe(2);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('should parse nested structures with mixed entities', async () => {
|
|
401
|
+
const User = entity(() => ({
|
|
402
|
+
__typename: t.typename('User'),
|
|
403
|
+
id: t.id,
|
|
404
|
+
name: t.string,
|
|
405
|
+
}));
|
|
406
|
+
|
|
407
|
+
const shape = t.object({
|
|
408
|
+
users: t.array(User),
|
|
409
|
+
admin: User,
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
const entityRefs = new Set<number>();
|
|
413
|
+
const data = {
|
|
414
|
+
users: [
|
|
415
|
+
{ __typename: 'User', id: 1, name: 'Alice' },
|
|
416
|
+
{ __typename: 'User', id: 2, name: 'Bob' },
|
|
417
|
+
],
|
|
418
|
+
admin: { __typename: 'User', id: 1, name: 'Alice' },
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const result = await parseEntities(data, shape, client, entityRefs);
|
|
422
|
+
|
|
423
|
+
// Should deduplicate the admin (same as users[0])
|
|
424
|
+
expect(getEntityMapSize(client)).toBe(2);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('should handle entities in records/dictionaries', async () => {
|
|
428
|
+
const User = entity(() => ({
|
|
429
|
+
__typename: t.typename('User'),
|
|
430
|
+
id: t.id,
|
|
431
|
+
name: t.string,
|
|
432
|
+
}));
|
|
433
|
+
|
|
434
|
+
mockFetch.get('/users/map', {
|
|
435
|
+
userMap: {
|
|
436
|
+
alice: { __typename: 'User', id: 1, name: 'Alice' },
|
|
437
|
+
bob: { __typename: 'User', id: 2, name: 'Bob' },
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
await testWithClient(client, async () => {
|
|
442
|
+
const getUserMap = query(t => ({
|
|
443
|
+
path: '/users/map',
|
|
444
|
+
response: {
|
|
445
|
+
userMap: t.record(User),
|
|
446
|
+
},
|
|
447
|
+
}));
|
|
448
|
+
|
|
449
|
+
const relay = getUserMap();
|
|
450
|
+
const result = await relay;
|
|
451
|
+
|
|
452
|
+
expect(result.userMap.alice.name).toBe('Alice');
|
|
453
|
+
expect(result.userMap.bob.name).toBe('Bob');
|
|
454
|
+
|
|
455
|
+
expect(getEntityMapSize(client)).toBe(2);
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('should handle union types with entities', async () => {
|
|
460
|
+
type TextPost = ExtractType<typeof TextPost>;
|
|
461
|
+
const TextPost = entity(() => ({
|
|
462
|
+
__typename: t.typename('TextPost'),
|
|
463
|
+
id: t.id,
|
|
464
|
+
content: t.string,
|
|
465
|
+
}));
|
|
466
|
+
|
|
467
|
+
type ImagePost = ExtractType<typeof ImagePost>;
|
|
468
|
+
const ImagePost = entity(() => ({
|
|
469
|
+
__typename: t.typename('ImagePost'),
|
|
470
|
+
id: t.id,
|
|
471
|
+
url: t.string,
|
|
472
|
+
}));
|
|
473
|
+
|
|
474
|
+
const PostUnion = t.union(TextPost, ImagePost);
|
|
475
|
+
|
|
476
|
+
mockFetch.get('/posts', {
|
|
477
|
+
posts: [
|
|
478
|
+
{ __typename: 'TextPost', type: 'text', id: '1', content: 'Hello' },
|
|
479
|
+
{ __typename: 'ImagePost', type: 'image', id: '2', url: '/img.jpg' },
|
|
480
|
+
{ __typename: 'TextPost', type: 'text', id: '3', content: 'World' },
|
|
481
|
+
],
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
await testWithClient(client, async () => {
|
|
485
|
+
const getPosts = query(t => ({
|
|
486
|
+
path: '/posts',
|
|
487
|
+
response: {
|
|
488
|
+
posts: t.array(PostUnion),
|
|
489
|
+
},
|
|
490
|
+
}));
|
|
491
|
+
|
|
492
|
+
const relay = getPosts();
|
|
493
|
+
const result = await relay;
|
|
494
|
+
|
|
495
|
+
expect(result.posts).toHaveLength(3);
|
|
496
|
+
|
|
497
|
+
const post1 = result.posts[0] as TextPost;
|
|
498
|
+
const post2 = result.posts[1] as ImagePost;
|
|
499
|
+
|
|
500
|
+
expect(post1.__typename).toBe('TextPost');
|
|
501
|
+
expect(post1.content).toBe('Hello');
|
|
502
|
+
|
|
503
|
+
expect(post2.__typename).toBe('ImagePost');
|
|
504
|
+
expect(post2.url).toBe('/img.jpg');
|
|
505
|
+
|
|
506
|
+
expect(getEntityMapSize(client)).toBe(3);
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
describe('Entity Map Management', () => {
|
|
512
|
+
it('should maintain entity map across queries', async () => {
|
|
513
|
+
const User = entity(() => ({
|
|
514
|
+
__typename: t.typename('User'),
|
|
515
|
+
id: t.id,
|
|
516
|
+
name: t.string,
|
|
517
|
+
}));
|
|
518
|
+
|
|
519
|
+
mockFetch.get('/users', {
|
|
520
|
+
users: [
|
|
521
|
+
{ __typename: 'User', id: 1, name: 'Alice' },
|
|
522
|
+
{ __typename: 'User', id: 2, name: 'Bob' },
|
|
523
|
+
],
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
await testWithClient(client, async () => {
|
|
527
|
+
const listUsers = query(t => ({
|
|
528
|
+
path: '/users',
|
|
529
|
+
response: { users: t.array(User) },
|
|
530
|
+
}));
|
|
531
|
+
|
|
532
|
+
// First query
|
|
533
|
+
const relay = listUsers();
|
|
534
|
+
await relay;
|
|
535
|
+
|
|
536
|
+
expect(getEntityMapSize(client)).toBe(2);
|
|
537
|
+
|
|
538
|
+
mockFetch.get('/users', {
|
|
539
|
+
users: [
|
|
540
|
+
{ __typename: 'User', id: 3, name: 'Charlie' },
|
|
541
|
+
{ __typename: 'User', id: 4, name: 'David' },
|
|
542
|
+
],
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
const result = await relay.refetch();
|
|
546
|
+
|
|
547
|
+
// The entity map should now have 4 entities
|
|
548
|
+
expect(getEntityMapSize(client)).toBe(4);
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
});
|