0.0.44 • Published 3 years ago

@bjmrq/pipe-flow v0.0.44

Weekly downloads
43
License
ISC
Repository
github
Last release
3 years ago

PipeFlow

THIS IS A WORK IN PROGRESS BREAKING CHANGED CAN HAPPEN ANY TIME BEFORE v1 release

A little utility to process data in a pipe

This is a little utility to process data in a pipe of functions using node.js runtime.

Your pipeline will flow a "context" containing the data you'll need and want to return thought your functions.

Your pipe will receive some data that will be attached to the "context" as an input, this data will be immutable.

The "context" also contain a mutable state entry. Anything you return from any of your function will be attached to this state.

In the "context" their is also a return entry so you can control what you wish to return at the end of your pipe.

You will also find some little helpers to help you with error handling.

And some utilities like: debugFlow / flowIf / flowIfElse /addToState / addToStateOn / addToReturn / addToReturnOn / returnWith

Installation

With npm:

npm install @bjmrq/pipe-flow

With Yarn:

yarn add @bjmrq/pipe-flow

Hello Word Example

Javascript:

const flowWith = pipeFlow(
  (context) => {
    const message = context.input.message // access the context
    context.return = message //control what you want to return
  }
)();// This is for optional error handling

const result = flowWith({ message: "Hello world" }) // create context from data
console.log(result) // "Hello world"

Arity:

pipeFlow(...functions, errorHandler, dataToTransformAsContext)

How It Works

Combine Functions

You can create a flow made of multiple functions that will execute one after an other from left to right, similar to a pipe function. Those function can either be sync or async functions.

const flowWith = pipeFlow(
  (context) => {
    console.log(context.input) // {id: 9}

    return { status: "ok" } // Attach data to the state
  },
  async (context) => {
    console.log(context.state.status) // "ok"
  }
)();

flowWith({ id: 9 });

Move Data Around

If you want to move some data from one function to an other simply return it in an object. Any object you return from one of your function will be merge in the state key of the "context".

const flowWith = pipeFlow(
  async (context) => {
    const product = await database("products").where(
      "id",
      context.input.productId // Access data from context
    );

    return { product }; // Attach data to the state
  },
  async (context) => {
    const productName = context.state.product.name // Access data from tate

    console.log(productName) // "A great product indeed"
  }
)();

flowWith({ productId: 9 });

Those are the keys accessible inside the context:

  • input: is the immutable input you gave as last argument to your pipeFlow
  • state: is a mutable key that you can use to pass data from one function to another
  • return: is the data that will be returned from your pipeFlow
  • error: you can attach an error to the error key, doing so will bypass other functions of the flow, only the error handler will be trigger, you can control wether you want to expose this error or not

Control What is Returned

You need to attach any data you wish to return to the return key of the "context", other keys will never be returned. If an error has happened during the flow it will be returned instead.

const flowWith = pipeFlow(
  async (context) => {
    const product = await database("products").where(
      "id",
      context.input.productId // Access data from context
    );

    return { product }; // Attach data to the state
  },
  async (context) => {
    context.return = context.state.product // Control data to return
  }
)();

const result = flowWith({ productId: 9 })
console.log(result) // { id: 9, name: "A great product indeed", type: "product",... }

Create a sub pipe

You can use subFlow to branch different flow together subFlow receive an already built context and will be able to attache data to it's return and state property.

const flowWith = pipeFlow(
  (context) => {
    console.log(context.input) // {id: 9}

    return { status: "ok" } // Attach data to the state
  },
  subFlow(
    async (context) => {
    return { subFlow: true } // "ok"
  },
  (context) => {
    context.return = context.state
  }
  )
)();

const result = flowWith({ id: 9 });
console.log(result) // { status: "ok", subFlow: true }

Error Handling

How it Works

If you want to control what is return from your flowPipe depending of different errors that might happen you need to attach the error to the error key of the "context". This will skip the execution of all other functions in your flow. The error should be attach to the "context" and not throw, to control the flow in your application. But if you forget to catch an error it will be attach to the "context" as well and returned as

