More advanced pipeline composition

Patrick Roza

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

Part of Functional Programming: Clean architecture and DDD series

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

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:

// ({ 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

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

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

What’s Next

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


Originally published at https://patrickroza.com.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade