@tldraw/store 4.1.0-canary.af5f4bce7236 → 4.1.0-canary.b34d5b101192

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 +1880 -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 +1880 -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 +144 -9
  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
@@ -16,30 +16,92 @@ import { RecordsDiff } from './RecordsDiff'
16
16
  import { diffSets } from './setUtils'
17
17
  import { CollectionDiff } from './Store'
18
18
 
19
- /** @public */
19
+ /**
20
+ * A type representing the diff of changes to a reactive store index.
21
+ * Maps property values to the collection differences for record IDs that have that property value.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * // For an index on book titles, the diff might look like:
26
+ * const titleIndexDiff: RSIndexDiff<Book, 'title'> = new Map([
27
+ * ['The Lathe of Heaven', { added: new Set(['book:1']), removed: new Set() }],
28
+ * ['Animal Farm', { added: new Set(), removed: new Set(['book:2']) }]
29
+ * ])
30
+ * ```
31
+ *
32
+ * @public
33
+ */
20
34
  export type RSIndexDiff<
21
35
  R extends UnknownRecord,
22
36
  Property extends string & keyof R = string & keyof R,
23
37
  > = Map<R[Property], CollectionDiff<IdOf<R>>>
24
38
 
25
- /** @public */
39
+ /**
40
+ * A type representing a reactive store index as a map from property values to sets of record IDs.
41
+ * This is used to efficiently look up records by a specific property value.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * // Index mapping book titles to the IDs of books with that title
46
+ * const titleIndex: RSIndexMap<Book, 'title'> = new Map([
47
+ * ['The Lathe of Heaven', new Set(['book:1'])],
48
+ * ['Animal Farm', new Set(['book:2', 'book:3'])]
49
+ * ])
50
+ * ```
51
+ *
52
+ * @public
53
+ */
26
54
  export type RSIndexMap<
27
55
  R extends UnknownRecord,
28
56
  Property extends string & keyof R = string & keyof R,
29
57
  > = Map<R[Property], Set<IdOf<R>>>
30
58
 
31
- /** @public */
59
+ /**
60
+ * A reactive computed index that provides efficient lookups of records by property values.
61
+ * Returns a computed value containing an RSIndexMap with diffs for change tracking.
62
+ *
63
+ * @example
64
+ * ```ts
65
+ * // Create an index on book authors
66
+ * const authorIndex: RSIndex<Book, 'authorId'> = store.query.index('book', 'authorId')
67
+ *
68
+ * // Get all books by a specific author
69
+ * const leguinBooks = authorIndex.get().get('author:leguin')
70
+ * ```
71
+ *
72
+ * @public
73
+ */
32
74
  export type RSIndex<
33
75
  R extends UnknownRecord,
34
76
  Property extends string & keyof R = string & keyof R,
35
77
  > = Computed<RSIndexMap<R, Property>, RSIndexDiff<R, Property>>
36
78
 
37
79
  /**
38
- * A class that provides a 'namespace' for the various kinds of indexes one may wish to derive from
39
- * the record store.
80
+ * A class that provides reactive querying capabilities for a record store.
81
+ * Offers methods to create indexes, filter records, and perform efficient lookups with automatic cache management.
82
+ * All queries are reactive and will automatically update when the underlying store data changes.
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * // Create a store with books
87
+ * const store = new Store({ schema: StoreSchema.create({ book: Book, author: Author }) })
88
+ *
89
+ * // Get reactive queries for books
90
+ * const booksByAuthor = store.query.index('book', 'authorId')
91
+ * const inStockBooks = store.query.records('book', () => ({ inStock: { eq: true } }))
92
+ * ```
93
+ *
40
94
  * @public
41
95
  */
