Skip to content

Latest commit

 

History

History
244 lines (179 loc) · 5.84 KB

README.md

File metadata and controls

244 lines (179 loc) · 5.84 KB

Interactor

CI Maintainability Test Coverage

Typescript implementation of collectiveidea/interactor Ruby gem.

Installation

npm i @batuhanw/interactor

or

yarn add @batuhanw/interactor

Getting Started

Interactors are simple, single-purpose objects used to encapsulate your application's business logic. Each interactor represents one thing your application does.

Interactor

To define an interactor, simply create a class that extends from Interactor and add call() instance method.

class CreateOrder extends Interactor {
  async call() {
    // Do something
  }
}

An interactor is used by invoking its static call() method.

CreateOrder.call();

When an Interactor's static call() method is invoked, it builds an instance of Context from given object.

CreateOrder.call({ params: { sku: 'sku', userId: 1 } });

And Context is accessible within the interactor's call() instance method.

class CreateOrder extends Interactor {
  async call() {
    const { params } = this.context;

    params.sku; // => 'sku'
    params.userId; // => 1
  }
}

An interactor can also mutate its Context.

class CreateOrder extends Interactor {
  async call() {
    const { params } = this.context;

    this.context.order = await OrderService.create(params);

    this.context.order; // => Order { id: 1, sku: 'sku', userId: 1 }
  }
}

When completed, interactor return its Context under result key of the object.

const { result } = await CreateOrder.call({ params: { sku: 'sku', userId: 1 } });

result.params; // { sku: 'sku', userId: 1 }
result.order; // Order { id: 1, sku: 'sku', userId: 1 }

If something goes wrong in your interactor, you can mark context as failed.

class CreateOrder extends Interactor {
  async call() {
    const { params } = this.context;

    try {
      this.context.order = await OrderService.create(params);
    } catch (error) {
      this.context.fail();
    }
  }
}

If you pass an object to the fail() method, it also updates the context. The followings are equivalent:

this.context.error = 'invalid SKU';
this.context.fail();

or

this.context.fail({ error: 'invalid SKU' });

You can ask a context if it's a failure.

const { result } = CreateOrder.call({ sku: 'sku', userId: 1 });

result.isFailure(); // => false
result.error; // => 'invalid SKU'

or if it's a success

const { result } = CreateOrder.call({ sku: 'sku', userId: 1 });

result.isSuccess(); // => true
result.order; // => Order { id: 1, sku: 'sku', userId: 1 }

When Context is failed with this.context.fail({ .. }) method, it throws InteractorFailure exception.

By default, InteractorFailure exception is swallowed by interactor.

It's possible to change this behaviour.

Calling the Interactor with catchInteractorFailure: false will throw InteractorFailure error. This error has context field that gets populated with current context at the time of failure.

try {
  const { result } = await CreateOrder.call(
    { params: { sku: 'sku', userId: 1 } },
    { catchInteractorFailure: false },
  );
} catch (e) {
  if (e instanceof InteractorFailure) {
    e.context; // Context { params: { sku: 1, userId: 1 }, error: 'invalid SKU' }
  }
}

Organizer

Organizer is a variation of interactor. It can run multiple interactors in order.

class PlaceOrder extends Organizer {
  Interactors = [CreateOrder, ReserveProduct];
}

And these interactors share the same context.

class CreateOrder extends Interactor {
  async call() {
    this.context.order = await OrderService.create(this.context.params);
  }
}

class ReserveProduct extends Interactor {
  async call() {
    const { order } = this.context;

    this.context.reservation = await ReservationService.create({ order });
  }
}

If any of the interactors fails, Organizer calls rollback() instance method on successfully called interactors in reverse order. Organizer won't call rollback() on the failed interactor itself.

class PlaceOrder extends Organizer {
  Interactors = [CreateOrder, ReserveProduct, ChargeCustomer];
}

class CreateOrder extends Interactor {
  // Called 1st
  async call() {
    this.context.order = await OrderService.create(this.context.params);
  }

  // Called 5th
  async rollback() {
    const { order } = this.context;

    await OrderService.markAsFailed({ order });
  }
}

class ReserveProduct extends Interactor {
  // Called 2nd
  async call() {
    const { order } = this.context;

    this.context.reservation = await ReservationService.create({ order });
  }

  // Called 4th
  async rollback() {
    const { id } = this.context.reservation;

    await ReservationService.destroy(id);
  }
}

class ChargeCustomer extends Interactor {
  // # Called 3rd
  async call() {
    const { user, order } = this.context;

    const payment = await PaymentService.charge({ user, order });

    this.context.fail({ error: 'payment failed' });
  }
}

It's also possible to organize other organizers.

class PlaceOrder extends Organizer {
  Interactors = [
    CreateOrder, // => CreateOrder interactor
    ReserveProduct, // ReserveProduct organizer => [ValidateStock, CreateReservation]
    SendNotifications, // SendNotifications organizer => [SendEmail, SendPush, SendSMS]
  ];
}

Check ./examples directory for more.