Implementation of saga pattern for NestJS


Keywords
nestjs, nest, saga
License
MIT
Install
npm install nestjs-saga@0.1.3

Documentation

nestjs-saga

npm npm GitHub branch checks state Known Vulnerabilities Libraries.io Dependabot

Basic implementation of saga pattern for NestJS (do not confuse it with the built-in sagas).

This module is not too much related to microservices sagas but could be used as a base to implement it.

Highly inspired by node-sagas but rewritten a bit for more convenient usage with NestJS.

installation

npm i nestjs-saga @nestjs/cqrs

usage

define

import { Builder, Saga } from 'nestjs-saga';

class FooSagaCommand {
  constructor(
    public bar: string,
    public baz: number,
  ) {}
}

class FooSagaResult {
  // ...
}

@Saga(FooSagaCommand)
class FooSaga {
  // Define `saga` field using Builder<FooSagaCommand> or if you want to return
  // some value use: Builder<FooSagaCommand, FooSagaResult>
  saga = new Builder<FooSagaCommand>()

    // Add a step with the name, invocation and compensation functions
    .step('do something')
    .invoke(this.step1)
    .withCompensation(this.step1Compensation)

    // Add another one, name and compensation could be omitted
    .step()
    .invoke(this.step2)

    // If builder with result type is used (Builder<Command, Result>) then it's
    // required to add last `return` step, final `build` step will be available
    // only after this one. If no result type provided in Builder then this
    // method won't be available in types and saga will return `undefined`
    .return(this.buildResult)

    // After all steps `build` should be called
    .build();

  // Each invocation and compensation methods are called with the command as an
  // argument
  step1(cmd: FooSagaCommand) {

    // Each time saga is called as a new instance, so it's safe to save it's
    // state in own fields
    this.step1Result = 42;
  }

  // If step throws error then compensation chain is started in a reverse order:
  // step1 -> step2 -> step3(X) -> compensation2 -> compensation1
  step2(cmd: FooSagaCommand) {
    if (this.step1Result != 42) throw new Error('oh no!');
  }

  // After all compensations are done `SagaInvocationError` is thrown. It will
  // wrap original error which can be accessed by `originalError` field
  step1Compensation(cmd: FooSagaCommand) {

    // If one of compensations throws error then compensations chain is stopped
    // and `SagaCompensationError` is thrown. It will wrap original error which
    // can be accessed by `originalError` field
    if (this.step1Result != 42) throw new Error('oh no!');
  }

  // If saga should return some result pass it's type to the Builder generic and
  // use `return` method in the build chain with a callback that returns this
  // class or type
  buildResult(cmd: FooSagaCommand): Result | Promise<Result> {
    return new Result();
  }
}

register

import { CqrsModule } from '@nestjs/cqrs';
import { SagaModule } from 'nestjs-saga';

@Module({
  imports: [
    CqrsModule,
    SagaModule.register({
      imports: [...], // optional
      providers: [...], // optional
      sagas: [FooSaga, BarSaga, BazSaga], // required
    }),
  ],
})
class AppModule {}

run

import { CommandBus } from '@nestjs/cqrs';
import { SagaInvocationError, SagaCompensationError } from 'nestjs-saga';

class AnyServiceOrController {
  constructor(private commandBus: CommandBus) {}

  someMethod() {
    try {
      // If saga defined with the result type, then result will be passed,
      // otherwise it's `undefined`
      const result = await this.commandBus.execute(new FooSagaCommand(...args));

    } catch (e) {
      if (e instanceof SagaInvocationError) {
        // Saga failed but all compensations succeeded.
        e.originalError // could be used to get access to original error
        e.step // can be used to understand which step failed

      } else if (e instanceof SagaCompensationError) {
        // Saga failed and one of compensations failed.
        e.originalError // could be used to get access to original error
        e.step // can be used to understand which step compensation failed
      }
    }
  }
}

Do you use this library?
Don't be shy to give it a star! ★

Also if you are into NestJS you might be interested in one of my other NestJS libs.