Say I have three entities a parent entity, representing ingestion of an array of arrays over a third party API, a Child Entity representing the parsing and processing of one such array members, and errors representing the failure to process alongside an error message.
Let's say I have the following types:
export type CreateChildEntity = () => TaskEither<ServiceError, void>;
export type CheckParentMappable = (
id: UUID
) => TaskEither<ServiceError, Option<UUID>>;
export type GetHydratedParentForMapping = (
id: UUID
) => TaskEither<ServiceError, HydratedType>;
export type MarkParentMapped = (id: UUID) => TaskEither<ServiceError, void>;
export type MarkParentFailed = (id: UUID) => TaskEither<ServiceError, void>;
export type LookupDetails = (
id: UUID,
firstName: string,
lastName: string
) => TaskEither<ServiceError, Option<ValidatedDetails>>;
export type TrackParentMappingError = (
id: UUID,
author: ValidatedAuthor,
message: string
) => TaskEither<ServiceError, void>;
export type Deps = Readonly<{
createChildEntity: CreateChildEntity;
checkParentMappable: CheckParentMappable;
getHydratedParentForMapping: GetHydratedParentForMapping;
markParentMapped: MarkParentMapped;
markParentFailed: MarkParentFailed;
lookupDetails: LookupDetails;
trackParentMappingError: TrackParentMappingError;
}>;
And then the following function:
import { Ctx, Deps, Input, Error } from "./types";
import { pipe } from "fp-ts/lib/pipeable";
import * as TE from "fp-ts/lib/TaskEither";
import { isSome } from "fp-ts/lib/Option";
export default (input: Input): Ctx<void> => mapParent(input);
const mapParent = (input: Input): Ctx<void> => (
deps: Deps
): TE.TaskEither<Error, void> =>
pipe(
deps.getHydratedParentForMapping(input.id),
TE.chain(hydratedParent =>
pipe(
TE.sequenceArray(
hydratedParent.authors.map(a => {
return pipe(
deps.lookupDetails(hydratedParent.id, a.firstName, a.lastName),
TE.chain(detail => {
if (isSome(detail)) {
return deps.createChildEntity();
} else {
return pipe(
deps.trackParentMappingError(
hydratedParent.id,
a,
"MISSING_DETAILS_ERROR"
),
TE.chain(_ => deps.markParentFailed(hydratedParent.id))
);
}
})
);
})
),
TE.chain(_ => deps.markParentMapped(hydratedParent.id))
)
)
);
Full code here: https://stackblitz.com/edit/typescript-3qv4t1?file=index-bad.ts
As long as I have validation rules inside markParentFailed
and markParentMapped
to ascertain that the parent does (or does not) have children/errors attached, this code works.
What I would like to achieve instead is to have the initial map of lookupDetails
across hydratedParent.authors
to return an Either, such that I can then completely partition the flow based on the existence of failures. In effect, something like so:
const constructEither = (hydratedParent: HydratedType): Ctx<void> => (deps: Deps): TE.TaskEither<Error, Either<ReadonlyArray<Readonly<{id: UUID, author: ValidatedAuthor, errorMessage: string}>>, ReadonlyArray<ValidatedDetails>>> => {
return TE.sequenceArray(
hydratedParent.authors.map(a => {
return pipe(
deps.lookupDetails(hydratedParent.id, a.firstName, a.lastName),
TE.chain(val => {
if(isSome(val)) {
return right(val.value)
} else {
return left({id: hydratedParent.id, author: a, errorMessage: "MISSING_DETAILS_ERROR"})
}
})
)
}))
}
const mapParent = (input: Input): Ctx<void> => (
deps: Deps
): TE.TaskEither<Error, void> =>
pipe(
deps.getHydratedParentForMapping(input.id),
TE.chain(hydratedParent =>
pipe(
constructEither(deps)(hydratedParent),
TE.chain(res => {
if(isRight(res)) {
pipe(
res.map(r => deps.createChildEntity()),
,TE.chain(_ => deps.markParentMapped(hydratedParent.id))
)
} else {
pipe(
res.map(r => deps.trackParentMappingError(
r.left.id,
r.left.author,
r.left.errorMessage
)),
TE.chain(_ => deps.markParentFailed(hydratedParent.id))
)
}
})
)
)
);
This fails to compile.
Additionally, it is my understanding that even if I get it to compile, TaskEither will ignore the left branch anyway, and, effectively, throw it as an Exception.
What is the fp-ts incantation for partitioning a TaskEither in this way?