@object-ui/data-objectstack 17.3.0 → 17.6.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +888 -0
  2. package/README.md +3 -11
  3. package/dist/index.cjs +1511 -1115
  4. package/dist/index.d.cts +1222 -824
  5. package/dist/index.d.ts +1222 -824
  6. package/dist/index.js +1503 -1111
  7. package/package.json +5 -6
  8. package/src/aggregate-capability.test.ts +0 -177
  9. package/src/bulkDelete.test.ts +0 -75
  10. package/src/bulkUpdate.test.ts +0 -93
  11. package/src/cache/MetadataCache.test.ts +0 -477
  12. package/src/cache/MetadataCache.ts +0 -289
  13. package/src/cloud.ts +0 -109
  14. package/src/connection.test.ts +0 -188
  15. package/src/contracts.ts +0 -115
  16. package/src/errors.test.ts +0 -426
  17. package/src/errors.ts +0 -293
  18. package/src/exportDownload.test.ts +0 -141
  19. package/src/filter-entry-translation.test.ts +0 -360
  20. package/src/filter-operator-ast-parity.test.ts +0 -71
  21. package/src/getObjectSchema.test.ts +0 -145
  22. package/src/importJob.test.ts +0 -71
  23. package/src/index.ts +0 -3658
  24. package/src/integration.ts +0 -200
  25. package/src/listViews.test.ts +0 -175
  26. package/src/metadata-client.test.ts +0 -303
  27. package/src/metadata-client.ts +0 -849
  28. package/src/occ.test.ts +0 -101
  29. package/src/onMutation.test.ts +0 -562
  30. package/src/onWriteWarning.test.ts +0 -273
  31. package/src/orderby-serialization.test.ts +0 -115
  32. package/src/queryDataset.test.ts +0 -132
  33. package/src/searchAll.test.ts +0 -120
  34. package/src/security.ts +0 -243
  35. package/src/spec-symbol-batch6.test.ts +0 -306
  36. package/src/studio.ts +0 -152
  37. package/src/userState.test.ts +0 -315
  38. package/src/userState.ts +0 -238
  39. package/src/v3-compat.test.ts +0 -240
  40. package/src/validation-error.test.ts +0 -94
