@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
package/src/lib/migrate.test.ts
CHANGED
|
@@ -3,8 +3,6 @@ import { UnknownRecord } from './BaseRecord'
|
|
|
3
3
|
import {
|
|
4
4
|
type Migration,
|
|
5
5
|
type MigrationId,
|
|
6
|
-
type MigrationSequence,
|
|
7
|
-
type StandaloneDependsOn,
|
|
8
6
|
createMigrationIds,
|
|
9
7
|
createMigrationSequence,
|
|
10
8
|
createRecordMigrationSequence,
|
|
@@ -14,50 +12,23 @@ import {
|
|
|
14
12
|
} from './migrate'
|
|
15
13
|
import { SerializedStore } from './Store'
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
})
|
|
15
|
+
// Tests for SPEC.md §16 (migration authoring) and §17 (migration sorting).
|
|
16
|
+
// Rule IDs like [M2] in test names refer to that document.
|
|
32
17
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
})
|
|
18
|
+
const m = (id: MigrationId, others?: { dependsOn?: MigrationId[] }): Migration => ({
|
|
19
|
+
...others,
|
|
20
|
+
id,
|
|
21
|
+
scope: 'record',
|
|
22
|
+
up() {
|
|
23
|
+
// noop
|
|
24
|
+
},
|
|
48
25
|
})
|
|
49
26
|
|
|
50
|
-
describe('
|
|
51
|
-
it('
|
|
52
|
-
const migration: Migration = {
|
|
53
|
-
id: 'test/1' as MigrationId,
|
|
54
|
-
scope: 'record',
|
|
55
|
-
up: (record: UnknownRecord) => ({ ...record, newField: 'default' }),
|
|
56
|
-
}
|
|
57
|
-
|
|
27
|
+
describe('migration authoring (M)', () => {
|
|
28
|
+
it('[M1] createMigrationSequence returns the validated sequence', () => {
|
|
58
29
|
const sequence = createMigrationSequence({
|
|
59
30
|
sequenceId: 'test',
|
|
60
|
-
sequence: [
|
|
31
|
+
sequence: [m('test/1')],
|
|
61
32
|
retroactive: false,
|
|
62
33
|
})
|
|
63
34
|
|
|
@@ -66,39 +37,111 @@ describe('createMigrationSequence', () => {
|
|
|
66
37
|
expect(sequence.sequence).toHaveLength(1)
|
|
67
38
|
})
|
|
68
39
|
|
|
69
|
-
it('
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
40
|
+
it('[M1] retroactive defaults to true', () => {
|
|
41
|
+
const sequence = createMigrationSequence({
|
|
42
|
+
sequenceId: 'test',
|
|
43
|
+
sequence: [m('test/1')],
|
|
44
|
+
})
|
|
45
|
+
expect(sequence.retroactive).toBe(true)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('[M1] createMigrationSequence validates the sequence', () => {
|
|
49
|
+
expect(() =>
|
|
50
|
+
createMigrationSequence({
|
|
51
|
+
sequenceId: 'test/invalid',
|
|
52
|
+
sequence: [],
|
|
53
|
+
})
|
|
54
|
+
).toThrow()
|
|
55
|
+
|
|
56
|
+
expect(() =>
|
|
57
|
+
createMigrationSequence({
|
|
58
|
+
sequenceId: 'test',
|
|
59
|
+
sequence: [m('test/2')],
|
|
60
|
+
})
|
|
61
|
+
).toThrow()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('[M2] a standalone dependsOn entry squashes into the next migration', () => {
|
|
65
|
+
const result = createMigrationSequence({
|
|
66
|
+
sequenceId: 'foo',
|
|
67
|
+
retroactive: false,
|
|
68
|
+
sequence: [{ dependsOn: ['bar/1'] }, m('foo/1')],
|
|
69
|
+
})
|
|
73
70
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
up: (record: UnknownRecord) => record,
|
|
78
|
-
dependsOn: ['existing/1' as MigrationId],
|
|
79
|
-
}
|
|
71
|
+
expect(result.sequence).toHaveLength(1)
|
|
72
|
+
expect(result.sequence[0].dependsOn).toEqual(['bar/1'])
|
|
73
|
+
})
|
|
80
74
|
|
|
75
|
+
it('[M2] a squashed dependsOn prepends to the migration’s own dependsOn', () => {
|
|
81
76
|
const sequence = createMigrationSequence({
|
|
82
77
|
sequenceId: 'test',
|
|
83
|
-
sequence: [
|
|
78
|
+
sequence: [
|
|
79
|
+
{ dependsOn: ['other/1' as MigrationId, 'another/2' as MigrationId] },
|
|
80
|
+
m('test/1', { dependsOn: ['existing/1' as MigrationId] }),
|
|
81
|
+
],
|
|
84
82
|
})
|
|
85
83
|
|
|
86
84
|
expect(sequence.sequence).toHaveLength(1)
|
|
87
85
|
expect(sequence.sequence[0].dependsOn).toEqual(['other/1', 'another/2', 'existing/1'])
|
|
88
86
|
})
|
|
89
87
|
|
|
90
|
-
it('
|
|
91
|
-
|
|
88
|
+
it('[M2] a standalone dependsOn between migrations attaches to the one after it', () => {
|
|
89
|
+
const result = createMigrationSequence({
|
|
90
|
+
sequenceId: 'foo',
|
|
91
|
+
retroactive: false,
|
|
92
|
+
sequence: [m('foo/1'), { dependsOn: ['bar/1'] }, m('foo/2')],
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
expect(result.sequence).toHaveLength(2)
|
|
96
|
+
expect(result.sequence[0].dependsOn ?? []).toEqual([])
|
|
97
|
+
expect(result.sequence[1].dependsOn).toEqual(['bar/1'])
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('[M2] a standalone dependsOn with no following migration is dropped', () => {
|
|
101
|
+
expect(
|
|
92
102
|
createMigrationSequence({
|
|
93
|
-
sequenceId: '
|
|
94
|
-
|
|
103
|
+
sequenceId: 'foo',
|
|
104
|
+
retroactive: false,
|
|
105
|
+
sequence: [{ dependsOn: ['bar/1'] }],
|
|
106
|
+
}).sequence
|
|
107
|
+
).toHaveLength(0)
|
|
108
|
+
|
|
109
|
+
const trailing = createMigrationSequence({
|
|
110
|
+
sequenceId: 'foo',
|
|
111
|
+
retroactive: false,
|
|
112
|
+
sequence: [m('foo/1'), { dependsOn: ['bar/1'] }],
|
|
113
|
+
})
|
|
114
|
+
expect(trailing.sequence).toHaveLength(1)
|
|
115
|
+
expect(trailing.sequence[0].dependsOn ?? []).toEqual([])
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('[M4] createMigrationIds formats sequenceId/version ids', () => {
|
|
119
|
+
expect(
|
|
120
|
+
createMigrationIds('com.myapp.book', {
|
|
121
|
+
addGenre: 1,
|
|
122
|
+
addPublisher: 2,
|
|
123
|
+
removeOldField: 3,
|
|
95
124
|
})
|
|
96
|
-
|
|
125
|
+
).toEqual({
|
|
126
|
+
addGenre: 'com.myapp.book/1',
|
|
127
|
+
addPublisher: 'com.myapp.book/2',
|
|
128
|
+
removeOldField: 'com.myapp.book/3',
|
|
129
|
+
})
|
|
97
130
|
})
|
|
98
|
-
})
|
|
99
131
|
|
|
100
|
-
|
|
101
|
-
|
|
132
|
+
it('[M4] parseMigrationId splits the sequence id and version', () => {
|
|
133
|
+
expect(parseMigrationId('com.myapp.book/5')).toEqual({
|
|
134
|
+
sequenceId: 'com.myapp.book',
|
|
135
|
+
version: 5,
|
|
136
|
+
})
|
|
137
|
+
expect(parseMigrationId('test/1')).toEqual({ sequenceId: 'test', version: 1 })
|
|
138
|
+
expect(parseMigrationId('com.example.app/42')).toEqual({
|
|
139
|
+
sequenceId: 'com.example.app',
|
|
140
|
+
version: 42,
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('[M5] createRecordMigrationSequence filters by record type', () => {
|
|
102
145
|
const sequence = createRecordMigrationSequence({
|
|
103
146
|
recordType: 'book',
|
|
104
147
|
sequenceId: 'com.myapp.book',
|
|
@@ -114,16 +157,12 @@ describe('createRecordMigrationSequence', () => {
|
|
|
114
157
|
expect(sequence.sequence).toHaveLength(1)
|
|
115
158
|
expect(sequence.sequence[0].scope).toBe('record')
|
|
116
159
|
|
|
117
|
-
// Test the filter function
|
|
118
160
|
const migration = sequence.sequence[0] as Extract<Migration, { scope: 'record' }>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
expect(migration.filter?.(bookRecord)).toBe(true)
|
|
123
|
-
expect(migration.filter?.(userRecord)).toBe(false)
|
|
161
|
+
expect(migration.filter?.({ id: 'book-1', typeName: 'book' } as any)).toBe(true)
|
|
162
|
+
expect(migration.filter?.({ id: 'user-1', typeName: 'user' } as any)).toBe(false)
|
|
124
163
|
})
|
|
125
164
|
|
|
126
|
-
it('
|
|
165
|
+
it('[M5] a sequence-level filter composes with the record type filter', () => {
|
|
127
166
|
const sequence = createRecordMigrationSequence({
|
|
128
167
|
recordType: 'shape',
|
|
129
168
|
filter: (record) => (record as any).shapeType === 'rectangle',
|
|
@@ -137,264 +176,202 @@ describe('createRecordMigrationSequence', () => {
|
|
|
137
176
|
})
|
|
138
177
|
|
|
139
178
|
const migration = sequence.sequence[0] as Extract<Migration, { scope: 'record' }>
|
|
140
|
-
|
|
141
|
-
id: 'shape-1' as any
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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)
|
|
179
|
+
expect(
|
|
180
|
+
migration.filter?.({ id: 'shape-1', typeName: 'shape', shapeType: 'rectangle' } as any)
|
|
181
|
+
).toBe(true)
|
|
182
|
+
expect(
|
|
183
|
+
migration.filter?.({ id: 'shape-2', typeName: 'shape', shapeType: 'circle' } as any)
|
|
184
|
+
).toBe(false)
|
|
185
|
+
expect(migration.filter?.({ id: 'user-1', typeName: 'user' } as any)).toBe(false)
|
|
155
186
|
})
|
|
156
187
|
})
|
|
157
188
|
|
|
158
|
-
describe('validateMigrations', () => {
|
|
159
|
-
it('
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
scope: 'store',
|
|
177
|
-
up: (store: SerializedStore<UnknownRecord>) => store,
|
|
178
|
-
},
|
|
179
|
-
],
|
|
180
|
-
}
|
|
189
|
+
describe('validateMigrations (M)', () => {
|
|
190
|
+
it('[M3] accepts well-formed sequences, including mixed scopes', () => {
|
|
191
|
+
expect(() =>
|
|
192
|
+
validateMigrations({
|
|
193
|
+
sequenceId: 'test',
|
|
194
|
+
retroactive: true,
|
|
195
|
+
sequence: [
|
|
196
|
+
m('test/1'),
|
|
197
|
+
m('test/2'),
|
|
198
|
+
{
|
|
199
|
+
id: 'test/3',
|
|
200
|
+
scope: 'store',
|
|
201
|
+
up: (store: SerializedStore<UnknownRecord>) => store,
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
})
|
|
205
|
+
).not.toThrow()
|
|
206
|
+
})
|
|
181
207
|
|
|
182
|
-
|
|
208
|
+
it('[M3] throws on an invalid sequence id', () => {
|
|
209
|
+
expect(() =>
|
|
210
|
+
validateMigrations({ retroactive: false, sequence: [], sequenceId: 'foo/bar' })
|
|
211
|
+
).toThrowErrorMatchingInlineSnapshot(`[Error: sequenceId cannot contain a '/', got foo/bar]`)
|
|
212
|
+
|
|
213
|
+
expect(() =>
|
|
214
|
+
validateMigrations({ retroactive: false, sequence: [], sequenceId: '' })
|
|
215
|
+
).toThrowErrorMatchingInlineSnapshot(`[Error: sequenceId must be a non-empty string]`)
|
|
183
216
|
})
|
|
184
217
|
|
|
185
|
-
it('throws on sequence
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
218
|
+
it('[M3] throws on a migration id outside the sequence or in the wrong format', () => {
|
|
219
|
+
expect(() =>
|
|
220
|
+
validateMigrations({
|
|
221
|
+
retroactive: false,
|
|
222
|
+
sequence: [m('foo.1' as any)],
|
|
223
|
+
sequenceId: 'foo',
|
|
224
|
+
})
|
|
225
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
226
|
+
`[Error: Every migration in sequence 'foo' must have an id starting with 'foo/'. Got invalid id: 'foo.1']`
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
expect(() =>
|
|
230
|
+
validateMigrations({
|
|
231
|
+
retroactive: false,
|
|
232
|
+
sequence: [m('foo/one' as any)],
|
|
233
|
+
sequenceId: 'foo',
|
|
234
|
+
})
|
|
235
|
+
).toThrowErrorMatchingInlineSnapshot(`[Error: Invalid migration id: 'foo/one']`)
|
|
191
236
|
|
|
192
|
-
expect(() =>
|
|
193
|
-
|
|
237
|
+
expect(() =>
|
|
238
|
+
validateMigrations({
|
|
239
|
+
retroactive: false,
|
|
240
|
+
sequence: [m('foo/1'), m('foo.2' as any)],
|
|
241
|
+
sequenceId: 'foo',
|
|
242
|
+
})
|
|
243
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
244
|
+
`[Error: Every migration in sequence 'foo' must have an id starting with 'foo/'. Got invalid id: 'foo.2']`
|
|
194
245
|
)
|
|
246
|
+
|
|
247
|
+
expect(() =>
|
|
248
|
+
validateMigrations({
|
|
249
|
+
retroactive: false,
|
|
250
|
+
sequence: [m('foo/1'), m('foo/two' as any)],
|
|
251
|
+
sequenceId: 'foo',
|
|
252
|
+
})
|
|
253
|
+
).toThrowErrorMatchingInlineSnapshot(`[Error: Invalid migration id: 'foo/two']`)
|
|
195
254
|
})
|
|
196
255
|
|
|
197
|
-
it('throws
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
256
|
+
it('[M3] throws if the first version is not 1', () => {
|
|
257
|
+
expect(() =>
|
|
258
|
+
validateMigrations({
|
|
259
|
+
retroactive: false,
|
|
260
|
+
sequence: [m('foo/2')],
|
|
261
|
+
sequenceId: 'foo',
|
|
262
|
+
})
|
|
263
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
264
|
+
`[Error: Expected the first migrationId to be 'foo/1' but got 'foo/2']`
|
|
265
|
+
)
|
|
266
|
+
})
|
|
209
267
|
|
|
210
|
-
|
|
211
|
-
|
|
268
|
+
it('[M3] throws if versions do not increase in increments of 1', () => {
|
|
269
|
+
expect(() =>
|
|
270
|
+
validateMigrations({
|
|
271
|
+
retroactive: false,
|
|
272
|
+
sequence: [m('foo/1'), m('foo/2'), m('foo/4')],
|
|
273
|
+
sequenceId: 'foo',
|
|
274
|
+
})
|
|
275
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
276
|
+
`[Error: Migration id numbers must increase in increments of 1, expected foo/3 but got 'foo/4']`
|
|
212
277
|
)
|
|
213
278
|
})
|
|
279
|
+
})
|
|
214
280
|
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
}
|
|
281
|
+
describe('sortMigrations (MS)', () => {
|
|
282
|
+
const sort = (migrations: Migration[]) => sortMigrations(migrations).map((m) => m.id)
|
|
227
283
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
)
|
|
284
|
+
it('[MS1] sorts migrations within a sequence by version', () => {
|
|
285
|
+
expect(sort([m('foo/2'), m('foo/1')])).toEqual(['foo/1', 'foo/2'])
|
|
286
|
+
expect(sort([m('foo/1'), m('foo/2')])).toEqual(['foo/1', 'foo/2'])
|
|
231
287
|
})
|
|
232
288
|
|
|
233
|
-
it('
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
}
|
|
289
|
+
it('[MS1] sorts each of several sequences by version', () => {
|
|
290
|
+
const result = sort([m('foo/2'), m('bar/2'), m('foo/1'), m('bar/1')])
|
|
291
|
+
expect(result.filter((id) => id.startsWith('foo/'))).toEqual(['foo/1', 'foo/2'])
|
|
292
|
+
expect(result.filter((id) => id.startsWith('bar/'))).toEqual(['bar/1', 'bar/2'])
|
|
293
|
+
})
|
|
250
294
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
295
|
+
it('[MS2] a migration sorts after its explicit dependencies', () => {
|
|
296
|
+
expect(
|
|
297
|
+
sort([m('foo/2'), m('bar/2'), m('foo/1'), m('bar/1', { dependsOn: ['foo/2'] })])
|
|
298
|
+
).toEqual(['foo/1', 'foo/2', 'bar/1', 'bar/2'])
|
|
299
|
+
|
|
300
|
+
expect(
|
|
301
|
+
sort([m('foo/2'), m('bar/2'), m('foo/1', { dependsOn: ['bar/2'] }), m('bar/1')])
|
|
302
|
+
).toEqual(['bar/1', 'bar/2', 'foo/1', 'foo/2'])
|
|
254
303
|
})
|
|
255
|
-
})
|
|
256
304
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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])
|
|
305
|
+
it('[MS2] handles chains of dependencies across sequences', () => {
|
|
306
|
+
const a1 = m('a/1')
|
|
307
|
+
const b1 = m('b/1', { dependsOn: ['a/1'] })
|
|
308
|
+
const c1 = m('c/1', { dependsOn: ['b/1'] })
|
|
309
|
+
const d1 = m('d/1', { dependsOn: ['a/1', 'c/1'] })
|
|
310
|
+
|
|
311
|
+
expect(sortMigrations([d1, c1, b1, a1])).toEqual([a1, b1, c1, d1])
|
|
271
312
|
})
|
|
272
313
|
|
|
273
|
-
it('
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
expect(sorted).toEqual([appMigration, libMigration])
|
|
314
|
+
it('[MS2] satisfies both sequence order and cross-sequence dependencies', () => {
|
|
315
|
+
const sorted = sort([
|
|
316
|
+
m('plugin/1', { dependsOn: ['app/2', 'lib/1'] }),
|
|
317
|
+
m('lib/2'),
|
|
318
|
+
m('lib/1', { dependsOn: ['app/1'] }),
|
|
319
|
+
m('app/2'),
|
|
320
|
+
m('app/1'),
|
|
321
|
+
])
|
|
322
|
+
|
|
323
|
+
expect(sorted.indexOf('app/1')).toBeLessThan(sorted.indexOf('app/2'))
|
|
324
|
+
expect(sorted.indexOf('lib/1')).toBeLessThan(sorted.indexOf('lib/2'))
|
|
325
|
+
expect(sorted.indexOf('app/1')).toBeLessThan(sorted.indexOf('lib/1'))
|
|
326
|
+
expect(sorted.indexOf('app/2')).toBeLessThan(sorted.indexOf('plugin/1'))
|
|
327
|
+
expect(sorted.indexOf('lib/1')).toBeLessThan(sorted.indexOf('plugin/1'))
|
|
288
328
|
})
|
|
289
329
|
|
|
290
|
-
it('
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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])
|
|
330
|
+
it('[MS3] schedules dependents close to their dependencies', () => {
|
|
331
|
+
// bar/3 depends on foo/1 — the bar sequence runs immediately after foo/1
|
|
332
|
+
expect(
|
|
333
|
+
sort([m('foo/2'), m('bar/3', { dependsOn: ['foo/1'] }), m('foo/1'), m('bar/1'), m('bar/2')])
|
|
334
|
+
).toEqual(['foo/1', 'bar/1', 'bar/2', 'bar/3', 'foo/2'])
|
|
317
335
|
})
|
|
318
336
|
|
|
319
|
-
it('
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
dependsOn: ['z/1' as MigrationId],
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const sorted = sortMigrations([earlier, later])
|
|
333
|
-
expect(sorted).toEqual([later, earlier])
|
|
337
|
+
it('[MS3] minimizes total distance for multiple explicit dependencies', () => {
|
|
338
|
+
expect(
|
|
339
|
+
sort([
|
|
340
|
+
m('foo/2'),
|
|
341
|
+
m('bar/2', { dependsOn: ['foo/1'] }),
|
|
342
|
+
m('foo/1'),
|
|
343
|
+
m('bar/1'),
|
|
344
|
+
m('baz/1', { dependsOn: ['foo/1'] }),
|
|
345
|
+
])
|
|
346
|
+
).toEqual(['foo/1', 'bar/1', 'bar/2', 'baz/1', 'foo/2'])
|
|
334
347
|
})
|
|
335
348
|
|
|
336
|
-
it('
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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')
|
|
349
|
+
it('[MS3] keeps chains of explicit dependencies consecutive', () => {
|
|
350
|
+
expect(
|
|
351
|
+
sort([
|
|
352
|
+
m('foo/2'),
|
|
353
|
+
m('bar/1', { dependsOn: ['foo/1'] }),
|
|
354
|
+
m('foo/1'),
|
|
355
|
+
m('baz/1', { dependsOn: ['bar/1'] }),
|
|
356
|
+
])
|
|
357
|
+
).toEqual(['foo/1', 'bar/1', 'baz/1', 'foo/2'])
|
|
351
358
|
})
|
|
352
359
|
|
|
353
|
-
it('
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
|
360
|
+
it('[MS4] throws on circular dependencies', () => {
|
|
361
|
+
expect(() => sort([m('foo/1', { dependsOn: ['foo/1'] })])).toThrowErrorMatchingInlineSnapshot(
|
|
362
|
+
`[Error: Circular dependency in migrations: foo/1]`
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
expect(() =>
|
|
366
|
+
sort([m('foo/1', { dependsOn: ['foo/2'] }), m('foo/2')])
|
|
367
|
+
).toThrowErrorMatchingInlineSnapshot(`[Error: Circular dependency in migrations: foo/1]`)
|
|
368
|
+
|
|
369
|
+
expect(() =>
|
|
370
|
+
sort([m('foo/1', { dependsOn: ['bar/1'] }), m('bar/1', { dependsOn: ['foo/1'] })])
|
|
371
|
+
).toThrowErrorMatchingInlineSnapshot(`[Error: Circular dependency in migrations: foo/1]`)
|
|
372
|
+
|
|
373
|
+
expect(() =>
|
|
374
|
+
sort([m('bar/1', { dependsOn: ['foo/1'] }), m('foo/1', { dependsOn: ['bar/1'] })])
|
|
375
|
+
).toThrowErrorMatchingInlineSnapshot(`[Error: Circular dependency in migrations: bar/1]`)
|
|
399
376
|
})
|
|
400
377
|
})
|