kopia lustrzana https://github.com/Tldraw/Tldraw
266 wiersze
9.2 KiB
TypeScript
266 wiersze
9.2 KiB
TypeScript
import assert from 'assert'
|
|
import { BaseRecord, RecordId } from '../BaseRecord'
|
|
import { createRecordType } from '../RecordType'
|
|
import { SerializedSchemaV2, StoreSchema } from '../StoreSchema'
|
|
import { MigrationSequence } from '../migrate'
|
|
|
|
const mockSequence = ({
|
|
id,
|
|
retroactive,
|
|
versions,
|
|
filter,
|
|
}: {
|
|
id: string
|
|
retroactive: boolean
|
|
versions: number
|
|
filter?: (r: TestRecordType) => boolean
|
|
}): MigrationSequence => ({
|
|
sequenceId: id,
|
|
retroactive,
|
|
sequence: new Array(versions).fill(0).map((_, i) => ({
|
|
id: `${id}/${i + 1}`,
|
|
scope: 'record',
|
|
filter: filter as any,
|
|
up(r) {
|
|
const record = r as TestRecordType
|
|
record.versions[id] ??= 0
|
|
record.versions[id]++
|
|
// noop
|
|
},
|
|
down(r) {
|
|
const record = r as TestRecordType
|
|
record.versions[id]--
|
|
},
|
|
})),
|
|
})
|
|
|
|
interface TestRecordType extends BaseRecord<'test', RecordId<TestRecordType>> {
|
|
versions: Record<string, number>
|
|
}
|
|
const TestRecordType = createRecordType<TestRecordType>('test', {
|
|
scope: 'document',
|
|
})
|
|
|
|
const makeSchema = (migrations: MigrationSequence[]) => {
|
|
return StoreSchema.create({ test: TestRecordType }, { migrations })
|
|
}
|
|
|
|
const makePersistedSchema = (...args: Array<[migrations: MigrationSequence, version: number]>) => {
|
|
return {
|
|
schemaVersion: 2,
|
|
sequences: Object.fromEntries(args.map(([m, v]) => [m.sequenceId, v])),
|
|
} satisfies SerializedSchemaV2
|
|
}
|
|
|
|
const makeTestRecord = (persistedSchema: SerializedSchemaV2) => {
|
|
return TestRecordType.create({
|
|
versions: Object.fromEntries(
|
|
Object.keys(persistedSchema.sequences).map((id) => [id, persistedSchema.sequences[id]])
|
|
),
|
|
})
|
|
}
|
|
|
|
test('going up from 0', () => {
|
|
const foo = mockSequence({ id: 'foo', retroactive: false, versions: 2 })
|
|
const bar = mockSequence({ id: 'bar', retroactive: false, versions: 3 })
|
|
const schema = makeSchema([foo, bar])
|
|
const persistedSchema = makePersistedSchema([foo, 0], [bar, 0])
|
|
|
|
const r = makeTestRecord(persistedSchema)
|
|
expect(r.versions).toEqual({ foo: 0, bar: 0 })
|
|
const update = schema.migratePersistedRecord(r, persistedSchema)
|
|
assert(update.type === 'success', 'the update should be successful')
|
|
|
|
// the original record did not change
|
|
expect(r.versions).toEqual({ foo: 0, bar: 0 })
|
|
|
|
// the updated record has the new versions
|
|
expect((update.value as TestRecordType).versions).toEqual({ foo: 2, bar: 3 })
|
|
})
|
|
|
|
test('going up with a retroactive: true and a retroactive: false', () => {
|
|
const foo = mockSequence({ id: 'foo', retroactive: true, versions: 2 })
|
|
const bar = mockSequence({ id: 'bar', retroactive: false, versions: 3 })
|
|
const schema = makeSchema([foo, bar])
|
|
const persistedSchema = makePersistedSchema()
|
|
|
|
const r = makeTestRecord(persistedSchema)
|
|
expect(r.versions).toEqual({})
|
|
const update = schema.migratePersistedRecord(r, persistedSchema)
|
|
assert(update.type === 'success', 'the update should be successful')
|
|
|
|
// the original record did not change
|
|
expect(r.versions).toEqual({})
|
|
|
|
// the updated record has the new versions
|
|
expect((update.value as TestRecordType).versions).toEqual({ foo: 2 })
|
|
})
|
|
|
|
test('going down to 0s', () => {
|
|
const foo = mockSequence({ id: 'foo', retroactive: false, versions: 2 })
|
|
const bar = mockSequence({ id: 'bar', retroactive: false, versions: 3 })
|
|
const schema = makeSchema([foo, bar])
|
|
const persistedSchema = makePersistedSchema([foo, 0], [bar, 0])
|
|
|
|
const r = makeTestRecord(schema.serialize())
|
|
expect(r.versions).toEqual({ foo: 2, bar: 3 })
|
|
const downgrade = schema.migratePersistedRecord(r, persistedSchema, 'down')
|
|
assert(downgrade.type === 'success', 'the downgrade should be successful')
|
|
|
|
// the original record did not change
|
|
expect(r.versions).toEqual({ foo: 2, bar: 3 })
|
|
|
|
// the downgraded record has the new versions
|
|
expect((downgrade.value as TestRecordType).versions).toEqual({ foo: 0, bar: 0 })
|
|
})
|
|
|
|
test('going down with a retroactive: true and a retroactive: false', () => {
|
|
const foo = mockSequence({ id: 'foo', retroactive: true, versions: 2 })
|
|
const bar = mockSequence({ id: 'bar', retroactive: false, versions: 3 })
|
|
const schema = makeSchema([foo, bar])
|
|
const persistedSchema = makePersistedSchema()
|
|
|
|
const r = makeTestRecord(schema.serialize())
|
|
expect(r.versions).toEqual({ foo: 2, bar: 3 })
|
|
const downgrade = schema.migratePersistedRecord(r, persistedSchema, 'down')
|
|
assert(downgrade.type === 'success', 'the downgrade should be successful')
|
|
|
|
// the original record did not change
|
|
expect(r.versions).toEqual({ foo: 2, bar: 3 })
|
|
|
|
// only the foo migrations were undone
|
|
expect((downgrade.value as TestRecordType).versions).toEqual({ foo: 0, bar: 3 })
|
|
})
|
|
|
|
test('going up with no changes', () => {
|
|
const foo = mockSequence({ id: 'foo', retroactive: false, versions: 2 })
|
|
const bar = mockSequence({ id: 'bar', retroactive: false, versions: 3 })
|
|
const schema = makeSchema([foo, bar])
|
|
const persistedSchema = makePersistedSchema([foo, 2], [bar, 3])
|
|
|
|
const r = makeTestRecord(persistedSchema)
|
|
expect(r.versions).toEqual({ foo: 2, bar: 3 })
|
|
const update = schema.migratePersistedRecord(r, persistedSchema)
|
|
assert(update.type === 'success', 'the update should be successful')
|
|
|
|
// the returned record should be the the input record, i.e. it should not have allocated a new record
|
|
expect(r).toBe(update.value)
|
|
})
|
|
|
|
test('going down with no changes', () => {
|
|
const foo = mockSequence({ id: 'foo', retroactive: false, versions: 2 })
|
|
const bar = mockSequence({ id: 'bar', retroactive: false, versions: 3 })
|
|
const schema = makeSchema([foo, bar])
|
|
const persistedSchema = makePersistedSchema([foo, 2], [bar, 3])
|
|
|
|
const r = makeTestRecord(persistedSchema)
|
|
expect(r.versions).toEqual({ foo: 2, bar: 3 })
|
|
const update = schema.migratePersistedRecord(r, persistedSchema, 'down')
|
|
assert(update.type === 'success', 'the update should be successful')
|
|
|
|
// the returned record should be the the input record, i.e. it should not have allocated a new record
|
|
expect(r).toBe(update.value)
|
|
})
|
|
|
|
test('respects filters', () => {
|
|
const foo = mockSequence({
|
|
id: 'foo',
|
|
retroactive: false,
|
|
versions: 2,
|
|
filter: (r) => (r as any).foo === true,
|
|
})
|
|
const bar = mockSequence({ id: 'bar', retroactive: false, versions: 3 })
|
|
const schema = makeSchema([foo, bar])
|
|
const persistedSchema = makePersistedSchema([foo, 0], [bar, 0])
|
|
|
|
const r = makeTestRecord(persistedSchema)
|
|
const update = schema.migratePersistedRecord(r, persistedSchema, 'up')
|
|
assert(update.type === 'success', 'the update should be successful')
|
|
|
|
// foo migrations shouldn't have been applied
|
|
expect((update.value as TestRecordType).versions).toEqual({ foo: 0, bar: 3 })
|
|
|
|
const r2 = { ...r, foo: true }
|
|
const update2 = schema.migratePersistedRecord(r2, persistedSchema, 'up')
|
|
assert(update2.type === 'success', 'the update should be successful')
|
|
|
|
// foo migrations should have been applied
|
|
expect((update2.value as TestRecordType).versions).toEqual({ foo: 2, bar: 3 })
|
|
})
|
|
|
|
test('does not go up or down if theres a store migration in the path', () => {
|
|
const foo = mockSequence({ id: 'foo', retroactive: false, versions: 3 })
|
|
foo.sequence[1] = {
|
|
id: 'foo/2',
|
|
scope: 'store',
|
|
up() {
|
|
// noop
|
|
},
|
|
down() {
|
|
// noop
|
|
},
|
|
}
|
|
const schema = makeSchema([foo])
|
|
const v0Schema = makePersistedSchema([foo, 0])
|
|
|
|
const r0 = makeTestRecord(v0Schema)
|
|
const r3 = makeTestRecord(schema.serialize())
|
|
const update = schema.migratePersistedRecord(r0, v0Schema, 'up')
|
|
expect(update.type).toBe('error')
|
|
const update2 = schema.migratePersistedRecord(r3, v0Schema, 'down')
|
|
expect(update2.type).toBe('error')
|
|
|
|
// snapshot migration up should still work
|
|
const update3 = schema.migrateStoreSnapshot({
|
|
schema: v0Schema,
|
|
store: { [r0.id]: r0 },
|
|
})
|
|
|
|
assert(update3.type === 'success', 'the update should be successful')
|
|
expect((update3.value[r0.id] as TestRecordType).versions).toEqual({ foo: 2 })
|
|
})
|
|
|
|
test('does not go down if theres a migrations without the down migrator in the path', () => {
|
|
const foo = mockSequence({ id: 'foo', retroactive: false, versions: 3 })
|
|
delete (foo.sequence[1] as any).down
|
|
const schema = makeSchema([foo])
|
|
const v0Schema = makePersistedSchema([foo, 0])
|
|
|
|
// going up still works
|
|
const r0 = makeTestRecord(v0Schema)
|
|
const update = schema.migratePersistedRecord(r0, v0Schema, 'up')
|
|
expect(update.type).toBe('success')
|
|
|
|
// going down does not
|
|
const r3 = makeTestRecord(schema.serialize())
|
|
const update2 = schema.migratePersistedRecord(r3, v0Schema, 'down')
|
|
expect(update2.type).toBe('error')
|
|
})
|
|
|
|
test('allows returning a new record from the migrator fn', () => {
|
|
const foo = mockSequence({ id: 'foo', retroactive: false, versions: 3 })
|
|
foo.sequence[1] = {
|
|
id: 'foo/2',
|
|
scope: 'record',
|
|
up(r) {
|
|
const record = r as TestRecordType
|
|
return { ...record, versions: { ...record.versions, foo: 2 } }
|
|
},
|
|
down(r) {
|
|
const record = r as TestRecordType
|
|
return { ...record, versions: { ...record.versions, foo: 1 } }
|
|
},
|
|
}
|
|
const schema = makeSchema([foo])
|
|
const v0Schema = makePersistedSchema([foo, 0])
|
|
|
|
const r0 = makeTestRecord(v0Schema)
|
|
const r3 = makeTestRecord(schema.serialize())
|
|
const update = schema.migratePersistedRecord(r0, v0Schema, 'up')
|
|
assert(update.type === 'success', 'the update should be successful')
|
|
expect((update.value as TestRecordType).versions).toEqual({ foo: 3 })
|
|
const update2 = schema.migratePersistedRecord(r3, v0Schema, 'down')
|
|
assert(update2.type === 'success', 'the update should be successful')
|
|
expect((update2.value as TestRecordType).versions).toEqual({ foo: 0 })
|
|
})
|