如何用fp-ts将TaskOption.Some转为TaskEither.Left实现用户创建校验
如何结合fp-ts实现用户创建时的邮箱唯一性校验
需求背景
需要实现一个用户创建函数,包含三个核心步骤:
- 检查用户邮箱是否未被注册
- 根据给定数据创建用户
- 向新用户发送邮件
现有基础函数定义:
createOne: ({ password, ...creatableUser }: CreatableUser): taskEither.TaskEither<ExceptionError, User> => { .... }
问题场景
尝试编写的代码如下,但无法实现当findByEmail返回TaskOption.Some(即用户已存在)时,返回TaskEither.Left错误的逻辑:
// * 疑问函数 export const createOne = ({ password, ...creatableUserData }: CreatableUser): taskEither.TaskEither<Error, User> => { // 生成新用户 const newUser: User = { ...creatableUserData, id: randomUUID(), }; return pipe( // * 检查用户是否尚未存在 creatableUserData.email, findByEmail, taskEither.fromTaskOption(() => new Error('User already exists')), // TODO: 需要在`Some`时返回错误 // * 将新`User`保存到仓库 taskEither.chain(() => save(newUser, password)), // * 向用户发送确认邮件 taskEither.chain(() => sendMail({ body: 'Welcome to App Team', }), ), taskEither.map(() => newUser), ); };
相关类型定义
import { randomUUID } from 'crypto'; import { TaskOption } from 'fp-ts/lib/TaskOption'; import { taskEither } from 'fp-ts'; import { TaskEither } from 'fp-ts/lib/TaskEither'; import { pipe } from 'fp-ts/lib/function'; // * 上下文类型 type CreatableUser = { password: string; email: string; //... } type User = { email: string; id: string; //... } declare const findByEmail: (email: string) => TaskOption<User>; declare const save: (user: User, password: string) => TaskEither<Error, void>; declare const sendMail: (message: {body: string}) => TaskEither<Error, void>;
需要实现:当用户已存在时,通过过滤或预测函数提前返回错误。
更新:已实现的解决方案
目前通过以下代码解决了问题,但仍有优化空间:
export const makeCreateOne = (usersRepository: UsersRepository, mailProvider: MailProvider) => ({ password, ...creatableUserData }: CreatableUser): TaskEither<ExceptionError, User> => { // 生成新用户 const newUser: User = { ...creatableUserData, id: randomUUID(), }; return pipe( // * 根据给定邮箱获取用户 creatableUserData.email, usersRepository.findByEmail, // * 如果是`Some`则返回`null` TO.match( // None: 用户不存在,可以创建新用户 () => true, // Some: 用户已存在,终止创建流程 () => null, ), TE.fromNullable(createExceptionError('User already exists', REQUEST_STATUS.not_found)), // * 将新`User`保存到仓库 TE.chain(() => usersRepository.save(newUser, password)), // * 向用户发送确认邮件 TE.chain(() => mailProvider.sendMail({ body: 'Welcome to App Team', }), ), TE.map(() => newUser), ); };
优化建议
方案1:使用taskOption.fold直接转换为TaskEither
不需要先转成true/null再用fromNullable,可以直接通过fold处理两种分支逻辑,代码更直观:
export const makeCreateOne = (usersRepository: UsersRepository, mailProvider: MailProvider) => ({ password, ...creatableUserData }: CreatableUser): TaskEither<ExceptionError, User> => { const newUser: User = { ...creatableUserData, id: randomUUID(), }; return pipe( creatableUserData.email, usersRepository.findByEmail, // 直接通过fold将TaskOption转为TaskEither TO.fold( // None: 用户不存在,继续流程,返回成功的空值 () => TE.right(void 0), // Some: 用户已存在,直接返回错误 () => TE.left(createExceptionError('User already exists', REQUEST_STATUS.not_found)) ), TE.chain(() => usersRepository.save(newUser, password)), TE.chain(() => mailProvider.sendMail({ body: 'Welcome to App Team' })), TE.map(() => newUser) ); };
方案2:封装邮箱唯一性校验的复用函数
如果这个校验逻辑会被多处使用,可以单独封装成函数,提高代码复用性和可读性:
const ensureEmailNotExists = (email: string, repo: UsersRepository): TaskEither<ExceptionError, void> => pipe( email, repo.findByEmail, TO.fold( () => TE.right(void 0), () => TE.left(createExceptionError('User already exists', REQUEST_STATUS.not_found)) ) ); export const makeCreateOne = (usersRepository: UsersRepository, mailProvider: MailProvider) => ({ password, ...creatableUserData }: CreatableUser): TaskEither<ExceptionError, User> => { const newUser: User = { ...creatableUserData, id: randomUUID(), }; return pipe( ensureEmailNotExists(creatableUserData.email, usersRepository), TE.chain(() => usersRepository.save(newUser, password)), TE.chain(() => mailProvider.sendMail({ body: 'Welcome to App Team' })), TE.map(() => newUser) ); };
方案3:避免提前创建newUser(可选优化)
如果校验不通过时不想提前生成newUser(比如避免不必要的randomUUID调用),可以把newUser的创建放到校验通过后的流程里:
export const makeCreateOne = (usersRepository: UsersRepository, mailProvider: MailProvider) => ({ password, ...creatableUserData }: CreatableUser): TaskEither<ExceptionError, User> => pipe( creatableUserData.email, usersRepository.findByEmail, TO.fold( () => TE.right(void 0), () => TE.left(createExceptionError('User already exists', REQUEST_STATUS.not_found)) ), // 校验通过后再生成新用户 TE.map(() => ({ ...creatableUserData, id: randomUUID(), })), TE.chain(newUser => pipe( usersRepository.save(newUser, password), TE.map(() => newUser) ) ), TE.chain(newUser => pipe( mailProvider.sendMail({ body: 'Welcome to App Team' }), TE.map(() => newUser) ) ) );
内容的提问来源于stack exchange,提问作者Puszkarek
相关产品推荐
相关产品推荐

