2023-04-25 11:01:25 +00:00
|
|
|
import { assert } from '@tldraw/utils'
|
2023-06-03 20:46:53 +00:00
|
|
|
import { BaseRecord, RecordId } from '../BaseRecord'
|
2023-04-25 11:01:25 +00:00
|
|
|
import { createRecordType } from '../RecordType'
|
2023-06-27 12:25:55 +00:00
|
|
|
import { SerializedStore } from '../Store'
|
2023-04-25 11:01:25 +00:00
|
|
|
import { StoreSchema } from '../StoreSchema'
|
2024-04-15 12:53:42 +00:00
|
|
|
import { createMigrationIds, createMigrationSequence } from '../migrate'
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2024-04-15 12:53:42 +00:00
|
|
|
const UserVersion = createMigrationIds('com.tldraw.user', {
|
2023-04-25 11:01:25 +00:00
|
|
|
AddLocale: 1,
|
|
|
|
AddPhoneNumber: 2,
|
2024-04-15 12:53:42 +00:00
|
|
|
} as const)
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
/** A user of tldraw */
|
2023-06-03 20:46:53 +00:00
|
|
|
interface User extends BaseRecord<'user', RecordId<User>> {
|
2023-04-25 11:01:25 +00:00
|
|
|
name: string
|
|
|
|
locale: string
|
|
|
|
phoneNumber: string | null
|
|
|
|
}
|
|
|
|
|
2024-04-15 12:53:42 +00:00
|
|
|
const userMigrations = createMigrationSequence({
|
|
|
|
sequenceId: 'com.tldraw.user',
|
|
|
|
retroactive: true,
|
|
|
|
sequence: [
|
|
|
|
{
|
|
|
|
id: UserVersion.AddLocale,
|
|
|
|
scope: 'record',
|
|
|
|
filter: (r) => r.typeName === 'user',
|
|
|
|
up: (record: any) => {
|
|
|
|
record.locale = 'en'
|
|
|
|
},
|
|
|
|
down: (record: any) => {
|
|
|
|
delete record.locale
|
2023-04-25 11:01:25 +00:00
|
|
|
},
|
|
|
|
},
|
2024-04-15 12:53:42 +00:00
|
|
|
{
|
|
|
|
id: UserVersion.AddPhoneNumber,
|
|
|
|
scope: 'record',
|
|
|
|
filter: (r) => r.typeName === 'user',
|
|
|
|
up: (record: any) => {
|
|
|
|
record.phoneNumber = null
|
|
|
|
},
|
|
|
|
down: (record: any) => {
|
|
|
|
delete record.phoneNumber
|
2023-04-25 11:01:25 +00:00
|
|
|
},
|
|
|
|
},
|
2024-04-15 12:53:42 +00:00
|
|
|
],
|
2023-04-25 11:01:25 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
const User = createRecordType<User>('user', {
|
|
|
|
validator: {
|
|
|
|
validate: (record) => {
|
|
|
|
assert(record && typeof record === 'object')
|
|
|
|
assert('id' in record && typeof record.id === 'string')
|
|
|
|
assert('name' in record && typeof record.name === 'string')
|
|
|
|
assert('locale' in record && typeof record.locale === 'string')
|
|
|
|
assert(
|
|
|
|
'phoneNumber' in record &&
|
|
|
|
(record.phoneNumber === null || typeof record.phoneNumber === 'string')
|
|
|
|
)
|
|
|
|
return record as User
|
|
|
|
},
|
|
|
|
},
|
derived presence state (#1204)
This PR adds
- A new `TLInstancePresence` record type, to collect info about the
presence state in a particular instance of the editor. This will
eventually be used to sync presence data instead of sending
instance-only state across the wire.
- **Record Scopes**
`RecordType` now has a `scope` property which can be one of three
things:
- `document`: the record belongs to the document and should be synced
and persisted freely. Currently: `TLDocument`, `TLPage`, `TLShape`, and
`TLAsset`
- `instance`: the record belongs to a single instance of the store and
should not be synced at all. It should not be persisted directly in most
cases, but rather compiled into a kind of 'instance configuration' to
store alongside the local document data so that when reopening the
associated document it can remember some of the previous instance state.
Currently: `TLInstance`, `TLInstancePageState`, `TLCamera`, `TLUser`,
`TLUserDocument`, `TLUserPresence`
- `presence`: the record belongs to a single instance of the store and
should not be persisted, but may be synced using the special presence
sync protocol. Currently just `TLInstancePresence`
This sets us up for the following changes, which are gonna be pretty
high-impact in terms of integrating tldraw into existing systems:
- Removing `instanceId` as a config option. Each instance gets a
randomly generated ID.
- We'd replace it with an `instanceConfig` option that has stuff like
selectedIds, camera positions, and so on. Then it's up to library users
to get and reinstate the instance config at persistence boundaries.
- Removing `userId` as config option, and removing the `TLUser` type
altogether.
- We might need to revisit when doing auth-enabled features like locking
shapes, but I suspect that will be separate.
2023-04-27 18:03:19 +00:00
|
|
|
scope: 'document',
|
2023-04-25 11:01:25 +00:00
|
|
|
}).withDefaultProperties(() => ({
|
|
|
|
/* STEP 6: Add any new default values for properties here */
|
|
|
|
name: 'New User',
|
|
|
|
}))
|
|
|
|
|
2024-04-15 12:53:42 +00:00
|
|
|
const ShapeVersion = createMigrationIds('com.tldraw.shape', {
|
2023-04-25 11:01:25 +00:00
|
|
|
AddRotation: 1,
|
|
|
|
AddParent: 2,
|
2024-04-15 12:53:42 +00:00
|
|
|
} as const)
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2024-04-15 12:53:42 +00:00
|
|
|
const RectangleVersion = createMigrationIds('com.tldraw.shape.rectangle', {
|
2023-04-25 11:01:25 +00:00
|
|
|
AddOpacity: 1,
|
2024-04-15 12:53:42 +00:00
|
|
|
} as const)
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2024-04-15 12:53:42 +00:00
|
|
|
const OvalVersion = createMigrationIds('com.tldraw.shape.oval', {
|
2023-04-25 11:01:25 +00:00
|
|
|
AddBorderStyle: 1,
|
2024-04-15 12:53:42 +00:00
|
|
|
} as const)
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2023-06-03 20:46:53 +00:00
|
|
|
type ShapeId = RecordId<Shape<object>>
|
2023-05-24 11:25:41 +00:00
|
|
|
|
|
|
|
interface Shape<Props> extends BaseRecord<'shape', ShapeId> {
|
2023-04-25 11:01:25 +00:00
|
|
|
type: string
|
|
|
|
x: number
|
|
|
|
y: number
|
|
|
|
rotation: number
|
2023-05-24 11:25:41 +00:00
|
|
|
parentId: ShapeId | null
|
2023-04-25 11:01:25 +00:00
|
|
|
props: Props
|
|
|
|
}
|
|
|
|
|
|
|
|
interface RectangleProps {
|
|
|
|
width: number
|
|
|
|
height: number
|
|
|
|
opactiy: number
|
|
|
|
}
|
|
|
|
|
|
|
|
interface OvalProps {
|
|
|
|
radius: number
|
|
|
|
borderStyle: 'solid' | 'dashed'
|
|
|
|
}
|
|
|
|
|
2024-04-15 12:53:42 +00:00
|
|
|
const rootShapeMigrations = createMigrationSequence({
|
|
|
|
sequenceId: 'com.tldraw.shape',
|
|
|
|
retroactive: true,
|
|
|
|
sequence: [
|
|
|
|
{
|
|
|
|
id: ShapeVersion.AddRotation,
|
|
|
|
scope: 'record',
|
|
|
|
filter: (r) => r.typeName === 'shape',
|
|
|
|
up: (record: any) => {
|
|
|
|
record.rotation = 0
|
|
|
|
},
|
|
|
|
down: (record: any) => {
|
|
|
|
delete record.rotation
|
2023-04-25 11:01:25 +00:00
|
|
|
},
|
|
|
|
},
|
2024-04-15 12:53:42 +00:00
|
|
|
{
|
|
|
|
id: ShapeVersion.AddParent,
|
|
|
|
scope: 'record',
|
|
|
|
filter: (r) => r.typeName === 'shape',
|
|
|
|
up: (record: any) => {
|
|
|
|
record.parentId = null
|
|
|
|
},
|
|
|
|
down: (record: any) => {
|
|
|
|
delete record.parentId
|
2023-04-25 11:01:25 +00:00
|
|
|
},
|
|
|
|
},
|
2024-04-15 12:53:42 +00:00
|
|
|
],
|
|
|
|
})
|
|
|
|
|
|
|
|
const rectangleMigrations = createMigrationSequence({
|
|
|
|
sequenceId: 'com.tldraw.shape.rectangle',
|
|
|
|
retroactive: true,
|
|
|
|
sequence: [
|
|
|
|
{
|
|
|
|
id: RectangleVersion.AddOpacity,
|
|
|
|
scope: 'record',
|
|
|
|
filter: (r) => r.typeName === 'shape' && (r as Shape<RectangleProps>).type === 'rectangle',
|
|
|
|
up: (record: any) => {
|
|
|
|
record.props.opacity = 1
|
2023-04-25 11:01:25 +00:00
|
|
|
},
|
2024-04-15 12:53:42 +00:00
|
|
|
down: (record: any) => {
|
|
|
|
delete record.props.opacity
|
2023-04-25 11:01:25 +00:00
|
|
|
},
|
2024-04-15 12:53:42 +00:00
|
|
|
},
|
|
|
|
],
|
|
|
|
})
|
|
|
|
|
|
|
|
const ovalMigrations = createMigrationSequence({
|
|
|
|
sequenceId: 'com.tldraw.shape.oval',
|
|
|
|
retroactive: true,
|
|
|
|
sequence: [
|
|
|
|
{
|
|
|
|
id: OvalVersion.AddBorderStyle,
|
|
|
|
scope: 'record',
|
|
|
|
filter: (r) => r.typeName === 'shape' && (r as Shape<OvalProps>).type === 'oval',
|
|
|
|
up: (record: any) => {
|
|
|
|
record.props.borderStyle = 'solid'
|
|
|
|
},
|
|
|
|
down: (record: any) => {
|
|
|
|
delete record.props.borderStyle
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
2023-04-25 11:01:25 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
const Shape = createRecordType<Shape<RectangleProps | OvalProps>>('shape', {
|
|
|
|
validator: {
|
|
|
|
validate: (record) => {
|
|
|
|
assert(record && typeof record === 'object')
|
|
|
|
assert('id' in record && typeof record.id === 'string')
|
|
|
|
assert('type' in record && typeof record.type === 'string')
|
|
|
|
assert('x' in record && typeof record.x === 'number')
|
|
|
|
assert('y' in record && typeof record.y === 'number')
|
|
|
|
assert('rotation' in record && typeof record.rotation === 'number')
|
|
|
|
return record as Shape<RectangleProps | OvalProps>
|
|
|
|
},
|
|
|
|
},
|
derived presence state (#1204)
This PR adds
- A new `TLInstancePresence` record type, to collect info about the
presence state in a particular instance of the editor. This will
eventually be used to sync presence data instead of sending
instance-only state across the wire.
- **Record Scopes**
`RecordType` now has a `scope` property which can be one of three
things:
- `document`: the record belongs to the document and should be synced
and persisted freely. Currently: `TLDocument`, `TLPage`, `TLShape`, and
`TLAsset`
- `instance`: the record belongs to a single instance of the store and
should not be synced at all. It should not be persisted directly in most
cases, but rather compiled into a kind of 'instance configuration' to
store alongside the local document data so that when reopening the
associated document it can remember some of the previous instance state.
Currently: `TLInstance`, `TLInstancePageState`, `TLCamera`, `TLUser`,
`TLUserDocument`, `TLUserPresence`
- `presence`: the record belongs to a single instance of the store and
should not be persisted, but may be synced using the special presence
sync protocol. Currently just `TLInstancePresence`
This sets us up for the following changes, which are gonna be pretty
high-impact in terms of integrating tldraw into existing systems:
- Removing `instanceId` as a config option. Each instance gets a
randomly generated ID.
- We'd replace it with an `instanceConfig` option that has stuff like
selectedIds, camera positions, and so on. Then it's up to library users
to get and reinstate the instance config at persistence boundaries.
- Removing `userId` as config option, and removing the `TLUser` type
altogether.
- We might need to revisit when doing auth-enabled features like locking
shapes, but I suspect that will be separate.
2023-04-27 18:03:19 +00:00
|
|
|
scope: 'document',
|
2023-04-25 11:01:25 +00:00
|
|
|
}).withDefaultProperties(() => ({
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
rotation: 0,
|
|
|
|
parentId: null,
|
|
|
|
}))
|
|
|
|
|
2024-04-15 12:53:42 +00:00
|
|
|
const StoreVersions = createMigrationIds('com.tldraw.store', {
|
2023-04-25 11:01:25 +00:00
|
|
|
RemoveOrg: 1,
|
2024-04-15 12:53:42 +00:00
|
|
|
})
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2024-04-15 12:53:42 +00:00
|
|
|
const snapshotMigrations = createMigrationSequence({
|
|
|
|
sequenceId: 'com.tldraw.store',
|
|
|
|
retroactive: true,
|
|
|
|
sequence: [
|
|
|
|
{
|
|
|
|
id: StoreVersions.RemoveOrg,
|
|
|
|
scope: 'store',
|
2023-06-27 12:25:55 +00:00
|
|
|
up: (store: SerializedStore<any>) => {
|
2023-04-25 11:01:25 +00:00
|
|
|
return Object.fromEntries(Object.entries(store).filter(([_, r]) => r.typeName !== 'org'))
|
|
|
|
},
|
2023-06-27 12:25:55 +00:00
|
|
|
down: (store: SerializedStore<any>) => {
|
2023-04-25 11:01:25 +00:00
|
|
|
// noop
|
|
|
|
return store
|
|
|
|
},
|
|
|
|
},
|
2024-04-15 12:53:42 +00:00
|
|
|
],
|
2023-04-25 11:01:25 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
export const testSchemaV1 = StoreSchema.create<User | Shape<any>>(
|
|
|
|
{
|
|
|
|
user: User,
|
|
|
|
shape: Shape,
|
|
|
|
},
|
|
|
|
{
|
2024-04-15 12:53:42 +00:00
|
|
|
migrations: [
|
|
|
|
snapshotMigrations,
|
|
|
|
rootShapeMigrations,
|
|
|
|
rectangleMigrations,
|
|
|
|
ovalMigrations,
|
|
|
|
userMigrations,
|
|
|
|
],
|
2023-04-25 11:01:25 +00:00
|
|
|
}
|
|
|
|
)
|