https://github.com/digaev/interactor-js
Interactor pattern implementation
https://github.com/digaev/interactor-js
Last synced: 4 months ago
JSON representation
Interactor pattern implementation
- Host: GitHub
- URL: https://github.com/digaev/interactor-js
- Owner: digaev
- Created: 2023-03-02T07:51:47.000Z (over 3 years ago)
- Default Branch: master
- Last Pushed: 2024-06-24T15:55:31.000Z (about 2 years ago)
- Last Synced: 2025-10-19T17:06:00.583Z (8 months ago)
- Language: TypeScript
- Homepage:
- Size: 98.6 KB
- Stars: 1
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# interactor-organizer
[](https://dl.circleci.com/status-badge/redirect/gh/digaev/interactor-js/tree/master)
[](https://coveralls.io/github/digaev/interactor-js?branch=master)
[](https://www.npmjs.com/package/interactor-organizer)
Interactor pattern implementation, inspired by Ruby gem [interactor](https://github.com/collectiveidea/interactor).
___
* [Getting started](#getting-started)
* [Interactors](#interactors)
* [Organizers](#organizers)
* [Usage](#usage)
## Getting started
```bash
npm i interactor-organizer
```
```ts
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' }
*/
```
## Interactors
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
`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
`after(): Promise`
Is called after `perform` only if the interactor didn't `fail`.
### before
`before(): Promise`
Is always called before `perform`.
### fail
`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
`perform(): Promise`
Your business logic goes here. Under the hood, this method is modified so that it calls the `after` and `before` hooks.
### rollback
`rollback(): Promise`
This method is only used by Organizers to allow successfully resolved interactors in the chain to undo the changes made by `perform`.
### static perform
`static perform(context?: any): Promise`
A shortcut to the instance method.
### context
`context: any`
Current context. An object.
### failure
`failure: boolean`
Indicates if the interactor failed.
### success
`success: boolean`
The opposite of `failure`.
## Organizers
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`)!
## Usage
Interactors example:
```ts
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:
```ts
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:
```ts
// The easiest way is to use the `organize` function
import { organize } from "interactor-organizer";
organize({}, [FirstInteractor, SecondInteractor]).then(console.log);
```
```ts
// A more elegant way is to create an Organizer
import { Organizer } from "interactor-organizer";
class CreateOrder extends Organizer {
static organize() {
return [PlaceOrder, ChargeCard];
}
}
```
```ts
// 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:
```ts
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
```