Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .changeset/action-method-example-data-door-path.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
"@objectstack/spec": patch
---

fix(spec): `ActionSchema.method`'s worked example now names the shipped data door

The `method` docblock's only worked example of a `type: 'api'` PATCH pointed at
`/api/v1/sys_api_key/{id}` — a path the router never mounts. The shipped data
door composes as `getApiBasePath()` + `crud.dataPrefix` + `/:object/:id`, so the
update endpoint is `PATCH /api/v1/data/:object/:id` on a default host. The `/data`
segment was missing, and nothing catches the difference at authoring time:
`objectstack validate` does not check `target`, `type: 'api'` has no author-time
route validation, and the action parses green, renders, is clickable, and 404s at
the click — the same silent shape as an unregistered handler, arriving through a
doc example. This mattered more than an ordinary stale comment because it is the
one worked example of that route on the published contract.

The example now reads `/api/v1/data/sys_api_key/${ctx.recordId}`. The object name
is unchanged on purpose — the error was the path STRUCTURE, not which object the
example picks — and the id is spelled with the `${ctx.X}` interpolation that
`target`'s own docblock documents, so the two placeholder conventions in this one
schema stop reading as interchangeable (a bare `{recordId}` is `newTabUrl`'s
convention alone).

Two things the corrected example now says that the old one did not:

- **The full path is host-dependent.** Under `enableProjectScoping` with
`projectResolution: 'required'` only
`/api/v1/environments/:environmentId/data/:object/:id` is registered, so even a
correctly spelled unscoped path still 404s on such a host. An author copying a
full path needs to know which base their host mounts.
- **A single-record field write has a declarative form now.** `operation: 'update'`
with `patch` writes the current record on the data plane as the caller, with no
endpoint, method or id placeholder to spell. The `type: 'api'` + `PATCH` form
remains the way to call an explicit endpoint.

Documentation only: no schema member, no `.describe()` and no runtime behaviour
changes. It is a `patch` rather than a `skip-changeset` because the corrected text
publishes — `@objectstack/spec` ships `dist/**` and `src/**/*.zod.ts`, and both
carry this docblock.
23 changes: 21 additions & 2 deletions packages/spec/src/ui/action.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1397,8 +1397,27 @@ const actionObject = () => strictObject({
]).optional().describe('Body wrapping: flat (default) or { wrap: key } to nest user-collected params under a key.'),
/**
* HTTP method to use when `type: 'api'`. Defaults to `POST`. Use `PATCH` to
* call data-API update endpoints (e.g. `/api/v1/sys_api_key/{id}` with
* `bodyExtra: { revoked: true }`).
* call data-API update endpoints. The shipped data door is
* `PATCH {apiBase}/data/:object/:id` — `getApiBasePath()` + `crud.dataPrefix`,
* i.e. `/api/v1` + `/data` on a default host — so the endpoint for one
* record is e.g. `/api/v1/data/sys_api_key/${ctx.recordId}` with
* `bodyExtra: { revoked: true }`. Two things the path depends on: the
* `/data` segment (omit it and the action renders, is clickable, and 404s
* at the click), and the `${ctx.X}` / `${param.X}` interpolation
* {@link target} documents — a bare `{recordId}` placeholder is
* {@link newTabUrl}'s convention alone and is not substituted here.
*
* The full path is host-dependent: under `enableProjectScoping` with
* `projectResolution: 'required'` only
* `/api/v1/environments/:environmentId/data/:object/:id` is registered, so
* an unscoped path 404s on such a host — write `target` against the base
* the host actually mounts.
*
* To write a field on the CURRENT record, prefer the declarative
* {@link operation} `'update'` with {@link patch}: the platform performs
* the write, so there is no endpoint, method or id placeholder to spell at
* all. The `type: 'api'` + `PATCH` form above remains the way to call an
* explicit endpoint.
*/
method: z.enum(['POST', 'PATCH', 'PUT', 'DELETE']).optional().describe('HTTP method for type:"api" actions. Defaults to POST.'),
/**
Expand Down
Loading