@tldraw/tlschema
Version:
tldraw infinite canvas SDK (schema).
435 lines (418 loc) • 12.3 kB
text/typescript
import {
RecordId,
UnknownRecord,
createMigrationIds,
createRecordMigrationSequence,
createRecordType,
} from '@tldraw/store'
import { mapObjectMapValues, uniqueId } from '@tldraw/utils'
import { T } from '@tldraw/validate'
import { TLArrowBinding } from '../bindings/TLArrowBinding'
import { TLBaseBinding, createBindingValidator } from '../bindings/TLBaseBinding'
import { SchemaPropsInfo } from '../createTLSchema'
import { TLPropsMigrations } from '../recordsWithProps'
/**
* The default set of bindings that are available in the editor.
* Currently includes only arrow bindings, but can be extended with custom bindings.
*
* @example
* ```ts
* // Arrow binding connects an arrow to shapes
* const arrowBinding: TLDefaultBinding = {
* id: 'binding:arrow1',
* typeName: 'binding',
* type: 'arrow',
* fromId: 'shape:arrow1',
* toId: 'shape:rectangle1',
* props: {
* terminal: 'end',
* normalizedAnchor: { x: 0.5, y: 0.5 },
* isExact: false,
* isPrecise: true
* }
* }
* ```
*
* @public
*/
export type TLDefaultBinding = TLArrowBinding
/**
* A type for a binding that is available in the editor but whose type is
* unknown—either one of the editor's default bindings or else a custom binding.
* Used internally for type-safe handling of bindings with unknown structure.
*
* @example
* ```ts
* // Function that works with any binding type
* function processBinding(binding: TLUnknownBinding) {
* console.log(`Processing ${binding.type} binding from ${binding.fromId} to ${binding.toId}`)
* // Handle binding properties generically
* }
* ```
*
* @public
*/
export type TLUnknownBinding = TLBaseBinding<string, object>
/** @public */
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface TLGlobalBindingPropsMap {}
/** @public */
// prettier-ignore
export type TLIndexedBindings = {
// We iterate over a union of augmented keys and default binding types.
// This allows us to include (or conditionally exclude or override) the default bindings in one go.
//
// In the `as` clause we are filtering out disabled bindings.
[K in keyof TLGlobalBindingPropsMap | TLDefaultBinding['type'] as K extends TLDefaultBinding['type']
? K extends keyof TLGlobalBindingPropsMap
? // if it extends a nullish value the user has disabled this binding type so we filter it out with never
TLGlobalBindingPropsMap[K] extends null | undefined
? never
: K
: K
: K]: K extends TLDefaultBinding['type']
? // if it's a default binding type we need to check if it's been overridden
K extends keyof TLGlobalBindingPropsMap
? // if it has been overriden then use the custom binding definition
TLBaseBinding<K, TLGlobalBindingPropsMap[K]>
: // if it has not been overriden then reuse existing type aliases for better type display
Extract<TLDefaultBinding, { type: K }>
: // use the custom binding definition
TLBaseBinding<K, TLGlobalBindingPropsMap[K & keyof TLGlobalBindingPropsMap]>
}
/**
* The set of all bindings that are available in the editor.
* Bindings represent relationships between shapes, such as arrows connecting to other shapes.
*
* You can use this type without a type argument to work with any binding, or pass
* a specific binding type string (e.g., `'arrow'`) to narrow down to that specific binding type.
*
* @example
* ```ts
* // Check binding type and handle accordingly
* function handleBinding(binding: TLBinding) {
* switch (binding.type) {
* case 'arrow':
* // Handle arrow binding
* break
* default:
* // Handle unknown custom binding
* break
* }
* }
*
* // Narrow to a specific binding type by passing the type as a generic argument
* function getArrowSourceId(binding: TLBinding<'arrow'>) {
* return binding.fromId // TypeScript knows this is a TLArrowBinding
* }
* ```
*
* @public
*/
export type TLBinding<K extends keyof TLIndexedBindings = keyof TLIndexedBindings> =
TLIndexedBindings[K]
/**
* Type for updating existing bindings with partial properties.
* Only the id and type are required, all other properties are optional.
*
* @example
* ```ts
* // Update arrow binding properties
* const bindingUpdate: TLBindingUpdate<TLArrowBinding> = {
* id: 'binding:arrow1',
* type: 'arrow',
* props: {
* normalizedAnchor: { x: 0.7, y: 0.3 } // Only update anchor position
* }
* }
*
* editor.updateBindings([bindingUpdate])
* ```
*
* @public
*/
export type TLBindingUpdate<T extends TLBinding = TLBinding> = T extends T
? {
id: TLBindingId
type: T['type']
typeName?: T['typeName']
fromId?: T['fromId']
toId?: T['toId']
props?: Partial<T['props']>
meta?: Partial<T['meta']>
}
: never
/**
* Type for creating new bindings with required fromId and toId.
* The id is optional and will be generated if not provided.
*
* @example
* ```ts
* // Create a new arrow binding
* const newBinding: TLBindingCreate<TLArrowBinding> = {
* type: 'arrow',
* fromId: 'shape:arrow1',
* toId: 'shape:rectangle1',
* props: {
* terminal: 'end',
* normalizedAnchor: { x: 0.5, y: 0.5 },
* isExact: false,
* isPrecise: true
* }
* }
*
* editor.createBindings([newBinding])
* ```
*
* @public
*/
export type TLBindingCreate<T extends TLBinding = TLBinding> = T extends T
? {
id?: TLBindingId
type: T['type']
typeName?: T['typeName']
fromId: T['fromId']
toId: T['toId']
props?: Partial<T['props']>
meta?: Partial<T['meta']>
}
: never
/**
* Branded string type for binding record identifiers.
* Prevents mixing binding IDs with other types of record IDs at compile time.
*
* @example
* ```ts
* import { createBindingId } from '@tldraw/tlschema'
*
* // Create a new binding ID
* const bindingId: TLBindingId = createBindingId()
*
* // Use in binding records
* const binding: TLBinding = {
* id: bindingId,
* type: 'arrow',
* fromId: 'shape:arrow1',
* toId: 'shape:rectangle1',
* // ... other properties
* }
* ```
*
* @public
*/
export type TLBindingId = RecordId<TLBinding>
/**
* Migration version identifiers for the root binding record schema.
* Currently empty as no migrations have been applied to the base binding structure.
*
* @example
* ```ts
* // Future migrations would be defined here
* const rootBindingVersions = createMigrationIds('com.tldraw.binding', {
* AddNewProperty: 1,
* } as const)
* ```
*
* @public
*/
export const rootBindingVersions = createMigrationIds('com.tldraw.binding', {} as const)
/**
* Migration sequence for the root binding record structure.
* Currently empty as the binding schema has not required any migrations yet.
*
* @example
* ```ts
* // Migrations would be automatically applied when loading old documents
* const migratedStore = migrator.migrateStoreSnapshot({
* schema: oldSchema,
* store: oldStoreSnapshot
* })
* ```
*
* @public
*/
export const rootBindingMigrations = createRecordMigrationSequence({
sequenceId: 'com.tldraw.binding',
recordType: 'binding',
sequence: [],
})
/**
* Type guard to check if a record is a TLBinding.
* Useful for filtering or type narrowing when working with mixed record types.
*
* @param record - The record to check
* @returns True if the record is a binding, false otherwise
*
* @example
* ```ts
* // Filter bindings from mixed records
* const allRecords = store.allRecords()
* const bindings = allRecords.filter(isBinding)
*
* // Type guard usage
* function processRecord(record: UnknownRecord) {
* if (isBinding(record)) {
* // record is now typed as TLBinding
* console.log(`Binding from ${record.fromId} to ${record.toId}`)
* }
* }
* ```
*
* @public
*/
export function isBinding(record?: UnknownRecord): record is TLBinding {
if (!record) return false
return record.typeName === 'binding'
}
/**
* Type guard to check if a string is a valid TLBindingId.
* Validates that the ID follows the correct format for binding identifiers.
*
* @param id - The string to check
* @returns True if the string is a valid binding ID, false otherwise
*
* @example
* ```ts
* // Validate binding IDs
* const maybeBindingId = 'binding:abc123'
* if (isBindingId(maybeBindingId)) {
* // maybeBindingId is now typed as TLBindingId
* const binding = store.get(maybeBindingId)
* }
*
* // Filter binding IDs from mixed ID array
* const mixedIds = ['shape:1', 'binding:2', 'page:3']
* const bindingIds = mixedIds.filter(isBindingId)
* ```
*
* @public
*/
export function isBindingId(id?: string): id is TLBindingId {
if (!id) return false
return id.startsWith('binding:')
}
/**
* Creates a new TLBindingId with proper formatting.
* Generates a unique ID if none is provided, or formats a provided ID correctly.
*
* @param id - Optional custom ID suffix. If not provided, a unique ID is generated
* @returns A properly formatted binding ID
*
* @example
* ```ts
* // Create with auto-generated ID
* const bindingId1 = createBindingId() // 'binding:abc123'
*
* // Create with custom ID
* const bindingId2 = createBindingId('myCustomBinding') // 'binding:myCustomBinding'
*
* // Use in binding creation
* const binding: TLBinding = {
* id: createBindingId(),
* type: 'arrow',
* fromId: 'shape:arrow1',
* toId: 'shape:rectangle1',
* // ... other properties
* }
* ```
*
* @public
*/
export function createBindingId(id?: string): TLBindingId {
return `binding:${id ?? uniqueId()}` as TLBindingId
}
/**
* Creates a migration sequence for binding properties.
* This is a pass-through function that validates and returns the provided migrations.
*
* @param migrations - The migration sequence for binding properties
* @returns The validated migration sequence
*
* @example
* ```ts
* // Define migrations for custom binding properties
* const myBindingMigrations = createBindingPropsMigrationSequence({
* sequence: [
* {
* id: 'com.myapp.binding.custom/1.0.0',
* up: (props) => ({ ...props, newProperty: 'default' }),
* down: ({ newProperty, ...props }) => props
* }
* ]
* })
* ```
*
* @public
*/
export function createBindingPropsMigrationSequence(
migrations: TLPropsMigrations
): TLPropsMigrations {
return migrations
}
/**
* Creates properly formatted migration IDs for binding property migrations.
* Follows the convention: 'com.tldraw.binding.\{bindingType\}/\{version\}'
*
* @param bindingType - The type of binding these migrations apply to
* @param ids - Object mapping migration names to version numbers
* @returns Object with formatted migration IDs
*
* @example
* ```ts
* // Create migration IDs for custom binding
* const myBindingVersions = createBindingPropsMigrationIds('myCustomBinding', {
* AddNewProperty: 1,
* UpdateProperty: 2
* })
*
* // Result:
* // {
* // AddNewProperty: 'com.tldraw.binding.myCustomBinding/1',
* // UpdateProperty: 'com.tldraw.binding.myCustomBinding/2'
* // }
* ```
*
* @public
*/
export function createBindingPropsMigrationIds<S extends string, T extends Record<string, number>>(
bindingType: S,
ids: T
): { [k in keyof T]: `com.tldraw.binding.${S}/${T[k]}` } {
return mapObjectMapValues(ids, (_k, v) => `com.tldraw.binding.${bindingType}/${v}`) as any
}
/**
* Creates a record type for TLBinding with validation based on the provided binding schemas.
* This function is used internally to configure the binding record type in the schema.
*
* @param bindings - Record mapping binding type names to their schema information
* @returns A configured record type for bindings with validation
*
* @example
* ```ts
* // Used internally when creating schemas
* const bindingRecordType = createBindingRecordType({
* arrow: {
* props: arrowBindingProps,
* meta: arrowBindingMeta
* }
* })
* ```
*
* @internal
*/
export function createBindingRecordType(bindings: Record<string, SchemaPropsInfo>) {
return createRecordType('binding', {
scope: 'document',
validator: T.model(
'binding',
T.union(
'type',
mapObjectMapValues(bindings, (type, { props, meta }) =>
createBindingValidator(type, props, meta)
)
)
),
}).withDefaultProperties(() => ({
meta: {},
}))
}