Skip to content

feat(metadata): saveMeta persists the operator spellings the spec normalized (objectui#2945) - #4107

Merged
os-zhuang merged 1 commit into
mainfrom
claude/savemeta-normalize-filter-2945
Jul 30, 2026
Merged

feat(metadata): saveMeta persists the operator spellings the spec normalized (objectui#2945)#4107
os-zhuang merged 1 commit into
mainfrom
claude/savemeta-normalize-filter-2945

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

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.operator is z.preprocess(normalizeFilterOperator, z.enum(VIEW_FILTER_OPERATORS)). A stored notEquals / gt / isNull is therefore folded to its canonical form during save-time validation — and the result is thrown away.

saveMetaItem persists the authored body verbatim, and that is deliberate: parsed.data strips the Studio-only auxiliary fields (isPinned, isDefault, sortOrder) that intentionally ride along with an overlay document (ADR-0005§Validation, and the comment on resolveOverlaySchema says so).

The consequence is not that a legacy spelling survives — it is that every save mints new legacy-alias rows.

VIEW_FILTER_OPERATOR_ALIASES describes 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 personalization PUT landed. 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: an operator whose parsed value differs from the authored one.

No key list.ViewFilterRule[] appears at five declared sites today — view filter, ViewTab.filter, page filterBy, and two component.zod.ts block 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:

  • only an operatorstring is rewritten, and only where both sides are strings — so a $-token FilterCondition, a different operator vocabulary entirely, cannot be reshaped by accident;
  • no key is added, removed, reordered or defaulted. The unary {field, operator} form does not acquire a value, even though the schema's own output gives it one;
  • a body already canonical is returned by identity, so the common case allocates nothing.

Behaviour change, stated plainly

A GET after a PUT now 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_json is unvalidated on both write and read, so its contents cannot be enumerated statically.

Verification

  • 11 new tests. One drives every alias VIEW_FILTER_OPERATOR_ALIASES still folds through the real ViewMetadataSchema — the schema getMetadataTypeSchema('view') returns, so the same one saveMetaItem validates against — and asserts the persisted body comes out canonical in each case.
  • The negative half is pinned just as hard: aux fields survive, key sets are unchanged, filter order is preserved, a $-token object is left alone, a mismatched or longer parsed tree contributes nothing.
  • Full @objectstack/metadata-protocol suite: 110 tests across 18 files, green.
  • tsc --noEmit: zero errors in protocol.ts and the new test (the package has no type-check script; 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

…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>
@vercel

vercelBot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredJul 30, 2026 9:59am

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol.

1 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/metadata-protocol)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-zhuang