Skip to content

Feature/version 8 - #463

Open
MrMaz wants to merge 138 commits into
mainfrom
feature/version-8
Open

Feature/version 8#463
MrMaz wants to merge 138 commits into
mainfrom
feature/version-8

Conversation

@MrMaz

@MrMaz MrMaz commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

MrMaz added 30 commits February 16, 2026 21:37
MrMaz added 30 commits August 29, 2026 13:09
package.json declares "node": ">=22.12.0", but both workflow matrices
still ran only 20.x, so a green PR never actually tested the runtime
being published.

Reported by @tnramalho on PR #463.
- nestjs-crud: the validation-fallback docs said a controller-level
  request.body default is "never used for validation" — that changed
  with #467; it's now resolved through the method-then-class hierarchy
  and validated. Also documents the parameter-level @CrudBody schema's
  precedence over the operation/controller defaults in generated docs.
- nestjs-core: removeOverlay only removes an overlay defined directly
  on that host, not one inherited from a parent context — undocumented.
- nestjs-repository: the "Passing Context" example still used the outer
  ctx inside a run() operation, contradicting the txCtx guidance already
  established elsewhere in the same README; the "Nesting" section said
  settlement happens "when the outermost call completes", which isn't
  accurate for concurrent (non-nested) participants — it's whichever
  participant exits last.
- nestjs-repository-typeorm: same outer-ctx-inside-run() inconsistency
  in the transaction integration example.

Found via an adversarial README-accuracy audit against everything
changed since e302e67.
Bump all 13 v8 packages plus root build tooling off Nest 12
alpha/next prereleases onto GA: @nestjs/common, core, testing,
swagger, config, cqrs, jwt, passport, typeorm, cli, platform-express,
schematics. Non-v8 packages remain on Nest 11.x, untouched.

@nestjs/swagger's SchemaObject.type widened to string | string[]
under OpenAPI 3.1, which required narrowing the array case in
crud-init-api-params.decorator.ts's type-normalization logic.
…f raw tokens

Downstream tooling (route audits, custom guards/interceptors) needs to know
whether a route is `@Transactional()` or `@AuthPublic()` without mirroring
our internal metadata key strings. Add `isTransactional()` /
`getTransactionalOptions()` and `isAuthPublic()`, keeping `TRANSACTIONAL_KEY`
and `AUTHENTICATION_MODULE_DISABLE_GUARDS_TOKEN` unexported — consumers read
the metadata through a stable function instead of coupling to how it's
stored.
…ontrollers

