snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
This commit is contained in:
276
packages/tlschema/src/recordsWithProps.ts
Normal file
276
packages/tlschema/src/recordsWithProps.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
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'
|
||||
|
||||
/**
|
||||
* Maps a record's property types to their corresponding validators.
|
||||
*
|
||||
* This utility type takes a record type with a `props` object and creates
|
||||
* a mapping where each property key maps to a validator for that property's type.
|
||||
* This is used to define validation schemas for record properties.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* interface MyShape extends TLBaseShape<'custom', { width: number; color: string }> {}
|
||||
*
|
||||
* // Define validators for the shape properties
|
||||
* const myShapeProps: RecordProps<MyShape> = {
|
||||
* width: T.number,
|
||||
* color: T.string
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type RecordProps<R extends UnknownRecord & { props: object }> = {
|
||||
[K in keyof R['props']]: T.Validatable<R['props'][K]>
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the TypeScript types from a record properties configuration.
|
||||
*
|
||||
* Takes a configuration object where values are validators and returns the
|
||||
* corresponding TypeScript types, with undefined values made optional.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const shapePropsConfig = {
|
||||
* width: T.number,
|
||||
* height: T.number,
|
||||
* color: T.optional(T.string)
|
||||
* }
|
||||
*
|
||||
* type ShapeProps = RecordPropsType<typeof shapePropsConfig>
|
||||
* // Result: { width: number; height: number; color?: string }
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type RecordPropsType<Config extends Record<string, T.Validatable<any>>> =
|
||||
MakeUndefinedOptional<{
|
||||
[K in keyof Config]: T.TypeOf<Config[K]>
|
||||
}>
|
||||
|
||||
/**
|
||||
* A migration definition for shape or record properties.
|
||||
*
|
||||
* Defines how to transform record properties when migrating between schema versions.
|
||||
* Each migration has an `up` function to upgrade data and an optional `down` function
|
||||
* to downgrade data if needed.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const addColorMigration: TLPropsMigration = {
|
||||
* id: 'com.myapp.shape.custom/1.0.0',
|
||||
* up: (props) => {
|
||||
* // Add a default color property
|
||||
* return { ...props, color: 'black' }
|
||||
* },
|
||||
* down: (props) => {
|
||||
* // Remove the color property
|
||||
* const { color, ...rest } = props
|
||||
* return rest
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface TLPropsMigration {
|
||||
readonly id: MigrationId
|
||||
readonly dependsOn?: MigrationId[]
|
||||
// eslint-disable-next-line tldraw/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)
|
||||
}
|
||||
|
||||
/**
|
||||
* A sequence of property migrations for a record type.
|
||||
*
|
||||
* Contains an ordered array of migrations that should be applied to transform
|
||||
* record properties from one version to another. Migrations can include both
|
||||
* property-specific migrations and standalone dependency declarations.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const myShapeMigrations: TLPropsMigrations = {
|
||||
* sequence: [
|
||||
* {
|
||||
* id: 'com.myapp.shape.custom/1.0.0',
|
||||
* up: (props) => ({ ...props, version: 1 })
|
||||
* },
|
||||
* {
|
||||
* id: 'com.myapp.shape.custom/2.0.0',
|
||||
* up: (props) => ({ ...props, newFeature: true })
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface TLPropsMigrations {
|
||||
readonly sequence: Array<StandaloneDependsOn | TLPropsMigration>
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes property migrations for all record types in a schema.
|
||||
*
|
||||
* Takes a collection of record configurations and converts their migrations
|
||||
* into proper migration sequences that can be used by the store system.
|
||||
* Handles different migration formats including legacy migrations.
|
||||
*
|
||||
* @param typeName - The base type name for the records (e.g., 'shape', 'binding')
|
||||
* @param records - Record of type names to their schema configuration
|
||||
* @returns Array of processed migration sequences
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const shapeRecords = {
|
||||
* geo: { props: geoProps, migrations: geoMigrations },
|
||||
* arrow: { props: arrowProps, migrations: arrowMigrations }
|
||||
* }
|
||||
*
|
||||
* const sequences = processPropsMigrations('shape', shapeRecords)
|
||||
* ```
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
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: true,
|
||||
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: true,
|
||||
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: true,
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a store migration from a props migration definition.
|
||||
*
|
||||
* Converts a high-level property migration into a low-level store migration
|
||||
* that can be applied to records. The resulting migration will only affect
|
||||
* records of the specified type and subtype.
|
||||
*
|
||||
* @param typeName - The base type name (e.g., 'shape', 'binding')
|
||||
* @param subType - The specific subtype (e.g., 'geo', 'arrow')
|
||||
* @param m - The property migration definition
|
||||
* @returns A store migration that applies the property transformation
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const propsMigration: TLPropsMigration = {
|
||||
* id: 'com.myapp.shape.custom/1.0.0',
|
||||
* up: (props) => ({ ...props, color: 'blue' })
|
||||
* }
|
||||
*
|
||||
* const storeMigration = createPropsMigration('shape', 'custom', propsMigration)
|
||||
* ```
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user