42
96
  export class StoreQueries<R extends UnknownRecord> {
97
+ /**
98
+ * Creates a new StoreQueries instance.
99
+ *
100
+ * recordMap - The atom map containing all records in the store
101
+ * history - The atom tracking the store's change history with diffs
102
+ *
103
+ * @internal
104
+ */
43
105
  constructor(
44
106
  private readonly recordMap: AtomMap<IdOf<R>, R>,
45
107
  private readonly history: Atom<number, RecordsDiff<R>>
@@ -60,10 +122,25 @@ export class StoreQueries<R extends UnknownRecord> {
60
122
  private historyCache = new Map<string, Computed<number, RecordsDiff<R>>>()
61
123
 
62
124
  /**
63
- * Create a derivation that contains the history for a given type
125
+ * Creates a reactive computed that tracks the change history for records of a specific type.
126
+ * The returned computed provides incremental diffs showing what records of the given type
127
+ * have been added, updated, or removed.
128
+ *
129
+ * @param typeName - The type name to filter the history by
130
+ * @returns A computed value containing the current epoch and diffs of changes for the specified type
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * // Track changes to book records only
135
+ * const bookHistory = store.query.filterHistory('book')
136
+ *
137
+ * // React to book changes
138
+ * react('book-changes', () => {
139
+ * const currentEpoch = bookHistory.get()
140
+ * console.log('Books updated at epoch:', currentEpoch)
141
+ * })
142
+ * ```
64
143
  *
65
- * @param typeName - The name of the type to filter by.
66
- * @returns A derivation that returns the ids of all records of the given type.
67
144
  * @public
68
145
  */
69
146
  public filterHistory<TypeName extends R['typeName']>(
@@ -156,10 +233,28 @@ export class StoreQueries<R extends UnknownRecord> {
156
233
  }
157
234
 
158
235
  /**
159
- * Create a derivation that returns an index on a property for the given type.
236
+ * Creates a reactive index that maps property values to sets of record IDs for efficient lookups.
237
+ * The index automatically updates when records are added, updated, or removed, and results are cached
238
+ * for performance.
239
+ *
240
+ * @param typeName - The type name of records to index
241
+ * @param property - The property name to index by
242
+ * @returns A reactive computed containing the index map with change diffs
243
+ *
244
+ * @example
245
+ * ```ts
246
+ * // Create an index of books by author ID
247
+ * const booksByAuthor = store.query.index('book', 'authorId')
248
+ *
249
+ * // Get all books by a specific author
250
+ * const authorBooks = booksByAuthor.get().get('author:leguin')
251
+ * console.log(authorBooks) // Set<RecordId<Book>>
252
+ *
253
+ * // Index by title for quick title lookups
254
+ * const booksByTitle = store.query.index('book', 'title')
255
+ * const booksLatheOfHeaven = booksByTitle.get().get('The Lathe of Heaven')
256
+ * ```
160
257
  *
161
- * @param typeName - The name of the type.
162
- * @param property - The name of the property.
163
258
  * @public
164
259
  */
165
260
  public index<
@@ -180,10 +275,13 @@ export class StoreQueries<R extends UnknownRecord> {
180
275
  }
181
276
 
182
277
  /**
183
- * Create a derivation that returns an index on a property for the given type.
278
+ * Creates a new index without checking the cache. This method performs the actual work
279
+ * of building the reactive index computation that tracks property values to record ID sets.
280
+ *
281
+ * @param typeName - The type name of records to index
282
+ * @param property - The property name to index by
283
+ * @returns A reactive computed containing the index map with change diffs
184
284
  *
185
- * @param typeName - The name of the type?.
186
- * @param property - The name of the property?.
187
285
  * @internal
188
286
  */
189
287
  __uncached_createIndex<
@@ -293,13 +391,26 @@ export class StoreQueries<R extends UnknownRecord> {
293
391
  }
294
392
 
295
393
  /**
296
- * Create a derivation that will return a signle record matching the given query.
394
+ * Creates a reactive query that returns the first record matching the given query criteria.
395
+ * Returns undefined if no matching record is found. The query automatically updates
396
+ * when records change.
397
+ *
398
+ * @param typeName - The type name of records to query
399
+ * @param queryCreator - Function that returns the query expression object to match against
400
+ * @param name - Optional name for the query computation (used for debugging)
401
+ * @returns A computed value containing the first matching record or undefined
402
+ *
403
+ * @example
404
+ * ```ts
405
+ * // Find the first book with a specific title
406
+ * const bookLatheOfHeaven = store.query.record('book', () => ({ title: { eq: 'The Lathe of Heaven' } }))
407
+ * console.log(bookLatheOfHeaven.get()?.title) // 'The Lathe of Heaven' or undefined
297
408
  *
298
- * It will return undefined if there is no matching record
409
+ * // Find any book in stock
410
+ * const anyInStockBook = store.query.record('book', () => ({ inStock: { eq: true } }))
411
+ * ```
299
412
  *
300
- * @param typeName - The name of the type?
301
- * @param queryCreator - A function that returns the query expression.
302
- * @param name - (optional) The name of the query.
413
+ * @public
303
414
  */
304
415
  record<TypeName extends R['typeName']>(
305
416
  typeName: TypeName,
@@ -318,11 +429,28 @@ export class StoreQueries<R extends UnknownRecord> {
318
429
  }
319
430
 
320
431
  /**
321
- * Create a derivation that will return an array of records matching the given query
432
+ * Creates a reactive query that returns an array of all records matching the given query criteria.
433
+ * The array automatically updates when records are added, updated, or removed.
434
+ *
435
+ * @param typeName - The type name of records to query
436
+ * @param queryCreator - Function that returns the query expression object to match against
437
+ * @param name - Optional name for the query computation (used for debugging)
438
+ * @returns A computed value containing an array of all matching records
439
+ *
440
+ * @example
441
+ * ```ts
442
+ * // Get all books in stock
443
+ * const inStockBooks = store.query.records('book', () => ({ inStock: { eq: true } }))
444
+ * console.log(inStockBooks.get()) // Book[]
322
445
  *
323
- * @param typeName - The name of the type?
324
- * @param queryCreator - A function that returns the query expression.
325
- * @param name - (optinal) The name of the query.
446
+ * // Get all books by a specific author
447
+ * const leguinBooks = store.query.records('book', () => ({ authorId: { eq: 'author:leguin' } }))
448
+ *
449
+ * // Get all books (no filter)
450
+ * const allBooks = store.query.records('book')
451
+ * ```
452
+ *
453
+ * @public
326
454
  */
327
455
  records<TypeName extends R['typeName']>(
328
456
  typeName: TypeName,
@@ -344,11 +472,29 @@ export class StoreQueries<R extends UnknownRecord> {
344
472
  }
345
473
 
346
474
  /**
347
- * Create a derivation that will return the ids of all records of the given type.
475
+ * Creates a reactive query that returns a set of record IDs matching the given query criteria.
476
+ * This is more efficient than `records()` when you only need the IDs and not the full record objects.
477
+ * The set automatically updates with collection diffs when records change.
478
+ *
479
+ * @param typeName - The type name of records to query
480
+ * @param queryCreator - Function that returns the query expression object to match against
481
+ * @param name - Optional name for the query computation (used for debugging)
482
+ * @returns A computed value containing a set of matching record IDs with collection diffs
483
+ *
484
+ * @example
485
+ * ```ts
486
+ * // Get IDs of all books in stock
487
+ * const inStockBookIds = store.query.ids('book', () => ({ inStock: { eq: true } }))
488
+ * console.log(inStockBookIds.get()) // Set<RecordId<Book>>
348
489
  *
349
- * @param typeName - The name of the type.
350
- * @param queryCreator - A function that returns the query expression.
351
- * @param name - (optinal) The name of the query.
490
+ * // Get all book IDs (no filter)
491
+ * const allBookIds = store.query.ids('book')
492
+ *
493
+ * // Use with other queries for efficient lookups
494
+ * const authorBookIds = store.query.ids('book', () => ({ authorId: { eq: 'author:leguin' } }))
495
+ * ```
496
+ *
497
+ * @public
352
498
  */
353
499
  ids<TypeName extends R['typeName']>(
354
500
  typeName: TypeName,
@@ -446,6 +592,27 @@ export class StoreQueries<R extends UnknownRecord> {
446
592
  )
447
593
  }
448
594
 
595
+ /**
596
+ * Executes a one-time query against the current store state and returns matching records.
597
+ * This is a non-reactive query that returns results immediately without creating a computed value.
598
+ * Use this when you need a snapshot of data at a specific point in time.
599
+ *
600
+ * @param typeName - The type name of records to query
601
+ * @param query - The query expression object to match against
602
+ * @returns An array of records that match the query at the current moment
603
+ *
604
+ * @example
605
+ * ```ts
606
+ * // Get current in-stock books (non-reactive)
607
+ * const currentInStockBooks = store.query.exec('book', { inStock: { eq: true } })
608
+ * console.log(currentInStockBooks) // Book[]
609
+ *
610
+ * // Unlike records(), this won't update when the data changes
611
+ * const staticBookList = store.query.exec('book', { authorId: { eq: 'author:leguin' } })
612
+ * ```
613
+ *
614
+ * @public
615
+ */
449
616
  exec<TypeName extends R['typeName']>(
450
617
  typeName: TypeName,
451
618
  query: QueryExpression<Extract<R, { typeName: TypeName }>>
@@ -0,0 +1,226 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import { BaseRecord, RecordId } from './BaseRecord'
3
+ import { createRecordType } from './RecordType'
4
+ import { Store } from './Store'
5
+ import { StoreSchema } from './StoreSchema'
6
+ import { createMigrationSequence } from './migrate'
7
+
8
+ // Test record types
9
+ interface Book extends BaseRecord<'book', RecordId<Book>> {
10
+ title: string
11
+ author: string
12
+ publishedYear: number
13
+ genre?: string
14
+ inStock?: boolean
15
+ }
16
+
17
+ const Book = createRecordType<Book>('book', {
18
+ validator: { validate: (book) => book as Book },
19
+ scope: 'document',
20
+ }).withDefaultProperties(() => ({
21
+ inStock: true,
22
+ publishedYear: 2023,
23
+ }))
24
+
25
+ interface Author extends BaseRecord<'author', RecordId<Author>> {
26
+ name: string
27
+ birthYear: number
28
+ isAlive?: boolean
29
+ }
30
+
31
+ const Author = createRecordType<Author>('author', {
32
+ validator: { validate: (author) => author as Author },
33
+ scope: 'document',
34
+ }).withDefaultProperties(() => ({
35
+ isAlive: true,
36
+ }))
37
+
38
+ describe('StoreSchema', () => {
39
+ describe('create', () => {
40
+ it('validates migration dependencies during creation', () => {
41
+ const invalidMigrations = createMigrationSequence({
42
+ sequenceId: 'com.tldraw.book',
43
+ sequence: [
44
+ {
45
+ id: 'com.tldraw.book/1',
46
+ scope: 'record',
47
+ dependsOn: ['com.tldraw.book/999'],
48
+ up: (record: any) => record,
49
+ },
50
+ ],
51
+ })
52
+
53
+ expect(() => {
54
+ StoreSchema.create({ book: Book }, { migrations: [invalidMigrations] })
55
+ }).toThrow()
56
+ })
57
+
58
+ it('prevents duplicate migration sequence IDs', () => {
59
+ const migration1 = createMigrationSequence({
60
+ sequenceId: 'com.tldraw.book',
61
+ sequence: [
62
+ {
63
+ id: 'com.tldraw.book/1',
64
+ scope: 'record',
65
+ up: (record: any) => record,
66
+ },
67
+ ],
68
+ })
69
+
70
+ const migration2 = createMigrationSequence({
71
+ sequenceId: 'com.tldraw.book', // Same ID
72
+ sequence: [
73
+ {
74
+ id: 'com.tldraw.book/1',
75
+ scope: 'record',
76
+ up: (record: any) => record,
77
+ },
78
+ ],
79
+ })
80
+
81
+ expect(() => {
82
+ StoreSchema.create({ book: Book }, { migrations: [migration1, migration2] })
83
+ }).toThrow('Duplicate migration sequenceId com.tldraw.book')
84
+ })
85
+ })
86
+
87
+ describe('validateRecord', () => {
88
+ it('throws for invalid record without validation failure handler', () => {
89
+ // Create a validator that actually throws
90
+ const StrictBook = createRecordType<Book>('book', {
91
+ validator: {
92
+ validate: (book) => {
93
+ if (!(book as any).title || !(book as any).author) {
94
+ throw new Error('Missing required fields')
95
+ }
96
+ return book as Book
97
+ },
98
+ },
99
+ scope: 'document',
100
+ })
101
+
102
+ const schema = StoreSchema.create({ book: StrictBook })
103
+ const store = new Store({ schema, props: {} })
104
+
105
+ // Missing required fields
106
+ const invalidBook = {
107
+ id: StrictBook.createId(),
108
+ typeName: 'book' as const,
109
+ // Missing title, author, publishedYear
110
+ } as any
111
+
112
+ expect(() => {
113
+ schema.validateRecord(store, invalidBook, 'createRecord', null)
114
+ }).toThrow('Missing required fields')
115
+ })
116
+
117
+ it('calls validation failure handler for invalid record', () => {
118
+ const onValidationFailure = vi.fn().mockReturnValue({
119
+ id: Book.createId(),
120
+ typeName: 'book' as const,
121
+ title: 'Fixed Title',
122
+ author: 'Fixed Author',
123
+ publishedYear: 2023,
124
+ inStock: true,
125
+ })
126
+
127
+ // Create a validator that throws
128
+ const StrictBook = createRecordType<Book>('book', {
129
+ validator: {
130
+ validate: (book) => {
131
+ if (!(book as any).title || !(book as any).author) {
132
+ throw new Error('Missing required fields')
133
+ }
134
+ return book as Book
135
+ },
136
+ },
137
+ scope: 'document',
138
+ })
139
+
140
+ const schema = StoreSchema.create({ book: StrictBook }, { onValidationFailure })
141
+ const store = new Store({ schema, props: {} })
142
+
143
+ const invalidBook = {
144
+ id: StrictBook.createId(),
145
+ typeName: 'book' as const,
146
+ } as any
147
+
148
+ const result = schema.validateRecord(store, invalidBook, 'createRecord', null)
149
+
150
+ expect(onValidationFailure).toHaveBeenCalledWith({
151
+ store,
152
+ record: invalidBook,
153
+ phase: 'createRecord',
154
+ recordBefore: null,
155
+ error: expect.any(Error),
156
+ })
157
+ expect(result.title).toBe('Fixed Title')
158
+ })
159
+
160
+ it('throws for unknown record type', () => {
161
+ const schema = StoreSchema.create({ book: Book })
162
+ const store = new Store({ schema, props: {} })
163
+
164
+ const unknownRecord = {
165
+ id: 'unknown:1',
166
+ typeName: 'unknown',
167
+ } as any
168
+
169
+ expect(() => {
170
+ schema.validateRecord(store, unknownRecord, 'createRecord', null)
171
+ }).toThrow('Missing definition for record type unknown')
172
+ })
173
+ })
174
+
175
+ describe('createIntegrityChecker', () => {
176
+ it('calls createIntegrityChecker option when provided', () => {
177
+ const createIntegrityChecker = vi.fn()
178
+ const schema = StoreSchema.create({ book: Book }, { createIntegrityChecker })
179
+ const store = new Store({ schema, props: {} })
180
+
181
+ schema.createIntegrityChecker(store)
182
+
183
+ expect(createIntegrityChecker).toHaveBeenCalledWith(store)
184
+ })
185
+ })
186
+
187
+ describe('serialize', () => {
188
+ it('serializes schema with current migration versions', () => {
189
+ const bookMigrations = createMigrationSequence({
190
+ sequenceId: 'com.tldraw.book',
191
+ sequence: [
192
+ { id: 'com.tldraw.book/1', scope: 'record', up: (r: any) => r },
193
+ { id: 'com.tldraw.book/2', scope: 'record', up: (r: any) => r },
194
+ ],
195
+ })
196
+
197
+ const authorMigrations = createMigrationSequence({
198
+ sequenceId: 'com.tldraw.author',
199
+ sequence: [{ id: 'com.tldraw.author/1', scope: 'record', up: (r: any) => r }],
200
+ })
201
+
202
+ const schema = StoreSchema.create(
203
+ { book: Book, author: Author },
204
+ { migrations: [bookMigrations, authorMigrations] }
205
+ )
206
+
207
+ const serialized = schema.serialize()
208
+
209
+ expect(serialized).toEqual({
210
+ schemaVersion: 2,
211
+ sequences: {
212
+ 'com.tldraw.book': 2,
213
+ 'com.tldraw.author': 1,
214
+ },
215
+ })
216
+ })
217
+ })
218
+
219
+ describe('getType', () => {
220
+ it('throws for invalid type name', () => {
221
+ const schema = StoreSchema.create({ book: Book })
222
+
223
+ expect(() => schema.getType('nonexistent')).toThrow('record type does not exists')
224
+ })
225
+ })
226
+ })