@tldraw/store 4.2.0-next.d76c345101d5 → 4.2.0-next.ee2c79e2a3cb
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/dist-cjs/index.d.ts +21 -12
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/StoreQueries.js +73 -27
- package/dist-cjs/lib/StoreQueries.js.map +2 -2
- package/dist-cjs/lib/executeQuery.js +38 -14
- package/dist-cjs/lib/executeQuery.js.map +2 -2
- package/dist-esm/index.d.mts +21 -12
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/StoreQueries.mjs +73 -27
- package/dist-esm/lib/StoreQueries.mjs.map +2 -2
- package/dist-esm/lib/executeQuery.mjs +38 -14
- package/dist-esm/lib/executeQuery.mjs.map +2 -2
- package/package.json +3 -3
- package/src/lib/StoreQueries.ts +102 -51
- package/src/lib/executeQuery.test.ts +928 -4
- package/src/lib/executeQuery.ts +78 -36
|
@@ -22,6 +22,14 @@ interface Book extends BaseRecord<'book', RecordId<Book>> {
|
|
|
22
22
|
rating: number
|
|
23
23
|
category: string
|
|
24
24
|
price: number
|
|
25
|
+
metadata: {
|
|
26
|
+
sessionId: string
|
|
27
|
+
status?: 'published' | 'draft' | 'archived'
|
|
28
|
+
copies?: number
|
|
29
|
+
extras: {
|
|
30
|
+
region: string
|
|
31
|
+
}
|
|
32
|
+
}
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
interface Review extends BaseRecord<'review', RecordId<Review>> {
|
|
@@ -62,6 +70,15 @@ const Book = createRecordType<Book>('book', {
|
|
|
62
70
|
if (!book.authorId.startsWith('author')) throw Error('Invalid authorId')
|
|
63
71
|
if (!Number.isFinite(book.publishedYear)) throw Error('Invalid publishedYear')
|
|
64
72
|
if (typeof book.inStock !== 'boolean') throw Error('Invalid inStock')
|
|
73
|
+
if (typeof book.metadata !== 'object' || book.metadata === null)
|
|
74
|
+
throw Error('Invalid metadata')
|
|
75
|
+
if (typeof book.metadata.sessionId !== 'string') throw Error('Invalid sessionId')
|
|
76
|
+
if (
|
|
77
|
+
typeof book.metadata.extras !== 'object' ||
|
|
78
|
+
book.metadata.extras === null ||
|
|
79
|
+
typeof book.metadata.extras.region !== 'string'
|
|
80
|
+
)
|
|
81
|
+
throw Error('Invalid extras')
|
|
65
82
|
return book
|
|
66
83
|
},
|
|
67
84
|
},
|
|
@@ -70,6 +87,12 @@ const Book = createRecordType<Book>('book', {
|
|
|
70
87
|
rating: 0,
|
|
71
88
|
category: 'fiction',
|
|
72
89
|
price: 10.99,
|
|
90
|
+
metadata: {
|
|
91
|
+
sessionId: 'session:default',
|
|
92
|
+
extras: {
|
|
93
|
+
region: 'global',
|
|
94
|
+
},
|
|
95
|
+
},
|
|
73
96
|
}))
|
|
74
97
|
|
|
75
98
|
const Review = createRecordType<Review>('review', {
|
|
@@ -142,6 +165,10 @@ const books = {
|
|
|
142
165
|
rating: 5,
|
|
143
166
|
category: 'sci-fi',
|
|
144
167
|
price: 12.99,
|
|
168
|
+
metadata: {
|
|
169
|
+
sessionId: 'session:alpha',
|
|
170
|
+
extras: { region: 'us' },
|
|
171
|
+
},
|
|
145
172
|
}),
|
|
146
173
|
neuromancer: Book.create({
|
|
147
174
|
title: 'Neuromancer',
|
|
@@ -151,6 +178,10 @@ const books = {
|
|
|
151
178
|
rating: 5,
|
|
152
179
|
category: 'cyberpunk',
|
|
153
180
|
price: 14.99,
|
|
181
|
+
metadata: {
|
|
182
|
+
sessionId: 'session:beta',
|
|
183
|
+
extras: { region: 'us' },
|
|
184
|
+
},
|
|
154
185
|
}),
|
|
155
186
|
dune: Book.create({
|
|
156
187
|
title: 'Dune',
|
|
@@ -160,6 +191,10 @@ const books = {
|
|
|
160
191
|
rating: 5,
|
|
161
192
|
category: 'sci-fi',
|
|
162
193
|
price: 13.99,
|
|
194
|
+
metadata: {
|
|
195
|
+
sessionId: 'session:beta',
|
|
196
|
+
extras: { region: 'eu' },
|
|
197
|
+
},
|
|
163
198
|
}),
|
|
164
199
|
fahrenheit451: Book.create({
|
|
165
200
|
title: 'Fahrenheit 451',
|
|
@@ -169,6 +204,10 @@ const books = {
|
|
|
169
204
|
rating: 4,
|
|
170
205
|
category: 'dystopian',
|
|
171
206
|
price: 11.99,
|
|
207
|
+
metadata: {
|
|
208
|
+
sessionId: 'session:beta',
|
|
209
|
+
extras: { region: 'us' },
|
|
210
|
+
},
|
|
172
211
|
}),
|
|
173
212
|
childhood: Book.create({
|
|
174
213
|
title: "Childhood's End",
|
|
@@ -178,6 +217,10 @@ const books = {
|
|
|
178
217
|
rating: 4,
|
|
179
218
|
category: 'sci-fi',
|
|
180
219
|
price: 10.99,
|
|
220
|
+
metadata: {
|
|
221
|
+
sessionId: 'session:gamma',
|
|
222
|
+
extras: { region: 'global' },
|
|
223
|
+
},
|
|
181
224
|
}),
|
|
182
225
|
hitchhiker: Book.create({
|
|
183
226
|
title: "The Hitchhiker's Guide to the Galaxy",
|
|
@@ -187,6 +230,10 @@ const books = {
|
|
|
187
230
|
rating: 5,
|
|
188
231
|
category: 'comedy-sci-fi',
|
|
189
232
|
price: 9.99,
|
|
233
|
+
metadata: {
|
|
234
|
+
sessionId: 'session:alpha',
|
|
235
|
+
extras: { region: 'uk' },
|
|
236
|
+
},
|
|
190
237
|
}),
|
|
191
238
|
robots: Book.create({
|
|
192
239
|
title: 'I, Robot',
|
|
@@ -196,6 +243,10 @@ const books = {
|
|
|
196
243
|
rating: 4,
|
|
197
244
|
category: 'sci-fi',
|
|
198
245
|
price: 12.99,
|
|
246
|
+
metadata: {
|
|
247
|
+
sessionId: 'session:alpha',
|
|
248
|
+
extras: { region: 'us' },
|
|
249
|
+
},
|
|
199
250
|
}),
|
|
200
251
|
}
|
|
201
252
|
|
|
@@ -297,7 +348,7 @@ describe('objectMatchesQuery', () => {
|
|
|
297
348
|
})
|
|
298
349
|
|
|
299
350
|
it('should not match when property is not a number', () => {
|
|
300
|
-
const book = { title: '1984', publishedYear: '1984'
|
|
351
|
+
const book = { title: '1984', publishedYear: '1984' }
|
|
301
352
|
const query = { publishedYear: { gt: 1980 } }
|
|
302
353
|
|
|
303
354
|
expect(objectMatchesQuery(query, book)).toBe(false)
|
|
@@ -328,6 +379,37 @@ describe('objectMatchesQuery', () => {
|
|
|
328
379
|
})
|
|
329
380
|
})
|
|
330
381
|
|
|
382
|
+
describe('nested object matching', () => {
|
|
383
|
+
it('should match when nested property satisfies criteria', () => {
|
|
384
|
+
const query = { metadata: { sessionId: { eq: 'session:alpha' } } }
|
|
385
|
+
|
|
386
|
+
expect(objectMatchesQuery(query, books.foundation)).toBe(true)
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
it('should not match when nested property fails criteria', () => {
|
|
390
|
+
const query = { metadata: { sessionId: { eq: 'session:alpha' } } }
|
|
391
|
+
|
|
392
|
+
expect(objectMatchesQuery(query, books.neuromancer)).toBe(false)
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
it('should match deeply nested properties', () => {
|
|
396
|
+
const query = { metadata: { extras: { region: { eq: 'eu' } } } }
|
|
397
|
+
|
|
398
|
+
expect(objectMatchesQuery(query, books.dune)).toBe(true)
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
it('should return false when nested object is missing', () => {
|
|
402
|
+
const book = {
|
|
403
|
+
typeName: 'book',
|
|
404
|
+
id: 'book:custom',
|
|
405
|
+
metadata: {},
|
|
406
|
+
} as Book
|
|
407
|
+
const query = { metadata: { sessionId: { eq: 'session:alpha' } } }
|
|
408
|
+
|
|
409
|
+
expect(objectMatchesQuery(query, book)).toBe(false)
|
|
410
|
+
})
|
|
411
|
+
})
|
|
412
|
+
|
|
331
413
|
describe('edge cases', () => {
|
|
332
414
|
it('should return true for empty query', () => {
|
|
333
415
|
const book = books.foundation
|
|
@@ -337,10 +419,16 @@ describe('objectMatchesQuery', () => {
|
|
|
337
419
|
})
|
|
338
420
|
|
|
339
421
|
it('should handle missing properties gracefully', () => {
|
|
340
|
-
const book = { title: 'Test' }
|
|
422
|
+
const book = { title: 'Test' }
|
|
341
423
|
const query = { nonexistentProperty: { eq: 'value' } }
|
|
342
424
|
|
|
343
|
-
expect(
|
|
425
|
+
expect(
|
|
426
|
+
objectMatchesQuery(
|
|
427
|
+
// @ts-expect-error - query is not a valid query expression for books
|
|
428
|
+
query,
|
|
429
|
+
book
|
|
430
|
+
)
|
|
431
|
+
).toBe(false)
|
|
344
432
|
})
|
|
345
433
|
})
|
|
346
434
|
})
|
|
@@ -431,6 +519,45 @@ describe('executeQuery', () => {
|
|
|
431
519
|
})
|
|
432
520
|
})
|
|
433
521
|
|
|
522
|
+
describe('nested object queries', () => {
|
|
523
|
+
it('should filter records using nested properties', () => {
|
|
524
|
+
const query = { metadata: { sessionId: { eq: 'session:alpha' } } }
|
|
525
|
+
const result = executeQuery(store.query, 'book', query)
|
|
526
|
+
|
|
527
|
+
const expectedIds = new Set([books.foundation.id, books.hitchhiker.id, books.robots.id])
|
|
528
|
+
|
|
529
|
+
expect(result).toEqual(expectedIds)
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
it('should combine nested and top-level criteria', () => {
|
|
533
|
+
const query = {
|
|
534
|
+
authorId: { eq: authors.asimov.id },
|
|
535
|
+
metadata: { sessionId: { eq: 'session:alpha' } },
|
|
536
|
+
}
|
|
537
|
+
const result = executeQuery(store.query, 'book', query)
|
|
538
|
+
|
|
539
|
+
const expectedIds = new Set([books.foundation.id, books.robots.id])
|
|
540
|
+
|
|
541
|
+
expect(result).toEqual(expectedIds)
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
it('should support deeper nested criteria', () => {
|
|
545
|
+
const query = { metadata: { extras: { region: { eq: 'eu' } } } }
|
|
546
|
+
const result = executeQuery(store.query, 'book', query)
|
|
547
|
+
|
|
548
|
+
expect(result).toEqual(new Set([books.dune.id]))
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
it('should work with reactive ids queries', () => {
|
|
552
|
+
const query = { metadata: { sessionId: { eq: 'session:beta' } } }
|
|
553
|
+
const idsQuery = store.query.ids('book', () => query)
|
|
554
|
+
|
|
555
|
+
expect(idsQuery.get()).toEqual(
|
|
556
|
+
new Set([books.neuromancer.id, books.dune.id, books.fahrenheit451.id])
|
|
557
|
+
)
|
|
558
|
+
})
|
|
559
|
+
})
|
|
560
|
+
|
|
434
561
|
describe('edge cases', () => {
|
|
435
562
|
it('should handle empty query', () => {
|
|
436
563
|
const query = {}
|
|
@@ -449,7 +576,12 @@ describe('executeQuery', () => {
|
|
|
449
576
|
expect(result).toEqual(expectedIds)
|
|
450
577
|
|
|
451
578
|
// Same query on books should return empty
|
|
452
|
-
const bookResult = executeQuery(
|
|
579
|
+
const bookResult = executeQuery(
|
|
580
|
+
store.query,
|
|
581
|
+
'book',
|
|
582
|
+
// @ts-expect-error - query is not a valid query expression for books
|
|
583
|
+
query
|
|
584
|
+
)
|
|
453
585
|
expect(bookResult).toEqual(new Set())
|
|
454
586
|
})
|
|
455
587
|
})
|
|
@@ -479,3 +611,795 @@ describe('executeQuery', () => {
|
|
|
479
611
|
})
|
|
480
612
|
})
|
|
481
613
|
})
|
|
614
|
+
|
|
615
|
+
describe('reactive nested queries', () => {
|
|
616
|
+
describe('adding records', () => {
|
|
617
|
+
it('should include newly added record that matches nested query', () => {
|
|
618
|
+
const query = { metadata: { sessionId: { eq: 'session:delta' } } }
|
|
619
|
+
const idsQuery = store.query.ids('book', () => query)
|
|
620
|
+
|
|
621
|
+
// Initially empty
|
|
622
|
+
expect(idsQuery.get()).toEqual(new Set())
|
|
623
|
+
|
|
624
|
+
// Add a book with matching nested property
|
|
625
|
+
const newBook = Book.create({
|
|
626
|
+
title: 'New Book',
|
|
627
|
+
authorId: authors.asimov.id,
|
|
628
|
+
publishedYear: 2023,
|
|
629
|
+
inStock: true,
|
|
630
|
+
metadata: {
|
|
631
|
+
sessionId: 'session:delta',
|
|
632
|
+
extras: { region: 'us' },
|
|
633
|
+
},
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
store.put([newBook])
|
|
637
|
+
|
|
638
|
+
// Should now include the new book
|
|
639
|
+
expect(idsQuery.get()).toEqual(new Set([newBook.id]))
|
|
640
|
+
})
|
|
641
|
+
|
|
642
|
+
it('should not include newly added record that does not match nested query', () => {
|
|
643
|
+
const query = { metadata: { sessionId: { eq: 'session:delta' } } }
|
|
644
|
+
const idsQuery = store.query.ids('book', () => query)
|
|
645
|
+
|
|
646
|
+
expect(idsQuery.get()).toEqual(new Set())
|
|
647
|
+
|
|
648
|
+
// Add a book with non-matching nested property
|
|
649
|
+
const newBook = Book.create({
|
|
650
|
+
title: 'Another Book',
|
|
651
|
+
authorId: authors.asimov.id,
|
|
652
|
+
publishedYear: 2023,
|
|
653
|
+
inStock: true,
|
|
654
|
+
metadata: {
|
|
655
|
+
sessionId: 'session:epsilon',
|
|
656
|
+
extras: { region: 'us' },
|
|
657
|
+
},
|
|
658
|
+
})
|
|
659
|
+
|
|
660
|
+
store.put([newBook])
|
|
661
|
+
|
|
662
|
+
// Should still be empty
|
|
663
|
+
expect(idsQuery.get()).toEqual(new Set())
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
it('should handle adding multiple records with mixed nested matches', () => {
|
|
667
|
+
const query = { metadata: { extras: { region: { eq: 'asia' } } } }
|
|
668
|
+
const idsQuery = store.query.ids('book', () => query)
|
|
669
|
+
|
|
670
|
+
expect(idsQuery.get()).toEqual(new Set())
|
|
671
|
+
|
|
672
|
+
const book1 = Book.create({
|
|
673
|
+
title: 'Book 1',
|
|
674
|
+
authorId: authors.asimov.id,
|
|
675
|
+
publishedYear: 2023,
|
|
676
|
+
inStock: true,
|
|
677
|
+
metadata: {
|
|
678
|
+
sessionId: 'session:alpha',
|
|
679
|
+
extras: { region: 'asia' },
|
|
680
|
+
},
|
|
681
|
+
})
|
|
682
|
+
|
|
683
|
+
const book2 = Book.create({
|
|
684
|
+
title: 'Book 2',
|
|
685
|
+
authorId: authors.gibson.id,
|
|
686
|
+
publishedYear: 2023,
|
|
687
|
+
inStock: true,
|
|
688
|
+
metadata: {
|
|
689
|
+
sessionId: 'session:beta',
|
|
690
|
+
extras: { region: 'us' },
|
|
691
|
+
},
|
|
692
|
+
})
|
|
693
|
+
|
|
694
|
+
const book3 = Book.create({
|
|
695
|
+
title: 'Book 3',
|
|
696
|
+
authorId: authors.herbert.id,
|
|
697
|
+
publishedYear: 2023,
|
|
698
|
+
inStock: true,
|
|
699
|
+
metadata: {
|
|
700
|
+
sessionId: 'session:gamma',
|
|
701
|
+
extras: { region: 'asia' },
|
|
702
|
+
},
|
|
703
|
+
})
|
|
704
|
+
|
|
705
|
+
store.put([book1, book2, book3])
|
|
706
|
+
|
|
707
|
+
// Only books 1 and 3 should match
|
|
708
|
+
expect(idsQuery.get()).toEqual(new Set([book1.id, book3.id]))
|
|
709
|
+
})
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
describe('removing records', () => {
|
|
713
|
+
it('should remove record from results when it is deleted', () => {
|
|
714
|
+
const query = { metadata: { sessionId: { eq: 'session:alpha' } } }
|
|
715
|
+
const idsQuery = store.query.ids('book', () => query)
|
|
716
|
+
|
|
717
|
+
// Initially should have foundation, hitchhiker, and robots
|
|
718
|
+
const initialIds = new Set([books.foundation.id, books.hitchhiker.id, books.robots.id])
|
|
719
|
+
expect(idsQuery.get()).toEqual(initialIds)
|
|
720
|
+
|
|
721
|
+
// Remove one of the matching books
|
|
722
|
+
store.remove([books.foundation.id])
|
|
723
|
+
|
|
724
|
+
// Should no longer include foundation
|
|
725
|
+
expect(idsQuery.get()).toEqual(new Set([books.hitchhiker.id, books.robots.id]))
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
it('should not affect results when removing non-matching record', () => {
|
|
729
|
+
const query = { metadata: { sessionId: { eq: 'session:alpha' } } }
|
|
730
|
+
const idsQuery = store.query.ids('book', () => query)
|
|
731
|
+
|
|
732
|
+
const initialIds = new Set([books.foundation.id, books.hitchhiker.id, books.robots.id])
|
|
733
|
+
expect(idsQuery.get()).toEqual(initialIds)
|
|
734
|
+
|
|
735
|
+
// Remove a non-matching book
|
|
736
|
+
store.remove([books.neuromancer.id])
|
|
737
|
+
|
|
738
|
+
// Results should be unchanged
|
|
739
|
+
expect(idsQuery.get()).toEqual(initialIds)
|
|
740
|
+
})
|
|
741
|
+
|
|
742
|
+
it('should handle removing all matching records', () => {
|
|
743
|
+
const query = { metadata: { sessionId: { eq: 'session:alpha' } } }
|
|
744
|
+
const idsQuery = store.query.ids('book', () => query)
|
|
745
|
+
|
|
746
|
+
expect(idsQuery.get()).toEqual(
|
|
747
|
+
new Set([books.foundation.id, books.hitchhiker.id, books.robots.id])
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
// Remove all matching books
|
|
751
|
+
store.remove([books.foundation.id, books.hitchhiker.id, books.robots.id])
|
|
752
|
+
|
|
753
|
+
// Should now be empty
|
|
754
|
+
expect(idsQuery.get()).toEqual(new Set())
|
|
755
|
+
})
|
|
756
|
+
})
|
|
757
|
+
|
|
758
|
+
describe('updating records', () => {
|
|
759
|
+
it('should add record to results when nested property is updated to match', () => {
|
|
760
|
+
const query = { metadata: { sessionId: { eq: 'session:omega' } } }
|
|
761
|
+
const idsQuery = store.query.ids('book', () => query)
|
|
762
|
+
|
|
763
|
+
// Initially empty (no books with session:omega)
|
|
764
|
+
expect(idsQuery.get()).toEqual(new Set())
|
|
765
|
+
|
|
766
|
+
// Update neuromancer to have session:omega
|
|
767
|
+
store.put([
|
|
768
|
+
{
|
|
769
|
+
...books.neuromancer,
|
|
770
|
+
metadata: {
|
|
771
|
+
sessionId: 'session:omega',
|
|
772
|
+
extras: { region: 'us' },
|
|
773
|
+
},
|
|
774
|
+
},
|
|
775
|
+
])
|
|
776
|
+
|
|
777
|
+
// Should now include neuromancer
|
|
778
|
+
expect(idsQuery.get()).toEqual(new Set([books.neuromancer.id]))
|
|
779
|
+
})
|
|
780
|
+
|
|
781
|
+
it('should remove record from results when nested property is updated to not match', () => {
|
|
782
|
+
const query = { metadata: { sessionId: { eq: 'session:alpha' } } }
|
|
783
|
+
const idsQuery = store.query.ids('book', () => query)
|
|
784
|
+
|
|
785
|
+
// Initially should have foundation, hitchhiker, and robots
|
|
786
|
+
expect(idsQuery.get()).toEqual(
|
|
787
|
+
new Set([books.foundation.id, books.hitchhiker.id, books.robots.id])
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
// Update foundation to have a different session
|
|
791
|
+
store.put([
|
|
792
|
+
{
|
|
793
|
+
...books.foundation,
|
|
794
|
+
metadata: {
|
|
795
|
+
sessionId: 'session:changed',
|
|
796
|
+
extras: { region: 'us' },
|
|
797
|
+
},
|
|
798
|
+
},
|
|
799
|
+
])
|
|
800
|
+
|
|
801
|
+
// Should no longer include foundation
|
|
802
|
+
expect(idsQuery.get()).toEqual(new Set([books.hitchhiker.id, books.robots.id]))
|
|
803
|
+
})
|
|
804
|
+
|
|
805
|
+
it('should handle deeply nested property updates', () => {
|
|
806
|
+
const query = { metadata: { extras: { region: { eq: 'antarctica' } } } }
|
|
807
|
+
const idsQuery = store.query.ids('book', () => query)
|
|
808
|
+
|
|
809
|
+
// Initially empty
|
|
810
|
+
expect(idsQuery.get()).toEqual(new Set())
|
|
811
|
+
|
|
812
|
+
// Update dune to have antarctica region
|
|
813
|
+
store.put([
|
|
814
|
+
{
|
|
815
|
+
...books.dune,
|
|
816
|
+
metadata: {
|
|
817
|
+
sessionId: 'session:beta',
|
|
818
|
+
extras: { region: 'antarctica' },
|
|
819
|
+
},
|
|
820
|
+
},
|
|
821
|
+
])
|
|
822
|
+
|
|
823
|
+
// Should now include dune
|
|
824
|
+
expect(idsQuery.get()).toEqual(new Set([books.dune.id]))
|
|
825
|
+
|
|
826
|
+
// Update dune back to eu
|
|
827
|
+
store.put([
|
|
828
|
+
{
|
|
829
|
+
...books.dune,
|
|
830
|
+
metadata: {
|
|
831
|
+
sessionId: 'session:beta',
|
|
832
|
+
extras: { region: 'eu' },
|
|
833
|
+
},
|
|
834
|
+
},
|
|
835
|
+
])
|
|
836
|
+
|
|
837
|
+
// Should no longer include dune
|
|
838
|
+
expect(idsQuery.get()).toEqual(new Set())
|
|
839
|
+
})
|
|
840
|
+
|
|
841
|
+
it('should maintain correct results when updating non-nested properties', () => {
|
|
842
|
+
const query = { metadata: { sessionId: { eq: 'session:alpha' } } }
|
|
843
|
+
const idsQuery = store.query.ids('book', () => query)
|
|
844
|
+
|
|
845
|
+
const initialIds = new Set([books.foundation.id, books.hitchhiker.id, books.robots.id])
|
|
846
|
+
expect(idsQuery.get()).toEqual(initialIds)
|
|
847
|
+
|
|
848
|
+
// Update a non-nested property (title) but keep nested property the same
|
|
849
|
+
store.put([
|
|
850
|
+
{
|
|
851
|
+
...books.foundation,
|
|
852
|
+
title: 'Foundation - Updated Edition',
|
|
853
|
+
},
|
|
854
|
+
])
|
|
855
|
+
|
|
856
|
+
// Results should be unchanged
|
|
857
|
+
expect(idsQuery.get()).toEqual(initialIds)
|
|
858
|
+
})
|
|
859
|
+
})
|
|
860
|
+
|
|
861
|
+
describe('combined nested and top-level queries', () => {
|
|
862
|
+
it('should correctly update when top-level property changes', () => {
|
|
863
|
+
const query = {
|
|
864
|
+
inStock: { eq: true },
|
|
865
|
+
metadata: { sessionId: { eq: 'session:alpha' } },
|
|
866
|
+
}
|
|
867
|
+
const idsQuery = store.query.ids('book', () => query)
|
|
868
|
+
|
|
869
|
+
// Initially foundation and robots are in stock with session:alpha
|
|
870
|
+
// hitchhiker is NOT in stock
|
|
871
|
+
expect(idsQuery.get()).toEqual(new Set([books.foundation.id, books.robots.id]))
|
|
872
|
+
|
|
873
|
+
// Update hitchhiker to be in stock
|
|
874
|
+
store.put([
|
|
875
|
+
{
|
|
876
|
+
...books.hitchhiker,
|
|
877
|
+
inStock: true,
|
|
878
|
+
},
|
|
879
|
+
])
|
|
880
|
+
|
|
881
|
+
// Should now include hitchhiker
|
|
882
|
+
expect(idsQuery.get()).toEqual(
|
|
883
|
+
new Set([books.foundation.id, books.hitchhiker.id, books.robots.id])
|
|
884
|
+
)
|
|
885
|
+
})
|
|
886
|
+
|
|
887
|
+
it('should correctly update when nested property changes in combined query', () => {
|
|
888
|
+
const query = {
|
|
889
|
+
inStock: { eq: true },
|
|
890
|
+
metadata: { sessionId: { eq: 'session:zeta' } },
|
|
891
|
+
}
|
|
892
|
+
const idsQuery = store.query.ids('book', () => query)
|
|
893
|
+
|
|
894
|
+
// Initially empty (no books with session:zeta)
|
|
895
|
+
expect(idsQuery.get()).toEqual(new Set())
|
|
896
|
+
|
|
897
|
+
// Update foundation to have session:zeta (it's already in stock)
|
|
898
|
+
store.put([
|
|
899
|
+
{
|
|
900
|
+
...books.foundation,
|
|
901
|
+
metadata: {
|
|
902
|
+
sessionId: 'session:zeta',
|
|
903
|
+
extras: { region: 'us' },
|
|
904
|
+
},
|
|
905
|
+
},
|
|
906
|
+
])
|
|
907
|
+
|
|
908
|
+
// Should now include foundation
|
|
909
|
+
expect(idsQuery.get()).toEqual(new Set([books.foundation.id]))
|
|
910
|
+
|
|
911
|
+
// Update foundation to be out of stock (but keep session:zeta)
|
|
912
|
+
store.put([
|
|
913
|
+
{
|
|
914
|
+
...books.foundation,
|
|
915
|
+
inStock: false,
|
|
916
|
+
metadata: {
|
|
917
|
+
sessionId: 'session:zeta',
|
|
918
|
+
extras: { region: 'us' },
|
|
919
|
+
},
|
|
920
|
+
},
|
|
921
|
+
])
|
|
922
|
+
|
|
923
|
+
// Should no longer include foundation
|
|
924
|
+
expect(idsQuery.get()).toEqual(new Set())
|
|
925
|
+
})
|
|
926
|
+
|
|
927
|
+
it('should handle complex updates with multiple nested levels', () => {
|
|
928
|
+
const query = {
|
|
929
|
+
category: { eq: 'sci-fi' },
|
|
930
|
+
metadata: { extras: { region: { eq: 'us' } } },
|
|
931
|
+
}
|
|
932
|
+
const idsQuery = store.query.ids('book', () => query)
|
|
933
|
+
|
|
934
|
+
// Initially foundation and robots are sci-fi with us region
|
|
935
|
+
expect(idsQuery.get()).toEqual(new Set([books.foundation.id, books.robots.id]))
|
|
936
|
+
|
|
937
|
+
// Add a new sci-fi book with us region
|
|
938
|
+
const newBook = Book.create({
|
|
939
|
+
title: 'New Sci-Fi',
|
|
940
|
+
authorId: authors.asimov.id,
|
|
941
|
+
publishedYear: 2023,
|
|
942
|
+
inStock: true,
|
|
943
|
+
category: 'sci-fi',
|
|
944
|
+
metadata: {
|
|
945
|
+
sessionId: 'session:new',
|
|
946
|
+
extras: { region: 'us' },
|
|
947
|
+
},
|
|
948
|
+
})
|
|
949
|
+
|
|
950
|
+
store.put([newBook])
|
|
951
|
+
|
|
952
|
+
expect(idsQuery.get()).toEqual(new Set([books.foundation.id, books.robots.id, newBook.id]))
|
|
953
|
+
|
|
954
|
+
// Change newBook's region
|
|
955
|
+
store.put([
|
|
956
|
+
{
|
|
957
|
+
...newBook,
|
|
958
|
+
metadata: {
|
|
959
|
+
sessionId: 'session:new',
|
|
960
|
+
extras: { region: 'eu' },
|
|
961
|
+
},
|
|
962
|
+
},
|
|
963
|
+
])
|
|
964
|
+
|
|
965
|
+
// Should no longer include newBook
|
|
966
|
+
expect(idsQuery.get()).toEqual(new Set([books.foundation.id, books.robots.id]))
|
|
967
|
+
|
|
968
|
+
// Change newBook's category but keep region as eu
|
|
969
|
+
store.put([
|
|
970
|
+
{
|
|
971
|
+
...newBook,
|
|
972
|
+
category: 'fantasy',
|
|
973
|
+
metadata: {
|
|
974
|
+
sessionId: 'session:new',
|
|
975
|
+
extras: { region: 'us' },
|
|
976
|
+
},
|
|
977
|
+
},
|
|
978
|
+
])
|
|
979
|
+
|
|
980
|
+
// Should still not include newBook (category doesn't match)
|
|
981
|
+
expect(idsQuery.get()).toEqual(new Set([books.foundation.id, books.robots.id]))
|
|
982
|
+
})
|
|
983
|
+
})
|
|
984
|
+
|
|
985
|
+
describe('query operators with nested properties', () => {
|
|
986
|
+
it('should handle gt operator on nested properties', () => {
|
|
987
|
+
// Add some books with different copy counts
|
|
988
|
+
const bookLowCopies = Book.create({
|
|
989
|
+
title: 'Low Copies Book',
|
|
990
|
+
authorId: authors.asimov.id,
|
|
991
|
+
publishedYear: 2020,
|
|
992
|
+
inStock: true,
|
|
993
|
+
metadata: {
|
|
994
|
+
sessionId: 'session:test',
|
|
995
|
+
copies: 5,
|
|
996
|
+
extras: { region: 'us' },
|
|
997
|
+
},
|
|
998
|
+
})
|
|
999
|
+
|
|
1000
|
+
const bookHighCopies = Book.create({
|
|
1001
|
+
title: 'High Copies Book',
|
|
1002
|
+
authorId: authors.gibson.id,
|
|
1003
|
+
publishedYear: 2021,
|
|
1004
|
+
inStock: true,
|
|
1005
|
+
metadata: {
|
|
1006
|
+
sessionId: 'session:test',
|
|
1007
|
+
copies: 100,
|
|
1008
|
+
extras: { region: 'us' },
|
|
1009
|
+
},
|
|
1010
|
+
})
|
|
1011
|
+
|
|
1012
|
+
const bookMidCopies = Book.create({
|
|
1013
|
+
title: 'Mid Copies Book',
|
|
1014
|
+
authorId: authors.herbert.id,
|
|
1015
|
+
publishedYear: 2022,
|
|
1016
|
+
inStock: true,
|
|
1017
|
+
metadata: {
|
|
1018
|
+
sessionId: 'session:test',
|
|
1019
|
+
copies: 50,
|
|
1020
|
+
extras: { region: 'us' },
|
|
1021
|
+
},
|
|
1022
|
+
})
|
|
1023
|
+
|
|
1024
|
+
store.put([bookLowCopies, bookHighCopies, bookMidCopies])
|
|
1025
|
+
|
|
1026
|
+
// Query for books with more than 25 copies
|
|
1027
|
+
const query = { metadata: { copies: { gt: 25 } } }
|
|
1028
|
+
const idsQuery = store.query.ids('book', () => query)
|
|
1029
|
+
|
|
1030
|
+
// Should include only books with > 25 copies
|
|
1031
|
+
const expectedIds = new Set([bookHighCopies.id, bookMidCopies.id])
|
|
1032
|
+
expect(idsQuery.get()).toEqual(expectedIds)
|
|
1033
|
+
|
|
1034
|
+
// Update one book to have fewer copies
|
|
1035
|
+
store.put([
|
|
1036
|
+
{
|
|
1037
|
+
...bookMidCopies,
|
|
1038
|
+
metadata: {
|
|
1039
|
+
...bookMidCopies.metadata,
|
|
1040
|
+
copies: 10,
|
|
1041
|
+
},
|
|
1042
|
+
},
|
|
1043
|
+
])
|
|
1044
|
+
|
|
1045
|
+
// Should no longer include bookMidCopies
|
|
1046
|
+
expect(idsQuery.get()).toEqual(new Set([bookHighCopies.id]))
|
|
1047
|
+
})
|
|
1048
|
+
|
|
1049
|
+
it('should handle neq operator on nested properties with updates', () => {
|
|
1050
|
+
const query = { metadata: { sessionId: { neq: 'session:alpha' } } }
|
|
1051
|
+
const idsQuery = store.query.ids('book', () => query)
|
|
1052
|
+
|
|
1053
|
+
// Should include all books except those with session:alpha
|
|
1054
|
+
const expectedIds = new Set([
|
|
1055
|
+
books.neuromancer.id,
|
|
1056
|
+
books.dune.id,
|
|
1057
|
+
books.fahrenheit451.id,
|
|
1058
|
+
books.childhood.id,
|
|
1059
|
+
])
|
|
1060
|
+
expect(idsQuery.get()).toEqual(expectedIds)
|
|
1061
|
+
|
|
1062
|
+
// Update one of the matching books to have session:alpha
|
|
1063
|
+
store.put([
|
|
1064
|
+
{
|
|
1065
|
+
...books.neuromancer,
|
|
1066
|
+
metadata: {
|
|
1067
|
+
sessionId: 'session:alpha',
|
|
1068
|
+
extras: { region: 'us' },
|
|
1069
|
+
},
|
|
1070
|
+
},
|
|
1071
|
+
])
|
|
1072
|
+
|
|
1073
|
+
// Should no longer include neuromancer
|
|
1074
|
+
expect(idsQuery.get()).toEqual(
|
|
1075
|
+
new Set([books.dune.id, books.fahrenheit451.id, books.childhood.id])
|
|
1076
|
+
)
|
|
1077
|
+
})
|
|
1078
|
+
|
|
1079
|
+
it('should handle neq with undefined nested values', () => {
|
|
1080
|
+
// Create books where some have a nested optional property and others don't
|
|
1081
|
+
const bookWithStatus = Book.create({
|
|
1082
|
+
title: 'Book With Status',
|
|
1083
|
+
authorId: authors.asimov.id,
|
|
1084
|
+
publishedYear: 2020,
|
|
1085
|
+
inStock: true,
|
|
1086
|
+
metadata: {
|
|
1087
|
+
sessionId: 'session:test',
|
|
1088
|
+
status: 'published',
|
|
1089
|
+
extras: { region: 'us' },
|
|
1090
|
+
},
|
|
1091
|
+
})
|
|
1092
|
+
|
|
1093
|
+
const bookWithDifferentStatus = Book.create({
|
|
1094
|
+
title: 'Book With Different Status',
|
|
1095
|
+
authorId: authors.gibson.id,
|
|
1096
|
+
publishedYear: 2021,
|
|
1097
|
+
inStock: true,
|
|
1098
|
+
metadata: {
|
|
1099
|
+
sessionId: 'session:test',
|
|
1100
|
+
status: 'draft',
|
|
1101
|
+
extras: { region: 'us' },
|
|
1102
|
+
},
|
|
1103
|
+
})
|
|
1104
|
+
|
|
1105
|
+
const bookWithoutStatus = Book.create({
|
|
1106
|
+
title: 'Book Without Status',
|
|
1107
|
+
authorId: authors.herbert.id,
|
|
1108
|
+
publishedYear: 2022,
|
|
1109
|
+
inStock: true,
|
|
1110
|
+
metadata: {
|
|
1111
|
+
sessionId: 'session:test',
|
|
1112
|
+
extras: { region: 'us' },
|
|
1113
|
+
},
|
|
1114
|
+
})
|
|
1115
|
+
|
|
1116
|
+
store.put([bookWithStatus, bookWithDifferentStatus, bookWithoutStatus])
|
|
1117
|
+
|
|
1118
|
+
// Query for books where status is not 'published'
|
|
1119
|
+
// Note: Records with undefined nested values are not indexed, so they won't appear in neq results
|
|
1120
|
+
// This is because the index only tracks records with defined values
|
|
1121
|
+
const query = { metadata: { status: { neq: 'published' } } }
|
|
1122
|
+
const idsQuery = store.query.ids(
|
|
1123
|
+
'book',
|
|
1124
|
+
// @ts-expect-error - query is not a valid query expression for books
|
|
1125
|
+
() => query
|
|
1126
|
+
)
|
|
1127
|
+
|
|
1128
|
+
// Should include only books with a defined status that is not 'published'
|
|
1129
|
+
// bookWithoutStatus is not in the index since its status is undefined
|
|
1130
|
+
expect(idsQuery.get()).toEqual(new Set([bookWithDifferentStatus.id]))
|
|
1131
|
+
|
|
1132
|
+
// Update bookWithDifferentStatus to be 'published'
|
|
1133
|
+
store.put([
|
|
1134
|
+
{
|
|
1135
|
+
...bookWithDifferentStatus,
|
|
1136
|
+
metadata: {
|
|
1137
|
+
...bookWithDifferentStatus.metadata,
|
|
1138
|
+
status: 'published',
|
|
1139
|
+
},
|
|
1140
|
+
} as any,
|
|
1141
|
+
])
|
|
1142
|
+
|
|
1143
|
+
// Should now be empty (bookWithoutStatus still not in index)
|
|
1144
|
+
expect(idsQuery.get()).toEqual(new Set())
|
|
1145
|
+
|
|
1146
|
+
// Add a third status to verify the index still works
|
|
1147
|
+
const bookWithArchivedStatus = Book.create({
|
|
1148
|
+
title: 'Book With Archived Status',
|
|
1149
|
+
authorId: authors.bradbury.id,
|
|
1150
|
+
publishedYear: 2023,
|
|
1151
|
+
inStock: true,
|
|
1152
|
+
metadata: {
|
|
1153
|
+
sessionId: 'session:test',
|
|
1154
|
+
status: 'archived',
|
|
1155
|
+
extras: { region: 'us' },
|
|
1156
|
+
},
|
|
1157
|
+
} as any)
|
|
1158
|
+
|
|
1159
|
+
store.put([bookWithArchivedStatus])
|
|
1160
|
+
|
|
1161
|
+
// Should now include only the archived book
|
|
1162
|
+
expect(idsQuery.get()).toEqual(new Set([bookWithArchivedStatus.id]))
|
|
1163
|
+
})
|
|
1164
|
+
|
|
1165
|
+
it('should handle multiple nested criteria with updates', () => {
|
|
1166
|
+
const query = {
|
|
1167
|
+
metadata: {
|
|
1168
|
+
sessionId: { neq: 'session:gamma' },
|
|
1169
|
+
extras: { region: { eq: 'us' } },
|
|
1170
|
+
},
|
|
1171
|
+
}
|
|
1172
|
+
const idsQuery = store.query.ids('book', () => query)
|
|
1173
|
+
|
|
1174
|
+
// Should include books with us region but not session:gamma
|
|
1175
|
+
expect(idsQuery.get()).toEqual(
|
|
1176
|
+
new Set([
|
|
1177
|
+
books.foundation.id,
|
|
1178
|
+
books.neuromancer.id,
|
|
1179
|
+
books.fahrenheit451.id,
|
|
1180
|
+
books.robots.id,
|
|
1181
|
+
])
|
|
1182
|
+
)
|
|
1183
|
+
|
|
1184
|
+
// Change foundation's region to uk
|
|
1185
|
+
store.put([
|
|
1186
|
+
{
|
|
1187
|
+
...books.foundation,
|
|
1188
|
+
metadata: {
|
|
1189
|
+
...books.foundation.metadata,
|
|
1190
|
+
extras: { region: 'uk' },
|
|
1191
|
+
},
|
|
1192
|
+
},
|
|
1193
|
+
])
|
|
1194
|
+
|
|
1195
|
+
// Should no longer include foundation
|
|
1196
|
+
expect(idsQuery.get()).toEqual(
|
|
1197
|
+
new Set([books.neuromancer.id, books.fahrenheit451.id, books.robots.id])
|
|
1198
|
+
)
|
|
1199
|
+
})
|
|
1200
|
+
})
|
|
1201
|
+
|
|
1202
|
+
describe('multiple subscribers', () => {
|
|
1203
|
+
it('should update all subscribers when records change', () => {
|
|
1204
|
+
const query = { metadata: { sessionId: { eq: 'session:theta' } } }
|
|
1205
|
+
const idsQuery1 = store.query.ids('book', () => query)
|
|
1206
|
+
const idsQuery2 = store.query.ids('book', () => query)
|
|
1207
|
+
|
|
1208
|
+
// Both should be empty initially
|
|
1209
|
+
expect(idsQuery1.get()).toEqual(new Set())
|
|
1210
|
+
expect(idsQuery2.get()).toEqual(new Set())
|
|
1211
|
+
|
|
1212
|
+
// Add a matching book
|
|
1213
|
+
const newBook = Book.create({
|
|
1214
|
+
title: 'Test Book',
|
|
1215
|
+
authorId: authors.asimov.id,
|
|
1216
|
+
publishedYear: 2023,
|
|
1217
|
+
inStock: true,
|
|
1218
|
+
metadata: {
|
|
1219
|
+
sessionId: 'session:theta',
|
|
1220
|
+
extras: { region: 'us' },
|
|
1221
|
+
},
|
|
1222
|
+
})
|
|
1223
|
+
|
|
1224
|
+
store.put([newBook])
|
|
1225
|
+
|
|
1226
|
+
// Both should now include the new book
|
|
1227
|
+
expect(idsQuery1.get()).toEqual(new Set([newBook.id]))
|
|
1228
|
+
expect(idsQuery2.get()).toEqual(new Set([newBook.id]))
|
|
1229
|
+
|
|
1230
|
+
// Remove the book
|
|
1231
|
+
store.remove([newBook.id])
|
|
1232
|
+
|
|
1233
|
+
// Both should be empty again
|
|
1234
|
+
expect(idsQuery1.get()).toEqual(new Set())
|
|
1235
|
+
expect(idsQuery2.get()).toEqual(new Set())
|
|
1236
|
+
})
|
|
1237
|
+
|
|
1238
|
+
it('should handle different queries on same nested properties', () => {
|
|
1239
|
+
const queryAlpha = { metadata: { sessionId: { eq: 'session:alpha' } } }
|
|
1240
|
+
const queryBeta = { metadata: { sessionId: { eq: 'session:beta' } } }
|
|
1241
|
+
|
|
1242
|
+
const idsQueryAlpha = store.query.ids('book', () => queryAlpha)
|
|
1243
|
+
const idsQueryBeta = store.query.ids('book', () => queryBeta)
|
|
1244
|
+
|
|
1245
|
+
expect(idsQueryAlpha.get()).toEqual(
|
|
1246
|
+
new Set([books.foundation.id, books.hitchhiker.id, books.robots.id])
|
|
1247
|
+
)
|
|
1248
|
+
expect(idsQueryBeta.get()).toEqual(
|
|
1249
|
+
new Set([books.neuromancer.id, books.dune.id, books.fahrenheit451.id])
|
|
1250
|
+
)
|
|
1251
|
+
|
|
1252
|
+
// Update foundation from alpha to beta
|
|
1253
|
+
store.put([
|
|
1254
|
+
{
|
|
1255
|
+
...books.foundation,
|
|
1256
|
+
metadata: {
|
|
1257
|
+
sessionId: 'session:beta',
|
|
1258
|
+
extras: { region: 'us' },
|
|
1259
|
+
},
|
|
1260
|
+
},
|
|
1261
|
+
])
|
|
1262
|
+
|
|
1263
|
+
// Alpha query should no longer include foundation
|
|
1264
|
+
expect(idsQueryAlpha.get()).toEqual(new Set([books.hitchhiker.id, books.robots.id]))
|
|
1265
|
+
|
|
1266
|
+
// Beta query should now include foundation
|
|
1267
|
+
expect(idsQueryBeta.get()).toEqual(
|
|
1268
|
+
new Set([books.foundation.id, books.neuromancer.id, books.dune.id, books.fahrenheit451.id])
|
|
1269
|
+
)
|
|
1270
|
+
})
|
|
1271
|
+
})
|
|
1272
|
+
|
|
1273
|
+
describe('batch operations', () => {
|
|
1274
|
+
it('should handle batch updates affecting nested queries', () => {
|
|
1275
|
+
const query = { metadata: { extras: { region: { eq: 'canada' } } } }
|
|
1276
|
+
const idsQuery = store.query.ids('book', () => query)
|
|
1277
|
+
|
|
1278
|
+
expect(idsQuery.get()).toEqual(new Set())
|
|
1279
|
+
|
|
1280
|
+
// Batch update multiple books to have canada region
|
|
1281
|
+
store.put([
|
|
1282
|
+
{
|
|
1283
|
+
...books.foundation,
|
|
1284
|
+
metadata: {
|
|
1285
|
+
sessionId: 'session:alpha',
|
|
1286
|
+
extras: { region: 'canada' },
|
|
1287
|
+
},
|
|
1288
|
+
},
|
|
1289
|
+
{
|
|
1290
|
+
...books.neuromancer,
|
|
1291
|
+
metadata: {
|
|
1292
|
+
sessionId: 'session:beta',
|
|
1293
|
+
extras: { region: 'canada' },
|
|
1294
|
+
},
|
|
1295
|
+
},
|
|
1296
|
+
{
|
|
1297
|
+
...books.dune,
|
|
1298
|
+
metadata: {
|
|
1299
|
+
sessionId: 'session:beta',
|
|
1300
|
+
extras: { region: 'canada' },
|
|
1301
|
+
},
|
|
1302
|
+
},
|
|
1303
|
+
])
|
|
1304
|
+
|
|
1305
|
+
// Should include all three books
|
|
1306
|
+
expect(idsQuery.get()).toEqual(
|
|
1307
|
+
new Set([books.foundation.id, books.neuromancer.id, books.dune.id])
|
|
1308
|
+
)
|
|
1309
|
+
|
|
1310
|
+
// Batch update to remove them all
|
|
1311
|
+
store.put([
|
|
1312
|
+
{
|
|
1313
|
+
...books.foundation,
|
|
1314
|
+
metadata: {
|
|
1315
|
+
sessionId: 'session:alpha',
|
|
1316
|
+
extras: { region: 'us' },
|
|
1317
|
+
},
|
|
1318
|
+
},
|
|
1319
|
+
{
|
|
1320
|
+
...books.neuromancer,
|
|
1321
|
+
metadata: {
|
|
1322
|
+
sessionId: 'session:beta',
|
|
1323
|
+
extras: { region: 'us' },
|
|
1324
|
+
},
|
|
1325
|
+
},
|
|
1326
|
+
{
|
|
1327
|
+
...books.dune,
|
|
1328
|
+
metadata: {
|
|
1329
|
+
sessionId: 'session:beta',
|
|
1330
|
+
extras: { region: 'eu' },
|
|
1331
|
+
},
|
|
1332
|
+
},
|
|
1333
|
+
])
|
|
1334
|
+
|
|
1335
|
+
// Should be empty again
|
|
1336
|
+
expect(idsQuery.get()).toEqual(new Set())
|
|
1337
|
+
})
|
|
1338
|
+
|
|
1339
|
+
it('should handle mixed batch operations (add, update, remove)', () => {
|
|
1340
|
+
const query = { metadata: { sessionId: { eq: 'session:mixed' } } }
|
|
1341
|
+
const idsQuery = store.query.ids('book', () => query)
|
|
1342
|
+
|
|
1343
|
+
expect(idsQuery.get()).toEqual(new Set())
|
|
1344
|
+
|
|
1345
|
+
const newBook1 = Book.create({
|
|
1346
|
+
title: 'New Book 1',
|
|
1347
|
+
authorId: authors.asimov.id,
|
|
1348
|
+
publishedYear: 2023,
|
|
1349
|
+
inStock: true,
|
|
1350
|
+
metadata: {
|
|
1351
|
+
sessionId: 'session:mixed',
|
|
1352
|
+
extras: { region: 'us' },
|
|
1353
|
+
},
|
|
1354
|
+
})
|
|
1355
|
+
|
|
1356
|
+
const newBook2 = Book.create({
|
|
1357
|
+
title: 'New Book 2',
|
|
1358
|
+
authorId: authors.gibson.id,
|
|
1359
|
+
publishedYear: 2023,
|
|
1360
|
+
inStock: true,
|
|
1361
|
+
metadata: {
|
|
1362
|
+
sessionId: 'session:mixed',
|
|
1363
|
+
extras: { region: 'us' },
|
|
1364
|
+
},
|
|
1365
|
+
})
|
|
1366
|
+
|
|
1367
|
+
// Add new books and update existing one in a single batch
|
|
1368
|
+
store.put([
|
|
1369
|
+
newBook1,
|
|
1370
|
+
newBook2,
|
|
1371
|
+
{
|
|
1372
|
+
...books.foundation,
|
|
1373
|
+
metadata: {
|
|
1374
|
+
sessionId: 'session:mixed',
|
|
1375
|
+
extras: { region: 'us' },
|
|
1376
|
+
},
|
|
1377
|
+
},
|
|
1378
|
+
])
|
|
1379
|
+
|
|
1380
|
+
expect(idsQuery.get()).toEqual(new Set([newBook1.id, newBook2.id, books.foundation.id]))
|
|
1381
|
+
|
|
1382
|
+
// Remove one new book, update another, and revert foundation
|
|
1383
|
+
store.remove([newBook1.id])
|
|
1384
|
+
store.put([
|
|
1385
|
+
{
|
|
1386
|
+
...newBook2,
|
|
1387
|
+
metadata: {
|
|
1388
|
+
sessionId: 'session:different',
|
|
1389
|
+
extras: { region: 'us' },
|
|
1390
|
+
},
|
|
1391
|
+
},
|
|
1392
|
+
{
|
|
1393
|
+
...books.foundation,
|
|
1394
|
+
metadata: {
|
|
1395
|
+
sessionId: 'session:alpha',
|
|
1396
|
+
extras: { region: 'us' },
|
|
1397
|
+
},
|
|
1398
|
+
},
|
|
1399
|
+
])
|
|
1400
|
+
|
|
1401
|
+
// Should now be empty
|
|
1402
|
+
expect(idsQuery.get()).toEqual(new Set())
|
|
1403
|
+
})
|
|
1404
|
+
})
|
|
1405
|
+
})
|