@@ -1,477 +0,0 @@
1
- /**
2
- * ObjectUI
3
- * Copyright (c) 2024-present ObjectStack Inc.
4
- *
5
- * This source code is licensed under the MIT license found in the
6
- * LICENSE file in the root directory of this source tree.
7
- */
8
-
9
- import { describe, it, expect, beforeEach, vi } from 'vitest';
10
- import { MetadataCache } from './MetadataCache';
11
-
12
- describe('MetadataCache', () => {
13
- let cache: MetadataCache;
14
-
15
- beforeEach(() => {
16
- cache = new MetadataCache({ maxSize: 3, ttl: 1000 }); // Small size and TTL for testing
17
- });
18
-
19
- describe('Cache Hit/Miss Scenarios', () => {
20
- it('should return cached value on cache hit', async () => {
21
- const fetcher = vi.fn(async () => ({ name: 'users', fields: [] }));
22
-
23
- // First call - cache miss
24
- const result1 = await cache.get('users', fetcher);
25
- expect(result1).toEqual({ name: 'users', fields: [] });
26
- expect(fetcher).toHaveBeenCalledTimes(1);
27
-
28
- // Second call - cache hit
29
- const result2 = await cache.get('users', fetcher);
30
- expect(result2).toEqual({ name: 'users', fields: [] });
31
- expect(fetcher).toHaveBeenCalledTimes(1); // Not called again
32
-
33
- const stats = cache.getStats();
34
- expect(stats.hits).toBe(1);
35
- expect(stats.misses).toBe(1);
36
- expect(stats.hitRate).toBe(0.5);
37
- });
38
-
39
- it('should call fetcher on cache miss', async () => {
40
- const fetcher = vi.fn(async () => ({ data: 'test' }));
41
-
42
- const result = await cache.get('test-key', fetcher);
43
-
44
- expect(result).toEqual({ data: 'test' });
45
- expect(fetcher).toHaveBeenCalledTimes(1);
46
-
47
- const stats = cache.getStats();
48
- expect(stats.misses).toBe(1);
49
- expect(stats.hits).toBe(0);
50
- });
51
-
52
- it('should handle multiple different keys', async () => {
53
- const fetcher1 = vi.fn(async () => ({ type: 'users' }));
54
- const fetcher2 = vi.fn(async () => ({ type: 'posts' }));
55
-
56
- const result1 = await cache.get('users', fetcher1);
57
- const result2 = await cache.get('posts', fetcher2);
58
-
59
- expect(result1).toEqual({ type: 'users' });
60
- expect(result2).toEqual({ type: 'posts' });
61
- expect(fetcher1).toHaveBeenCalledTimes(1);
62
- expect(fetcher2).toHaveBeenCalledTimes(1);
63
-
64
- // Get again - both should be cached
65
- await cache.get('users', fetcher1);
66
- await cache.get('posts', fetcher2);
67
-
68
- expect(fetcher1).toHaveBeenCalledTimes(1);
69
- expect(fetcher2).toHaveBeenCalledTimes(1);
70
- });
71
- });
72
-
73
- describe('TTL Expiration', () => {
74
- it('should expire entries after TTL', async () => {
75
- const cache = new MetadataCache({ maxSize: 10, ttl: 100 }); // 100ms TTL
76
- const fetcher = vi.fn(async () => ({ data: 'test' }));
77
-
78
- // First fetch
79
- await cache.get('test', fetcher);
80
- expect(fetcher).toHaveBeenCalledTimes(1);
81
-
82
- // Immediate second fetch - should be cached
83
- await cache.get('test', fetcher);
84
- expect(fetcher).toHaveBeenCalledTimes(1);
85
-
86
- // Wait for TTL to expire
87
- await new Promise(resolve => setTimeout(resolve, 150));
88
-
89
- // Should fetch again after expiration
90
- await cache.get('test', fetcher);
91
- expect(fetcher).toHaveBeenCalledTimes(2);
92
- });
93
-
94
- it('should update timestamp on cache hit', async () => {
95
- const cache = new MetadataCache({ maxSize: 10, ttl: 200 });
96
- const fetcher = vi.fn(async () => ({ data: 'test' }));
97
-
98
- await cache.get('test', fetcher);
99
-
100
- // Access again after 100ms
101
- await new Promise(resolve => setTimeout(resolve, 100));
102
- await cache.get('test', fetcher);
103
-
104
- // Access again after another 110ms (total 210ms from first, ensuring TTL has passed)
105
- await new Promise(resolve => setTimeout(resolve, 110));
106
-
107
- // Should still be in cache because we're checking timestamp, not last accessed
108
- // Actually, the implementation uses timestamp for expiration, not lastAccessed
109
- // So after 210ms total (> 200ms TTL), it should expire
110
- await cache.get('test', fetcher);
111
-
112
- // Should have been called twice - initial + after expiration
113
- expect(fetcher).toHaveBeenCalledTimes(2);
114
- });
115
-
116
- it('should not return expired entries via has()', async () => {
117
- const cache = new MetadataCache({ maxSize: 10, ttl: 100 });
118
- const fetcher = vi.fn(async () => ({ data: 'test' }));
119
-
120
- await cache.get('test', fetcher);
121
-
122
- expect(cache.has('test')).toBe(true);
123
-
124
- // Wait for expiration
125
- await new Promise(resolve => setTimeout(resolve, 150));
126
-
127
- expect(cache.has('test')).toBe(false);
128
- });
129
- });
130
-
131
- describe('LRU Eviction', () => {
132
- it('should evict least recently used entry when maxSize is reached', async () => {
133
- // Cache size is 3
134
- const fetcher1 = vi.fn(async () => ({ id: 1 }));
135
- const fetcher2 = vi.fn(async () => ({ id: 2 }));
136
- const fetcher3 = vi.fn(async () => ({ id: 3 }));
137
- const fetcher4 = vi.fn(async () => ({ id: 4 }));
138
-
139
- // Fill cache
140
- await cache.get('key1', fetcher1);
141
- await cache.get('key2', fetcher2);
142
- await cache.get('key3', fetcher3);
143
-
144
- expect(cache.getStats().size).toBe(3);
145
-
146
- // Add fourth item - should evict key1 (least recently used)
147
- await cache.get('key4', fetcher4);
148
-
149
- expect(cache.getStats().size).toBe(3);
150
- expect(cache.getStats().evictions).toBe(1);
151
-
152
- // key1 should not be in cache anymore
153
- await cache.get('key1', fetcher1);
154
- expect(fetcher1).toHaveBeenCalledTimes(2); // Called again
155
-
156
- // After re-adding key1, key2 should have been evicted
157
- // So cache now has: key3, key4, key1
158
-
159
- // key3, key4 should still be cached
160
- await cache.get('key3', fetcher3);
161
- await cache.get('key4', fetcher4);
162
- expect(fetcher3).toHaveBeenCalledTimes(1);
163
- expect(fetcher4).toHaveBeenCalledTimes(1);
164
-
165
- // key2 should have been evicted when key1 was re-added
166
- await cache.get('key2', fetcher2);
167
- expect(fetcher2).toHaveBeenCalledTimes(2);
168
- });
169
-
170
- it('should update LRU order on access', async () => {
171
- const fetcher1 = vi.fn(async () => ({ id: 1 }));
172
- const fetcher2 = vi.fn(async () => ({ id: 2 }));
173
- const fetcher3 = vi.fn(async () => ({ id: 3 }));
174
- const fetcher4 = vi.fn(async () => ({ id: 4 }));
175
-
176
- // Fill cache: key1, key2, key3
177
- await cache.get('key1', fetcher1);
178
- await cache.get('key2', fetcher2);
179
- await cache.get('key3', fetcher3);
180
-
181
- // Access key1 again - should move it to the end (most recently used)
182
- // Cache order: key2, key3, key1
183
- await cache.get('key1', fetcher1);
184
-
185
- // Add key4 - should evict key2 (now the LRU)
186
- // Cache order: key3, key1, key4
187
- await cache.get('key4', fetcher4);
188
-
189
- // Verify key2 was evicted
190
- await cache.get('key2', fetcher2);
191
- expect(fetcher2).toHaveBeenCalledTimes(2);
192
-
193
- // After re-adding key2, key3 should have been evicted
194
- // Cache order: key1, key4, key2
195
-
196
- // key1, key4 should still be cached
197
- await cache.get('key1', fetcher1);
198
- await cache.get('key4', fetcher4);
199
- expect(fetcher1).toHaveBeenCalledTimes(1); // Only called once initially (re-access was a cache hit)
200
- expect(fetcher4).toHaveBeenCalledTimes(1);
201
-
202
- // key3 should have been evicted when key2 was re-added
203
- await cache.get('key3', fetcher3);
204
- expect(fetcher3).toHaveBeenCalledTimes(2);
205
- });
206
- });
207
-
208
- describe('Concurrent Access', () => {
209
- it('should handle concurrent requests for the same key', async () => {
210
- let fetchCount = 0;
211
- const fetcher = vi.fn(async () => {
212
- fetchCount++;
213
- await new Promise(resolve => setTimeout(resolve, 50));
214
- return { data: 'test', fetchCount };
215
- });
216
-
217
- // Make multiple concurrent requests
218
- const results = await Promise.all([
219
- cache.get('test', fetcher),
220
- cache.get('test', fetcher),
221
- cache.get('test', fetcher),
222
- ]);
223
-
224
- // All should return the same data
225
- // Note: Due to async nature, the first call will fetch and others might also fetch
226
- // if they check before the first one completes. This is acceptable behavior.
227
- // But at least one should be cached if they complete after the first one.
228
- expect(results[0]).toBeDefined();
229
- expect(results[1]).toBeDefined();
230
- expect(results[2]).toBeDefined();
231
- });
232
-
233
- it('should handle concurrent requests for different keys', async () => {
234
- const fetcher1 = vi.fn(async () => {
235
- await new Promise(resolve => setTimeout(resolve, 30));
236
- return { id: 1 };
237
- });
238
- const fetcher2 = vi.fn(async () => {
239
- await new Promise(resolve => setTimeout(resolve, 30));
240
- return { id: 2 };
241
- });
242
- const fetcher3 = vi.fn(async () => {
243
- await new Promise(resolve => setTimeout(resolve, 30));
244
- return { id: 3 };
245
- });
246
-
247
- const results = await Promise.all([
248
- cache.get('key1', fetcher1),
249
- cache.get('key2', fetcher2),
250
- cache.get('key3', fetcher3),
251
- ]);
252
-
253
- expect(results[0]).toEqual({ id: 1 });
254
- expect(results[1]).toEqual({ id: 2 });
255
- expect(results[2]).toEqual({ id: 3 });
256
- expect(fetcher1).toHaveBeenCalledTimes(1);
257
- expect(fetcher2).toHaveBeenCalledTimes(1);
258
- expect(fetcher3).toHaveBeenCalledTimes(1);
259
- });
260
- });
261
-
262
- describe('Cache Management', () => {
263
- it('should invalidate specific key', async () => {
264
- const fetcher = vi.fn(async () => ({ data: 'test' }));
265
-
266
- await cache.get('test', fetcher);
267
- expect(fetcher).toHaveBeenCalledTimes(1);
268
-
269
- cache.invalidate('test');
270
-
271
- await cache.get('test', fetcher);
272
- expect(fetcher).toHaveBeenCalledTimes(2);
273
- });
274
-
275
- it('should invalidate all keys', async () => {
276
- const fetcher1 = vi.fn(async () => ({ id: 1 }));
277
- const fetcher2 = vi.fn(async () => ({ id: 2 }));
278
-
279
- await cache.get('key1', fetcher1);
280
- await cache.get('key2', fetcher2);
281
-
282
- cache.invalidate(); // No key = invalidate all
283
-
284
- await cache.get('key1', fetcher1);
285
- await cache.get('key2', fetcher2);
286
-
287
- expect(fetcher1).toHaveBeenCalledTimes(2);
288
- expect(fetcher2).toHaveBeenCalledTimes(2);
289
- });
290
-
291
- it('should clear cache and reset stats', async () => {
292
- const fetcher = vi.fn(async () => ({ data: 'test' }));
293
-
294
- await cache.get('key1', fetcher);
295
- await cache.get('key2', fetcher);
296
- await cache.get('key1', fetcher); // Hit
297
-
298
- const statsBefore = cache.getStats();
299
- expect(statsBefore.size).toBe(2);
300
- expect(statsBefore.hits).toBe(1);
301
- expect(statsBefore.misses).toBe(2);
302
-
303
- cache.clear();
304
-
305
- const statsAfter = cache.getStats();
306
- expect(statsAfter.size).toBe(0);
307
- expect(statsAfter.hits).toBe(0);
308
- expect(statsAfter.misses).toBe(0);
309
- expect(statsAfter.evictions).toBe(0);
310
- });
311
- });
312
-
313
- describe('Statistics', () => {
314
- it('should track cache statistics correctly', async () => {
315
- const fetcher = vi.fn(async () => ({ data: 'test' }));
316
-
317
- // Initial stats
318
- let stats = cache.getStats();
319
- expect(stats.size).toBe(0);
320
- expect(stats.maxSize).toBe(3);
321
- expect(stats.hits).toBe(0);
322
- expect(stats.misses).toBe(0);
323
- expect(stats.evictions).toBe(0);
324
- expect(stats.hitRate).toBe(0);
325
-
326
- // First access - miss
327
- await cache.get('key1', fetcher);
328
- stats = cache.getStats();
329
- expect(stats.size).toBe(1);
330
- expect(stats.misses).toBe(1);
331
- expect(stats.hitRate).toBe(0);
332
-
333
- // Second access - hit
334
- await cache.get('key1', fetcher);
335
- stats = cache.getStats();
336
- expect(stats.hits).toBe(1);
337
- expect(stats.hitRate).toBe(0.5);
338
-
339
- // Third access - hit
340
- await cache.get('key1', fetcher);
341
- stats = cache.getStats();
342
- expect(stats.hits).toBe(2);
343
- expect(stats.hitRate).toBeCloseTo(0.667, 2);
344
- });
345
-
346
- it('should track evictions', async () => {
347
- const fetcher = vi.fn(async () => ({ data: 'test' }));
348
-
349
- // Fill cache to max
350
- await cache.get('key1', fetcher);
351
- await cache.get('key2', fetcher);
352
- await cache.get('key3', fetcher);
353
-
354
- let stats = cache.getStats();
355
- expect(stats.evictions).toBe(0);
356
-
357
- // Trigger eviction
358
- await cache.get('key4', fetcher);
359
-
360
- stats = cache.getStats();
361
- expect(stats.evictions).toBe(1);
362
-
363
- // Trigger more evictions
364
- await cache.get('key5', fetcher);
365
- await cache.get('key6', fetcher);
366
-
367
- stats = cache.getStats();
368
- expect(stats.evictions).toBe(3);
369
- });
370
- });
371
-
372
- describe('Edge Cases', () => {
373
- it('should handle fetcher that throws error', async () => {
374
- const fetcher = vi.fn(async () => {
375
- throw new Error('Fetch failed');
376
- });
377
-
378
- await expect(cache.get('test', fetcher)).rejects.toThrow('Fetch failed');
379
-
380
- // Should not cache the error
381
- const stats = cache.getStats();
382
- expect(stats.size).toBe(0);
383
- });
384
-
385
- it('should handle null/undefined values', async () => {
386
- const fetcher1 = vi.fn(async () => null);
387
- const fetcher2 = vi.fn(async () => undefined);
388
-
389
- const result1 = await cache.get('null-key', fetcher1);
390
- const result2 = await cache.get('undefined-key', fetcher2);
391
-
392
- expect(result1).toBeNull();
393
- expect(result2).toBeUndefined();
394
-
395
- // Should still cache these values
396
- await cache.get('null-key', fetcher1);
397
- await cache.get('undefined-key', fetcher2);
398
-
399
- expect(fetcher1).toHaveBeenCalledTimes(1);
400
- expect(fetcher2).toHaveBeenCalledTimes(1);
401
- });
402
-
403
- it('should handle empty string key', async () => {
404
- const fetcher = vi.fn(async () => ({ data: 'test' }));
405
-
406
- const result = await cache.get('', fetcher);
407
- expect(result).toEqual({ data: 'test' });
408
-
409
- await cache.get('', fetcher);
410
- expect(fetcher).toHaveBeenCalledTimes(1);
411
- });
412
-
413
- it('should coalesce concurrent fetches for the same key', async () => {
414
- let resolveFetch: (v: { data: string }) => void = () => {};
415
- const fetcher = vi.fn(
416
- () =>
417
- new Promise<{ data: string }>((resolve) => {
418
- resolveFetch = resolve;
419
- })
420
- );
421
-
422
- const p1 = cache.get('shared-key', fetcher);
423
- const p2 = cache.get('shared-key', fetcher);
424
- const p3 = cache.get('shared-key', fetcher);
425
-
426
- // All three calls should share a single in-flight fetcher invocation
427
- expect(fetcher).toHaveBeenCalledTimes(1);
428
-
429
- resolveFetch({ data: 'shared' });
430
- const [r1, r2, r3] = await Promise.all([p1, p2, p3]);
431
-
432
- expect(r1).toEqual({ data: 'shared' });
433
- expect(r2).toBe(r1);
434
- expect(r3).toBe(r1);
435
-
436
- const stats = cache.getStats();
437
- expect(stats.coalesced).toBe(2);
438
- expect(stats.misses).toBe(1);
439
- });
440
-
441
- it('should clear inflight slot after fetch rejection so retries are possible', async () => {
442
- const fetcher = vi
443
- .fn()
444
- .mockRejectedValueOnce(new Error('boom'))
445
- .mockResolvedValueOnce({ data: 'ok' });
446
-
447
- await expect(cache.get('retry-key', fetcher)).rejects.toThrow('boom');
448
- const result = await cache.get('retry-key', fetcher);
449
-
450
- expect(result).toEqual({ data: 'ok' });
451
- expect(fetcher).toHaveBeenCalledTimes(2);
452
- });
453
-
454
- it('should expose prime() to seed cache from bulk endpoints', async () => {
455
- cache.prime('object:account', { name: 'account', fields: [] });
456
-
457
- const fetcher = vi.fn(async () => ({ name: 'account', fields: [{ name: 'id' }] }));
458
- const result = await cache.get('object:account', fetcher);
459
-
460
- expect(fetcher).not.toHaveBeenCalled();
461
- expect(result).toEqual({ name: 'account', fields: [] });
462
- });
463
-
464
- it('should handle very large cache', async () => {
465
- const largeCache = new MetadataCache({ maxSize: 10000, ttl: 60000 });
466
-
467
- // Add many entries
468
- for (let i = 0; i < 1000; i++) {
469
- await largeCache.get(`key-${i}`, async () => ({ id: i }));
470
- }
471
-
472
- const stats = largeCache.getStats();
473
- expect(stats.size).toBe(1000);
474
- expect(stats.evictions).toBe(0);
475
- });
476
- });
477
- });