@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,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
|
+
});
|