CrudContextOverlay (and with it ctx.query's parsed filter/sort/pagination)
only populates for methods carrying one of the nine @crud<Operation> tags.
A hand-written endpoint that doesn't fit one of those — a custom
search/aggregate/report route — had no way to reuse CRUD's validated
query-string contract without either shoehorning itself into @CrudList or
hand-rolling its own parser.

@CrudQueryParams() wraps the same CrudQueryParser generated routes use in a
plain createParamDecorator, independent of @CrudController, an entity, or an
operation tag. @CrudQueryParamsApi() documents the same standard
filter/sort/pagination @apiquery set generated List/Read routes get, reusing
Swagger.createQueryParamsMeta directly.
…DMEs

Left over from the alpha-to-GA upgrade — these packages have been on
NestJS 12 GA since 4dd9d82.
Reconciles the NestJS 12 section against verified repo state (re-pin work
done, cqrs blocker cleared, shared.utils file list stale) and restructures
the whole file into a single Fable-reviewed priority order across all
former Critical/High/Nice-To-Have sections. Drops the stale v7->v8
resource-types item and flags the two items still needing design/research
before they're actionable.
Exact-pinned @nestjs/common/core/config/swagger in `dependencies` across
all 13 v8 packages risked a duplicate decorator metadata registry for any
consumer on a different NestJS 12 patch version - already a live concern
for the downstream consumer on the published alphas. @nestjs/cqrs gets the
same treatment now that @nestjs/cqrs@12.0.0 GA is installed, matching the
shape already established in nestjs-core/nestjs-crud, which also makes the
.yarnrc.yml packageExtensions override for cqrs's own peer requirements
dead - removed.

Auditing actual usage per package (not just the existing dependencies
entry) surfaced three misplaced/dead deps along the way: @nestjs/swagger
was entirely unused in nestjs-invitation and nestjs-federated (deleted),
and test-only (not production) in nestjs-cache/role/user (moved to
devDependencies only, not peer). Also found that @nestjs/cqrs itself
declares @nestjs/common/core as required peers of itself, so
nestjs-password/invitation/federated needed @nestjs/core added even though
their own source never imports it directly - the old .yarnrc.yml override
was silently papering over that gap.

Verified with a clean tsc -b --force, yarn lint, yarn test, and
yarn test:e2e (all green), plus a peer-warning diff against the
pre-change baseline showing no new warning classes.
… docs

CrudQueryBuilder.paramNamesMap had no `join` entry, so
Swagger.createQueryParamsMeta's join parameter always carried
name: undefined - the original TODO framed this as a naming bug.
Investigating further, `join` turns out not to be a real query-string
capability at all: it's not part of CrudParsedQueryInterface, and nothing
in the request parser ever reads it. Relations are configured server-side
via the separate @CrudJoin() route decorator, not requested per-call.

Rather than name the phantom parameter, remove it from both the List and
Read OpenAPI parameter sets so generated docs stop describing a query
param the server never reads. Added a red-first unit test for
Swagger.createQueryParamsMeta (no existing coverage) and dropped the
now-unnecessary undefined-name filtering workaround from
crud-query-params-api.decorator.spec.ts.
…/replace

TypeORM's own @VersionColumn auto-increments on every save(), but the
WHERE clause it builds is keyed only by primary key - never AND version
= expected. Two concurrent writers reading the same stale row both
succeed, the second silently clobbering the first's already-committed
change, with no error either side. Every real v8 entity has a version
column via CommonPostgresEntity/CommonSqliteEntity, so this was a live
data-loss risk, not an edge case.

doUpdate/doReplace now detect a version column via a new
RepositoryAdapter.getVersionColumn() (backed by a new isVersion flag
on RepositoryColumnMetadataInterface) and, when present, guard the
write with an atomic compare-and-swap: repo.increment(lockWhere,
versionColumn, 0) - a genuine atomic UPDATE ... WHERE version =
:expected, using +0 deliberately so it reports affected correctly
without performing a second real increment of its own (repo.save()'s
own auto-increment remains the sole real bump). A mismatch throws the
new OptimisticLockException (409). Entities without a version column
are unaffected - identical repo.merge()+repo.save() path as before.

The guard and the follow-up field write are two separate statements,
so without a shared transaction a third writer could still interleave
between them. TransactionScope.run() closes that window - joins the
caller's transaction if one is already active (e.g. via
@transactional()), or opens a short-lived one otherwise -
transparently, via a new optional TransactionScope wired through
createTypeOrmProvider(). When TypeOrmRepositoryModule is used directly
without RepositoryModule.forRoot() (TransactionScope isn't provided in
that config) and the caller isn't already inside their own
transaction, update/replace on a versioned entity now throws
immediately rather than silently running the two statements
unprotected.

Also widens RepoPermeatorFactory's error passthrough from
instanceof RepositoryQueryException to instanceof RuntimeException,
needed for OptimisticLockException to reach callers as a 409 instead
of being flattened into a generic 500 - the narrower check had no
apparent reason to exclude other purpose-built exceptions.

Breaking: RepositoryColumnMetadataInterface gained a required
isVersion field. Any code constructing column-metadata objects against
this interface (e.g. a custom RepositoryAdapter's own buildColumns()
equivalent) needs to add it - a one-line, mechanical fix per column,
with no behavioral impact if left false. Runtime behavior for adapters
that don't populate it is unaffected: getVersionColumn() returns
undefined and the original unprotected path runs exactly as before.
…s/shared.utils deep import

9 v8 files relied on an internal Nest path that only resolves via v12's
wildcard `./*` export and isn't part of its public API surface, so it could
disappear in any minor. Added isNil/isNumber/isObject/isString/isUndefined
as local, owned utilities in nestjs-core and repointed every import there.

