@tldraw/store 4.1.0-canary.e2133d922c9e → 4.1.0-canary.e259b517a450

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 (72) hide show
  1. package/dist-cjs/index.d.ts +1884 -153
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/lib/AtomMap.js +241 -1
  4. package/dist-cjs/lib/AtomMap.js.map +2 -2
  5. package/dist-cjs/lib/BaseRecord.js.map +2 -2
  6. package/dist-cjs/lib/ImmutableMap.js +141 -0
  7. package/dist-cjs/lib/ImmutableMap.js.map +2 -2
  8. package/dist-cjs/lib/IncrementalSetConstructor.js +45 -5
  9. package/dist-cjs/lib/IncrementalSetConstructor.js.map +2 -2
  10. package/dist-cjs/lib/RecordType.js +116 -21
  11. package/dist-cjs/lib/RecordType.js.map +2 -2
  12. package/dist-cjs/lib/RecordsDiff.js.map +2 -2
  13. package/dist-cjs/lib/Store.js +233 -39
  14. package/dist-cjs/lib/Store.js.map +2 -2
  15. package/dist-cjs/lib/StoreQueries.js +135 -22
  16. package/dist-cjs/lib/StoreQueries.js.map +2 -2
  17. package/dist-cjs/lib/StoreSchema.js +207 -2
  18. package/dist-cjs/lib/StoreSchema.js.map +2 -2
  19. package/dist-cjs/lib/StoreSideEffects.js +102 -10
  20. package/dist-cjs/lib/StoreSideEffects.js.map +2 -2
  21. package/dist-cjs/lib/executeQuery.js.map +2 -2
  22. package/dist-cjs/lib/migrate.js.map +2 -2
  23. package/dist-cjs/lib/setUtils.js.map +2 -2
  24. package/dist-esm/index.d.mts +1884 -153
  25. package/dist-esm/index.mjs +1 -1
  26. package/dist-esm/lib/AtomMap.mjs +241 -1
  27. package/dist-esm/lib/AtomMap.mjs.map +2 -2
  28. package/dist-esm/lib/BaseRecord.mjs.map +2 -2
  29. package/dist-esm/lib/ImmutableMap.mjs +141 -0
  30. package/dist-esm/lib/ImmutableMap.mjs.map +2 -2
  31. package/dist-esm/lib/IncrementalSetConstructor.mjs +45 -5
  32. package/dist-esm/lib/IncrementalSetConstructor.mjs.map +2 -2
  33. package/dist-esm/lib/RecordType.mjs +116 -21
  34. package/dist-esm/lib/RecordType.mjs.map +2 -2
  35. package/dist-esm/lib/RecordsDiff.mjs.map +2 -2
  36. package/dist-esm/lib/Store.mjs +233 -39
  37. package/dist-esm/lib/Store.mjs.map +2 -2
  38. package/dist-esm/lib/StoreQueries.mjs +135 -22
  39. package/dist-esm/lib/StoreQueries.mjs.map +2 -2
  40. package/dist-esm/lib/StoreSchema.mjs +207 -2
  41. package/dist-esm/lib/StoreSchema.mjs.map +2 -2
  42. package/dist-esm/lib/StoreSideEffects.mjs +102 -10
  43. package/dist-esm/lib/StoreSideEffects.mjs.map +2 -2
  44. package/dist-esm/lib/executeQuery.mjs.map +2 -2
  45. package/dist-esm/lib/migrate.mjs.map +2 -2
  46. package/dist-esm/lib/setUtils.mjs.map +2 -2
  47. package/package.json +3 -3
  48. package/src/lib/AtomMap.ts +241 -1
  49. package/src/lib/BaseRecord.test.ts +44 -0
  50. package/src/lib/BaseRecord.ts +118 -4
  51. package/src/lib/ImmutableMap.test.ts +103 -0
  52. package/src/lib/ImmutableMap.ts +212 -0
  53. package/src/lib/IncrementalSetConstructor.test.ts +111 -0
  54. package/src/lib/IncrementalSetConstructor.ts +63 -6
  55. package/src/lib/RecordType.ts +149 -25
  56. package/src/lib/RecordsDiff.test.ts +144 -0
  57. package/src/lib/RecordsDiff.ts +145 -10
  58. package/src/lib/Store.test.ts +827 -0
  59. package/src/lib/Store.ts +533 -67
  60. package/src/lib/StoreQueries.test.ts +627 -0
  61. package/src/lib/StoreQueries.ts +194 -27
  62. package/src/lib/StoreSchema.test.ts +226 -0
  63. package/src/lib/StoreSchema.ts +386 -8
  64. package/src/lib/StoreSideEffects.test.ts +239 -19
  65. package/src/lib/StoreSideEffects.ts +266 -19
  66. package/src/lib/devFreeze.test.ts +137 -0
  67. package/src/lib/executeQuery.test.ts +481 -0
  68. package/src/lib/executeQuery.ts +80 -2
  69. package/src/lib/migrate.test.ts +400 -0
  70. package/src/lib/migrate.ts +187 -14
  71. package/src/lib/setUtils.test.ts +105 -0
  72. package/src/lib/setUtils.ts +44 -4