const flowWith = pipeFlow(
  async (context) => {
    const product = await database("products").where(
      "id",
      context.input.productId // Access data from context
    );

    if(!product) {

      context.error = { // Attache the error on the "context"
        code: 404,
        message: "Product Not Found"
      };

    }

    return { product }; // Attach data to the state
  },
  // If an error has been attach in the previous function this one will not run
  async (context) => {
    const updatedProduct = await database("product").where(
      "id",
      context.state.product.id
    ).update(
      { sold: true}
    );

    context.return = updatedProduct;
  }
)();

This will result the following error

{
  code: 404,
  message: 'Product Not found',
  error: Error: Not found
           at ...
}
  • The type of an error to attach to the error key of the "context" should looks like this:
  • code: the error code
  • message: the error message
  • error: the error itself created from the message, will include stacktrace
type FlowError = {
  statusCode: number;
  message: string;
  error?: Error;
};

(to help you with formatting errors see the error helpers section)

  • If an unexpected error happens during your flow and you did not catch it will return the following response with a HTTP status code of 500
{
  "code": 1,
  "message": "Internal Error",
  "error": Error: something wrong happened
              at..
}

Error Helpers

You can use little error helper to format the errors attached to the "context".

  • errorBuilder: the error builder will help you build the error to be returned to the user, it is a curried function so you can pass it's parameter one at the time.
  • code (default to 1): the error code
  • message or error (default to "Internal Error" message): the message that will be attach to the error and use to create Error object

example 1:

context.error = errorBuilder()()

Will return

{
  code: 1,
  message: "Internal Error"
  error: new Error()
}

example 2:

context.error = errorBuilder(9)(new Error("Could not process arguments"))
// Same as
context.error = errorBuilder(9)("Could not process arguments")

Will return

{
  code: 9,
  message: "Could not process arguments",
  error: new Error("Could not process arguments")
}

Some predefined ones are derived from the builder but you can easily create yours

  • simpleError: code=1 provided
  • argumentError: code=9 provided
  • notFoundError: code=404 provided
  • notAuthorizedError: code=403 provided
  • unprocessableError: code=422 provided

Error builder in action

const notAuthorizedError = errorBuilder(403); // Import 
const { notAuthorizedError } = require("@bjmrq/pipe-flow")
exports.handler = pipeFlow(
  (context) => {
    const authorizationToken = context.input.token;

    if (!authorizationToken) {
      context.error = notAuthorizedError("You can't do that");
    }

    return { isAuthenticated: true }; // Only taken in consideration if no error was attached to the "context"
  },
)();

Extra Error Handler

If you wish to have extra logic triggered when an error occurres (send log to remote place, call a cloud service..) you can provide pipeFlow with an extra function.