A new spec pins each guard's behavior against the original Nest internal
across a battery of values, so any future drift between the two fails the
build instead of silently breaking consumers.
TODOs.md is a working backlog, not a changelog — git history already
records what shipped. Trimmed the "already fixed" narrative from the
preamble and item #2, and removed a spec comment that pointed back at
the TODOs file (a dangling reference once the item is removed from it).
5 application handlers/listener injected the raw OTP settings token
directly, reaching into the whole infrastructure config bag and
re-implementing the same command-override-vs-configured-default
resolution three separate times, plus a triplicated
OtpTypeNotDefinedException lookup.

Adds OtpPolicy (domain), wired via a module-definition provider
factory following the existing PasswordPolicy/CacheExpirationPolicy
precedent, so handlers depend on the policy class instead of a
settings token. Moves OtpTypeServiceInterface into the domain layer
since the policy needs it and must not import from infrastructure.

create-otp.handler.ts keeps the passcode generator call at its
original point in the sequence (after schema validation and namespace
resolution) rather than folding it into the type-resolution step,
since the generator is a consumer-supplied extension point that can
have side effects (e.g. counter-based generation).

The 2 nestjs-access-control sites with the same shape are left as-is:
their settings interface has exactly one field, which is already
public API via AccessControlContext.getAccessControl().
Applies the codebase's existing "rethrow if already HttpException"
convention (from nestjs-crud's CQRS handlers) to the sites that missed
it, and closes two sites that destroyed the original error outright.

- CrudContextOverlay and the invitation accept handler now rethrow an
  already-typed exception instead of flattening everything into a
  generic one; the crud e2e assertions reflect the more specific
  errorCode/message that now surfaces.
- The four cache/otp/role DI resolvers thread originalError through
  their catch instead of a bare `catch {}` that dropped it.
- A hook class registered via @usehooks() but missing its @hook()
  decorator, or missing from providers entirely, now throws instead of
  silently never running.
- The two notification ports threw inside a void-discarded promise
  .catch(), which could crash the process via an unhandled rejection;
  they now resolve silently instead, preserving the deliberate
  enumeration-protection contract at the call site.
- One-line comments on the auth guards/strategy explain why they must
  keep collapsing every failure to one status (username/token
  enumeration), so they aren't mistaken for the pattern above.

Left alone on purpose: repo-permeator-factory.ts's RuntimeException
guard is correct for its layer and was not unified to HttpException.
The exception-logging blackout, default-500 message suppression, and
misconfiguration-vs-client-error classification are real but are
delivery/observability concerns outside this pass's boundary — tracked
in TODOs.md instead.
…er log-ready

RuntimeException.fault ('client' | 'usage' | 'internal') states who is at
fault for an exception, independent of httpStatus, so a peer logging module
can pick a log level without guessing from HTTP status — the current status
based heuristic mislabels both directions today (e.g. client errors that
default to 500, config errors that render 4xx). Classified across all 88
RuntimeException subclasses plus their direct-throw call-site overrides.

fault defaults to 'internal' (fail loud on anything unclassified), flows
through RuntimeExceptionOptions the same way httpStatus already does, and is
deliberately excluded from getResponse() — it's triage data for logging, not
part of the wire contract. No response body or HTTP behavior changes.

Adds a table-driven exception-fault.spec.ts per package as an anti-drift
check: a new exception class added without a row fails a test instead of
silently inheriting a default.

TODOs.md: removes the "exceptions are never logged" item — the readiness
gap it described is now closed; building the actual logging filter is a
peer module's concern, not tracked in this repo's backlog.
Of the 48 exception classes without an explicit httpStatus, most are either
family base classes correctly defaulting to 500, or usage/internal-fault
leaves where a hidden message on a 500 is the intended behavior. Only 4
client-fault exceptions were genuinely wrong: their message was unreachable
on the wire behind a status that gave the caller no way to act on it.

- CacheInvalidExpiredDateException -> 400, matching
  OtpInvalidExpirationDateException for the identical scenario
- InvitationAlreadyAcceptedException / InvitationRevokedException -> 409,
  matching TokenAlreadyRevokedException's "already revoked" precedent
- AuthRouterAuthenticationFailedException -> 401, matching every sibling
  *UnauthorizedException in the auth package

TODOs.md: removes the httpStatus-coverage item now that the real gap is
closed; the remaining implicit-500 cases are correct as-is.
CrudContextException isn't bimodal anymore — 995c0f7 already rethrows
HttpException before it can fall through to CrudContextException, so
malformed ?filter= traffic surfaces as CrudQueryParserException
(fault: 'client') and never reaches it. All 4 remaining throw sites are
genuine wiring/usage errors, matching its fault: 'internal' default.

A ConfigurationException family would also be a new, unprecedented
pattern here: every other usage-fault error in the repo is already its
own distinctly-named leaf class, greppable by name/errorCode, with
fault: 'usage' giving the field-level signal. Nothing actionable
remains under this item.
ConfigurableCrudBuilder's Path 1 (pre-decorated controller class passed
via `{ controller: { class } }`) silently skipped creating the adapter
provider when @CrudEntity/@CrudController metadata was missing, instead
of failing loudly. A copy-pasted controller with the decorator stripped
would build without error and only surface as a broken request at
runtime.

