@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.
@@ -0,0 +1,182 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { createMockFetch } from './utils.js';
3
+
4
+ describe('createMockFetch', () => {
5
+ it('should handle GET requests', async () => {
6
+ const mockFetch = createMockFetch();
7
+ mockFetch.get('/users/123', { id: 123, name: 'Alice' });
8
+
9
+ const response = await mockFetch('/users/123', { method: 'GET' });
10
+ const data = await response.json();
11
+
12
+ expect(data).toEqual({ id: 123, name: 'Alice' });
13
+ expect(response.status).toBe(200);
14
+ expect(response.ok).toBe(true);
15
+ });
16
+
17
+ it('should handle POST requests with custom status', async () => {
18
+ const mockFetch = createMockFetch();
19
+ mockFetch.post('/users', { id: 456, name: 'Bob' }, { status: 201 });
20
+
21
+ const response = await mockFetch('/users', { method: 'POST' });
22
+ const data = await response.json();
23
+
24
+ expect(data).toEqual({ id: 456, name: 'Bob' });
25
+ expect(response.status).toBe(201);
26
+ });
27
+
28
+ it('should handle PUT requests', async () => {
29
+ const mockFetch = createMockFetch();
30
+ mockFetch.put('/users/123', { id: 123, name: 'Updated Alice' });
31
+
32
+ const response = await mockFetch('/users/123', { method: 'PUT' });
33
+ const data = await response.json();
34
+
35
+ expect(data).toEqual({ id: 123, name: 'Updated Alice' });
36
+ });
37
+
38
+ it('should handle DELETE requests', async () => {
39
+ const mockFetch = createMockFetch();
40
+ mockFetch.delete('/users/123', { success: true });
41
+
42
+ const response = await mockFetch('/users/123', { method: 'DELETE' });
43
+ const data = await response.json();
44
+
45
+ expect(data).toEqual({ success: true });
46
+ });
47
+
48
+ it('should handle PATCH requests', async () => {
49
+ const mockFetch = createMockFetch();
50
+ mockFetch.patch('/users/123', { id: 123, email: 'new@example.com' });
51
+
52
+ const response = await mockFetch('/users/123', { method: 'PATCH' });
53
+ const data = await response.json();
54
+
55
+ expect(data).toEqual({ id: 123, email: 'new@example.com' });
56
+ });
57
+
58
+ it('should support custom headers', async () => {
59
+ const mockFetch = createMockFetch();
60
+ mockFetch.get(
61
+ '/users/123',
62
+ { id: 123 },
63
+ {
64
+ headers: { 'X-Custom-Header': 'test-value' },
65
+ },
66
+ );
67
+
68
+ const response = await mockFetch('/users/123', { method: 'GET' });
69
+
70
+ expect(response.headers.get('X-Custom-Header')).toBe('test-value');
71
+ });
72
+
73
+ it('should support delays', async () => {
74
+ const mockFetch = createMockFetch();
75
+ mockFetch.get('/users/123', { id: 123 }, { delay: 100 });
76
+
77
+ const start = Date.now();
78
+ await mockFetch('/users/123', { method: 'GET' });
79
+ const duration = Date.now() - start;
80
+
81
+ expect(duration).toBeGreaterThanOrEqual(90); // Allow some margin
82
+ });
83
+
84
+ it('should match path parameters', async () => {
85
+ const mockFetch = createMockFetch();
86
+ mockFetch.get('/users/[id]', { id: 123, name: 'Alice' });
87
+
88
+ const response = await mockFetch('/users/123', { method: 'GET' });
89
+ const data = await response.json();
90
+
91
+ expect(data).toEqual({ id: 123, name: 'Alice' });
92
+ });
93
+
94
+ it('should match multiple path parameters', async () => {
95
+ const mockFetch = createMockFetch();
96
+ mockFetch.get('/users/[userId]/posts/[postId]', { userId: 5, postId: 10 });
97
+
98
+ const response = await mockFetch('/users/5/posts/10', { method: 'GET' });
99
+ const data = await response.json();
100
+
101
+ expect(data).toEqual({ userId: 5, postId: 10 });
102
+ });
103
+
104
+ it('should throw error for unmocked routes', async () => {
105
+ const mockFetch = createMockFetch();
106
+
107
+ await expect(mockFetch('/users/123', { method: 'GET' })).rejects.toThrow(
108
+ 'No mock response configured for GET /users/123',
109
+ );
110
+ });
111
+
112
+ it('should track all calls', async () => {
113
+ const mockFetch = createMockFetch();
114
+ mockFetch.get('/users/123', { id: 123 });
115
+ mockFetch.post('/users', { id: 456 });
116
+
117
+ await mockFetch('/users/123', { method: 'GET' });
118
+ await mockFetch('/users', { method: 'POST', body: '{}' });
119
+
120
+ expect(mockFetch.calls).toHaveLength(2);
121
+ expect(mockFetch.calls[0].url).toBe('/users/123');
122
+ expect(mockFetch.calls[1].url).toBe('/users');
123
+ });
124
+
125
+ it('should reset routes and calls', async () => {
126
+ const mockFetch = createMockFetch();
127
+ mockFetch.get('/users/123', { id: 123 });
128
+
129
+ await mockFetch('/users/123', { method: 'GET' });
130
+ expect(mockFetch.calls).toHaveLength(1);
131
+
132
+ mockFetch.reset();
133
+
134
+ expect(mockFetch.calls).toHaveLength(0);
135
+ await expect(mockFetch('/users/123', { method: 'GET' })).rejects.toThrow('No mock response configured');
136
+ });
137
+
138
+ it('should default to GET method', async () => {
139
+ const mockFetch = createMockFetch();
140
+ mockFetch.get('/users/123', { id: 123 });
141
+
142
+ const response = await mockFetch('/users/123');
143
+ const data = await response.json();
144
+
145
+ expect(data).toEqual({ id: 123 });
146
+ });
147
+
148
+ it('should handle query parameters in URLs', async () => {
149
+ const mockFetch = createMockFetch();
150
+ mockFetch.get('/users', { users: [] });
151
+
152
+ const response = await mockFetch('/users?page=1&limit=10', { method: 'GET' });
153
+ const data = await response.json();
154
+
155
+ expect(data).toEqual({ users: [] });
156
+ });
157
+
158
+ it('should only use each route once by default', async () => {
159
+ const mockFetch = createMockFetch();
160
+ mockFetch.get('/users/123', { id: 123, name: 'Alice' });
161
+
162
+ // First call should succeed
163
+ await mockFetch('/users/123', { method: 'GET' });
164
+
165
+ // Second call should fail because route was already used
166
+ await expect(mockFetch('/users/123', { method: 'GET' })).rejects.toThrow('No mock response configured');
167
+ });
168
+
169
+ it('should allow multiple setups for repeated calls', async () => {
170
+ const mockFetch = createMockFetch();
171
+ mockFetch.get('/users/123', { id: 123, name: 'Alice' });
172
+ mockFetch.get('/users/123', { id: 123, name: 'Updated Alice' });
173
+
174
+ const response1 = await mockFetch('/users/123', { method: 'GET' });
175
+ const data1 = await response1.json();
176
+ expect(data1).toEqual({ id: 123, name: 'Alice' });
177
+
178
+ const response2 = await mockFetch('/users/123', { method: 'GET' });
179
+ const data2 = await response2.json();
180
+ expect(data2).toEqual({ id: 123, name: 'Updated Alice' });
181
+ });
182
+ });
@@ -0,0 +1,421 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { SyncQueryStore, MemoryPersistentStore, valueKeyFor, refIdsKeyFor, refCountKeyFor } from '../QueryStore.js';
3
+ import { QueryClient } from '../QueryClient.js';
4
+ import { entity, t } from '../typeDefs.js';
5
+ import { parseEntities } from '../parseEntities.js';
6
+ import { hashValue } from 'signalium/utils';
7
+
8
+ // Helper to get documents from kv store for testing
9
+ async function getDocument(kv: any, key: number): Promise<unknown | undefined> {
10
+ const value = kv.getString(valueKeyFor(key));
11
+ return value ? JSON.parse(value) : undefined;
12
+ }
13
+
14
+ describe('Parse Entities', () => {
15
+ let client: QueryClient;
16
+ let kv: any;
17
+ let store: any;
18
+
19
+ beforeEach(() => {
20
+ kv = new MemoryPersistentStore();
21
+ const queryStore = new SyncQueryStore(kv);
22
+ client = new QueryClient(queryStore, { fetch: fetch });
23
+ store = queryStore;
24
+ });
25
+
26
+ describe('nested entities', () => {
27
+ it('should track refs for deeply nested entities (A->B->C)', async () => {
28
+ const EntityC = entity(() => ({
29
+ __typename: t.typename('EntityC'),
30
+ id: t.id,
31
+ name: t.string,
32
+ }));
33
+
34
+ const EntityB = entity(() => ({
35
+ __typename: t.typename('EntityB'),
36
+ id: t.id,
37
+ name: t.string,
38
+ c: EntityC,
39
+ }));
40
+
41
+ const EntityA = entity(() => ({
42
+ __typename: t.typename('EntityA'),
43
+ id: t.id,
44
+ name: t.string,
45
+ b: EntityB,
46
+ }));
47
+
48
+ const QueryResult = t.object({
49
+ data: EntityA,
50
+ });
51
+
52
+ const result = {
53
+ data: {
54
+ __typename: 'EntityA',
55
+ id: 1,
56
+ name: 'A',
57
+ b: {
58
+ __typename: 'EntityB',
59
+ id: 2,
60
+ name: 'B',
61
+ c: {
62
+ __typename: 'EntityC',
63
+ id: 3,
64
+ name: 'C',
65
+ },
66
+ },
67
+ },
68
+ };
69
+
70
+ const entityRefs = new Set<number>();
71
+ await parseEntities(result, QueryResult, client, entityRefs);
72
+
73
+ // Top-level object is not an entity, so it pushes EntityA's key up
74
+ expect(entityRefs.size).toBe(1);
75
+
76
+ // Get the keys for each entity
77
+ const keyA = hashValue('EntityA:1');
78
+ const keyB = hashValue('EntityB:2');
79
+ const keyC = hashValue('EntityC:3');
80
+
81
+ expect(entityRefs.has(keyA)).toBe(true);
82
+
83
+ // EntityA should reference only EntityB (immediate child)
84
+ const refsA = await kv.getBuffer(refIdsKeyFor(keyA));
85
+ expect(refsA).toBeDefined();
86
+ const refsAArray = Array.from(refsA!);
87
+ expect(refsAArray).toContain(keyB);
88
+ expect(refsAArray).not.toContain(keyC); // Not transitive
89
+ expect(refsAArray.length).toBe(1);
90
+
91
+ // EntityB should reference only EntityC (immediate child)
92
+ const refsB = await kv.getBuffer(refIdsKeyFor(keyB));
93
+ expect(refsB).toBeDefined();
94
+ const refsBArray = Array.from(refsB!);
95
+ expect(refsBArray).toContain(keyC);
96
+ expect(refsBArray.length).toBe(1);
97
+
98
+ // EntityC should have no refs (leaf node)
99
+ const refsC = await kv.getBuffer(refIdsKeyFor(keyC));
100
+ expect(refsC).toBeUndefined();
101
+ });
102
+
103
+ it('should track refs for sibling entities (A->[B,C])', async () => {
104
+ const EntityB = entity(() => ({
105
+ __typename: t.typename('EntityB'),
106
+ id: t.id,
107
+ name: t.string,
108
+ }));
109
+
110
+ const EntityC = entity(() => ({
111
+ __typename: t.typename('EntityC'),
112
+ id: t.id,
113
+ name: t.string,
114
+ }));
115
+
116
+ const EntityA = entity(() => ({
117
+ __typename: t.typename('EntityA'),
118
+ id: t.id,
119
+ name: t.string,
120
+ b: EntityB,
121
+ c: EntityC,
122
+ }));
123
+
124
+ const QueryResult = t.object({
125
+ data: EntityA,
126
+ });
127
+
128
+ const result = {
129
+ data: {
130
+ __typename: 'EntityA',
131
+ id: 1,
132
+ name: 'A',
133
+ b: {
134
+ __typename: 'EntityB',
135
+ id: 2,
136
+ name: 'B',
137
+ },
138
+ c: {
139
+ __typename: 'EntityC',
140
+ id: 3,
141
+ name: 'C',
142
+ },
143
+ },
144
+ };
145
+
146
+ const entityRefs = new Set<number>();
147
+ await parseEntities(result, QueryResult, client, entityRefs);
148
+
149
+ // Top-level object is not an entity, so it pushes EntityA's key up
150
+ expect(entityRefs.size).toBe(1);
151
+
152
+ const keyA = hashValue('EntityA:1');
153
+ const keyB = hashValue('EntityB:2');
154
+ const keyC = hashValue('EntityC:3');
155
+
156
+ expect(entityRefs.has(keyA)).toBe(true);
157
+
158
+ // EntityA should reference both B and C (immediate children)
159
+ const refsA = await kv.getBuffer(refIdsKeyFor(keyA));
160
+ expect(refsA).toBeDefined();
161
+ const refsAArray = Array.from(refsA!);
162
+ expect(refsAArray).toContain(keyB);
163
+ expect(refsAArray).toContain(keyC);
164
+ expect(refsAArray.length).toBe(2);
165
+ });
166
+ });
167
+
168
+ describe('entities in collections', () => {
169
+ it('should track refs for entities in arrays', async () => {
170
+ const EntityItem = entity(() => ({
171
+ __typename: t.typename('EntityItem'),
172
+ id: t.id,
173
+ name: t.string,
174
+ }));
175
+
176
+ const QueryResult = t.object({
177
+ items: t.array(EntityItem),
178
+ });
179
+
180
+ const result = {
181
+ items: [
182
+ { __typename: 'EntityItem', id: 1, name: 'Item1' },
183
+ { __typename: 'EntityItem', id: 2, name: 'Item2' },
184
+ { __typename: 'EntityItem', id: 3, name: 'Item3' },
185
+ ],
186
+ };
187
+
188
+ const entityRefs = new Set<number>();
189
+ await parseEntities(result, QueryResult, client, entityRefs);
190
+
191
+ // Top-level object is not an entity, and arrays push their entity children up
192
+ // So entityRefs should have the three entity keys
193
+ expect(entityRefs.size).toBe(3);
194
+
195
+ const key1 = hashValue('EntityItem:1');
196
+ const key2 = hashValue('EntityItem:2');
197
+ const key3 = hashValue('EntityItem:3');
198
+
199
+ expect(entityRefs.has(key1)).toBe(true);
200
+ expect(entityRefs.has(key2)).toBe(true);
201
+ expect(entityRefs.has(key3)).toBe(true);
202
+
203
+ // Each entity should be stored
204
+ expect(await getDocument(kv, key1)).toBeDefined();
205
+ expect(await getDocument(kv, key2)).toBeDefined();
206
+ expect(await getDocument(kv, key3)).toBeDefined();
207
+
208
+ // None of the leaf entities should have refs
209
+ expect(await kv.getBuffer(refIdsKeyFor(key1))).toBeUndefined();
210
+ expect(await kv.getBuffer(refIdsKeyFor(key2))).toBeUndefined();
211
+ expect(await kv.getBuffer(refIdsKeyFor(key3))).toBeUndefined();
212
+ });
213
+
214
+ it('should track refs for entities in records', async () => {
215
+ const EntityValue = entity(() => ({
216
+ __typename: t.typename('EntityValue'),
217
+ id: t.id,
218
+ value: t.string,
219
+ }));
220
+
221
+ const QueryResult = t.object({
222
+ map: t.record(EntityValue),
223
+ });
224
+
225
+ const result = {
226
+ map: {
227
+ a: { __typename: 'EntityValue', id: 1, value: 'A' },
228
+ b: { __typename: 'EntityValue', id: 2, value: 'B' },
229
+ c: { __typename: 'EntityValue', id: 3, value: 'C' },
230
+ },
231
+ };
232
+
233
+ const entityRefs = new Set<number>();
234
+ await parseEntities(result, QueryResult, client, entityRefs);
235
+
236
+ // Top-level object is not an entity, records push their entity children up
237
+ expect(entityRefs.size).toBe(3);
238
+
239
+ const key1 = hashValue('EntityValue:1');
240
+ const key2 = hashValue('EntityValue:2');
241
+ const key3 = hashValue('EntityValue:3');
242
+
243
+ expect(entityRefs.has(key1)).toBe(true);
244
+ expect(entityRefs.has(key2)).toBe(true);
245
+ expect(entityRefs.has(key3)).toBe(true);
246
+
247
+ // None of the leaf entities should have refs
248
+ expect(await kv.getBuffer(refIdsKeyFor(key1))).toBeUndefined();
249
+ expect(await kv.getBuffer(refIdsKeyFor(key2))).toBeUndefined();
250
+ expect(await kv.getBuffer(refIdsKeyFor(key3))).toBeUndefined();
251
+ });
252
+
253
+ it('should handle nested entities in arrays', async () => {
254
+ const EntityChild = entity(() => ({
255
+ __typename: t.typename('EntityChild'),
256
+ id: t.id,
257
+ name: t.string,
258
+ }));
259
+
260
+ const EntityParent = entity(() => ({
261
+ __typename: t.typename('EntityParent'),
262
+ id: t.id,
263
+ name: t.string,
264
+ child: EntityChild,
265
+ }));
266
+
267
+ const QueryResult = t.object({
268
+ items: t.array(EntityParent),
269
+ });
270
+
271
+ const result = {
272
+ items: [
273
+ {
274
+ __typename: 'EntityParent',
275
+ id: 1,
276
+ name: 'Parent1',
277
+ child: { __typename: 'EntityChild', id: 10, name: 'Child10' },
278
+ },
279
+ {
280
+ __typename: 'EntityParent',
281
+ id: 2,
282
+ name: 'Parent2',
283
+ child: { __typename: 'EntityChild', id: 20, name: 'Child20' },
284
+ },
285
+ ],
286
+ };
287
+
288
+ const entityRefs = new Set<number>();
289
+ await parseEntities(result, QueryResult, client, entityRefs);
290
+
291
+ // Should have parent keys
292
+ expect(entityRefs.size).toBe(2);
293
+
294
+ const keyP1 = hashValue('EntityParent:1');
295
+ const keyP2 = hashValue('EntityParent:2');
296
+ const keyC10 = hashValue('EntityChild:10');
297
+ const keyC20 = hashValue('EntityChild:20');
298
+
299
+ expect(entityRefs.has(keyP1)).toBe(true);
300
+ expect(entityRefs.has(keyP2)).toBe(true);
301
+
302
+ // Parent1 should reference Child10
303
+ const refsP1 = await kv.getBuffer(refIdsKeyFor(keyP1));
304
+ expect(refsP1).toBeDefined();
305
+ expect(Array.from(refsP1!)).toContain(keyC10);
306
+
307
+ // Parent2 should reference Child20
308
+ const refsP2 = await kv.getBuffer(refIdsKeyFor(keyP2));
309
+ expect(refsP2).toBeDefined();
310
+ expect(Array.from(refsP2!)).toContain(keyC20);
311
+ });
312
+ });
313
+
314
+ describe('shared entities', () => {
315
+ it('should handle same entity referenced multiple times', async () => {
316
+ const EntityShared = entity(() => ({
317
+ __typename: t.typename('EntityShared'),
318
+ id: t.id,
319
+ name: t.string,
320
+ }));
321
+
322
+ const EntityContainer = entity(() => ({
323
+ __typename: t.typename('EntityContainer'),
324
+ id: t.id,
325
+ first: EntityShared,
326
+ second: EntityShared,
327
+ }));
328
+
329
+ const QueryResult = t.object({
330
+ data: EntityContainer,
331
+ });
332
+
333
+ const sharedEntity = {
334
+ __typename: 'EntityShared',
335
+ id: 99,
336
+ name: 'Shared',
337
+ };
338
+
339
+ const result = {
340
+ data: {
341
+ __typename: 'EntityContainer',
342
+ id: 1,
343
+ first: sharedEntity,
344
+ second: sharedEntity,
345
+ },
346
+ };
347
+
348
+ const entityRefs = new Set<number>();
349
+ await parseEntities(result, QueryResult, client, entityRefs);
350
+
351
+ // Top-level object pushes Container's key
352
+ expect(entityRefs.size).toBe(1);
353
+
354
+ const keyContainer = hashValue('EntityContainer:1');
355
+ const keyShared = hashValue('EntityShared:99');
356
+
357
+ expect(entityRefs.has(keyContainer)).toBe(true);
358
+
359
+ // Container should reference the shared entity
360
+ const refsContainer = await kv.getBuffer(refIdsKeyFor(keyContainer));
361
+ expect(refsContainer).toBeDefined();
362
+ const refsArray = Array.from(refsContainer!).filter(k => k !== 0);
363
+
364
+ // The refs array may contain duplicates (first, second both point to same entity)
365
+ // But that's ok - the important part is the ref count is correct
366
+ expect(refsArray).toContain(keyShared);
367
+
368
+ // Ref count should be 1 (documentStore deduplicates when processing refs)
369
+ const refCount = await kv.getNumber(refCountKeyFor(keyShared));
370
+ expect(refCount).toBe(1);
371
+ });
372
+ });
373
+
374
+ describe('complex structures', () => {
375
+ it('should handle entity with array of nested entities', async () => {
376
+ const EntityTag = entity(() => ({
377
+ __typename: t.typename('EntityTag'),
378
+ id: t.id,
379
+ label: t.string,
380
+ }));
381
+
382
+ const EntityPost = entity(() => ({
383
+ __typename: t.typename('EntityPost'),
384
+ id: t.id,
385
+ title: t.string,
386
+ tags: t.array(EntityTag),
387
+ }));
388
+
389
+ const QueryResult = t.object({
390
+ post: EntityPost,
391
+ });
392
+
393
+ const result = {
394
+ post: {
395
+ __typename: 'EntityPost',
396
+ id: 1,
397
+ title: 'My Post',
398
+ tags: [
399
+ { __typename: 'EntityTag', id: 10, label: 'tech' },
400
+ { __typename: 'EntityTag', id: 20, label: 'coding' },
401
+ ],
402
+ },
403
+ };
404
+
405
+ const entityRefs = new Set<number>();
406
+ await parseEntities(result, QueryResult, client, entityRefs);
407
+
408
+ const keyPost = hashValue('EntityPost:1');
409
+ const keyTag10 = hashValue('EntityTag:10');
410
+ const keyTag20 = hashValue('EntityTag:20');
411
+
412
+ // Post should reference both tags
413
+ const refsPost = await kv.getBuffer(refIdsKeyFor(keyPost));
414
+ expect(refsPost).toBeDefined();
415
+ const refsArray = Array.from(refsPost!);
416
+ expect(refsArray).toContain(keyTag10);
417
+ expect(refsArray).toContain(keyTag20);
418
+ expect(refsArray.length).toBe(2); // We should have 2 unique refs
419
+ });
420
+ });
421
+ });