Faster validations + record reference stability at the same time (#2848)
This PR adds a validation mode whereby previous known-to-be-valid values can be used to speed up the validation process itself. At the same time it enables us to do fine-grained equality checking on records much more quickly than by using something like lodash isEqual, and using that we can prevent triggering effects for record updates that don't actually alter any values in the store. Here's some preliminary perf testing of average time spent in `store.put()` during some common interactions | task | before (ms) | after (ms) | | ---- | ---- | ---- | | drawing lines | 0.0403 | 0.0214 | | drawing boxes | 0.0408 | 0.0348 | | translating lines | 0.0352 | 0.0042 | | translating boxes | 0.0051 | 0.0032 | | rotating lines | 0.0312 | 0.0065 | | rotating boxes | 0.0053 | 0.0035 | | brush selecting boxes | 0.0200 | 0.0232 | | traversal with shapes | 0.0130 | 0.0108 | | traversal without shapes | 0.0201 | 0.0173 | **traversal** means moving the camera and pointer around the canvas #### Discussion At the scale of hundredths of a millisecond these .put operations are so fast that even if they became literally instantaneous the change would not be human perceptible. That said, there is an overall marked improvement here. Especially for dealing with draw shapes. These figures are also mostly in line with expectations, aside from a couple of things: - I don't understand why the `brush selecting boxes` task got slower after the change. - I don't understand why the `traversal` tasks are slower than the `translating boxes` task, both before and after. I would expect that .putting shape records would be much slower than .putting pointer/camera records (since the latter have fewer and simpler properties) ### Change Type - [x] `patch` — Bug fix ### Test Plan 1. Add a step-by-step description of how to test your PR here. 2. - [ ] Unit Tests - [ ] End to end tests ### Release Notes - Add a brief release note for your PR here.
This commit is contained in:
parent
50f77fe75c
commit
4a2040f92c
35 changed files with 1486 additions and 293 deletions
|
@ -11,7 +11,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "cross-env NODE_ENV=development wrangler dev --log-level info --persist-to tmp-assets",
|
"dev": "cross-env NODE_ENV=development wrangler dev --log-level info --persist-to tmp-assets",
|
||||||
"test-ci": "lazy inherit --passWithNoTests",
|
"test-ci": "lazy inherit --passWithNoTests",
|
||||||
"test": "yarn run -T jest",
|
"test": "yarn run -T jest --passWithNoTests",
|
||||||
"test-coverage": "lazy inherit --passWithNoTests",
|
"test-coverage": "lazy inherit --passWithNoTests",
|
||||||
"lint": "yarn run -T tsx ../../scripts/lint.ts"
|
"lint": "yarn run -T tsx ../../scripts/lint.ts"
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,7 +15,7 @@ const SIZES = [
|
||||||
{ value: 'm', icon: 'size-medium' },
|
{ value: 'm', icon: 'size-medium' },
|
||||||
{ value: 'l', icon: 'size-large' },
|
{ value: 'l', icon: 'size-large' },
|
||||||
{ value: 'xl', icon: 'size-extra-large' },
|
{ value: 'xl', icon: 'size-extra-large' },
|
||||||
]
|
] as const
|
||||||
|
|
||||||
// There's a guide at the bottom of this file!
|
// There's a guide at the bottom of this file!
|
||||||
|
|
||||||
|
|
|
@ -31,9 +31,12 @@ export const FilterStyleUi = track(function FilterStyleUi() {
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
editor.batch(() => {
|
editor.batch(() => {
|
||||||
if (editor.isIn('select')) {
|
if (editor.isIn('select')) {
|
||||||
editor.setStyleForSelectedShapes(MyFilterStyle, e.target.value)
|
editor.setStyleForSelectedShapes(
|
||||||
|
MyFilterStyle,
|
||||||
|
MyFilterStyle.validate(e.target.value)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
editor.setStyleForNextShapes(MyFilterStyle, e.target.value)
|
editor.setStyleForNextShapes(MyFilterStyle, MyFilterStyle.validate(e.target.value))
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
"check-scripts": "tsx scripts/check-scripts.ts",
|
"check-scripts": "tsx scripts/check-scripts.ts",
|
||||||
"api-check": "lazy api-check",
|
"api-check": "lazy api-check",
|
||||||
"test-ci": "lazy test-ci",
|
"test-ci": "lazy test-ci",
|
||||||
"test": "yarn run -T jest",
|
"test": "lazy test",
|
||||||
"test-coverage": "lazy test-coverage && node scripts/offer-coverage.mjs",
|
"test-coverage": "lazy test-coverage && node scripts/offer-coverage.mjs",
|
||||||
"e2e": "lazy e2e --filter='apps/examples'"
|
"e2e": "lazy e2e --filter='apps/examples'"
|
||||||
},
|
},
|
||||||
|
|
|
@ -32,6 +32,7 @@ import { Signal } from '@tldraw/state';
|
||||||
import { StoreSchema } from '@tldraw/store';
|
import { StoreSchema } from '@tldraw/store';
|
||||||
import { StoreSnapshot } from '@tldraw/store';
|
import { StoreSnapshot } from '@tldraw/store';
|
||||||
import { StyleProp } from '@tldraw/tlschema';
|
import { StyleProp } from '@tldraw/tlschema';
|
||||||
|
import { StylePropValue } from '@tldraw/tlschema';
|
||||||
import { TLArrowShape } from '@tldraw/tlschema';
|
import { TLArrowShape } from '@tldraw/tlschema';
|
||||||
import { TLArrowShapeArrowheadStyle } from '@tldraw/tlschema';
|
import { TLArrowShapeArrowheadStyle } from '@tldraw/tlschema';
|
||||||
import { TLAsset } from '@tldraw/tlschema';
|
import { TLAsset } from '@tldraw/tlschema';
|
||||||
|
@ -864,7 +865,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
setOpacityForSelectedShapes(opacity: number, historyOptions?: TLCommandHistoryOptions): this;
|
setOpacityForSelectedShapes(opacity: number, historyOptions?: TLCommandHistoryOptions): this;
|
||||||
setSelectedShapes(shapes: TLShape[] | TLShapeId[], historyOptions?: TLCommandHistoryOptions): this;
|
setSelectedShapes(shapes: TLShape[] | TLShapeId[], historyOptions?: TLCommandHistoryOptions): this;
|
||||||
setStyleForNextShapes<T>(style: StyleProp<T>, value: T, historyOptions?: TLCommandHistoryOptions): this;
|
setStyleForNextShapes<T>(style: StyleProp<T>, value: T, historyOptions?: TLCommandHistoryOptions): this;
|
||||||
setStyleForSelectedShapes<T>(style: StyleProp<T>, value: T, historyOptions?: TLCommandHistoryOptions): this;
|
setStyleForSelectedShapes<S extends StyleProp<any>>(style: S, value: StylePropValue<S>, historyOptions?: TLCommandHistoryOptions): this;
|
||||||
shapeUtils: {
|
shapeUtils: {
|
||||||
readonly [K in string]?: ShapeUtil<TLUnknownShape>;
|
readonly [K in string]?: ShapeUtil<TLUnknownShape>;
|
||||||
};
|
};
|
||||||
|
@ -884,7 +885,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
stretchShapes(shapes: TLShape[] | TLShapeId[], operation: 'horizontal' | 'vertical'): this;
|
stretchShapes(shapes: TLShape[] | TLShapeId[], operation: 'horizontal' | 'vertical'): this;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
styleProps: {
|
styleProps: {
|
||||||
[key: string]: Map<StyleProp<unknown>, string>;
|
[key: string]: Map<StyleProp<any>, string>;
|
||||||
};
|
};
|
||||||
readonly textMeasure: TextManager;
|
readonly textMeasure: TextManager;
|
||||||
toggleLock(shapes: TLShape[] | TLShapeId[]): this;
|
toggleLock(shapes: TLShape[] | TLShapeId[]): this;
|
||||||
|
@ -1462,10 +1463,10 @@ export { react }
|
||||||
// @public
|
// @public
|
||||||
export class ReadonlySharedStyleMap {
|
export class ReadonlySharedStyleMap {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
[Symbol.iterator](): IterableIterator<[StyleProp<unknown>, SharedStyle<unknown>]>;
|
[Symbol.iterator](): IterableIterator<[StyleProp<any>, SharedStyle<unknown>]>;
|
||||||
constructor(entries?: Iterable<[StyleProp<unknown>, SharedStyle<unknown>]>);
|
constructor(entries?: Iterable<[StyleProp<unknown>, SharedStyle<unknown>]>);
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
entries(): IterableIterator<[StyleProp<unknown>, SharedStyle<unknown>]>;
|
entries(): IterableIterator<[StyleProp<any>, SharedStyle<unknown>]>;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
equals(other: ReadonlySharedStyleMap): boolean;
|
equals(other: ReadonlySharedStyleMap): boolean;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
@ -1473,9 +1474,9 @@ export class ReadonlySharedStyleMap {
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
getAsKnownValue<T>(prop: StyleProp<T>): T | undefined;
|
getAsKnownValue<T>(prop: StyleProp<T>): T | undefined;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
keys(): IterableIterator<StyleProp<unknown>>;
|
keys(): IterableIterator<StyleProp<any>>;
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
protected map: Map<StyleProp<unknown>, SharedStyle<unknown>>;
|
protected map: Map<StyleProp<any>, SharedStyle<unknown>>;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
get size(): number;
|
get size(): number;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
|
|
@ -17658,7 +17658,7 @@
|
||||||
"excerptTokens": [
|
"excerptTokens": [
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "setStyleForSelectedShapes<T>(style: "
|
"text": "setStyleForSelectedShapes<S extends "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Reference",
|
"kind": "Reference",
|
||||||
|
@ -17667,15 +17667,28 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "<T>"
|
"text": "<any>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ">(style: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "S"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": ", value: "
|
"text": ", value: "
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "Reference",
|
||||||
|
"text": "StylePropValue",
|
||||||
|
"canonicalReference": "@tldraw/tlschema!StylePropValue:type"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "T"
|
"text": "<S>"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
|
@ -17701,10 +17714,10 @@
|
||||||
],
|
],
|
||||||
"typeParameters": [
|
"typeParameters": [
|
||||||
{
|
{
|
||||||
"typeParameterName": "T",
|
"typeParameterName": "S",
|
||||||
"constraintTokenRange": {
|
"constraintTokenRange": {
|
||||||
"startIndex": 0,
|
"startIndex": 1,
|
||||||
"endIndex": 0
|
"endIndex": 3
|
||||||
},
|
},
|
||||||
"defaultTypeTokenRange": {
|
"defaultTypeTokenRange": {
|
||||||
"startIndex": 0,
|
"startIndex": 0,
|
||||||
|
@ -17714,8 +17727,8 @@
|
||||||
],
|
],
|
||||||
"isStatic": false,
|
"isStatic": false,
|
||||||
"returnTypeTokenRange": {
|
"returnTypeTokenRange": {
|
||||||
"startIndex": 8,
|
"startIndex": 11,
|
||||||
"endIndex": 9
|
"endIndex": 12
|
||||||
},
|
},
|
||||||
"releaseTag": "Public",
|
"releaseTag": "Public",
|
||||||
"isProtected": false,
|
"isProtected": false,
|
||||||
|
@ -17723,14 +17736,6 @@
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"parameterName": "style",
|
"parameterName": "style",
|
||||||
"parameterTypeTokenRange": {
|
|
||||||
"startIndex": 1,
|
|
||||||
"endIndex": 3
|
|
||||||
},
|
|
||||||
"isOptional": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parameterName": "value",
|
|
||||||
"parameterTypeTokenRange": {
|
"parameterTypeTokenRange": {
|
||||||
"startIndex": 4,
|
"startIndex": 4,
|
||||||
"endIndex": 5
|
"endIndex": 5
|
||||||
|
@ -17738,10 +17743,18 @@
|
||||||
"isOptional": false
|
"isOptional": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameterName": "historyOptions",
|
"parameterName": "value",
|
||||||
"parameterTypeTokenRange": {
|
"parameterTypeTokenRange": {
|
||||||
"startIndex": 6,
|
"startIndex": 6,
|
||||||
"endIndex": 7
|
"endIndex": 8
|
||||||
|
},
|
||||||
|
"isOptional": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameterName": "historyOptions",
|
||||||
|
"parameterTypeTokenRange": {
|
||||||
|
"startIndex": 9,
|
||||||
|
"endIndex": 10
|
||||||
},
|
},
|
||||||
"isOptional": true
|
"isOptional": true
|
||||||
}
|
}
|
||||||
|
@ -18263,7 +18276,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "<unknown>, string>;\n }"
|
"text": "<any>, string>;\n }"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
|
@ -28523,7 +28536,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "<unknown>, "
|
"text": "<any>, "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Reference",
|
"kind": "Reference",
|
||||||
|
@ -28632,7 +28645,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "<unknown>, "
|
"text": "<any>, "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Reference",
|
"kind": "Reference",
|
||||||
|
@ -28872,7 +28885,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "<unknown>>"
|
"text": "<any>>"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
InstancePageStateRecordType,
|
InstancePageStateRecordType,
|
||||||
PageRecordType,
|
PageRecordType,
|
||||||
StyleProp,
|
StyleProp,
|
||||||
|
StylePropValue,
|
||||||
TLArrowShape,
|
TLArrowShape,
|
||||||
TLAsset,
|
TLAsset,
|
||||||
TLAssetId,
|
TLAssetId,
|
||||||
|
@ -737,7 +738,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
*/
|
*/
|
||||||
shapeUtils: { readonly [K in string]?: ShapeUtil<TLUnknownShape> }
|
shapeUtils: { readonly [K in string]?: ShapeUtil<TLUnknownShape> }
|
||||||
|
|
||||||
styleProps: { [key: string]: Map<StyleProp<unknown>, string> }
|
styleProps: { [key: string]: Map<StyleProp<any>, string> }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a shape util from a shape itself.
|
* Get a shape util from a shape itself.
|
||||||
|
@ -7385,9 +7386,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
setStyleForSelectedShapes<T>(
|
setStyleForSelectedShapes<S extends StyleProp<any>>(
|
||||||
style: StyleProp<T>,
|
style: S,
|
||||||
value: T,
|
value: StylePropValue<S>,
|
||||||
historyOptions?: TLCommandHistoryOptions
|
historyOptions?: TLCommandHistoryOptions
|
||||||
): this {
|
): this {
|
||||||
const selectedShapes = this.getSelectedShapes()
|
const selectedShapes = this.getSelectedShapes()
|
||||||
|
|
|
@ -35,7 +35,7 @@ function sharedStyleEquals<T>(a: SharedStyle<T>, b: SharedStyle<T> | undefined)
|
||||||
*/
|
*/
|
||||||
export class ReadonlySharedStyleMap {
|
export class ReadonlySharedStyleMap {
|
||||||
/** @internal */
|
/** @internal */
|
||||||
protected map: Map<StyleProp<unknown>, SharedStyle<unknown>>
|
protected map: Map<StyleProp<any>, SharedStyle<unknown>>
|
||||||
|
|
||||||
constructor(entries?: Iterable<[StyleProp<unknown>, SharedStyle<unknown>]>) {
|
constructor(entries?: Iterable<[StyleProp<unknown>, SharedStyle<unknown>]>) {
|
||||||
this.map = new Map(entries)
|
this.map = new Map(entries)
|
||||||
|
|
|
@ -163,9 +163,7 @@ export class RecordType<R extends UnknownRecord, RequiredProperties extends keyo
|
||||||
typeName: R['typeName'], config: {
|
typeName: R['typeName'], config: {
|
||||||
readonly createDefaultProperties: () => Exclude<OmitMeta<R>, RequiredProperties>;
|
readonly createDefaultProperties: () => Exclude<OmitMeta<R>, RequiredProperties>;
|
||||||
readonly migrations: Migrations;
|
readonly migrations: Migrations;
|
||||||
readonly validator?: {
|
readonly validator?: StoreValidator<R>;
|
||||||
validate: (r: unknown) => R;
|
|
||||||
} | StoreValidator<R>;
|
|
||||||
readonly scope?: RecordScope;
|
readonly scope?: RecordScope;
|
||||||
});
|
});
|
||||||
clone(record: R): R;
|
clone(record: R): R;
|
||||||
|
@ -183,11 +181,9 @@ export class RecordType<R extends UnknownRecord, RequiredProperties extends keyo
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
readonly scope: RecordScope;
|
readonly scope: RecordScope;
|
||||||
readonly typeName: R['typeName'];
|
readonly typeName: R['typeName'];
|
||||||
validate(record: unknown): R;
|
validate(record: unknown, recordBefore?: R): R;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
readonly validator: {
|
readonly validator: StoreValidator<R>;
|
||||||
validate: (r: unknown) => R;
|
|
||||||
} | StoreValidator<R>;
|
|
||||||
withDefaultProperties<DefaultProps extends Omit<Partial<R>, 'id' | 'typeName'>>(createDefaultProperties: () => DefaultProps): RecordType<R, Exclude<RequiredProperties, keyof DefaultProps>>;
|
withDefaultProperties<DefaultProps extends Omit<Partial<R>, 'id' | 'typeName'>>(createDefaultProperties: () => DefaultProps): RecordType<R, Exclude<RequiredProperties, keyof DefaultProps>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -344,6 +340,7 @@ export type StoreSnapshot<R extends UnknownRecord> = {
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type StoreValidator<R extends UnknownRecord> = {
|
export type StoreValidator<R extends UnknownRecord> = {
|
||||||
validate: (record: unknown) => R;
|
validate: (record: unknown) => R;
|
||||||
|
validateUsingKnownGoodVersion?: (knownGoodVersion: R, record: unknown) => R;
|
||||||
};
|
};
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
|
|
|
@ -2062,7 +2062,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": ";\n readonly validator?: {\n validate: (r: unknown) => R;\n } | "
|
"text": ";\n readonly validator?: "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Reference",
|
"kind": "Reference",
|
||||||
|
@ -2650,6 +2650,14 @@
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "unknown"
|
"text": "unknown"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ", recordBefore?: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "R"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "): "
|
"text": "): "
|
||||||
|
@ -2665,8 +2673,8 @@
|
||||||
],
|
],
|
||||||
"isStatic": false,
|
"isStatic": false,
|
||||||
"returnTypeTokenRange": {
|
"returnTypeTokenRange": {
|
||||||
"startIndex": 3,
|
"startIndex": 5,
|
||||||
"endIndex": 4
|
"endIndex": 6
|
||||||
},
|
},
|
||||||
"releaseTag": "Public",
|
"releaseTag": "Public",
|
||||||
"isProtected": false,
|
"isProtected": false,
|
||||||
|
@ -2679,6 +2687,14 @@
|
||||||
"endIndex": 2
|
"endIndex": 2
|
||||||
},
|
},
|
||||||
"isOptional": false
|
"isOptional": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameterName": "recordBefore",
|
||||||
|
"parameterTypeTokenRange": {
|
||||||
|
"startIndex": 3,
|
||||||
|
"endIndex": 4
|
||||||
|
},
|
||||||
|
"isOptional": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"isOptional": false,
|
"isOptional": false,
|
||||||
|
@ -2694,10 +2710,6 @@
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "readonly validator: "
|
"text": "readonly validator: "
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "{\n validate: (r: unknown) => R;\n } | "
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"kind": "Reference",
|
"kind": "Reference",
|
||||||
"text": "StoreValidator",
|
"text": "StoreValidator",
|
||||||
|
@ -2718,7 +2730,7 @@
|
||||||
"name": "validator",
|
"name": "validator",
|
||||||
"propertyTypeTokenRange": {
|
"propertyTypeTokenRange": {
|
||||||
"startIndex": 1,
|
"startIndex": 1,
|
||||||
"endIndex": 4
|
"endIndex": 3
|
||||||
},
|
},
|
||||||
"isStatic": false,
|
"isStatic": false,
|
||||||
"isProtected": false,
|
"isProtected": false,
|
||||||
|
@ -5535,7 +5547,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "{\n validate: (record: unknown) => R;\n}"
|
"text": "{\n validate: (record: unknown) => R;\n validateUsingKnownGoodVersion?: (knownGoodVersion: R, record: unknown) => R;\n}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
|
|
|
@ -29,7 +29,7 @@ export class RecordType<
|
||||||
> {
|
> {
|
||||||
readonly createDefaultProperties: () => Exclude<OmitMeta<R>, RequiredProperties>
|
readonly createDefaultProperties: () => Exclude<OmitMeta<R>, RequiredProperties>
|
||||||
readonly migrations: Migrations
|
readonly migrations: Migrations
|
||||||
readonly validator: StoreValidator<R> | { validate: (r: unknown) => R }
|
readonly validator: StoreValidator<R>
|
||||||
|
|
||||||
readonly scope: RecordScope
|
readonly scope: RecordScope
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ export class RecordType<
|
||||||
config: {
|
config: {
|
||||||
readonly createDefaultProperties: () => Exclude<OmitMeta<R>, RequiredProperties>
|
readonly createDefaultProperties: () => Exclude<OmitMeta<R>, RequiredProperties>
|
||||||
readonly migrations: Migrations
|
readonly migrations: Migrations
|
||||||
readonly validator?: StoreValidator<R> | { validate: (r: unknown) => R }
|
readonly validator?: StoreValidator<R>
|
||||||
readonly scope?: RecordScope
|
readonly scope?: RecordScope
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
@ -198,7 +198,10 @@ export class RecordType<
|
||||||
* Check that the passed in record passes the validations for this type. Returns its input
|
* Check that the passed in record passes the validations for this type. Returns its input
|
||||||
* correctly typed if it does, but throws an error otherwise.
|
* correctly typed if it does, but throws an error otherwise.
|
||||||
*/
|
*/
|
||||||
validate(record: unknown): R {
|
validate(record: unknown, recordBefore?: R): R {
|
||||||
|
if (recordBefore && this.validator.validateUsingKnownGoodVersion) {
|
||||||
|
return this.validator.validateUsingKnownGoodVersion(recordBefore, record)
|
||||||
|
}
|
||||||
return this.validator.validate(record)
|
return this.validator.validate(record)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,6 +84,7 @@ export type StoreSnapshot<R extends UnknownRecord> = {
|
||||||
/** @public */
|
/** @public */
|
||||||
export type StoreValidator<R extends UnknownRecord> = {
|
export type StoreValidator<R extends UnknownRecord> = {
|
||||||
validate: (record: unknown) => R
|
validate: (record: unknown) => R
|
||||||
|
validateUsingKnownGoodVersion?: (knownGoodVersion: R, record: unknown) => R
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
@ -389,24 +390,19 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
|
||||||
if (beforeUpdate) record = beforeUpdate(initialValue, record, source)
|
if (beforeUpdate) record = beforeUpdate(initialValue, record, source)
|
||||||
|
|
||||||
// Validate the record
|
// Validate the record
|
||||||
record = this.schema.validateRecord(
|
const validated = this.schema.validateRecord(
|
||||||
this,
|
this,
|
||||||
record,
|
record,
|
||||||
phaseOverride ?? 'updateRecord',
|
phaseOverride ?? 'updateRecord',
|
||||||
initialValue
|
initialValue
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (validated === initialValue) continue
|
||||||
|
|
||||||
recordAtom.set(devFreeze(record))
|
recordAtom.set(devFreeze(record))
|
||||||
|
|
||||||
// need to deref atom in case nextValue is not identical but is .equals?
|
didChange = true
|
||||||
const finalValue = recordAtom.__unsafe__getWithoutCapture()
|
updates[record.id] = [initialValue, recordAtom.__unsafe__getWithoutCapture()]
|
||||||
|
|
||||||
// If the value has changed, assign it to updates.
|
|
||||||
// todo: is this always going to be true?
|
|
||||||
if (initialValue !== finalValue) {
|
|
||||||
didChange = true
|
|
||||||
updates[record.id] = [initialValue, finalValue]
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (beforeCreate) record = beforeCreate(record, source)
|
if (beforeCreate) record = beforeCreate(record, source)
|
||||||
|
|
||||||
|
|
|
@ -85,7 +85,7 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
|
||||||
if (!recordType) {
|
if (!recordType) {
|
||||||
throw new Error(`Missing definition for record type ${record.typeName}`)
|
throw new Error(`Missing definition for record type ${record.typeName}`)
|
||||||
}
|
}
|
||||||
return recordType.validate(record)
|
return recordType.validate(record, recordBefore ?? undefined)
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (this.options.onValidationFailure) {
|
if (this.options.onValidationFailure) {
|
||||||
return this.options.onValidationFailure({
|
return this.options.onValidationFailure({
|
||||||
|
|
|
@ -195,9 +195,9 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
isPrecise: boolean;
|
isPrecise: boolean;
|
||||||
}>;
|
}>;
|
||||||
point: ObjectValidator< {
|
point: ObjectValidator< {
|
||||||
type: "point";
|
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
type: "point";
|
||||||
}>;
|
}>;
|
||||||
}, never>;
|
}, never>;
|
||||||
end: UnionValidator<"type", {
|
end: UnionValidator<"type", {
|
||||||
|
@ -209,9 +209,9 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
||||||
isPrecise: boolean;
|
isPrecise: boolean;
|
||||||
}>;
|
}>;
|
||||||
point: ObjectValidator< {
|
point: ObjectValidator< {
|
||||||
type: "point";
|
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
type: "point";
|
||||||
}>;
|
}>;
|
||||||
}, never>;
|
}, never>;
|
||||||
bend: Validator<number>;
|
bend: Validator<number>;
|
||||||
|
@ -1253,7 +1253,7 @@ export function TldrawUiButtonIcon({ icon, small, invertIcon }: TLUiButtonIconPr
|
||||||
export function TldrawUiButtonLabel({ children }: TLUiButtonLabelProps): JSX_2.Element;
|
export function TldrawUiButtonLabel({ children }: TLUiButtonLabelProps): JSX_2.Element;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const TldrawUiButtonPicker: MemoExoticComponent<(<T extends string>(props: TLUiButtonPickerProps<T>) => JSX_2.Element)>;
|
export const TldrawUiButtonPicker: typeof _TldrawUiButtonPicker;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function TldrawUiComponentsProvider({ overrides, children, }: TLUiComponentsProviderProps): JSX_2.Element;
|
export function TldrawUiComponentsProvider({ overrides, children, }: TLUiComponentsProviderProps): JSX_2.Element;
|
||||||
|
@ -2108,7 +2108,7 @@ export function useNativeClipboardEvents(): void;
|
||||||
export function useReadonly(): boolean;
|
export function useReadonly(): boolean;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function useRelevantStyles(stylesToCheck?: readonly (EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "yellow"> | EnumStyleProp<"dashed" | "dotted" | "draw" | "solid"> | EnumStyleProp<"l" | "m" | "s" | "xl"> | EnumStyleProp<"none" | "pattern" | "semi" | "solid">)[]): null | ReadonlySharedStyleMap;
|
export function useRelevantStyles(stylesToCheck?: readonly StyleProp<any>[]): null | ReadonlySharedStyleMap;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function useTldrawUiComponents(): Partial<{
|
export function useTldrawUiComponents(): Partial<{
|
||||||
|
|
|
@ -1387,7 +1387,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "<{\n type: \"point\";\n x: number;\n y: number;\n }>;\n }, never>;\n end: import(\"@tldraw/editor\")."
|
"text": "<{\n x: number;\n y: number;\n type: \"point\";\n }>;\n }, never>;\n end: import(\"@tldraw/editor\")."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Reference",
|
"kind": "Reference",
|
||||||
|
@ -1432,7 +1432,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "<{\n type: \"point\";\n x: number;\n y: number;\n }>;\n }, never>;\n bend: import(\"@tldraw/editor\")."
|
"text": "<{\n x: number;\n y: number;\n type: \"point\";\n }>;\n }, never>;\n bend: import(\"@tldraw/editor\")."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Reference",
|
"kind": "Reference",
|
||||||
|
@ -14716,34 +14716,12 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "import(\"react\")."
|
"text": "typeof "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Reference",
|
"kind": "Reference",
|
||||||
"text": "MemoExoticComponent",
|
"text": "_TldrawUiButtonPicker",
|
||||||
"canonicalReference": "@types/react!React.MemoExoticComponent:type"
|
"canonicalReference": "@tldraw/tldraw!~_TldrawUiButtonPicker:function"
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "<(<T extends string>(props: "
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Reference",
|
|
||||||
"text": "TLUiButtonPickerProps",
|
|
||||||
"canonicalReference": "@tldraw/tldraw!TLUiButtonPickerProps:interface"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "<T>) => import(\"react/jsx-runtime\")."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Reference",
|
|
||||||
"text": "JSX.Element",
|
|
||||||
"canonicalReference": "@types/react!JSX.Element:interface"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": ")>"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"fileUrlPath": "packages/tldraw/src/lib/ui/components/primitives/TldrawUiButtonPicker.tsx",
|
"fileUrlPath": "packages/tldraw/src/lib/ui/components/primitives/TldrawUiButtonPicker.tsx",
|
||||||
|
@ -14752,7 +14730,7 @@
|
||||||
"name": "TldrawUiButtonPicker",
|
"name": "TldrawUiButtonPicker",
|
||||||
"variableTypeTokenRange": {
|
"variableTypeTokenRange": {
|
||||||
"startIndex": 1,
|
"startIndex": 1,
|
||||||
"endIndex": 8
|
"endIndex": 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -23506,43 +23484,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "readonly (import(\"@tldraw/editor\")."
|
"text": "readonly "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Reference",
|
"kind": "Reference",
|
||||||
"text": "EnumStyleProp",
|
"text": "StyleProp",
|
||||||
"canonicalReference": "@tldraw/tlschema!EnumStyleProp:class"
|
"canonicalReference": "@tldraw/tlschema!StyleProp:class"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "<\"black\" | \"blue\" | \"green\" | \"grey\" | \"light-blue\" | \"light-green\" | \"light-red\" | \"light-violet\" | \"orange\" | \"red\" | \"violet\" | \"yellow\"> | import(\"@tldraw/editor\")."
|
"text": "<any>[]"
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Reference",
|
|
||||||
"text": "EnumStyleProp",
|
|
||||||
"canonicalReference": "@tldraw/tlschema!EnumStyleProp:class"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "<\"dashed\" | \"dotted\" | \"draw\" | \"solid\"> | import(\"@tldraw/editor\")."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Reference",
|
|
||||||
"text": "EnumStyleProp",
|
|
||||||
"canonicalReference": "@tldraw/tlschema!EnumStyleProp:class"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "<\"l\" | \"m\" | \"s\" | \"xl\"> | import(\"@tldraw/editor\")."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Reference",
|
|
||||||
"text": "EnumStyleProp",
|
|
||||||
"canonicalReference": "@tldraw/tlschema!EnumStyleProp:class"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"kind": "Content",
|
|
||||||
"text": "<\"none\" | \"pattern\" | \"semi\" | \"solid\">)[]"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
|
@ -23564,8 +23515,8 @@
|
||||||
],
|
],
|
||||||
"fileUrlPath": "packages/tldraw/src/lib/ui/hooks/useRevelantStyles.ts",
|
"fileUrlPath": "packages/tldraw/src/lib/ui/hooks/useRevelantStyles.ts",
|
||||||
"returnTypeTokenRange": {
|
"returnTypeTokenRange": {
|
||||||
"startIndex": 11,
|
"startIndex": 5,
|
||||||
"endIndex": 13
|
"endIndex": 7
|
||||||
},
|
},
|
||||||
"releaseTag": "Public",
|
"releaseTag": "Public",
|
||||||
"overloadIndex": 1,
|
"overloadIndex": 1,
|
||||||
|
@ -23574,7 +23525,7 @@
|
||||||
"parameterName": "stylesToCheck",
|
"parameterName": "stylesToCheck",
|
||||||
"parameterTypeTokenRange": {
|
"parameterTypeTokenRange": {
|
||||||
"startIndex": 1,
|
"startIndex": 1,
|
||||||
"endIndex": 10
|
"endIndex": 4
|
||||||
},
|
},
|
||||||
"isOptional": true
|
"isOptional": true
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,8 @@ import {
|
||||||
LineShapeSplineStyle,
|
LineShapeSplineStyle,
|
||||||
ReadonlySharedStyleMap,
|
ReadonlySharedStyleMap,
|
||||||
StyleProp,
|
StyleProp,
|
||||||
|
TLArrowShapeArrowheadStyle,
|
||||||
|
TLDefaultVerticalAlignStyle,
|
||||||
minBy,
|
minBy,
|
||||||
useEditor,
|
useEditor,
|
||||||
useValue,
|
useValue,
|
||||||
|
@ -199,7 +201,7 @@ function TextStylePickerSet({ styles }: { styles: ReadonlySharedStyleMap }) {
|
||||||
<TldrawUiButtonIcon icon="vertical-align-center" />
|
<TldrawUiButtonIcon icon="vertical-align-center" />
|
||||||
</TldrawUiButton>
|
</TldrawUiButton>
|
||||||
) : (
|
) : (
|
||||||
<DropdownPicker
|
<DropdownPicker<TLDefaultVerticalAlignStyle>
|
||||||
type="icon"
|
type="icon"
|
||||||
id="geo-vertical-alignment"
|
id="geo-vertical-alignment"
|
||||||
uiType="verticalAlign"
|
uiType="verticalAlign"
|
||||||
|
@ -270,7 +272,7 @@ function ArrowheadStylePickerSet({ styles }: { styles: ReadonlySharedStyleMap })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DoubleDropdownPicker
|
<DoubleDropdownPicker<TLArrowShapeArrowheadStyle>
|
||||||
label={'style-panel.arrowheads'}
|
label={'style-panel.arrowheads'}
|
||||||
uiTypeA="arrowheadStart"
|
uiTypeA="arrowheadStart"
|
||||||
styleA={ArrowShapeArrowheadStartStyle}
|
styleA={ArrowShapeArrowheadStartStyle}
|
||||||
|
|
|
@ -27,7 +27,7 @@ interface DoubleDropdownPickerProps<T extends string> {
|
||||||
onValueChange: (style: StyleProp<T>, value: T, squashing: boolean) => void
|
onValueChange: (style: StyleProp<T>, value: T, squashing: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DoubleDropdownPicker = React.memo(function DoubleDropdownPicker<T extends string>({
|
function _DoubleDropdownPicker<T extends string>({
|
||||||
label,
|
label,
|
||||||
uiTypeA,
|
uiTypeA,
|
||||||
uiTypeB,
|
uiTypeB,
|
||||||
|
@ -137,4 +137,9 @@ export const DoubleDropdownPicker = React.memo(function DoubleDropdownPicker<T e
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
|
// need to memo like this to get generics
|
||||||
|
export const DoubleDropdownPicker = React.memo(
|
||||||
|
_DoubleDropdownPicker
|
||||||
|
) as typeof _DoubleDropdownPicker
|
||||||
|
|
|
@ -25,7 +25,7 @@ interface DropdownPickerProps<T extends string> {
|
||||||
onValueChange: (style: StyleProp<T>, value: T, squashing: boolean) => void
|
onValueChange: (style: StyleProp<T>, value: T, squashing: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DropdownPicker = React.memo(function DropdownPicker<T extends string>({
|
function _DropdownPicker<T extends string>({
|
||||||
id,
|
id,
|
||||||
label,
|
label,
|
||||||
uiType,
|
uiType,
|
||||||
|
@ -76,4 +76,7 @@ export const DropdownPicker = React.memo(function DropdownPicker<T extends strin
|
||||||
</TldrawUiDropdownMenuContent>
|
</TldrawUiDropdownMenuContent>
|
||||||
</TldrawUiDropdownMenuRoot>
|
</TldrawUiDropdownMenuRoot>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
|
// need to export like this to get generics
|
||||||
|
export const DropdownPicker = React.memo(_DropdownPicker) as typeof _DropdownPicker
|
||||||
|
|
|
@ -25,10 +25,7 @@ export interface TLUiButtonPickerProps<T extends string> {
|
||||||
onValueChange: (style: StyleProp<T>, value: T, squashing: boolean) => void
|
onValueChange: (style: StyleProp<T>, value: T, squashing: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
function _TldrawUiButtonPicker<T extends string>(props: TLUiButtonPickerProps<T>) {
|
||||||
export const TldrawUiButtonPicker = memo(function TldrawUiButtonPicker<T extends string>(
|
|
||||||
props: TLUiButtonPickerProps<T>
|
|
||||||
) {
|
|
||||||
const {
|
const {
|
||||||
uiType,
|
uiType,
|
||||||
items,
|
items,
|
||||||
|
@ -125,4 +122,6 @@ export const TldrawUiButtonPicker = memo(function TldrawUiButtonPicker<T extends
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
/** @public */
|
||||||
|
export const TldrawUiButtonPicker = memo(_TldrawUiButtonPicker) as typeof _TldrawUiButtonPicker
|
||||||
|
|
|
@ -5,11 +5,12 @@ import {
|
||||||
DefaultSizeStyle,
|
DefaultSizeStyle,
|
||||||
ReadonlySharedStyleMap,
|
ReadonlySharedStyleMap,
|
||||||
SharedStyleMap,
|
SharedStyleMap,
|
||||||
|
StyleProp,
|
||||||
useEditor,
|
useEditor,
|
||||||
useValue,
|
useValue,
|
||||||
} from '@tldraw/editor'
|
} from '@tldraw/editor'
|
||||||
|
|
||||||
const selectToolStyles = Object.freeze([
|
const selectToolStyles: readonly StyleProp<any>[] = Object.freeze([
|
||||||
DefaultColorStyle,
|
DefaultColorStyle,
|
||||||
DefaultDashStyle,
|
DefaultDashStyle,
|
||||||
DefaultFillStyle,
|
DefaultFillStyle,
|
||||||
|
|
|
@ -56,7 +56,7 @@ const tldrawFileValidator: T.Validator<TldrawFile> = T.object({
|
||||||
}),
|
}),
|
||||||
records: T.arrayOf(
|
records: T.arrayOf(
|
||||||
T.object({
|
T.object({
|
||||||
id: T.string as T.Validator<RecordId<any>>,
|
id: T.string as any as T.Validator<RecordId<any>>,
|
||||||
typeName: T.string,
|
typeName: T.string,
|
||||||
}).allowUnknownProperties()
|
}).allowUnknownProperties()
|
||||||
),
|
),
|
||||||
|
|
|
@ -45,12 +45,12 @@ export const arrowShapeProps: {
|
||||||
normalizedAnchor: VecModel;
|
normalizedAnchor: VecModel;
|
||||||
isExact: boolean;
|
isExact: boolean;
|
||||||
isPrecise: boolean;
|
isPrecise: boolean;
|
||||||
}>;
|
} & {}>;
|
||||||
point: T.ObjectValidator<{
|
point: T.ObjectValidator<{
|
||||||
type: "point";
|
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
}>;
|
type: "point";
|
||||||
|
} & {}>;
|
||||||
}, never>;
|
}, never>;
|
||||||
end: T.UnionValidator<"type", {
|
end: T.UnionValidator<"type", {
|
||||||
binding: T.ObjectValidator<{
|
binding: T.ObjectValidator<{
|
||||||
|
@ -59,12 +59,12 @@ export const arrowShapeProps: {
|
||||||
normalizedAnchor: VecModel;
|
normalizedAnchor: VecModel;
|
||||||
isExact: boolean;
|
isExact: boolean;
|
||||||
isPrecise: boolean;
|
isPrecise: boolean;
|
||||||
}>;
|
} & {}>;
|
||||||
point: T.ObjectValidator<{
|
point: T.ObjectValidator<{
|
||||||
type: "point";
|
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
}>;
|
type: "point";
|
||||||
|
} & {}>;
|
||||||
}, never>;
|
}, never>;
|
||||||
bend: T.Validator<number>;
|
bend: T.Validator<number>;
|
||||||
text: T.Validator<string>;
|
text: T.Validator<string>;
|
||||||
|
@ -116,13 +116,19 @@ export const CameraRecordType: RecordType<TLCamera, never>;
|
||||||
export const canvasUiColorTypeValidator: T.Validator<"accent" | "black" | "laser" | "muted-1" | "selection-fill" | "selection-stroke" | "white">;
|
export const canvasUiColorTypeValidator: T.Validator<"accent" | "black" | "laser" | "muted-1" | "selection-fill" | "selection-stroke" | "white">;
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export function createAssetValidator<Type extends string, Props extends JsonObject>(type: Type, props: T.Validator<Props>): T.ObjectValidator<{
|
export function createAssetValidator<Type extends string, Props extends JsonObject>(type: Type, props: T.Validator<Props>): T.ObjectValidator<{ [P in "id" | "meta" | "typeName" | (undefined extends Props ? never : "props") | (undefined extends Type ? never : "type")]: {
|
||||||
id: TLAssetId;
|
id: TLAssetId;
|
||||||
typeName: 'asset';
|
typeName: 'asset';
|
||||||
type: Type;
|
type: Type;
|
||||||
props: Props;
|
props: Props;
|
||||||
meta: JsonObject;
|
meta: JsonObject;
|
||||||
}>;
|
}[P]; } & { [P_1 in (undefined extends Props ? "props" : never) | (undefined extends Type ? "type" : never)]?: {
|
||||||
|
id: TLAssetId;
|
||||||
|
typeName: 'asset';
|
||||||
|
type: Type;
|
||||||
|
props: Props;
|
||||||
|
meta: JsonObject;
|
||||||
|
}[P_1] | undefined; }>;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export const createPresenceStateDerivation: ($user: Signal<{
|
export const createPresenceStateDerivation: ($user: Signal<{
|
||||||
|
@ -139,7 +145,7 @@ export function createShapeValidator<Type extends string, Props extends JsonObje
|
||||||
[K in keyof Props]: T.Validatable<Props[K]>;
|
[K in keyof Props]: T.Validatable<Props[K]>;
|
||||||
}, meta?: {
|
}, meta?: {
|
||||||
[K in keyof Meta]: T.Validatable<Meta[K]>;
|
[K in keyof Meta]: T.Validatable<Meta[K]>;
|
||||||
}): T.ObjectValidator<TLBaseShape<Type, Props>>;
|
}): T.ObjectValidator<{ [P in "id" | "index" | "isLocked" | "meta" | "opacity" | "parentId" | "rotation" | "typeName" | "x" | "y" | (undefined extends Props ? never : "props") | (undefined extends Type ? never : "type")]: TLBaseShape<Type, Props>[P]; } & { [P_1 in (undefined extends Props ? "props" : never) | (undefined extends Type ? "type" : never)]?: TLBaseShape<Type, Props>[P_1] | undefined; }>;
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export function createTLSchema({ shapes }: {
|
export function createTLSchema({ shapes }: {
|
||||||
|
@ -196,7 +202,7 @@ export const drawShapeProps: {
|
||||||
segments: T.ArrayOfValidator<{
|
segments: T.ArrayOfValidator<{
|
||||||
type: "free" | "straight";
|
type: "free" | "straight";
|
||||||
points: VecModel[];
|
points: VecModel[];
|
||||||
}>;
|
} & {}>;
|
||||||
isComplete: T.Validator<boolean>;
|
isComplete: T.Validator<boolean>;
|
||||||
isClosed: T.Validator<boolean>;
|
isClosed: T.Validator<boolean>;
|
||||||
isPen: T.Validator<boolean>;
|
isPen: T.Validator<boolean>;
|
||||||
|
@ -515,7 +521,7 @@ export const highlightShapeProps: {
|
||||||
segments: T.ArrayOfValidator<{
|
segments: T.ArrayOfValidator<{
|
||||||
type: "free" | "straight";
|
type: "free" | "straight";
|
||||||
points: VecModel[];
|
points: VecModel[];
|
||||||
}>;
|
} & {}>;
|
||||||
isComplete: T.Validator<boolean>;
|
isComplete: T.Validator<boolean>;
|
||||||
isPen: T.Validator<boolean>;
|
isPen: T.Validator<boolean>;
|
||||||
};
|
};
|
||||||
|
@ -533,10 +539,10 @@ export const imageShapeProps: {
|
||||||
playing: T.Validator<boolean>;
|
playing: T.Validator<boolean>;
|
||||||
url: T.Validator<string>;
|
url: T.Validator<string>;
|
||||||
assetId: T.Validator<TLAssetId | null>;
|
assetId: T.Validator<TLAssetId | null>;
|
||||||
crop: T.Validator<{
|
crop: T.Validator<({
|
||||||
topLeft: VecModel;
|
topLeft: VecModel;
|
||||||
bottomRight: VecModel;
|
bottomRight: VecModel;
|
||||||
} | null>;
|
} & {}) | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
|
@ -716,12 +722,8 @@ export const rootShapeMigrations: Migrations;
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export type SchemaShapeInfo = {
|
export type SchemaShapeInfo = {
|
||||||
migrations?: Migrations;
|
migrations?: Migrations;
|
||||||
props?: Record<string, {
|
props?: Record<string, AnyValidator>;
|
||||||
validate: (prop: any) => any;
|
meta?: Record<string, AnyValidator>;
|
||||||
}>;
|
|
||||||
meta?: Record<string, {
|
|
||||||
validate: (prop: any) => any;
|
|
||||||
}>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
|
@ -755,8 +757,13 @@ export class StyleProp<Type> implements T.Validatable<Type> {
|
||||||
readonly type: T.Validatable<Type>;
|
readonly type: T.Validatable<Type>;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
validate(value: unknown): Type;
|
validate(value: unknown): Type;
|
||||||
|
// (undocumented)
|
||||||
|
validateUsingKnownGoodVersion(prevValue: Type, newValue: unknown): Type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
export type StylePropValue<T extends StyleProp<any>> = T extends StyleProp<infer U> ? U : never;
|
||||||
|
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
export const textShapeMigrations: Migrations;
|
export const textShapeMigrations: Migrations;
|
||||||
|
|
||||||
|
|
|
@ -355,7 +355,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": ";\n isExact: boolean;\n isPrecise: boolean;\n }>;\n point: "
|
"text": ";\n isExact: boolean;\n isPrecise: boolean;\n } & {}>;\n point: "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Reference",
|
"kind": "Reference",
|
||||||
|
@ -364,7 +364,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "<{\n type: \"point\";\n x: number;\n y: number;\n }>;\n }, never>;\n end: "
|
"text": "<{\n x: number;\n y: number;\n type: \"point\";\n } & {}>;\n }, never>;\n end: "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Reference",
|
"kind": "Reference",
|
||||||
|
@ -400,7 +400,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": ";\n isExact: boolean;\n isPrecise: boolean;\n }>;\n point: "
|
"text": ";\n isExact: boolean;\n isPrecise: boolean;\n } & {}>;\n point: "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Reference",
|
"kind": "Reference",
|
||||||
|
@ -409,7 +409,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "<{\n type: \"point\";\n x: number;\n y: number;\n }>;\n }, never>;\n bend: "
|
"text": "<{\n x: number;\n y: number;\n type: \"point\";\n } & {}>;\n }, never>;\n bend: "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Reference",
|
"kind": "Reference",
|
||||||
|
@ -880,7 +880,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "<{\n id: "
|
"text": "<{ [P in \"id\" | \"meta\" | \"typeName\" | (undefined extends Props ? never : \"props\") | (undefined extends Type ? never : \"type\")]: {\n id: "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Reference",
|
"kind": "Reference",
|
||||||
|
@ -898,7 +898,25 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": ";\n}>"
|
"text": ";\n}[P]; } & { [P_1 in (undefined extends Props ? \"props\" : never) | (undefined extends Type ? \"type\" : never)]?: {\n id: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Reference",
|
||||||
|
"text": "TLAssetId",
|
||||||
|
"canonicalReference": "@tldraw/tlschema!TLAssetId:type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ";\n typeName: 'asset';\n type: Type;\n props: Props;\n meta: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Reference",
|
||||||
|
"text": "JsonObject",
|
||||||
|
"canonicalReference": "@tldraw/utils!JsonObject:type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ";\n}[P_1] | undefined; }>"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
|
@ -908,7 +926,7 @@
|
||||||
"fileUrlPath": "packages/tlschema/src/assets/TLBaseAsset.ts",
|
"fileUrlPath": "packages/tlschema/src/assets/TLBaseAsset.ts",
|
||||||
"returnTypeTokenRange": {
|
"returnTypeTokenRange": {
|
||||||
"startIndex": 10,
|
"startIndex": 10,
|
||||||
"endIndex": 16
|
"endIndex": 20
|
||||||
},
|
},
|
||||||
"releaseTag": "Public",
|
"releaseTag": "Public",
|
||||||
"overloadIndex": 1,
|
"overloadIndex": 1,
|
||||||
|
@ -1154,7 +1172,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "<"
|
"text": "<{ [P in \"id\" | \"index\" | \"isLocked\" | \"meta\" | \"opacity\" | \"parentId\" | \"rotation\" | \"typeName\" | \"x\" | \"y\" | (undefined extends Props ? never : \"props\") | (undefined extends Type ? never : \"type\")]: "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Reference",
|
"kind": "Reference",
|
||||||
|
@ -1163,7 +1181,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "<Type, Props>>"
|
"text": "<Type, Props>[P]; } & { [P_1 in (undefined extends Props ? \"props\" : never) | (undefined extends Type ? \"type\" : never)]?: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Reference",
|
||||||
|
"text": "TLBaseShape",
|
||||||
|
"canonicalReference": "@tldraw/tlschema!TLBaseShape:interface"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "<Type, Props>[P_1] | undefined; }>"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
|
@ -1173,7 +1200,7 @@
|
||||||
"fileUrlPath": "packages/tlschema/src/shapes/TLBaseShape.ts",
|
"fileUrlPath": "packages/tlschema/src/shapes/TLBaseShape.ts",
|
||||||
"returnTypeTokenRange": {
|
"returnTypeTokenRange": {
|
||||||
"startIndex": 17,
|
"startIndex": 17,
|
||||||
"endIndex": 21
|
"endIndex": 23
|
||||||
},
|
},
|
||||||
"releaseTag": "Public",
|
"releaseTag": "Public",
|
||||||
"overloadIndex": 1,
|
"overloadIndex": 1,
|
||||||
|
@ -1698,7 +1725,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "[];\n }>;\n isComplete: "
|
"text": "[];\n } & {}>;\n isComplete: "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Reference",
|
"kind": "Reference",
|
||||||
|
@ -2304,7 +2331,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "[];\n }>;\n isComplete: "
|
"text": "[];\n } & {}>;\n isComplete: "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Reference",
|
"kind": "Reference",
|
||||||
|
@ -2408,7 +2435,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "<{\n topLeft: import(\"../misc/geometry-types\")."
|
"text": "<({\n topLeft: import(\"../misc/geometry-types\")."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Reference",
|
"kind": "Reference",
|
||||||
|
@ -2426,7 +2453,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": ";\n } | null>;\n}"
|
"text": ";\n } & {}) | null>;\n}"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"fileUrlPath": "packages/tlschema/src/shapes/TLImageShape.ts",
|
"fileUrlPath": "packages/tlschema/src/shapes/TLImageShape.ts",
|
||||||
|
@ -3061,7 +3088,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "<string, {\n validate: (prop: any) => any;\n }>;\n meta?: "
|
"text": "<string, "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Reference",
|
||||||
|
"text": "AnyValidator",
|
||||||
|
"canonicalReference": "@tldraw/tlschema!~AnyValidator:type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ">;\n meta?: "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Reference",
|
"kind": "Reference",
|
||||||
|
@ -3070,7 +3106,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "<string, {\n validate: (prop: any) => any;\n }>;\n}"
|
"text": "<string, "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Reference",
|
||||||
|
"text": "AnyValidator",
|
||||||
|
"canonicalReference": "@tldraw/tlschema!~AnyValidator:type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ">;\n}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
|
@ -3082,7 +3127,7 @@
|
||||||
"name": "SchemaShapeInfo",
|
"name": "SchemaShapeInfo",
|
||||||
"typeTokenRange": {
|
"typeTokenRange": {
|
||||||
"startIndex": 1,
|
"startIndex": 1,
|
||||||
"endIndex": 8
|
"endIndex": 12
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -3548,6 +3593,70 @@
|
||||||
"isOptional": false,
|
"isOptional": false,
|
||||||
"isAbstract": false,
|
"isAbstract": false,
|
||||||
"name": "validate"
|
"name": "validate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Method",
|
||||||
|
"canonicalReference": "@tldraw/tlschema!StyleProp#validateUsingKnownGoodVersion:member(1)",
|
||||||
|
"docComment": "",
|
||||||
|
"excerptTokens": [
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "validateUsingKnownGoodVersion(prevValue: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "Type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ", newValue: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "): "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "Type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ";"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isStatic": false,
|
||||||
|
"returnTypeTokenRange": {
|
||||||
|
"startIndex": 5,
|
||||||
|
"endIndex": 6
|
||||||
|
},
|
||||||
|
"releaseTag": "Public",
|
||||||
|
"isProtected": false,
|
||||||
|
"overloadIndex": 1,
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"parameterName": "prevValue",
|
||||||
|
"parameterTypeTokenRange": {
|
||||||
|
"startIndex": 1,
|
||||||
|
"endIndex": 2
|
||||||
|
},
|
||||||
|
"isOptional": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameterName": "newValue",
|
||||||
|
"parameterTypeTokenRange": {
|
||||||
|
"startIndex": 3,
|
||||||
|
"endIndex": 4
|
||||||
|
},
|
||||||
|
"isOptional": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isOptional": false,
|
||||||
|
"isAbstract": false,
|
||||||
|
"name": "validateUsingKnownGoodVersion"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"implementsTokenRanges": [
|
"implementsTokenRanges": [
|
||||||
|
@ -3557,6 +3666,67 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "TypeAlias",
|
||||||
|
"canonicalReference": "@tldraw/tlschema!StylePropValue:type",
|
||||||
|
"docComment": "/**\n * @public\n */\n",
|
||||||
|
"excerptTokens": [
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "export type StylePropValue<T extends "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Reference",
|
||||||
|
"text": "StyleProp",
|
||||||
|
"canonicalReference": "@tldraw/tlschema!StyleProp:class"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "<any>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "> = "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "T extends "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Reference",
|
||||||
|
"text": "StyleProp",
|
||||||
|
"canonicalReference": "@tldraw/tlschema!StyleProp:class"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "<infer U> ? U : never"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ";"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fileUrlPath": "packages/tlschema/src/styles/StyleProp.ts",
|
||||||
|
"releaseTag": "Public",
|
||||||
|
"name": "StylePropValue",
|
||||||
|
"typeParameters": [
|
||||||
|
{
|
||||||
|
"typeParameterName": "T",
|
||||||
|
"constraintTokenRange": {
|
||||||
|
"startIndex": 1,
|
||||||
|
"endIndex": 3
|
||||||
|
},
|
||||||
|
"defaultTypeTokenRange": {
|
||||||
|
"startIndex": 0,
|
||||||
|
"endIndex": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"typeTokenRange": {
|
||||||
|
"startIndex": 4,
|
||||||
|
"endIndex": 7
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "Variable",
|
"kind": "Variable",
|
||||||
"canonicalReference": "@tldraw/tlschema!textShapeProps:var",
|
"canonicalReference": "@tldraw/tlschema!textShapeProps:var",
|
||||||
|
|
|
@ -27,14 +27,14 @@ export const assetIdValidator = idValidator<TLAssetId>('asset')
|
||||||
export function createAssetValidator<Type extends string, Props extends JsonObject>(
|
export function createAssetValidator<Type extends string, Props extends JsonObject>(
|
||||||
type: Type,
|
type: Type,
|
||||||
props: T.Validator<Props>
|
props: T.Validator<Props>
|
||||||
): T.ObjectValidator<{
|
) {
|
||||||
id: TLAssetId
|
return T.object<{
|
||||||
typeName: 'asset'
|
id: TLAssetId
|
||||||
type: Type
|
typeName: 'asset'
|
||||||
props: Props
|
type: Type
|
||||||
meta: JsonObject
|
props: Props
|
||||||
}> {
|
meta: JsonObject
|
||||||
return T.object({
|
}>({
|
||||||
id: assetIdValidator,
|
id: assetIdValidator,
|
||||||
typeName: T.literal('asset'),
|
typeName: T.literal('asset'),
|
||||||
type: T.literal(type),
|
type: T.literal(type),
|
||||||
|
|
|
@ -14,11 +14,16 @@ import { createShapeRecordType, getShapePropKeysByStyle } from './records/TLShap
|
||||||
import { storeMigrations } from './store-migrations'
|
import { storeMigrations } from './store-migrations'
|
||||||
import { StyleProp } from './styles/StyleProp'
|
import { StyleProp } from './styles/StyleProp'
|
||||||
|
|
||||||
|
type AnyValidator = {
|
||||||
|
validate: (prop: any) => any
|
||||||
|
validateUsingKnownGoodVersion?: (prevVersion: any, newVersion: any) => any
|
||||||
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type SchemaShapeInfo = {
|
export type SchemaShapeInfo = {
|
||||||
migrations?: Migrations
|
migrations?: Migrations
|
||||||
props?: Record<string, { validate: (prop: any) => any }>
|
props?: Record<string, AnyValidator>
|
||||||
meta?: Record<string, { validate: (prop: any) => any }>
|
meta?: Record<string, AnyValidator>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
|
|
@ -136,7 +136,7 @@ export {
|
||||||
type TLTextShapeProps,
|
type TLTextShapeProps,
|
||||||
} from './shapes/TLTextShape'
|
} from './shapes/TLTextShape'
|
||||||
export { videoShapeMigrations, videoShapeProps, type TLVideoShape } from './shapes/TLVideoShape'
|
export { videoShapeMigrations, videoShapeProps, type TLVideoShape } from './shapes/TLVideoShape'
|
||||||
export { EnumStyleProp, StyleProp } from './styles/StyleProp'
|
export { EnumStyleProp, StyleProp, type StylePropValue } from './styles/StyleProp'
|
||||||
export {
|
export {
|
||||||
DefaultColorStyle,
|
DefaultColorStyle,
|
||||||
DefaultColorThemePalette,
|
DefaultColorThemePalette,
|
||||||
|
|
|
@ -52,8 +52,8 @@ export function createShapeValidator<
|
||||||
type: T.literal(type),
|
type: T.literal(type),
|
||||||
isLocked: T.boolean,
|
isLocked: T.boolean,
|
||||||
opacity: opacityValidator,
|
opacity: opacityValidator,
|
||||||
props: props ? T.object(props) : (T.jsonValue as T.ObjectValidator<Props>),
|
props: props ? T.object(props) : (T.jsonValue as any),
|
||||||
meta: meta ? T.object(meta) : (T.jsonValue as T.ObjectValidator<Meta>),
|
meta: meta ? T.object(meta) : (T.jsonValue as any),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -90,6 +90,14 @@ export class StyleProp<Type> implements T.Validatable<Type> {
|
||||||
validate(value: unknown) {
|
validate(value: unknown) {
|
||||||
return this.type.validate(value)
|
return this.type.validate(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateUsingKnownGoodVersion(prevValue: Type, newValue: unknown) {
|
||||||
|
if (this.type.validateUsingKnownGoodVersion) {
|
||||||
|
return this.type.validateUsingKnownGoodVersion(prevValue, newValue)
|
||||||
|
} else {
|
||||||
|
return this.validate(newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -107,3 +115,8 @@ export class EnumStyleProp<T> extends StyleProp<T> {
|
||||||
super(id, defaultValue, T.literalEnum(...values))
|
super(id, defaultValue, T.literalEnum(...values))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export type StylePropValue<T extends StyleProp<any>> = T extends StyleProp<infer U> ? U : never
|
||||||
|
|
|
@ -86,7 +86,11 @@ const number: Validator<number>;
|
||||||
// @public
|
// @public
|
||||||
function object<Shape extends object>(config: {
|
function object<Shape extends object>(config: {
|
||||||
readonly [K in keyof Shape]: Validatable<Shape[K]>;
|
readonly [K in keyof Shape]: Validatable<Shape[K]>;
|
||||||
}): ObjectValidator<Shape>;
|
}): ObjectValidator<{
|
||||||
|
[P in ExtractRequiredKeys<Shape>]: Shape[P];
|
||||||
|
} & {
|
||||||
|
[P in ExtractOptionalKeys<Shape>]?: Shape[P];
|
||||||
|
}>;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export class ObjectValidator<Shape extends object> extends Validator<Shape> {
|
export class ObjectValidator<Shape extends object> extends Validator<Shape> {
|
||||||
|
@ -136,6 +140,7 @@ declare namespace T {
|
||||||
nullable,
|
nullable,
|
||||||
literalEnum,
|
literalEnum,
|
||||||
ValidatorFn,
|
ValidatorFn,
|
||||||
|
ValidatorUsingKnownGoodVersionFn,
|
||||||
Validatable,
|
Validatable,
|
||||||
ValidationError,
|
ValidationError,
|
||||||
TypeOf,
|
TypeOf,
|
||||||
|
@ -166,7 +171,7 @@ declare namespace T {
|
||||||
export { T }
|
export { T }
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type TypeOf<V extends Validatable<unknown>> = V extends Validatable<infer T> ? T : never;
|
type TypeOf<V extends Validatable<any>> = V extends Validatable<infer T> ? T : never;
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
function union<Key extends string, Config extends UnionValidatorConfig<Key, Config>>(key: Key, config: Config): UnionValidator<Key, Config>;
|
function union<Key extends string, Config extends UnionValidatorConfig<Key, Config>>(key: Key, config: Config): UnionValidator<Key, Config>;
|
||||||
|
@ -187,6 +192,7 @@ const unknownObject: Validator<Record<string, unknown>>;
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type Validatable<T> = {
|
type Validatable<T> = {
|
||||||
validate: (value: unknown) => T;
|
validate: (value: unknown) => T;
|
||||||
|
validateUsingKnownGoodVersion?: (knownGoodValue: T, newValue: unknown) => T;
|
||||||
};
|
};
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
|
@ -202,7 +208,7 @@ class ValidationError extends Error {
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export class Validator<T> implements Validatable<T> {
|
export class Validator<T> implements Validatable<T> {
|
||||||
constructor(validationFn: ValidatorFn<T>);
|
constructor(validationFn: ValidatorFn<T>, validateUsingKnownGoodVersionFn?: undefined | ValidatorUsingKnownGoodVersionFn<T, T>);
|
||||||
check(name: string, checkFn: (value: T) => void): Validator<T>;
|
check(name: string, checkFn: (value: T) => void): Validator<T>;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
check(checkFn: (value: T) => void): Validator<T>;
|
check(checkFn: (value: T) => void): Validator<T>;
|
||||||
|
@ -212,12 +218,19 @@ export class Validator<T> implements Validatable<T> {
|
||||||
refine<U>(otherValidationFn: (value: T) => U): Validator<U>;
|
refine<U>(otherValidationFn: (value: T) => U): Validator<U>;
|
||||||
validate(value: unknown): T;
|
validate(value: unknown): T;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
|
validateUsingKnownGoodVersion(knownGoodValue: T, newValue: unknown): T;
|
||||||
|
// (undocumented)
|
||||||
|
readonly validateUsingKnownGoodVersionFn?: undefined | ValidatorUsingKnownGoodVersionFn<T, T>;
|
||||||
|
// (undocumented)
|
||||||
readonly validationFn: ValidatorFn<T>;
|
readonly validationFn: ValidatorFn<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type ValidatorFn<T> = (value: unknown) => T;
|
type ValidatorFn<T> = (value: unknown) => T;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type ValidatorUsingKnownGoodVersionFn<In, Out = In> = (knownGoodValue: In, value: unknown) => Out;
|
||||||
|
|
||||||
// (No @packageDocumentation comment for this package)
|
// (No @packageDocumentation comment for this package)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
@ -2142,7 +2142,25 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "<Shape>"
|
"text": "<{\n [P in "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Reference",
|
||||||
|
"text": "ExtractRequiredKeys",
|
||||||
|
"canonicalReference": "@tldraw/validate!~ExtractRequiredKeys:type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "<Shape>]: Shape[P];\n} & {\n [P in "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Reference",
|
||||||
|
"text": "ExtractOptionalKeys",
|
||||||
|
"canonicalReference": "@tldraw/validate!~ExtractOptionalKeys:type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "<Shape>]?: Shape[P];\n}>"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
|
@ -2152,7 +2170,7 @@
|
||||||
"fileUrlPath": "packages/validate/src/lib/validation.ts",
|
"fileUrlPath": "packages/validate/src/lib/validation.ts",
|
||||||
"returnTypeTokenRange": {
|
"returnTypeTokenRange": {
|
||||||
"startIndex": 7,
|
"startIndex": 7,
|
||||||
"endIndex": 9
|
"endIndex": 13
|
||||||
},
|
},
|
||||||
"releaseTag": "Public",
|
"releaseTag": "Public",
|
||||||
"overloadIndex": 1,
|
"overloadIndex": 1,
|
||||||
|
@ -2722,7 +2740,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "<unknown>"
|
"text": "<any>"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
|
@ -3193,7 +3211,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "{\n validate: (value: unknown) => T;\n}"
|
"text": "{\n validate: (value: unknown) => T;\n validateUsingKnownGoodVersion?: (knownGoodValue: T, newValue: unknown) => T;\n}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
|
@ -3461,6 +3479,23 @@
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "<T>"
|
"text": "<T>"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ", validateUsingKnownGoodVersionFn?: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "undefined | "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Reference",
|
||||||
|
"text": "ValidatorUsingKnownGoodVersionFn",
|
||||||
|
"canonicalReference": "@tldraw/validate!T.ValidatorUsingKnownGoodVersionFn:type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "<T, T>"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": ");"
|
"text": ");"
|
||||||
|
@ -3477,6 +3512,14 @@
|
||||||
"endIndex": 3
|
"endIndex": 3
|
||||||
},
|
},
|
||||||
"isOptional": false
|
"isOptional": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameterName": "validateUsingKnownGoodVersionFn",
|
||||||
|
"parameterTypeTokenRange": {
|
||||||
|
"startIndex": 4,
|
||||||
|
"endIndex": 7
|
||||||
|
},
|
||||||
|
"isOptional": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -3841,6 +3884,109 @@
|
||||||
"isAbstract": false,
|
"isAbstract": false,
|
||||||
"name": "validate"
|
"name": "validate"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "Method",
|
||||||
|
"canonicalReference": "@tldraw/validate!T.Validator#validateUsingKnownGoodVersion:member(1)",
|
||||||
|
"docComment": "",
|
||||||
|
"excerptTokens": [
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "validateUsingKnownGoodVersion(knownGoodValue: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "T"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ", newValue: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "): "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "T"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ";"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isStatic": false,
|
||||||
|
"returnTypeTokenRange": {
|
||||||
|
"startIndex": 5,
|
||||||
|
"endIndex": 6
|
||||||
|
},
|
||||||
|
"releaseTag": "Public",
|
||||||
|
"isProtected": false,
|
||||||
|
"overloadIndex": 1,
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"parameterName": "knownGoodValue",
|
||||||
|
"parameterTypeTokenRange": {
|
||||||
|
"startIndex": 1,
|
||||||
|
"endIndex": 2
|
||||||
|
},
|
||||||
|
"isOptional": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameterName": "newValue",
|
||||||
|
"parameterTypeTokenRange": {
|
||||||
|
"startIndex": 3,
|
||||||
|
"endIndex": 4
|
||||||
|
},
|
||||||
|
"isOptional": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isOptional": false,
|
||||||
|
"isAbstract": false,
|
||||||
|
"name": "validateUsingKnownGoodVersion"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Property",
|
||||||
|
"canonicalReference": "@tldraw/validate!T.Validator#validateUsingKnownGoodVersionFn:member",
|
||||||
|
"docComment": "",
|
||||||
|
"excerptTokens": [
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "readonly validateUsingKnownGoodVersionFn?: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "undefined | "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Reference",
|
||||||
|
"text": "ValidatorUsingKnownGoodVersionFn",
|
||||||
|
"canonicalReference": "@tldraw/validate!T.ValidatorUsingKnownGoodVersionFn:type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "<T, T>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ";"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isReadonly": true,
|
||||||
|
"isOptional": true,
|
||||||
|
"releaseTag": "Public",
|
||||||
|
"name": "validateUsingKnownGoodVersionFn",
|
||||||
|
"propertyTypeTokenRange": {
|
||||||
|
"startIndex": 1,
|
||||||
|
"endIndex": 4
|
||||||
|
},
|
||||||
|
"isStatic": false,
|
||||||
|
"isProtected": false,
|
||||||
|
"isAbstract": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "Property",
|
"kind": "Property",
|
||||||
"canonicalReference": "@tldraw/validate!T.Validator#validationFn:member",
|
"canonicalReference": "@tldraw/validate!T.Validator#validationFn:member",
|
||||||
|
@ -3922,6 +4068,64 @@
|
||||||
"startIndex": 1,
|
"startIndex": 1,
|
||||||
"endIndex": 2
|
"endIndex": 2
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "TypeAlias",
|
||||||
|
"canonicalReference": "@tldraw/validate!T.ValidatorUsingKnownGoodVersionFn:type",
|
||||||
|
"docComment": "/**\n * @public\n */\n",
|
||||||
|
"excerptTokens": [
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "export type ValidatorUsingKnownGoodVersionFn<In, Out = "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "In"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "> = "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "(knownGoodValue: In, value: unknown) => Out"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ";"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fileUrlPath": "packages/validate/src/lib/validation.ts",
|
||||||
|
"releaseTag": "Public",
|
||||||
|
"name": "ValidatorUsingKnownGoodVersionFn",
|
||||||
|
"typeParameters": [
|
||||||
|
{
|
||||||
|
"typeParameterName": "In",
|
||||||
|
"constraintTokenRange": {
|
||||||
|
"startIndex": 0,
|
||||||
|
"endIndex": 0
|
||||||
|
},
|
||||||
|
"defaultTypeTokenRange": {
|
||||||
|
"startIndex": 0,
|
||||||
|
"endIndex": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"typeParameterName": "Out",
|
||||||
|
"constraintTokenRange": {
|
||||||
|
"startIndex": 0,
|
||||||
|
"endIndex": 0
|
||||||
|
},
|
||||||
|
"defaultTypeTokenRange": {
|
||||||
|
"startIndex": 1,
|
||||||
|
"endIndex": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"typeTokenRange": {
|
||||||
|
"startIndex": 3,
|
||||||
|
"endIndex": 4
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -4224,6 +4428,23 @@
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": "<T>"
|
"text": "<T>"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ", validateUsingKnownGoodVersionFn?: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "undefined | "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Reference",
|
||||||
|
"text": "ValidatorUsingKnownGoodVersionFn",
|
||||||
|
"canonicalReference": "@tldraw/validate!T.ValidatorUsingKnownGoodVersionFn:type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "<T, T>"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "Content",
|
"kind": "Content",
|
||||||
"text": ");"
|
"text": ");"
|
||||||
|
@ -4240,6 +4461,14 @@
|
||||||
"endIndex": 3
|
"endIndex": 3
|
||||||
},
|
},
|
||||||
"isOptional": false
|
"isOptional": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameterName": "validateUsingKnownGoodVersionFn",
|
||||||
|
"parameterTypeTokenRange": {
|
||||||
|
"startIndex": 4,
|
||||||
|
"endIndex": 7
|
||||||
|
},
|
||||||
|
"isOptional": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -4604,6 +4833,109 @@
|
||||||
"isAbstract": false,
|
"isAbstract": false,
|
||||||
"name": "validate"
|
"name": "validate"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "Method",
|
||||||
|
"canonicalReference": "@tldraw/validate!Validator#validateUsingKnownGoodVersion:member(1)",
|
||||||
|
"docComment": "",
|
||||||
|
"excerptTokens": [
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "validateUsingKnownGoodVersion(knownGoodValue: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "T"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ", newValue: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "unknown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "): "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "T"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ";"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isStatic": false,
|
||||||
|
"returnTypeTokenRange": {
|
||||||
|
"startIndex": 5,
|
||||||
|
"endIndex": 6
|
||||||
|
},
|
||||||
|
"releaseTag": "Public",
|
||||||
|
"isProtected": false,
|
||||||
|
"overloadIndex": 1,
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"parameterName": "knownGoodValue",
|
||||||
|
"parameterTypeTokenRange": {
|
||||||
|
"startIndex": 1,
|
||||||
|
"endIndex": 2
|
||||||
|
},
|
||||||
|
"isOptional": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameterName": "newValue",
|
||||||
|
"parameterTypeTokenRange": {
|
||||||
|
"startIndex": 3,
|
||||||
|
"endIndex": 4
|
||||||
|
},
|
||||||
|
"isOptional": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isOptional": false,
|
||||||
|
"isAbstract": false,
|
||||||
|
"name": "validateUsingKnownGoodVersion"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Property",
|
||||||
|
"canonicalReference": "@tldraw/validate!Validator#validateUsingKnownGoodVersionFn:member",
|
||||||
|
"docComment": "",
|
||||||
|
"excerptTokens": [
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "readonly validateUsingKnownGoodVersionFn?: "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "undefined | "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Reference",
|
||||||
|
"text": "ValidatorUsingKnownGoodVersionFn",
|
||||||
|
"canonicalReference": "@tldraw/validate!T.ValidatorUsingKnownGoodVersionFn:type"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": "<T, T>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Content",
|
||||||
|
"text": ";"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isReadonly": true,
|
||||||
|
"isOptional": true,
|
||||||
|
"releaseTag": "Public",
|
||||||
|
"name": "validateUsingKnownGoodVersionFn",
|
||||||
|
"propertyTypeTokenRange": {
|
||||||
|
"startIndex": 1,
|
||||||
|
"endIndex": 4
|
||||||
|
},
|
||||||
|
"isStatic": false,
|
||||||
|
"isProtected": false,
|
||||||
|
"isAbstract": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "Property",
|
"kind": "Property",
|
||||||
"canonicalReference": "@tldraw/validate!Validator#validationFn:member",
|
"canonicalReference": "@tldraw/validate!Validator#validationFn:member",
|
||||||
|
|
|
@ -53,6 +53,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"lazyrepo": "0.0.0-alpha.27"
|
"lazyrepo": "0.0.0-alpha.27",
|
||||||
|
"lodash.isequal": "^4.5.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,26 @@ import {
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type ValidatorFn<T> = (value: unknown) => T
|
export type ValidatorFn<T> = (value: unknown) => T
|
||||||
|
/** @public */
|
||||||
|
export type ValidatorUsingKnownGoodVersionFn<In, Out = In> = (
|
||||||
|
knownGoodValue: In,
|
||||||
|
value: unknown
|
||||||
|
) => Out
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type Validatable<T> = { validate: (value: unknown) => T }
|
export type Validatable<T> = {
|
||||||
|
validate: (value: unknown) => T
|
||||||
|
/**
|
||||||
|
* This is a performance optimizing version of validate that can use a previous
|
||||||
|
* version of the value to avoid revalidating every part of the new value if
|
||||||
|
* any part of it has not changed since the last validation.
|
||||||
|
*
|
||||||
|
* If the value has not changed but is not referentially equal, the function
|
||||||
|
* should return the previous value.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
validateUsingKnownGoodVersion?: (knownGoodValue: T, newValue: unknown) => T
|
||||||
|
}
|
||||||
|
|
||||||
function formatPath(path: ReadonlyArray<number | string>): string | null {
|
function formatPath(path: ReadonlyArray<number | string>): string | null {
|
||||||
if (!path.length) {
|
if (!path.length) {
|
||||||
|
@ -92,11 +109,14 @@ function typeToString(value: unknown): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export type TypeOf<V extends Validatable<unknown>> = V extends Validatable<infer T> ? T : never
|
export type TypeOf<V extends Validatable<any>> = V extends Validatable<infer T> ? T : never
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export class Validator<T> implements Validatable<T> {
|
export class Validator<T> implements Validatable<T> {
|
||||||
constructor(readonly validationFn: ValidatorFn<T>) {}
|
constructor(
|
||||||
|
readonly validationFn: ValidatorFn<T>,
|
||||||
|
readonly validateUsingKnownGoodVersionFn?: ValidatorUsingKnownGoodVersionFn<T>
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that the passed value is of the correct type and returns it. The returned value is
|
* Asserts that the passed value is of the correct type and returns it. The returned value is
|
||||||
|
@ -110,6 +130,18 @@ export class Validator<T> implements Validatable<T> {
|
||||||
return validated
|
return validated
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateUsingKnownGoodVersion(knownGoodValue: T, newValue: unknown): T {
|
||||||
|
if (Object.is(knownGoodValue, newValue)) {
|
||||||
|
return knownGoodValue as T
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.validateUsingKnownGoodVersionFn) {
|
||||||
|
return this.validateUsingKnownGoodVersionFn(knownGoodValue, newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.validate(newValue)
|
||||||
|
}
|
||||||
|
|
||||||
/** Checks that the passed value is of the correct type. */
|
/** Checks that the passed value is of the correct type. */
|
||||||
isValid(value: unknown): value is T {
|
isValid(value: unknown): value is T {
|
||||||
try {
|
try {
|
||||||
|
@ -141,9 +173,19 @@ export class Validator<T> implements Validatable<T> {
|
||||||
* if the value can't be converted to the new type, or return the new type otherwise.
|
* if the value can't be converted to the new type, or return the new type otherwise.
|
||||||
*/
|
*/
|
||||||
refine<U>(otherValidationFn: (value: T) => U): Validator<U> {
|
refine<U>(otherValidationFn: (value: T) => U): Validator<U> {
|
||||||
return new Validator((value) => {
|
return new Validator(
|
||||||
return otherValidationFn(this.validate(value))
|
(value) => {
|
||||||
})
|
return otherValidationFn(this.validate(value))
|
||||||
|
},
|
||||||
|
|
||||||
|
(knownGoodValue, newValue) => {
|
||||||
|
const validated = this.validateUsingKnownGoodVersion(knownGoodValue as any, newValue)
|
||||||
|
if (Object.is(knownGoodValue, validated)) {
|
||||||
|
return knownGoodValue
|
||||||
|
}
|
||||||
|
return otherValidationFn(validated)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -179,13 +221,40 @@ export class Validator<T> implements Validatable<T> {
|
||||||
/** @public */
|
/** @public */
|
||||||
export class ArrayOfValidator<T> extends Validator<T[]> {
|
export class ArrayOfValidator<T> extends Validator<T[]> {
|
||||||
constructor(readonly itemValidator: Validatable<T>) {
|
constructor(readonly itemValidator: Validatable<T>) {
|
||||||
super((value) => {
|
super(
|
||||||
const arr = array.validate(value)
|
(value) => {
|
||||||
for (let i = 0; i < arr.length; i++) {
|
const arr = array.validate(value)
|
||||||
prefixError(i, () => itemValidator.validate(arr[i]))
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
prefixError(i, () => itemValidator.validate(arr[i]))
|
||||||
|
}
|
||||||
|
return arr as T[]
|
||||||
|
},
|
||||||
|
(knownGoodValue, newValue) => {
|
||||||
|
if (!itemValidator.validateUsingKnownGoodVersion) return this.validate(newValue)
|
||||||
|
const arr = array.validate(newValue)
|
||||||
|
let isDifferent = knownGoodValue.length !== arr.length
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
const item = arr[i]
|
||||||
|
if (i >= knownGoodValue.length) {
|
||||||
|
isDifferent = true
|
||||||
|
prefixError(i, () => itemValidator.validate(item))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// sneaky quick check here to avoid the prefix + validator overhead
|
||||||
|
if (Object.is(knownGoodValue[i], item)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const checkedItem = prefixError(i, () =>
|
||||||
|
itemValidator.validateUsingKnownGoodVersion!(knownGoodValue[i], item)
|
||||||
|
)
|
||||||
|
if (!Object.is(checkedItem, knownGoodValue[i])) {
|
||||||
|
isDifferent = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isDifferent ? (newValue as T[]) : knownGoodValue
|
||||||
}
|
}
|
||||||
return arr as T[]
|
)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nonEmpty() {
|
nonEmpty() {
|
||||||
|
@ -213,27 +282,68 @@ export class ObjectValidator<Shape extends object> extends Validator<Shape> {
|
||||||
},
|
},
|
||||||
private readonly shouldAllowUnknownProperties = false
|
private readonly shouldAllowUnknownProperties = false
|
||||||
) {
|
) {
|
||||||
super((object) => {
|
super(
|
||||||
if (typeof object !== 'object' || object === null) {
|
(object) => {
|
||||||
throw new ValidationError(`Expected object, got ${typeToString(object)}`)
|
if (typeof object !== 'object' || object === null) {
|
||||||
}
|
throw new ValidationError(`Expected object, got ${typeToString(object)}`)
|
||||||
|
}
|
||||||
|
|
||||||
for (const [key, validator] of Object.entries(config)) {
|
for (const [key, validator] of Object.entries(config)) {
|
||||||
prefixError(key, () => {
|
prefixError(key, () => {
|
||||||
;(validator as Validator<unknown>).validate(getOwnProperty(object, key))
|
;(validator as Validator<unknown>).validate(getOwnProperty(object, key))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!shouldAllowUnknownProperties) {
|
if (!shouldAllowUnknownProperties) {
|
||||||
for (const key of Object.keys(object)) {
|
for (const key of Object.keys(object)) {
|
||||||
if (!hasOwnProperty(config, key)) {
|
if (!hasOwnProperty(config, key)) {
|
||||||
throw new ValidationError(`Unexpected property`, [key])
|
throw new ValidationError(`Unexpected property`, [key])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return object as Shape
|
return object as Shape
|
||||||
})
|
},
|
||||||
|
(knownGoodValue, newValue) => {
|
||||||
|
if (typeof newValue !== 'object' || newValue === null) {
|
||||||
|
throw new ValidationError(`Expected object, got ${typeToString(newValue)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let isDifferent = false
|
||||||
|
|
||||||
|
for (const [key, validator] of Object.entries(config)) {
|
||||||
|
const prev = getOwnProperty(knownGoodValue, key)
|
||||||
|
const next = getOwnProperty(newValue, key)
|
||||||
|
// sneaky quick check here to avoid the prefix + validator overhead
|
||||||
|
if (Object.is(prev, next)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const checked = prefixError(key, () => {
|
||||||
|
return (validator as Validator<unknown>).validateUsingKnownGoodVersion(prev, next)
|
||||||
|
})
|
||||||
|
if (!Object.is(checked, prev)) {
|
||||||
|
isDifferent = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldAllowUnknownProperties) {
|
||||||
|
for (const key of Object.keys(newValue)) {
|
||||||
|
if (!hasOwnProperty(config, key)) {
|
||||||
|
throw new ValidationError(`Unexpected property`, [key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of Object.keys(knownGoodValue)) {
|
||||||
|
if (!hasOwnProperty(newValue, key)) {
|
||||||
|
isDifferent = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isDifferent ? (newValue as Shape) : knownGoodValue
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
allowUnknownProperties() {
|
allowUnknownProperties() {
|
||||||
|
@ -257,7 +367,7 @@ export class ObjectValidator<Shape extends object> extends Validator<Shape> {
|
||||||
extend<Extension extends Record<string, unknown>>(extension: {
|
extend<Extension extends Record<string, unknown>>(extension: {
|
||||||
readonly [K in keyof Extension]: Validatable<Extension[K]>
|
readonly [K in keyof Extension]: Validatable<Extension[K]>
|
||||||
}): ObjectValidator<Shape & Extension> {
|
}): ObjectValidator<Shape & Extension> {
|
||||||
return new ObjectValidator({ ...this.config, ...extension }) as ObjectValidator<
|
return new ObjectValidator({ ...this.config, ...extension }) as any as ObjectValidator<
|
||||||
Shape & Extension
|
Shape & Extension
|
||||||
>
|
>
|
||||||
}
|
}
|
||||||
|
@ -280,25 +390,61 @@ export class UnionValidator<
|
||||||
private readonly config: Config,
|
private readonly config: Config,
|
||||||
private readonly unknownValueValidation: (value: object, variant: string) => UnknownValue
|
private readonly unknownValueValidation: (value: object, variant: string) => UnknownValue
|
||||||
) {
|
) {
|
||||||
super((input) => {
|
super(
|
||||||
if (typeof input !== 'object' || input === null) {
|
(input) => {
|
||||||
throw new ValidationError(`Expected an object, got ${typeToString(input)}`, [])
|
this.expectObject(input)
|
||||||
}
|
|
||||||
|
|
||||||
const variant = getOwnProperty(input, key) as keyof Config | undefined
|
const { matchingSchema, variant } = this.getMatchingSchemaAndVariant(input)
|
||||||
if (typeof variant !== 'string') {
|
if (matchingSchema === undefined) {
|
||||||
throw new ValidationError(
|
return this.unknownValueValidation(input, variant)
|
||||||
`Expected a string for key "${key}", got ${typeToString(variant)}`
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchingSchema = hasOwnProperty(config, variant) ? config[variant] : undefined
|
return prefixError(`(${key} = ${variant})`, () => matchingSchema.validate(input))
|
||||||
if (matchingSchema === undefined) {
|
},
|
||||||
return this.unknownValueValidation(input, variant)
|
(prevValue, newValue) => {
|
||||||
}
|
this.expectObject(newValue)
|
||||||
|
this.expectObject(prevValue)
|
||||||
|
|
||||||
return prefixError(`(${key} = ${variant})`, () => matchingSchema.validate(input))
|
const { matchingSchema, variant } = this.getMatchingSchemaAndVariant(newValue)
|
||||||
})
|
if (matchingSchema === undefined) {
|
||||||
|
return this.unknownValueValidation(newValue, variant)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getOwnProperty(prevValue, key) !== getOwnProperty(newValue, key)) {
|
||||||
|
// the type has changed so bail out and do a regular validation
|
||||||
|
return prefixError(`(${key} = ${variant})`, () => matchingSchema.validate(newValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
return prefixError(`(${key} = ${variant})`, () => {
|
||||||
|
if (matchingSchema.validateUsingKnownGoodVersion) {
|
||||||
|
return matchingSchema.validateUsingKnownGoodVersion(prevValue, newValue)
|
||||||
|
} else {
|
||||||
|
return matchingSchema.validate(newValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private expectObject(value: unknown): asserts value is object {
|
||||||
|
if (typeof value !== 'object' || value === null) {
|
||||||
|
throw new ValidationError(`Expected an object, got ${typeToString(value)}`, [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getMatchingSchemaAndVariant(object: object): {
|
||||||
|
matchingSchema: Validatable<any> | undefined
|
||||||
|
variant: string
|
||||||
|
} {
|
||||||
|
const variant = getOwnProperty(object, this.key) as keyof Config | undefined
|
||||||
|
if (typeof variant !== 'string') {
|
||||||
|
throw new ValidationError(
|
||||||
|
`Expected a string for key "${this.key}", got ${typeToString(variant)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchingSchema = hasOwnProperty(this.config, variant) ? this.config[variant] : undefined
|
||||||
|
return { matchingSchema, variant }
|
||||||
}
|
}
|
||||||
|
|
||||||
validateUnknownVariants<Unknown>(
|
validateUnknownVariants<Unknown>(
|
||||||
|
@ -314,20 +460,65 @@ export class DictValidator<Key extends string, Value> extends Validator<Record<K
|
||||||
public readonly keyValidator: Validatable<Key>,
|
public readonly keyValidator: Validatable<Key>,
|
||||||
public readonly valueValidator: Validatable<Value>
|
public readonly valueValidator: Validatable<Value>
|
||||||
) {
|
) {
|
||||||
super((object) => {
|
super(
|
||||||
if (typeof object !== 'object' || object === null) {
|
(object) => {
|
||||||
throw new ValidationError(`Expected object, got ${typeToString(object)}`)
|
if (typeof object !== 'object' || object === null) {
|
||||||
}
|
throw new ValidationError(`Expected object, got ${typeToString(object)}`)
|
||||||
|
}
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(object)) {
|
for (const [key, value] of Object.entries(object)) {
|
||||||
prefixError(key, () => {
|
prefixError(key, () => {
|
||||||
keyValidator.validate(key)
|
keyValidator.validate(key)
|
||||||
valueValidator.validate(value)
|
valueValidator.validate(value)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return object as Record<Key, Value>
|
return object as Record<Key, Value>
|
||||||
})
|
},
|
||||||
|
(knownGoodValue, newValue) => {
|
||||||
|
if (typeof newValue !== 'object' || newValue === null) {
|
||||||
|
throw new ValidationError(`Expected object, got ${typeToString(newValue)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let isDifferent = false
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(newValue)) {
|
||||||
|
if (!hasOwnProperty(knownGoodValue, key)) {
|
||||||
|
isDifferent = true
|
||||||
|
prefixError(key, () => {
|
||||||
|
keyValidator.validate(key)
|
||||||
|
valueValidator.validate(value)
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const prev = getOwnProperty(knownGoodValue, key)
|
||||||
|
const next = value
|
||||||
|
// sneaky quick check here to avoid the prefix + validator overhead
|
||||||
|
if (Object.is(prev, next)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const checked = prefixError(key, () => {
|
||||||
|
if (valueValidator.validateUsingKnownGoodVersion) {
|
||||||
|
return valueValidator.validateUsingKnownGoodVersion(prev as any, next)
|
||||||
|
} else {
|
||||||
|
return valueValidator.validate(next)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!Object.is(checked, prev)) {
|
||||||
|
isDifferent = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of Object.keys(knownGoodValue)) {
|
||||||
|
if (!hasOwnProperty(newValue, key)) {
|
||||||
|
isDifferent = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isDifferent ? (newValue as Record<Key, Value>) : knownGoodValue
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -477,6 +668,14 @@ export const unknownObject = new Validator<Record<string, unknown>>((value) => {
|
||||||
return value as Record<string, unknown>
|
return value as Record<string, unknown>
|
||||||
})
|
})
|
||||||
|
|
||||||
|
type ExtractRequiredKeys<T extends object> = {
|
||||||
|
[K in keyof T]: undefined extends T[K] ? never : K
|
||||||
|
}[keyof T]
|
||||||
|
|
||||||
|
type ExtractOptionalKeys<T extends object> = {
|
||||||
|
[K in keyof T]: undefined extends T[K] ? K : never
|
||||||
|
}[keyof T]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate an object has a particular shape.
|
* Validate an object has a particular shape.
|
||||||
*
|
*
|
||||||
|
@ -484,8 +683,18 @@ export const unknownObject = new Validator<Record<string, unknown>>((value) => {
|
||||||
*/
|
*/
|
||||||
export function object<Shape extends object>(config: {
|
export function object<Shape extends object>(config: {
|
||||||
readonly [K in keyof Shape]: Validatable<Shape[K]>
|
readonly [K in keyof Shape]: Validatable<Shape[K]>
|
||||||
}): ObjectValidator<Shape> {
|
}): ObjectValidator<
|
||||||
return new ObjectValidator(config)
|
{ [P in ExtractRequiredKeys<Shape>]: Shape[P] } & { [P in ExtractOptionalKeys<Shape>]?: Shape[P] }
|
||||||
|
> {
|
||||||
|
return new ObjectValidator(config) as any
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||||
|
return (
|
||||||
|
typeof value === 'object' &&
|
||||||
|
value !== null &&
|
||||||
|
(value.constructor === Object || !value.constructor)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidJson(value: any): value is JsonValue {
|
function isValidJson(value: any): value is JsonValue {
|
||||||
|
@ -502,7 +711,7 @@ function isValidJson(value: any): value is JsonValue {
|
||||||
return value.every(isValidJson)
|
return value.every(isValidJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === 'object') {
|
if (isPlainObject(value)) {
|
||||||
return Object.values(value).every(isValidJson)
|
return Object.values(value).every(isValidJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -514,13 +723,64 @@ function isValidJson(value: any): value is JsonValue {
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export const jsonValue = new Validator<JsonValue>((value): JsonValue => {
|
export const jsonValue: Validator<JsonValue> = new Validator<JsonValue>(
|
||||||
if (isValidJson(value)) {
|
(value): JsonValue => {
|
||||||
return value as JsonValue
|
if (isValidJson(value)) {
|
||||||
}
|
return value as JsonValue
|
||||||
|
}
|
||||||
|
|
||||||
throw new ValidationError(`Expected json serializable value, got ${typeof value}`)
|
throw new ValidationError(`Expected json serializable value, got ${typeof value}`)
|
||||||
})
|
},
|
||||||
|
(knownGoodValue, newValue) => {
|
||||||
|
if (Array.isArray(knownGoodValue) && Array.isArray(newValue)) {
|
||||||
|
let isDifferent = knownGoodValue.length !== newValue.length
|
||||||
|
for (let i = 0; i < newValue.length; i++) {
|
||||||
|
if (i >= knownGoodValue.length) {
|
||||||
|
isDifferent = true
|
||||||
|
jsonValue.validate(newValue[i])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const prev = knownGoodValue[i]
|
||||||
|
const next = newValue[i]
|
||||||
|
if (Object.is(prev, next)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const checked = jsonValue.validateUsingKnownGoodVersion!(prev, next)
|
||||||
|
if (!Object.is(checked, prev)) {
|
||||||
|
isDifferent = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isDifferent ? (newValue as JsonValue) : knownGoodValue
|
||||||
|
} else if (isPlainObject(knownGoodValue) && isPlainObject(newValue)) {
|
||||||
|
let isDifferent = false
|
||||||
|
for (const key of Object.keys(newValue)) {
|
||||||
|
if (!hasOwnProperty(knownGoodValue, key)) {
|
||||||
|
isDifferent = true
|
||||||
|
jsonValue.validate(newValue[key])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const prev = knownGoodValue[key]
|
||||||
|
const next = newValue[key]
|
||||||
|
if (Object.is(prev, next)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const checked = jsonValue.validateUsingKnownGoodVersion!(prev!, next)
|
||||||
|
if (!Object.is(checked, prev)) {
|
||||||
|
isDifferent = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const key of Object.keys(knownGoodValue)) {
|
||||||
|
if (!hasOwnProperty(newValue, key)) {
|
||||||
|
isDifferent = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isDifferent ? (newValue as JsonValue) : knownGoodValue
|
||||||
|
} else {
|
||||||
|
return jsonValue.validate(newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate an object has a particular shape.
|
* Validate an object has a particular shape.
|
||||||
|
@ -581,14 +841,20 @@ export function model<T extends { readonly id: string }>(
|
||||||
name: string,
|
name: string,
|
||||||
validator: Validatable<T>
|
validator: Validatable<T>
|
||||||
): Validator<T> {
|
): Validator<T> {
|
||||||
return new Validator((value) => {
|
return new Validator(
|
||||||
const prefix =
|
(value) => {
|
||||||
value && typeof value === 'object' && 'id' in value && typeof value.id === 'string'
|
return prefixError(name, () => validator.validate(value))
|
||||||
? `${name}(id = ${value.id})`
|
},
|
||||||
: name
|
(prevValue, newValue) => {
|
||||||
|
return prefixError(name, () => {
|
||||||
return prefixError(prefix, () => validator.validate(value))
|
if (validator.validateUsingKnownGoodVersion) {
|
||||||
})
|
return validator.validateUsingKnownGoodVersion(prevValue, newValue)
|
||||||
|
} else {
|
||||||
|
return validator.validate(newValue)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
@ -604,18 +870,37 @@ export function setEnum<T>(values: ReadonlySet<T>): Validator<T> {
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function optional<T>(validator: Validatable<T>): Validator<T | undefined> {
|
export function optional<T>(validator: Validatable<T>): Validator<T | undefined> {
|
||||||
return new Validator((value) => {
|
return new Validator(
|
||||||
if (value === undefined) return undefined
|
(value) => {
|
||||||
return validator.validate(value)
|
if (value === undefined) return undefined
|
||||||
})
|
return validator.validate(value)
|
||||||
|
},
|
||||||
|
(knownGoodValue, newValue) => {
|
||||||
|
if (knownGoodValue === undefined && newValue === undefined) return undefined
|
||||||
|
if (newValue === undefined) return undefined
|
||||||
|
if (validator.validateUsingKnownGoodVersion && knownGoodValue !== undefined) {
|
||||||
|
return validator.validateUsingKnownGoodVersion(knownGoodValue as T, newValue)
|
||||||
|
}
|
||||||
|
return validator.validate(newValue)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function nullable<T>(validator: Validatable<T>): Validator<T | null> {
|
export function nullable<T>(validator: Validatable<T>): Validator<T | null> {
|
||||||
return new Validator((value) => {
|
return new Validator(
|
||||||
if (value === null) return null
|
(value) => {
|
||||||
return validator.validate(value)
|
if (value === null) return null
|
||||||
})
|
return validator.validate(value)
|
||||||
|
},
|
||||||
|
(knownGoodValue, newValue) => {
|
||||||
|
if (newValue === null) return null
|
||||||
|
if (validator.validateUsingKnownGoodVersion && knownGoodValue !== null) {
|
||||||
|
return validator.validateUsingKnownGoodVersion(knownGoodValue as T, newValue)
|
||||||
|
}
|
||||||
|
return validator.validate(newValue)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
|
|
379
packages/validate/src/test/validation.fuzz.test.ts
Normal file
379
packages/validate/src/test/validation.fuzz.test.ts
Normal file
|
@ -0,0 +1,379 @@
|
||||||
|
import { mapObjectMapValues } from '@tldraw/utils'
|
||||||
|
import isEqual from 'lodash.isequal'
|
||||||
|
import { T, Validator } from '..'
|
||||||
|
|
||||||
|
class RandomSource {
|
||||||
|
private seed: number
|
||||||
|
|
||||||
|
constructor(seed: number) {
|
||||||
|
this.seed = seed
|
||||||
|
}
|
||||||
|
|
||||||
|
nextFloat(): number {
|
||||||
|
this.seed = (this.seed * 9301 + 49297) % 233280
|
||||||
|
return this.seed / 233280
|
||||||
|
}
|
||||||
|
|
||||||
|
nextInt(max: number): number {
|
||||||
|
return Math.floor(this.nextFloat() * max)
|
||||||
|
}
|
||||||
|
|
||||||
|
nextIntInRange(min: number, max: number): number {
|
||||||
|
return this.nextInt(max - min) + min
|
||||||
|
}
|
||||||
|
|
||||||
|
nextId(): string {
|
||||||
|
return this.nextInt(Number.MAX_SAFE_INTEGER).toString(36)
|
||||||
|
}
|
||||||
|
|
||||||
|
selectOne<T>(arr: readonly T[]): T {
|
||||||
|
return arr[this.nextInt(arr.length)]
|
||||||
|
}
|
||||||
|
|
||||||
|
choice(probability: number): boolean {
|
||||||
|
return this.nextFloat() < probability
|
||||||
|
}
|
||||||
|
|
||||||
|
executeOne<Result>(
|
||||||
|
_choices: Record<string, (() => Result) | { weight?: number; do(): Result }>
|
||||||
|
): Result {
|
||||||
|
const choices = Object.values(_choices).map((choice) => {
|
||||||
|
if (typeof choice === 'function') {
|
||||||
|
return { weight: 1, do: choice }
|
||||||
|
}
|
||||||
|
return choice
|
||||||
|
})
|
||||||
|
const totalWeight = Object.values(choices).reduce(
|
||||||
|
(total, choice) => total + (choice.weight ?? 1),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
const randomWeight = this.nextInt(totalWeight)
|
||||||
|
let weight = 0
|
||||||
|
for (const choice of Object.values(choices)) {
|
||||||
|
weight += choice.weight ?? 1
|
||||||
|
if (randomWeight < weight) {
|
||||||
|
return choice.do()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('unreachable')
|
||||||
|
}
|
||||||
|
|
||||||
|
nextPropertyName(): string {
|
||||||
|
return this.selectOne(['foo', 'bar', 'baz', 'qux', 'mux', 'bah'])
|
||||||
|
}
|
||||||
|
|
||||||
|
nextJsonValue(): any {
|
||||||
|
return this.executeOne<any>({
|
||||||
|
string: { weight: 1, do: () => this.nextId() },
|
||||||
|
number: { weight: 1, do: () => this.nextFloat() },
|
||||||
|
integer: { weight: 1, do: () => this.nextInt(100) },
|
||||||
|
boolean: { weight: 1, do: () => this.choice(0.5) },
|
||||||
|
null: { weight: 1, do: () => null },
|
||||||
|
array: {
|
||||||
|
weight: 1,
|
||||||
|
do: () => {
|
||||||
|
const numItems = this.nextInt(4)
|
||||||
|
const result = []
|
||||||
|
for (let i = 0; i < numItems; i++) {
|
||||||
|
result.push(this.nextJsonValue())
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
},
|
||||||
|
object: {
|
||||||
|
weight: 1,
|
||||||
|
do: () => {
|
||||||
|
const numItems = this.nextInt(4)
|
||||||
|
const result = {} as any
|
||||||
|
for (let i = 0; i < numItems; i++) {
|
||||||
|
result[this.nextPropertyName()] = this.nextJsonValue()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
nextTestType(depth: number): TestType {
|
||||||
|
if (depth >= 3) {
|
||||||
|
return this.selectOne(Object.values(builtinTypes))
|
||||||
|
}
|
||||||
|
return this.executeOne<TestType>({
|
||||||
|
primitive: () => this.selectOne(Object.values(builtinTypes)),
|
||||||
|
array: () => generateArrayType(this, depth),
|
||||||
|
object: () => generateObjectType(this, {}, depth),
|
||||||
|
union: () => generateUnionType(this, depth),
|
||||||
|
dict: () => generateDictType(this, depth),
|
||||||
|
model: () => {
|
||||||
|
const objType = generateObjectType(this, {}, depth)
|
||||||
|
const name = this.nextPropertyName()
|
||||||
|
return {
|
||||||
|
...objType,
|
||||||
|
validator: T.model(name, objType.validator),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestType = {
|
||||||
|
validator: T.Validator<any>
|
||||||
|
generateValid: (source: RandomSource) => any
|
||||||
|
generateInvalid: (source: RandomSource) => any
|
||||||
|
}
|
||||||
|
const builtinTypes = {
|
||||||
|
string: {
|
||||||
|
validator: T.string,
|
||||||
|
generateValid: (source) => source.selectOne(['a', 'b', 'c', 'd']),
|
||||||
|
generateInvalid: (source) => source.selectOne([5, /regexp/, {}]),
|
||||||
|
},
|
||||||
|
number: {
|
||||||
|
validator: T.number,
|
||||||
|
generateValid: (source) => source.nextInt(5),
|
||||||
|
generateInvalid: (source) => source.selectOne(['a', /num/]),
|
||||||
|
},
|
||||||
|
integer: {
|
||||||
|
validator: T.integer,
|
||||||
|
generateValid: (source) => source.nextInt(5),
|
||||||
|
generateInvalid: (source) => source.selectOne([0.2, '3', 5n, /int/]),
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
validator: T.jsonValue,
|
||||||
|
generateValid: (source) => source.nextJsonValue(),
|
||||||
|
generateInvalid: (source) => source.selectOne([/regexp/, 343n, { key: /regexp/ }]),
|
||||||
|
},
|
||||||
|
} as const satisfies Record<string, TestType>
|
||||||
|
|
||||||
|
function generateObjectType(
|
||||||
|
source: RandomSource,
|
||||||
|
injectProperties: Record<string, TestType>,
|
||||||
|
depth: number
|
||||||
|
): TestType {
|
||||||
|
const numProperties = source.nextIntInRange(1, 5)
|
||||||
|
const propertyTypes: Record<string, TestType> = {
|
||||||
|
...injectProperties,
|
||||||
|
}
|
||||||
|
const optionalTypes = new Set<string>()
|
||||||
|
const nullableTypes = new Set<string>()
|
||||||
|
for (let i = 0; i < numProperties; i++) {
|
||||||
|
const type = source.nextTestType(depth + 1)
|
||||||
|
const name = source.nextPropertyName()
|
||||||
|
if (source.choice(0.2)) {
|
||||||
|
optionalTypes.add(name)
|
||||||
|
}
|
||||||
|
if (source.choice(0.2)) {
|
||||||
|
nullableTypes.add(name)
|
||||||
|
}
|
||||||
|
let validator = type.validator
|
||||||
|
if (nullableTypes.has(name)) {
|
||||||
|
validator = validator.nullable()
|
||||||
|
}
|
||||||
|
if (optionalTypes.has(name)) {
|
||||||
|
validator = validator.optional()
|
||||||
|
}
|
||||||
|
propertyTypes[name] = { ...type, validator }
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateValid = (source: RandomSource) => {
|
||||||
|
const result = {} as any
|
||||||
|
for (const [name, type] of Object.entries(propertyTypes)) {
|
||||||
|
if (optionalTypes.has(name) && source.choice(0.2)) {
|
||||||
|
continue
|
||||||
|
} else if (nullableTypes.has(name) && source.choice(0.2)) {
|
||||||
|
result[name] = null
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result[name] = type.generateValid(source)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
validator: T.object(mapObjectMapValues(propertyTypes, (_, { validator }) => validator)),
|
||||||
|
generateValid,
|
||||||
|
generateInvalid: (source) => {
|
||||||
|
return source.executeOne<any>({
|
||||||
|
otherType: () =>
|
||||||
|
source.executeOne<any>({
|
||||||
|
string: () => source.selectOne(['a', 'b', 'c', 'd']),
|
||||||
|
number: () => source.nextInt(5),
|
||||||
|
array: () => [source.nextId(), source.nextFloat()],
|
||||||
|
bool: () => true,
|
||||||
|
}),
|
||||||
|
missingProperty: () => {
|
||||||
|
const val = generateValid(source)
|
||||||
|
const keyToDelete = source.selectOne(
|
||||||
|
Object.keys(val).filter((key) => !optionalTypes.has(key))
|
||||||
|
)
|
||||||
|
if (!keyToDelete) {
|
||||||
|
// no non-optional properties, do a invalid property test instead
|
||||||
|
val[keyToDelete] =
|
||||||
|
propertyTypes[source.selectOne(Object.keys(propertyTypes))].generateInvalid(source)
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
delete val[keyToDelete]
|
||||||
|
return val
|
||||||
|
},
|
||||||
|
extraProperty: () => {
|
||||||
|
const val = generateValid(source)
|
||||||
|
val[source.nextPropertyName() + '_'] = source.nextJsonValue()
|
||||||
|
return val
|
||||||
|
},
|
||||||
|
invalidProperty: () => {
|
||||||
|
const val = generateValid(source)
|
||||||
|
const keyToChange = source.selectOne(Object.keys(propertyTypes))
|
||||||
|
val[keyToChange] = propertyTypes[keyToChange].generateInvalid(source)
|
||||||
|
return val
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateDictType(source: RandomSource, depth: number): TestType {
|
||||||
|
const keyType = builtinTypes.string
|
||||||
|
const keySet = ['a', 'b', 'c', 'd', 'e', 'f'] as const
|
||||||
|
const valueType = source.nextTestType(depth + 1)
|
||||||
|
|
||||||
|
const validator = T.dict(keyType.validator, valueType.validator)
|
||||||
|
|
||||||
|
const generateValid = (source: RandomSource) => {
|
||||||
|
const result = {} as any
|
||||||
|
const numItems = source.nextInt(4)
|
||||||
|
for (let i = 0; i < numItems; i++) {
|
||||||
|
result[source.selectOne(keySet)] = valueType.generateValid(source)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
validator,
|
||||||
|
generateValid,
|
||||||
|
generateInvalid: (source) => {
|
||||||
|
const result = generateValid(source)
|
||||||
|
const key = source.selectOne(Object.keys(result)) ?? source.nextPropertyName()
|
||||||
|
result[key] = valueType.generateInvalid(source)
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLiteralType(value: string): TestType {
|
||||||
|
return {
|
||||||
|
validator: T.literal(value),
|
||||||
|
generateValid: () => value,
|
||||||
|
generateInvalid: (source) => source.selectOne(['_invalid_' + value, 2324, {}]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateUnionType(source: RandomSource, depth: number): TestType {
|
||||||
|
const key = source.selectOne(['type', 'name', 'kind'])
|
||||||
|
const numMembers = source.nextIntInRange(1, 4)
|
||||||
|
const members: TestType[] = []
|
||||||
|
const unionMap: Record<string, Validator<any>> = {}
|
||||||
|
for (let i = 0; i < numMembers; i++) {
|
||||||
|
const id = source.nextId()
|
||||||
|
const keyType = createLiteralType(id)
|
||||||
|
const type = generateObjectType(source, { [key]: keyType }, depth + 1)
|
||||||
|
members.push(type)
|
||||||
|
unionMap[id] = type.validator
|
||||||
|
}
|
||||||
|
const validator = T.union(key, unionMap)
|
||||||
|
|
||||||
|
return {
|
||||||
|
validator,
|
||||||
|
generateValid: (source) => {
|
||||||
|
const member = source.selectOne(members)
|
||||||
|
return member.generateValid(source)
|
||||||
|
},
|
||||||
|
generateInvalid(source) {
|
||||||
|
return source.executeOne<any>({
|
||||||
|
otherType: () => source.selectOne(['_invalid_', 2324, {}]),
|
||||||
|
badMember: {
|
||||||
|
weight: 4,
|
||||||
|
do() {
|
||||||
|
const member = source.selectOne(members)
|
||||||
|
return member.generateInvalid(source)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateArrayType(source: RandomSource, depth: number): TestType {
|
||||||
|
const valueType = source.nextTestType(depth + 1)
|
||||||
|
const validator = T.arrayOf(valueType.validator)
|
||||||
|
const generateValid = (source: RandomSource) => {
|
||||||
|
const result = [] as any[]
|
||||||
|
const numItems = source.nextInt(4)
|
||||||
|
for (let i = 0; i < numItems; i++) {
|
||||||
|
result.push(valueType.generateValid(source))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
validator,
|
||||||
|
generateValid,
|
||||||
|
generateInvalid: (source) => {
|
||||||
|
return source.executeOne<any>({
|
||||||
|
otherType: () =>
|
||||||
|
source.executeOne<any>({
|
||||||
|
string: () => source.nextId(),
|
||||||
|
number: () => source.nextInt(100),
|
||||||
|
object: () => ({ key: source.nextId() }),
|
||||||
|
}),
|
||||||
|
invalidItem: () => {
|
||||||
|
const val = generateValid(source)
|
||||||
|
if (val.length === 0) {
|
||||||
|
return [valueType.generateInvalid(source)]
|
||||||
|
}
|
||||||
|
const indexToChange = source.nextInt(val.length)
|
||||||
|
val[indexToChange] = valueType.generateInvalid(source)
|
||||||
|
return val
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runTest(seed: number) {
|
||||||
|
test(`fuzz test with seed ${seed}`, () => {
|
||||||
|
const source = new RandomSource(seed)
|
||||||
|
const type = source.nextTestType(0)
|
||||||
|
const oldValid = type.generateValid(source)
|
||||||
|
const newValid = source.choice(0.5) ? type.generateValid(source) : oldValid
|
||||||
|
const didChange = !isEqual(oldValid, newValid)
|
||||||
|
const invalid = type.generateInvalid(source)
|
||||||
|
|
||||||
|
expect(type.validator.validate(oldValid)).toBe(oldValid)
|
||||||
|
expect(type.validator.validate(newValid)).toBe(newValid)
|
||||||
|
expect(() => {
|
||||||
|
type.validator.validate(invalid)
|
||||||
|
}).toThrow()
|
||||||
|
|
||||||
|
expect(() => type.validator.validateUsingKnownGoodVersion(oldValid, newValid)).not.toThrow()
|
||||||
|
expect(() => type.validator.validateUsingKnownGoodVersion(oldValid, invalid)).toThrow()
|
||||||
|
|
||||||
|
if (didChange) {
|
||||||
|
expect(type.validator.validateUsingKnownGoodVersion(oldValid, newValid)).toBe(newValid)
|
||||||
|
} else {
|
||||||
|
expect(type.validator.validateUsingKnownGoodVersion(oldValid, newValid)).toBe(oldValid)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const NUM_TESTS = 1000
|
||||||
|
const source = new RandomSource(Math.random())
|
||||||
|
|
||||||
|
// 54480484
|
||||||
|
const onlySeed: null | number = null
|
||||||
|
|
||||||
|
if (onlySeed) {
|
||||||
|
runTest(onlySeed)
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < NUM_TESTS; i++) {
|
||||||
|
const seed = source.nextInt(100000000)
|
||||||
|
runTest(seed)
|
||||||
|
}
|
||||||
|
}
|
|
@ -59,7 +59,7 @@ describe('validations', () => {
|
||||||
x: 132,
|
x: 132,
|
||||||
y: NaN,
|
y: NaN,
|
||||||
})
|
})
|
||||||
).toThrowErrorMatchingInlineSnapshot(`"At shape().y: Expected a number, got NaN"`)
|
).toThrowErrorMatchingInlineSnapshot(`"At shape.y: Expected a number, got NaN"`)
|
||||||
|
|
||||||
expect(() =>
|
expect(() =>
|
||||||
T.model(
|
T.model(
|
||||||
|
@ -70,7 +70,7 @@ describe('validations', () => {
|
||||||
})
|
})
|
||||||
).validate({ id: 'abc13', color: 'rubbish' })
|
).validate({ id: 'abc13', color: 'rubbish' })
|
||||||
).toThrowErrorMatchingInlineSnapshot(
|
).toThrowErrorMatchingInlineSnapshot(
|
||||||
`"At shape().color: Expected "red" or "green" or "blue", got rubbish"`
|
`"At shape.color: Expected "red" or "green" or "blue", got rubbish"`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -7559,6 +7559,7 @@ __metadata:
|
||||||
dependencies:
|
dependencies:
|
||||||
"@tldraw/utils": "workspace:*"
|
"@tldraw/utils": "workspace:*"
|
||||||
lazyrepo: "npm:0.0.0-alpha.27"
|
lazyrepo: "npm:0.0.0-alpha.27"
|
||||||
|
lodash.isequal: "npm:^4.5.0"
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue