Skip to content

chore(deps): NestJS 12 stable - #111

Draft
tnramalho wants to merge 22 commits into
mainfrom
feat/nestjs-12-stable
Draft

chore(deps): NestJS 12 stable#111
tnramalho wants to merge 22 commits into
mainfrom
feat/nestjs-12-stable

Conversation

@tnramalho

Copy link
Copy Markdown
Collaborator

Pull Request

Summary

Moves the NestJS packages off the 12.0.0 prereleases now that the stable line
is published.

  • @nestjs/common / core / platform-express / testing: 12.0.0-alpha.6
    12.0.1
  • @nestjs/swagger / cli / schematics: their alphas → 12.0.0

No source change. Only manifests and the lockfile. Kept separate from #105
on purpose: that PR is already large, and a dependency bump with no code should
be reviewable — and revertable — on its own.

Based on feat/schema-engine-104 (#105), not main, because #105 is what
introduced the Nest 12 alphas. Retarget to main once #105 lands.

@nestjs/cqrs deliberately stays on 11.0.3

@nestjs/cqrs@12.0.0 is out, and this PR does not take it.

@concepta/nestjs-* 8.0.0-alpha.9 declares @nestjs/cqrs: ^11.0.0 as a peer
dependency. Bumping it here would put every consumer of Rockets in an
unsatisfiable peer range, pointing at a major upstream has not claimed to
support. It moves when upstream widens that range — not before, and not
silently.

Why this is low risk

The public API report comes back byte-identical. That is the useful signal
here: nothing Rockets exposes shifted between the alpha and the stable release,
so this is a version change and not a behavior change.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update
  • Refactoring
  • Dependency update

Verification

Run against this branch:

  • yarn build
  • yarn api:report:check-built — 7/7, report byte-identical
  • yarn typecheck:spec
  • yarn test — 106 files, 1115 tests
  • yarn test:e2e — 52 files, 442 tests (no flake this run)
  • yarn lint:all

Checklist

  • My code follows the existing patterns in the codebase
  • I have updated relevant documentation
  • I have added tests for new functionality (n/a — no source change)

tnramalho and others added 22 commits August 25, 2026 22:41
Rockets used @nestjs/config only for registerAs + ConfigModule.forFeature to
hand default settings to upstream's createSettingsProvider. Those defaults
are now plain Nest providers registered by each module (core, swagger-ui,
server, auth); ConfigModule is no longer re-exported.

Why now: the upstream 8.0.0-alpha.9 graph pins @nestjs/config@12.0.0-next.0,
which is ESM-only — a CJS require() from the Rockets dist fails at runtime
while tsc stays green. The root resolutions pin is removed so upstream keeps
the version it declares. rockets-server-auth keeps it as a devDependency for
the e2e ConfigService stub.

RFC #104, stage 1.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…t shim

`@concepta/rockets-core/standard-schema` and `/standard-schema/swagger`
had no consumer in Rockets or its examples and duplicated what Nest 12
ships natively (`@Body({ schema })` + `StandardSchemaValidationPipe`,
`@SerializeOptions({ schema })`, `ApiResponse({ standardSchema })`).

Generated DTOs no longer stamp `@Allow()` on their keys: the stamp only
existed to survive a foreign `ValidationPipe({ whitelist: true })`, a
pipe that must not sit in front of a schema-validated route at all.

`isStandardSchema` / `getCarriedStandardSchema` move to
`common/utils/standard-schema.util.ts` (internal). Public API report
updated: the two entry points and their 24 declarations are gone.

RFC #104, stage 2.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`zod-swagger-golden.e2e-spec.ts`, `zod-parity.e2e-spec.ts` and their
hand-written control resources compared a class-DTO `defineResource` twin
against the zod resource. Class-DTO authoring is being retired, so the
twin goes; `zod-library.e2e-spec.ts` keeps every document and runtime
assertion against the zod author/book pair alone. `contract.json` is
unchanged (the controls were never mounted in the app).

RFC #104, stage 3.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…chemas

One schema engine. Every request body and response in Rockets is a named
zod schema (`withOpenApi(schema, id)`), validated per route by Nest's
native StandardSchemaValidationPipe with the Rockets exception factory
(`details[]` on every 400), serialized by the response schema (undeclared
columns never leave, Date -> ISO), and documented from the schema's own
JSON Schema bridge with a Rockets converter that `$ref`s named components.

Core: `defineResource` dto/input/output/paginated are schemas; zod
compiler emits `.zod.schemas`; `f.date()`, audit columns as `z.date()`;
response-exposed `z.iso.datetime()` rejected; optional fields admit the
`null` a nullable column reads back; `userMetadata` is
`{ entity, updateSchema, responseSchema }`; `SchemaValidatorConflictCheck`
refuses a global StandardSchemaValidationPipe; planner rejects two schema
instances under one component id; `createDocument` installs the converter
and lifts `definitions` out of upstream's inline CRUD request bodies;
exceptions filter vendors `mapHttpStatus` and handles alpha.9's
`RuntimeException extends HttpException` (hook 4xx no longer served as
500). Removed: createPaginatedDto, FreeFormJson, ROCKETS_TO_* options,
ZodBodyValidationInterceptor, whitelistedFromDto (-> validateWithSchema),
UserUpdateDto/UserResponseDto, BaseUser*Dto, PersistenceRow, ZodResourceDtos.

Server: `/me` rebuilt as `buildMeController(config)` (userMetadata null
before the first PATCH; hidden columns stay hidden). Auth: user/role/
invitation DTO classes are schemas composed from upstream schemas; admin,
signup, token, recovery and invitation routes on the engine; password
rotation runs consume + write in one transaction scope (the consume-only
scope left a finished transaction on the request context); new OTP login
e2e. `@concepta/nestjs-common` dropped. Examples and both contract.json
regenerated; docs updated.

Known gap (upstream): `CrudInitApiBody` inlines generated CRUD request
bodies, so `${Name}CreateDto`-style request components are inlined rather
than `$ref`'d (responses are).

RFC #104, stage 4.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…gine; legacy validators removed

Stages 5 and 6 of RFC #104, landed together: their agents ran in
parallel and share the regenerated contract.json / public API report.

Stage 5 — operationResource: compiled descriptors carry `inputSchema`,
`paramsSchema` and a schema `output` (no DTO classes). The generated
controller has a class-level StandardSchemaValidationPipe with the
Rockets factory; the body is `@Body({ schema })` behind a payload-shape
guard (missing body -> {}, array/scalar/Buffer -> 400 naming the whole
body), the query `@Query({ schema })` and the params `@Param({ schema })`
(raw params merged so extra path params still reach ctx.params). Output
is validated inline (null / mismatch -> 500). OpenAPI comes from the
schema bridge: named `<Base>Input` / `<Base>Output` components, query and
path params one per property. Defect fixed on the way: the generated
method exposed no `design:paramtypes`, so swagger documented no body,
query or params for any operation. Component-id uniqueness across CRUD
and operation resources is one planner check.

Stage 6 — hand-written routes: rockets-auth's OTP, change-password,
invitation revoke / acceptance-payload and admin role-assignment bodies,
sample-server's pet-share and sample-code-review's DTOs are named zod
schemas validated by each controller's own pipe (invitation-acceptance
`payload` is now validated). class-validator, class-transformer and
nestjs-zod are removed from every manifest; compileDtoClass / namedZodDto
are gone; the packed-consumer check installs no validation library beside
zod. Exception: rockets-auth keeps class-validator/class-transformer as
plain dependencies while nestjs-email/event (7.x) pull nestjs-common@7,
which requires them at import — the packed consumer proved it (locally
they resolved from a node_modules OUTSIDE the repo).