exports.handler = pipeFlow(
  async (context) => {

    const product = await database("product").where(
      "id",
      context.state.productId
    );

    if (!product) {
      context.error = notFoundError(new Error("Could not find this product"));
    }
    
    context.return = product
// Extra error handling
)((context) => {
  sendLogs(context.error)
});
  • In the error handler you will have access to the whole "context" that caused the error with it's state and the error itself
  • The "context" in the error handler is a copy of the "context" that will be return, mutating it will not change the returned value

Utilities

  • debugFlow: will help you debug the state of your context (optionally your can pass an array of string to retrieve the value at a given path) example:
const flowWith = pipeFlow(
  (context) => {
    return { status: "ok" } // Attach data to the state
  },
  debugFlow() // { input: { id: 9 }, state: { status: "ok" }, error: undefined, return undefined}
)();

flowWith({ id: 9 });

const flowWith = pipeFlow(
  (context) => {
    return { status: "ok" } // Attach data to the state
  },
  debugFlow(["state", "status"]) // "ok"
)();

flowWith({ id: 9 });
  • addToReturn: will add the return value of a given function to the return property of the context
const flowWith = pipeFlow(
  addToReturn((context) => { userId: context.input.id }),
)();

const result = flowWith({ id: 9 });
console.log(result) // { userId: 9 }
  • addToState: will add the return value of a given function to the state property of the context
const flowWith = pipeFlow(
  addToState((context) => { userId: context.input.id }),
  (context) => {
    console.log(context.state) // { userId: 9 }
  }
)();

const result = flowWith({ id: 9 });
  • addToReturnOn: will add the return value of a given function to the specified key to the return property of the context
const flowWith = pipeFlow(
  addToReturnOn("userId", (context) => context.input.id ),
)();

const result = flowWith({ id: 9 });
console.log(result) // { userId: 9 }
  • addToStateOn: will add the return value of a given function to the specified key to the state property of the context
const flowWith = pipeFlow(
  addToStateOn("user", (context) => { userId: context.input.id }),
  (context) => {
    console.log(context.state) // { user: { userId: 9 }}
  }
)();

const result = flowWith({ id: 9 });
  • addToStateImmutableOn: will add the return value of a given function to the specified key to the state property of the context, and make it read only
  • returnWith: usually used at the end of the pipeFlow or subFlow, will return the given path from the context
const flowWith = pipeFlow(
  addToStateOn("user", (context) => { userId: context.input.id }),
  (context) => {
    console.log(context.state) // { user: { userId: 9 }}
  },
  returnWith(["state", "object", "userId"])
)();

const result = flowWith({ id: 9 });
console.log(result) // 9
  • flowIf: Execute a given function of the flow only if the given predicate return true
const flowWith = pipeFlow(
  addToStateOn("shouldExecute", () => true),
  flowIf(
      (context) => context.state.shouldExecute,
      (context) => {
        context.return = { hasExecuted: true };
      }
    )
)();

const result = flowWith({ id: 9 });
console.log(result) // { hasExecuted: true }
const flowWith = pipeFlow(
  addToStateOn("shouldExecute", false}),
  flowIf(
      (context) => context.state.shouldExecute,
      (context) => {
        context.return = { hasExecuted: true };
      }
    )
)();

const result = flowWith({ id: 9 });
console.log(result) // undefined
  • flowOn: Execute a given function of the flow on a specific value from the context accessed by an array of keys
const flowWith = pipeFlow(
  addToStateOn("number", () => 1),
  flowOn(["state", "number"],
      (number) => ({
        addedNumber: number + 1,
      })
    ),
  returnWith(["state", "addedNumber"])
)();

const result = flowWith({ initialData: "woo" });
console.log(result) // 2
  • flowOnTo: Execute a given function of the flow on a specific value from the context accessed by an array of keys and and attach it to the state on a given key
const flowWith = pipeFlow(
  addToStateOn("number", () => 1),
  flowOnTo(
    ["state", "number"],
    "addedNumber",
    (number) => number + 1,
  ),
  returnWith(["state", "addedNumber"])
)();

const result = flowWith({ initialData: "woo" });
console.log(result) // 2

The Flow and it's Context Recap

A flow is similar to a pipe function in functional programming, you can combine your functions from left to right, and the "context" will flow thought them, what you return from those functions will be attach to the state of the "context" so it can be passed on to the next function of the flow. Anything you wish to return can be attached to the return entry of the "context".

Those are the keys accessible inside the "context"

  • input: is the context data you passed to you pipe, it is immutable
  • state: is a mutable key that you can use to pass data from one function to another
  • error: you can attach an error to the error key, doing so will bypass other functions of the flow, only the error handler will be trigger. (will be returned from your pipe if any)
  • return: what will be returned from your pipe

If you want to pass data from one function to an other you can use the state key

Typescript

You will find different types available

type FlowContext
type FlowMiddleware
0.0.44

3 years ago

0.0.40

3 years ago

0.0.41

3 years ago

0.0.42

3 years ago

0.0.43

3 years ago

0.0.30

3 years ago

0.0.28

3 years ago

0.0.27

3 years ago

0.0.26

3 years ago

0.0.25

3 years ago

0.0.24

3 years ago

0.0.23

3 years ago

0.0.21

3 years ago

0.0.22

3 years ago

0.0.20

3 years ago

0.0.19

3 years ago

0.0.18

3 years ago

0.0.15

3 years ago

0.0.16

3 years ago

0.0.13

3 years ago

0.0.12

3 years ago

0.0.11

3 years ago

0.0.10

3 years ago

0.0.9

3 years ago

0.0.8

3 years ago

0.0.7

3 years ago

0.0.6

3 years ago

0.0.5

3 years ago

0.0.3

3 years ago