Interactor pattern implementation, inspired by Ruby gem interactor.
npm i interactor-organizerimport { 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:
InteractorSafeInteractor
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