You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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/formulaExpressionEngine — 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/specSelectOption 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.
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.
Context
This is the remaining, cross-repo half of B3 (dynamic field options / cascading selects, #1583). The objectui client side is done and merged:
visibleWhen+dependsOn, gating, cascade-clear): 表单支持级联/依赖选项:新增 SelectOption.visibleWhen 通用谓词 #2284 / Cascading lookup (dependsOn) broken in forms: stays gated after parent is chosen; table picker bypasses the dependent filter #2215 (v12.0.0).lintOptionPredicates), role-gated demo, ADR-0058, and a real-browser cascade e2e: B3: cascading-option guardrail, role-gated demo, ADR-0058 + browser e2e (#1583) #2547 (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 whosevisibleWhenis FALSE, or the filter is trivially bypassable.Scope
1. Server-side option-value enforcement — framework (
@objectstack/objectql)Mirror the existing
requiredWhenenforcement documented in objectui ADR-0036 (docs/adr/0036-field-conditional-rules.md), but reject an out-of-domain value rather than strip it:requiredWhenover the merged record{ ...previous, ...patch }and pushes{ field, code: 'required' }, alongsidestripReadonlyWhenFields): for each field in the write that is an option field (select/radio/multiselect) whose options carry avisibleWhen, find the option(s) matching the submitted value, evaluate each such option'svisibleWhenover the merged record + actor (current_user), and on FALSE push a violation (e.g.{ field, code: 'invalid_option' }). Multi-select checks every submitted value.requiredWhen/readonlyWhen.needsPriorRecordso an optionvisibleWhenreferencingrecord.*triggers the prior-record fetch on UPDATE (a cascade predicate needs sibling values).@objectstack/formulaExpressionEngine— the same dialect the objectui client resolverresolveVisibleOptionsuses (packages/core/src/evaluator/optionRules.ts), so client and server verdicts agree.@objectstack/specSelectOptionschema (thatvisibleWhenis present), and howcurrent_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 exampleexamples/schema-catalog/src/schemas/fields-select/cascading-options.json:country(cn/us) →provincewith field-leveldependsOn: 'country'and per-optionvisibleWhen: "record.country == 'cn'"etc.visibleWhen: "'admin' in current_user.roles", to exercise the server enforcement from item 1.3. Live-backend e2e — objectui (
e2e/live/)e2e/live/cascading-options.spec.tsfollowinge2e/live/field-conditional-rules.spec.tsand theselectOptionhelper (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).e2e/live/specs) so CI stays green.main(do not reuse the merged branch).Verification
pnpm testfor the rule-validator additions.cd ../framework/examples/app-showcase && pnpm exec objectstack dev --seed-admin --fresh -p 4010cd apps/console && DEV_PROXY_TARGET=http://localhost:4010 pnpm exec vite --port 5190 --strictPortLIVE_APP_URL=http://localhost:5190 LIVE_API_URL=http://localhost:4010 pnpm test:e2e:live -g cascadingcurl -X POSTan out-of-set option value tohttp://localhost:4010/api/v1/data/<object>→ expect a 4xx / violation.Deliverables
References
docs/adr/0036-field-conditional-rules.md(the server-contract pattern to mirror)docs/adr/0058-cascading-select-options.md(B3 decision + server-enforcement contract)packages/core/src/evaluator/optionRules.ts/optionLint.ts(client resolver + guardrail — the dialect the server must match)