diff --git a/.changeset/19365-automation-runs-cursor-hasmore.md b/.changeset/19365-automation-runs-cursor-hasmore.md index 232d8315a3..2d6e29a992 100644 --- a/.changeset/19365-automation-runs-cursor-hasmore.md +++ b/.changeset/19365-automation-runs-cursor-hasmore.md @@ -29,7 +29,7 @@ FROM ListRunsRequestSchema.parse({ name: 'f', cursor: 'n_007' }) -> { name: 'f', limit: 20, cursor: 'n_007' } // forwarded, then dropped TO ListRunsRequestSchema.parse({ name: 'f', cursor: 'n_007' }) - -> throws: '`cursor` was removed from GET /api/automation/:name/runs in + -> throws: '`cursor` was removed from GET /api/v1/automation/:name/runs in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) …' ``` diff --git a/.changeset/20034-automation-contract-api-v1-paths.md b/.changeset/20034-automation-contract-api-v1-paths.md new file mode 100644 index 0000000000..148fb5beac --- /dev/null +++ b/.changeset/20034-automation-contract-api-v1-paths.md @@ -0,0 +1,28 @@ +--- +'@objectstack/spec': minor +--- + +`AutomationApiContracts` now names the paths the platform actually serves — `/api/v1/automation…` instead of `/api/automation…` (#20034). + +The dispatcher mounts the automation door at its `prefix` plus `/automation`, the prefix defaults to `/api/v1`, and `objectstack serve` passes none. So all nine declared paths answered `404 ENDPOINT_NOT_FOUND` on the default composition while the same requests under `/api/v1/automation` answered `200`, and the generated API reference printed the nine unserved paths as the endpoints. Every other `*ApiContracts` map in `@objectstack/spec/api` already carried `/api/v1`; this one was the only outlier. The runtime is unchanged — only the declaration moves. + +Clause-②: no + +**What moved on the published surface** + +| entry | from | to | +| --- | --- | --- | +| `listFlows` (`GET`), `createFlow` (`POST`) | `/api/automation` | `/api/v1/automation` | +| `getFlow` (`GET`), `updateFlow` (`PUT`), `deleteFlow` (`DELETE`) | `/api/automation/:name` | `/api/v1/automation/:name` | +| `triggerFlow` (`POST`) | `/api/automation/:name/trigger` | `/api/v1/automation/:name/trigger` | +| `toggleFlow` (`POST`) | `/api/automation/:name/toggle` | `/api/v1/automation/:name/toggle` | +| `listRuns` (`GET`) | `/api/automation/:name/runs` | `/api/v1/automation/:name/runs` | +| `getRun` (`GET`) | `/api/automation/:name/runs/:runId` | `/api/v1/automation/:name/runs/:runId` | + +The module's `Base path` and endpoint list move with them, and so does the text `ListRunsRequestSchema` raises for a retired `cursor`: it now names `GET /api/v1/automation/:name/runs`. + +**Who notices.** A caller that built request URLs from these constants was calling paths nothing served on the default composition; it now reaches the serving door with no code change. A caller that hard-coded one of the old strings should send the `/api/v1/automation…` form. The `path` type is unchanged (`string`), no accepted input narrows, and no method changes. + +A host that mounts the dispatcher under a different prefix — `@objectstack/hono`'s `createHonoApp`, whose `prefix` defaults to `/api`, is the in-repo example — serves every contract family under that prefix, so it replaces the leading `/api/v1` of any `*ApiContracts` path, now including these nine. The environment-scoped mount (`/api/v1/environments/:environmentId/automation…`, the only one served under `projectResolution: 'required'`) is not declared here, as it is not in any other contract map. + +**Kept from drifting again.** A new test in `@objectstack/runtime` boots the dispatcher plugin with its default prefix and requires every contract route to be one it mounts, and to be a row of the runtime route ledger under the `/api/v1` wire prefix that the live-mount parity gate probes. diff --git a/content/docs/references/api/automation-api.mdx b/content/docs/references/api/automation-api.mdx index 0b4f9d4814..8864e444ed 100644 --- a/content/docs/references/api/automation-api.mdx +++ b/content/docs/references/api/automation-api.mdx @@ -10,19 +10,25 @@ Automation API Protocol Defines REST CRUD endpoint schemas for managing automation flows, triggering executions, and querying execution history. -Base path: /api/automation +Base path: /api/v1/automation + +The wire paths the platform serves: the dispatcher mounts this door at its +`prefix` (default `/api/v1`, the one `objectstack serve` uses) plus +`/automation`. A drift pin in `@objectstack/runtime` +(`automation-api-contract-mounts.test.ts`) holds every `path` in +`AutomationApiContracts` to that mount table. **Endpoints** ``` -GET /api/automation — List flows -GET /api/automation/:name — Get flow -POST /api/automation — Create flow -PUT /api/automation/:name — Update flow -DELETE /api/automation/:name — Delete flow -POST /api/automation/:name/trigger — Trigger flow execution -POST /api/automation/:name/toggle — Enable/disable flow -GET /api/automation/:name/runs — List execution runs -GET /api/automation/:name/runs/:runId — Get single execution run +GET /api/v1/automation — List flows +GET /api/v1/automation/:name — Get flow +POST /api/v1/automation — Create flow +PUT /api/v1/automation/:name — Update flow +DELETE /api/v1/automation/:name — Delete flow +POST /api/v1/automation/:name/trigger — Trigger flow execution +POST /api/v1/automation/:name/toggle — Enable/disable flow +GET /api/v1/automation/:name/runs — List execution runs +GET /api/v1/automation/:name/runs/:runId — Get single execution run ``` @@ -524,7 +530,7 @@ const result = AutomationApiErrorCode.parse(data); | **name** | `string` | ✅ | Flow machine name (snake_case) | | **status** | `Enum<'pending' \| 'running' \| 'paused' \| 'completed' \| 'failed' \| 'cancelled' \| 'timed_out' \| 'retrying' \| 'refused'>` | optional | Filter by execution status | | **limit** | `integer` | optional (default: `20`) | Maximum number of runs to return | -| **cursor** | `never` | optional | [REMOVED] `cursor` was removed from GET /api/automation/:name/runs in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it was VALIDATED at the boundary and then read by nothing: the option reached the service and the engine never looked at it, no emit site has ever written the response half `nextCursor`, and the only ordering this door has is a required but non-unique `startedAt` timestamp that nothing ever minted a resume point from — so a caller looping "until the cursor runs out" re-read the first and only window forever, with no error. Delete the key. `limit` is the real window and STAYS: it is read end to end (boundary to service to store) and bounded to 1..100, so ask for a wider window instead of a next page. Read the response `hasMore` to learn whether the window was short — it is now COMPUTED from the engine rather than the constant `false` it used to be. | +| **cursor** | `never` | optional | [REMOVED] `cursor` was removed from GET /api/v1/automation/:name/runs in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — it was VALIDATED at the boundary and then read by nothing: the option reached the service and the engine never looked at it, no emit site has ever written the response half `nextCursor`, and the only ordering this door has is a required but non-unique `startedAt` timestamp that nothing ever minted a resume point from — so a caller looping "until the cursor runs out" re-read the first and only window forever, with no error. Delete the key. `limit` is the real window and STAYS: it is read end to end (boundary to service to store) and bounded to 1..100, so ask for a wider window instead of a next page. Read the response `hasMore` to learn whether the window was short — it is now COMPUTED from the engine rather than the constant `false` it used to be. | --- diff --git a/packages/runtime/src/automation-api-contract-mounts.test.ts b/packages/runtime/src/automation-api-contract-mounts.test.ts new file mode 100644 index 0000000000..14684b7749 --- /dev/null +++ b/packages/runtime/src/automation-api-contract-mounts.test.ts @@ -0,0 +1,114 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * #20034 — `AutomationApiContracts` names the wire paths the dispatcher mounts. + * + * ## The drift this pins + * + * The published contract (`@objectstack/spec/api`) declared its nine flow + * endpoints under `/api/automation`, while the dispatcher mounts the automation + * door at `config.prefix || '/api/v1'` plus `/automation`, and + * `objectstack serve` passes no prefix. Every declared path answered + * `404 ENDPOINT_NOT_FOUND` on the composed runtime, and nothing noticed: the + * spec's own test pinned the nine strings to themselves, so it stayed green + * through exactly that drift. Only a test that reads BOTH sides can see it, and + * only this package can import both: the spec has no dependency on the runtime. + * + * ## The two legs + * + * 1. MOUNT — boot `createDispatcherPlugin` with NO `prefix`, the composition + * `objectstack serve` uses, and require every contract `METHOD path` to be a + * route it registered. The prefix is the plugin's own default, never a + * constant written here, so moving either side alone turns this red. + * 2. LEDGER — require every contract route to be a `route-ledger.ts` row under + * the `/api/v1` prefix the ledger header documents. That row is what the + * live-mount parity gate (#7526, `route-ledger-live-mount-parity`) probes + * through the real router, so registration here is carried on to + * reachability there. + * + * ⚠️ What this does not cover: the environment-scoped mount + * (`${prefix}/environments/:environmentId/automation`, the only one registered + * under `projectResolution: 'required'`). The contract declares the unscoped + * paths only, as every other `*ApiContracts` map does. + */ + +import { describe, it, expect } from 'vitest'; +import { AutomationApiContracts } from '@objectstack/spec/api'; + +import { createDispatcherPlugin } from './dispatcher-plugin.js'; +import { ROUTE_LEDGER } from './route-ledger.js'; + +/** The wire prefix `route-ledger.ts` documents for its non-`absolute` rows. */ +const LEDGER_WIRE_PREFIX = '/api/v1'; + +/** Records `VERB /path` for every registration, in order; mounts nothing. */ +function recordingServer() { + const routes: string[] = []; + const rec = (verb: string) => (path: string, _handler: unknown) => { + routes.push(`${verb} ${path}`); + }; + return { + routes, + server: { + get: rec('GET'), + post: rec('POST'), + put: rec('PUT'), + delete: rec('DELETE'), + patch: rec('PATCH'), + }, + }; +} + +function pluginCtx(server: unknown) { + const kernel = { + getService: () => undefined, + getServiceAsync: async () => undefined, + }; + return { + getKernel: () => kernel, + getService: (name: string) => (name === 'http.server' ? server : undefined), + environmentId: undefined, + logger: { info() {}, warn() {}, error() {}, debug() {} }, + hook: () => {}, + on: () => {}, + } as any; +} + +const CONTRACT_ROUTES = Object.entries(AutomationApiContracts).map( + ([key, contract]) => ({ key, route: `${contract.method} ${contract.path}` }), +); + +describe('#20034 — AutomationApiContracts ↔ what the dispatcher serves', () => { + it('declares routes at all (a vacuous map would pass both legs)', () => { + expect(CONTRACT_ROUTES.length).toBeGreaterThan(0); + }); + + it('every contract route is mounted by the dispatcher at its DEFAULT prefix', async () => { + const { server, routes } = recordingServer(); + // No `prefix`: this is the composition `objectstack serve` builds. + const plugin = createDispatcherPlugin({ securityHeaders: false }); + await plugin.start?.(pluginCtx(server)); + + const unmounted = CONTRACT_ROUTES.filter(({ route }) => !routes.includes(route)); + expect( + unmounted, + 'AutomationApiContracts names routes the dispatcher does not mount at its default prefix; ' + + `mounted automation routes: ${routes.filter((r) => r.includes('/automation')).join(', ')}`, + ).toEqual([]); + }); + + it('every contract route is a route-ledger row under the documented `/api/v1` wire prefix', () => { + const ledgerWire = new Set( + ROUTE_LEDGER.map((row) => { + if (row.absolute) return row.route; + const sp = row.route.indexOf(' '); + return `${row.route.slice(0, sp)} ${LEDGER_WIRE_PREFIX}${row.route.slice(sp + 1)}`; + }), + ); + const unledgered = CONTRACT_ROUTES.filter(({ route }) => !ledgerWire.has(route)); + expect( + unledgered, + 'AutomationApiContracts names routes with no route-ledger.ts row under the `/api/v1` wire prefix', + ).toEqual([]); + }); +}); diff --git a/packages/runtime/src/query-param.ts b/packages/runtime/src/query-param.ts index 3729b56146..2bf9c06932 100644 --- a/packages/runtime/src/query-param.ts +++ b/packages/runtime/src/query-param.ts @@ -176,7 +176,7 @@ export function parseIntegerParam( /** * A CLOSED-SET parameter — a filter whose declared values are an enum on the - * wire (`?status=failed` on `GET /api/automation/:name/runs`, whose + * wire (`?status=failed` on `GET /api/v1/automation/:name/runs`, whose * `ListRunsRequestSchema` bounds it to `ExecutionStatus` itself — the enum * rather than a copy of its members, so the bound is whatever that vocabulary * declares rather than a count fixed on the day this line was written. #7359 diff --git a/packages/services/service-automation/src/run-list-truncation.test.ts b/packages/services/service-automation/src/run-list-truncation.test.ts index 6935b68636..31a237b05c 100644 --- a/packages/services/service-automation/src/run-list-truncation.test.ts +++ b/packages/services/service-automation/src/run-list-truncation.test.ts @@ -3,7 +3,7 @@ /** * #19543 — `AutomationEngine.listRunsPage` and the truncation boundary. * - * `GET /api/automation/:name/runs` used to answer `{ runs, hasMore: false }` + * `GET /api/v1/automation/:name/runs` used to answer `{ runs, hasMore: false }` * with the `false` written as a literal, beside a list the engine had already * cut with `.slice(0, limit)`. A caller asking for one row of a thousand was * handed one row and told that was all of them, with a `200` and nothing in diff --git a/packages/spec/src/api/automation-api.zod.test.ts b/packages/spec/src/api/automation-api.zod.test.ts index 9f1ac48e5e..06ffab6aad 100644 --- a/packages/spec/src/api/automation-api.zod.test.ts +++ b/packages/spec/src/api/automation-api.zod.test.ts @@ -800,15 +800,15 @@ describe('AutomationApiContracts', () => { }); it('should define correct paths', () => { - expect(AutomationApiContracts.listFlows.path).toBe('/api/automation'); - expect(AutomationApiContracts.getFlow.path).toBe('/api/automation/:name'); - expect(AutomationApiContracts.createFlow.path).toBe('/api/automation'); - expect(AutomationApiContracts.updateFlow.path).toBe('/api/automation/:name'); - expect(AutomationApiContracts.deleteFlow.path).toBe('/api/automation/:name'); - expect(AutomationApiContracts.triggerFlow.path).toBe('/api/automation/:name/trigger'); - expect(AutomationApiContracts.toggleFlow.path).toBe('/api/automation/:name/toggle'); - expect(AutomationApiContracts.listRuns.path).toBe('/api/automation/:name/runs'); - expect(AutomationApiContracts.getRun.path).toBe('/api/automation/:name/runs/:runId'); + expect(AutomationApiContracts.listFlows.path).toBe('/api/v1/automation'); + expect(AutomationApiContracts.getFlow.path).toBe('/api/v1/automation/:name'); + expect(AutomationApiContracts.createFlow.path).toBe('/api/v1/automation'); + expect(AutomationApiContracts.updateFlow.path).toBe('/api/v1/automation/:name'); + expect(AutomationApiContracts.deleteFlow.path).toBe('/api/v1/automation/:name'); + expect(AutomationApiContracts.triggerFlow.path).toBe('/api/v1/automation/:name/trigger'); + expect(AutomationApiContracts.toggleFlow.path).toBe('/api/v1/automation/:name/toggle'); + expect(AutomationApiContracts.listRuns.path).toBe('/api/v1/automation/:name/runs'); + expect(AutomationApiContracts.getRun.path).toBe('/api/v1/automation/:name/runs/:runId'); }); it('should have input and output schemas for all endpoints', () => { diff --git a/packages/spec/src/api/automation-api.zod.ts b/packages/spec/src/api/automation-api.zod.ts index 2c987b97c5..829a72b295 100644 --- a/packages/spec/src/api/automation-api.zod.ts +++ b/packages/spec/src/api/automation-api.zod.ts @@ -11,19 +11,25 @@ import { ExecutionLogSchema, ExecutionStatus, FlowRunSummarySchema } from '../au * Defines REST CRUD endpoint schemas for managing automation flows, * triggering executions, and querying execution history. * - * Base path: /api/automation + * Base path: /api/v1/automation + * + * The wire paths the platform serves: the dispatcher mounts this door at its + * `prefix` (default `/api/v1`, the one `objectstack serve` uses) plus + * `/automation`. A drift pin in `@objectstack/runtime` + * (`automation-api-contract-mounts.test.ts`) holds every `path` in + * {@link AutomationApiContracts} to that mount table. * * @example Endpoints * ``` - * GET /api/automation — List flows - * GET /api/automation/:name — Get flow - * POST /api/automation — Create flow - * PUT /api/automation/:name — Update flow - * DELETE /api/automation/:name — Delete flow - * POST /api/automation/:name/trigger — Trigger flow execution - * POST /api/automation/:name/toggle — Enable/disable flow - * GET /api/automation/:name/runs — List execution runs - * GET /api/automation/:name/runs/:runId — Get single execution run + * GET /api/v1/automation — List flows + * GET /api/v1/automation/:name — Get flow + * POST /api/v1/automation — Create flow + * PUT /api/v1/automation/:name — Update flow + * DELETE /api/v1/automation/:name — Delete flow + * POST /api/v1/automation/:name/trigger — Trigger flow execution + * POST /api/v1/automation/:name/toggle — Enable/disable flow + * GET /api/v1/automation/:name/runs — List execution runs + * GET /api/v1/automation/:name/runs/:runId — Get single execution run * ``` */ @@ -50,13 +56,13 @@ export const AutomationRunPathParamsSchema = lazySchema(() => AutomationFlowPath export type AutomationRunPathParams = z.input; // ========================================== -// 2. List Flows (GET /api/automation) +// 2. List Flows (GET /api/v1/automation) // ========================================== /** * Query parameters for listing automation flows. * - * @example GET /api/automation?status=active&limit=20 + * @example GET /api/v1/automation?status=active&limit=20 */ export const ListFlowsRequestSchema = lazySchema(() => z.object({ status: z.enum(['draft', 'active', 'obsolete', 'invalid']).optional() @@ -103,7 +109,7 @@ export type ListFlowsResponse = z.input; export type ListFlowsResponseParsed = z.infer; // ========================================== -// 3. Get Flow (GET /api/automation/:name) +// 3. Get Flow (GET /api/v1/automation/:name) // ========================================== /** @@ -123,13 +129,13 @@ export type GetFlowResponse = z.input; export type GetFlowResponseParsed = z.infer; // ========================================== -// 4. Create Flow (POST /api/automation) +// 4. Create Flow (POST /api/v1/automation) // ========================================== /** * Request body for creating a new flow. * - * @example POST /api/automation + * @example POST /api/v1/automation * { name: 'approval_flow', label: 'Approval Flow', type: 'autolaunched', ... } */ export const CreateFlowRequestSchema = lazySchema(() => FlowSchema); @@ -144,7 +150,7 @@ export type CreateFlowRequestParsed = z.infer; * `FlowSchema.parse` output with schema defaults materialized (`version`, * `status`, `runAs`, per-edge `type`/`isDefault`) and `edge.condition` * strings lowered to their `{dialect, source}` envelopes — the same shape - * `GET /api/automation/:name` answers, never an echo of the request bytes. + * `GET /api/v1/automation/:name` answers, never an echo of the request bytes. */ export const CreateFlowResponseSchema = lazySchema(() => BaseResponseSchema.extend({ data: FlowSchema.describe('The created flow, canonicalized — the parsed shape the engine stored, identical to what a subsequent GET answers'), @@ -154,7 +160,7 @@ export type CreateFlowResponse = z.input; export type CreateFlowResponseParsed = z.infer; // ========================================== -// 5. Update Flow (PUT /api/automation/:name) +// 5. Update Flow (PUT /api/v1/automation/:name) // ========================================== /** @@ -166,7 +172,7 @@ export type CreateFlowResponseParsed = z.infer; * `.partial()` here declared a partial-update capability nothing implements. * A real partial-update capability would be its own feature card. * - * @example PUT /api/automation/approval_flow + * @example PUT /api/v1/automation/approval_flow * { name: 'approval_flow', definition: { name: 'approval_flow', label: 'Approval Flow', type: 'autolaunched', nodes: [...], edges: [...] } } */ export const UpdateFlowRequestSchema = lazySchema(() => AutomationFlowPathParamsSchema.extend({ @@ -191,7 +197,7 @@ export type UpdateFlowResponse = z.input; export type UpdateFlowResponseParsed = z.infer; // ========================================== -// 6. Delete Flow (DELETE /api/automation/:name) +// 6. Delete Flow (DELETE /api/v1/automation/:name) // ========================================== /** @@ -214,13 +220,13 @@ export type DeleteFlowResponse = z.input; export type DeleteFlowResponseParsed = z.infer; // ========================================== -// 7. Trigger Flow (POST /api/automation/:name/trigger) +// 7. Trigger Flow (POST /api/v1/automation/:name/trigger) // ========================================== /** * Request body for triggering a flow execution. * - * @example POST /api/automation/approval_flow/trigger + * @example POST /api/v1/automation/approval_flow/trigger * { record: { id: 'rec-1' }, object: 'account', event: 'on_create' } */ export const TriggerFlowRequestSchema = lazySchema(() => AutomationFlowPathParamsSchema.extend({ @@ -398,14 +404,14 @@ export type TriggerFlowResponse = z.input; export type TriggerFlowResponseParsed = z.infer; // ========================================== -// 7b. Resume failure details (POST /api/automation/:name/runs/:runId/resume, 400 FLOW_FAILED) +// 7b. Resume failure details (POST /api/v1/automation/:name/runs/:runId/resume, 400 FLOW_FAILED) // ========================================== /** * The machine-readable half of a resume failure, as it reaches the caller * (#15221; the #16472 family ruling, maintainer 2026-09-07, option A). * - * `POST /api/automation/:name/runs/:runId/resume` answers a run that consumed + * `POST /api/v1/automation/:name/runs/:runId/resume` answers a run that consumed * its pause and then failed with `400 FLOW_FAILED` (#8684), and the * `error.details` of that answer carried the run's two artefacts only — the * author's `errorMessage` and the per-node `summary`. The engine's own verdict @@ -477,13 +483,13 @@ export type ResumeFailureDetails = z.input; export type ResumeFailureDetailsParsed = z.infer; // ========================================== -// 8. Toggle Flow (POST /api/automation/:name/toggle) +// 8. Toggle Flow (POST /api/v1/automation/:name/toggle) // ========================================== /** * Request body for enabling/disabling a flow. * - * @example POST /api/automation/approval_flow/toggle + * @example POST /api/v1/automation/approval_flow/toggle * { enabled: true } */ export const ToggleFlowRequestSchema = lazySchema(() => AutomationFlowPathParamsSchema.extend({ @@ -505,7 +511,7 @@ export type ToggleFlowResponse = z.input; export type ToggleFlowResponseParsed = z.infer; // ========================================== -// 9. List Runs (GET /api/automation/:name/runs) +// 9. List Runs (GET /api/v1/automation/:name/runs) // ========================================== /** @@ -519,7 +525,7 @@ export type ToggleFlowResponseParsed = z.infer; * at the authoring site) and raises this text at parse time. */ const RUNS_LIST_CURSOR_REMOVED = - '`cursor` was removed from GET /api/automation/:name/runs in @objectstack/spec 17.5.0 ' + '`cursor` was removed from GET /api/v1/automation/:name/runs in @objectstack/spec 17.5.0 ' + '(ADR-0049 enforce-or-remove) — it was VALIDATED at the boundary and then read by nothing: ' + 'the option reached the service and the engine never looked at it, no emit site has ever ' + 'written the response half `nextCursor`, and the only ordering this door has is a required ' @@ -554,7 +560,7 @@ const RUNS_LIST_CURSOR_REMOVED = * unfiltered set and nothing in the status, headers or body distinguishes that * from a request served as asked. * - * @example GET /api/automation/approval_flow/runs?status=completed&limit=10 + * @example GET /api/v1/automation/approval_flow/runs?status=completed&limit=10 */ export const ListRunsRequestSchema = lazySchema(() => AutomationFlowPathParamsSchema.extend({ // [#7359] The canonical `ExecutionStatus`, not a copy of its members. The @@ -605,7 +611,7 @@ export type ListRunsResponse = z.input; export type ListRunsResponseParsed = z.infer; // ========================================== -// 10. Get Run (GET /api/automation/:name/runs/:runId) +// 10. Get Run (GET /api/v1/automation/:name/runs/:runId) // ========================================== /** @@ -655,55 +661,55 @@ export type AutomationApiErrorCode = z.input; export const AutomationApiContracts = { listFlows: { method: 'GET' as const, - path: '/api/automation', + path: '/api/v1/automation', input: ListFlowsRequestSchema, output: ListFlowsResponseSchema, }, getFlow: { method: 'GET' as const, - path: '/api/automation/:name', + path: '/api/v1/automation/:name', input: GetFlowRequestSchema, output: GetFlowResponseSchema, }, createFlow: { method: 'POST' as const, - path: '/api/automation', + path: '/api/v1/automation', input: CreateFlowRequestSchema, output: CreateFlowResponseSchema, }, updateFlow: { method: 'PUT' as const, - path: '/api/automation/:name', + path: '/api/v1/automation/:name', input: UpdateFlowRequestSchema, output: UpdateFlowResponseSchema, }, deleteFlow: { method: 'DELETE' as const, - path: '/api/automation/:name', + path: '/api/v1/automation/:name', input: DeleteFlowRequestSchema, output: DeleteFlowResponseSchema, }, triggerFlow: { method: 'POST' as const, - path: '/api/automation/:name/trigger', + path: '/api/v1/automation/:name/trigger', input: TriggerFlowRequestSchema, output: TriggerFlowResponseSchema, }, toggleFlow: { method: 'POST' as const, - path: '/api/automation/:name/toggle', + path: '/api/v1/automation/:name/toggle', input: ToggleFlowRequestSchema, output: ToggleFlowResponseSchema, }, listRuns: { method: 'GET' as const, - path: '/api/automation/:name/runs', + path: '/api/v1/automation/:name/runs', input: ListRunsRequestSchema, output: ListRunsResponseSchema, }, getRun: { method: 'GET' as const, - path: '/api/automation/:name/runs/:runId', + path: '/api/v1/automation/:name/runs/:runId', input: GetRunRequestSchema, output: GetRunResponseSchema, }, diff --git a/packages/spec/src/migrations/entries/semantic/18.automation-runs-cursor-retired.ts b/packages/spec/src/migrations/entries/semantic/18.automation-runs-cursor-retired.ts index cf4445205f..4a90fec3f8 100644 --- a/packages/spec/src/migrations/entries/semantic/18.automation-runs-cursor-retired.ts +++ b/packages/spec/src/migrations/entries/semantic/18.automation-runs-cursor-retired.ts @@ -8,7 +8,7 @@ export const entry: SemanticMigration = { // code span AND a table cell. surface: 'api.listRuns cursor — the pagination query parameter of ' - + 'GET /api/automation/:name/runs declared by ListRunsRequestSchema, its slot on ' + + 'GET /api/v1/automation/:name/runs declared by ListRunsRequestSchema, its slot on ' + 'IAutomationService.listRuns, and its option on all three @objectstack/client run-list ' + 'surfaces (automation.runs.list, automation.listRuns, environment().automation.listRuns). ' + 'The limit parameter of the same door is NOT part of this retirement and is unchanged, ' @@ -40,7 +40,7 @@ export const entry: SemanticMigration = { + "the Console's flow-runs page sends it today. Retiring it would have been a " + 'regression, and its `.default(20)` stays with it. ' + 'The same card computes `hasMore`, which is the half a bare retirement would have left ' - + 'lying. `GET /api/automation/:name/runs` shipped a literal `hasMore: false` beside a ' + + 'lying. `GET /api/v1/automation/:name/runs` shipped a literal `hasMore: false` beside a ' + 'list the engine had already truncated with `.slice(0, limit)`, so a caller asking for ' + 'one row of a thousand was handed one row and told that was all of them. The engine ' + 'now reports truncation to the door through a new optional contract member, ' @@ -69,7 +69,7 @@ export const entry: SemanticMigration = { + '#6361 retired the notifications `cursor`: the client dropped the option and recorded ' + 'the removal in its docblock. ADR-0049 / ADR-0087, #19543.', acceptanceCriteria: - 'No caller sends `cursor` to `GET /api/automation/:name/runs`, and that is true of every ' + 'No caller sends `cursor` to `GET /api/v1/automation/:name/runs`, and that is true of every ' + 'channel this repo ships rather than of the schema alone. Writing it on a ' + '`ListRunsRequest` is a `tsc` error (the input type is `never`), and any value reaching a ' + 'parse raises the prescription rather than a generic unrecognized-key issue. The option is ' diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index 30cf33582d..e61253e4de 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -5940,7 +5940,7 @@ const step18: MigrationStep = { // code span AND a table cell. surface: 'api.listRuns cursor — the pagination query parameter of ' - + 'GET /api/automation/:name/runs declared by ListRunsRequestSchema, its slot on ' + + 'GET /api/v1/automation/:name/runs declared by ListRunsRequestSchema, its slot on ' + 'IAutomationService.listRuns, and its option on all three @objectstack/client run-list ' + 'surfaces (automation.runs.list, automation.listRuns, environment().automation.listRuns). ' + 'The limit parameter of the same door is NOT part of this retirement and is unchanged, ' @@ -5972,7 +5972,7 @@ const step18: MigrationStep = { + "the Console's flow-runs page sends it today. Retiring it would have been a " + 'regression, and its `.default(20)` stays with it. ' + 'The same card computes `hasMore`, which is the half a bare retirement would have left ' - + 'lying. `GET /api/automation/:name/runs` shipped a literal `hasMore: false` beside a ' + + 'lying. `GET /api/v1/automation/:name/runs` shipped a literal `hasMore: false` beside a ' + 'list the engine had already truncated with `.slice(0, limit)`, so a caller asking for ' + 'one row of a thousand was handed one row and told that was all of them. The engine ' + 'now reports truncation to the door through a new optional contract member, ' @@ -6001,7 +6001,7 @@ const step18: MigrationStep = { + '#6361 retired the notifications `cursor`: the client dropped the option and recorded ' + 'the removal in its docblock. ADR-0049 / ADR-0087, #19543.', acceptanceCriteria: - 'No caller sends `cursor` to `GET /api/automation/:name/runs`, and that is true of every ' + 'No caller sends `cursor` to `GET /api/v1/automation/:name/runs`, and that is true of every ' + 'channel this repo ships rather than of the schema alone. Writing it on a ' + '`ListRunsRequest` is a `tsc` error (the input type is `never`), and any value reaching a ' + 'parse raises the prescription rather than a generic unrecognized-key issue. The option is '