@signalium/query 0.1.0 → 1.0.1

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 (113) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/cjs/EntityMap.js +2 -2
  3. package/dist/cjs/EntityMap.js.map +1 -1
  4. package/dist/cjs/NetworkManager.js +105 -0
  5. package/dist/cjs/NetworkManager.js.map +1 -0
  6. package/dist/cjs/QueryClient.js +390 -76
  7. package/dist/cjs/QueryClient.js.map +1 -1
  8. package/dist/cjs/QueryStore.js +295 -3
  9. package/dist/cjs/QueryStore.js.map +1 -1
  10. package/dist/cjs/index.js +18 -1
  11. package/dist/cjs/index.js.map +1 -1
  12. package/dist/cjs/package.json +3 -0
  13. package/dist/cjs/parseEntities.js +3 -0
  14. package/dist/cjs/parseEntities.js.map +1 -1
  15. package/dist/cjs/proxy.js +19 -0
  16. package/dist/cjs/proxy.js.map +1 -1
  17. package/dist/cjs/query.js +40 -2
  18. package/dist/cjs/query.js.map +1 -1
  19. package/dist/cjs/stores/async.js +6 -0
  20. package/dist/cjs/stores/async.js.map +1 -0
  21. package/dist/cjs/stores/sync.js +7 -0
  22. package/dist/cjs/stores/sync.js.map +1 -0
  23. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  24. package/dist/cjs/type-utils.js +3 -0
  25. package/dist/cjs/type-utils.js.map +1 -0
  26. package/dist/cjs/types.js +19 -1
  27. package/dist/cjs/types.js.map +1 -1
  28. package/dist/esm/EntityMap.js +3 -3
  29. package/dist/esm/EntityMap.js.map +1 -1
  30. package/dist/esm/NetworkManager.d.ts +48 -0
  31. package/dist/esm/NetworkManager.d.ts.map +1 -0
  32. package/dist/esm/NetworkManager.js +101 -0
  33. package/dist/esm/NetworkManager.js.map +1 -0
  34. package/dist/esm/QueryClient.d.ts +81 -25
  35. package/dist/esm/QueryClient.d.ts.map +1 -1
  36. package/dist/esm/QueryClient.js +390 -76
  37. package/dist/esm/QueryClient.js.map +1 -1
  38. package/dist/esm/QueryStore.d.ts +64 -2
  39. package/dist/esm/QueryStore.d.ts.map +1 -1
  40. package/dist/esm/QueryStore.js +293 -2
  41. package/dist/esm/QueryStore.js.map +1 -1
  42. package/dist/esm/index.d.ts +6 -3
  43. package/dist/esm/index.d.ts.map +1 -1
  44. package/dist/esm/index.js +4 -1
  45. package/dist/esm/index.js.map +1 -1
  46. package/dist/esm/parseEntities.d.ts.map +1 -1
  47. package/dist/esm/parseEntities.js +3 -0
  48. package/dist/esm/parseEntities.js.map +1 -1
  49. package/dist/esm/proxy.d.ts +6 -0
  50. package/dist/esm/proxy.d.ts.map +1 -1
  51. package/dist/esm/proxy.js +18 -0
  52. package/dist/esm/proxy.js.map +1 -1
  53. package/dist/esm/query.d.ts +30 -29
  54. package/dist/esm/query.d.ts.map +1 -1
  55. package/dist/esm/query.js +39 -3
  56. package/dist/esm/query.js.map +1 -1
  57. package/dist/esm/stores/async.d.ts +2 -0
  58. package/dist/esm/stores/async.d.ts.map +1 -0
  59. package/dist/esm/stores/async.js +2 -0
  60. package/dist/esm/stores/async.js.map +1 -0
  61. package/dist/esm/stores/sync.d.ts +2 -0
  62. package/dist/esm/stores/sync.d.ts.map +1 -0
  63. package/dist/esm/stores/sync.js +2 -0
  64. package/dist/esm/stores/sync.js.map +1 -0
  65. package/dist/esm/type-utils.d.ts +12 -0
  66. package/dist/esm/type-utils.d.ts.map +1 -0
  67. package/dist/esm/type-utils.js +2 -0
  68. package/dist/esm/type-utils.js.map +1 -0
  69. package/dist/esm/types.d.ts +62 -5
  70. package/dist/esm/types.d.ts.map +1 -1
  71. package/dist/esm/types.js +18 -0
  72. package/dist/esm/types.js.map +1 -1
  73. package/index.d.ts +1 -0
  74. package/package.json +25 -4
  75. package/stores/async.d.ts +1 -0
  76. package/stores/async.js +15 -0
  77. package/stores/sync.d.ts +1 -0
  78. package/stores/sync.js +15 -0
  79. package/.turbo/turbo-build.log +0 -12
  80. package/ENTITY_STORE_DESIGN.md +0 -386
  81. package/dist/tsconfig.esm.tsbuildinfo +0 -1
  82. package/src/EntityMap.ts +0 -63
  83. package/src/QueryClient.ts +0 -482
  84. package/src/QueryStore.ts +0 -322
  85. package/src/__tests__/caching-persistence.test.ts +0 -983
  86. package/src/__tests__/entity-system.test.ts +0 -556
  87. package/src/__tests__/gc-time.test.ts +0 -327
  88. package/src/__tests__/mock-fetch.test.ts +0 -186
  89. package/src/__tests__/parse-entities.test.ts +0 -425
  90. package/src/__tests__/path-interpolation.test.ts +0 -225
  91. package/src/__tests__/reactivity.test.ts +0 -424
  92. package/src/__tests__/refetch-interval.test.ts +0 -262
  93. package/src/__tests__/rest-query-api.test.ts +0 -568
  94. package/src/__tests__/stale-time.test.ts +0 -357
  95. package/src/__tests__/type-to-string.test.ts +0 -129
  96. package/src/__tests__/utils.ts +0 -258
  97. package/src/__tests__/validation-edge-cases.test.ts +0 -821
  98. package/src/errors.ts +0 -124
  99. package/src/index.ts +0 -7
  100. package/src/parseEntities.ts +0 -213
  101. package/src/pathInterpolator.ts +0 -74
  102. package/src/proxy.ts +0 -257
  103. package/src/query.ts +0 -164
  104. package/src/react/__tests__/basic.test.tsx +0 -926
  105. package/src/react/__tests__/component.test.tsx +0 -984
  106. package/src/react/__tests__/utils.tsx +0 -71
  107. package/src/typeDefs.ts +0 -351
  108. package/src/types.ts +0 -132
  109. package/src/utils.ts +0 -66
  110. package/tsconfig.cjs.json +0 -14
  111. package/tsconfig.esm.json +0 -13
  112. package/tsconfig.json +0 -20
  113. package/vitest.config.ts +0 -65
