Feature/version 8 - #463
Open
MrMaz wants to merge 138 commits into
Open
Conversation
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.
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.
No description provided.