Also: sample-server scope hooks forward `ctx` to their repository
lookups (rule 16); docs, CHANGELOGs, api policy note.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Review of the RFC #104 branch (adversarial pass) plus the first external
consumer run (realtystack) surfaced these; all fixed with a failing test
first.

- A create body that validates to `{}` is a valid create. Generated
  resources run on `RocketsCrudAdapter`, which drops the bare 400 the
  upstream adapter answered to an empty validated payload. An
  all-server-stamped sub-resource (`POST /parents/:id/stamps {}`) is the
  new e2e case.
- The fail-closed response check walks every wrapper (intersection,
  tuple, record/map/set value, readonly, catch, any single-child
  wrapper) and covers hand-supplied paginated envelopes.
- Hidden columns stay hidden at every depth of a computed field.
- One OpenAPI component describes one side: a schema used as both a
  request and a response is a document-build error.
- `f.date()` rejects null and booleans instead of storing 1970.
- Invitation acceptance validates `userMetadata` with the same default
  schema as signup and admin when the app configures none (a smuggled
  `userId` no longer reaches the row).
- A `@Body/@Query/@PARAM({ schema })` that no StandardSchemaValidationPipe
  reaches fails the boot (`requireSchemaPipe`); the route audit is
  always on, the route policy stays opt-in.
