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-sharing sys_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

vercel Bot commented Jul 20, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 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-actions github-actions Bot 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 main Jul 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>
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 fix objectstack-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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/m tests

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