More advanced pipeline composition
Patrick Roza
Posted on June 10, 2019
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
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?"
Posted on June 10, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.