Path 3 (hybrid controller + operations) already throws for this exact
condition; Path 1 now does the same, reusing CrudDecoratorException
(fault: 'usage') rather than a plain Error, matching how the rest of the
package already reports decorator misuse. Path 3's throw is updated to
use the same exception class and to name the offending controller in
the message.

Checking adapter metadata eagerly would have been wrong: @CrudController
always defaults it, but @CrudEntity alone (without @CrudController) is a
valid lighter-weight usage that never sets it, so entity is the only
metadata whose absence is unambiguously a wiring bug.
VerifyNotificationPort/RecoveryNotificationPort dispatch a CQRS command
to send verify/recovery emails and fire-and-forget it: `.catch(() =>
undefined)` on every send method, deliberately, to avoid an unhandled
rejection crashing the process and to avoid branching on send success
(which would leak whether an email address exists). That left genuine
failures - a provider outage, a broken template - reaching no one.

On catch, both ports now publish a NotificationSendFailedEvent via
EventBus instead of swallowing the error. This reuses the existing
TokenIssuedEvent/TokenRevokedEvent pattern already in this package
rather than introducing a new extension mechanism - integrators
subscribe with @EventsHandler the same way they already do for token
events. The event carries ctx, email, the command class, and the
error, letting a subscriber log or alert without changing the
port's own silent, uniform response to the caller.
…vent work

Four Opus reviews (one per commit: 25f6e4b, bf4ef98, 2638fff, d200ba5) plus a
Fable review of the resulting plan found the mechanism work from this session sound,
but the sweeps that used it were not exhaustive. Several misses landed on the exact
defect they were meant to fix. This closes them out.

Classification and httpStatus fixes:
- assertUserId/RoleId/CacheId/InvitationId/InvitationCode: fault:'client' with no
  httpStatus meant these rendered 500 with the real message suppressed by
  getResponse()'s >=500 fallback - the same defect bf4ef98 set out to fix. Now 400
  with a safeMessage so the internal "got %s" detail stays server-side.
- InvitationNotAcceptedException: one class, one fault, covering both an internal
  wrap and "wrong/expired passcode" (client) - passcode guessing generated unbounded
  ERROR noise. Split by call site.
