composable-flows
v1.0.3
Published
Compose flows with readable interface
Downloads
1
Readme
Composable Flows
Compose flows and use cases using functions.
import { Flow } from 'composable-flows'
function emailValidator(email: string) {
return true
}
class EmailSender {
async send(email: string): Promise<string> {
return Promise.resolve(`E-mail sent to ${email}`)
}
}
const flow = new Flow([emailValidator, new EmailSender().send])
;(async () => {
await flow.execute('[email protected]')
await flow.allOk((resultValues) => {
console.log(resultValues)
})
})()
Import/Require
Import
import { Flow, FlowMode } from 'composable-flows'
Require
const { Flow, FlowMode } = require('composable-flows')
Browser
<script src="dist/browser/index.js"></script>
Options
| Option name | Default value | Description | Possible values | |
| ------------ | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | --- |
| isStoppabble | false | If flow should stop when an error occurs | true,false | |
| isSafe | true | If should not thrown an exception when an error occurs | true,false | |
| mode | FlowMode.DEFAULT | Defines how flow will be executed. DEFAULT: the stages run with same parameter passed on execute
method. PIPELINE: the result of a stage is passed as parameter of next stage. | FlowMode.DEFAULT, FlowMode.PIPELINE | |
Passing parameters
A Flow allow you to pass a single parameter to all stages in a single execute
call.
The same parameter is injected in all stages.
// a parameter passed on `execute`
await flow.execute('[email protected]')
// is injected on each stage
function emailValidator(email: string) {
// email is '[email protected]'
}
class EmailSender {
async send(email: string): Promise<string> {
// email is '[email protected]' also
}
}
Using promises?
The execute
method returns a promise.
// using `then`
flow.execute('[email protected]').then((result) => {})
// or using async/await
const result = await flow.execute('[email protected]')
Stages
Each step of flow (each item of the array) is called Stage
.
Stages can be functions or methods.
// a simple function
function simpleFunction(email: string) {}
// a method
class Example {
method(email: string) {}
}
const example = new Example()
const flow = new Flow([simpleFunction, example.method])
Callbacks
Flow comes with callbacks to handle the flow result. This is useful to leverage exeception handling and increase code readability.
Success callbacks
Get the result of all stage when they all run successfully
await flow.allOk((resultValues) => {
console.log(resultValues)
})
Get the result of stages, when having any stage that run successfully
await flow.anyOk((resultValues) => {
console.log(resultValues)
})
Get the result of a specific stage by its index
const flow = new Flow([emailValidator, new EmailSender().send])
const index = 0
await flow.ok(index, (resultValue) => {
//resultValue is the result of emailValidator
console.log(resultValue)
})
Get the result of a specific stage by its name
const flow = new Flow([
emailValidator,
{ 'send email': new EmailSender().send },
])
await flow.ok('send email', (resultValue) => {
//resultValue is the result of EmailSender.send
console.log(resultValue)
})
Error callbacks
Get the errors of stages when they all fail
await flow.allFail((errors) => {
console.log(errors)
})
Get the errors of stages, when having any stage fails
await flow.anyFail((errors) => {
console.log(errors)
})
Get the result of a specific stage by its index
const flow = new Flow([emailValidator, new EmailSender().send])
const index = 0
await flow.fail(index, (error) => {
// error is the error of emailValidator
console.log(error)
})
Get the error of a specific stage by its name
const flow = new Flow([
emailValidator,
{ 'send email': new EmailSender().send },
])
await flow.fail('send email', (error) => {
//error is the error of EmailSender.send
console.log(error)
})
Get the result without using callbacks
If you don't want to use the callbacks and wants to handle the result by yourself.
const result = await flow.execute('[email protected]')
console.log('result', result)
The resulting log is:
{
result: StageResult {
isError: false,
error: undefined,
value: 'E-mail sent to [email protected]'
},
resultAll: [
IndexedStageResult {
isError: false,
error: undefined,
value: true,
id: 0
},
IndexedStageResult {
isError: false,
error: undefined,
value: 'E-mail sent to [email protected]',
id: 1
}
]
}
Multiples callbacks
You can use multiples callbacks in the same flow.
const flow = new Flow([emailValidator, new EmailSender().send])
await flow.allOk((resultValues) => {
console.log(resultValues)
})
await flow.anyFail((errors) => {
console.log(errors)
})
Callbacks as promises
Each callback call is a promise. This is because depending on you flow, you can wait or not for the callback completion. Let's suppose you have an express.js controller like that:
app.get('/', async function (req, res) {
const flow = new Flow([emailValidator, new EmailSender().send])
await flow.execute('[email protected]')
flow.allOk((resultValues) => {
// This will be called when all stages run
res.json({ resultValues })
})
flow.anyFail((errors) => {
// This will be called when any stage throws an exeception
res.status(400).json({ errors })
})
})
This will work fine with or without the await
keyword on flow.allOk
and flow.anyFail
.
But, let suppose you run an asynchronous code inside of each callback:
function notifyUser(value) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(`[NOTIFY] ${value}`)
resolve()
}, 200)
})
}
const flow = new Flow([emailValidator, new EmailSender().send])
await flow.execute('[email protected]')
flow.allOk(async (resultValues) => {
console.log('allOk', resultValues)
await notifyUser('allOk')
})
flow.anyOk(async (resultValues) => {
console.log('anyOk', resultValues)
await notifyUser('anyOk')
})
console.log('done')
res.send({ status: 'ok' })
The resulting log will be:
allOk [ true, 'E-mail sent to [email protected]' ]
anyOk [ true, 'E-mail sent to [email protected]' ]
done
[NOTIFY] allOk
[NOTIFY] anyOk
That means, the notifyUser
function is being resolved only after the response has been sent.
If you want to wait the callbacks, before sending the response, you must add the await
keyword on the callbacks, like that:
const flow = new Flow([emailValidator, new EmailSender().send])
await flow.execute('[email protected]')
await flow.allOk(async (resultValues) => {
console.log('allOk', resultValues)
await notifyUser('allOk')
})
await flow.anyOk(async (resultValues) => {
console.log('anyOk', resultValues)
await notifyUser('anyOk')
})
console.log('done')
res.send({ status: 'ok' })`
And the resulting log will be:
allOk [ true, 'E-mail sent to [email protected]' ]
[NOTIFY] allOk
anyOk [ true, 'E-mail sent to [email protected]' ]
[NOTIFY] anyOk
done
Thus, this will make the callbacks to be resolved, before continuing the execution.
Exception handling
By default, Flow will never thrown an exception. To handle exception and get the errors, you should use the proper callbacks: fail
, anyFail
and allFail
.
But, if you want the handle the exception by yourself, you just simply need to pass the option isSafe: false
and use try/catch
for that.
If isSafe
is false
and an exeception occurs, the flow will be interrupted.
function emailValidator(email: string) {
console.log('>> 1. validating email', email)
throw new Error('Error on validating user')
}
class EmailSender {
async send(email: string): Promise<string> {
console.log('>> 2. email sent')
return Promise.resolve(`E-mail sent to ${email}`)
}
}
const options: FlowOptions = {
isSafe: false,
}
const flow = new Flow([emailValidator, new EmailSender().send], options)
;(async () => {
try {
const result = await flow.execute('[email protected]')
console.log('result', result)
} catch (error) {
console.log((error as Error).message)
}
})()
Note
// This new Flow([], { isSafe: true }) // is equivalent to new Flow([])
Stop the flow
By default, if an exception occurs, the flow will never stop, unless you say so with isStoppable: true
.
function emailValidator(email: string) {
console.log('>> 1. validating email', email)
throw new Error('Error on validating user')
}
class EmailSender {
async send(email: string): Promise<string> {
console.log('>> 2. email sent')
return Promise.resolve(`E-mail sent to ${email}`)
}
}
const options: FlowOptions = {
isStoppable: true,
}
const flow = new Flow([emailValidator, new EmailSender().send], options)
;(async () => {
await flow.execute('[email protected]')
// EmailSender().send was never executed since emailValidator throws an exeception
await flow.anyFail((errors) => {
console.log(errors)
})
})()
Pipeline mode
By default, the parameter passed on execute
function, is used to call each stage.
With PIPELINE mode, the result of a stage is used as parameter of the next stage.
In this mode, the parameter passed on execute
is used only in the first stage.
Also, in PIPELINE mode, if an exception occurs, the flow is interrupted. Since the result of a stage is used as parameter of next, in case of exeception, the next stage cannot have the result of a failed stage.
So, in PIPELINE mode, isStoppable
is always true
.
import { Flow, FlowMode } from 'composable-flows'
interface UserInput {
email: string
}
interface User {
id: number
email: string
}
interface Event {
name: string
datetime: Date
}
export class GetUserInfo {
get(userInput: UserInput): User {
console.log('1. getting user information:[%s]', userInput.email)
// this result will be input of EmailSender.send
return {
id: 1,
email: userInput.email,
}
}
}
export class EmailSender {
async send(user: User): Promise<Event> {
console.log('2. sending email:[%s]', user.email)
// this result will be input of Database.storeEvent
return Promise.resolve({
name: 'email',
datetime: new Date(),
})
}
}
export class Database {
async storeEvent(event: Event): Promise<boolean> {
console.log(
'3. storing log for event:[%s] at [%s]',
event.name,
event.datetime,
)
return Promise.resolve(true)
}
}
const getUserInfo = new GetUserInfo()
const emailSender = new EmailSender()
const database = new Database()
const options = {
mode: FlowMode.PIPELINE,
}
const flow = new Flow<UserInput>(
[getUserInfo.get, emailSender.send, database.storeEvent],
options,
)
// This parameter will be passed only to the first stage
flow.execute({ email: '[email protected]' }).then((result) => {
console.log('done', JSON.stringify(result, null, 2))
})
Note
// This new Flow([], { mode: FlowMode.DEFAULT }) // is equivalent to new Flow([])
Advanced usage
Some way you can use Flow.
function emailValidator(email: string) {
console.log('validating email', email)
return true
}
class EmailSender {
async send(email: string): Promise<string> {
console.log('email sent to %s', email)
return Promise.resolve(`E-mail sent to ${email}`)
}
}
const emailSender = new EmailSender()
const flow = new Flow([
// normal function
emailValidator,
// method
emailSender.send,
// bind function
emailSender.send.bind(emailSender, '[email protected]'),
// an anonymous function
(email: string) => {
const newEmail = email.replace('@email.com', '@completelydifferent.com')
emailSender.send(newEmail)
return 'DONE'
},
])
;(async () => {
await flow.execute('[email protected]')
await flow.allOk((resultValues) => {
console.log(resultValues)
})
})()
Note
When using PIPELINE mode, the anonymous functions are required to return the value since it will be used as input of next stage.
const flow = new Flow([ // an anonymous function (email: string) => { const newEmail = email.replace('@email.com', '@different.com') const response = emailSender.send(newEmail) // Remember to return here, when needed return response }, ])