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
{{ message }}
Repository navigation
Commit cc92f75
Browse filesBrowse the repository at this point in the historyBrowse files
feat(rest): public form submit answers the thank-you panel's engine-generated 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
feat(rest): the public form submit answer carries the engine-generated fields its thank-you panel names
6
+
7
+
Clause-②: yes (widening)
8
+
Metadata-protocol: none
9
+
10
+
`POST /api/v1/forms/:slug/submit` answers `PublicFormSubmitResponseSchema` (`@objectstack/spec/api`): the created record's `id`, as before, plus `fields` when the form's `thank-you` panel names fields of the record as `{{record.field_name}}`. `fields` holds the stored values of exactly the names `thankYouRecordFieldNames(form.submitBehavior)` returns, so a web-to-case visitor can be shown the case number the engine just generated. A form whose panel names no field still answers `{ id }` alone.
11
+
12
+
Nothing else the insert stored is answered: a `beforeInsert` / `afterInsert` hook, an elevated one included, may have derived a stored value from records the anonymous form grant cannot read. So the route re-checks every named field when it answers, and drops a name unless the object declares it with a type in `RUNTIME_OWNED_FIELD_TYPES` (today `autonumber`) and the row the insert answered carries it as a string or a number. That covers an object retyped after the form was published, which the `thank-you-record-field` authoring rule cannot see. `fields` is left out when no name survives.
13
+
14
+
The route ledger now names `PublicFormSubmitResponseSchema` as this route's `responseSchema`.
Copy file name to clipboardExpand all lines: content/docs/ui/forms.mdx
+11-2Lines changed: 11 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -118,7 +118,8 @@ name reaches one thing only:
118
118
permissions (`allowCreate`, `allowRead`, …) are not read on this path, so they
119
119
neither grant nor limit the insert.
120
120
-**The visitor never sees that record.** The submit answers with the new
121
-
record's `id` only (section 4). Register a `guest_portal` set only if you want
121
+
record's `id`, plus only the engine-generated fields its thank-you panel
122
+
names (section 4). Register a `guest_portal` set only if you want
122
123
fields masked on the record the route gets back; nothing else needs one.
123
124
-**No object hook can read it.** A hook's `ctx.session` and `ctx.user` carry
124
125
neither `permissions` nor `anonymous`, and on a public submission both are
@@ -213,6 +214,14 @@ Response on success (HTTP `201 Created`) — the created record's id, and nothin
213
214
{ "id": "r7p8cUZoBJbFWudt" }
214
215
```
215
216
217
+
The one addition: when the form's `thank-you` panel names fields of the record as `{{record.field_name}}`, the answer also carries `fields`, holding the stored values of exactly those fields. A panel may name only a field the engine generates on insert (an `autonumber` field, such as a case number) — the `thank-you-record-field` rule refuses any other at `os validate` and at publish, and the route re-checks each name's type when it answers, so a field retyped after publish is left out:
Any other value the insert stored never reaches the visitor: a `beforeInsert` / `afterInsert` hook may have derived it from records the anonymous grant cannot read. The body's shape is `PublicFormSubmitResponseSchema` (`@objectstack/spec/api`).
224
+
216
225
The anonymous caller is never shown the stored row. It already knows what it submitted, and everything else on the row — a `defaultValue`, a field a `beforeInsert` / `afterInsert` hook stamped, a value an elevated hook derived from existing records — belongs to readers the form grants nothing. So the whitelist-stripped `status` and the hook defaults in the example above land on the row but are not in the answer. A host that needs the stored record reads it through an authenticated read (`GET /api/v1/data/:object/:id`) with a principal allowed to see it.
Copy file name to clipboardExpand all lines: content/docs/ui/public-data-collection.mdx
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -48,7 +48,7 @@ Only the fields listed in `sections[].fields` are accepted on submit; everything
48
48
49
49
The whitelist cannot hold a file field (`file`, `image`, `avatar`, `video`, `audio`): a visitor has no upload route, so a public form declaring one is refused at `os validate` / `os build` and at publish (`public-form-file-field`). Collect files from registered, signed-in users on an internal form instead.
50
50
51
-
The submit's execution context also names a `guest_portal` permission set. That name authorizes nothing: the grant is decided before any permission set resolves, so the form works the same whether or not a set of that name exists, and such a set's object permissions (`allowCreate`, `allowRead`, …) are not read on this path. Where a deployment registers one, only its field permissions, and the capabilities it holds against a field's `requiredPermissions`, are read, to mask the created record the server hands back to the route. The visitor never sees that record: the submit answers with the new record's `id`only.
51
+
The submit's execution context also names a `guest_portal` permission set. That name authorizes nothing: the grant is decided before any permission set resolves, so the form works the same whether or not a set of that name exists, and such a set's object permissions (`allowCreate`, `allowRead`, …) are not read on this path. Where a deployment registers one, only its field permissions, and the capabilities it holds against a field's `requiredPermissions`, are read, to mask the created record the server hands back to the route. The visitor never sees that record: the submit answers with the new record's `id`, plus only the engine-generated fields (an `autonumber` case number) the form's thank-you panel names.
52
52
53
53
System-managed anchors (`owner_id`, `organization_id`, audit columns, `id`) are **never** accepted from a public submission — not even when a section declares them, and not through the zero-sections all-fields fallback. The route filter and the data layer enforce the same set (`PUBLIC_FORM_SERVER_MANAGED_FIELDS` from `@objectstack/spec/security`), so an anonymous visitor cannot forge record ownership or tenant placement regardless of how the form is declared (#3022).
0 commit comments