fix: validate inserts against the schema at the path#2611
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: 38df72b The changes in this PR will be included in the next version bump. This PR includes changesets to release 16 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Contributor
📦 Bundle Stats —
|
| Metric | Value | vs main (7d499a0) |
|---|---|---|
| Internal (raw) | 720.5 KB | -2.5 KB, -0.3% |
| Internal (gzip) | 138.3 KB | -404 B, -0.3% |
| Bundled (raw) | 1.32 MB | -1016 B, -0.1% |
| Bundled (gzip) | 298.7 KB | -172 B, -0.1% |
| Import time | 94ms | +1ms, +0.8% |
@portabletext/editor/behaviors
| Metric | Value | vs main (7d499a0) |
|---|---|---|
| Internal (raw) | 467 B | - |
| Internal (gzip) | 207 B | - |
| Bundled (raw) | 424 B | - |
| Bundled (gzip) | 171 B | - |
| Import time | 2ms | -0ms, -1.2% |
@portabletext/editor/plugins
| Metric | Value | vs main (7d499a0) |
|---|---|---|
| Internal (raw) | 3.1 KB | - |
| Internal (gzip) | 967 B | - |
| Bundled (raw) | 2.9 KB | - |
| Bundled (gzip) | 899 B | - |
| Import time | 8ms | -0ms, -1.5% |
@portabletext/editor/selectors
| Metric | Value | vs main (7d499a0) |
|---|---|---|
| Internal (raw) | 70.1 KB | -3.0 KB, -4.1% |
| Internal (gzip) | 12.8 KB | -556 B, -4.1% |
| Bundled (raw) | 66.9 KB | -1.4 KB, -2.0% |
| Bundled (gzip) | 12.0 KB | -207 B, -1.7% |
| Import time | 7ms | -0ms, -0.3% |
@portabletext/editor/utils
| Metric | Value | vs main (7d499a0) |
|---|---|---|
| Internal (raw) | 27.5 KB | -1.9 KB, -6.4% |
| Internal (gzip) | 5.7 KB | -379 B, -6.1% |
| Bundled (raw) | 25.6 KB | -435 B, -1.6% |
| Bundled (gzip) | 5.4 KB | -56 B, -1.0% |
| Import time | 6ms | -0ms, -1.8% |
🗺️ . · ./behaviors · ./plugins · ./selectors · ./utils · Artifacts
Details
- Import time regressions over 10% are flagged with
⚠️ - Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.
80a7d2c to
fccf02e
Compare
fccf02e to
365e64a
Compare
365e64a to
f67af12
Compare
9bfc7fd to
3a8e6ad
Compare
3a8e6ad to
eeec4a0
Compare
28eae1a to
05919c9
Compare
05919c9 to
a865144
Compare
a865144 to
9408e35
Compare
…ainer's `of` declaration
Containers declare which types are allowed inside them via the `of` array on a child field. `getSubSchema(schema, of)` returns the resolved `Schema` view that applies inside such a container, so operations and validators that ask "what's allowed at this position?" can treat the result like any other top-level `Schema`.
The `{type: 'block'}` entry (if present) supplies the resolved styles, decorators, annotations, lists, and inlineObjects. Non-block `of` members become the schema's block objects.
9408e35 to
955f81f
Compare
955f81f to
2635bd8
Compare
The editor enforces the schema as a contract for new data crossing into documents. `insert.block` and `insert.child` now validate against the schema that applies at the target path. Operations introducing types not declared in that schema have no effect. `child.set` with an unknown type also no longer logs an error; it noops. Previously, validation was strict only inside containers and permissive at the root - unknown decorators, annotations, and inline objects at the root were silently accepted. That asymmetry was a mistake. The schema is the contract for what's allowed where, applied uniformly to every write boundary regardless of depth. Existing document data is never modified by this enforcement. A document with content authored under an older or different schema continues to load and render unchanged. Schema validation is for new data crossing into the editor, not for cleaning up data already there.
2635bd8 to
38df72b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pasting a stock ticker into a callout where the callout's schema doesn't allow it, the editor today inserts it anyway. The toolbar then opens the inline-object popover, the user edits the symbol, hits save, and the document silently doesn't update. Down the chain
child.setthrows because the type isn't in the path's schema; the throw gets caught and logged, the user sees nothing.The cause is a render-vs-insert-vs-set asymmetry. Render is permissive (
defineLeafmatches by global_typechain). Insert is permissive (parsers run against the root schema). Set is strict (validates against the schema at the path). Phantom data slips in through insert, then becomes uneditable.This makes inserts validate against the schema at the path like sets already do. The schema is the contract for what types are allowed where, applied uniformly to all write boundaries:
insert.blockderives the schema at the destination and parses the block against it. If the block's type isn't declared there, the operation noops.insert.childderives the schema at the focus block and parses the child against it. If neither span nor inline-object parsers find a match, the operation noops.child.setwas already path-scoped but threw when the type wasn't in the schema; it now noops to match insert ops.Render stays permissive: a document authored under a looser schema continues to render in full. Existing data is never modified by this validation; only new data crossing into the editor is checked.
Root-level was a mistake
Validation was previously strict only inside containers and permissive at the root - unknown decorators, annotations, and inline objects at the root were silently accepted, tracked optimistically. That asymmetry was a mistake. The schema is the contract everywhere, applied uniformly to every write boundary regardless of depth.
Mechanism
The editor walks ancestors via
getEnclosingContainerand returns theSchemathat applies at a given path: a registered container's resolved sub-schema when one is found, root schema otherwise. The same walker (getPathSubSchema) is used by every consumer that asks "what's allowed here?" - operations, normalization, selectors. There is one concept (Schema) and one walker.The parsers (
parseBlock,parseInlineObject,parseSpan,parseChild,parseMarkDefs,parseBlocks) takeschema: Schemaas a required parameter. Every caller is explicit about which schema it parses against:getPathSubSchema(snapshot, path)derived from the operation's target path.context.schemadirectly because they have no path context - they're parsing arbitrary external data, the document's top schema is what they're parsing against.Deriving the schema at the destination in
insert.blockinsert.blockvalidates against the schema where the new block will land. The destination depends on placement and what the caller passed forat:atis a range andplacement: 'before'- the new block lands at the start of the range. UserangeStart(at).path.atis a range and any other placement - the new block lands at or near the end of the range. UserangeEnd(at).path.atis undefined and the editor has content - fall back to the editor's end. UseeditorEnd(editor).path.The path is fed through
getPathSubSchemawhich walks up to the enclosing container. SincegetEnclosingContainerskips text blocks (they're not registered containers), passing a leaf path is fine: the walk goes leaf → text block → container or root, and lands on the container's resolved sub-schema when there is one.For collapsed selections (the common case)
rangeStartandrangeEndare the same point so the start/end branch doesn't matter. The branch only matters for expanded selections that cross containers.getSubSchemain@portabletext/schemaThe schema package gains
getSubSchema(schema, of)- a public function that derives the resolved sub-schema for a container'sofdeclaration. The{type: 'block'}entry (if present) supplies the resolved styles, decorators, annotations, lists, and inlineObjects; non-blockofmembers become the schema's block objects.This was previously editor-internal but it's pure schema math with no editor concerns. Anything that reads a Sanity schema and asks "what's allowed inside this nested container?" can use it directly.
The editor's
getPathSubSchema(snapshot, path)composes this withgetEnclosingContainerto answer the path-relative question.