feat(metadata): saveMeta persists the operator spellings the spec normalized (objectui#2945) - #4107
Merged
Merged
Conversation
…malized (objectui#2945) ViewFilterRuleSchema.operator is z.preprocess(normalizeFilterOperator, …), so a stored `notEquals`/`gt`/`isNull` is folded to canonical during save-time validation — and the result was then discarded, because saveMetaItem persists the authored body verbatim (deliberately: parsed.data strips the Studio-only aux fields that ride along with an overlay, ADR-0005 §Validation). So every save minted new legacy-alias rows. VIEW_FILTER_OPERATOR_ALIASES documents itself as "a migration bridge [that] may be dropped in a future major", but with new alias rows arriving continuously there is no point at which the last one is behind you — a migration rewriting existing rows would be obsolete at the next personalization PUT. This is prerequisite 2 of the consolidation blocked in objectui#2945. graftNormalizedOperators walks the authored body and parsed.data in lockstep BY STRUCTURE and copies exactly one thing: an `operator` whose parsed value differs. No path list — ViewFilterRule[] has five declared sites today and the walk covers all of them plus any added later; enumerating them would reproduce here the duplication #2945 exists to remove. Guarded on both sides being strings, so a `$`-token FilterCondition cannot be reshaped. Nothing added, removed, reordered or defaulted: the unary {field, operator} form does not acquire a `value` even though the schema's own output would. Returns by identity when nothing changed. Behaviour change, stated plainly: a GET after a PUT now returns the canonical spelling, not the one the author sent. That is the spelling the spec defines and every renderer accepts (objectui#2974, objectui#2989 pinned all three objectui translation tables to the full vocabulary). Existing rows are untouched — this stops the bleeding, it is not the migration. 11 new tests, one driving EVERY alias the spec still folds through the real ViewMetadataSchema; package suite 110 tests / 18 files green. Refs objectstack-ai/objectui#2945, objectstack-ai/objectui#2901 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This was referenced Jul 30, 2026
Closed
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.
Prerequisite 2 of the vocabulary consolidation blocked in objectstack-ai/objectui#2945. Prerequisite 1 (this repo's #3948 item 1 — drivers throw instead of silently skipping) has already landed, so this is what remains before the alias table can be retired at all.
The defect
ViewFilterRuleSchema.operatorisz.preprocess(normalizeFilterOperator, z.enum(VIEW_FILTER_OPERATORS)). A storednotEquals/gt/isNullis therefore folded to its canonical form during save-time validation — and the result is thrown away.saveMetaItempersists the authored body verbatim, and that is deliberate:parsed.datastrips the Studio-only auxiliary fields (isPinned,isDefault,sortOrder) that intentionally ride along with an overlay document (ADR-0005§Validation, and the comment onresolveOverlaySchemasays so).The consequence is not that a legacy spelling survives — it is that every save mints new legacy-alias rows.
VIEW_FILTER_OPERATOR_ALIASESdescribes itself as "a migration bridge [that] may be dropped in a future major". With new alias rows arriving continuously there is no point at which the last one is behind you: a migration that rewrote every existing row would be obsolete the moment the next console personalizationPUTlanded. That is exactly why objectui#2945 lists this as a prerequisite rather than a nice-to-have.The fix
graftNormalizedOperators(authored, parsed.data)grafts the normalization back on without giving up the verbatim body. It walks both trees in lockstep by structure and copies across exactly one thing: anoperatorwhose parsed value differs from the authored one.No key list.
ViewFilterRule[]appears at five declared sites today — viewfilter,ViewTab.filter, pagefilterBy, and twocomponent.zod.tsblock props — and the structural walk covers all of them, plus any added later. Enumerating paths here would have reproduced in this file the exact duplication objectui#2945 exists to remove.Nothing else moves:
operatorstring is rewritten, and only where both sides are strings — so a$-tokenFilterCondition, a different operator vocabulary entirely, cannot be reshaped by accident;{field, operator}form does not acquire avalue, even though the schema's own output gives it one;Behaviour change, stated plainly
A
GETafter aPUTnow returns the canonical spelling rather than the one the author sent. That is the spelling the spec defines; every renderer accepts it (objectstack-ai/objectui#2974 and objectstack-ai/objectui#2989 pinned all three of objectui's operator translation tables to the full vocabulary, in both directions, with parity guards); and it is the point of the change.Existing rows are untouched. This stops the bleeding — it is not the migration. Prerequisite 3 of objectui#2945 (scan production before writing one) still stands, and is still not something CI can do:
sys_saved_report.query_jsonis unvalidated on both write and read, so its contents cannot be enumerated statically.Verification
VIEW_FILTER_OPERATOR_ALIASESstill folds through the realViewMetadataSchema— the schemagetMetadataTypeSchema('view')returns, so the same onesaveMetaItemvalidates against — and asserts the persisted body comes out canonical in each case.$-token object is left alone, a mismatched or longer parsed tree contributes nothing.@objectstack/metadata-protocolsuite: 110 tests across 18 files, green.tsc --noEmit: zero errors inprotocol.tsand the new test (the package has notype-checkscript; the 21 diagnostics a raw invocation reports are pre-existing, all in seed-loader/mutation-listener test files this PR does not touch).Refs objectstack-ai/objectui#2945, objectstack-ai/objectui#2901, #3948
🤖 Generated with Claude Code