@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.
- package/CHANGELOG.md +21 -0
- package/dist/cjs/EntityMap.js +2 -2
- package/dist/cjs/EntityMap.js.map +1 -1
- package/dist/cjs/NetworkManager.js +105 -0
- package/dist/cjs/NetworkManager.js.map +1 -0
- package/dist/cjs/QueryClient.js +390 -76
- package/dist/cjs/QueryClient.js.map +1 -1
- package/dist/cjs/QueryStore.js +295 -3
- package/dist/cjs/QueryStore.js.map +1 -1
- package/dist/cjs/index.js +18 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/parseEntities.js +3 -0
- package/dist/cjs/parseEntities.js.map +1 -1
- package/dist/cjs/proxy.js +19 -0
- package/dist/cjs/proxy.js.map +1 -1
- package/dist/cjs/query.js +40 -2
- package/dist/cjs/query.js.map +1 -1
- package/dist/cjs/stores/async.js +6 -0
- package/dist/cjs/stores/async.js.map +1 -0
- package/dist/cjs/stores/sync.js +7 -0
- package/dist/cjs/stores/sync.js.map +1 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/cjs/type-utils.js +3 -0
- package/dist/cjs/type-utils.js.map +1 -0
- package/dist/cjs/types.js +19 -1
- package/dist/cjs/types.js.map +1 -1
- package/dist/esm/EntityMap.js +3 -3
- package/dist/esm/EntityMap.js.map +1 -1
- package/dist/esm/NetworkManager.d.ts +48 -0
- package/dist/esm/NetworkManager.d.ts.map +1 -0
- package/dist/esm/NetworkManager.js +101 -0
- package/dist/esm/NetworkManager.js.map +1 -0
- package/dist/esm/QueryClient.d.ts +81 -25
- package/dist/esm/QueryClient.d.ts.map +1 -1
- package/dist/esm/QueryClient.js +390 -76
- package/dist/esm/QueryClient.js.map +1 -1
- package/dist/esm/QueryStore.d.ts +64 -2
- package/dist/esm/QueryStore.d.ts.map +1 -1
- package/dist/esm/QueryStore.js +293 -2
- package/dist/esm/QueryStore.js.map +1 -1
- package/dist/esm/index.d.ts +6 -3
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +4 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/parseEntities.d.ts.map +1 -1
- package/dist/esm/parseEntities.js +3 -0
- package/dist/esm/parseEntities.js.map +1 -1
- package/dist/esm/proxy.d.ts +6 -0
- package/dist/esm/proxy.d.ts.map +1 -1
- package/dist/esm/proxy.js +18 -0
- package/dist/esm/proxy.js.map +1 -1
- package/dist/esm/query.d.ts +30 -29
- package/dist/esm/query.d.ts.map +1 -1
- package/dist/esm/query.js +39 -3
- package/dist/esm/query.js.map +1 -1
- package/dist/esm/stores/async.d.ts +2 -0
- package/dist/esm/stores/async.d.ts.map +1 -0
- package/dist/esm/stores/async.js +2 -0
- package/dist/esm/stores/async.js.map +1 -0
- package/dist/esm/stores/sync.d.ts +2 -0
- package/dist/esm/stores/sync.d.ts.map +1 -0
- package/dist/esm/stores/sync.js +2 -0
- package/dist/esm/stores/sync.js.map +1 -0
- package/dist/esm/type-utils.d.ts +12 -0
- package/dist/esm/type-utils.d.ts.map +1 -0
- package/dist/esm/type-utils.js +2 -0
- package/dist/esm/type-utils.js.map +1 -0
- package/dist/esm/types.d.ts +62 -5
- package/dist/esm/types.d.ts.map +1 -1
- package/dist/esm/types.js +18 -0
- package/dist/esm/types.js.map +1 -1
- package/index.d.ts +1 -0
- package/package.json +25 -4
- package/stores/async.d.ts +1 -0
- package/stores/async.js +15 -0
- package/stores/sync.d.ts +1 -0
- package/stores/sync.js +15 -0
- package/.turbo/turbo-build.log +0 -12
- package/ENTITY_STORE_DESIGN.md +0 -386
- package/dist/tsconfig.esm.tsbuildinfo +0 -1
- package/src/EntityMap.ts +0 -63
- package/src/QueryClient.ts +0 -482
- package/src/QueryStore.ts +0 -322
- package/src/__tests__/caching-persistence.test.ts +0 -983
- package/src/__tests__/entity-system.test.ts +0 -556
- package/src/__tests__/gc-time.test.ts +0 -327
- package/src/__tests__/mock-fetch.test.ts +0 -186
- package/src/__tests__/parse-entities.test.ts +0 -425
- package/src/__tests__/path-interpolation.test.ts +0 -225
- package/src/__tests__/reactivity.test.ts +0 -424
- package/src/__tests__/refetch-interval.test.ts +0 -262
- package/src/__tests__/rest-query-api.test.ts +0 -568
- package/src/__tests__/stale-time.test.ts +0 -357
- package/src/__tests__/type-to-string.test.ts +0 -129
- package/src/__tests__/utils.ts +0 -258
- package/src/__tests__/validation-edge-cases.test.ts +0 -821
- package/src/errors.ts +0 -124
- package/src/index.ts +0 -7
- package/src/parseEntities.ts +0 -213
- package/src/pathInterpolator.ts +0 -74
- package/src/proxy.ts +0 -257
- package/src/query.ts +0 -164
- package/src/react/__tests__/basic.test.tsx +0 -926
- package/src/react/__tests__/component.test.tsx +0 -984
- package/src/react/__tests__/utils.tsx +0 -71
- package/src/typeDefs.ts +0 -351
- package/src/types.ts +0 -132
- package/src/utils.ts +0 -66
- package/tsconfig.cjs.json +0 -14
- package/tsconfig.esm.json +0 -13
- package/tsconfig.json +0 -20
- package/vitest.config.ts +0 -65
|
@@ -1,425 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } 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
|
-
afterEach(() => {
|
|
27
|
-
client?.destroy();
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe('nested entities', () => {
|
|
31
|
-
it('should track refs for deeply nested entities (A->B->C)', async () => {
|
|
32
|
-
const EntityC = entity(() => ({
|
|
33
|
-
__typename: t.typename('EntityC'),
|
|
34
|
-
id: t.id,
|
|
35
|
-
name: t.string,
|
|
36
|
-
}));
|
|
37
|
-
|
|
38
|
-
const EntityB = entity(() => ({
|
|
39
|
-
__typename: t.typename('EntityB'),
|
|
40
|
-
id: t.id,
|
|
41
|
-
name: t.string,
|
|
42
|
-
c: EntityC,
|
|
43
|
-
}));
|
|
44
|
-
|
|
45
|
-
const EntityA = entity(() => ({
|
|
46
|
-
__typename: t.typename('EntityA'),
|
|
47
|
-
id: t.id,
|
|
48
|
-
name: t.string,
|
|
49
|
-
b: EntityB,
|
|
50
|
-
}));
|
|
51
|
-
|
|
52
|
-
const QueryResult = t.object({
|
|
53
|
-
data: EntityA,
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const result = {
|
|
57
|
-
data: {
|
|
58
|
-
__typename: 'EntityA',
|
|
59
|
-
id: 1,
|
|
60
|
-
name: 'A',
|
|
61
|
-
b: {
|
|
62
|
-
__typename: 'EntityB',
|
|
63
|
-
id: 2,
|
|
64
|
-
name: 'B',
|
|
65
|
-
c: {
|
|
66
|
-
__typename: 'EntityC',
|
|
67
|
-
id: 3,
|
|
68
|
-
name: 'C',
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const entityRefs = new Set<number>();
|
|
75
|
-
await parseEntities(result, QueryResult, client, entityRefs);
|
|
76
|
-
|
|
77
|
-
// Top-level object is not an entity, so it pushes EntityA's key up
|
|
78
|
-
expect(entityRefs.size).toBe(1);
|
|
79
|
-
|
|
80
|
-
// Get the keys for each entity
|
|
81
|
-
const keyA = hashValue('EntityA:1');
|
|
82
|
-
const keyB = hashValue('EntityB:2');
|
|
83
|
-
const keyC = hashValue('EntityC:3');
|
|
84
|
-
|
|
85
|
-
expect(entityRefs.has(keyA)).toBe(true);
|
|
86
|
-
|
|
87
|
-
// EntityA should reference only EntityB (immediate child)
|
|
88
|
-
const refsA = await kv.getBuffer(refIdsKeyFor(keyA));
|
|
89
|
-
expect(refsA).toBeDefined();
|
|
90
|
-
const refsAArray = Array.from(refsA!);
|
|
91
|
-
expect(refsAArray).toContain(keyB);
|
|
92
|
-
expect(refsAArray).not.toContain(keyC); // Not transitive
|
|
93
|
-
expect(refsAArray.length).toBe(1);
|
|
94
|
-
|
|
95
|
-
// EntityB should reference only EntityC (immediate child)
|
|
96
|
-
const refsB = await kv.getBuffer(refIdsKeyFor(keyB));
|
|
97
|
-
expect(refsB).toBeDefined();
|
|
98
|
-
const refsBArray = Array.from(refsB!);
|
|
99
|
-
expect(refsBArray).toContain(keyC);
|
|
100
|
-
expect(refsBArray.length).toBe(1);
|
|
101
|
-
|
|
102
|
-
// EntityC should have no refs (leaf node)
|
|
103
|
-
const refsC = await kv.getBuffer(refIdsKeyFor(keyC));
|
|
104
|
-
expect(refsC).toBeUndefined();
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('should track refs for sibling entities (A->[B,C])', async () => {
|
|
108
|
-
const EntityB = entity(() => ({
|
|
109
|
-
__typename: t.typename('EntityB'),
|
|
110
|
-
id: t.id,
|
|
111
|
-
name: t.string,
|
|
112
|
-
}));
|
|
113
|
-
|
|
114
|
-
const EntityC = entity(() => ({
|
|
115
|
-
__typename: t.typename('EntityC'),
|
|
116
|
-
id: t.id,
|
|
117
|
-
name: t.string,
|
|
118
|
-
}));
|
|
119
|
-
|
|
120
|
-
const EntityA = entity(() => ({
|
|
121
|
-
__typename: t.typename('EntityA'),
|
|
122
|
-
id: t.id,
|
|
123
|
-
name: t.string,
|
|
124
|
-
b: EntityB,
|
|
125
|
-
c: EntityC,
|
|
126
|
-
}));
|
|
127
|
-
|
|
128
|
-
const QueryResult = t.object({
|
|
129
|
-
data: EntityA,
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
const result = {
|
|
133
|
-
data: {
|
|
134
|
-
__typename: 'EntityA',
|
|
135
|
-
id: 1,
|
|
136
|
-
name: 'A',
|
|
137
|
-
b: {
|
|
138
|
-
__typename: 'EntityB',
|
|
139
|
-
id: 2,
|
|
140
|
-
name: 'B',
|
|
141
|
-
},
|
|
142
|
-
c: {
|
|
143
|
-
__typename: 'EntityC',
|
|
144
|
-
id: 3,
|
|
145
|
-
name: 'C',
|
|
146
|
-
},
|
|
147
|
-
},
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
const entityRefs = new Set<number>();
|
|
151
|
-
await parseEntities(result, QueryResult, client, entityRefs);
|
|
152
|
-
|
|
153
|
-
// Top-level object is not an entity, so it pushes EntityA's key up
|
|
154
|
-
expect(entityRefs.size).toBe(1);
|
|
155
|
-
|
|
156
|
-
const keyA = hashValue('EntityA:1');
|
|
157
|
-
const keyB = hashValue('EntityB:2');
|
|
158
|
-
const keyC = hashValue('EntityC:3');
|
|
159
|
-
|
|
160
|
-
expect(entityRefs.has(keyA)).toBe(true);
|
|
161
|
-
|
|
162
|
-
// EntityA should reference both B and C (immediate children)
|
|
163
|
-
const refsA = await kv.getBuffer(refIdsKeyFor(keyA));
|
|
164
|
-
expect(refsA).toBeDefined();
|
|
165
|
-
const refsAArray = Array.from(refsA!);
|
|
166
|
-
expect(refsAArray).toContain(keyB);
|
|
167
|
-
expect(refsAArray).toContain(keyC);
|
|
168
|
-
expect(refsAArray.length).toBe(2);
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
describe('entities in collections', () => {
|
|
173
|
-
it('should track refs for entities in arrays', async () => {
|
|
174
|
-
const EntityItem = entity(() => ({
|
|
175
|
-
__typename: t.typename('EntityItem'),
|
|
176
|
-
id: t.id,
|
|
177
|
-
name: t.string,
|
|
178
|
-
}));
|
|
179
|
-
|
|
180
|
-
const QueryResult = t.object({
|
|
181
|
-
items: t.array(EntityItem),
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
const result = {
|
|
185
|
-
items: [
|
|
186
|
-
{ __typename: 'EntityItem', id: 1, name: 'Item1' },
|
|
187
|
-
{ __typename: 'EntityItem', id: 2, name: 'Item2' },
|
|
188
|
-
{ __typename: 'EntityItem', id: 3, name: 'Item3' },
|
|
189
|
-
],
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
const entityRefs = new Set<number>();
|
|
193
|
-
await parseEntities(result, QueryResult, client, entityRefs);
|
|
194
|
-
|
|
195
|
-
// Top-level object is not an entity, and arrays push their entity children up
|
|
196
|
-
// So entityRefs should have the three entity keys
|
|
197
|
-
expect(entityRefs.size).toBe(3);
|
|
198
|
-
|
|
199
|
-
const key1 = hashValue('EntityItem:1');
|
|
200
|
-
const key2 = hashValue('EntityItem:2');
|
|
201
|
-
const key3 = hashValue('EntityItem:3');
|
|
202
|
-
|
|
203
|
-
expect(entityRefs.has(key1)).toBe(true);
|
|
204
|
-
expect(entityRefs.has(key2)).toBe(true);
|
|
205
|
-
expect(entityRefs.has(key3)).toBe(true);
|
|
206
|
-
|
|
207
|
-
// Each entity should be stored
|
|
208
|
-
expect(await getDocument(kv, key1)).toBeDefined();
|
|
209
|
-
expect(await getDocument(kv, key2)).toBeDefined();
|
|
210
|
-
expect(await getDocument(kv, key3)).toBeDefined();
|
|
211
|
-
|
|
212
|
-
// None of the leaf entities should have refs
|
|
213
|
-
expect(await kv.getBuffer(refIdsKeyFor(key1))).toBeUndefined();
|
|
214
|
-
expect(await kv.getBuffer(refIdsKeyFor(key2))).toBeUndefined();
|
|
215
|
-
expect(await kv.getBuffer(refIdsKeyFor(key3))).toBeUndefined();
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it('should track refs for entities in records', async () => {
|
|
219
|
-
const EntityValue = entity(() => ({
|
|
220
|
-
__typename: t.typename('EntityValue'),
|
|
221
|
-
id: t.id,
|
|
222
|
-
value: t.string,
|
|
223
|
-
}));
|
|
224
|
-
|
|
225
|
-
const QueryResult = t.object({
|
|
226
|
-
map: t.record(EntityValue),
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
const result = {
|
|
230
|
-
map: {
|
|
231
|
-
a: { __typename: 'EntityValue', id: 1, value: 'A' },
|
|
232
|
-
b: { __typename: 'EntityValue', id: 2, value: 'B' },
|
|
233
|
-
c: { __typename: 'EntityValue', id: 3, value: 'C' },
|
|
234
|
-
},
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
const entityRefs = new Set<number>();
|
|
238
|
-
await parseEntities(result, QueryResult, client, entityRefs);
|
|
239
|
-
|
|
240
|
-
// Top-level object is not an entity, records push their entity children up
|
|
241
|
-
expect(entityRefs.size).toBe(3);
|
|
242
|
-
|
|
243
|
-
const key1 = hashValue('EntityValue:1');
|
|
244
|
-
const key2 = hashValue('EntityValue:2');
|
|
245
|
-
const key3 = hashValue('EntityValue:3');
|
|
246
|
-
|
|
247
|
-
expect(entityRefs.has(key1)).toBe(true);
|
|
248
|
-
expect(entityRefs.has(key2)).toBe(true);
|
|
249
|
-
expect(entityRefs.has(key3)).toBe(true);
|
|
250
|
-
|
|
251
|
-
// None of the leaf entities should have refs
|
|
252
|
-
expect(await kv.getBuffer(refIdsKeyFor(key1))).toBeUndefined();
|
|
253
|
-
expect(await kv.getBuffer(refIdsKeyFor(key2))).toBeUndefined();
|
|
254
|
-
expect(await kv.getBuffer(refIdsKeyFor(key3))).toBeUndefined();
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it('should handle nested entities in arrays', async () => {
|
|
258
|
-
const EntityChild = entity(() => ({
|
|
259
|
-
__typename: t.typename('EntityChild'),
|
|
260
|
-
id: t.id,
|
|
261
|
-
name: t.string,
|
|
262
|
-
}));
|
|
263
|
-
|
|
264
|
-
const EntityParent = entity(() => ({
|
|
265
|
-
__typename: t.typename('EntityParent'),
|
|
266
|
-
id: t.id,
|
|
267
|
-
name: t.string,
|
|
268
|
-
child: EntityChild,
|
|
269
|
-
}));
|
|
270
|
-
|
|
271
|
-
const QueryResult = t.object({
|
|
272
|
-
items: t.array(EntityParent),
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
const result = {
|
|
276
|
-
items: [
|
|
277
|
-
{
|
|
278
|
-
__typename: 'EntityParent',
|
|
279
|
-
id: 1,
|
|
280
|
-
name: 'Parent1',
|
|
281
|
-
child: { __typename: 'EntityChild', id: 10, name: 'Child10' },
|
|
282
|
-
},
|
|
283
|
-
{
|
|
284
|
-
__typename: 'EntityParent',
|
|
285
|
-
id: 2,
|
|
286
|
-
name: 'Parent2',
|
|
287
|
-
child: { __typename: 'EntityChild', id: 20, name: 'Child20' },
|
|
288
|
-
},
|
|
289
|
-
],
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
const entityRefs = new Set<number>();
|
|
293
|
-
await parseEntities(result, QueryResult, client, entityRefs);
|
|
294
|
-
|
|
295
|
-
// Should have parent keys
|
|
296
|
-
expect(entityRefs.size).toBe(2);
|
|
297
|
-
|
|
298
|
-
const keyP1 = hashValue('EntityParent:1');
|
|
299
|
-
const keyP2 = hashValue('EntityParent:2');
|
|
300
|
-
const keyC10 = hashValue('EntityChild:10');
|
|
301
|
-
const keyC20 = hashValue('EntityChild:20');
|
|
302
|
-
|
|
303
|
-
expect(entityRefs.has(keyP1)).toBe(true);
|
|
304
|
-
expect(entityRefs.has(keyP2)).toBe(true);
|
|
305
|
-
|
|
306
|
-
// Parent1 should reference Child10
|
|
307
|
-
const refsP1 = await kv.getBuffer(refIdsKeyFor(keyP1));
|
|
308
|
-
expect(refsP1).toBeDefined();
|
|
309
|
-
expect(Array.from(refsP1!)).toContain(keyC10);
|
|
310
|
-
|
|
311
|
-
// Parent2 should reference Child20
|
|
312
|
-
const refsP2 = await kv.getBuffer(refIdsKeyFor(keyP2));
|
|
313
|
-
expect(refsP2).toBeDefined();
|
|
314
|
-
expect(Array.from(refsP2!)).toContain(keyC20);
|
|
315
|
-
});
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
describe('shared entities', () => {
|
|
319
|
-
it('should handle same entity referenced multiple times', async () => {
|
|
320
|
-
const EntityShared = entity(() => ({
|
|
321
|
-
__typename: t.typename('EntityShared'),
|
|
322
|
-
id: t.id,
|
|
323
|
-
name: t.string,
|
|
324
|
-
}));
|
|
325
|
-
|
|
326
|
-
const EntityContainer = entity(() => ({
|
|
327
|
-
__typename: t.typename('EntityContainer'),
|
|
328
|
-
id: t.id,
|
|
329
|
-
first: EntityShared,
|
|
330
|
-
second: EntityShared,
|
|
331
|
-
}));
|
|
332
|
-
|
|
333
|
-
const QueryResult = t.object({
|
|
334
|
-
data: EntityContainer,
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
const sharedEntity = {
|
|
338
|
-
__typename: 'EntityShared',
|
|
339
|
-
id: 99,
|
|
340
|
-
name: 'Shared',
|
|
341
|
-
};
|
|
342
|
-
|
|
343
|
-
const result = {
|
|
344
|
-
data: {
|
|
345
|
-
__typename: 'EntityContainer',
|
|
346
|
-
id: 1,
|
|
347
|
-
first: sharedEntity,
|
|
348
|
-
second: sharedEntity,
|
|
349
|
-
},
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
const entityRefs = new Set<number>();
|
|
353
|
-
await parseEntities(result, QueryResult, client, entityRefs);
|
|
354
|
-
|
|
355
|
-
// Top-level object pushes Container's key
|
|
356
|
-
expect(entityRefs.size).toBe(1);
|
|
357
|
-
|
|
358
|
-
const keyContainer = hashValue('EntityContainer:1');
|
|
359
|
-
const keyShared = hashValue('EntityShared:99');
|
|
360
|
-
|
|
361
|
-
expect(entityRefs.has(keyContainer)).toBe(true);
|
|
362
|
-
|
|
363
|
-
// Container should reference the shared entity
|
|
364
|
-
const refsContainer = await kv.getBuffer(refIdsKeyFor(keyContainer));
|
|
365
|
-
expect(refsContainer).toBeDefined();
|
|
366
|
-
const refsArray = Array.from(refsContainer!).filter(k => k !== 0);
|
|
367
|
-
|
|
368
|
-
// The refs array may contain duplicates (first, second both point to same entity)
|
|
369
|
-
// But that's ok - the important part is the ref count is correct
|
|
370
|
-
expect(refsArray).toContain(keyShared);
|
|
371
|
-
|
|
372
|
-
// Ref count should be 1 (documentStore deduplicates when processing refs)
|
|
373
|
-
const refCount = await kv.getNumber(refCountKeyFor(keyShared));
|
|
374
|
-
expect(refCount).toBe(1);
|
|
375
|
-
});
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
describe('complex structures', () => {
|
|
379
|
-
it('should handle entity with array of nested entities', async () => {
|
|
380
|
-
const EntityTag = entity(() => ({
|
|
381
|
-
__typename: t.typename('EntityTag'),
|
|
382
|
-
id: t.id,
|
|
383
|
-
label: t.string,
|
|
384
|
-
}));
|
|
385
|
-
|
|
386
|
-
const EntityPost = entity(() => ({
|
|
387
|
-
__typename: t.typename('EntityPost'),
|
|
388
|
-
id: t.id,
|
|
389
|
-
title: t.string,
|
|
390
|
-
tags: t.array(EntityTag),
|
|
391
|
-
}));
|
|
392
|
-
|
|
393
|
-
const QueryResult = t.object({
|
|
394
|
-
post: EntityPost,
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
const result = {
|
|
398
|
-
post: {
|
|
399
|
-
__typename: 'EntityPost',
|
|
400
|
-
id: 1,
|
|
401
|
-
title: 'My Post',
|
|
402
|
-
tags: [
|
|
403
|
-
{ __typename: 'EntityTag', id: 10, label: 'tech' },
|
|
404
|
-
{ __typename: 'EntityTag', id: 20, label: 'coding' },
|
|
405
|
-
],
|
|
406
|
-
},
|
|
407
|
-
};
|
|
408
|
-
|
|
409
|
-
const entityRefs = new Set<number>();
|
|
410
|
-
await parseEntities(result, QueryResult, client, entityRefs);
|
|
411
|
-
|
|
412
|
-
const keyPost = hashValue('EntityPost:1');
|
|
413
|
-
const keyTag10 = hashValue('EntityTag:10');
|
|
414
|
-
const keyTag20 = hashValue('EntityTag:20');
|
|
415
|
-
|
|
416
|
-
// Post should reference both tags
|
|
417
|
-
const refsPost = await kv.getBuffer(refIdsKeyFor(keyPost));
|
|
418
|
-
expect(refsPost).toBeDefined();
|
|
419
|
-
const refsArray = Array.from(refsPost!);
|
|
420
|
-
expect(refsArray).toContain(keyTag10);
|
|
421
|
-
expect(refsArray).toContain(keyTag20);
|
|
422
|
-
expect(refsArray.length).toBe(2); // We should have 2 unique refs
|
|
423
|
-
});
|
|
424
|
-
});
|
|
425
|
-
});
|
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { createPathInterpolator } from '../pathInterpolator.js';
|
|
3
|
-
|
|
4
|
-
describe('createPathInterpolator', () => {
|
|
5
|
-
describe('basic path interpolation', () => {
|
|
6
|
-
it('should interpolate a single parameter', () => {
|
|
7
|
-
const interpolate = createPathInterpolator('/users/[userId]');
|
|
8
|
-
const result = interpolate({ userId: '123' });
|
|
9
|
-
expect(result).toBe('/users/123');
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('should interpolate multiple parameters', () => {
|
|
13
|
-
const interpolate = createPathInterpolator('/users/[userId]/posts/[postId]');
|
|
14
|
-
const result = interpolate({ userId: '123', postId: '456' });
|
|
15
|
-
expect(result).toBe('/users/123/posts/456');
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('should handle consecutive parameters', () => {
|
|
19
|
-
const interpolate = createPathInterpolator('/items/[category][subcategory]');
|
|
20
|
-
const result = interpolate({ category: 'books', subcategory: 'fiction' });
|
|
21
|
-
expect(result).toBe('/items/booksfiction');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('should handle path with no parameters', () => {
|
|
25
|
-
const interpolate = createPathInterpolator('/static/path');
|
|
26
|
-
const result = interpolate({});
|
|
27
|
-
expect(result).toBe('/static/path');
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('should handle path starting with parameter', () => {
|
|
31
|
-
const interpolate = createPathInterpolator('[tenant]/users/[userId]');
|
|
32
|
-
const result = interpolate({ tenant: 'acme', userId: '123' });
|
|
33
|
-
expect(result).toBe('acme/users/123');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('should handle path ending with parameter', () => {
|
|
37
|
-
const interpolate = createPathInterpolator('/users/[userId]');
|
|
38
|
-
const result = interpolate({ userId: '123' });
|
|
39
|
-
expect(result).toBe('/users/123');
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
describe('URL encoding', () => {
|
|
44
|
-
it('should URL-encode special characters in path parameters', () => {
|
|
45
|
-
const interpolate = createPathInterpolator('/users/[userId]');
|
|
46
|
-
const result = interpolate({ userId: 'user@example.com' });
|
|
47
|
-
expect(result).toBe('/users/user%40example.com');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('should URL-encode spaces', () => {
|
|
51
|
-
const interpolate = createPathInterpolator('/search/[query]');
|
|
52
|
-
const result = interpolate({ query: 'hello world' });
|
|
53
|
-
expect(result).toBe('/search/hello%20world');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('should URL-encode forward slashes', () => {
|
|
57
|
-
const interpolate = createPathInterpolator('/files/[path]');
|
|
58
|
-
const result = interpolate({ path: 'folder/subfolder/file.txt' });
|
|
59
|
-
expect(result).toBe('/files/folder%2Fsubfolder%2Ffile.txt');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('should handle unicode characters', () => {
|
|
63
|
-
const interpolate = createPathInterpolator('/items/[name]');
|
|
64
|
-
const result = interpolate({ name: '日本語' });
|
|
65
|
-
expect(result).toBe('/items/%E6%97%A5%E6%9C%AC%E8%AA%9E');
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
describe('query string parameters', () => {
|
|
70
|
-
it('should append extra parameters as query string', () => {
|
|
71
|
-
const interpolate = createPathInterpolator('/users/[userId]');
|
|
72
|
-
const result = interpolate({ userId: '123', page: 2, limit: 10 });
|
|
73
|
-
expect(result).toBe('/users/123?page=2&limit=10');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('should append all non-path parameters as query string', () => {
|
|
77
|
-
const interpolate = createPathInterpolator('/users/[userId]/posts/[postId]');
|
|
78
|
-
const result = interpolate({
|
|
79
|
-
userId: '123',
|
|
80
|
-
postId: '456',
|
|
81
|
-
page: 2,
|
|
82
|
-
limit: 10,
|
|
83
|
-
sort: 'desc',
|
|
84
|
-
});
|
|
85
|
-
expect(result).toBe('/users/123/posts/456?page=2&limit=10&sort=desc');
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('should handle only query parameters when path has no params', () => {
|
|
89
|
-
const interpolate = createPathInterpolator('/search');
|
|
90
|
-
const result = interpolate({ q: 'test', page: 1 });
|
|
91
|
-
expect(result).toBe('/search?q=test&page=1');
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('should skip undefined query parameters', () => {
|
|
95
|
-
const interpolate = createPathInterpolator('/users/[userId]');
|
|
96
|
-
const result = interpolate({ userId: '123', page: 2, limit: undefined });
|
|
97
|
-
expect(result).toBe('/users/123?page=2');
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('should include null and empty string values in query params', () => {
|
|
101
|
-
const interpolate = createPathInterpolator('/users/[userId]');
|
|
102
|
-
const result = interpolate({ userId: '123', filter: null, name: '' });
|
|
103
|
-
expect(result).toBe('/users/123?filter=null&name=');
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('should handle boolean query parameters', () => {
|
|
107
|
-
const interpolate = createPathInterpolator('/items');
|
|
108
|
-
const result = interpolate({ active: true, deleted: false });
|
|
109
|
-
expect(result).toBe('/items?active=true&deleted=false');
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('should handle numeric query parameters', () => {
|
|
113
|
-
const interpolate = createPathInterpolator('/items');
|
|
114
|
-
const result = interpolate({ id: 0, count: 100 });
|
|
115
|
-
expect(result).toBe('/items?id=0&count=100');
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
describe('type coercion', () => {
|
|
120
|
-
it('should convert numeric path parameters to string', () => {
|
|
121
|
-
const interpolate = createPathInterpolator('/users/[userId]');
|
|
122
|
-
const result = interpolate({ userId: 123 });
|
|
123
|
-
expect(result).toBe('/users/123');
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('should convert boolean path parameters to string', () => {
|
|
127
|
-
const interpolate = createPathInterpolator('/settings/[enabled]');
|
|
128
|
-
const result = interpolate({ enabled: true });
|
|
129
|
-
expect(result).toBe('/settings/true');
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('should convert null path parameters to string', () => {
|
|
133
|
-
const interpolate = createPathInterpolator('/items/[id]');
|
|
134
|
-
const result = interpolate({ id: null });
|
|
135
|
-
expect(result).toBe('/items/null');
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('should handle object conversion to string', () => {
|
|
139
|
-
const interpolate = createPathInterpolator('/items/[id]');
|
|
140
|
-
const result = interpolate({ id: { value: 123 } });
|
|
141
|
-
expect(result).toBe('/items/%5Bobject%20Object%5D');
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
describe('edge cases', () => {
|
|
146
|
-
it('should handle empty path template', () => {
|
|
147
|
-
const interpolate = createPathInterpolator('');
|
|
148
|
-
const result = interpolate({});
|
|
149
|
-
expect(result).toBe('');
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('should handle empty params object', () => {
|
|
153
|
-
const interpolate = createPathInterpolator('/users/[userId]/posts/[postId]');
|
|
154
|
-
const result = interpolate({});
|
|
155
|
-
expect(result).toBe('/users/undefined/posts/undefined');
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('should handle missing path parameter values', () => {
|
|
159
|
-
const interpolate = createPathInterpolator('/users/[userId]/posts/[postId]');
|
|
160
|
-
const result = interpolate({ userId: '123' });
|
|
161
|
-
expect(result).toBe('/users/123/posts/undefined');
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('should handle parameter names with underscores', () => {
|
|
165
|
-
const interpolate = createPathInterpolator('/users/[user_id]');
|
|
166
|
-
const result = interpolate({ user_id: '123' });
|
|
167
|
-
expect(result).toBe('/users/123');
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('should handle parameter names with hyphens', () => {
|
|
171
|
-
const interpolate = createPathInterpolator('/users/[user-id]');
|
|
172
|
-
const result = interpolate({ 'user-id': '123' });
|
|
173
|
-
expect(result).toBe('/users/123');
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('should handle parameter names with numbers', () => {
|
|
177
|
-
const interpolate = createPathInterpolator('/items/[item1]/[item2]');
|
|
178
|
-
const result = interpolate({ item1: 'first', item2: 'second' });
|
|
179
|
-
expect(result).toBe('/items/first/second');
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('should be reusable for multiple interpolations', () => {
|
|
183
|
-
const interpolate = createPathInterpolator('/users/[userId]');
|
|
184
|
-
|
|
185
|
-
const result1 = interpolate({ userId: '123' });
|
|
186
|
-
const result2 = interpolate({ userId: '456' });
|
|
187
|
-
const result3 = interpolate({ userId: '789', page: 1 });
|
|
188
|
-
|
|
189
|
-
expect(result1).toBe('/users/123');
|
|
190
|
-
expect(result2).toBe('/users/456');
|
|
191
|
-
expect(result3).toBe('/users/789?page=1');
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('should handle complex real-world example', () => {
|
|
195
|
-
const interpolate = createPathInterpolator('/api/v1/tenants/[tenantId]/users/[userId]/documents/[documentId]');
|
|
196
|
-
const result = interpolate({
|
|
197
|
-
tenantId: 'acme-corp',
|
|
198
|
-
userId: 'user@example.com',
|
|
199
|
-
documentId: '12345',
|
|
200
|
-
version: 2,
|
|
201
|
-
format: 'pdf',
|
|
202
|
-
download: true,
|
|
203
|
-
});
|
|
204
|
-
expect(result).toBe(
|
|
205
|
-
'/api/v1/tenants/acme-corp/users/user%40example.com/documents/12345?version=2&format=pdf&download=true',
|
|
206
|
-
);
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
describe('performance characteristics', () => {
|
|
211
|
-
it('should create the interpolator once and reuse it efficiently', () => {
|
|
212
|
-
const interpolate = createPathInterpolator('/users/[userId]/posts/[postId]');
|
|
213
|
-
|
|
214
|
-
// Simulate multiple calls (as would happen in production)
|
|
215
|
-
const results = [];
|
|
216
|
-
for (let i = 0; i < 1000; i++) {
|
|
217
|
-
results.push(interpolate({ userId: `user${i}`, postId: `post${i}` }));
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
expect(results[0]).toBe('/users/user0/posts/post0');
|
|
221
|
-
expect(results[999]).toBe('/users/user999/posts/post999');
|
|
222
|
-
expect(results.length).toBe(1000);
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
});
|