@tldraw/store 5.2.0-next.ee0fa4d6244f → 5.2.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.
- package/DOCS.md +790 -0
- package/README.md +9 -1
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/ImmutableMap.js +0 -13
- package/dist-cjs/lib/ImmutableMap.js.map +2 -2
- package/dist-cjs/lib/RecordType.js +1 -1
- package/dist-cjs/lib/RecordType.js.map +2 -2
- package/dist-cjs/lib/RecordsDiff.js +26 -14
- package/dist-cjs/lib/RecordsDiff.js.map +2 -2
- package/dist-cjs/lib/Store.js +6 -5
- package/dist-cjs/lib/Store.js.map +2 -2
- package/dist-cjs/lib/StoreSchema.js +1 -1
- package/dist-cjs/lib/StoreSchema.js.map +2 -2
- package/dist-cjs/lib/executeQuery.js +1 -1
- package/dist-cjs/lib/executeQuery.js.map +2 -2
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/ImmutableMap.mjs +0 -13
- package/dist-esm/lib/ImmutableMap.mjs.map +2 -2
- package/dist-esm/lib/RecordType.mjs +1 -1
- package/dist-esm/lib/RecordType.mjs.map +2 -2
- package/dist-esm/lib/RecordsDiff.mjs +26 -14
- package/dist-esm/lib/RecordsDiff.mjs.map +2 -2
- package/dist-esm/lib/Store.mjs +7 -6
- package/dist-esm/lib/Store.mjs.map +2 -2
- package/dist-esm/lib/StoreSchema.mjs +1 -1
- package/dist-esm/lib/StoreSchema.mjs.map +2 -2
- package/dist-esm/lib/executeQuery.mjs +1 -1
- package/dist-esm/lib/executeQuery.mjs.map +2 -2
- package/package.json +9 -5
- package/src/lib/{test/AtomMap.test.ts → AtomMap.test.ts} +77 -111
- package/src/lib/AtomSet.test.ts +116 -0
- package/src/lib/BaseRecord.test.ts +10 -22
- package/src/lib/ImmutableMap.test.ts +114 -71
- package/src/lib/ImmutableMap.ts +1 -0
- package/src/lib/IncrementalSetConstructor.test.ts +76 -81
- package/src/lib/RecordType.test.ts +216 -0
- package/src/lib/RecordType.ts +1 -1
- package/src/lib/RecordsDiff.test.ts +112 -106
- package/src/lib/RecordsDiff.ts +43 -18
- package/src/lib/Store.test.ts +570 -630
- package/src/lib/Store.ts +9 -10
- package/src/lib/StoreListeners.test.ts +462 -0
- package/src/lib/StoreQueries.test.ts +586 -434
- package/src/lib/StoreSchema.test.ts +1012 -174
- package/src/lib/StoreSchema.ts +1 -1
- package/src/lib/StoreSideEffects.test.ts +546 -158
- package/src/lib/devFreeze.test.ts +94 -124
- package/src/lib/executeQuery.test.ts +77 -31
- package/src/lib/executeQuery.ts +3 -1
- package/src/lib/migrate.test.ts +273 -296
- package/src/lib/setUtils.test.ts +38 -79
- package/src/lib/test/createMigrations.test.ts +0 -75
- package/src/lib/test/dependsOn.test.ts +0 -166
- package/src/lib/test/getMigrationsSince.test.ts +0 -121
- package/src/lib/test/migrate.test.ts +0 -118
- package/src/lib/test/migratePersistedRecord.test.ts +0 -265
- package/src/lib/test/migrationCaching.test.ts +0 -209
- package/src/lib/test/recordStore.test.ts +0 -1567
- package/src/lib/test/recordStoreQueries.test.ts +0 -814
- package/src/lib/test/recordType.test.ts +0 -19
- package/src/lib/test/sortMigrations.test.ts +0 -83
- package/src/lib/test/upgradeSchema.test.ts +0 -80
- package/src/lib/test/validate.test.ts +0 -178
- package/src/lib/test/validateMigrations.test.ts +0 -165
|
@@ -1,121 +1,67 @@
|
|
|
1
|
-
import { atom, computed, RESET_VALUE } from '@tldraw/state'
|
|
1
|
+
import { atom, computed, EMPTY_ARRAY, RESET_VALUE } from '@tldraw/state'
|
|
2
2
|
import { beforeEach, describe, expect, it } from 'vitest'
|
|
3
3
|
import { BaseRecord, RecordId } from './BaseRecord'
|
|
4
4
|
import { createRecordType } from './RecordType'
|
|
5
5
|
import { Store } from './Store'
|
|
6
|
-
import { StoreQueries } from './StoreQueries'
|
|
7
6
|
import { StoreSchema } from './StoreSchema'
|
|
8
7
|
|
|
9
|
-
//
|
|
8
|
+
// Tests for SPEC.md §11 (filtered history), §12 (indexes), and §13 (id, record, and exec
|
|
9
|
+
// queries). Rule IDs like [QI3] in test names refer to that document.
|
|
10
|
+
|
|
10
11
|
interface Author extends BaseRecord<'author', RecordId<Author>> {
|
|
11
12
|
name: string
|
|
12
13
|
age: number
|
|
13
|
-
isActive: boolean
|
|
14
|
-
publishedBooks: number
|
|
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
14
|
}
|
|
25
|
-
|
|
26
15
|
const Author = createRecordType<Author>('author', {
|
|
27
16
|
validator: {
|
|
28
17
|
validate(value) {
|
|
29
18
|
const author = value as Author
|
|
30
|
-
if (author.typeName !== 'author') throw Error(
|
|
31
|
-
if (!author.id.startsWith('author:')) throw Error(
|
|
32
|
-
if (!Number.isFinite(author.age)) throw Error(
|
|
33
|
-
if (author.age < 0) throw Error(
|
|
34
|
-
if (typeof author.isActive !== 'boolean') throw Error('Invalid isActive')
|
|
19
|
+
if (author.typeName !== 'author') throw Error()
|
|
20
|
+
if (!author.id.startsWith('author:')) throw Error()
|
|
21
|
+
if (!Number.isFinite(author.age)) throw Error()
|
|
22
|
+
if (author.age < 0) throw Error()
|
|
35
23
|
return author
|
|
36
24
|
},
|
|
37
25
|
},
|
|
38
26
|
scope: 'document',
|
|
39
|
-
}).withDefaultProperties(() => ({ age:
|
|
27
|
+
}).withDefaultProperties(() => ({ age: 23 }))
|
|
40
28
|
|
|
29
|
+
interface Book extends BaseRecord<'book', RecordId<Book>> {
|
|
30
|
+
title: string
|
|
31
|
+
authorId: RecordId<Author>
|
|
32
|
+
}
|
|
41
33
|
const Book = createRecordType<Book>('book', {
|
|
42
34
|
validator: {
|
|
43
35
|
validate(value) {
|
|
44
36
|
const book = value as Book
|
|
45
|
-
if (!book.id.startsWith('book:')) throw Error(
|
|
46
|
-
if (book.typeName !== 'book') throw Error(
|
|
47
|
-
if (typeof book.title !== 'string') throw Error(
|
|
48
|
-
if (!book.authorId.startsWith('author')) throw Error(
|
|
49
|
-
if (!Number.isFinite(book.publishedYear)) throw Error('Invalid publishedYear')
|
|
50
|
-
if (typeof book.inStock !== 'boolean') throw Error('Invalid inStock')
|
|
37
|
+
if (!book.id.startsWith('book:')) throw Error()
|
|
38
|
+
if (book.typeName !== 'book') throw Error()
|
|
39
|
+
if (typeof book.title !== 'string') throw Error()
|
|
40
|
+
if (!book.authorId.startsWith('author')) throw Error()
|
|
51
41
|
return book
|
|
52
42
|
},
|
|
53
43
|
},
|
|
54
44
|
scope: 'document',
|
|
55
|
-
})
|
|
45
|
+
})
|
|
56
46
|
|
|
57
|
-
// Test data
|
|
58
47
|
const authors = {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
clarke: Author.create({ name: 'Arthur C. Clarke', age: 90, publishedBooks: 80 }),
|
|
48
|
+
tolkein: Author.create({ name: 'J.R.R. Tolkein' }),
|
|
49
|
+
bradbury: Author.create({ name: 'Ray Bradbury' }),
|
|
50
|
+
davidMitchellSerious: Author.create({ name: 'David Mitchell' }),
|
|
51
|
+
davidMitchellFunny: Author.create({ name: 'David Mitchell' }),
|
|
64
52
|
}
|
|
65
53
|
|
|
66
54
|
const books = {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
inStock: true,
|
|
72
|
-
rating: 5,
|
|
73
|
-
category: 'sci-fi',
|
|
74
|
-
}),
|
|
75
|
-
neuromancer: Book.create({
|
|
76
|
-
title: 'Neuromancer',
|
|
77
|
-
authorId: authors.gibson.id,
|
|
78
|
-
publishedYear: 1984,
|
|
79
|
-
inStock: true,
|
|
80
|
-
rating: 5,
|
|
81
|
-
category: 'cyberpunk',
|
|
82
|
-
}),
|
|
83
|
-
dune: Book.create({
|
|
84
|
-
title: 'Dune',
|
|
85
|
-
authorId: authors.herbert.id,
|
|
86
|
-
publishedYear: 1965,
|
|
87
|
-
inStock: false,
|
|
88
|
-
rating: 5,
|
|
89
|
-
category: 'sci-fi',
|
|
90
|
-
}),
|
|
91
|
-
fahrenheit451: Book.create({
|
|
92
|
-
title: 'Fahrenheit 451',
|
|
93
|
-
authorId: authors.bradbury.id,
|
|
94
|
-
publishedYear: 1953,
|
|
95
|
-
inStock: true,
|
|
96
|
-
rating: 4,
|
|
97
|
-
category: 'dystopian',
|
|
98
|
-
}),
|
|
99
|
-
childhood: Book.create({
|
|
100
|
-
title: "Childhood's End",
|
|
101
|
-
authorId: authors.clarke.id,
|
|
102
|
-
publishedYear: 1953,
|
|
103
|
-
inStock: true,
|
|
104
|
-
rating: 4,
|
|
105
|
-
category: 'sci-fi',
|
|
106
|
-
}),
|
|
107
|
-
robotSeries: Book.create({
|
|
108
|
-
title: 'I, Robot',
|
|
109
|
-
authorId: authors.asimov.id,
|
|
110
|
-
publishedYear: 1950,
|
|
111
|
-
inStock: false,
|
|
112
|
-
rating: 4,
|
|
113
|
-
category: 'sci-fi',
|
|
55
|
+
cloudAtlas: Book.create({ title: 'Cloud Atlas', authorId: authors.davidMitchellSerious.id }),
|
|
56
|
+
myLifeInComedy: Book.create({
|
|
57
|
+
title: 'My Life in Comedy',
|
|
58
|
+
authorId: authors.davidMitchellFunny.id,
|
|
114
59
|
}),
|
|
60
|
+
lotr: Book.create({ title: 'Lord of the Rings', authorId: authors.tolkein.id }),
|
|
61
|
+
farenheit: Book.create({ title: 'Farenheit 451', authorId: authors.bradbury.id }),
|
|
115
62
|
}
|
|
116
63
|
|
|
117
64
|
let store: Store<Author | Book>
|
|
118
|
-
let queries: StoreQueries<Author | Book>
|
|
119
65
|
|
|
120
66
|
beforeEach(() => {
|
|
121
67
|
store = new Store({
|
|
@@ -125,503 +71,709 @@ beforeEach(() => {
|
|
|
125
71
|
book: Book,
|
|
126
72
|
}),
|
|
127
73
|
})
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
74
|
+
store.put([
|
|
75
|
+
authors.tolkein,
|
|
76
|
+
authors.bradbury,
|
|
77
|
+
books.lotr,
|
|
78
|
+
books.farenheit,
|
|
79
|
+
authors.davidMitchellFunny,
|
|
80
|
+
authors.davidMitchellSerious,
|
|
81
|
+
books.cloudAtlas,
|
|
82
|
+
books.myLifeInComedy,
|
|
83
|
+
])
|
|
132
84
|
})
|
|
133
85
|
|
|
134
|
-
describe('
|
|
135
|
-
it('
|
|
136
|
-
|
|
137
|
-
|
|
86
|
+
describe('filtered history (QH)', () => {
|
|
87
|
+
it('[QH1] is cached per type name', () => {
|
|
88
|
+
expect(store.query.filterHistory('author')).toBe(store.query.filterHistory('author'))
|
|
89
|
+
})
|
|
138
90
|
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
store.put([newAuthor])
|
|
91
|
+
it('[QH1] contains only changes to records of the given type', () => {
|
|
92
|
+
const authorHistory = store.query.filterHistory('author')
|
|
142
93
|
|
|
143
|
-
|
|
144
|
-
expect(afterAddEpoch).toBeGreaterThan(initialEpoch)
|
|
94
|
+
authorHistory.get()
|
|
145
95
|
|
|
146
|
-
|
|
147
|
-
const newBook = Book.create({
|
|
148
|
-
title: 'New Book',
|
|
149
|
-
authorId: newAuthor.id,
|
|
150
|
-
publishedYear: 2023,
|
|
151
|
-
inStock: true,
|
|
152
|
-
rating: 3,
|
|
153
|
-
})
|
|
154
|
-
const beforeBookAdd = authorHistory.lastChangedEpoch
|
|
155
|
-
store.put([newBook])
|
|
96
|
+
let lastChangedEpoch = authorHistory.lastChangedEpoch
|
|
156
97
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
98
|
+
expect(authorHistory.getDiffSince(lastChangedEpoch - 1)).toBe(RESET_VALUE)
|
|
99
|
+
|
|
100
|
+
// updating an author changes the history
|
|
101
|
+
store.put([{ ...authors.davidMitchellFunny, age: 30 }])
|
|
160
102
|
|
|
161
|
-
it('should handle record updates in filtered history', () => {
|
|
162
|
-
const authorHistory = queries.filterHistory('author')
|
|
163
103
|
authorHistory.get()
|
|
164
104
|
|
|
165
|
-
|
|
105
|
+
expect(lastChangedEpoch).toBeLessThan(authorHistory.lastChangedEpoch)
|
|
166
106
|
|
|
167
|
-
|
|
168
|
-
|
|
107
|
+
expect(authorHistory.getDiffSince(lastChangedEpoch)).toMatchObject([
|
|
108
|
+
{ updated: { [authors.davidMitchellFunny.id]: [{ age: 23 }, { age: 30 }] } },
|
|
109
|
+
])
|
|
169
110
|
|
|
170
|
-
|
|
171
|
-
authorHistory.get()
|
|
111
|
+
lastChangedEpoch = authorHistory.lastChangedEpoch
|
|
172
112
|
|
|
173
|
-
|
|
113
|
+
// adding and removing authors shows up too
|
|
114
|
+
const newAuthor = Author.create({ name: 'Stanley Briggs' })
|
|
115
|
+
store.put([newAuthor])
|
|
116
|
+
store.remove([authors.bradbury.id])
|
|
174
117
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
118
|
+
expect(authorHistory.getDiffSince(lastChangedEpoch)).toMatchObject([
|
|
119
|
+
{
|
|
120
|
+
added: { [newAuthor.id]: { name: 'Stanley Briggs' } },
|
|
121
|
+
removed: { [authors.bradbury.id]: { name: 'Ray Bradbury' } },
|
|
122
|
+
},
|
|
123
|
+
])
|
|
181
124
|
})
|
|
182
125
|
|
|
183
|
-
it('
|
|
184
|
-
const authorHistory =
|
|
126
|
+
it('[QH3] changes to other types produce no observable change', () => {
|
|
127
|
+
const authorHistory = store.query.filterHistory('author')
|
|
185
128
|
authorHistory.get()
|
|
186
129
|
|
|
187
130
|
const lastChangedEpoch = authorHistory.lastChangedEpoch
|
|
188
131
|
|
|
189
|
-
|
|
190
|
-
|
|
132
|
+
store.put([
|
|
133
|
+
{ ...books.lotr, title: 'The Lord of the Rings Part I: The Fellowship of the Ring' },
|
|
134
|
+
])
|
|
191
135
|
|
|
192
|
-
// Access the computed to trigger update
|
|
193
136
|
authorHistory.get()
|
|
194
137
|
|
|
195
|
-
expect(lastChangedEpoch).
|
|
138
|
+
expect(authorHistory.lastChangedEpoch).toEqual(lastChangedEpoch)
|
|
139
|
+
expect(authorHistory.getDiffSince(lastChangedEpoch)).toEqual([])
|
|
140
|
+
})
|
|
196
141
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
142
|
+
it('[QH2] changes that cancel out in a window produce no change', () => {
|
|
143
|
+
const authorHistory = store.query.filterHistory('author')
|
|
144
|
+
|
|
145
|
+
const epoch = authorHistory.get()
|
|
146
|
+
const newAuthor = Author.create({ name: 'Stanley Briggs' })
|
|
147
|
+
|
|
148
|
+
store.put([newAuthor])
|
|
149
|
+
store.put([{ ...newAuthor, age: 38 }])
|
|
150
|
+
store.remove([newAuthor.id])
|
|
151
|
+
|
|
152
|
+
expect(authorHistory.get()).toEqual(epoch)
|
|
153
|
+
|
|
154
|
+
store.remove([authors.tolkein.id])
|
|
155
|
+
store.put([authors.tolkein])
|
|
156
|
+
|
|
157
|
+
expect(authorHistory.get()).toEqual(epoch)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('[QH2] update entries are removed when the record is deleted', () => {
|
|
161
|
+
const authorHistory = store.query.filterHistory('author')
|
|
162
|
+
|
|
163
|
+
authorHistory.get()
|
|
164
|
+
|
|
165
|
+
const lastChangedEpoch = authorHistory.lastChangedEpoch
|
|
166
|
+
|
|
167
|
+
store.put([{ ...authors.davidMitchellFunny, age: 38 }])
|
|
168
|
+
store.put([{ ...authors.davidMitchellFunny, age: 343 }])
|
|
169
|
+
store.remove([authors.davidMitchellFunny.id])
|
|
170
|
+
|
|
171
|
+
expect(authorHistory.getDiffSince(lastChangedEpoch)).toMatchObject([
|
|
172
|
+
{
|
|
173
|
+
// shows the original, not the updated version
|
|
174
|
+
removed: { [authors.davidMitchellFunny.id]: { age: 23 } },
|
|
175
|
+
},
|
|
176
|
+
])
|
|
203
177
|
})
|
|
204
178
|
|
|
205
|
-
it('
|
|
206
|
-
const authorHistory =
|
|
179
|
+
it('[QH2] multiple update entries collapse into one', () => {
|
|
180
|
+
const authorHistory = store.query.filterHistory('author')
|
|
181
|
+
|
|
207
182
|
authorHistory.get()
|
|
208
183
|
|
|
209
|
-
const
|
|
210
|
-
const tempAuthor = Author.create({ name: 'Temp Author' })
|
|
184
|
+
const lastChangedEpoch = authorHistory.lastChangedEpoch
|
|
211
185
|
|
|
212
|
-
|
|
213
|
-
store.put([
|
|
214
|
-
store.remove([tempAuthor.id])
|
|
186
|
+
store.put([{ ...authors.davidMitchellFunny, age: 38 }])
|
|
187
|
+
store.put([{ ...authors.davidMitchellFunny, age: 343 }])
|
|
215
188
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
189
|
+
expect(authorHistory.getDiffSince(lastChangedEpoch)).toMatchObject([
|
|
190
|
+
{
|
|
191
|
+
updated: { [authors.davidMitchellFunny.id]: [{ age: 23 }, { age: 343 }] },
|
|
192
|
+
},
|
|
193
|
+
])
|
|
219
194
|
})
|
|
220
195
|
|
|
221
|
-
it('
|
|
222
|
-
const authorHistory =
|
|
196
|
+
it('[QH2] an add followed by an update folds into the add', () => {
|
|
197
|
+
const authorHistory = store.query.filterHistory('author')
|
|
198
|
+
|
|
223
199
|
authorHistory.get()
|
|
224
200
|
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
expect(diff).not.toBe(RESET_VALUE)
|
|
238
|
-
if (diff !== RESET_VALUE) {
|
|
239
|
-
expect(diff).toHaveLength(1)
|
|
240
|
-
// Should have tempAuthor1 as added (with updated age)
|
|
241
|
-
expect(diff[0].added).toHaveProperty(tempAuthor1.id)
|
|
242
|
-
// Should not have tempAuthor2 since it was added then removed
|
|
243
|
-
expect(diff[0].added).not.toHaveProperty(tempAuthor2.id)
|
|
244
|
-
expect(diff[0].removed).not.toHaveProperty(tempAuthor2.id)
|
|
245
|
-
}
|
|
201
|
+
const lastChangedEpoch = authorHistory.lastChangedEpoch
|
|
202
|
+
|
|
203
|
+
const newAuthor = Author.create({ name: 'Stanley Briggs' })
|
|
204
|
+
|
|
205
|
+
store.put([newAuthor])
|
|
206
|
+
store.put([{ ...newAuthor, age: 38 }])
|
|
207
|
+
|
|
208
|
+
expect(authorHistory.getDiffSince(lastChangedEpoch)).toMatchObject([
|
|
209
|
+
{
|
|
210
|
+
added: { [newAuthor.id]: { age: 38 } },
|
|
211
|
+
},
|
|
212
|
+
])
|
|
246
213
|
})
|
|
247
214
|
})
|
|
248
215
|
|
|
249
|
-
describe('
|
|
250
|
-
it('
|
|
251
|
-
|
|
252
|
-
|
|
216
|
+
describe('indexes (QI)', () => {
|
|
217
|
+
it('[QI2] are cached per type and property', () => {
|
|
218
|
+
expect(store.query.index('author', 'name')).toBe(store.query.index('author', 'name'))
|
|
219
|
+
expect(store.query.index('author', 'name')).not.toBe(store.query.index('author', 'age'))
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('[QI1] map each property value to the ids of records with that value', () => {
|
|
223
|
+
const authorNameIndex = store.query.index('author', 'name')
|
|
224
|
+
|
|
225
|
+
expect(authorNameIndex.get().get('J.R.R. Tolkein')).toEqual(new Set([authors.tolkein.id]))
|
|
226
|
+
expect(authorNameIndex.get().get('David Mitchell')).toEqual(
|
|
227
|
+
new Set([authors.davidMitchellFunny.id, authors.davidMitchellSerious.id])
|
|
228
|
+
)
|
|
229
|
+
expect(authorNameIndex.get().get('Nonexistent')).toBeUndefined()
|
|
253
230
|
|
|
254
|
-
|
|
255
|
-
expect(
|
|
256
|
-
expect(
|
|
231
|
+
const bookTitleIndex = store.query.index('book', 'title')
|
|
232
|
+
expect(bookTitleIndex.get().get('Cloud Atlas')).toEqual(new Set([books.cloudAtlas.id]))
|
|
233
|
+
expect(bookTitleIndex.get().get('Lord of the Rings')).toEqual(new Set([books.lotr.id]))
|
|
257
234
|
})
|
|
258
235
|
|
|
259
|
-
it('
|
|
260
|
-
const
|
|
261
|
-
const
|
|
236
|
+
it('[QI1] records whose value is undefined are not indexed', () => {
|
|
237
|
+
const newAuthor = { ...Author.create({ name: 'No Name' }), name: undefined } as any
|
|
238
|
+
const index = store.query.index('author', 'name')
|
|
239
|
+
store.put([newAuthor])
|
|
240
|
+
expect(index.get().get(undefined)).toBeUndefined()
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('[QI3] adding records updates the index, with diffs', () => {
|
|
244
|
+
const authorNameIndex = store.query.index('author', 'name')
|
|
245
|
+
// deref to make it compute once
|
|
246
|
+
expect(authorNameIndex.get().get('J.R.R. Tolkein')).toEqual(new Set([authors.tolkein.id]))
|
|
262
247
|
|
|
248
|
+
let lastChangedEpoch = authorNameIndex.lastChangedEpoch
|
|
263
249
|
const newAuthor = Author.create({ name: 'New Author' })
|
|
264
|
-
const lastEpoch = nameIndex.lastChangedEpoch
|
|
265
250
|
|
|
266
251
|
store.put([newAuthor])
|
|
267
252
|
|
|
268
|
-
|
|
269
|
-
const updatedIndex = nameIndex.get()
|
|
270
|
-
expect(nameIndex.lastChangedEpoch).toBeGreaterThan(lastEpoch)
|
|
271
|
-
expect(updatedIndex.get('New Author')).toEqual(new Set([newAuthor.id]))
|
|
253
|
+
expect(authorNameIndex.get().get('New Author')).toEqual(new Set([newAuthor.id]))
|
|
272
254
|
|
|
273
|
-
const diff =
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
279
|
-
|
|
255
|
+
const diff = authorNameIndex.getDiffSince(lastChangedEpoch)
|
|
256
|
+
if (diff === RESET_VALUE) throw new Error('should not be reset')
|
|
257
|
+
expect(diff).toHaveLength(1)
|
|
258
|
+
expect(diff[0].get('New Author')).toEqual({
|
|
259
|
+
added: new Set([newAuthor.id]),
|
|
260
|
+
})
|
|
261
|
+
expect(diff[0].size).toBe(1)
|
|
280
262
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
263
|
+
const moreNewAuthors = [
|
|
264
|
+
Author.create({ name: 'New Author' }),
|
|
265
|
+
Author.create({ name: 'New Author' }),
|
|
266
|
+
Author.create({ name: 'New Author' }),
|
|
267
|
+
]
|
|
284
268
|
|
|
285
|
-
|
|
269
|
+
lastChangedEpoch = authorNameIndex.lastChangedEpoch
|
|
270
|
+
store.put(moreNewAuthors)
|
|
286
271
|
|
|
287
|
-
|
|
288
|
-
|
|
272
|
+
expect(authorNameIndex.get().get('New Author')).toEqual(
|
|
273
|
+
new Set([newAuthor.id, ...moreNewAuthors.map((a) => a.id)])
|
|
274
|
+
)
|
|
289
275
|
|
|
290
|
-
|
|
291
|
-
const updatedIndex = nameIndex.get()
|
|
292
|
-
expect(nameIndex.lastChangedEpoch).toBeGreaterThan(lastEpoch)
|
|
293
|
-
expect(updatedIndex.get('Isaac Asimov')).toBeUndefined()
|
|
294
|
-
expect(updatedIndex.get('Dr. Isaac Asimov')).toEqual(new Set([authors.asimov.id]))
|
|
276
|
+
const diff2 = authorNameIndex.getDiffSince(lastChangedEpoch)
|
|
295
277
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
expect(diff[0].get('Isaac Asimov')).toEqual({ removed: new Set([authors.asimov.id]) })
|
|
301
|
-
expect(diff[0].get('Dr. Isaac Asimov')).toEqual({ added: new Set([authors.asimov.id]) })
|
|
302
|
-
}
|
|
278
|
+
if (diff2 === RESET_VALUE) throw new Error('should not be reset')
|
|
279
|
+
|
|
280
|
+
expect(diff2).toHaveLength(1)
|
|
281
|
+
expect(diff2[0].get('New Author')).toEqual({ added: new Set(moreNewAuthors.map((a) => a.id)) })
|
|
303
282
|
})
|
|
304
|
-
})
|
|
305
283
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}))
|
|
284
|
+
it('[QI3] updating a record moves its id between value sets', () => {
|
|
285
|
+
const authorNameIndex = store.query.index('author', 'name')
|
|
286
|
+
|
|
287
|
+
expect(authorNameIndex.get().get('J.R.R. Tolkein')).toEqual(new Set([authors.tolkein.id]))
|
|
311
288
|
|
|
312
|
-
|
|
289
|
+
const lastChangedEpoch = authorNameIndex.lastChangedEpoch
|
|
290
|
+
|
|
291
|
+
store.put([{ ...authors.bradbury, name: 'J.R.R. Tolkein' }])
|
|
292
|
+
|
|
293
|
+
expect(authorNameIndex.get().get('J.R.R. Tolkein')).toEqual(
|
|
294
|
+
new Set([authors.tolkein.id, authors.bradbury.id])
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
const diff = authorNameIndex.getDiffSince(lastChangedEpoch)
|
|
298
|
+
if (diff === RESET_VALUE) throw new Error('should not be reset')
|
|
299
|
+
expect(diff).toHaveLength(1)
|
|
300
|
+
expect(diff[0].size).toBe(2)
|
|
301
|
+
expect(diff[0].get('J.R.R. Tolkein')).toEqual({
|
|
302
|
+
added: new Set([authors.bradbury.id]),
|
|
303
|
+
})
|
|
304
|
+
expect(diff[0].get('Ray Bradbury')).toEqual({
|
|
305
|
+
removed: new Set([authors.bradbury.id]),
|
|
306
|
+
})
|
|
313
307
|
})
|
|
314
308
|
|
|
315
|
-
it('
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
309
|
+
it('[QI3] removing records updates the index; empty value sets are dropped', () => {
|
|
310
|
+
const authorNameIndex = store.query.index('author', 'name')
|
|
311
|
+
|
|
312
|
+
expect(authorNameIndex.get().get('J.R.R. Tolkein')).toEqual(new Set([authors.tolkein.id]))
|
|
313
|
+
|
|
314
|
+
let lastChangedEpoch = authorNameIndex.lastChangedEpoch
|
|
315
|
+
|
|
316
|
+
store.remove([authors.tolkein.id])
|
|
317
|
+
|
|
318
|
+
expect(authorNameIndex.get().get('J.R.R. Tolkein')).toEqual(undefined)
|
|
319
|
+
|
|
320
|
+
const diff = authorNameIndex.getDiffSince(lastChangedEpoch)
|
|
321
|
+
if (diff === RESET_VALUE) throw new Error('should not be reset')
|
|
322
|
+
expect(diff).toHaveLength(1)
|
|
323
|
+
expect(diff[0].size).toBe(1)
|
|
324
|
+
expect(diff[0].get('J.R.R. Tolkein')).toEqual({
|
|
325
|
+
removed: new Set([authors.tolkein.id]),
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
lastChangedEpoch = authorNameIndex.lastChangedEpoch
|
|
329
|
+
store.remove([
|
|
330
|
+
authors.bradbury.id,
|
|
331
|
+
authors.davidMitchellFunny.id,
|
|
332
|
+
authors.davidMitchellSerious.id,
|
|
333
|
+
])
|
|
334
|
+
|
|
335
|
+
expect(authorNameIndex.get().get('Ray Bradbury')).toEqual(undefined)
|
|
336
|
+
expect(authorNameIndex.get().get('David Mitchell')).toEqual(undefined)
|
|
337
|
+
|
|
338
|
+
const diff2 = authorNameIndex.getDiffSince(lastChangedEpoch)
|
|
339
|
+
|
|
340
|
+
if (diff2 === RESET_VALUE) throw new Error('should not be reset')
|
|
319
341
|
|
|
320
|
-
expect(
|
|
342
|
+
expect(diff2).toHaveLength(1)
|
|
343
|
+
expect(diff2[0].size).toBe(2)
|
|
344
|
+
|
|
345
|
+
expect(diff2[0].get('Ray Bradbury')).toEqual({
|
|
346
|
+
removed: new Set([authors.bradbury.id]),
|
|
347
|
+
})
|
|
348
|
+
expect(diff2[0].get('David Mitchell')).toEqual({
|
|
349
|
+
removed: new Set([authors.davidMitchellFunny.id, authors.davidMitchellSerious.id]),
|
|
350
|
+
})
|
|
321
351
|
})
|
|
322
352
|
|
|
323
|
-
it('
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
353
|
+
it('[QI3] removals and additions for the same value combine in one diff', () => {
|
|
354
|
+
const authorNameIndex = store.query.index('author', 'name')
|
|
355
|
+
expect(authorNameIndex.get().get('J.R.R. Tolkein')).toEqual(new Set([authors.tolkein.id]))
|
|
356
|
+
|
|
357
|
+
let lastChangedEpoch = authorNameIndex.lastChangedEpoch
|
|
358
|
+
|
|
359
|
+
// do remove first
|
|
360
|
+
store.remove([authors.tolkein.id])
|
|
361
|
+
const newAuthor = Author.create({ name: 'J.R.R. Tolkein' })
|
|
362
|
+
store.put([newAuthor])
|
|
363
|
+
|
|
364
|
+
expect(authorNameIndex.get().get('J.R.R. Tolkein')).toEqual(new Set([newAuthor.id]))
|
|
365
|
+
|
|
366
|
+
const diff = authorNameIndex.getDiffSince(lastChangedEpoch)
|
|
367
|
+
|
|
368
|
+
if (diff === RESET_VALUE) throw new Error('should not be reset')
|
|
369
|
+
|
|
370
|
+
expect(diff).toHaveLength(1)
|
|
371
|
+
expect(diff[0].size).toBe(1)
|
|
327
372
|
|
|
328
|
-
expect(
|
|
373
|
+
expect(diff[0].get('J.R.R. Tolkein')).toEqual({
|
|
374
|
+
removed: new Set([authors.tolkein.id]),
|
|
375
|
+
added: new Set([newAuthor.id]),
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
lastChangedEpoch = authorNameIndex.lastChangedEpoch
|
|
379
|
+
// do updates
|
|
380
|
+
store.put([{ ...authors.davidMitchellFunny, name: 'Ray Bradbury' }])
|
|
381
|
+
store.put([{ ...authors.bradbury, name: 'David Mitchell' }])
|
|
382
|
+
|
|
383
|
+
expect(authorNameIndex.get().get('Ray Bradbury')).toEqual(
|
|
384
|
+
new Set([authors.davidMitchellFunny.id])
|
|
385
|
+
)
|
|
386
|
+
expect(authorNameIndex.get().get('David Mitchell')).toEqual(
|
|
387
|
+
new Set([authors.bradbury.id, authors.davidMitchellSerious.id])
|
|
388
|
+
)
|
|
329
389
|
|
|
330
|
-
|
|
331
|
-
store.put([{ ...authors.asimov, age: 73 }])
|
|
390
|
+
const diff2 = authorNameIndex.getDiffSince(lastChangedEpoch)
|
|
332
391
|
|
|
333
|
-
|
|
392
|
+
if (diff2 === RESET_VALUE) throw new Error('should not be reset')
|
|
393
|
+
|
|
394
|
+
expect(diff2).toHaveLength(1)
|
|
395
|
+
expect(diff2[0].size).toBe(2)
|
|
396
|
+
|
|
397
|
+
expect(diff2[0].get('Ray Bradbury')).toEqual({
|
|
398
|
+
added: new Set([authors.davidMitchellFunny.id]),
|
|
399
|
+
removed: new Set([authors.bradbury.id]),
|
|
400
|
+
})
|
|
401
|
+
expect(diff2[0].get('David Mitchell')).toEqual({
|
|
402
|
+
added: new Set([authors.bradbury.id]),
|
|
403
|
+
removed: new Set([authors.davidMitchellFunny.id]),
|
|
404
|
+
})
|
|
334
405
|
})
|
|
335
406
|
|
|
336
|
-
it('
|
|
337
|
-
const
|
|
407
|
+
it('[QI4] keeps the same map object if the indexed values did not change', () => {
|
|
408
|
+
const authorNameIndex = store.query.index('author', 'name')
|
|
338
409
|
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
}))
|
|
410
|
+
const value = authorNameIndex.get()
|
|
411
|
+
const lastChangedEpoch = authorNameIndex.lastChangedEpoch
|
|
342
412
|
|
|
343
|
-
|
|
413
|
+
// change a property the index does not cover
|
|
414
|
+
store.put([{ ...authors.tolkein, age: 23 }])
|
|
415
|
+
store.put([{ ...authors.tolkein, age: 99 }])
|
|
344
416
|
|
|
345
|
-
|
|
346
|
-
expect(
|
|
417
|
+
expect(authorNameIndex.get()).toBe(value)
|
|
418
|
+
expect(authorNameIndex.lastChangedEpoch).toBe(lastChangedEpoch)
|
|
347
419
|
})
|
|
348
420
|
|
|
349
|
-
it('
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
})
|
|
421
|
+
it('[QI5] a backslash-delimited property indexes a nested path', () => {
|
|
422
|
+
interface Doc extends BaseRecord<'doc', RecordId<Doc>> {
|
|
423
|
+
metadata: { sessionId?: string }
|
|
424
|
+
}
|
|
425
|
+
const Doc = createRecordType<Doc>('doc', { scope: 'document' })
|
|
426
|
+
const docStore = new Store({
|
|
427
|
+
props: {},
|
|
428
|
+
schema: StoreSchema.create<Doc>({ doc: Doc }),
|
|
429
|
+
})
|
|
354
430
|
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
431
|
+
const docA = Doc.create({ metadata: { sessionId: 'alpha' } })
|
|
432
|
+
const docB = Doc.create({ metadata: { sessionId: 'beta' } })
|
|
433
|
+
const docNone = Doc.create({ metadata: {} })
|
|
434
|
+
docStore.put([docA, docB, docNone])
|
|
435
|
+
|
|
436
|
+
const index = docStore.query.index('doc', 'metadata\\sessionId')
|
|
437
|
+
expect(index.get().get('alpha')).toEqual(new Set([docA.id]))
|
|
438
|
+
expect(index.get().get('beta')).toEqual(new Set([docB.id]))
|
|
439
|
+
// missing nested values are not indexed
|
|
440
|
+
expect(index.get().get(undefined)).toBeUndefined()
|
|
441
|
+
|
|
442
|
+
// incremental updates work through the nested path
|
|
443
|
+
docStore.update(docA.id, (d) => ({ ...d, metadata: { sessionId: 'beta' } }))
|
|
444
|
+
expect(index.get().get('alpha')).toBeUndefined()
|
|
445
|
+
expect(index.get().get('beta')).toEqual(new Set([docB.id, docA.id]))
|
|
359
446
|
})
|
|
360
447
|
})
|
|
361
448
|
|
|
362
|
-
describe('
|
|
363
|
-
it('
|
|
364
|
-
const
|
|
365
|
-
const books = allBooks.get()
|
|
449
|
+
describe('id queries (QQ)', () => {
|
|
450
|
+
it('[QQ1] with no query, contains all ids of the type', () => {
|
|
451
|
+
const bookQuery = store.query.ids('book')
|
|
366
452
|
|
|
367
|
-
expect(
|
|
368
|
-
|
|
453
|
+
expect(bookQuery.get()).toEqual(
|
|
454
|
+
new Set([books.cloudAtlas.id, books.farenheit.id, books.lotr.id, books.myLifeInComedy.id])
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
const authorQuery = store.query.ids('author')
|
|
458
|
+
|
|
459
|
+
expect(authorQuery.get()).toEqual(
|
|
460
|
+
new Set([
|
|
461
|
+
authors.bradbury.id,
|
|
462
|
+
authors.davidMitchellFunny.id,
|
|
463
|
+
authors.davidMitchellSerious.id,
|
|
464
|
+
authors.tolkein.id,
|
|
465
|
+
])
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
const newAuthor = Author.create({ name: 'J.R.R. Tolkein' })
|
|
469
|
+
const newBook = Book.create({ title: 'The Hobbit', authorId: newAuthor.id })
|
|
470
|
+
store.put([newAuthor, newBook])
|
|
471
|
+
|
|
472
|
+
expect(bookQuery.get()).toContain(newBook.id)
|
|
473
|
+
expect(authorQuery.get()).toContain(newAuthor.id)
|
|
369
474
|
})
|
|
370
475
|
|
|
371
|
-
it('
|
|
372
|
-
const
|
|
373
|
-
|
|
476
|
+
it('[QQ1] filters ids by the query expression', () => {
|
|
477
|
+
const jrr = store.query.ids('author', () => ({
|
|
478
|
+
name: { eq: 'J.R.R. Tolkein' },
|
|
374
479
|
}))
|
|
480
|
+
expect(jrr.get()).toEqual(new Set([authors.tolkein.id]))
|
|
375
481
|
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
482
|
+
const mitchell = store.query.ids('author', () => ({
|
|
483
|
+
name: { eq: 'David Mitchell' },
|
|
484
|
+
}))
|
|
485
|
+
expect(mitchell.get()).toEqual(
|
|
486
|
+
new Set([authors.davidMitchellFunny.id, authors.davidMitchellSerious.id])
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
store.put([{ ...authors.davidMitchellFunny, age: 30 }])
|
|
490
|
+
const mitchell30 = store.query.ids('author', () => ({
|
|
491
|
+
name: { eq: 'David Mitchell' },
|
|
492
|
+
age: { eq: 30 },
|
|
493
|
+
}))
|
|
494
|
+
expect(mitchell30.get()).toEqual(new Set([authors.davidMitchellFunny.id]))
|
|
379
495
|
})
|
|
380
496
|
|
|
381
|
-
it('
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
rating: { eq: 5 },
|
|
497
|
+
it('[QQ1] emits collection diffs as records change', () => {
|
|
498
|
+
const mitchell = store.query.ids('author', () => ({
|
|
499
|
+
name: { eq: 'David Mitchell' },
|
|
385
500
|
}))
|
|
386
501
|
|
|
387
|
-
|
|
388
|
-
|
|
502
|
+
mitchell.get()
|
|
503
|
+
const lastEpoch = mitchell.lastChangedEpoch
|
|
504
|
+
|
|
505
|
+
const newAuthor = Author.create({ name: 'David Mitchell' })
|
|
506
|
+
store.put([newAuthor])
|
|
507
|
+
store.remove([authors.davidMitchellFunny.id])
|
|
508
|
+
|
|
509
|
+
const diff = mitchell.getDiffSince(lastEpoch)
|
|
510
|
+
if (diff === RESET_VALUE) throw new Error('should not be reset')
|
|
511
|
+
expect(diff).toHaveLength(1)
|
|
512
|
+
expect(diff[0]).toEqual({
|
|
513
|
+
added: new Set([newAuthor.id]),
|
|
514
|
+
removed: new Set([authors.davidMitchellFunny.id]),
|
|
515
|
+
})
|
|
389
516
|
})
|
|
390
517
|
|
|
391
|
-
it('
|
|
392
|
-
|
|
393
|
-
|
|
518
|
+
it('[QQ2] a reactive query expression rebuilds with a correct diff when it changes', () => {
|
|
519
|
+
store.put([{ ...authors.davidMitchellFunny, age: 30 }])
|
|
520
|
+
|
|
521
|
+
const currentAuthor = atom('currentAuthor', 'David Mitchell')
|
|
522
|
+
const currentAge = atom('currentAge', 30)
|
|
523
|
+
|
|
524
|
+
const mitchell30 = store.query.ids('author', () => ({
|
|
525
|
+
name: { eq: currentAuthor.get() },
|
|
526
|
+
age: { eq: currentAge.get() },
|
|
527
|
+
}))
|
|
528
|
+
|
|
529
|
+
expect(mitchell30.get()).toEqual(new Set([authors.davidMitchellFunny.id]))
|
|
530
|
+
|
|
531
|
+
let lastChangedEpoch = mitchell30.lastChangedEpoch
|
|
532
|
+
currentAge.set(23)
|
|
533
|
+
|
|
534
|
+
expect(mitchell30.get()).toEqual(new Set([authors.davidMitchellSerious.id]))
|
|
535
|
+
|
|
536
|
+
const diff = mitchell30.getDiffSince(lastChangedEpoch)
|
|
537
|
+
|
|
538
|
+
if (diff === RESET_VALUE) throw new Error('should not be reset')
|
|
394
539
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
inStock: true,
|
|
400
|
-
rating: 3,
|
|
540
|
+
expect(diff).toHaveLength(1)
|
|
541
|
+
expect(diff[0]).toEqual({
|
|
542
|
+
added: new Set([authors.davidMitchellSerious.id]),
|
|
543
|
+
removed: new Set([authors.davidMitchellFunny.id]),
|
|
401
544
|
})
|
|
402
545
|
|
|
403
|
-
|
|
546
|
+
currentAuthor.set('J.R.R. Tolkein')
|
|
404
547
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
548
|
+
lastChangedEpoch = mitchell30.lastChangedEpoch
|
|
549
|
+
|
|
550
|
+
expect(mitchell30.get()).toEqual(new Set([authors.tolkein.id]))
|
|
408
551
|
|
|
409
|
-
|
|
410
|
-
const allBooks = queries.records('book')
|
|
411
|
-
const initialCount = allBooks.get().length
|
|
552
|
+
const diff2 = mitchell30.getDiffSince(lastChangedEpoch)
|
|
412
553
|
|
|
413
|
-
|
|
554
|
+
if (diff2 === RESET_VALUE) throw new Error('should not be reset')
|
|
414
555
|
|
|
415
|
-
expect(
|
|
416
|
-
expect(
|
|
556
|
+
expect(diff2).toHaveLength(1)
|
|
557
|
+
expect(diff2[0]).toEqual({
|
|
558
|
+
added: new Set([authors.tolkein.id]),
|
|
559
|
+
removed: new Set([authors.davidMitchellSerious.id]),
|
|
560
|
+
})
|
|
417
561
|
})
|
|
418
562
|
|
|
419
|
-
it('
|
|
420
|
-
|
|
421
|
-
|
|
563
|
+
it('[QQ1] supports not-equals matches', () => {
|
|
564
|
+
store.put([{ ...authors.davidMitchellFunny, age: 30 }])
|
|
565
|
+
const mitchell = store.query.ids('author', () => ({
|
|
566
|
+
name: { neq: 'David Mitchell' },
|
|
422
567
|
}))
|
|
423
568
|
|
|
424
|
-
|
|
425
|
-
const initialCount = initialBooks.length
|
|
569
|
+
expect(mitchell.get()).toEqual(new Set([authors.tolkein.id, authors.bradbury.id]))
|
|
426
570
|
|
|
427
|
-
|
|
428
|
-
|
|
571
|
+
const ageNot23 = store.query.ids('author', () => ({
|
|
572
|
+
age: { neq: 23 },
|
|
573
|
+
}))
|
|
429
574
|
|
|
430
|
-
|
|
431
|
-
expect(updatedBooks).toHaveLength(initialCount - 1)
|
|
432
|
-
expect(updatedBooks.some((book) => book.id === books.foundation.id)).toBe(false)
|
|
575
|
+
expect(ageNot23.get()).toEqual(new Set([authors.davidMitchellFunny.id]))
|
|
433
576
|
})
|
|
434
577
|
|
|
435
|
-
it('
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
category: { eq: targetCategory.get() },
|
|
578
|
+
it('[QQ1] updates as records are added, removed, and updated', () => {
|
|
579
|
+
const mitchell = store.query.ids('author', () => ({
|
|
580
|
+
name: { eq: 'David Mitchell' },
|
|
581
|
+
age: { neq: 30 },
|
|
440
582
|
}))
|
|
441
583
|
|
|
442
|
-
|
|
443
|
-
|
|
584
|
+
expect(mitchell.get()).toEqual(
|
|
585
|
+
new Set([authors.davidMitchellFunny.id, authors.davidMitchellSerious.id])
|
|
586
|
+
)
|
|
444
587
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
expect(cyberpunkBooks.every((book) => book.category === 'cyberpunk')).toBe(true)
|
|
448
|
-
expect(cyberpunkBooks).toHaveLength(1)
|
|
449
|
-
expect(cyberpunkBooks[0]).toEqual(books.neuromancer)
|
|
450
|
-
})
|
|
451
|
-
})
|
|
588
|
+
store.put([{ ...authors.davidMitchellFunny, age: 30 }])
|
|
589
|
+
expect(mitchell.get()).toEqual(new Set([authors.davidMitchellSerious.id]))
|
|
452
590
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
591
|
+
store.put([{ ...authors.davidMitchellFunny, age: 23 }])
|
|
592
|
+
expect(mitchell.get()).toEqual(
|
|
593
|
+
new Set([authors.davidMitchellSerious.id, authors.davidMitchellFunny.id])
|
|
594
|
+
)
|
|
457
595
|
|
|
458
|
-
|
|
459
|
-
expect(
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
596
|
+
store.remove([authors.davidMitchellFunny.id])
|
|
597
|
+
expect(mitchell.get()).toEqual(new Set([authors.davidMitchellSerious.id]))
|
|
598
|
+
|
|
599
|
+
const newAuthor = Author.create({ name: 'David Mitchell' })
|
|
600
|
+
store.put([newAuthor])
|
|
601
|
+
expect(mitchell.get()).toEqual(new Set([authors.davidMitchellSerious.id, newAuthor.id]))
|
|
463
602
|
})
|
|
464
603
|
|
|
465
|
-
it('
|
|
466
|
-
const
|
|
467
|
-
|
|
604
|
+
it('[QQ3] does not change if unrelated records are added, updated, or removed', () => {
|
|
605
|
+
const mitchell = store.query.ids('author', () => ({
|
|
606
|
+
name: { eq: 'David Mitchell' },
|
|
468
607
|
}))
|
|
469
608
|
|
|
470
|
-
|
|
471
|
-
|
|
609
|
+
expect(mitchell.get()).toEqual(
|
|
610
|
+
new Set([authors.davidMitchellFunny.id, authors.davidMitchellSerious.id])
|
|
611
|
+
)
|
|
472
612
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
613
|
+
const lastChangedEpoch = mitchell.lastChangedEpoch
|
|
614
|
+
|
|
615
|
+
const newAuthor = Author.create({ name: 'William Shakespeare' })
|
|
616
|
+
store.put([newAuthor])
|
|
617
|
+
|
|
618
|
+
mitchell.get()
|
|
619
|
+
expect(mitchell.lastChangedEpoch).toEqual(lastChangedEpoch)
|
|
620
|
+
|
|
621
|
+
store.remove([authors.tolkein.id])
|
|
622
|
+
|
|
623
|
+
mitchell.get()
|
|
624
|
+
expect(mitchell.lastChangedEpoch).toEqual(lastChangedEpoch)
|
|
478
625
|
})
|
|
479
626
|
|
|
480
|
-
it('
|
|
481
|
-
const
|
|
482
|
-
|
|
627
|
+
it('[QQ3] does not change if matching records change in irrelevant ways', () => {
|
|
628
|
+
const mitchell = store.query.ids('author', () => ({
|
|
629
|
+
name: { eq: 'David Mitchell' },
|
|
483
630
|
}))
|
|
484
631
|
|
|
485
|
-
|
|
486
|
-
|
|
632
|
+
expect(mitchell.get()).toEqual(
|
|
633
|
+
new Set([authors.davidMitchellFunny.id, authors.davidMitchellSerious.id])
|
|
634
|
+
)
|
|
487
635
|
|
|
488
|
-
|
|
489
|
-
const newBook = Book.create({
|
|
490
|
-
title: 'New In-Stock Book',
|
|
491
|
-
authorId: authors.asimov.id,
|
|
492
|
-
publishedYear: 2023,
|
|
493
|
-
inStock: true,
|
|
494
|
-
rating: 4,
|
|
495
|
-
})
|
|
496
|
-
store.put([newBook])
|
|
636
|
+
const lastChangedEpoch = mitchell.lastChangedEpoch
|
|
497
637
|
|
|
498
|
-
|
|
499
|
-
expect(updatedIds.has(newBook.id)).toBe(true)
|
|
638
|
+
store.put([{ ...authors.davidMitchellFunny, age: 30 }])
|
|
500
639
|
|
|
501
|
-
|
|
502
|
-
expect(
|
|
503
|
-
if (diff !== RESET_VALUE) {
|
|
504
|
-
expect(diff).toHaveLength(1)
|
|
505
|
-
expect(diff[0].added?.has(newBook.id)).toBe(true)
|
|
506
|
-
}
|
|
507
|
-
})
|
|
640
|
+
mitchell.get()
|
|
641
|
+
expect(mitchell.lastChangedEpoch).toEqual(lastChangedEpoch)
|
|
508
642
|
|
|
509
|
-
|
|
510
|
-
|
|
643
|
+
// a change that does affect the query, just to check
|
|
644
|
+
store.put([{ ...authors.davidMitchellFunny, name: 'steve' }])
|
|
511
645
|
|
|
512
|
-
|
|
513
|
-
|
|
646
|
+
mitchell.get()
|
|
647
|
+
expect(mitchell.lastChangedEpoch).toBeGreaterThan(lastChangedEpoch)
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
it('[QQ3] round trips (remove/add-back, add/remove) leave the set unchanged', () => {
|
|
651
|
+
const mitchell = store.query.ids('author', () => ({
|
|
652
|
+
name: { eq: 'David Mitchell' },
|
|
514
653
|
}))
|
|
515
654
|
|
|
516
|
-
|
|
517
|
-
|
|
655
|
+
expect(mitchell.get()).toEqual(
|
|
656
|
+
new Set([authors.davidMitchellFunny.id, authors.davidMitchellSerious.id])
|
|
657
|
+
)
|
|
518
658
|
|
|
519
|
-
|
|
520
|
-
const lastEpoch = query.lastChangedEpoch
|
|
521
|
-
targetYear.set(1970)
|
|
659
|
+
const lastChangedEpoch = mitchell.lastChangedEpoch
|
|
522
660
|
|
|
523
|
-
|
|
524
|
-
|
|
661
|
+
store.remove([authors.davidMitchellFunny.id])
|
|
662
|
+
store.put([authors.davidMitchellFunny])
|
|
525
663
|
|
|
526
|
-
|
|
527
|
-
expect(
|
|
664
|
+
mitchell.get()
|
|
665
|
+
expect(mitchell.lastChangedEpoch).toEqual(lastChangedEpoch)
|
|
528
666
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
667
|
+
const newMitchell = Author.create({ name: 'David Mitchell' })
|
|
668
|
+
store.put([newMitchell])
|
|
669
|
+
store.remove([newMitchell.id])
|
|
670
|
+
|
|
671
|
+
mitchell.get()
|
|
672
|
+
expect(mitchell.lastChangedEpoch).toEqual(lastChangedEpoch)
|
|
533
673
|
})
|
|
674
|
+
})
|
|
675
|
+
|
|
676
|
+
const bookComparator = (a: Book, b: Book) => a.title.localeCompare(b.title)
|
|
677
|
+
|
|
678
|
+
describe('record queries (QQ)', () => {
|
|
679
|
+
it('[QQ4] records() returns all matching records', () => {
|
|
680
|
+
const allBooks = store.query.records('book')
|
|
681
|
+
|
|
682
|
+
expect(allBooks.get().sort(bookComparator)).toEqual(
|
|
683
|
+
[books.cloudAtlas, books.farenheit, books.lotr, books.myLifeInComedy].sort(bookComparator)
|
|
684
|
+
)
|
|
534
685
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
inStock: { eq: true },
|
|
686
|
+
const farenheit = store.query.records('book', () => ({
|
|
687
|
+
title: { eq: 'Farenheit 451' },
|
|
538
688
|
}))
|
|
689
|
+
expect(farenheit.get()).toEqual([books.farenheit])
|
|
539
690
|
|
|
540
|
-
|
|
541
|
-
|
|
691
|
+
const funnyGuide = Book.create({
|
|
692
|
+
title: 'How to be a funny man',
|
|
693
|
+
authorId: authors.davidMitchellFunny.id,
|
|
694
|
+
})
|
|
695
|
+
store.put([funnyGuide])
|
|
696
|
+
const mitchell = store.query.records('book', () => ({
|
|
697
|
+
authorId: { eq: authors.davidMitchellFunny.id },
|
|
698
|
+
}))
|
|
699
|
+
expect(mitchell.get().sort(bookComparator)).toEqual(
|
|
700
|
+
[books.myLifeInComedy, funnyGuide].sort(bookComparator)
|
|
701
|
+
)
|
|
702
|
+
})
|
|
542
703
|
|
|
543
|
-
|
|
544
|
-
store.
|
|
704
|
+
it('[QQ4] records() keeps the same array when the members did not change', () => {
|
|
705
|
+
const allBooks = store.query.records('book')
|
|
706
|
+
const value = allBooks.get()
|
|
545
707
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
expect(diff).toHaveLength(1)
|
|
550
|
-
expect(diff[0].added?.has(books.dune.id)).toBe(true)
|
|
551
|
-
}
|
|
708
|
+
// updating an author does not change the book array members
|
|
709
|
+
store.put([{ ...authors.tolkein, age: 50 }])
|
|
710
|
+
expect(allBooks.get()).toBe(value)
|
|
552
711
|
})
|
|
553
|
-
})
|
|
554
712
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
})
|
|
713
|
+
it('[QQ4] records() supports reactive query expressions', () => {
|
|
714
|
+
const currentAuthor = atom('currentAuthor', authors.davidMitchellFunny.id)
|
|
715
|
+
const booksQuery = store.query.records('book', () => ({
|
|
716
|
+
authorId: { eq: currentAuthor.get() },
|
|
717
|
+
}))
|
|
718
|
+
|
|
719
|
+
expect(booksQuery.get()).toEqual([books.myLifeInComedy])
|
|
720
|
+
|
|
721
|
+
currentAuthor.set(authors.tolkein.id)
|
|
560
722
|
|
|
561
|
-
expect(
|
|
562
|
-
expect(result.every((author) => author.isActive === true)).toBe(true)
|
|
723
|
+
expect(booksQuery.get()).toEqual([books.lotr])
|
|
563
724
|
})
|
|
564
725
|
|
|
565
|
-
it('
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
726
|
+
it('[QQ4] record() returns one matching record or undefined', () => {
|
|
727
|
+
const farenheit = store.query.record('book', () => ({
|
|
728
|
+
title: { eq: 'Farenheit 451' },
|
|
729
|
+
}))
|
|
730
|
+
expect(farenheit.get()).toEqual(books.farenheit)
|
|
570
731
|
|
|
571
|
-
|
|
572
|
-
|
|
732
|
+
const nonexistent = store.query.record('book', () => ({
|
|
733
|
+
title: { eq: 'No Such Book' },
|
|
734
|
+
}))
|
|
735
|
+
expect(nonexistent.get()).toBeUndefined()
|
|
573
736
|
})
|
|
574
|
-
})
|
|
575
737
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
return sciFiBooks.size
|
|
582
|
-
})
|
|
738
|
+
it('[QQ4] record() updates reactively', () => {
|
|
739
|
+
const tolkein = store.query.record('author', () => ({
|
|
740
|
+
name: { eq: 'J.R.R. Tolkein' },
|
|
741
|
+
}))
|
|
742
|
+
expect(tolkein.get()?.age).toBe(23)
|
|
583
743
|
|
|
584
|
-
|
|
585
|
-
expect(
|
|
586
|
-
|
|
587
|
-
// Add a sci-fi book
|
|
588
|
-
const newSciFiBook = Book.create({
|
|
589
|
-
title: 'New Sci-Fi',
|
|
590
|
-
authorId: authors.asimov.id,
|
|
591
|
-
publishedYear: 2023,
|
|
592
|
-
inStock: true,
|
|
593
|
-
rating: 4,
|
|
594
|
-
category: 'sci-fi',
|
|
595
|
-
})
|
|
596
|
-
store.put([newSciFiBook])
|
|
744
|
+
store.put([{ ...authors.tolkein, age: 81 }])
|
|
745
|
+
expect(tolkein.get()?.age).toBe(81)
|
|
597
746
|
|
|
598
|
-
|
|
747
|
+
store.remove([authors.tolkein.id])
|
|
748
|
+
expect(tolkein.get()).toBeUndefined()
|
|
599
749
|
})
|
|
600
750
|
|
|
601
|
-
it('
|
|
602
|
-
const
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
for (const [authorId, bookIds] of authorIndex.get()) {
|
|
607
|
-
counts.set(authorId, bookIds.size)
|
|
608
|
-
}
|
|
751
|
+
it('[QQ5] exec runs a one-shot, non-reactive query', () => {
|
|
752
|
+
const mitchells = store.query.exec('author', { name: { eq: 'David Mitchell' } })
|
|
753
|
+
expect(new Set(mitchells)).toEqual(
|
|
754
|
+
new Set([authors.davidMitchellFunny, authors.davidMitchellSerious])
|
|
755
|
+
)
|
|
609
756
|
|
|
610
|
-
|
|
611
|
-
})
|
|
757
|
+
// not reactive: subsequent store changes don't affect the returned array
|
|
758
|
+
store.put([Author.create({ name: 'David Mitchell' })])
|
|
759
|
+
expect(mitchells).toHaveLength(2)
|
|
760
|
+
})
|
|
612
761
|
|
|
613
|
-
|
|
762
|
+
it('[QQ5] exec returns the shared empty array when nothing matches', () => {
|
|
763
|
+
expect(store.query.exec('author', { name: { eq: 'No Such Author' } })).toBe(EMPTY_ARRAY)
|
|
764
|
+
})
|
|
765
|
+
})
|
|
614
766
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
inStock: true,
|
|
621
|
-
rating: 4,
|
|
767
|
+
describe('integration with reactive state (QI, QQ)', () => {
|
|
768
|
+
it('[QI3] computed values see index updates', () => {
|
|
769
|
+
const tolkeinBookCount = computed('tolkein-books', () => {
|
|
770
|
+
const byAuthor = store.query.index('book', 'authorId')
|
|
771
|
+
return byAuthor.get().get(authors.tolkein.id)?.size ?? 0
|
|
622
772
|
})
|
|
623
|
-
store.put([newAsimovBook])
|
|
624
773
|
|
|
625
|
-
expect(
|
|
774
|
+
expect(tolkeinBookCount.get()).toBe(1)
|
|
775
|
+
|
|
776
|
+
store.put([Book.create({ title: 'The Hobbit', authorId: authors.tolkein.id })])
|
|
777
|
+
expect(tolkeinBookCount.get()).toBe(2)
|
|
626
778
|
})
|
|
627
779
|
})
|