More advanced pipeline composition
Tags: javascript, typescript, rop, functional
Published: 2019-06-10 10:31:00

---
title: More advanced pipeline composition
published: true
tags: javascript,typescript,rop,functional
canonical_url: https://patrickroza.com/blog/more-advanced-pipeline-composition/
series: Functional Programming - Clean architecture and DDD
description: When we add more functional composition tools to our belt, we can start composing usecase pipelines that are both terse and descriptive.
---

When we add more functional composition tools to our belt, we can start composing usecase pipelines that are both terse and descriptive.

## Operators

- From previous article: `map`: `(value => newValue) => Result<newValue, ...>`
- `flatMap`: `(value => newResult) => newResult`
- `toTup`: `(value => newValue) => readonly [newValue, value]`
- `tee`: `(value => any) => Result<value, ...>`
- `resultTuple`: `(...[Result<..., ...>]) => Result<readonly [value, value2, ...], error[]>`

## Sample

```typescript
type CreateError = CombinedValidationError | InvalidStateError | ValidationError | ApiError | DbError

// ({ templateId: string, pax: Pax, startDate: string }) => Result<TrainTripId, CreateError>
pipe(
  flatMap(validateCreateTrainTripInfo), // R<{ pax: PaxDefinition, startDate: FutureDate, templateId: TemplateId}, CombinedValidationError>
  flatMap(toTup(({ templateId }) => getTrip(templateId))), // R<[TripWithSelectedTravelClass, { pax... }], ...>
  map(([trip, proposal]) => TrainTrip.create(proposal, trip)), // R<TrainTrip, ...>
  tee(db.trainTrips.add), // R<TrainTrip, ...>
  map(trainTrip => trainTrip.id), // R<TrainTripId, ...>
)
```

The `validateCreateTrainTripInfo` function:

```typescript
// ({ templateId: string, pax: Pax, startDate: string}) => Result<({ pax: PaxDefinition, startDate: FutureDate, templateId: TemplateId }), CombinedValidationError>
pipe(
  flatMap(({ pax, startDate, templateId }) =>
    resultTuple(
      PaxDefinition.create(pax).pipe(mapErr(toFieldError("pax"))),
      FutureDate.create(startDate).pipe(mapErr(toFieldError("startDate"))),
      validateString(templateId).pipe(mapErr(toFieldError("templateId"))),
    ).pipe(mapErr(combineValidationErrors)),
  ),
  map(([pax, startDate, templateId]) => ({
    pax, startDate, templateId,
  })),
)
```

_Both are taken from [usecases/createTrainTrip.ts](https://github.com/patroza/fp-app-framework/blob/master/samples/basic/src/TrainTrip/usecases/createTrainTrip.ts)_

This validator facilitates domain level validation, not to be confused with REST level DTO validation. It prepares the validated DTO data for input to the domain factory `TrainTrip.create`. These domain rules are neatly packaged in the Value objects `FutureDate` and `PaxDefinition`, reducing complexity and knowledge creep in the factory.

Again, if [tc39 proposal-pipeline-operator](https://github.com/tc39/proposal-pipeline-operator) would land, we can write more terse and beautiful code.

### CombinedValidationErrors

We're wrapping each `ValidationError` into a `FieldValidationError`, so that we have the name of the field in the error context, then at the end we combine them into a single error, which can be easily examined and serialized to e.g JSON on the REST api to be consumed and examined by the client.

e.g:

```typescript
if (err instanceof CombinedValidationError) {
  ctx.body = {
    fields: combineErrors(err.errors),
    message,
  }
  ctx.status = 400
}

const combineErrors = (ers: any[]) => ers.reduce((prev: any, cur) => {
  if (cur instanceof FieldValidationError) {
    if (cur.error instanceof CombinedValidationError) {
      prev[cur.fieldName] = combineErrors(cur.error.errors)
    } else {
      prev[cur.fieldName] = cur.message
    }
  }
  return prev
}, {})
```

## Source

As always you can also find the full framework and sample source at [patroza/fp-app-framework](https://github.com/patroza/fp-app-framework)

## What's Next

Next in the series, I plan to examine the question: "When to return errors, and when to throw them?"