- Hand-written auth request bodies keep their component names
  (`LocalLoginDto`, `RefreshDto`, `Recovery*Dto`).
- Docs that still described the class-DTO era are rewritten.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…name

The report records declarations reachable from public signatures; the
token/recovery controllers' body type aliases now point at the named
Rockets schemas (LocalLoginDto, RefreshDto, Recovery*Dto).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adversarial review of the branch plus the realtystack run; every item
landed with a test that failed first.

- Generated CRUD request bodies are $ref'd to their named component in
  the OpenAPI document again (`restoreNamedRequestBodies` drops the
  inline stamp upstream's CrudInitApiBody leaves where the route's
  @Body({ schema }) is named — upstream #467). Both contracts regenerated:
  every *CreateDto / *UpdateDto / *ReplaceDto component is back.
- The one-component-one-side rule covers nested named schemas: one nested
  id emitted with two shapes is an error, not a last-wins merge.
- Hand-written @SerializeOptions({ schema }) gets the fail-closed check
  at boot (`requireClosedResponse`); `requireSchemaPipe` has its own
  exemption list (`allowUnvalidatedSchema`) instead of riding on `allow`.
- rockets-auth: admin PATCH /admin/users/:id and /admin/roles/:id bodies
  are validated (the schema moved from the controller to the operation —
  upstream stamps the pipe from the operation only), the user update runs
  in one outermost transaction scope (it answered 500 on the finished
  transaction upstream leaves on the context, #468), and the metadata
  repository pins userId from the caller on the update branch.
- Migration note for the renamed/removed OpenAPI components.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…uard

- The /me metadata handlers forward the request context to every
  repository call and pin userId from the caller on the update branch;
  UpsertUserMetadataCommand(ctx, userId, data) and
  GetUserMetadataQuery(ctx, userId) take the context first (breaking for
  handler overrides; migration note in the changelogs).
- A generated CRUD body with no schema fails the boot under
  requireSchemaPipe — the structural guard for the admin-update defect
  (a controller-level request.body documents and validates nothing).
  Two e2e fixtures that relied on unvalidated bodies now declare theirs.
- allowUnvalidatedSchema gets the same more-than-one-match guard as allow.
- The audit report lists responses documented with standardSchema but
  serialized through no @SerializeOptions (unserializedResponseSchemas).
- Migration notes: explicit @ApiBody beside a named @Body is dropped;
  allow / allowControllers no longer exempt the schema-pipe check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… reader

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pipe in-side, Node 20.19 floor

- Computed-field projection rebuilds every composite wrapper (union,
  intersection, pipe, default, catch, readonly, nonoptional, lazy) so a
  `dto: { response: false }` column below one never reaches the wire;
  a hidden column below a wrapper it cannot rebuild (discriminated
  union, tuple, record, map, set) fails at definition time. Runtime
  regression for `f.compute(z.union([nested, fallback]))`.
- `assertFailClosedResponse` walks BOTH sides of a pipe: an ordinary
  `.transform()` keeps its (open) object on the IN side. Regression for
  `.passthrough().transform(v => v)`.
- Node floor is 20.19.0 in every published `engines`, the docs and the
  CI matrix (the CommonJS build loads the ESM Nest 12 line through
  require(esm); 20.18 fails with ERR_REQUIRE_ESM).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Loading vitest.config.mts straight into Node needs native TypeScript
stripping (Node 22+); the 20.19.0 leg exists to test the published
packages' floor (build, tests, packed consumer), not the repo tooling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…19 floor

Second pass on the PR #105 review, after an adversarial check of the
first one:

- `.default()` / `.catch()` hand their payload over without running the
  inner schema, so a hidden column below them cannot be stripped — they
  are rejected at definition time (they were wrongly "rebuilt").
- A recursive `z.lazy` with a hidden column overflowed the stack: the
  rebuilt lazy is memoized per source instance so walker identity holds.
- The strip now runs on all three response paths — computed fields, JSON
  columns and exposed relations — not only under `f.compute()`.
- The fail-closed walker reads a pipe's IN side only when its OUT is a
  transform; `z.pipe(open, closed)` strips on the way out and passes.
- The Vitest runner preloads `@nestjs/core` (setup file), which removes
  the ERR_REQUIRE_CYCLE_MODULE Node 20.19 raised when `@nestjs/cqrs`
  required the still-evaluating ESM module. Both CI legs now run the full
  suite; `fail-fast: false`; the module cache is keyed by Node version.

Regressions for each case fail against the previous commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pass-through pipes

Third pass on the PR #105 review round, after a second adversarial seal:

- An `operationResource` output built from an entity schema strips its
  `dto: { response: false }` fields — the fourth response path, beside
  computed fields, JSON columns and exposed relations. E2E asserts the
  secret is absent from the HTTP body.
- A TOP-LEVEL `.default()` on a field with a hidden column is rejected
  at definition time (it was silently dropped, and the row then failed
  serialization at runtime); `.prefault()` is rebuilt — its payload does
  run through the inner schema.
- The fail-closed walker reads a pipe's IN side whenever its OUT passes
  values through (transform, any, unknown, custom), not only transforms.
- CI: one test-results artifact per matrix leg (the report reads the
  22.x one); release-readiness pins the exact 20.19.0 floor; the wording
  no longer claims the example apps run in the PR matrix.
- The cqrs/ESM runner gap is filed upstream (nestjs/nest#17583) and the
  preload file says when to remove it.

Regressions for each case fail against the previous commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…level preprocess

Fourth pass on the PR #105 review round, after a third adversarial seal:

- A hand-written response schema (dto.response, operations.*.output,
  dto.paginated, userMetadata.responseSchema) is not projected and keeps
  the author's component id, so a `dto: { response: false }` field
  inside it is rejected at definition time with a pointer at `.omit()`
  (`assertNoHiddenFields`) instead of reaching the wire.
- The projected field is stripped WITH its wrappers (`stripHidden` on
  the field itself): a top-level `z.preprocess` is kept and rebuilt, and
  a top-level `.default()` / `.catch()` is rejected like a nested one —
  no more peel-then-partially-reapply.
- The fail-closed walker's pass-through test is recursive: `any`,
  `unknown`, `custom`, `transform` behind optional / nullable / readonly
  / lazy or as a union member also make a pipe's IN side walkable.
- Stale CI comment about the packed-consumer contract corrected.

Regressions for each case (six new) fail against the previous commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…posites

Fifth pass on the PR #105 review round, after a fourth adversarial seal:

- rockets-auth hands `userCrud.model` / `roleCrud.model` straight to
  upstream CRUD serialization (no defineResource projection), so only
  the component name was checked. The signup, admin-users and
  admin-roles modules now also require a fail-closed schema with no
  `dto: { response: false }` field (`assertNoHiddenFields`, exported from
  core). Regression per module, each failing against the previous commit.
- The fail-closed walker's pass-through test reaches composites: an OUT
  that is an array / object / record / intersection / nested pipe holding
  `any` / `unknown` / `custom` / a transform hands input through, so the
  pipe's IN side is walked. Five new spec rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ail-closed guard

Sixth pass on the PR #105 review round, after a fifth adversarial seal:

- `passesThrough` descended through `schemaChildren`, whose pipe branch
  called `passesThrough` again with a fresh cycle set — a recursive lazy
  that crosses a `z.preprocess` overflowed the stack in every walker
  (fail-closed check, hidden-field detection, projection). It is now
  memoized per schema instance, with the in-progress entry reading
  `false`, so a cycle terminates. Regressions on all three entry points
  fail against the previous commit with RangeError.
- The fail-closed guard on rockets-auth's `userCrud.model` /
  `roleCrud.model` had no test: an open (`.catchall()`) model is now
  rejected by a regression per module.
- CONFIGURATION.md documents strip-vs-reject (projected vs hand-written
  response schemas) and the deliberate over-flag on pass-through pipes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ields in serializer schemas

Seventh pass on the PR #105 review round, after a sixth adversarial seal:

- The pass-through memo stored an in-progress `false` as a final answer,
  which made the fail-closed verdict depend on visit order and fail
  OPEN (the same schema reported nothing when the branch that closed a
  cycle was visited first). Only `true` is memoized now (the predicate is
  monotone); termination comes from a per-walk in-progress set threaded
  through the shared child walker. Regressions pin both field orders and
  a warm-then-probe run over shared instances; both fail against the
  previous commit.
- The route audit rejects a hand-written `@SerializeOptions({ schema })`
  that declares a `dto: { response: false }` field
  (`RouteAuditEntry.hiddenResponseField`, `requireClosedResponse`) — the
  last response path with neither strip nor reject.
- CONFIGURATION: `op.sse()` declares no `output` at all (not `output:
  false`).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…memo

The previous commit said both memo regressions failed against c571386;
only the field-order one did — its warm-up case computed the cold
verdict first, which cached `true` on the shared node before the walk
that would have poisoned it. The warm-up now comes first, on the same
instances, with the transform inside the cycle; it fails on c571386.

Also: the two JSDoc blocks orphaned by the last insertions are back on
`assertNoHiddenFields` / `readOpenResponseSchema`, the open-response
reader reuses `readSerializerSchema`, and the CHANGELOG notes the
complexity trade of a walker with no `false` memo.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…route audit

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he OpenAPI converter

Two gaps found by probing what a class DTO could express that a zod
schema has to express too.

A second recursive schema aborted document generation. `z.toJSONSchema`
names a definition it extracted but cannot name — the inner object of a
`z.lazy()` recursion — positionally as `__schema0`, restarting the counter
per converted schema, so two unrelated recursive schemas in one app both
claimed that name and the second one threw a shape-mismatch error that
blamed the request/response split. Anonymous definitions are now qualified
with the owning component id, and every `$ref` that pointed at them is
rewritten — including the `#/$defs/` and `#/definitions/` forms Swagger
normalises only after the converter returns, which is what left a
dangling ref on the first attempt.

A discriminated union was documented as a bare `oneOf`, losing the tag
that makes it discriminated: a generated client had to try each branch
instead of switching on the property. When every branch is named,
`discriminator` is emitted with an explicit `mapping` — required, not
cosmetic, since the implicit form matches the tag value against the
component name and 'circle' is not 'CircleDto'. A union with one unnamed
branch is left alone rather than given a partial mapping. Components are
matched by branch set, because the same union node is reached under an
operation's generated wrapper id and emitted under the authored id.

Neither shows up in the committed example contracts: both need a feature
the examples do not use, so this is a latent ceiling rather than a defect
in the shipped documents.
`@nestjs/common` / `core` / `platform-express` / `testing` move from
`12.0.0-alpha.6` to `12.0.1`; `@nestjs/swagger` / `cli` / `schematics` from
their alphas to `12.0.0`.

No source change was needed. The public API report comes back
byte-identical, so nothing Rockets exposes shifted with the release —
build, spec typecheck, 1115 unit tests, 442 e2e tests and lint all pass on
the stable line.

`@nestjs/cqrs` stays on `11.0.3` on purpose. `@concepta/nestjs-*`
`8.0.0-alpha.9` declares `@nestjs/cqrs: ^11.0.0` as a peer, so taking
`12.0.0` here would put every consumer in an unsatisfiable peer range for a
version upstream has not claimed to support. It moves when upstream widens
that range.
@tnramalho
tnramalho marked this pull request as draft August 28, 2026 12:14
Base automatically changed from feat/schema-engine-104 to main September 9, 2026 21:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant