Interactor pattern implementation, inspired by Ruby gem interactor.
npm i interactor-organizer
import { Interactor } from 'interactor-organizer';
class DoSomething extends Interactor {
async after() {
console.log('after');
}
async before() {
console.log('before');
}
// Your business logic goes here
async perform() {
console.log('perform', this.context);
try {
this.context.bar = 'baz';
} catch (error) {
this.fail({ error });
}
}
}
async function main() {
// Perform the interactor
const interactor = await DoSomething.perform({ foo: 'bar' });
console.log(interactor.failure, interactor.success, interactor.context);
}
main();
// output
/**
before
perform { foo: 'bar' }
after
false true { foo: 'bar', bar: 'baz' }
*/
Every interactor has after
, before
, fail
, perform
and rollback
methods, they are very similar to the Ruby gem methods, the only "new" method is perform
(which is used here instead of call
).
There are two classes of interactors:
Interactor
SafeInteractor
The only difference between them is that SafeInteractor
will never reject, instead, it calls fail({ error })
, while Interactor
will reject unless you catch and handle errors yourself.
constructor(context?: any)
Anything you want to pass to the interactor or return from it should be stored in context
. Expected an object, default {}
.
after(): Promise<any>
Is called after perform
only if the interactor didn't fail
.
before(): Promise<any>
Is always called before perform
.
fail(context?: any): void
If something went wrong use this method. It sets the interactor's property failure
to true
(which is also used by Organizers).
context
is appended to the current context. Expected an object.
perform(): Promise<any>
Your business logic goes here. Under the hood, this method is modified so that it calls the after
and before
hooks.
rollback(): Promise<any>
This method is only used by Organizers to allow successfully resolved interactors in the chain to undo the changes made by perform
.
static perform(context?: any): Promise<Interactor>
A shortcut to the instance method.
context: any
Current context. An object.
failure: boolean
Indicates if the interactor failed.
success: boolean
The opposite of failure
.
Organizers sequentially perform
interactors, if any interactor in the chain fails all the previous interactors will rollback
(from the last resolved to the first). If any rollback
rejects the organizer will reject as well (any further interactors won't rollback
)!
Interactors example:
import { Interactor } from "interactor-organizer";
class PlaceOrder extends Interactor {
get order() {
return this.context.order;
}
get user() {
return this.context.user;
}
async perform() {
this.order.user = { _id: this.user._id };
return client.db().collection('orders').insertOne(this.order)
.then((result) => {
this.order._id = result.insertedId;
})
// We could inherit PlaceOrder from SafeInteractor to let it catch errors for us
.catch((error) => {
this.fail({ error });
});
}
async rollback() {
// Delete the order if ChargeCard fails
return client.db().collection('orders').deleteOne({ _id: this.order._id })
}
}
class ChargeCard extends Interactor {
async perform() {
// API call to the payment system
}
}
There are helper functions to create an Interactor class runtime:
import { createInteractor } from "interactor-organizer";
// Do not use arrow/anonymous functions if you want to access `this`
const FirstInteractor = createInteractor(function perform() { console.log('first'); });
const SecondInteractor = createInteractor(function perform() { console.log('second'); });
Organizers example:
// The easiest way is to use the `organize` function
import { organize } from "interactor-organizer";
organize({}, [FirstInteractor, SecondInteractor]).then(console.log);
// A more elegant way is to create an Organizer
import { Organizer } from "interactor-organizer";
class CreateOrder extends Organizer {
static organize() {
return [PlaceOrder, ChargeCard];
}
}
// orders.controller.ts
function createOrder(req, res, next) {
CreateOrder.perform({ order: ...req.body, user: req.user })
.then((result) => {
if (result.failure) {
throw result.context.error;
}
res.status(201).json({ _id: result.context.order._id });
})
.catch(next);
}
Checking for failure
every time may not always can be convenient, instead, you can throw errors from the organizer:
class StrictOrganizer extends Organizer {
static async perform(context: any = {}) {
return super.perform(context)
.then((result) => {
if (result.failure) {
throw result.context.error || new Error(`${this.name} failed`);
}
return result;
});
}
}
// Inherit your organizers from StrictOrganizer