reference.md
2,392 tokens · o200k_base · 10,276 bytes
Source excerpt starting at line 1.# Public API v1 — reference Detail for tasks that need it. Essentials and the rule tiers are in[SKILL.md](SKILL.md). Open the cited files; they are the source of truth. ## List endpoints and cursor pagination The internal API mixes cursor- and page-based pagination. New Public API listendpoints use **cursor-based** pagination — do not copy an internal controller'smodel. Copy the working flow from `v1/controllers/tags.public.controller.ts` (and`workflows.public.controller.ts` for a `@Param` list) rather than pasting asnippet here — a copy would drift. The moving parts: - Input DTO takes `limit: publicApiPaginationSchema.limit` plus an opaque `cursor: z.string().optional()` — cherry-pick `limit` but never spread the whole `publicApiPaginationSchema`. That schema also exports `offset`, used by internal-API-style page params elsewhere; a Public API list DTO must never expose it as a query param. See `ListTagsQueryDto` for the shape to copy.- `decodeCursor` / `encodeNextCursor` live in `v1/shared/services/pagination.service.ts`. Decode the incoming cursor to `{ offset, limit }`, guard the decoded shape, and pass `offset`/`limit` to the service — never `{ skip, take }`. `offset` is an internal implementation detail of the cursor here, never a client-facing query param. TypeORM's `skip`/`take` stay inside the repository, at the `find` call.- Treat the cursor as opaque; never hand-encode a token.- Return an envelope `{ data, nextCursor }` — never a bare array.- `encodeNextCursor(...)` returns `null` when there is no further page; surface that as `nextCursor: null`.- An invalid/undecodable cursor is a `400` via the existing bad-request error.- For an existing list endpoint, keep its current *cursor* semantics unchanged. A leaked `offset` query param (DTO spreading `publicApiPaginationSchema` instead of picking `limit`) is a defect to remove, not a contract to preserve — decorator-routed DTOs validate via a plain `z.object()`, which silently strips unknown query keys rather than rejecting them, so removing `offset` from the DTO makes it inert rather than erroring for existing callers. The output DTO wraps the list as `{ data, nextCursor }` and is declared with`@ApiResponse(...)` so the registry strips undeclared fields. ## Updates and write-only secrets - Default to `PUT`. The update DTO describes the full mutable object; the validation layer rejects a partial payload (typically `400`). Don't implement merge semantics behind a `PUT`.- Don't add a new `PATCH` unless the task explicitly requires partial-update semantics or an established resource-specific exception applies.- Migrating an update endpoint keeps its current public HTTP semantics. Write-only secrets (credentials, tokens, keys) support GET→PUT round-trip via aresource-specific sentinel/placeholder (e.g. credentials use`CREDENTIAL_BLANKING_VALUE` / related helpers — do not invent a new format): - `GET` never returns the real secret; it returns that sentinel (or omits the field). Never echo a real secret in responses or error details (including test-connection and upstream error messages).- On `PUT`, sending the **exact** sentinel from `GET` means **keep** the stored secret. Sending **any other** value means **replace** it. Do not treat "looks masked" or "field omitted" as keep unless the resource helper/tests say so.- Reuse the resource's redact/unredact (or equivalent) helper in the service — don't reimplement sentinel detection or credential merge in the controller, and never persist the sentinel as a real secret.- Sentinel support does not make the whole `PUT` a partial update; other required client-manageable fields stay required. Server-managed/immutable fields from `GET` (`id`, timestamps, …) follow the resource DTO (ignored or not required on write). ## Test-before-save endpoints A connection/config-test endpoint validates the config in the **request body**,not stored state — unless the endpoint explicitly verifies an already-savedresource. Secret handling is the same as any other endpoint — see above. ## Errors - Don't leak persistence errors, stack traces, or internal messages. Reuse existing domain errors when the registry already maps them to the right public errors; otherwise map at the controller boundary.- Follow the error semantics of the nearest existing public controller; invalid input and invalid cursors use the existing bad-request pattern.- When migrating, preserve the documented status codes and public error behavior. ## Testing matrix Always (in SKILL.md): happy path, input-validation failure, missing API-keyscope, RBAC denial. Add whichever apply, matching the nearest existing tests: - At least one integration test under `packages/cli/test/integration/public-api/` that exercises the real service/DB path for the main success case (and paging/RBAC where they matter). Controller unit tests with a mocked service are fine for wiring/validation edges — not as the only coverage of behavior.- Cursor paging: first page, final page with `nextCursor: null`, invalid cursor, `limit` handling.- Not-found and conflict semantics.- Response carries no sensitive/internal fields.- Credential resources: response has no real secret (sentinel or omitted); `PUT` with the exact sentinel keeps the secret; any other value replaces it; the sentinel is never persisted as a real secret.- Migration: path, method, status codes, scope, and response contract are unchanged. Tests alone cannot show this — see [Verifying a migration](#verifying-a-migration). ## Migrating legacy EOV endpoints Legacy `express-openapi-validator` endpoints live under`v1/handlers/`, wired through `openapi.yml` with `x-eov-operation-*` and requesttypes in `packages/cli/src/public-api/types.ts`. Treat these as migration targets,not templates. - Prefer migrating to `@PublicApiController` over extending the handler.- Preserve the public contract: path, method, scopes, status codes, response shape, and pagination.- Move HTTP concerns into the controller and business orchestration into the shared service; keep the controller a thin HTTP boundary. If the legacy handler was itself already just a thin wrapper around an internal `@RestController` (calling its methods directly, e.g. `Container.get(SomeController).createThing(req, res, payload)`), the new public controller can call that same internal controller directly — no need to duplicate its validation/business logic.- A route must be served by either the EOV handler or a controller, not both — the build's `mergeDecoratorDocument` (`v1/openapi-gen/generate.ts`) throws on a path+method declared by both sides. Remove the legacy wiring (its path's `$ref` entry in `openapi.yml`, the `x-eov-operation-*` handler, and its `handler.ts`) only after the new controller is registered, `pnpm build` regenerates the spec cleanly, and tests are updated.- Fully delete the migrated legacy files: the handler's `.ts`, its `spec/paths/*.yml` and `spec/schemas/*.yml`, and any now-dead request type in `packages/cli/src/public-api/types.ts`. Then check `v1/shared/spec/schemas/_index.yml` and `v1/shared/spec/parameters/_index.yml` for entries that `$ref` one of the deleted schema/parameter files — those are separate from the path's own `$ref` in `openapi.yml` and are easy to miss; left dangling, the next bundle fails on a broken `$ref`.- If the legacy handler gated on a license (`isLicensed('feat:x')` middleware), `@Licensed('feat:x')` now replicates that for a controller route (see the decorator table in [SKILL.md](SKILL.md#declaring-a-controller)) — but only for a single feature. If the legacy check was an any-of/all-of over several flags (e.g. `LicenseState.isProvisioningLicensed()`), `@Licensed` can't express that; replicate it manually in the controller instead, don't drop it - this is exactly what the internal `provisioning.controller.ee.ts` and `role-mapping-rule.controller.ee.ts` already do, since neither uses `@Licensed` for that reason.- As a legacy file drops repository access / the `export =` tuple, remove its entry from the `off` allowlists for `no-repository-in-public-api-handler` and `require-public-api-controller` in `packages/cli/eslint.config.mjs` (shrink-only — never extend them).- For complex legacy-only, multipart, or non-standard endpoints, study the nearest existing handler first.- Keep each field in its original position when you extract a request shape shared by two routes, and destructure out the ones a route doesn't take. The generator emits properties in shape order, so a moved field rewrites the `*.generated.yml` of a route the PR wasn't changing. ## Verifying a migration Tests are written against the new code, so they can't show that the old behaviorsurvived. These checks can: - Diff live responses against master. Run the route on an instance per branch and compare status, body, and error message for the same requests. Reading the old YAML beside the new Zod schema doesn't find the differences.- Check whether a field is absent or `null`. `activeVersion` omitted is not the same response as `activeVersion: null`; use a conditional spread to omit it.- Check query-param coercion at the edges (`""`, `"0"`, `"false"`, absent). A Zod DTO and the old validator don't coerce identically.- Request a route the PR didn't migrate. A shared DTO change can stop the spec bundle compiling at startup, which turns every legacy route into a `500` without failing a test. ## CI and merging Two failures that a migration hits outside the code itself: - Merge master in before merging. A generator change on master leaves every branch's committed `*.generated.yml` stale, and `generated-spec-drift.test.ts` then fails only on the merge commit, so the PR itself stays green and `MERGEABLE`. `gh pr checks` hides `merge_group` runs; look for the run on `gh-readonly-queue/master/pr-<number>-<sha>`.- Ask a maintainer for a `/size-limit-override` early. Generated YAML counts toward the 1,000-line PR size limit, so a single-route migration can exceed it on generator output alone.
Referenced from SKILL.md
Source excerpt starting at line 57.means keep; any other value replaces. Detail: [Updates and write-only secrets](reference.md#updates-and-write-only-secrets).- "Test connection/config" endpoints validate the request body (test-before-save).
Source excerpt starting at line 150.- Secrets: never return a real secret; use the resource's sentinel/placeholder (or omit). See [Updates and write-only secrets](reference.md#updates-and-write-only-secrets).
Source excerpt starting at line 163.defect to remove, not a contract to preserve. Detail:[List endpoints and cursor pagination](reference.md#list-endpoints-and-cursor-pagination).
Source excerpt starting at line 191.not-found/conflict, no sensitive fields, credential keep/replace, migrationcontract) — see [Testing matrix](reference.md#testing-matrix). Match the nearestexisting tests.
Source excerpt starting at line 195.## More detail (reference.md)
Source excerpt starting at line 197.- [List endpoints and cursor pagination](reference.md#list-endpoints-and-cursor-pagination)- [Updates and write-only secrets](reference.md#updates-and-write-only-secrets)- [Test-before-save endpoints](reference.md#test-before-save-endpoints)- [Errors](reference.md#errors)- [Testing matrix](reference.md#testing-matrix)- [Migrating legacy EOV endpoints](reference.md#migrating-legacy-eov-endpoints)- [Verifying a migration](reference.md#verifying-a-migration)- [CI and merging](reference.md#ci-and-merging)