Skip to content

feat: add transactional support - #2864

Open
minottic wants to merge 4 commits into
masterfrom
trans_attach
Open

feat: add transactional support#2864
minottic wants to merge 4 commits into
masterfrom
trans_attach

Conversation

@minottic

@minottic minottic commented Jul 31, 2026

Copy link
Copy Markdown
Member

Description

Introduces transactions support in nestJs. Since transactions work in mongo only when replicaSets is enabled, the methods fallback to non-transactions when replicaset is not enabled.

It exposes three helpers:

  1. @Transactional to use as a decorator it attaches all downstream mongo calls to the same transaction. Useful for example for updates that need to fetch from DB the original value
  2. same as @Transactional but more explicit
mongoTransactionService.run(async () => {
   const [dataset] = await this.datasetModel.create([dto]);
   return dataset;
   });
  1. to control what session to attach to
this.mongoTransactionService.run(
    async (session) => this.datasetModel.create([dto], { session }),
     { ambient: false },
   );

Using 1 and 2 automatically attaches all downstream mongo operations to the same mongo session (transaction). This happens by adding the session to the asyncLocalStorage and loading it with pre hooks in mongo. Independent calls of 1 and 2 (non nested) will have independent sessions/transactions since they are scoped to the run() closure.

When nesting multiple 1s or 2s together the outermost session is used. This prevents confusion when decorating a function with @Transactional that calls another decorated (with @Transactional) function. For example:

return this.mongoTransactionService.run(async () => {
  // here session1 gets created
  await this.datasetModel.create([dto]);
  return this.mongoTransactionService.run(async () =>
    this.datablockModel.insertMany(blocks), // -> this will reuse the session1
  );
});

Since the session of the transaction is closed when the inner function emits, non awaited functions can "emit" before finish and make the session close before the transaction is complete. To guard against this it's good to enforce awaited promises by linting them, covered by #2863 since it's good practice anyway.

Motivation

Transactions are fundamental when disjoint operations in the DB must have consistent data, for example a multi-stage find and update or a block revert on failure. This PR should make applying them easy

Tests included

  • Included for each change/fix?
  • Passing?

Documentation

  • swagger documentation updated (required for API changes)
  • official documentation updated

official documentation info

Summary by Sourcery

Introduce application-wide MongoDB transaction support integrated with NestJS and Mongoose, including a decorator-based API and ambient session handling.

New Features:

  • Add MongoTransactionService to run operations within MongoDB transactions with automatic fallback when transactions are unsupported.
  • Add @transactional decorator to transparently wrap service methods in transactions and reuse existing ambient sessions.
  • Introduce a global MongoTransactionModule to expose a singleton MongoTransactionService across the app.
  • Add a Mongoose session plugin that attaches the current ambient session to queries, aggregates, and document saves via pre hooks.

Enhancements:

  • Register MongoTransactionModule in AppModule so transaction support is available application-wide.

Tests:

  • Add unit tests for MongoTransactionService transaction behavior, fallback handling, and nesting semantics.
  • Add unit tests for the Mongoose session plugin to verify ambient session propagation across queries, aggregates, and documents.
  • Add unit tests for the async session context utilities to ensure correct scoping and non-leakage of sessions.
  • Add unit tests for the @transactional decorator, including nested transactional method behavior.

@minottic
minottic requested a review from a team as a code owner July 31, 2026 16:59

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • The MongoTransactionModule/getMongoTransactionService global singleton pattern introduces hidden coupling and ordering constraints (e.g. in tests or multiple Nest apps); consider refactoring @Transactional into an interceptor/mixin that uses Nest DI directly instead of relying on mutable module-level state.
  • In MongoTransactionService.run, when transactionsSupported === false you bypass the try/finally and just call fn(undefined); if fn later starts using sessions conditionally, you may want to centralize the fn invocation inside a single try block for both transactional and fallback paths to keep behavior (e.g. logging, instrumentation, error handling) consistent.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `MongoTransactionModule`/`getMongoTransactionService` global singleton pattern introduces hidden coupling and ordering constraints (e.g. in tests or multiple Nest apps); consider refactoring `@Transactional` into an interceptor/mixin that uses Nest DI directly instead of relying on mutable module-level state.
- In `MongoTransactionService.run`, when `transactionsSupported === false` you bypass the `try/finally` and just call `fn(undefined)`; if `fn` later starts using sessions conditionally, you may want to centralize the `fn` invocation inside a single `try` block for both transactional and fallback paths to keep behavior (e.g. logging, instrumentation, error handling) consistent.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant