Subchapter 41.1
reference.mdMarkdown10 KBView on GitHub
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.
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:
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.{ data, nextCursor } — never a bare array.encodeNextCursor(...) returns null when there is no further page; surface
that as nextCursor: null.400 via the existing bad-request error.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.
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.PATCH unless the task explicitly requires partial-update
semantics or an established resource-specific exception applies.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):
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).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.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).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.
Always (in SKILL.md): happy path, input-validation failure, missing API-key scope, RBAC denial. Add whichever apply, matching the nearest existing tests:
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.nextCursor: null, invalid cursor,
limit handling.PUT with the exact sentinel keeps the secret; any other value replaces it;
the sentinel is never persisted as a real secret.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.
@PublicApiController over extending the handler.@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.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..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.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()), @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.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).*.generated.yml
of a route the PR wasn’t changing.Tests are written against the new code, so they can’t show that the old behavior survived. These checks can:
null. activeVersion omitted is not the
same response as activeVersion: null; use a conditional spread to omit it."", "0", "false", absent). A Zod
DTO and the old validator don’t coerce identically.500 without
failing a test.Two failures that a migration hits outside the code itself:
*.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>./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.