@@ -1,568 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
- import { SyncQueryStore, MemoryPersistentStore } from '../QueryStore.js';
3
- import { QueryClient } from '../QueryClient.js';
4
- import { entity, t } from '../typeDefs.js';
5
- import { query } from '../query.js';
6
- import { createMockFetch, testWithClient, getEntityMapSize } from './utils.js';
7
-
8
- /**
9
- * REST Query API Tests
10
- *
11
- * These tests focus on the PUBLIC query() API - what users will actually use.
12
- * All external fetch calls are mocked.
13
- */
14
-
15
- describe('REST Query API', () => {
16
- let client: QueryClient;
17
- let mockFetch: ReturnType<typeof createMockFetch>;
18
-
19
- beforeEach(() => {
20
- const store = new SyncQueryStore(new MemoryPersistentStore());
21
- mockFetch = createMockFetch();
22
- client = new QueryClient(store, { fetch: mockFetch as any });
23
- });
24
-
25
- afterEach(() => {
26
- client?.destroy();
27
- });
28
-
29
- describe('Basic Query Execution', () => {
30
- it('should execute a GET query with path parameters', async () => {
31
- mockFetch.get('/users/[id]', { id: 123, name: 'Test User' });
32
-
33
- const getUser = query(t => ({
34
- path: '/users/[id]',
35
- response: {
36
- id: t.number,
37
- name: t.string,
38
- },
39
- }));
40
-
41
- await testWithClient(client, async () => {
42
- const relay = getUser({ id: '123' });
43
- const result = await relay;
44
-
45
- expect(result.id).toBe(123);
46
- expect(result.name).toBe('Test User');
47
- expect(mockFetch.calls[0].url).toBe('/users/123');
48
- expect(mockFetch.calls[0].options.method).toBe('GET');
49
- });
50
- });
51
-
52
- it('should execute a GET query with search parameters', async () => {
53
- mockFetch.get('/users', { users: [], page: 1, total: 0 });
54
-
55
- const listUsers = query(t => ({
56
- path: '/users',
57
- searchParams: {
58
- page: t.number,
59
- limit: t.number,
60
- },
61
- response: {
62
- users: t.array(
63
- t.object({
64
- id: t.number,
65
- name: t.string,
66
- }),
67
- ),
68
- page: t.number,
69
- total: t.number,
70
- },
71
- }));
72
-
73
- await testWithClient(client, async () => {
74
- const relay = listUsers({ page: 1, limit: 10 });
75
- const result = await relay;
76
-
77
- expect(result.page).toBe(1);
78
- expect(result.total).toBe(0);
79
- // Verify URL was constructed with search params
80
- const callUrl = mockFetch.calls[0].url;
81
- expect(callUrl).toContain('page=1');
82
- expect(callUrl).toContain('limit=10');
83
- });
84
- });
85
-
86
- it('should execute a GET query with both path and search params', async () => {
87
- mockFetch.get('/users/[userId]/posts', { posts: [], userId: 5 });
88
-
89
- const getUserPosts = query(t => ({
90
- path: '/users/[userId]/posts',
91
- searchParams: {
92
- status: t.string,
93
- },
94
- response: {
95
- posts: t.array(
96
- t.object({
97
- id: t.number,
98
- title: t.string,
99
- }),
100
- ),
101
- userId: t.number,
102
- },
103
- }));
104
-
105
- await testWithClient(client, async () => {
106
- const relay = getUserPosts({ userId: '5', status: 'published' });
107
- const result = await relay;
108
-
109
- expect(result.userId).toBe(5);
110
- const callUrl = mockFetch.calls[0].url;
111
- expect(callUrl).toContain('/users/5/posts');
112
- expect(callUrl).toContain('status=published');
113
- });
114
- });
115
-
116
- it('should execute POST requests', async () => {
117
- mockFetch.post('/users', { id: 456, name: 'New User', created: true });
118
-
119
- const createUser = query(t => ({
120
- path: '/users',
121
- method: 'POST',
122
- response: {
123
- id: t.number,
124
- name: t.string,
125
- created: t.boolean,
126
- },
127
- }));
128
-
129
- await testWithClient(client, async () => {
130
- const relay = createUser();
131
- const result = await relay;
132
-
133
- expect(result.id).toBe(456);
134
- expect(result.created).toBe(true);
135
- expect(mockFetch.calls[0].url).toBe('/users');
136
- expect(mockFetch.calls[0].options.method).toBe('POST');
137
- });
138
- });
139
-
140
- it('should execute PUT requests', async () => {
141
- mockFetch.put('/users/[id]', { id: 123, name: 'Updated User', updated: true });
142
-
143
- const updateUser = query(t => ({
144
- path: '/users/[id]',
145
- method: 'PUT',
146
- response: {
147
- id: t.number,
148
- name: t.string,
149
- updated: t.boolean,
150
- },
151
- }));
152
-
153
- await testWithClient(client, async () => {
154
- const relay = updateUser({ id: '123' });
155
- const result = await relay;
156
-
157
- expect(result.updated).toBe(true);
158
- expect(mockFetch.calls[0].url).toBe('/users/123');
159
- expect(mockFetch.calls[0].options.method).toBe('PUT');
160
- });
161
- });
162
-
163
- it('should execute DELETE requests', async () => {
164
- mockFetch.delete('/users/[id]', { success: true, id: 123 });
165
-
166
- const deleteUser = query(t => ({
167
- path: '/users/[id]',
168
- method: 'DELETE',
169
- response: {
170
- success: t.boolean,
171
- id: t.number,
172
- },
173
- }));
174
-
175
- await testWithClient(client, async () => {
176
- const relay = deleteUser({ id: '123' });
177
- const result = await relay;
178
-
179
- expect(result.success).toBe(true);
180
- expect(mockFetch.calls[0].url).toBe('/users/123');
181
- expect(mockFetch.calls[0].options.method).toBe('DELETE');
182
- });
183
- });
184
-
185
- it('should execute PATCH requests', async () => {
186
- mockFetch.patch('/users/[id]', {
187
- id: 123,
188
- email: 'new@example.com',
189
- patched: true,
190
- });
191
-
192
- const patchUser = query(t => ({
193
- path: '/users/[id]',
194
- method: 'PATCH',
195
- response: {
196
- id: t.number,
197
- email: t.string,
198
- patched: t.boolean,
199
- },
200
- }));
201
-
202
- await testWithClient(client, async () => {
203
- const relay = patchUser({ id: '123' });
204
- const result = await relay;
205
-
206
- expect(result.patched).toBe(true);
207
- expect(mockFetch.calls[0].url).toBe('/users/123');
208
- expect(mockFetch.calls[0].options.method).toBe('PATCH');
209
- });
210
- });
211
- });
212
-
213
- describe('Error Handling', () => {
214
- it('should handle network errors', async () => {
215
- const error = new Error('Network connection failed');
216
- mockFetch.get('/users/[id]', null, { error });
217
-
218
- const getUser = query(t => ({
219
- path: '/users/[id]',
220
- response: {
221
- id: t.number,
222
- name: t.string,
223
- },
224
- }));
225
-
226
- await testWithClient(client, async () => {
227
- const relay = getUser({ id: '123' });
228
-
229
- await expect(relay).rejects.toThrow('Network connection failed');
230
- expect(relay.isRejected).toBe(true);
231
- expect(relay.error).toBe(error);
232
- });
233
- });
234
-
235
- it('should handle malformed JSON responses', async () => {
236
- mockFetch.get('/users/[id]', null, {
237
- jsonError: new Error('Unexpected token in JSON'),
238
- });
239
-
240
- const getUser = query(t => ({
241
- path: '/users/[id]',
242
- response: {
243
- id: t.number,
244
- name: t.string,
245
- },
246
- }));
247
-
248
- await testWithClient(client, async () => {
249
- const relay = getUser({ id: '123' });
250
-
251
- await expect(relay).rejects.toThrow('Unexpected token in JSON');
252
-
253
- expect(relay.isRejected).toBe(true);
254
- expect(relay.error).toBeInstanceOf(Error);
255
- expect((relay.error as Error).message).toBe('Unexpected token in JSON');
256
- });
257
- });
258
-
259
- it('should require QueryClient context', async () => {
260
- const getUser = query(t => ({
261
- path: '/users/[id]',
262
- response: {
263
- id: t.number,
264
- name: t.string,
265
- },
266
- }));
267
-
268
- // Call without context
269
- expect(() => getUser({ id: '123' })).toThrow('QueryClient not found');
270
- });
271
- });
272
-
273
- describe('Query Deduplication', () => {
274
- it('should deduplicate identical queries', async () => {
275
- mockFetch.get('/users/[id]', { id: 123, name: 'Test User' });
276
-
277
- const getUser = query(t => ({
278
- path: '/users/[id]',
279
- response: {
280
- id: t.number,
281
- name: t.string,
282
- },
283
- }));
284
-
285
- await testWithClient(client, async () => {
286
- const relay1 = getUser({ id: '123' });
287
- const relay2 = getUser({ id: '123' });
288
- const relay3 = getUser({ id: '123' });
289
-
290
- // Should return the same relay instance
291
- expect(relay1).toBe(relay2);
292
- expect(relay2).toBe(relay3);
293
-
294
- await relay1;
295
-
296
- // Should only fetch once
297
- expect(mockFetch.calls).toHaveLength(1);
298
- });
299
- });
300
-
301
- it('should create separate queries for different parameters', async () => {
302
- // Mocks are matched in LIFO order (last added is matched first)
303
- mockFetch.get('/users/1', { id: 1, name: 'User' });
304
- mockFetch.get('/users/2', { id: 2, name: 'User' });
305
-
306
- const getUser = query(t => ({
307
- path: '/users/[id]',
308
- response: {
309
- id: t.number,
310
- name: t.string,
311
- },
312
- }));
313
-
314
- await testWithClient(client, async () => {
315
- const relay1 = getUser({ id: '1' });
316
- const relay2 = getUser({ id: '2' });
317
-
318
- // Should be different relay instances
319
- expect(relay1).not.toBe(relay2);
320
-
321
- const [result1, result2] = await Promise.all([relay1, relay2]);
322
-
323
- expect(result1.id).toBe(1);
324
- expect(result2.id).toBe(2);
325
- expect(mockFetch.calls).toHaveLength(2);
326
- });
327
- });
328
- });
329
-
330
- describe('Response Type Handling', () => {
331
- it('should handle primitive response types', async () => {
332
- mockFetch.get('/message', 'Hello, World!');
333
-
334
- const getMessage = query(t => ({
335
- path: '/message',
336
- response: t.string,
337
- }));
338
-
339
- await testWithClient(client, async () => {
340
- const relay = getMessage();
341
- const result = await relay;
342
-
343
- expect(result).toBe('Hello, World!');
344
- });
345
- });
346
-
347
- it('should handle array responses', async () => {
348
- mockFetch.get('/numbers', [1, 2, 3, 4, 5]);
349
-
350
- const getNumbers = query(t => ({
351
- path: '/numbers',
352
- response: t.array(t.number),
353
- }));
354
-
355
- await testWithClient(client, async () => {
356
- const relay = getNumbers();
357
- const result = await relay;
358
-
359
- expect(result).toEqual([1, 2, 3, 4, 5]);
360
- });
361
- });
362
-
363
- it('should handle nested object responses', async () => {
364
- mockFetch.get('/user', {
365
- user: {
366
- id: 1,
367
- profile: {
368
- name: 'Alice',
369
- email: 'alice@example.com',
370
- },
371
- },
372
- });
373
-
374
- const getUser = query(t => ({
375
- path: '/user',
376
- response: {
377
- user: t.object({
378
- id: t.number,
379
- profile: t.object({
380
- name: t.string,
381
- email: t.string,
382
- }),
383
- }),
384
- },
385
- }));
386
-
387
- await testWithClient(client, async () => {
388
- const relay = getUser();
389
- const result = await relay;
390
-
391
- expect(result.user.profile.name).toBe('Alice');
392
- expect(result.user.profile.email).toBe('alice@example.com');
393
- });
394
- });
395
- });
396
-
397
- describe('Entity Handling', () => {
398
- it('should handle entity responses', async () => {
399
- const User = entity(() => ({
400
- __typename: t.typename('User'),
401
- id: t.id,
402
- name: t.string,
403
- email: t.string,
404
- }));
405
-
406
- mockFetch.get('/users/[id]', {
407
- user: {
408
- __typename: 'User',
409
- id: 1,
410
- name: 'Alice',
411
- email: 'alice@example.com',
412
- },
413
- });
414
-
415
- const getUser = query(t => ({
416
- path: '/users/[id]',
417
- response: {
418
- user: User,
419
- },
420
- }));
421
-
422
- await testWithClient(client, async () => {
423
- const relay = getUser({ id: '1' });
424
- const result = await relay;
425
-
426
- expect(result.user.name).toBe('Alice');
427
- expect(result.user.email).toBe('alice@example.com');
428
-
429
- // Verify entity was cached
430
- expect(getEntityMapSize(client)).toBe(1);
431
- });
432
- });
433
-
434
- it('should handle array of entities', async () => {
435
- const User = entity(() => ({
436
- __typename: t.typename('User'),
437
- id: t.id,
438
- name: t.string,
439
- }));
440
-
441
- mockFetch.get('/users', {
442
- users: [
443
- { __typename: 'User', id: 1, name: 'Alice' },
444
- { __typename: 'User', id: 2, name: 'Bob' },
445
- { __typename: 'User', id: 3, name: 'Charlie' },
446
- ],
447
- });
448
-
449
- const listUsers = query(t => ({
450
- path: '/users',
451
- response: {
452
- users: t.array(User),
453
- },
454
- }));
455
-
456
- await testWithClient(client, async () => {
457
- const relay = listUsers();
458
- const result = await relay;
459
-
460
- expect(result.users).toHaveLength(3);
461
- expect(result.users[0].name).toBe('Alice');
462
- expect(result.users[1].name).toBe('Bob');
463
- expect(result.users[2].name).toBe('Charlie');
464
-
465
- // Verify all entities were cached
466
- expect(getEntityMapSize(client)).toBe(3);
467
- });
468
- });
469
-
470
- it('should deduplicate entities across queries', async () => {
471
- const User = entity(() => ({
472
- __typename: t.typename('User'),
473
- id: t.id,
474
- name: t.string,
475
- }));
476
-
477
- mockFetch.get('/users/[id]', {
478
- user: { __typename: 'User', id: 1, name: 'Alice' },
479
- });
480
- mockFetch.get('/users', {
481
- users: [{ __typename: 'User', id: 1, name: 'Alice' }],
482
- });
483
-
484
- await testWithClient(client, async () => {
485
- const getUser = query(t => ({
486
- path: '/users/[id]',
487
- response: {
488
- user: User,
489
- },
490
- }));
491
-
492
- const listUsers = query(t => ({
493
- path: '/users',
494
- response: {
495
- users: t.array(User),
496
- },
497
- }));
498
-
499
- const relay1 = getUser({ id: '1' });
500
- const result1 = await relay1;
501
-
502
- const relay2 = listUsers();
503
- const result2 = await relay2;
504
-
505
- // Should be the same entity proxy
506
- expect(result1.user).toBe(result2.users[0]);
507
- });
508
- });
509
- });
510
-
511
- describe('Optional Parameters', () => {
512
- it('should handle optional search parameters', async () => {
513
- mockFetch.get('/users', { users: [] });
514
- mockFetch.get('/users', { users: [] });
515
- mockFetch.get('/users', { users: [] });
516
-
517
- await testWithClient(client, async () => {
518
- const listUsers = query(t => ({
519
- path: '/users',
520
- searchParams: {
521
- page: t.union(t.number, t.undefined),
522
- limit: t.union(t.number, t.undefined),
523
- },
524
- response: {
525
- users: t.array(t.object({ id: t.number, name: t.string })),
526
- },
527
- }));
528
-
529
- // Call without optional params
530
- const relay1 = listUsers({});
531
- await relay1;
532
-
533
- // Call with some params
534
- const relay2 = listUsers({ page: 1 });
535
- await relay2;
536
-
537
- // Call with all params
538
- const relay3 = listUsers({ page: 1, limit: 10 });
539
- await relay3;
540
-
541
- expect(mockFetch.calls).toHaveLength(3);
542
- });
543
- });
544
- });
545
-
546
- describe('Type Safety', () => {
547
- it('should infer correct types for path parameters', async () => {
548
- mockFetch.get('/items/[itemId]/details/[detailId]', { id: 1, name: 'Test' });
549
-
550
- const getItem = query(t => ({
551
- path: '/items/[itemId]/details/[detailId]',
552
- response: {
553
- id: t.number,
554
- name: t.string,
555
- },
556
- }));
557
-
558
- await testWithClient(client, async () => {
559
- // TypeScript should require both path params
560
- const relay = getItem({ itemId: '1', detailId: '2' });
561
- await relay;
562
-
563
- expect(mockFetch.calls[0].url).toContain('/items/1/details/2');
564
- expect(mockFetch.calls[0].options.method).toBe('GET');
565
- });
566
- });
567
- });
568
- });