diff --git a/.changeset/action-method-example-data-door-path.md b/.changeset/action-method-example-data-door-path.md new file mode 100644 index 0000000000..cdf7fae0ab --- /dev/null +++ b/.changeset/action-method-example-data-door-path.md @@ -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. diff --git a/packages/spec/src/ui/action.zod.ts b/packages/spec/src/ui/action.zod.ts index c21a6e06ab..56dd841865 100644 --- a/packages/spec/src/ui/action.zod.ts +++ b/packages/spec/src/ui/action.zod.ts @@ -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.'), /**