- AuthRouterAuthenticationFailedException: the guard wrapped *any* thrown error as
  client/401, so a provider outage produced zero ERROR logs. Now rethrows
  HttpException first (matching crud-context.overlay's existing pattern) and only
  classifies genuinely unexpected errors, as internal/500.
- IdentityUserRelationshipException: a dangling identity/user FK in our own data was
  client/404. Now internal/500.
- CrudContextException: 2 of 4 throw sites are wiring errors (missing
  @CrudEntity/@Crudoperation, missing ExecutionContext) that were internal by
  default; classified usage. The catch-all set BAD_REQUEST on what's by construction
  a server-side failure; now INTERNAL_SERVER_ERROR.
- InvitationUserUndefinedException: 2 of 3 call sites take a client-supplied email;
  classified client/400 with a safeMessage. The third (dangling userId) stays at the
  usage default.
- crud-serialize.interceptor.ts: a schema-validation failure was usage, contradicting
  its own "server bug, not a client error" comment. Now internal.
- 15 direct RuntimeException/FederationException throw sites across nestjs-repository,
  nestjs-repository-typeorm, and nestjs-core were never swept for fault at all
  (the original sweep covered *.exception.ts files and CrudException's call sites,
  not bare RuntimeException throws). All classified. toMilliseconds' fault is now a
  parameter, not a blanket default, since its two callers reach it via different
  paths - cache's default-substitution path is usage, otp's required-schema-field
  path is client.
- 6 classes placed `fault` after `...options` while excluding only `httpStatus` from
  the options type, so a caller's fault override type-checked and was silently
  discarded. Moved before the spread, matching the ~80 other classes.

Anti-drift specs: the 13 per-package exception-fault.spec.ts tables had complete
coverage but no discovery step - a class added without a row failed nothing, contrary
to what the original commit claimed. Added collectRuntimeExceptionClassNames
(nestjs-core/testing) which walks *.exception.ts files and checks the prototype chain,
and a discovery test per package. Verified it fails against a deliberately unlisted
dummy class before wiring it in.

Notification event (d200ba5): the .catch() handler returned eventBus.publish(...)
and was void'd, so a rejecting publisher (anything beyond the default in-memory one)
reintroduced the unhandled-rejection crash the original .catch(() => undefined) was
there to prevent. Wrapped in try/catch and terminated with its own .catch(). Swapped
the event's bare `error: unknown` for AuthenticationEmailException, which already
existed for exactly this purpose (fault:'internal', README-documented, thrown
nowhere) instead of pushing the instanceof-guessing problem onto subscribers.

CRUD Path 1 (2638fff): the stated justification for not checking the adapter was
wrong - CrudInit() (which resolves query/command classes) is applied only by
@CrudController and Path 3, never Path 1, so a @CrudEntity-only Path 1 controller
gets zero handlers and no adapter provider, and 500s on every request despite
booting clean. Path 1 now throws when both are empty. This is a breaking change for
any consumer relying on that silent (broken) fallback.

Docs: nestjs-invitation/cache/authentication READMEs updated for the status changes
above; added status-assertion specs for the three bf4ef98 changes (409/409/401)
that shipped without one.

Not restored: the TODOs.md logging-readiness item deleted by 25f6e4b. Its premise
(fault is available but nothing logs yet) is accurate, but it isn't actionable work
for this repo - it's a peer module's job - so it doesn't belong in the backlog.
roleCreateSchema and roleUpdateSchema used z.string().default('') for both
fields. Because CrudInitValidation's StandardSchemaValidationPipe transforms
(returns the parsed value), a partial PATCH body materialized the omitted
field as '' and Role.update() spread it straight into the aggregate's props,
persisting a blanked name/description on every partial update. PUT to a
non-existent id with {} silently created a role named ''.

This is a regression from a79bcd7 ("migrate to pure schema implementation"),
not preserved v7 behavior as TODOs.md assumed: verified against a real
class-transformer 0.5.1 install that excludeExtraneousValues: true left an
omitted `name` as undefined (not the property-initializer default), and
legacy RoleDto's `@IsString() name` (no `@IsOptional()`) rejected that with a
400. The property initializer was never actually reachable, so this change
mostly restores v7 behavior rather than breaking it.

- roleCreateSchema: name is now required and non-blank (`.trim().min(1)`);
  description keeps `.default('')` (legacy `@IsOptional()`). Also backs PUT
  and the create-batch schema, so PUT with {} to a nonexistent id now 400s
  instead of creating a blank-named role.
- roleUpdateSchema: both fields are now `.optional()` — a true partial. An
  omitted field is left untouched; a present-but-blank name is still
  rejected. RoleUpdatableInterface widened to Partial<Pick<...>> to match
  (conformsTo<Interface>() requires the schema's output to be assignable to
  it).
- Added e2e coverage for the exact defect (partial PATCH no longer wipes
  name) plus the missing-name 400s on POST/PUT, confirmed red against the
  old schemas before the fix landed.

Breaking: roleCreateSchema, roleUpdateSchema, RoleUpdatableInterface, and
roleCreateBatchSchema are public exports. POST/PUT without a name, or with a
blank/whitespace-only name, now 400 instead of succeeding.
… bodies

Per-operation api.body (description, examples, required) was silently
dropped whenever the operation ended up with a schema-based request body —
the normal case for every schema-based module (role, cache, user,
invitation). CrudApiBody({...api?.body}) was only ever called when the
operation had no local schema; even then, crud-init-api-body.decorator.ts
stripped that placeholder the moment a schema resolved from the metadata
hierarchy, discarding whatever api.body it carried.

Root cause: CrudApiBody broke the convention every other CrudApiX decorator
follows (CrudApiParam/CrudApiQuery/CrudApiResponse store metadata for a
CrudInitApiX class decorator to apply later) by calling @ApiBody()
immediately instead. That's why crud-init-api-body.decorator.ts needed an
awkward strip-then-rebuild step in the first place.

- CrudApiBody is now a CrudMetadata.createDecorator metadata store, matching
  its siblings. crud-init-api-body.decorator.ts is now the sole place
  @ApiBody() is ever called, merging the stored api.body options into the
  ApiBody() it builds from the resolved schema (standardSchema always wins
  over a caller-supplied schema/type; required defaults true when unset).
- The strip-before-append block stays, retargeted: it's no longer stripping
  CrudApiBody's old placeholder (that concept is gone), it's keeping
  CrudInitApiBody's own write idempotent across CrudInit() re-runs. This
  matters because CrudInit() genuinely runs twice on the hybrid-builder path
  (configurable-crud.builder.ts re-runs it after augmenting an
  already-@CrudController-decorated class), ApiBody()'s own metadata storage
  is append-only, and Swagger's document-build dedup keeps the FIRST body
  entry among duplicates. Deleting the block would have silently made a
  hybrid override's schema lose to the controller-level default in the
  generated docs while runtime validation kept using the override —
  confirmed against crud.module.forfeature.spec.ts's CompanyControllerD,
  which exercises exactly this shape with no swagger assertion to catch it.
- Preserved the schemaless-operation behavior exactly: api.body (or a bare
  {}) still renders swagger's own `{ type: 'string' }` default body when no
  schema resolves anywhere, matching today's behavior bit for bit.

Breaking: CrudApiBody is a public export whose behavior changes from
"applies @ApiBody() immediately" to "stores options for CrudInitApiBody to
apply". Checked every usage in the repo — it's only ever called by the 4
operation decorators, and every real controller is @CrudController-decorated
(which always runs CrudInit() -> CrudInitApiBody()), so this is safe
repo-wide, but would break a hand-authored controller that called
CrudApiBody() standalone outside that.

Known, unfixed limitations (documented, not addressed here): api.body
type-checks but is silently ignored on List/Read/Delete/SoftDelete/Restore
(only the 4 write operations read it); the hybrid-builder augment path drops
api.body on a method that already carries an operation decorator, same as
every other api.* option; a @CrudBody-pinned schema that differs from the
operation's request.body gets api.body's description/examples attached to
whichever schema resolves.

Adds regression coverage in swagger-request-body.spec.ts for both loss
paths, the schemaless case, and the re-run idempotency case — the last one
caught during review as the one existing test suite (crud.module.forfeature
.spec.ts) that would have silently regressed under a naive
strip-block-deletion fix.
Five parallel Fable audits (one per package group) checked every v8
README against the 18 commits from 84e88ad through f3ef70e, verifying
each claim directly against source before it counted. 24 findings
across 12 of the 13 v8 READMEs, all independently re-verified.

Dominant issue: cc9ea72 moved @nestjs/* from dependencies to
peerDependencies in all 13 packages, but only 7 READMEs were touched
in that window (mostly a few lines as a side effect). Every v8 README
was telling consumers to run an install that can't bootstrap. Fixed
install snippets and dependency/peer tables package-by-package against
the real package.json, calling out @nestjs/cqrs as optional-in-name-
but-required-in-practice, and @nestjs/swagger as required only for
core/crud/authentication.

Also fixed, verified against source:
- repository-typeorm: optimistic locking's actual effect on
  update/replace (merges into a fresh re-read row, not the caller's
  entity; a caught OptimisticLockException still dooms the enclosing
  transaction) — the README claimed "behaves identically"
- repository: hook-pipeline passthrough widened to any RuntimeException,
  not just RepositoryQueryException; required isVersion field on
  RepositoryColumnMetadataInterface undocumented
- core: fault classification, RuntimeExceptionFault export, two new
  hook exceptions, five new type guards, and
  collectRuntimeExceptionClassNames were all missing; @nestjs/common/
  core/swagger were still listed as bundled dependencies
- authentication: AuthRouterAuthenticationFailedException's documented
  401 is unreachable (guard always overrides to 500/internal); the
  guard's HttpException passthrough and NotificationSendFailedEvent
  had no coverage
- crud: api.body's "schema has no effect" claim didn't cover the
  schemaless fallback branch; update/replace's new 409 conflict path
  was undocumented
- otp: the new OtpPolicy (fronting settings access across all command/
  query handlers) had no README section at all

Also fixed two pre-existing (out-of-range) errors caught in passing:
crud's validation section contradicted itself on whether a
controller-level request.body default is used for validation (it is —
crud-init-validation.decorator.ts resolves method → class), and core's
ExceptionInterface table row listed fields that actually live on
RuntimeExceptionInterface.

Also repointed the NestJS version badge in all 13 v8 READMEs, broken
independently of this range (pointed at conceptadev/rockets, which has
no packages/*/package.json on its default branch) — empirically
verified against the live shields.io endpoint and repointed at
conceptadev/nestjs-modules/peer/@nestjs/common/feature/version-8,
the only form of four tested that resolves today. Will need the branch
segment dropped once version-8 merges to main.

