Skip to content

iamolegga/nestjs-saga

Repository files navigation

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.

About

Implementation of saga pattern for NestJS

Topics

Resources

License

Stars

Watchers

Forks

Contributors 5