1.0.15 • Published 10 months ago

interactor-organizer v1.0.15

Weekly downloads
-
License
ISC
Repository
github
Last release
10 months ago

interactor-organizer

CircleCI Coverage Status npm

Interactor pattern implementation, inspired by Ruby gem interactor.


Getting started

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' }
*/

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<any>

Is called after perform only if the interactor didn't fail.

before

before(): Promise<any>

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<any>

Your business logic goes here. Under the hood, this method is modified so that it calls the after and before hooks.

rollback

rollback(): Promise<any>

This method is only used by Organizers if the interactor failed, to undo changes made by perform.

static perform

static perform(context?: any): Promise<Interactor>

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:

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
1.0.15

10 months ago

1.0.14

1 year ago

1.0.13

1 year ago

1.0.12

1 year ago

1.0.11

1 year ago

1.0.10

1 year ago

1.0.9

1 year ago

1.0.8

1 year ago

1.0.7

1 year ago

1.0.6

1 year ago

1.0.5

1 year ago

1.0.4

1 year ago

1.0.3

1 year ago

1.0.2

1 year ago

1.0.1

1 year ago

1.0.0

1 year ago