yarn lint clean; yarn test:all holds baseline exactly (2481 unit,
372 e2e) — docs-only change.
Every EventContextHost now carries a framework-owned correlationId,
causationId, and recordedAt header, auto-derived from the ambient
context via createEventContext instead of being left for callers to
populate ad hoc. A new CorrelationContextOverlay (registered by
CoreModule) seeds the pair from the inbound x-correlation-id header,
or a self-correlated pair is synthesized when none is traceable.

The framework-agnostic core (causal-context/) is kept import-clean
from @nestjs/*/@concepta/*, now enforced by an ESLint boundary rule,
so the resolver/factory logic is unit-testable without NestJS and
stays extraction-ready if a second consumer ever needs it.

BREAKING CHANGE: EventContextHost's header generic is now constrained
to require correlationId/causationId/recordedAt; direct construction
with an incomplete header object no longer compiles. Use
createEventContext(ctx, extraHeaders, metadata) instead.
CreateUserHandler saved the user row before CreateUserCredentialCommand
validated password strength, so a weak password left a persisted,
credential-less account behind unless the surrounding transaction
factory happened to be registered — invisible on any adapter that
registers none. UserPasswordPort.create() (strength check + hash) now
runs before the user is written; CreateUserCredentialCommand and
UserCredentialsService.setPassword accept the resulting
PasswordStorageInterface directly, via the existing isPasswordStorage
guard, so the password is never hashed twice.

Fixes #469.

BREAKING CHANGE: CreateUserCredentialCommand.password and
UserCredentialsService.setPassword's password parameter now accept
string | PasswordStorageInterface. Existing string callers are
unaffected; anything subclassing UserCredentialsService or
re-declaring the command's shape must widen to match.
TransactionScope.run() silently executes without a transaction when
the factory registry is empty — writes commit non-atomically and
onRollback callbacks never fire, with nothing distinguishing that from
a real commit. Warn once, at onApplicationBootstrap when the count is
final, and again from the first run() as a fallback in case a custom
logger transport wasn't attached yet when the boot-time warning fired.

Related to #469.
Domain services generating their own event contexts shipped in
14a1189 — createEventContext is now used at all 29 production call
sites across 7 packages.
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.

2 participants