UNPKG

@tldraw/tlschema

Version:

A tiny little drawing app (schema).

148 lines (139 loc) 4.13 kB
import { Migration, MigrationId, MigrationSequence, RecordType, StandaloneDependsOn, UnknownRecord, createMigrationSequence, } from '@tldraw/store' import { MakeUndefinedOptional, assert } from '@tldraw/utils' import { T } from '@tldraw/validate' import { SchemaPropsInfo } from './createTLSchema' /** @public */ export type RecordProps<R extends UnknownRecord & { props: object }> = { [K in keyof R['props']]: T.Validatable<R['props'][K]> } /** @public */ export type RecordPropsType<Config extends Record<string, T.Validatable<any>>> = MakeUndefinedOptional<{ [K in keyof Config]: T.TypeOf<Config[K]> }> /** * @public */ export interface TLPropsMigration { readonly id: MigrationId readonly dependsOn?: MigrationId[] // eslint-disable-next-line @typescript-eslint/method-signature-style readonly up: (props: any) => any /** * If a down migration was deployed more than a couple of months ago it should be safe to retire it. * We only really need them to smooth over the transition between versions, and some folks do keep * browser tabs open for months without refreshing, but at a certain point that kind of behavior is * on them. Plus anyway recently chrome has started to actually kill tabs that are open for too long * rather than just suspending them, so if other browsers follow suit maybe it's less of a concern. * * @public */ readonly down?: 'none' | 'retired' | ((props: any) => any) } /** * @public */ export interface TLPropsMigrations { readonly sequence: Array<StandaloneDependsOn | TLPropsMigration> } export function processPropsMigrations<R extends UnknownRecord & { type: string; props: object }>( typeName: R['typeName'], records: Record<string, SchemaPropsInfo> ) { const result: MigrationSequence[] = [] for (const [subType, { migrations }] of Object.entries(records)) { const sequenceId = `com.tldraw.${typeName}.${subType}` if (!migrations) { // provide empty migrations sequence to allow for future migrations result.push( createMigrationSequence({ sequenceId, retroactive: false, sequence: [], }) ) } else if ('sequenceId' in migrations) { assert( sequenceId === migrations.sequenceId, `sequenceId mismatch for ${subType} ${RecordType} migrations. Expected '${sequenceId}', got '${migrations.sequenceId}'` ) result.push(migrations) } else if ('sequence' in migrations) { result.push( createMigrationSequence({ sequenceId, retroactive: false, sequence: migrations.sequence.map((m) => 'id' in m ? createPropsMigration(typeName, subType, m) : m ), }) ) } else { // legacy migrations, will be removed in the future result.push( createMigrationSequence({ sequenceId, retroactive: false, sequence: Object.keys(migrations.migrators) .map((k) => Number(k)) .sort((a: number, b: number) => a - b) .map( (version): Migration => ({ id: `${sequenceId}/${version}`, scope: 'record', filter: (r) => r.typeName === typeName && (r as R).type === subType, up: (record: any) => { const result = migrations.migrators[version].up(record) if (result) { return result } }, down: (record: any) => { const result = migrations.migrators[version].down(record) if (result) { return result } }, }) ), }) ) } } return result } export function createPropsMigration<R extends UnknownRecord & { type: string; props: object }>( typeName: R['typeName'], subType: R['type'], m: TLPropsMigration ): Migration { return { id: m.id, dependsOn: m.dependsOn, scope: 'record', filter: (r) => r.typeName === typeName && (r as R).type === subType, up: (record: any) => { const result = m.up(record.props) if (result) { record.props = result } }, down: typeof m.down === 'function' ? (record: any) => { const result = (m.down as (props: any) => any)(record.props) if (result) { record.props = result } } : undefined, } }