Repository navigation
feat(rest): the public form submit answer carries the engine-generated fields its thank-you panel names - #28479
Conversation
…enerated fields POST /forms/:slug/submit answers PublicFormSubmitResponseSchema: the created id, plus `fields` holding the stored values of exactly the names thankYouRecordFieldNames(form.submitBehavior) returns. Each name is re-checked at answer time against RUNTIME_OWNED_FIELD_TYPES on the object's current declaration and dropped otherwise, and only a string or number the createData row carries is answered. The route ledger names the schema; the docs state the answer. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzzJHmF6J8Rdez8TWu
…table rules Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzzJHmF6J8Rdez8TWu
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzzJHmF6J8Rdez8TWu
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 5 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 19 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 fbaf5a8fd7b5e32027756d2c58cd0c7538ba736b && git checkout fbaf5a8fd7b5e32027756d2c58cd0c7538ba736b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5e706e7a9e938925de326ed52b6d65475f43d095 ab25d2a7f6f7b4cc37745c107e3b76e64ffa5d75 && git checkout -B drift-repro 5e706e7a9e938925de326ed52b6d65475f43d095 && git merge --no-ff ab25d2a7f6f7b4cc37745c107e3b76e64ffa5d75
node scripts/docs-audit/affected-docs.mjs --json 5e706e7a9e938925de326ed52b6d65475f43d095
|
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: |
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract reviewServed-tier: ① Ruling → code
② Declarations
③ Pins and ablations
④ Traps and should-fix
Implemented-by: VERDICT: PASS |
|
domain:server#1 · os-tesla · session_01L9qibz3ZUbZ8Qcn1rDw9yc 维护者速读
席位意见:契约复审 PASS,使用的正是批准的名称。可以直接落地。 席位核对ACCEPT · seat
|
Requested by Tesla · project thread
Fixes #28443. This is the server step of #28005 (ruling B, record 6545210989; names approved in record 6548758413). The spec half landed in #28440; the ui half (#28444) follows this one.
Clause-②: yes (widening)
Metadata-protocol: none
维护者速读
POST /api/v1/forms/:slug/submit的回包,除了新记录的id,现在还会带上fields:表单「感谢页」里用{{record.字段名}}点名的、由引擎在插入时生成的字段值(今天只有自动编号autonumber,比如工单号)。感谢页没点名任何字段的表单,回包仍然只有{ id }。PublicFormSubmitResponseSchema、thankYouRecordFieldNames)已在 feat(spec,lint): a form's thank-you panel names the record's engine-generated fields, and the public submit answer has a declared shape #28440 合入,本 PR 是 server 这一步。{ id }。packages/rest/src/form-routes.ts的thankYouAnswerFields(两条丢弃规则)和提交处理末尾组装answer的几行;packages/rest/src/public-form-submit-answer.test.ts新增的[#28005]一组用例。pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/public-form-submit-answer.test.ts,预期 10 条全绿,其中点名自动编号的表单回包恰为{ id, fields: { case_number } }。What changes
The answer. After the insert, the submit handler reads
thankYouRecordFieldNames(match.form.submitBehavior)(@objectstack/spec/ui), the one reading of which names a thank-you panel holds. When it returns at least one name,thankYouAnswerFieldsbuildsfieldsfrom the rowcreateDataanswered, and the body is typedPublicFormSubmitResponse. A panel with no token, or a form with any othersubmitBehavior, answers{ id }exactly as before.Two drop rules, applied at answer time. A named field reaches the answer only if both hold:
typeis inRUNTIME_OWNED_FIELD_TYPES(@objectstack/spec/data, todayautonumber). A field retyped after publish fails this rule. So does a name that is no declared field: a system column such ascreated_at, a lookup such asowner_id, or a prototype key such asconstructor.null: a field masked by the grant's result masker, one stripped asinternal: true, or one never generated. Anullor any other shape is dropped too.fieldsis left out when no name survives. The row is the onecreateDataalready answered, so this adds no second read. It is stripped ofinternal: truefields and masked for the grant. The object declaration is read at most once per submission: the read therequired/ file / option judgements already make is now shared with the answer through a memo.Route ledger. The
POST /api/v1/forms/:slug/submitrow namesresponseSchema: 'PublicFormSubmitResponseSchema', with its conformance test cited in the note.Comments and docs that stated the answer. These now say
{ id }plus the panel-named engine-generated fields:[#22437]comment inform-routes.tsand the privateregisterFormEndpointsdocblock inrest-server.ts;no-record-echotoprotocol-ingress, because the values come offcreateData's stripped record;content/docs/ui/forms.mdx§4 (the success response and the minimal client) and the masker bullet, andcontent/docs/ui/public-data-collection.mdx.Tests
packages/rest/src/public-form-submit-answer.test.ts, new[#28005]block. Every answer is parsed withPublicFormSubmitResponseSchema(strict):autonumberfield answers{ id, fields: { case_number } }. No other stored value (derived, defaulted, owner, email, timestamps) appears in the serialized body.{ id }only.textafter publish is dropped. Names that are no declared field (created_at), a lookup (owner_id) orconstructorare dropped, and the autonumber beside them is kept.null.PublicFormSubmitResponseSchema, and that name resolves to the very schema the cases parse with.rest-write-response-internal-fields.tripwire.test.ts: the disposition change above.Whole package on
ab25d2a7:pnpm --filter @objectstack/rest exec vitest run:Test Files 363 passed (363),Tests 7166 passed | 352 skipped (7518).pnpm --filter @objectstack/rest typecheck: exit 0 (tsc, plus the test-layer check).packages/clientroute-ledger-response-schema.test.ts+client-url-conformance.test.ts: 10/10.Ablations
Each mutation went through
scripts/ablation-replace.mjsonpackages/rest/src/form-routes.ts. The anchor landed (count 1 → 0), and the file was restored with the blob equal to HEAD (bd027a8f4c2a). The suite imports./rest-serverfromsrc, so no dist was involved.fields(answer.fields = { ...result.record })if (typeof type !== 'string' || !RUNTIME_OWNED_FIELD_TYPES.has(type)) continue;→void type;)if (typeof value !== 'string' && typeof value !== 'number') continue;→void 0;)nullcaseA first version had a third check (the row has the name as an own key). Its ablation stayed green, because the value rule already drops an absent name. It was folded into the value rule (commit
ba54dc43), so each remaining check is one an ablation can red.Live measure (showcase, --fresh)
Premise, on
9b1dc992(main after #28440): an admin authoredprobe_case(acase_numberautonumberCASE-{0000}) and a public form view whose thank-you message names{{record.case_number}}. The anonymous submit answered{"id":"AVTOnpdO_UpgsK3G"}with HTTP 201, while the stored row read back by the admin carried"case_number":"CASE-0001". The showcasecontact-usform answered{"id":…}too.After, on
ab25d2a7(same recipe, fresh boot):{"id":"2nE-CA41J2muTSu-","fields":{"case_number":"CASE-0001"}}· 201contact-us(no token) →{"id":"65dfSz6YqZFzzpvC"}· 201case_numberretyped totext(PUT /meta/object/probe_case?force=true), then submitted with"case_number":"FORGED"in the body →{"id":"H1HHBhQH941n-pjM"}· 201 (the name was dropped)Acceptance notes
fieldsis omitted, not{}, when every named field is dropped. The card saysfieldsappears "only when it returns at least one". This PR reads that as at least one surviving value, so the body never carries an empty map. A renderer readingfields?.[name]sees the same thing either way.autonumbertotextis refused with409 DESTRUCTIVE_CHANGEunless forced. After a forced retype, the view that names the field is not re-judged bythank-you-record-field, because that rule runs on avieworpagewrite. The answer-time type rule is what holds that case, as the card intended.packages/verify/src/handle.ts(run) still says the public form route "answers{ id }". It is true for every form whose panel names no field.content/docs/releases/v18/18-0.mdxsays the same thing and is release-owned.claude/project-thread-c1orucis the card thread's platform-assigned branch; claim amendment 6549411063 names it.Verification
Local, on
ab25d2a7:node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 91 families. Each was run with stdin from/dev/null, and its exit was captured by redirect before any pipe. All 91 exit 0. Four first exited non-zero on build state, not on the diff.check:plugin-teardown-shape --self-test(exit 3, a shallow clone) was re-run after fetching its pinned fixture commit.check:skill-examples(exit 3, no declarations) was re-run after building@objectstack/client{,-react}.check:dts-closureandcheck:dual-build-cjs-loads(exit 1, packages built withOS_SKIP_DTS=1) were re-run after a forced full declarations build of every non-ui package.--ran:91 derived famil(ies) accounted for — 91 run, 0 NOT-MEASURED, exit 0. Scoped lint, the repo's eslint with--no-inline-configoverpackages/rest/src: exit 0. Run record:server-seat-1/r2026-10-31/out/28443-gates-ran-ab25d2a7.list.Generated by Claude Code