Skip to content

B3 framework-side: server-side option-value enforcement + live e2e (follow-up to #1583 / #2547) #2559

Description

@os-zhuang

Context

This is the remaining, cross-repo half of B3 (dynamic field options / cascading selects, #1583). The objectui client side is done and merged:

Two acceptance items of #1583 remain, and both are primarily in objectstack-ai/framework (the objectql server + the app-showcase backend). They realize the dual-side guarantee: client hiding of an option is UX only; the server must reject a submitted option value whose visibleWhen is FALSE, or the filter is trivially bypassable.

Note: this issue is filed in objectui because that's where the #1583 tracker lives; the bulk of the work is a framework PR, plus one objectui PR for the live e2e. Assign an agent that has objectstack-ai/framework access.

Scope

1. Server-side option-value enforcement — framework (@objectstack/objectql)

Mirror the existing requiredWhen enforcement documented in objectui ADR-0036 (docs/adr/0036-field-conditional-rules.md), but reject an out-of-domain value rather than strip it:

  • In the objectql rule-validator (the path that already evaluates requiredWhen over the merged record { ...previous, ...patch } and pushes { field, code: 'required' }, alongside stripReadonlyWhenFields): for each field in the write that is an option field (select / radio / multiselect) whose options carry a visibleWhen, find the option(s) matching the submitted value, evaluate each such option's visibleWhen over the merged record + actor (current_user), and on FALSE push a violation (e.g. { field, code: 'invalid_option' }). Multi-select checks every submitted value.
  • Fail-open + log on a broken/absent predicate — never block a legitimate write. Same posture as requiredWhen / readonlyWhen.
  • Extend needsPriorRecord so an option visibleWhen referencing record.* triggers the prior-record fetch on UPDATE (a cascade predicate needs sibling values).
  • Reuse the canonical @objectstack/formula ExpressionEngine — the same dialect the objectui client resolver resolveVisibleOptions uses (packages/core/src/evaluator/optionRules.ts), so client and server verdicts agree.
  • First step is exploration — confirm the actual rule-validator file/paths, the @objectstack/spec SelectOption schema (that visibleWhen is present), and how current_user/actor is bound into server predicate evaluation. The names above are from ADR-0036 and may differ in the current framework tree.

Tests (alongside the existing rule-validator tests): out-of-set value rejected; in-set value accepted; role-gated value rejected for non-admin / accepted for admin; broken predicate fails open; UPDATE fetches the prior record when a cascade option predicate is present.

2. Showcase cascading fixture — framework (examples/app-showcase)

Add to a showcase object (e.g. showcase_field_zoo, or a small new object) a cascading pair and a role-gated option, mirroring the objectui catalog example examples/schema-catalog/src/schemas/fields-select/cascading-options.json:

  • country (cn / us) → province with field-level dependsOn: 'country' and per-option visibleWhen: "record.country == 'cn'" etc.
  • one role-gated option, e.g. visibleWhen: "'admin' in current_user.roles", to exercise the server enforcement from item 1.

3. Live-backend e2e — objectui (e2e/live/)

  • Add e2e/live/cascading-options.spec.ts following e2e/live/field-conditional-rules.spec.ts and the selectOption helper (e2e/live/helpers.ts). Open the showcase object's create form, drive the parent select, assert the child's offered options re-filter and the stale value clears, then assert the server rejects an out-of-set value (submit a bad value via the API and expect a violation).
  • It must auto-skip when the live backend is absent (like the other e2e/live/ specs) so CI stays green.
  • ⚠️ Because B3: cascading-option guardrail, role-gated demo, ADR-0058 + browser e2e (#1583) #2547 is merged, this goes on a fresh branch off latest main (do not reuse the merged branch).

Verification

  • framework: pnpm test for the rule-validator additions.
  • end-to-end (per objectui AGENTS.md "每个 agent 独立测试栈", isolated ports):
    • backend: cd ../framework/examples/app-showcase && pnpm exec objectstack dev --seed-admin --fresh -p 4010
    • console: cd apps/console && DEV_PROXY_TARGET=http://localhost:4010 pnpm exec vite --port 5190 --strictPort
    • live e2e: LIVE_APP_URL=http://localhost:5190 LIVE_API_URL=http://localhost:4010 pnpm test:e2e:live -g cascading
    • server-rejection spot check: curl -X POST an out-of-set option value to http://localhost:4010/api/v1/data/<object> → expect a 4xx / violation.

Deliverables

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions