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