A body the old endpoint refused came back as `request/body/name must be
string`; validating the same body with Zod produced `Expected string, received
number` instead. The status and the reason were unchanged, but the text callers
parse was not — 85 of the 113 replayed bodies differed only in wording.
The wording belongs to the DTOs that took over from a hand-written schema, not
to the transport, so it lives in an error map the DTO opts into. Endpoints that
never had a spec to inherit from — `roles`, `role-mapping-rules` — keep their
own messages.
`Z.class` now applies an error map at parse time rather than attaching it to the
object, which is what lets one map cover nested nodes, node groups and settings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The old create handler answered with the raw entity, so its response carried
`parentFolder` — retrieval never did, and the published `workflow.yml` never
documented it. Callers read it today, so create keeps returning it and the
generated spec now says so.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The migrated body accepted four things the hand-written spec rejected with a
400, found by diffing live responses against master:
- `description`, which `workflowCreate.yml` never listed
- nodes carrying unknown or wrongly typed properties, which `node.yml`
constrained with `additionalProperties: false`
- a `staticData` string that is not JSON, which `format: jsonString` caught
- and it rejected `shared`, the one relation the old spec left writable
A non-object node also reached the persistence layer and surfaced as a 500
rather than a 400.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The names were free-standing strings, so renaming a field on the response
schema would leave this list stale and quietly downgrade the error message to
the generic "unrecognized key". `satisfies` makes that a build failure.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The generated request spec listed `id`, `active`, `createdAt` and the other
server-managed fields as ordinary properties with empty schemas, because the
generator cannot express a "must be absent" check. Alongside
`additionalProperties: false`, the published contract positively invited callers
to send fields the server always rejects, and an SDK generated from it would
expose them as settable. The hand-written spec marked them `readOnly`.
They are now left out of the request shape, so the spec describes only what a
caller may send. The strict object rejects them as unrecognised keys, and an
error map keeps the specific message rather than letting a read-only field look
like a typo:
{ "id": "abc" } -> 'id' is a read-only field and cannot be set
{ "nam": "oops" } -> Unrecognized key(s) in object: 'nam'
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both comments described what the code does, which the code already showed. They
now record the reason a reviewer cannot see: Zod strips unknown keys where the
hand-written spec rejected them, and `/discover` can only read request bodies
out of that spec, which a decorator route has no entry in.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`workflowNodeGroup.yml` specified the structure: `id`, `name` and `nodeIds`
required, an optional `description` capped at 155 characters, and no other keys.
The DTO only checked that `nodeGroups` was an array, so four things the old API
rejected were being accepted:
nodeGroups: [{}] missing required fields
nodeGroups: [1, 2, 3] not objects
nodeGroups: [{ ...valid, bogus: 1 }] unknown key
nodeGroups: [{ ...valid, description: 156 }] over the length cap
Declared in the public DTO rather than reusing the internal
`workflowNodeGroupsSchema`, so a change to the internal request shape cannot
silently move the public contract.
The cap stays a literal 155 rather than `GROUP_DESCRIPTION_MAX_LENGTH` (145):
the engine truncates to that value instead of rejecting, so borrowing it would
start rejecting requests the old API accepted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Moves workflow creation onto `@PublicApiController` and removes the legacy
handler, its hand-written spec files, and the now-unused request type. Behaviour
is unchanged: the same `WorkflowCreationService` call, the same redaction check,
the same derived settings stripped before saving.
`workflows.yml` held only this operation once the list route moved in #36015, so
the file and its `openapi.yml` entry go with it.
`workflowPublicDto.generated.yml` appears because a second route now references
the response DTO, so the generator hoists it into a shared component. The 237
lines it gains are the 237 `getWorkflow.generated.yml` loses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mirrors the hand-written `workflowCreate.yml` and `workflowSettings.yml` that
express-openapi-validator enforced, so the migrated endpoint accepts and rejects
exactly what it did before:
- unknown top-level keys are rejected, via a new optional `{ strict: true }` on
`Z.class` (default behaviour is unchanged for every other DTO)
- all 17 documented settings are typed, with their enums, and unknown settings
are rejected
- server-managed fields such as `id` and `active` are rejected with a read-only
error rather than silently ignored
- `binaryMode` and `credentialResolverId` are accepted, because the old spec
listed them; the controller strips them afterwards
`nodes`, `connections`, `staticData` and `pinData` stay shape-only checks. Their
contents are node-type specific and only the workflow engine can validate them,
which is also what the old spec did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`GET /discover?includeSchemas=true` describes each endpoint's request body, but
only ever looked in the hand-written spec files. An endpoint described by a Zod
DTO had nothing to find, so it reported no schema at all.
Three endpoints are affected today: `PUT /workflows/{id}/tags`, `POST /roles`
and `POST /role-mapping-rules`. Nothing caught it because no test asserted a
schema for those.
The description now comes from the route's own `@Body` DTO, converted by the
same generator that writes the spec files, so the two cannot disagree.
Also repoints the reference-resolution test. It used `createWorkflow`, whose
generated schema holds no references, so it had started passing without
exercising anything. It now uses `createDestination`, still described by
hand-written files, and scans the whole resolved schema instead of one field.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>