Repository navigation
fix(runtime): a per-record action called with no recordId is refused 400 naming it, and an unknown id 404s at the door - #29976
Conversation
…400 naming it, and an unknown id 404s at the door An action declared execution: 'perRecord' (api or flow) called with no recordId is refused 400 VALIDATION_ERROR naming it before anything is forwarded or started, through both action doors. The same api action with an id the caller's scope does not resolve answers the door's 404 RECORD_NOT_FOUND instead of being forwarded. Aggregate and undeclared actions run record-less as before (#29173). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzf1rWiBfPzSuFgeCcr
|
domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5 Contract reviewServed-tier: Face: declaration ( ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
|
domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5 席位核对Verdict: ACCEPT at head
|
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 28 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 413c316ccfcc5c8471f07525875e78c6af95d651 && git checkout 413c316ccfcc5c8471f07525875e78c6af95d651
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 18112bfa776b896f705c182f07b8baeff45957a7 e2aec5f47e559fbdcff590526fe1c0414d809f94 && git checkout -B drift-repro 18112bfa776b896f705c182f07b8baeff45957a7 && git merge --no-ff e2aec5f47e559fbdcff590526fe1c0414d809f94
node scripts/docs-audit/affected-docs.mjs --json 18112bfa776b896f705c182f07b8baeff45957a7
|
…, not run record-less Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzf1rWiBfPzSuFgeCcr
|
domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5 席位核对Verdict: ACCEPT at head
|
Requested by Yibo Wang · project thread
Fixes #29881
维护者速读
execution: 'perRecord'的动作(api 型或 flow 型)在两个动作门(REST/actions与 MCPrun_action)缺recordId(或为空串)时,入流程/转发前即答400 VALIDATION_ERROR指名recordId;同一 api 动作带一个调用者解析不到的 id 时,答门自己的404 RECORD_NOT_FOUND,不再转发。showcase_recalc_estimate是type: 'api'指向 flow 端点(卡片以为是 flow 型动作,实为 api 型);缺 id 或 id 不存在时都转发进流程,拿回400 FLOW_FAILED和内部节点名load_task。actions: anapiaction called with no recordId forwards an unresolved{id}path and answers 404 ENDPOINT_NOT_FOUND instead of naming the missing record id #28932 只按target里的{id}令牌判,这里 target 无令牌,所以改读动作自己的execution声明。main上本来就失败(同 fix(runtime): an api action whose target names its record is refused 400 naming recordId when called without one, never forwarded to a path with a hole in it #29005 的判法,Clause-②: no);aggregate与未声明execution的动作照旧无 id 运行(action door: a record-less aggregate api action that declares recordIdParam is refused 400 ("identifies its record by id"), so bulk "Recalculate Selection" never runs #29173),审批动作未声明execution,approvals: a non-admin approver's decision from the request page answers 404 RECORD_NOT_FOUND — the action door loads sys_approval_request in the caller's data scope #28657 的转发不动。@objectstack/runtimepatch。回滚 = revert 本 PR。packages/runtime/src/action-execution.ts的perRecordActionRecordIdRefusal及其两处调用(buildApiActionRequest、dispatchFlowAction),和forwardsOnDeniedSubjectLoad新增的一行。pnpm --filter @objectstack/runtime exec vitest run src/action-flow-per-record-record-id.test.ts src/domains/api-action-forward.test.ts,预期92 passed。Clause-②: no
Metadata-protocol: none
What changes
@objectstack/runtime,action-execution.ts. A newperRecordActionRecordIdRefusal(action, recordId)answers{ status: 400, message }when the action declaresexecution: 'perRecord'and no record id was given (undefined,nullor''). It reads only the declaration and the id. The message has the actions: anapiaction called with no recordId forwards an unresolved{id}path and answers 404 ENDPOINT_NOT_FOUND instead of naming the missing record id #28932 shape, and namesrecordIdand the declaration:buildApiActionRequest, right beside actions: anapiaction called with no recordId forwards an unresolved{id}path and answers 404 ENDPOINT_NOT_FOUND instead of naming the missing record id #28932'starget-token refusal, so nothing is forwarded;dispatchFlowAction, thrown withstatus: 400and the code derived from it (VALIDATION_ERROR), so no run is created and no node's diagnostic reaches the caller.forwardsOnDeniedSubjectLoadreturnsfalsefor an action declaredexecution: 'perRecord'. A per-record api action whose record the caller's scope does not resolve (an unknown id, or a row it cannot read; the two are one answer by design) gets the door's shared404 RECORD_NOT_FOUNDbefore anything runs, as a flow or script action already did. Before, it was forwarded, and the recalc flow answered400 FLOW_FAILEDwrapping "Record … not found".aggregate, or declaring noexecution, run record-less as before (action door: a record-less aggregate api action that declares recordIdParam is refused 400 ("identifies its record by id"), so bulk "Recalculate Selection" never runs #29173).approval_approve(the approvals: a non-admin approver's decision from the request page answers 404 RECORD_NOT_FOUND — the action door loads sys_approval_request in the caller's data scope #28657 shape) declares noexecutionand keeps forwarding on a denied load.action-execution.tsis not re-exported from the package entry, so the new function adds nothing to the public surface.Why
Card #29881:
POST /api/v1/actions/showcase_task/showcase_recalc_estimate {}answered400 FLOW_FAILEDwith "Node 'recalc_each' failed: … Node 'load_task' failed: get_record: refusing to run — 1 filter operand(s) evaluated to nothing:recordId…". With an unknown id it answered the same code wrapping "Record … not found". The caller could not tell that it had left out an argument, and the flow's internal node names reached it.Premise readings (measured at
18112bfa):type: 'api'actions whosetarget(/api/v1/apps/showcase/recalc) is atype: 'flow'endpoint. actions: anapiaction called with no recordId forwards an unresolved{id}path and answers 404 ENDPOINT_NOT_FOUND instead of naming the missing record id #28932's check reads thetargetfor a record token, and this target has none, so the call was forwarded with no id. The per-record signal the platform already has is the action's ownexecutiondeclaration (ActionSchema.execution, An action cannot declare whichbulkActionsdispatch contract its body is written for — bare-string fan-out andexecution: 'aggregate'deliver opposite input shapes through one authoring surface #17319). The fix reads that for the api arm, and also fortype: 'flow'actions, which had the same gap.showcase_recalc_selectionis declaredexecution: 'aggregate'. It is NOT per-record, and action door: a record-less aggregate api action that declares recordIdParam is refused 400 ("identifies its record by id"), so bulk "Recalculate Selection" never runs #29173 deliberately lets a record-less aggregate call through, so it keeps running with no id. It is this PR's "not per-record keeps running" control. What its endpoint makes of an empty selection (the sameFLOW_FAILEDwith node names) belongs to that flow, not the action door, and this PR leaves it as it is.apiaction called with no recordId forwards an unresolved{id}path and answers 404 ENDPOINT_NOT_FOUND instead of naming the missing record id #28932's own arm (approval_approve) does not 404 at the door. It forwards, and the approvals route answers its own404 REQUEST_NOT_FOUND. Matching that here would still hand the caller the flow's node names, so the per-record declaration also opts the action out of the approvals: a non-admin approver's decision from the request page answers 404 RECORD_NOT_FOUND — the action door loads sys_approval_request in the caller's data scope #28657 forward-on-denied-load exception. The door's existing404 RECORD_NOT_FOUNDenvelope is used, and no new code is added.Accept set (
Clause-②: no, the #29005 reading). Every call refused now already failed onmain. With no id, the forward ran a flow that refused at its first read. With an unresolvable id, it ran a flow that could not read the row as the caller (runAs: 'user'). In-repo census ofexecution: 'perRecord': four showcase actions. Two areoperation: 'update'(that arm is untouched). Two are api actions: the recalc one, andshowcase_generate_invoice_pdf→/api/v1/print/records, whose route reads the record as the caller. No plugin package declaresperRecord.Verification
packages/runtime/src/domains/api-action-forward.test.ts, new[#29881]describe, through the realHttpDispatcherREST door andinvokeBusinessAction(MCP):{},{ params: {} }and{ recordId: '' }, the request is refused400 VALIDATION_ERRORnamingrecordId, and the target is never called;404 RECORD_NOT_FOUND, and nothing is forwarded;{ recordId }, and the aggregate twin with no id is forwarded;forwardsOnDeniedSubjectLoadis false for per-record and true for aggregate and undeclared;buildApiActionRequestbuilds no request without an id.packages/runtime/src/action-flow-per-record-record-id.test.ts(new): atype: 'flow'per-record action, on both doors, is refused 400 namingrecordId, andautomation.executeis never called. An unknown id keeps the 404. A real id runs. Aggregate and undeclared flow actions run with no id.packages/qa/dogfood/test/api-action-door-forward.dogfood.test.ts, new[#29881]case on a booted stock showcase, as admin:showcase_recalc_estimatewith{}and with{ recordId: '' }answers 400VALIDATION_ERRORnamingrecordId; the body contains neitherload_tasknorFLOW_FAILED;RECORD_NOT_FOUNDwith noload_task;showcase_recalc_selection {}is not refused at the door.REST: no recordId … answers 400 VALIDATION_ERROR→AssertionError: {}: expected 200 to be 400, and the 404 pin →expected 200 to be 404. Restored: 92 passed.main'saction-execution.ts. The[#29881]case failed with the card's body verbatim:{"code":"FLOW_FAILED","message":"Node 'recalc_each' failed: map 'recalc_each': item 0 (subflow 'showcase_recalc_task_estimate') failed: Node 'load_task' failed: get_record: refusing to run — 1 filter operand(s) evaluated to nothing: \recordId` …"}: expected 'FLOW_FAILED' to be 'VALIDATION_ERROR'`. Rebuilt with the fix: 11 passed.@objectstack/runtime...closure build: exit 0. The@objectstack/dogfood^...closure build: exit 0. Runtimepnpm test: 540 files, 8614 passed / 19 skipped, exit 0.test:repo: 3 files, 1211 passed, exit 0.tsc --noEmit: exit 0.check:test-typecheck: exit 0.pnpm lint: exit 0.node scripts/pm/dispatch-gates.mjs --commandsat the change set vsorigin/main18112bfa: 64 derived commands, all exit 0, with two notes:check-plugin-teardown-shape.mjs --self-testfirst exited 3 because its fixture commit621a4876was not in the shallow clone; after fetching it: 48 cases pass, exit 0.check:dual-build-cjs-loadsfirst exited 3 because 7 package dists were missing; after building them: exit 0 (116 entry points, 71 packages).check:authz-resolver,check:changeset-fixed,check:error-code-casing,check:filter-alias-parity,check:route-ledger-census): all exit 0.dispatch-gates --ran:✓ 64 derived famil(ies) accounted for — 64 run, 0 NOT-MEASURED.63068066. The pushed head3648acbbis the same tree: only the commit message was reworded, for the trailer hook.ui/files.Human Minimum
packages/specholds no runtime logic. (spec untouched)pnpm lintpasses.main, one pull request.packages/spectouched: n/a.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.维护者速读section is the seat's Chinese summary for the maintainer).🤖 Generated with Claude Code
https://claude.ai/code/session_01WGufzf1rWiBfPzSuFgeCcr