UNPKG

@tldraw/tlschema

Version:

tldraw infinite canvas SDK (schema).

438 lines (421 loc) 15 kB
import { LegacyMigrations, MigrationSequence, StoreSchema, StoreValidator } from '@tldraw/store' import { objectMapValues } from '@tldraw/utils' import { T } from '@tldraw/validate' import { TLBaseAsset } from './assets/TLBaseAsset' import { bookmarkAssetMigrations, bookmarkAssetProps } from './assets/TLBookmarkAsset' import { imageAssetMigrations, imageAssetProps } from './assets/TLImageAsset' import { videoAssetMigrations, videoAssetProps } from './assets/TLVideoAsset' import { arrowBindingMigrations, arrowBindingProps } from './bindings/TLArrowBinding' import { TLDefaultAsset, TLUnknownAsset, assetMigrations, createAssetRecordType, } from './records/TLAsset' import { TLBinding, TLDefaultBinding, createBindingRecordType } from './records/TLBinding' import { CameraRecordType, cameraMigrations } from './records/TLCamera' import { CustomRecordInfo, createCustomRecordType, processCustomRecordMigrations, } from './records/TLCustomRecord' import { DocumentRecordType, documentMigrations } from './records/TLDocument' import { createInstanceRecordType, instanceMigrations } from './records/TLInstance' import { PageRecordType, pageMigrations } from './records/TLPage' import { InstancePageStateRecordType, instancePageStateMigrations } from './records/TLPageState' import { PointerRecordType, pointerMigrations } from './records/TLPointer' import { InstancePresenceRecordType, instancePresenceMigrations } from './records/TLPresence' import { TLRecord } from './records/TLRecord' import { TLDefaultShape, TLShape, createShapeRecordType, getShapePropKeysByStyle, rootShapeMigrations, } from './records/TLShape' import { UserRecordType, createUserRecordType, userMigrations } from './records/TLUser' import { RecordProps, TLPropsMigrations, processPropsMigrations } from './recordsWithProps' import { arrowShapeMigrations, arrowShapeProps } from './shapes/TLArrowShape' import { TLBaseShape } from './shapes/TLBaseShape' import { bookmarkShapeMigrations, bookmarkShapeProps } from './shapes/TLBookmarkShape' import { drawShapeMigrations, drawShapeProps } from './shapes/TLDrawShape' import { embedShapeMigrations, embedShapeProps } from './shapes/TLEmbedShape' import { frameShapeMigrations, frameShapeProps } from './shapes/TLFrameShape' import { geoShapeMigrations, geoShapeProps } from './shapes/TLGeoShape' import { groupShapeMigrations, groupShapeProps } from './shapes/TLGroupShape' import { highlightShapeMigrations, highlightShapeProps } from './shapes/TLHighlightShape' import { imageShapeMigrations, imageShapeProps } from './shapes/TLImageShape' import { lineShapeMigrations, lineShapeProps } from './shapes/TLLineShape' import { noteShapeMigrations, noteShapeProps } from './shapes/TLNoteShape' import { textShapeMigrations, textShapeProps } from './shapes/TLTextShape' import { videoShapeMigrations, videoShapeProps } from './shapes/TLVideoShape' import { storeMigrations } from './store-migrations' import { StyleProp } from './styles/StyleProp' import { TLStoreProps, createIntegrityChecker, onValidationFailure } from './TLStore' /** * Configuration information for a schema type (shape, binding, or asset), including its properties, * metadata, and migration sequences for data evolution over time. * * @public * @example * ```ts * import { arrowShapeMigrations, arrowShapeProps } from './shapes/TLArrowShape' * * const myShapeSchema: SchemaPropsInfo = { * migrations: arrowShapeMigrations, * props: arrowShapeProps, * meta: { * customField: T.string, * }, * } * ``` */ export interface SchemaPropsInfo { /** * Migration sequences for handling data evolution over time. Can be legacy migrations, * props-specific migrations, or general migration sequences. */ migrations?: LegacyMigrations | TLPropsMigrations | MigrationSequence /** * Validation schema for the shape, binding, or asset properties. */ props?: Record<string, StoreValidator<any>> /** * Validation schema for metadata fields. */ meta?: Record<string, StoreValidator<any>> } /** * The complete schema definition for a tldraw store, encompassing all record types, * validation rules, and migration sequences. This schema defines the structure of * the persistent data model used by tldraw. * * @public * @example * ```ts * import { createTLSchema, defaultShapeSchemas } from '@tldraw/tlschema' * import { Store } from '@tldraw/store' * * const schema: TLSchema = createTLSchema({ * shapes: defaultShapeSchemas, * }) * * const store = new Store({ schema }) * ``` */ export type TLSchema = StoreSchema<TLRecord, TLStoreProps> /** * Default shape schema configurations for all built-in tldraw shape types. * Each shape type includes its validation props and migration sequences. * * This object contains schema information for: * - arrow: Directional lines that can bind to other shapes * - bookmark: Website bookmark cards with preview information * - draw: Freehand drawing paths created with drawing tools * - embed: Embedded content from external services (YouTube, Figma, etc.) * - frame: Container shapes for organizing content * - geo: Geometric shapes (rectangles, ellipses, triangles, etc.) * - group: Logical groupings of multiple shapes * - highlight: Highlighting strokes from the highlighter tool * - image: Raster image shapes referencing image assets * - line: Multi-point lines and splines * - note: Sticky note shapes with text content * - text: Rich text shapes with formatting support * - video: Video shapes referencing video assets * * @public * @example * ```ts * import { createTLSchema, defaultShapeSchemas } from '@tldraw/tlschema' * * // Use all default shapes * const schema = createTLSchema({ * shapes: defaultShapeSchemas, * }) * * // Use only specific default shapes * const minimalSchema = createTLSchema({ * shapes: { * geo: defaultShapeSchemas.geo, * text: defaultShapeSchemas.text, * }, * }) * ``` */ export const defaultShapeSchemas = { arrow: { migrations: arrowShapeMigrations, props: arrowShapeProps }, bookmark: { migrations: bookmarkShapeMigrations, props: bookmarkShapeProps }, draw: { migrations: drawShapeMigrations, props: drawShapeProps }, embed: { migrations: embedShapeMigrations, props: embedShapeProps }, frame: { migrations: frameShapeMigrations, props: frameShapeProps }, geo: { migrations: geoShapeMigrations, props: geoShapeProps }, group: { migrations: groupShapeMigrations, props: groupShapeProps }, highlight: { migrations: highlightShapeMigrations, props: highlightShapeProps }, image: { migrations: imageShapeMigrations, props: imageShapeProps }, line: { migrations: lineShapeMigrations, props: lineShapeProps }, note: { migrations: noteShapeMigrations, props: noteShapeProps }, text: { migrations: textShapeMigrations, props: textShapeProps }, video: { migrations: videoShapeMigrations, props: videoShapeProps }, } satisfies { [T in TLDefaultShape['type']]: { migrations: SchemaPropsInfo['migrations'] props: RecordProps<TLBaseShape<T, Extract<TLDefaultShape, { type: T }>['props']>> } } /** * Default binding schema configurations for all built-in tldraw binding types. * Bindings represent relationships between shapes, such as arrows connected to shapes. * * Currently includes: * - arrow: Bindings that connect arrow shapes to other shapes at specific anchor points * * @public * @example * ```ts * import { createTLSchema, defaultBindingSchemas } from '@tldraw/tlschema' * * // Use default bindings * const schema = createTLSchema({ * bindings: defaultBindingSchemas, * }) * * // Add custom binding alongside defaults * const customSchema = createTLSchema({ * bindings: { * ...defaultBindingSchemas, * myCustomBinding: { * props: myCustomBindingProps, * migrations: myCustomBindingMigrations, * }, * }, * }) * ``` */ export const defaultBindingSchemas = { arrow: { migrations: arrowBindingMigrations, props: arrowBindingProps }, } satisfies { [T in TLDefaultBinding['type']]: SchemaPropsInfo } /** * Default asset schema configurations for all built-in tldraw asset types. * * @public * @example * ```ts * import { createTLSchema, defaultAssetSchemas } from '@tldraw/tlschema' * * const schema = createTLSchema({ * assets: defaultAssetSchemas, * }) * ``` */ export const defaultAssetSchemas = { image: { migrations: imageAssetMigrations, props: imageAssetProps }, video: { migrations: videoAssetMigrations, props: videoAssetProps }, bookmark: { migrations: bookmarkAssetMigrations, props: bookmarkAssetProps }, } satisfies { [T in TLDefaultAsset['type']]: { migrations: SchemaPropsInfo['migrations'] props: RecordProps<TLBaseAsset<T, Extract<TLDefaultAsset, { type: T }>['props']>> } } /** * Configuration for extending the user record type with custom metadata * validators and migration sequences. * * @example * ```ts * import { T } from '@tldraw/validate' * * const userSchema: UserSchemaInfo = { * meta: { * isAdmin: T.boolean, * department: T.string, * }, * } * ``` * * @public */ export interface UserSchemaInfo { /** * Validators for custom metadata fields on user records. Each field is * treated as optional — user records without these fields remain valid, * but when present, values are validated against the provided validators. */ meta?: Record<string, T.Validatable<any>> /** * Additional migration sequences for evolving custom user data over time. */ migrations?: readonly MigrationSequence[] } /** * Creates a complete TLSchema for use with tldraw stores. This schema defines the structure, * validation, and migration sequences for all record types in a tldraw application. * * The schema includes all core record types (pages, cameras, instances, etc.) plus the * shape, binding, asset, and custom record types you specify. Style properties are * automatically collected from all shapes to ensure consistency across the application. * * @param options - Configuration options for the schema * - shapes - Shape schema configurations. Defaults to defaultShapeSchemas if not provided * - bindings - Binding schema configurations. Defaults to defaultBindingSchemas if not provided * - assets - Asset schema configurations. Defaults to defaultAssetSchemas if not provided * - user - Custom user record configuration with meta validators and migrations * - records - Custom record type configurations. These are additional record types beyond * the built-in shapes, bindings, assets, etc. * - migrations - Additional migration sequences to include in the schema * @returns A complete TLSchema ready for use with Store creation * * @public * @example * ```ts * import { * createTLSchema, * defaultShapeSchemas, * defaultBindingSchemas, * defaultAssetSchemas, * } from '@tldraw/tlschema' * import { Store } from '@tldraw/store' * * // Create schema with all default shapes, bindings, and assets * const schema = createTLSchema() * * // Create schema with custom shapes added * const customSchema = createTLSchema({ * shapes: { * ...defaultShapeSchemas, * myCustomShape: { * props: myCustomShapeProps, * migrations: myCustomShapeMigrations, * }, * }, * bindings: defaultBindingSchemas, * assets: defaultAssetSchemas, * }) * * // Create schema with custom user metadata * const schemaWithCustomUser = createTLSchema({ * user: { * meta: { * isAdmin: T.boolean, * department: T.string, * }, * }, * }) * * // Create schema with custom record types * const schemaWithCustomRecords = createTLSchema({ * records: { * comment: { * scope: 'document', * validator: T.object({ * id: T.string, * typeName: T.literal('comment'), * text: T.string, * shapeId: T.string, * }), * }, * }, * }) * * // Use the schema with a store * const store = new Store({ * schema: customSchema, * props: { * defaultName: 'My Drawing', * }, * }) * ``` */ export function createTLSchema({ shapes = defaultShapeSchemas, bindings = defaultBindingSchemas, assets = defaultAssetSchemas, user, records = {}, migrations, }: { shapes?: Record<string, SchemaPropsInfo> bindings?: Record<string, SchemaPropsInfo> assets?: Record<string, SchemaPropsInfo> user?: UserSchemaInfo records?: Record<string, CustomRecordInfo> migrations?: readonly MigrationSequence[] } = {}): TLSchema { const stylesById = new Map<string, StyleProp<unknown>>() for (const shape of objectMapValues(shapes)) { for (const style of getShapePropKeysByStyle(shape.props ?? {}).keys()) { if (stylesById.has(style.id) && stylesById.get(style.id) !== style) { throw new Error(`Multiple StyleProp instances with the same id: ${style.id}`) } stylesById.set(style.id, style) } } const ShapeRecordType = createShapeRecordType(shapes) const BindingRecordType = createBindingRecordType(bindings) const _AssetRecordType = createAssetRecordType(assets) const InstanceRecordType = createInstanceRecordType(stylesById) const CustomUserRecordType = user ? createUserRecordType(user) : UserRecordType // Create RecordTypes for custom records const builtInTypeNames = new Set([ 'asset', 'binding', 'camera', 'document', 'instance', 'instance_page_state', 'page', 'instance_presence', 'pointer', 'shape', 'store', 'user', ]) const customRecordTypes: Record<string, { createId: any }> = {} for (const [typeName, config] of Object.entries(records)) { if (builtInTypeNames.has(typeName)) { throw new Error( `Custom record type name '${typeName}' conflicts with tldraw's built-in record type of that name. Choose a different name instead.` ) } customRecordTypes[typeName] = createCustomRecordType(typeName, config) } return StoreSchema.create( { asset: _AssetRecordType, binding: BindingRecordType, camera: CameraRecordType, document: DocumentRecordType, instance: InstanceRecordType, instance_page_state: InstancePageStateRecordType, page: PageRecordType, instance_presence: InstancePresenceRecordType, pointer: PointerRecordType, shape: ShapeRecordType, user: CustomUserRecordType, ...customRecordTypes, }, { migrations: [ storeMigrations, assetMigrations, cameraMigrations, documentMigrations, instanceMigrations, instancePageStateMigrations, pageMigrations, instancePresenceMigrations, pointerMigrations, rootShapeMigrations, userMigrations, ...processPropsMigrations<TLUnknownAsset>('asset', assets), ...processPropsMigrations<TLShape>('shape', shapes), ...processPropsMigrations<TLBinding>('binding', bindings), ...processCustomRecordMigrations(records), ...(user?.migrations ?? []), ...(migrations ?? []), ], onValidationFailure, createIntegrityChecker, } ) }