@@ -0,0 +1,137 @@
1
+ import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { devFreeze } from './devFreeze'
3
+
4
+ // Mock process.env for testing
5
+ const originalEnv = process.env.NODE_ENV
6
+
7
+ describe('devFreeze', () => {
8
+ beforeEach(() => {
9
+ // Reset any mocks
10
+ vi.restoreAllMocks()
11
+ })
12
+
13
+ describe('production mode behavior', () => {
14
+ beforeEach(() => {
15
+ // Mock production environment
16
+ vi.stubGlobal('process', { env: { NODE_ENV: 'production' } })
17
+ })
18
+
19
+ it('should return objects unchanged in production mode', () => {
20
+ const obj = { a: 1, b: { c: 2 } }
21
+ const result = devFreeze(obj)
22
+
23
+ expect(result).toBe(obj) // Same reference
24
+ expect(Object.isFrozen(result)).toBe(false)
25
+ expect(Object.isFrozen(result.b)).toBe(false)
26
+ })
27
+
28
+ it('should not validate prototypes in production mode', () => {
29
+ const _consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
30
+
31
+ // Create object with custom prototype that would normally throw
32
+ class CustomClass {
33
+ value = 42
34
+ }
35
+ const obj = new CustomClass()
36
+
37
+ expect(() => devFreeze(obj)).not.toThrow()
38
+ expect(_consoleSpy).not.toHaveBeenCalled()
39
+ })
40
+ })
41
+
42
+ describe('development mode behavior', () => {
43
+ beforeEach(() => {
44
+ // Mock development environment
45
+ vi.stubGlobal('process', { env: { NODE_ENV: 'development' } })
46
+ })
47
+
48
+ it('should freeze objects recursively', () => {
49
+ const obj = {
50
+ a: 1,
51
+ b: {
52
+ c: 2,
53
+ d: {
54
+ e: 3,
55
+ },
56
+ },
57
+ f: [1, { g: 4 }],
58
+ }
59
+
60
+ const result = devFreeze(obj)
61
+
62
+ expect(result).toBe(obj) // Same reference
63
+ expect(Object.isFrozen(result)).toBe(true)
64
+ expect(Object.isFrozen(result.b)).toBe(true)
65
+ expect(Object.isFrozen(result.b.d)).toBe(true)
66
+ expect(Object.isFrozen(result.f)).toBe(true)
67
+ expect(Object.isFrozen(result.f[1])).toBe(true)
68
+ })
69
+
70
+ it('should reject primitives', () => {
71
+ const _consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
72
+
73
+ // Primitives have built-in prototypes that aren't allowed
74
+ expect(() => devFreeze('string')).toThrow('cannot include non-js data in a record')
75
+ expect(() => devFreeze(42)).toThrow('cannot include non-js data in a record')
76
+ expect(() => devFreeze(true)).toThrow('cannot include non-js data in a record')
77
+
78
+ // null and undefined cause TypeError when Object.getPrototypeOf is called
79
+ expect(() => devFreeze(null)).toThrow('Cannot convert undefined or null to object')
80
+ expect(() => devFreeze(undefined)).toThrow('Cannot convert undefined or null to object')
81
+ })
82
+
83
+ it('should allow valid prototypes', () => {
84
+ // Object.prototype
85
+ const obj = { a: 1 }
86
+ expect(() => devFreeze(obj)).not.toThrow()
87
+ expect(Object.isFrozen(obj)).toBe(true)
88
+
89
+ // null prototype
90
+ const nullProtoObj = Object.create(null)
91
+ nullProtoObj.a = 1
92
+ expect(() => devFreeze(nullProtoObj)).not.toThrow()
93
+ expect(Object.isFrozen(nullProtoObj)).toBe(true)
94
+
95
+ // Arrays
96
+ const arr = [1, 2, 3]
97
+ expect(() => devFreeze(arr)).not.toThrow()
98
+ expect(Object.isFrozen(arr)).toBe(true)
99
+
100
+ // structuredClone objects
101
+ const cloned = structuredClone({ a: 1 })
102
+ expect(() => devFreeze(cloned)).not.toThrow()
103
+ expect(Object.isFrozen(cloned)).toBe(true)
104
+ })
105
+
106
+ it('should reject invalid prototypes', () => {
107
+ const _consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
108
+
109
+ // Custom class instances
110
+ class CustomClass {
111
+ value = 42
112
+ }
113
+ expect(() => devFreeze(new CustomClass())).toThrow('cannot include non-js data in a record')
114
+
115
+ // Built-in object types
116
+ expect(() => devFreeze(new Date())).toThrow('cannot include non-js data in a record')
117
+ expect(() => devFreeze(/regex/)).toThrow('cannot include non-js data in a record')
118
+ expect(() => devFreeze(new Map())).toThrow('cannot include non-js data in a record')
119
+ expect(() => devFreeze(new Set())).toThrow('cannot include non-js data in a record')
120
+
121
+ // Nested invalid objects
122
+ const objWithInvalidNested = {
123
+ valid: { a: 1 },
124
+ invalid: new Date(),
125
+ }
126
+ expect(() => devFreeze(objWithInvalidNested)).toThrow(
127
+ 'cannot include non-js data in a record'
128
+ )
129
+ })
130
+ })
131
+
132
+ // Clean up after all tests
133
+ afterAll(() => {
134
+ // Restore original environment
135
+ vi.stubGlobal('process', { env: { NODE_ENV: originalEnv } })
136
+ })
137
+ })
@@ -0,0 +1,481 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest'
2
+ import { BaseRecord, RecordId } from './BaseRecord'
3
+ import { executeQuery, objectMatchesQuery } from './executeQuery'
4
+ import { createRecordType } from './RecordType'
5
+ import { Store } from './Store'
6
+ import { StoreSchema } from './StoreSchema'
7
+
8
+ // Test record types
9
+ interface Author extends BaseRecord<'author', RecordId<Author>> {
10
+ name: string
11
+ age: number
12
+ isActive: boolean
13
+ publishedBooks: number
14
+ country: string
15
+ }
16
+
17
+ interface Book extends BaseRecord<'book', RecordId<Book>> {
18
+ title: string
19
+ authorId: RecordId<Author>
20
+ publishedYear: number
21
+ inStock: boolean
22
+ rating: number
23
+ category: string
24
+ price: number
25
+ }
26
+
27
+ interface Review extends BaseRecord<'review', RecordId<Review>> {
28
+ bookId: RecordId<Book>
29
+ rating: number
30
+ reviewerName: string
31
+ content: string
32
+ isVerified: boolean
33
+ }
34
+
35
+ const Author = createRecordType<Author>('author', {
36
+ validator: {
37
+ validate(value) {
38
+ const author = value as Author
39
+ if (author.typeName !== 'author') throw Error('Invalid typeName')
40
+ if (!author.id.startsWith('author:')) throw Error('Invalid id')
41
+ if (!Number.isFinite(author.age)) throw Error('Invalid age')
42
+ if (author.age < 0) throw Error('Negative age')
43
+ if (typeof author.isActive !== 'boolean') throw Error('Invalid isActive')
44
+ return author
45
+ },
46
+ },
47
+ scope: 'document',
48
+ }).withDefaultProperties(() => ({
49
+ age: 25,
50
+ isActive: true,
51
+ publishedBooks: 0,
52
+ country: 'USA',
53
+ }))
54
+
55
+ const Book = createRecordType<Book>('book', {
56
+ validator: {
57
+ validate(value) {
58
+ const book = value as Book
59
+ if (!book.id.startsWith('book:')) throw Error('Invalid book id')
60
+ if (book.typeName !== 'book') throw Error('Invalid book typeName')
61
+ if (typeof book.title !== 'string') throw Error('Invalid title')
62
+ if (!book.authorId.startsWith('author')) throw Error('Invalid authorId')
63
+ if (!Number.isFinite(book.publishedYear)) throw Error('Invalid publishedYear')
64
+ if (typeof book.inStock !== 'boolean') throw Error('Invalid inStock')
65
+ return book
66
+ },
67
+ },
68
+ scope: 'document',
69
+ }).withDefaultProperties(() => ({
70
+ rating: 0,
71
+ category: 'fiction',
72
+ price: 10.99,
73
+ }))
74
+
75
+ const Review = createRecordType<Review>('review', {
76
+ validator: {
77
+ validate(value) {
78
+ const review = value as Review
79
+ if (!review.id.startsWith('review:')) throw Error('Invalid review id')
80
+ if (review.typeName !== 'review') throw Error('Invalid review typeName')
81
+ if (!review.bookId.startsWith('book:')) throw Error('Invalid bookId')
82
+ if (!Number.isFinite(review.rating)) throw Error('Invalid rating')
83
+ if (typeof review.isVerified !== 'boolean') throw Error('Invalid isVerified')
84
+ return review
85
+ },
86
+ },
87
+ scope: 'document',
88
+ }).withDefaultProperties(() => ({
89
+ isVerified: false,
90
+ content: '',
91
+ reviewerName: 'Anonymous',
92
+ }))
93
+
94
+ // Test data
95
+ const authors = {
96
+ asimov: Author.create({
97
+ name: 'Isaac Asimov',
98
+ age: 72,
99
+ publishedBooks: 200,
100
+ country: 'USA',
101
+ }),
102
+ gibson: Author.create({
103
+ name: 'William Gibson',
104
+ age: 75,
105
+ publishedBooks: 15,
106
+ country: 'USA',
107
+ }),
108
+ herbert: Author.create({
109
+ name: 'Frank Herbert',
110
+ age: 65,
111
+ publishedBooks: 30,
112
+ country: 'USA',
113
+ }),
114
+ bradbury: Author.create({
115
+ name: 'Ray Bradbury',
116
+ age: 91,
117
+ publishedBooks: 100,
118
+ isActive: false,
119
+ country: 'USA',
120
+ }),
121
+ clarke: Author.create({
122
+ name: 'Arthur C. Clarke',
123
+ age: 90,
124
+ publishedBooks: 80,
125
+ country: 'UK',
126
+ }),
127
+ adams: Author.create({
128
+ name: 'Douglas Adams',
129
+ age: 49,
130
+ publishedBooks: 12,
131
+ isActive: false,
132
+ country: 'UK',
133
+ }),
134
+ }
135
+
136
+ const books = {
137
+ foundation: Book.create({
138
+ title: 'Foundation',
139
+ authorId: authors.asimov.id,
140
+ publishedYear: 1951,
141
+ inStock: true,
142
+ rating: 5,
143
+ category: 'sci-fi',
144
+ price: 12.99,
145
+ }),
146
+ neuromancer: Book.create({
147
+ title: 'Neuromancer',
148
+ authorId: authors.gibson.id,
149
+ publishedYear: 1984,
150
+ inStock: true,
151
+ rating: 5,
152
+ category: 'cyberpunk',
153
+ price: 14.99,
154
+ }),
155
+ dune: Book.create({
156
+ title: 'Dune',
157
+ authorId: authors.herbert.id,
158
+ publishedYear: 1965,
159
+ inStock: false,
160
+ rating: 5,
161
+ category: 'sci-fi',
162
+ price: 13.99,
163
+ }),
164
+ fahrenheit451: Book.create({
165
+ title: 'Fahrenheit 451',
166
+ authorId: authors.bradbury.id,
167
+ publishedYear: 1953,
168
+ inStock: true,
169
+ rating: 4,
170
+ category: 'dystopian',
171
+ price: 11.99,
172
+ }),
173
+ childhood: Book.create({
174
+ title: "Childhood's End",
175
+ authorId: authors.clarke.id,
176
+ publishedYear: 1953,
177
+ inStock: true,
178
+ rating: 4,
179
+ category: 'sci-fi',
180
+ price: 10.99,
181
+ }),
182
+ hitchhiker: Book.create({
183
+ title: "The Hitchhiker's Guide to the Galaxy",
184
+ authorId: authors.adams.id,
185
+ publishedYear: 1979,
186
+ inStock: false,
187
+ rating: 5,
188
+ category: 'comedy-sci-fi',
189
+ price: 9.99,
190
+ }),
191
+ robots: Book.create({
192
+ title: 'I, Robot',
193
+ authorId: authors.asimov.id,
194
+ publishedYear: 1950,
195
+ inStock: true,
196
+ rating: 4,
197
+ category: 'sci-fi',
198
+ price: 12.99,
199
+ }),
200
+ }
201
+
202
+ const reviews = {
203
+ foundationReview1: Review.create({
204
+ bookId: books.foundation.id,
205
+ rating: 5,
206
+ reviewerName: 'Sci-Fi Fan',
207
+ content: 'Amazing epic saga!',
208
+ isVerified: true,
209
+ }),
210
+ foundationReview2: Review.create({
211
+ bookId: books.foundation.id,
212
+ rating: 4,
213
+ reviewerName: 'Book Lover',
214
+ content: 'Great world building',
215
+ isVerified: false,
216
+ }),
217
+ duneReview: Review.create({
218
+ bookId: books.dune.id,
219
+ rating: 5,
220
+ reviewerName: 'Epic Reader',
221
+ content: 'A masterpiece of science fiction',
222
+ isVerified: true,
223
+ }),
224
+ neuromancerReview: Review.create({
225
+ bookId: books.neuromancer.id,
226
+ rating: 3,
227
+ reviewerName: 'Casual Reader',
228
+ content: 'Hard to follow at times',
229
+ isVerified: false,
230
+ }),
231
+ }
232
+
233
+ let store: Store<Author | Book | Review, unknown>
234
+
235
+ beforeEach(() => {
236
+ const schema = StoreSchema.create({
237
+ author: Author,
238
+ book: Book,
239
+ review: Review,
240
+ })
241
+
242
+ store = new Store({
243
+ schema,
244
+ props: {},
245
+ }) as unknown as Store<Author | Book | Review, unknown>
246
+
247
+ // Add all test data to store
248
+ store.put([...Object.values(authors), ...Object.values(books), ...Object.values(reviews)])
249
+ })
250
+
251
+ describe('objectMatchesQuery', () => {
252
+ describe('equality matching (eq)', () => {
253
+ it('should match when property equals the target value', () => {
254
+ const book = books.foundation
255
+ const query = { inStock: { eq: true } }
256
+
257
+ expect(objectMatchesQuery(query, book)).toBe(true)
258
+ })
259
+
260
+ it('should not match when property does not equal the target value', () => {
261
+ const book = books.dune // inStock: false
262
+ const query = { inStock: { eq: true } }
263
+
264
+ expect(objectMatchesQuery(query, book)).toBe(false)
265
+ })
266
+ })
267
+
268
+ describe('inequality matching (neq)', () => {
269
+ it('should match when property does not equal the target value', () => {
270
+ const book = books.foundation // category: 'sci-fi'
271
+ const query = { category: { neq: 'romance' } }
272
+
273
+ expect(objectMatchesQuery(query, book)).toBe(true)
274
+ })
275
+
276
+ it('should not match when property equals the target value', () => {
277
+ const book = books.foundation // category: 'sci-fi'
278
+ const query = { category: { neq: 'sci-fi' } }
279
+
280
+ expect(objectMatchesQuery(query, book)).toBe(false)
281
+ })
282
+ })
283
+
284
+ describe('greater than matching (gt)', () => {
285
+ it('should match when numeric property is greater than target', () => {
286
+ const book = books.neuromancer // publishedYear: 1984
287
+ const query = { publishedYear: { gt: 1980 } }
288
+
289
+ expect(objectMatchesQuery(query, book)).toBe(true)
290
+ })
291
+
292
+ it('should not match when numeric property equals target', () => {
293
+ const book = books.neuromancer // publishedYear: 1984
294
+ const query = { publishedYear: { gt: 1984 } }
295
+
296
+ expect(objectMatchesQuery(query, book)).toBe(false)
297
+ })
298
+
299
+ it('should not match when property is not a number', () => {
300
+ const book = { title: '1984', publishedYear: '1984' as any }
301
+ const query = { publishedYear: { gt: 1980 } }
302
+
303
+ expect(objectMatchesQuery(query, book)).toBe(false)
304
+ })
305
+ })
306
+
307
+ describe('multiple criteria matching', () => {
308
+ it('should match when all criteria are satisfied', () => {
309
+ const book = books.foundation // inStock: true, publishedYear: 1951, category: 'sci-fi'
310
+ const query = {
311
+ inStock: { eq: true },
312
+ publishedYear: { gt: 1950 },
313
+ category: { neq: 'romance' },
314
+ }
315
+
316
+ expect(objectMatchesQuery(query, book)).toBe(true)
317
+ })
318
+
319
+ it('should not match when any criteria is not satisfied', () => {
320
+ const book = books.dune // inStock: false, publishedYear: 1965, category: 'sci-fi'
321
+ const query = {
322
+ inStock: { eq: true }, // This will fail
323
+ publishedYear: { gt: 1950 },
324
+ category: { neq: 'romance' },
325
+ }
326
+
327
+ expect(objectMatchesQuery(query, book)).toBe(false)
328
+ })
329
+ })
330
+
331
+ describe('edge cases', () => {
332
+ it('should return true for empty query', () => {
333
+ const book = books.foundation
334
+ const query = {}
335
+
336
+ expect(objectMatchesQuery(query, book)).toBe(true)
337
+ })
338
+
339
+ it('should handle missing properties gracefully', () => {
340
+ const book = { title: 'Test' } as any
341
+ const query = { nonexistentProperty: { eq: 'value' } }
342
+
343
+ expect(objectMatchesQuery(query, book)).toBe(false)
344
+ })
345
+ })
346
+ })
347
+
348
+ describe('executeQuery', () => {
349
+ describe('equality queries (eq)', () => {
350
+ it('should find records with matching string values', () => {
351
+ const query = { category: { eq: 'sci-fi' } }
352
+ const result = executeQuery(store.query, 'book', query)
353
+
354
+ const expectedIds = new Set([
355
+ books.foundation.id,
356
+ books.dune.id,
357
+ books.childhood.id,
358
+ books.robots.id,
359
+ ])
360
+
361
+ expect(result).toEqual(expectedIds)
362
+ })
363
+
364
+ it('should find records with matching RecordId values', () => {
365
+ const query = { authorId: { eq: authors.asimov.id } }
366
+ const result = executeQuery(store.query, 'book', query)
367
+
368
+ const expectedIds = new Set([books.foundation.id, books.robots.id])
369
+
370
+ expect(result).toEqual(expectedIds)
371
+ })
372
+
373
+ it('should return empty set when no records match', () => {
374
+ const query = { category: { eq: 'nonexistent-category' } }
375
+ const result = executeQuery(store.query, 'book', query)
376
+
377
+ expect(result).toEqual(new Set())
378
+ })
379
+ })
380
+
381
+ describe('inequality queries (neq)', () => {
382
+ it('should find records that do not match string values', () => {
383
+ const query = { category: { neq: 'sci-fi' } }
384
+ const result = executeQuery(store.query, 'book', query)
385
+
386
+ const expectedIds = new Set([
387
+ books.neuromancer.id,
388
+ books.fahrenheit451.id,
389
+ books.hitchhiker.id,
390
+ ])
391
+
392
+ expect(result).toEqual(expectedIds)
393
+ })
394
+ })
395
+
396
+ describe('greater than queries (gt)', () => {
397
+ it('should find records with values greater than threshold', () => {
398
+ const query = { publishedYear: { gt: 1970 } }
399
+ const result = executeQuery(store.query, 'book', query)
400
+
401
+ const expectedIds = new Set([books.neuromancer.id, books.hitchhiker.id])
402
+
403
+ expect(result).toEqual(expectedIds)
404
+ })
405
+
406
+ it('should return empty set when no values are greater', () => {
407
+ const query = { publishedYear: { gt: 2000 } }
408
+ const result = executeQuery(store.query, 'book', query)
409
+
410
+ expect(result).toEqual(new Set())
411
+ })
412
+ })
413
+
414
+ describe('combined queries', () => {
415
+ it('should handle mixed query types', () => {
416
+ const query = {
417
+ inStock: { eq: true },
418
+ publishedYear: { gt: 1950 },
419
+ category: { neq: 'romance' },
420
+ }
421
+ const result = executeQuery(store.query, 'book', query)
422
+
423
+ const expectedIds = new Set([
424
+ books.foundation.id,
425
+ books.neuromancer.id,
426
+ books.fahrenheit451.id,
427
+ books.childhood.id,
428
+ ])
429
+
430
+ expect(result).toEqual(expectedIds)
431
+ })
432
+ })
433
+
434
+ describe('edge cases', () => {
435
+ it('should handle empty query', () => {
436
+ const query = {}
437
+ const result = executeQuery(store.query, 'book', query)
438
+
439
+ // Empty query in executeQuery returns empty set (special handling is done in StoreQueries)
440
+ expect(result).toEqual(new Set())
441
+ })
442
+
443
+ it('should handle different record types separately', () => {
444
+ // Query for authors, should not return books
445
+ const query = { name: { eq: 'Isaac Asimov' } }
446
+ const result = executeQuery(store.query, 'author', query)
447
+
448
+ const expectedIds = new Set([authors.asimov.id])
449
+ expect(result).toEqual(expectedIds)
450
+
451
+ // Same query on books should return empty
452
+ const bookResult = executeQuery(store.query, 'book', query as any)
453
+ expect(bookResult).toEqual(new Set())
454
+ })
455
+ })
456
+
457
+ describe('store integration', () => {
458
+ it('should update results when store changes', () => {
459
+ const query = { category: { eq: 'mystery' } }
460
+
461
+ // Initially no mystery books
462
+ let result = executeQuery(store.query, 'book', query)
463
+ expect(result).toEqual(new Set())
464
+
465
+ // Add a mystery book
466
+ const mysteryBook = Book.create({
467
+ title: 'Murder Mystery',
468
+ authorId: authors.asimov.id,
469
+ publishedYear: 2000,
470
+ inStock: true,
471
+ category: 'mystery',
472
+ })
473
+
474
+ store.put([mysteryBook])
475
+
476
+ // Should now find the mystery book
477
+ result = executeQuery(store.query, 'book', query)
478
+ expect(result).toEqual(new Set([mysteryBook.id]))
479
+ })
480
+ })
481
+ })