chore(deps): NestJS 12 stable - #111
Draft
tnramalho wants to merge 22 commits into
Draft
Conversation
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
marked this pull request as draft
August 28, 2026 12:14
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.0No 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), notmain, because #105 is whatintroduced the Nest 12 alphas. Retarget to
mainonce #105 lands.@nestjs/cqrsdeliberately stays on 11.0.3@nestjs/cqrs@12.0.0is out, and this PR does not take it.@concepta/nestjs-*8.0.0-alpha.9declares@nestjs/cqrs: ^11.0.0as a peerdependency. 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
Verification
Run against this branch:
yarn buildyarn api:report:check-built— 7/7, report byte-identicalyarn typecheck:specyarn test— 106 files, 1115 testsyarn test:e2e— 52 files, 442 tests (no flake this run)yarn lint:allChecklist