Typescript implementation of collectiveidea/interactor Ruby gem.
npm i @batuhanw/interactoror
yarn add @batuhanw/interactorInteractors are simple, single-purpose objects used to encapsulate your application's business logic. Each interactor represents one thing your application does.
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 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.