reference.md
2,392 tokens · o200k_base · 10,276 bytes
Public API v1 — reference
Detail for tasks that need it. Essentials and the rule tiers are in 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 list endpoints use cursor-based pagination — do not copy an internal controller's model.
Copy the working flow from v1/controllers/tags.public.controller.ts (and
workflows.public.controller.ts for a @Param list) rather than pasting a
snippet here — a copy would drift. The moving parts:
- Input DTO takes
limit: publicApiPaginationSchema.limitplus an opaquecursor: z.string().optional()— cherry-picklimitbut never spread the wholepublicApiPaginationSchema. That schema also exportsoffset, used by internal-API-style page params elsewhere; a Public API list DTO must never expose it as a query param. SeeListTagsQueryDtofor the shape to copy. decodeCursor/encodeNextCursorlive inv1/shared/services/pagination.service.ts. Decode the incoming cursor to{ offset, limit }, guard the decoded shape, and passoffset/limitto the service — never{ skip, take }.offsetis an internal implementation detail of the cursor here, never a client-facing query param. TypeORM'sskip/takestay inside the repository, at thefindcall.- Treat the cursor as opaque; never hand-encode a token.
- Return an envelope
{ data, nextCursor }— never a bare array. encodeNextCursor(...)returnsnullwhen there is no further page; surface that asnextCursor: null.- An invalid/undecodable cursor is a
400via the existing bad-request error. - For an existing list endpoint, keep its current cursor semantics unchanged.
A leaked
offsetquery param (DTO spreadingpublicApiPaginationSchemainstead of pickinglimit) is a defect to remove, not a contract to preserve — decorator-routed DTOs validate via a plainz.object(), which silently strips unknown query keys rather than rejecting them, so removingoffsetfrom 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 (typically400). Don't implement merge semantics behind aPUT. - Don't add a new
PATCHunless 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 a
resource-specific sentinel/placeholder (e.g. credentials use
CREDENTIAL_BLANKING_VALUE / related helpers — do not invent a new format):
GETnever 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 fromGETmeans 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
PUTa partial update; other required client-manageable fields stay required. Server-managed/immutable fields fromGET(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-saved resource. 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-key scope, 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,limithandling. - Not-found and conflict semantics.
- Response carries no sensitive/internal fields.
- Credential resources: response has no real secret (sentinel or omitted);
PUTwith 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.
Migrating legacy EOV endpoints
Legacy express-openapi-validator endpoints live under
v1/handlers/, wired through openapi.yml with x-eov-operation-* and request
types in packages/cli/src/public-api/types.ts. Treat these as migration targets,
not templates.
- Prefer migrating to
@PublicApiControllerover 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$refentry inopenapi.yml, thex-eov-operation-*handler, and itshandler.ts) only after the new controller is registered,pnpm buildregenerates the spec cleanly, and tests are updated. - Fully delete the migrated legacy files: the handler's
.ts, itsspec/paths/*.ymlandspec/schemas/*.yml, and any now-dead request type inpackages/cli/src/public-api/types.ts. Then checkv1/shared/spec/schemas/_index.ymlandv1/shared/spec/parameters/_index.ymlfor entries that$refone of the deleted schema/parameter files — those are separate from the path's own$refinopenapi.ymland 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) — but only for a single feature. If the legacy check was an any-of/all-of over several flags (e.g.LicenseState.isProvisioningLicensed()),@Licensedcan't express that; replicate it manually in the controller instead, don't drop it - this is exactly what the internalprovisioning.controller.ee.tsandrole-mapping-rule.controller.ee.tsalready do, since neither uses@Licensedfor that reason. - As a legacy file drops repository access / the
export =tuple, remove its entry from theoffallowlists forno-repository-in-public-api-handlerandrequire-public-api-controllerinpackages/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.ymlof 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 behavior survived. 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.activeVersionomitted is not the same response asactiveVersion: 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
500without 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.ymlstale, andgenerated-spec-drift.test.tsthen fails only on the merge commit, so the PR itself stays green andMERGEABLE.gh pr checkshidesmerge_groupruns; look for the run ongh-readonly-queue/master/pr-<number>-<sha>. - Ask a maintainer for a
/size-limit-overrideearly. 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)