Skip to content

fix(spec): enforce ViewFilterRule operator enum with legacy-alias normalization - #3373

Merged
os-zhuang merged 3 commits into
mainfrom
claude/viewfilter-operator-enum
Jul 21, 2026
Merged

fix(spec): enforce ViewFilterRule operator enum with legacy-alias normalization#3373
os-zhuang merged 3 commits into
mainfrom
claude/viewfilter-operator-enum

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes#3368.

Summary

ViewFilterRuleSchema.operator was a free-form z.string(), so JSON-Schema consumers (notably ObjectUI's SchemaForm) could not render an operator dropdown and invalid operators passed validation silently. This replaces it with a canonical enum + on-parse alias normalization, chosen over a bare strict enum so already-stored view metadata does not break.

  • operatorz.preprocess(normalizeFilterOperator, z.enum(VIEW_FILTER_OPERATORS)).
    • The z.enum surfaces the canonical operator list to z.toJSONSchema in io: 'input' mode (verified), so SchemaForm auto-renders a clean dropdown.
    • The z.preprocess folds legacy shorthand/camelCase spellings (eq, gt, gte, isNull, notEquals, nin, …) to canonical on parse, so consumers see exactly one vocabulary and stored data keeps validating.
    • Genuinely unknown operators are rejected.
  • Exports the single source of truth: VIEW_FILTER_OPERATORS, ViewFilterOperator, VIEW_FILTER_OPERATOR_ALIASES, and normalizeFilterOperator().
  • Pins widget: 'filter-builder' on the list view filter field in view.form.ts — matching the existing dataset.form.ts / page.form.ts and the schema's own JSDoc example — so the Studio editor renders the field-type-aware visual builder instead of raw repeater rows (contract-first: the fix lives in the producer/metadata, not the renderer).
  • Canonicalizes plugin-sharingsys_share_link filters (isNullis_null, isNotNullis_not_null), preserving null semantics (not the is_empty rewrite).
  • Keeps the legacy string sort form (deprecation note only) and retains the objectui#2601 regression fixture — the sort removal is intentionally left to its own deprecation cycle.

Decision: relative-date operators

this_quarter et al. are not filter-rule operators — they are date-range presets (DashboardFilterSchema.defaultRange) and were unimplemented at the filter-rule runtime level. They are dropped from the operator vocabulary; JSDoc and fixtures updated accordingly.

Why not the previous attempt (#3363)

That PR used a strict enum with no aliases, which breaks stored data and ObjectUI's own FilterBuilderWidget writes (gt/gte), bundled the unrelated breaking sort removal, deleted the objectui#2601 regression fixture, and rewrote isNullis_empty (changing semantics). Its "all tests pass" came from editing the tests themselves.

Test plan

  • pnpm --filter @objectstack/spec test — 256 files, 6809 tests pass.
  • pnpm --filter @objectstack/spec exec tsc --noEmit — clean.
  • Added coverage: alias normalization (gtegreater_than_or_equal, eqequals, isNullis_null, ninnot_in, …) and unknown-operator rejection.

Paired with objectstack-ai/objectui#2764.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PwbhoMeqh33xFWq5M77QTJ


Generated by Claude Code

…malization
Replace the free-form `operator: z.string()` on ViewFilterRuleSchema with a
canonical `z.enum` (VIEW_FILTER_OPERATORS) wrapped in `z.preprocess` that folds
legacy shorthand/camelCase spellings (eq, gt, isNull, notEquals, …) to canonical
on parse. This surfaces a clean operator enum to JSON-Schema consumers (ObjectUI
SchemaForm renders a dropdown) and rejects genuinely unknown operators, while
already-stored metadata keeps validating and upgrades to one vocabulary.
- Export VIEW_FILTER_OPERATORS, ViewFilterOperator, VIEW_FILTER_OPERATOR_ALIASES
and a normalizeFilterOperator() helper as the single source of truth.
- Pin `widget: 'filter-builder'` on the list view `filter` field in view.form.ts,
matching dataset/page forms so the visual builder renders instead of a repeater.
- Canonicalize plugin-sharing sys_share_link filters (isNull -> is_null,
isNotNull -> is_not_null), preserving null semantics.
- Keep the legacy string `sort` form (deprecation note only); retain the
objectui#2601 regression fixture.
- Relative-date operators (this_quarter, …) are not filter-rule operators; tests
updated to canonical operators and alias-normalization / unknown-rejection
coverage added.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwbhoMeqh33xFWq5M77QTJ
@vercel

vercelBot commented Jul 20, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 20, 2026 6:33pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-sharing, @objectstack/spec.

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

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via packages/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via packages/spec)
  • content/docs/concepts/north-star.mdx(via packages/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/cli.mdx(via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via packages/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via packages/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/spec)
  • content/docs/permissions/authorization.mdx(via packages/plugins/plugin-sharing, @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via packages/plugins/plugin-sharing, @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/rls.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/plugin-sharing, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/plugins/plugin-sharing, packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx(via @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v16.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/spec)
  • content/docs/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)
  • content/docs/ui/translations.mdx(via @objectstack/spec)
  • content/docs/ui/views.mdx(via @objectstack/spec)

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.

Auto-generated reference docs reflect the ViewFilterRule operator enum
(replacing the free-form `string`). Regenerated via gen:schema && gen:docs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwbhoMeqh33xFWq5M77QTJ
@github-actionsgithub-actionsBot added the documentation Improvements or additions to documentation label Jul 20, 2026
Additive-only: VIEW_FILTER_OPERATORS, VIEW_FILTER_OPERATOR_ALIASES,
ViewFilterOperator, normalizeFilterOperator (0 breaking, 4 added).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwbhoMeqh33xFWq5M77QTJ
@os-zhuang
os-zhuang marked this pull request as ready for review July 21, 2026 00:56
@os-zhuang
os-zhuang merged commit d419826 into mainJul 21, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/viewfilter-operator-enum branch July 21, 2026 00:56
os-zhuang added a commit that referenced this pull request Jul 21, 2026
…ty dir (#3376)
The `Check Changeset` gate counted every `.md` in `.changeset/`
(`find | wc -l`), so it only asserted the directory was non-empty — never
that THIS PR contributed a changeset. In pre-release (RC) mode `changeset
version` retains consumed changeset files, so the directory is permanently
non-empty and the gate can never go red: #3373 merged a real spec /
api-surface fix with no changeset while the step reported "Found 104
changeset(s)".
Diff the base commit instead and count only changesets the PR adds. An
empty-frontmatter changeset still counts — it is the sanctioned "releases
nothing" declaration. Proven to go RED on #3373's diff (0 added) and GREEN
on #3360's (1 added).
Also backfills the missing #3373 changeset so the ViewFilterRule operator
enum fix actually ships in the next release.
Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@github-actionsgithub-actionsBot mentioned this pull request Jul 21, 2026
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 4, 2026
…stack-ai#4894) (objectstack-ai#4896)
Both red checks on objectstack-ai#4422 (`chore: version packages (rc)`) are defects in the
gates themselves, and both recur on every Changesets release PR.
Check Changeset was structurally unsatisfiable for the release PR. The gate
counts changesets a PR *adds* (`git diff --diff-filter=A` vs the base) — right
for an ordinary PR, and the fixobjectstack-ai#3373 landed after a global `find | wc -l`
proved unable to ever go red in RC mode. But the release PR is the *consuming*
side: it applies pending changesets into versions and CHANGELOGs and adds none,
by construction. Nobody labels a bot-authored PR `skip-changeset`, so the
release sat blocked on a check that could only be red. `changeset-release/main`
is now exempt at the job level, pinned to the bot author as well as the branch
name so a hand-pushed branch of that name cannot borrow the exemption.
Scaffold E2E skewed the protocol major against itself during an RC window. The
install step already falls back to `latest` when the repo's version is not yet
published (`@objectstack/cli@^17.0.0-rc.2` -> ETARGET -> retry as `latest`).
That fallback rewrote the generated project's dependencies but not its
manifest, and the template stamps the repo's protocol major
(`engines: { protocol: '^17' }`, written at version time by
sync-template-versions.mjs) while `latest` still pointed at 16.x. The ADR-0087
D1 handshake then correctly refused to boot the artifact — the gate working, on
a skew the step had introduced:
package 'e2e-app' targets protocol ^17 (engines.protocol) but this runtime is
protocol 16.0.0
The fallback now re-stamps `engines.protocol` to the major actually installed,
read off node_modules/@objectstack/spec (PROTOCOL_VERSION is kept in lockstep
with that package's own major, asserted by protocol-version.test.ts), and logs
a `::notice` so the run's true protocol is visible rather than silently
rewritten. Confined to the fallback branch: on the normal path the project
installs the repo's own version, the majors agree by construction, and a
template stamping the wrong major must still fail — which is what
template-consistency.test.ts is for. Re-stamping runs before `npm run build`,
so the artifact and the Docker image (already pinned to the resolved CLI
version by the same reasoning) stay in step.
CI configuration only; releases nothing.
Claude-Session: https://claude.ai/code/session_01BbNVKv6KgPzuQ5p76nMgnf
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationprotocol:uisize/mtests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec(view): canonicalize ViewFilterRule operator vocabulary (enum + legacy aliases) and deprecate legacy string sort

2 participants

@os-zhuang@claude