@tldraw/store 4.1.0-canary.a5989c7a02c8 → 4.1.0-canary.ae12c0a5a37b
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 +1884 -153
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/AtomMap.js +241 -1
- package/dist-cjs/lib/AtomMap.js.map +2 -2
- package/dist-cjs/lib/BaseRecord.js.map +2 -2
- package/dist-cjs/lib/ImmutableMap.js +141 -0
- package/dist-cjs/lib/ImmutableMap.js.map +2 -2
- package/dist-cjs/lib/IncrementalSetConstructor.js +45 -5
- package/dist-cjs/lib/IncrementalSetConstructor.js.map +2 -2
- package/dist-cjs/lib/RecordType.js +116 -21
- package/dist-cjs/lib/RecordType.js.map +2 -2
- package/dist-cjs/lib/RecordsDiff.js.map +2 -2
- package/dist-cjs/lib/Store.js +233 -39
- package/dist-cjs/lib/Store.js.map +2 -2
- package/dist-cjs/lib/StoreQueries.js +135 -22
- package/dist-cjs/lib/StoreQueries.js.map +2 -2
- package/dist-cjs/lib/StoreSchema.js +207 -2
- package/dist-cjs/lib/StoreSchema.js.map +2 -2
- package/dist-cjs/lib/StoreSideEffects.js +102 -10
- package/dist-cjs/lib/StoreSideEffects.js.map +2 -2
- package/dist-cjs/lib/executeQuery.js.map +2 -2
- package/dist-cjs/lib/migrate.js.map +2 -2
- package/dist-cjs/lib/setUtils.js.map +2 -2
- package/dist-esm/index.d.mts +1884 -153
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/AtomMap.mjs +241 -1
- package/dist-esm/lib/AtomMap.mjs.map +2 -2
- package/dist-esm/lib/BaseRecord.mjs.map +2 -2
- package/dist-esm/lib/ImmutableMap.mjs +141 -0
- package/dist-esm/lib/ImmutableMap.mjs.map +2 -2
- package/dist-esm/lib/IncrementalSetConstructor.mjs +45 -5
- package/dist-esm/lib/IncrementalSetConstructor.mjs.map +2 -2
- package/dist-esm/lib/RecordType.mjs +116 -21
- package/dist-esm/lib/RecordType.mjs.map +2 -2
- package/dist-esm/lib/RecordsDiff.mjs.map +2 -2
- package/dist-esm/lib/Store.mjs +233 -39
- package/dist-esm/lib/Store.mjs.map +2 -2
- package/dist-esm/lib/StoreQueries.mjs +135 -22
- package/dist-esm/lib/StoreQueries.mjs.map +2 -2
- package/dist-esm/lib/StoreSchema.mjs +207 -2
- package/dist-esm/lib/StoreSchema.mjs.map +2 -2
- package/dist-esm/lib/StoreSideEffects.mjs +102 -10
- package/dist-esm/lib/StoreSideEffects.mjs.map +2 -2
- package/dist-esm/lib/executeQuery.mjs.map +2 -2
- package/dist-esm/lib/migrate.mjs.map +2 -2
- package/dist-esm/lib/setUtils.mjs.map +2 -2
- package/package.json +3 -3
- package/src/lib/AtomMap.ts +241 -1
- package/src/lib/BaseRecord.test.ts +44 -0
- package/src/lib/BaseRecord.ts +118 -4
- package/src/lib/ImmutableMap.test.ts +103 -0
- package/src/lib/ImmutableMap.ts +212 -0
- package/src/lib/IncrementalSetConstructor.test.ts +111 -0
- package/src/lib/IncrementalSetConstructor.ts +63 -6
- package/src/lib/RecordType.ts +149 -25
- package/src/lib/RecordsDiff.test.ts +144 -0
- package/src/lib/RecordsDiff.ts +145 -10
- package/src/lib/Store.test.ts +827 -0
- package/src/lib/Store.ts +533 -67
- package/src/lib/StoreQueries.test.ts +627 -0
- package/src/lib/StoreQueries.ts +194 -27
- package/src/lib/StoreSchema.test.ts +226 -0
- package/src/lib/StoreSchema.ts +386 -8
- package/src/lib/StoreSideEffects.test.ts +239 -19
- package/src/lib/StoreSideEffects.ts +266 -19
- package/src/lib/devFreeze.test.ts +137 -0
- package/src/lib/executeQuery.test.ts +481 -0
- package/src/lib/executeQuery.ts +80 -2
- package/src/lib/migrate.test.ts +400 -0
- package/src/lib/migrate.ts +187 -14
- package/src/lib/setUtils.test.ts +105 -0
- package/src/lib/setUtils.ts +44 -4
package/src/lib/executeQuery.ts
CHANGED
|
@@ -2,16 +2,69 @@ import { IdOf, UnknownRecord } from './BaseRecord'
|
|
|
2
2
|
import { intersectSets } from './setUtils'
|
|
3
3
|
import { StoreQueries } from './StoreQueries'
|
|
4
4
|
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Defines matching criteria for query values. Supports equality, inequality, and greater-than comparisons.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* // Exact match
|
|
11
|
+
* const exactMatch: QueryValueMatcher<string> = { eq: 'Science Fiction' }
|
|
12
|
+
*
|
|
13
|
+
* // Not equal to
|
|
14
|
+
* const notMatch: QueryValueMatcher<string> = { neq: 'Romance' }
|
|
15
|
+
*
|
|
16
|
+
* // Greater than (numeric values only)
|
|
17
|
+
* const greaterThan: QueryValueMatcher<number> = { gt: 2020 }
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
6
22
|
export type QueryValueMatcher<T> = { eq: T } | { neq: T } | { gt: number }
|
|
7
23
|
|
|
8
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Query expression for filtering records by their property values. Maps record property names
|
|
26
|
+
* to matching criteria.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* // Query for books published after 2020 that are in stock
|
|
31
|
+
* const bookQuery: QueryExpression<Book> = {
|
|
32
|
+
* publishedYear: { gt: 2020 },
|
|
33
|
+
* inStock: { eq: true }
|
|
34
|
+
* }
|
|
35
|
+
*
|
|
36
|
+
* // Query for books not by a specific author
|
|
37
|
+
* const notByAuthor: QueryExpression<Book> = {
|
|
38
|
+
* authorId: { neq: 'author:tolkien' }
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* @public
|
|
43
|
+
*/
|
|
9
44
|
export type QueryExpression<R extends object> = {
|
|
10
45
|
[k in keyof R & string]?: QueryValueMatcher<R[k]>
|
|
11
46
|
// todo: handle nesting
|
|
12
47
|
// | (R[k] extends object ? { match: QueryExpression<R[k]> } : never)
|
|
13
48
|
}
|
|
14
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Tests whether an object matches the given query expression by checking each property
|
|
52
|
+
* against its corresponding matcher criteria.
|
|
53
|
+
*
|
|
54
|
+
* @param query - The query expression containing matching criteria for object properties
|
|
55
|
+
* @param object - The object to test against the query
|
|
56
|
+
* @returns True if the object matches all criteria in the query, false otherwise
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* const book = { title: '1984', publishedYear: 1949, inStock: true }
|
|
61
|
+
* const query = { publishedYear: { gt: 1945 }, inStock: { eq: true } }
|
|
62
|
+
*
|
|
63
|
+
* const matches = objectMatchesQuery(query, book) // true
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* @public
|
|
67
|
+
*/
|
|
15
68
|
export function objectMatchesQuery<T extends object>(query: QueryExpression<T>, object: T) {
|
|
16
69
|
for (const [key, _matcher] of Object.entries(query)) {
|
|
17
70
|
const matcher = _matcher as QueryValueMatcher<T>
|
|
@@ -26,6 +79,31 @@ export function objectMatchesQuery<T extends object>(query: QueryExpression<T>,
|
|
|
26
79
|
return true
|
|
27
80
|
}
|
|
28
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Executes a query against the store using reactive indexes to efficiently find matching record IDs.
|
|
84
|
+
* Uses the store's internal indexes for optimal performance, especially for equality matches.
|
|
85
|
+
*
|
|
86
|
+
* @param store - The store queries interface providing access to reactive indexes
|
|
87
|
+
* @param typeName - The type name of records to query (e.g., 'book', 'author')
|
|
88
|
+
* @param query - Query expression defining the matching criteria
|
|
89
|
+
* @returns A Set containing the IDs of all records that match the query criteria
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* // Find IDs of all books published after 2020 that are in stock
|
|
94
|
+
* const bookIds = executeQuery(store, 'book', {
|
|
95
|
+
* publishedYear: { gt: 2020 },
|
|
96
|
+
* inStock: { eq: true }
|
|
97
|
+
* })
|
|
98
|
+
*
|
|
99
|
+
* // Find IDs of books not by a specific author
|
|
100
|
+
* const otherBookIds = executeQuery(store, 'book', {
|
|
101
|
+
* authorId: { neq: 'author:tolkien' }
|
|
102
|
+
* })
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* @public
|
|
106
|
+
*/
|
|
29
107
|
export function executeQuery<R extends UnknownRecord, TypeName extends R['typeName']>(
|
|
30
108
|
store: StoreQueries<R>,
|
|
31
109
|
typeName: TypeName,
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { UnknownRecord } from './BaseRecord'
|
|
3
|
+
import { SerializedStore } from './Store'
|
|
4
|
+
import {
|
|
5
|
+
type Migration,
|
|
6
|
+
type MigrationId,
|
|
7
|
+
type MigrationSequence,
|
|
8
|
+
type StandaloneDependsOn,
|
|
9
|
+
createMigrationIds,
|
|
10
|
+
createMigrationSequence,
|
|
11
|
+
createRecordMigrationSequence,
|
|
12
|
+
parseMigrationId,
|
|
13
|
+
sortMigrations,
|
|
14
|
+
validateMigrations,
|
|
15
|
+
} from './migrate'
|
|
16
|
+
|
|
17
|
+
describe('createMigrationIds', () => {
|
|
18
|
+
it('creates properly formatted migration IDs', () => {
|
|
19
|
+
const ids = createMigrationIds('com.myapp.book', {
|
|
20
|
+
addGenre: 1,
|
|
21
|
+
addPublisher: 2,
|
|
22
|
+
removeOldField: 3,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
expect(ids).toEqual({
|
|
26
|
+
addGenre: 'com.myapp.book/1',
|
|
27
|
+
addPublisher: 'com.myapp.book/2',
|
|
28
|
+
removeOldField: 'com.myapp.book/3',
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('parseMigrationId', () => {
|
|
34
|
+
it('parses migration IDs correctly', () => {
|
|
35
|
+
expect(parseMigrationId('com.myapp.book/5' as MigrationId)).toEqual({
|
|
36
|
+
sequenceId: 'com.myapp.book',
|
|
37
|
+
version: 5,
|
|
38
|
+
})
|
|
39
|
+
expect(parseMigrationId('test/1' as MigrationId)).toEqual({
|
|
40
|
+
sequenceId: 'test',
|
|
41
|
+
version: 1,
|
|
42
|
+
})
|
|
43
|
+
expect(parseMigrationId('com.example.app/42' as MigrationId)).toEqual({
|
|
44
|
+
sequenceId: 'com.example.app',
|
|
45
|
+
version: 42,
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('createMigrationSequence', () => {
|
|
51
|
+
it('creates and validates migration sequences', () => {
|
|
52
|
+
const migration: Migration = {
|
|
53
|
+
id: 'test/1' as MigrationId,
|
|
54
|
+
scope: 'record',
|
|
55
|
+
up: (record: UnknownRecord) => ({ ...record, newField: 'default' }),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const sequence = createMigrationSequence({
|
|
59
|
+
sequenceId: 'test',
|
|
60
|
+
sequence: [migration],
|
|
61
|
+
retroactive: false,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
expect(sequence.sequenceId).toBe('test')
|
|
65
|
+
expect(sequence.retroactive).toBe(false)
|
|
66
|
+
expect(sequence.sequence).toHaveLength(1)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('squashes standalone dependsOn entries', () => {
|
|
70
|
+
const dependsOn: StandaloneDependsOn = {
|
|
71
|
+
dependsOn: ['other/1' as MigrationId, 'another/2' as MigrationId],
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const migration: Migration = {
|
|
75
|
+
id: 'test/1' as MigrationId,
|
|
76
|
+
scope: 'record',
|
|
77
|
+
up: (record: UnknownRecord) => record,
|
|
78
|
+
dependsOn: ['existing/1' as MigrationId],
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const sequence = createMigrationSequence({
|
|
82
|
+
sequenceId: 'test',
|
|
83
|
+
sequence: [dependsOn, migration],
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
expect(sequence.sequence).toHaveLength(1)
|
|
87
|
+
expect(sequence.sequence[0].dependsOn).toEqual(['other/1', 'another/2', 'existing/1'])
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('validates the migration sequence', () => {
|
|
91
|
+
expect(() => {
|
|
92
|
+
createMigrationSequence({
|
|
93
|
+
sequenceId: 'test/invalid',
|
|
94
|
+
sequence: [],
|
|
95
|
+
})
|
|
96
|
+
}).toThrow()
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('createRecordMigrationSequence', () => {
|
|
101
|
+
it('creates record-scoped migrations with type filtering', () => {
|
|
102
|
+
const sequence = createRecordMigrationSequence({
|
|
103
|
+
recordType: 'book',
|
|
104
|
+
sequenceId: 'com.myapp.book',
|
|
105
|
+
sequence: [
|
|
106
|
+
{
|
|
107
|
+
id: 'com.myapp.book/1' as MigrationId,
|
|
108
|
+
up: (record: UnknownRecord) => ({ ...record, newField: 'value' }),
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
expect(sequence.sequenceId).toBe('com.myapp.book')
|
|
114
|
+
expect(sequence.sequence).toHaveLength(1)
|
|
115
|
+
expect(sequence.sequence[0].scope).toBe('record')
|
|
116
|
+
|
|
117
|
+
// Test the filter function
|
|
118
|
+
const migration = sequence.sequence[0] as Extract<Migration, { scope: 'record' }>
|
|
119
|
+
const bookRecord = { id: 'book-1' as any, typeName: 'book' } as any as UnknownRecord
|
|
120
|
+
const userRecord = { id: 'user-1' as any, typeName: 'user' } as any as UnknownRecord
|
|
121
|
+
|
|
122
|
+
expect(migration.filter?.(bookRecord)).toBe(true)
|
|
123
|
+
expect(migration.filter?.(userRecord)).toBe(false)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('combines record type filter with custom filter', () => {
|
|
127
|
+
const sequence = createRecordMigrationSequence({
|
|
128
|
+
recordType: 'shape',
|
|
129
|
+
filter: (record) => (record as any).shapeType === 'rectangle',
|
|
130
|
+
sequenceId: 'com.myapp.shape',
|
|
131
|
+
sequence: [
|
|
132
|
+
{
|
|
133
|
+
id: 'com.myapp.shape/1' as MigrationId,
|
|
134
|
+
up: (record: UnknownRecord) => record,
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
const migration = sequence.sequence[0] as Extract<Migration, { scope: 'record' }>
|
|
140
|
+
const rectangleShape = {
|
|
141
|
+
id: 'shape-1' as any,
|
|
142
|
+
typeName: 'shape',
|
|
143
|
+
shapeType: 'rectangle',
|
|
144
|
+
} as any as UnknownRecord
|
|
145
|
+
const circleShape = {
|
|
146
|
+
id: 'shape-2' as any,
|
|
147
|
+
typeName: 'shape',
|
|
148
|
+
shapeType: 'circle',
|
|
149
|
+
} as any as UnknownRecord
|
|
150
|
+
const userRecord = { id: 'user-1' as any, typeName: 'user' } as any as UnknownRecord
|
|
151
|
+
|
|
152
|
+
expect(migration.filter?.(rectangleShape)).toBe(true)
|
|
153
|
+
expect(migration.filter?.(circleShape)).toBe(false)
|
|
154
|
+
expect(migration.filter?.(userRecord)).toBe(false)
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
describe('validateMigrations', () => {
|
|
159
|
+
it('validates sequential migration versions', () => {
|
|
160
|
+
const sequence: MigrationSequence = {
|
|
161
|
+
sequenceId: 'test',
|
|
162
|
+
retroactive: true,
|
|
163
|
+
sequence: [
|
|
164
|
+
{
|
|
165
|
+
id: 'test/1' as MigrationId,
|
|
166
|
+
scope: 'record',
|
|
167
|
+
up: (record: UnknownRecord) => record,
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: 'test/2' as MigrationId,
|
|
171
|
+
scope: 'record',
|
|
172
|
+
up: (record: UnknownRecord) => record,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: 'test/3' as MigrationId,
|
|
176
|
+
scope: 'store',
|
|
177
|
+
up: (store: SerializedStore<UnknownRecord>) => store,
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
expect(() => validateMigrations(sequence)).not.toThrow()
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('throws on sequence ID with slash', () => {
|
|
186
|
+
const sequence: MigrationSequence = {
|
|
187
|
+
sequenceId: 'test/invalid',
|
|
188
|
+
retroactive: true,
|
|
189
|
+
sequence: [],
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
expect(() => validateMigrations(sequence)).toThrow(
|
|
193
|
+
"sequenceId cannot contain a '/', got test/invalid"
|
|
194
|
+
)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('throws on invalid migration ID format', () => {
|
|
198
|
+
const sequence: MigrationSequence = {
|
|
199
|
+
sequenceId: 'test',
|
|
200
|
+
retroactive: true,
|
|
201
|
+
sequence: [
|
|
202
|
+
{
|
|
203
|
+
id: 'invalid-id' as MigrationId,
|
|
204
|
+
scope: 'record',
|
|
205
|
+
up: (record: UnknownRecord) => record,
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
expect(() => validateMigrations(sequence)).toThrow(
|
|
211
|
+
"Every migration in sequence 'test' must have an id starting with 'test/'. Got invalid id: 'invalid-id'"
|
|
212
|
+
)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('throws on first migration not starting at version 1', () => {
|
|
216
|
+
const sequence: MigrationSequence = {
|
|
217
|
+
sequenceId: 'test',
|
|
218
|
+
retroactive: true,
|
|
219
|
+
sequence: [
|
|
220
|
+
{
|
|
221
|
+
id: 'test/5' as MigrationId,
|
|
222
|
+
scope: 'record',
|
|
223
|
+
up: (record: UnknownRecord) => record,
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
expect(() => validateMigrations(sequence)).toThrow(
|
|
229
|
+
"Expected the first migrationId to be 'test/1' but got 'test/5'"
|
|
230
|
+
)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('throws on non-sequential migration versions', () => {
|
|
234
|
+
const sequence: MigrationSequence = {
|
|
235
|
+
sequenceId: 'test',
|
|
236
|
+
retroactive: true,
|
|
237
|
+
sequence: [
|
|
238
|
+
{
|
|
239
|
+
id: 'test/1' as MigrationId,
|
|
240
|
+
scope: 'record',
|
|
241
|
+
up: (record: UnknownRecord) => record,
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
id: 'test/3' as MigrationId,
|
|
245
|
+
scope: 'record',
|
|
246
|
+
up: (record: UnknownRecord) => record,
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
expect(() => validateMigrations(sequence)).toThrow(
|
|
252
|
+
"Migration id numbers must increase in increments of 1, expected test/2 but got 'test/3'"
|
|
253
|
+
)
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
describe('sortMigrations', () => {
|
|
258
|
+
it('sorts migrations by sequence order', () => {
|
|
259
|
+
const migration1: Migration = {
|
|
260
|
+
id: 'app/1' as MigrationId,
|
|
261
|
+
scope: 'record',
|
|
262
|
+
up: (record: UnknownRecord) => record,
|
|
263
|
+
}
|
|
264
|
+
const migration2: Migration = {
|
|
265
|
+
id: 'app/2' as MigrationId,
|
|
266
|
+
scope: 'record',
|
|
267
|
+
up: (record: UnknownRecord) => record,
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
expect(sortMigrations([migration2, migration1])).toEqual([migration1, migration2])
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('respects explicit dependencies', () => {
|
|
274
|
+
const libMigration: Migration = {
|
|
275
|
+
id: 'lib/1' as MigrationId,
|
|
276
|
+
scope: 'record',
|
|
277
|
+
up: (record: UnknownRecord) => record,
|
|
278
|
+
dependsOn: ['app/1' as MigrationId],
|
|
279
|
+
}
|
|
280
|
+
const appMigration: Migration = {
|
|
281
|
+
id: 'app/1' as MigrationId,
|
|
282
|
+
scope: 'record',
|
|
283
|
+
up: (record: UnknownRecord) => record,
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const sorted = sortMigrations([libMigration, appMigration])
|
|
287
|
+
expect(sorted).toEqual([appMigration, libMigration])
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('handles complex dependency chains', () => {
|
|
291
|
+
const a1: Migration = {
|
|
292
|
+
id: 'a/1' as MigrationId,
|
|
293
|
+
scope: 'record',
|
|
294
|
+
up: (record: UnknownRecord) => record,
|
|
295
|
+
}
|
|
296
|
+
const b1: Migration = {
|
|
297
|
+
id: 'b/1' as MigrationId,
|
|
298
|
+
scope: 'record',
|
|
299
|
+
up: (record: UnknownRecord) => record,
|
|
300
|
+
dependsOn: ['a/1' as MigrationId],
|
|
301
|
+
}
|
|
302
|
+
const c1: Migration = {
|
|
303
|
+
id: 'c/1' as MigrationId,
|
|
304
|
+
scope: 'record',
|
|
305
|
+
up: (record: UnknownRecord) => record,
|
|
306
|
+
dependsOn: ['b/1' as MigrationId],
|
|
307
|
+
}
|
|
308
|
+
const d1: Migration = {
|
|
309
|
+
id: 'd/1' as MigrationId,
|
|
310
|
+
scope: 'record',
|
|
311
|
+
up: (record: UnknownRecord) => record,
|
|
312
|
+
dependsOn: ['a/1' as MigrationId, 'c/1' as MigrationId],
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const sorted = sortMigrations([d1, c1, b1, a1])
|
|
316
|
+
expect(sorted).toEqual([a1, b1, c1, d1])
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('prioritizes explicit dependencies over sequence order', () => {
|
|
320
|
+
const later: Migration = {
|
|
321
|
+
id: 'z/1' as MigrationId,
|
|
322
|
+
scope: 'record',
|
|
323
|
+
up: (record: UnknownRecord) => record,
|
|
324
|
+
}
|
|
325
|
+
const earlier: Migration = {
|
|
326
|
+
id: 'a/1' as MigrationId,
|
|
327
|
+
scope: 'record',
|
|
328
|
+
up: (record: UnknownRecord) => record,
|
|
329
|
+
dependsOn: ['z/1' as MigrationId],
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const sorted = sortMigrations([earlier, later])
|
|
333
|
+
expect(sorted).toEqual([later, earlier])
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it('throws on circular dependencies', () => {
|
|
337
|
+
const a: Migration = {
|
|
338
|
+
id: 'a/1' as MigrationId,
|
|
339
|
+
scope: 'record',
|
|
340
|
+
up: (record: UnknownRecord) => record,
|
|
341
|
+
dependsOn: ['b/1' as MigrationId],
|
|
342
|
+
}
|
|
343
|
+
const b: Migration = {
|
|
344
|
+
id: 'b/1' as MigrationId,
|
|
345
|
+
scope: 'record',
|
|
346
|
+
up: (record: UnknownRecord) => record,
|
|
347
|
+
dependsOn: ['a/1' as MigrationId],
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
expect(() => sortMigrations([a, b])).toThrow('Circular dependency in migrations: a/1')
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
it('handles multiple sequences with cross-dependencies', () => {
|
|
354
|
+
const app1: Migration = {
|
|
355
|
+
id: 'app/1' as MigrationId,
|
|
356
|
+
scope: 'record',
|
|
357
|
+
up: (record: UnknownRecord) => record,
|
|
358
|
+
}
|
|
359
|
+
const app2: Migration = {
|
|
360
|
+
id: 'app/2' as MigrationId,
|
|
361
|
+
scope: 'record',
|
|
362
|
+
up: (record: UnknownRecord) => record,
|
|
363
|
+
}
|
|
364
|
+
const lib1: Migration = {
|
|
365
|
+
id: 'lib/1' as MigrationId,
|
|
366
|
+
scope: 'record',
|
|
367
|
+
up: (record: UnknownRecord) => record,
|
|
368
|
+
dependsOn: ['app/1' as MigrationId],
|
|
369
|
+
}
|
|
370
|
+
const lib2: Migration = {
|
|
371
|
+
id: 'lib/2' as MigrationId,
|
|
372
|
+
scope: 'record',
|
|
373
|
+
up: (record: UnknownRecord) => record,
|
|
374
|
+
}
|
|
375
|
+
const plugin1: Migration = {
|
|
376
|
+
id: 'plugin/1' as MigrationId,
|
|
377
|
+
scope: 'record',
|
|
378
|
+
up: (record: UnknownRecord) => record,
|
|
379
|
+
dependsOn: ['app/2' as MigrationId, 'lib/1' as MigrationId],
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const sorted = sortMigrations([plugin1, lib2, lib1, app2, app1])
|
|
383
|
+
|
|
384
|
+
// Verify constraints are satisfied
|
|
385
|
+
const app1Index = sorted.indexOf(app1)
|
|
386
|
+
const app2Index = sorted.indexOf(app2)
|
|
387
|
+
const lib1Index = sorted.indexOf(lib1)
|
|
388
|
+
const lib2Index = sorted.indexOf(lib2)
|
|
389
|
+
const plugin1Index = sorted.indexOf(plugin1)
|
|
390
|
+
|
|
391
|
+
// Sequence dependencies
|
|
392
|
+
expect(app1Index).toBeLessThan(app2Index)
|
|
393
|
+
expect(lib1Index).toBeLessThan(lib2Index)
|
|
394
|
+
|
|
395
|
+
// Explicit dependencies
|
|
396
|
+
expect(app1Index).toBeLessThan(lib1Index) // lib/1 depends on app/1
|
|
397
|
+
expect(app2Index).toBeLessThan(plugin1Index) // plugin/1 depends on app/2
|
|
398
|
+
expect(lib1Index).toBeLessThan(plugin1Index) // plugin/1 depends on lib/1
|
|
399
|
+
})
|
|
400
|
+
})
|