Repository navigation
Conversation
…egate presets Records that must never change can now be declared append-only end to end. nestjs-core: DomainAggregate is now a base with no persistence opinions (id, props, events). MutableAggregate carries what DomainAggregate did (version, audit dates, stamps); AppendOnlyAggregate carries only dateCreated. mutableAggregateSchema and appendOnlyAggregateSchema pair with them, and CorrectableInterface types a record that is an original or a correction with a reason. DomainMapper drops its redundant Props parameter. nestjs-repository: writePolicy on forFeature() entries. An append-only entity refuses update, replace, upsert, delete, deleteMany, softDelete and restore with AppendOnlyViolationException (409); create stays open. Startup refuses a soft-delete column on an append-only entity, and requireWritePolicyDeclaration makes a declaration mandatory. resolveCorrectionChain follows corrections to the record in effect. nestjs-repository-typeorm: AppendOnlyPostgresEntity and AppendOnlySqliteEntity base entities. BREAKING CHANGE: extend MutableAggregate where you extended DomainAggregate; DomainMapper<Entity, Props, Aggregate> is now DomainMapper<Entity, Aggregate>; domainAggregateSchema is now mutableAggregateSchema.
An identity referred to its user through a relation property with no column of its own, the one shipped entity whose foreign key could only be written as a relation object. It now holds the user's id: the domain, schemas and service use userId, the base entities store it in a userId column, and the mapper copies only the identity's own fields so a loaded relation never reaches a write. BREAKING CHANGE: IdentityInterface.user (ReferenceIdInterface) is now userId (string), and Identity.user is now Identity.userId. The abstract user property is gone from IdentitySqliteEntity and IdentityPostgresEntity, which now declare a userId column. Rename your join column to userId (or add the column), keep a user relation only if you want the foreign key constraint, declared with @joincolumn({ name: 'userId' }), and read identity.userId instead of identity.user.id.
… and registration
- Writes carry the entity's own columns only; the stored row a write names is reduced to its own
columns.
- Reads name own columns and declared, joined relations; results holding anything else throw.
allowEagerRelations is the one opt-in. Joins into row-scoped rows are refused.
- count is decided by where, join and withDeleted; text operators match literally; database errors
surface as RepositoryDatabaseException.
- Drivers report own columns, relation target columns and URNs; the TypeORM driver adds nothing
on its own.
- Every entity is registered once in RepositoryModule.forRoot and exposed with forFeature(keys);
startup checks throw while the application is created.
- Row scope, hook and write policy tests move into nestjs-repository.
BREAKING CHANGE: register entities in RepositoryModule.forRoot({ drivers: [{ driver, entities }] })
and import them with forFeature(keys); drivers implement register(entities) and adapters are bound
with bind() instead of the set* methods. Write payloads refuse relations (send the foreign key
column) and non-columns; transform, merge, prepare, force, joinType and the Join helpers are
removed. Reads naming non-columns, unjoined relation filters or sorts, or joins into row-scoped
rows return 400. Drivers must report targetColumns, urn, targetUrn and through.urn, implement
doCount and describeDatabaseError, and return only what the call asked for. Text conditions use
WhereTextOperator. Eager relations load only with allowEagerRelations.
@usehooks stored any class it was given. A class without the class-level @hook() decorator got no subsystem type, so every operation filtered it out and it silently never ran, although the core README promised a hard failure. @usehooks now throws HookNotDecoratedException as soon as the decorated class is defined, whether the hook is passed bare or as { hook, type }. BREAKING CHANGE: a class passed to @usehooks() without @hook() (or a subsystem decorator such as @RepoHook()) now throws HookNotDecoratedException at class definition instead of being ignored. Add the missing decorator.
The response filter returned after the first grant on a route, even when the user's roles satisfied none of it, so a route whose matching grant came second was returned unfiltered. It now tries each grant in order and applies the first one granted, as the guard does. Single-grant routes are unchanged.
- crud: @RepoHook takes a spec; @AfterRead skips count and findAndCount; add the count, upsert and deleteMany hook rows - core: @usehooks refuses undecorated hooks at class definition; HookWithSpec has no spec; Operation is an enum with tuple and union helpers - user: no passwordSalt; passwordHash is not nullable; entities defaults - user, role, invitation: nestjs-crud is an optional peer - role: CrudContextOverlay; Context Overlay in the table of contents - invitation: constraints is PlainLiteralObject | null - otp: expiresIn uses ms strings, bare numbers are milliseconds; four handlers use OtpPolicy; resolveTypeService takes no override - cache, otp, user: register @entity() subclasses in RepositoryModule.forRoot - cache, otp, role, user, authentication, federated: drop the global option, which callers cannot pass - authentication: the invalid-credentials message on the wire - access-control: how the guard matches grants; `any` grants still run query services and attribute filters
The core modules never exported the custom-repository token, so forFeature() always built the default repository. The token is now exported and injected as required, and each override is built once per forFeature key around that key's repository. Subclasses can reach the mapper, which is now protected. BREAKING CHANGE: repositories.cache, repositories.otp, repositories.role and repositories.roleAssignment must be classes constructed like CacheRepository, OtpRepository, RoleRepository or RoleAssignmentRepository; extend the matching class.
…ns subscribers The optimistic-lock check is an UPDATE, so TypeORM stamps @UpdateDateColumn on it and broadcasts it to subscribers. The stamp can't cause a lost update or a false conflict, so it is documented rather than worked around, and the TODO is closed.
… WebSocket Overlays, @ctx(), @authuser() and TransactionalRunner read the HTTP request on every transport, so on WebSocket the context stuck to the socket (one message's route hooks applied to the next) and on RPC a primitive payload threw a TypeError. findAppContext/requireAppContext resolve the context from the ExecutionContext: the request on HTTP; on RPC (preview) a host per call, keyed by the transport's context object and the handler; none on WebSocket or gRPC call-stream handlers, where @ctx(), @authuser() and @transactional() throw AppContextUnavailableException. Overlays declare the transports they attach on (default http) and use this.appContext(context). Hooks and the otp/cache/role namespaces attach on RPC; correlation, auth user and crud stay HTTP-only.
…ly or not at all - A relation marked federated in forRoot() is read through the related entity's own repository, found by entity name, after hooks and row scope, on find, findOne, count and findAndCount. - Joins attach to-one, to-many and many-to-many (through a registered junction) relations; a filtered join attaches only the related rows that matched. - A filter on a federated relation becomes a condition on the root's own column, so the root's store pages, sorts and counts the read: pages and totals are exact. - federation.maxKeys (default 1000) bounds every key set: the root side or the related side is used, whichever fits; a filter too broad on both sides is refused. - Related reads run under the caller's ctx as it arrived, and are refused if a hook redefined the row scope on it. - Startup checks each federated relation: its target is registered exactly once, its columns match the target repository's, both sides have the column it is read by, the junction is registered with its key columns, and distinctFilter names a target column. - Removes the relation-first orchestrator, its strategies and its mock-based tests, replaced by tests against real adapters. BREAKING CHANGE: a read is refused with a 400 FederationException when it sorts by a federated relation, uses isNull on one, combines one with anything else under OR, joins one without selecting the column it is read by, or needs more keys than federation.maxKeys. find, findOne and count now read federated relations, not only findAndCount, and a filtered join attaches only the matching related rows. Startup refuses a federated relation whose target is unregistered or registered more than once, whose columns differ from its repository's, or that names a missing key column; allowEagerRelations alongside any federated relation; a relations entry naming a relation the entity lacks; and distinctFilter without federated: true. RepositoryRelationMetadataInterface loses federated and distinctFilter, and RepositoryBindingInterface.federationOrchestrator becomes federation. The TypeORM driver no longer takes relationsConfig. The relation-first strategy and the FEDERATION_* constants are removed. nestjs-crud no longer exports CrudResponseMetrics, and paginated responses lose metrics.
- RowScopeBase no longer re-compares the tenant columns of the row its lookup returns; the lookup's own WHERE decides. - upsert reads the stored row once, unscoped, and refuses a soft-deleted row after row scope runs, so another tenant's soft-deleted row is a 404. - scopeWrite's unhandled-operation branch is a compile-time check. - Trim comments that repeated the README or narrated design history.
A relation marked `cascade: true` in forRoot() is written with its entity: each related row goes through its own repository (hooks, own-column checks, row scope), with its foreign key set to the entity's key. - create/createMany create related rows after the entity. - update creates and updates the rows a payload lists and keeps the rest; replace/upsert also delete the rows a listed relation leaves out. - delete/deleteMany/softDelete/restore pass to related rows after the entity's row scope check and before the entity is written. - A to-one names its one stored row; null deletes it. - Startup refuses cascade on an owning to-one, many-to-many, a federated relation, an unregistered target, a composite-key parent, a soft-deleted parent over rows without a delete date, a cycle, or append-only rows under a mutable parent. - The repository never opens a transaction: a cascade is atomic only inside the implementer's. - New CascadeWriteException (400) for a malformed cascade payload.
- params.scope is a plain copy of the RowScopeCtx overlay's own values, so a key the overlay lacks no longer reads through to the context. - The repository hooks example no longer shows tenancy: hooks cover only the operations they decorate. Tenant isolation belongs to Row Scope. - README notes that cascades write related rows through their own repository, and that writes bypassing the repository bypass row scope.
…them
BREAKING CHANGE: RowScopeBase takes no repository:
`RowScopeBase<Entity, Scope>(repo, { scopeKey, column, label })` becomes
`RowScopeBase<Scope>({ scope: ['tenantId'], joinable? })`; `{ column, key }` maps a column to a
differently named RowScopeCtx key. `resolveScope` is no longer overridable, and `label`,
`RowScopeBaseRepository` and `RowScopeValues` are removed. scopeWrite params gain `repository`.
RowScopeInterface gains optional `scopeColumns` and `joinable`; joins into scoped rows are
allowed when both resolvers are joinable on the same columns and keys and the join includes
them. Every scoped entity must have a primary key, and each scope column must exist on it
(checked at startup). Relation metadata gains optional `joinOn`. Join refusals read
`Cannot join "x" of Entity`.
…rced columns RowScopeBase takes a relax option: a pure function of the caller's scope returning the scope columns that caller is not held to on reads. Reads filter only the columns left; relaxing every column reads all rows, while a resolver with no scope columns still reads nothing. Writes are never relaxed: every write is pre-checked and stamped with the caller's full scope. RowScopeInterface gains an optional relaxedColumns(scope). The join check between scoped entities is now made per caller on enforced columns: both resolvers joinable, every column the target enforces is enforced by the root with the same key, and the join matches each to itself. A target with no scope columns is never joinable into. Cascades read related rows as the caller, so a relaxed caller is refused (404) when a cascade reaches rows they may read but not write; documented.
TypeORM 1.x names an entity manager's data source `dataSource` and keeps `connection` as a deprecated alias; 0.3 has only `connection`, so reading `manager.dataSource` broke the 0.3 build. A small helper typed as either shape narrows with `in`, with no cast and no deprecated access on 1.x.
…eORM's column order A relation's `on` pair was the first join column TypeORM listed. TypeORM 1.x lists the column referencing the primary key first; 0.3 keeps declaration order, so a foreign key declared as `(tenantId, orderId)` reported `on` as `tenantId`. The pair whose referenced column is primary is now `on` and leads `joinOn`, whatever order the columns are listed in.
The fixture drew keys from about 30 faker job areas with a fixed type and assignee, so seeding two rows hit the unique key now and then.
An entity registered with activity: { store, values?, omit? } gets one
record per stored row each write changes, written through the store's own
repository with the caller's ctx, so it joins any open transaction.
Startup refuses composite keys, an unregistered, recorded or
non-append-only store, a store missing activity columns, and an
unscoped store for a scoped entity.
createMany and deleteMany now refuse a driver result without one row per
input, for every entity.
Revoking an invitation cleared the user's OTPs from an event listener after commit, on a fresh context. A lost event, or a listener failure, left a revoked invitation's passcode working. revokeByUserId now clears the OTPs once, in the revoke's own transaction, and InvitationRevokedListener is removed. accept() revokes sibling invitations through the same path, so the clear is atomic with the acceptance too.
The no-factory warning and its TSDoc said onRollback callbacks never fire. They do: a throwing operation marks the scope failed and settle() flushes the rollback callbacks, factory or not. What is lost is atomicity, so writes made before the throw stay while onRollback runs. Reword both, and pin the behaviour with a test.
Add the optional/outbox entry point to nestjs-repository. DurableMessaging
publishes @nestjs/outbox messages and records inbox deliveries in the
transaction a repository registration's writes use, so a message commits or
rolls back with the writes beside it and a redelivered message runs its work
once.
DurableMessagingModule.forRoot({ key, driver, outbox }) sets up OutboxModule,
asks the registration's driver for a store on its datasource through the new
DurableDriverInterface port (bind(repository) -> transaction key + store),
and registers that store before the outbox locks its registry. Implementers
never build a store or touch the native connection. Startup refuses an
unregistered key or a registration its driver cannot carry
(UnsupportedDurableDriverException), and a second forRoot() in one
application.
@nestjs/outbox is an optional peer dependency, imported only under
src/durable/ (lint guard added). Tests run against InMemoryOutboxStore with
a transaction fixture the store joins, so rollback cases are not vacuous.
Add the optional/outbox entry point to nestjs-repository-typeorm. TypeOrmDurable implements the durable driver port: it builds the @nestjs/outbox store on the registration's own data source, PostgresOutboxStore for postgres and MySqlOutboxStore for mysql, and refuses any other database (MariaDB included), a non-TypeORM repository, or one built without a transaction with UnsupportedDurableDriverException. The data source is resolved through ModuleRef from the TypeORM module's own provider, so the repository never hands it out: TypeOrmRepository gains only string getters, transactionKey and dataSourceName. @nestjs/outbox is an optional peer, and the lint guard now keeps it out of the main entry of the driver and nestjs-crud too, while they stay free to import typeorm. pg and mysql2 are pinned dev dependencies so the tests can build unconnected data sources of each dialect. The README gains the durable messaging recipe.
This branch has not been deployed
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.