如何从随后在NestJS CQRS中失败的命令中引发HTTP异常?

时间:2018-11-27 02:54:37

标签: javascript typescript cqrs nestjs

我正在使用 NestJS CQRS 配方来管理两个实体(用户和UserProfile)之间的交互。该体系结构是API网关NestJS服务器+每个微服务(用户,UserProfile等)的NestJS服务器。

我已经通过API Gateway上的User和UserProfile模块使用自己的sagas / events / commands建立了基本的交互:

  • 创建用户时,将创建用户个人资料
  • 用户个人资料创建失败时,将删除先前创建的用户

详细信息:

在用户模块中,CreateUser 命令引发一个用户创建的事件,该事件会被用户 saga 拦截,触发CreateUserProfile 命令(来自UserProfile 模块)。

如果后者失败,则会引发UserProfileFailedToCreate 事件,并被UserProfile saga 拦截,这将触发DeleteUser 命令(来自用户模块)。

一切正常。

如果 CreateUser 命令失败,则我resolve(Promise.reject(new HttpException(error, error.status))通知最终用户在用户创建过程中出现了问题。

我的问题是我无法为 CreateUserProfile 命令复制相同的行为,因为显然已经从第一个命令解决了HTTP请求承诺。

所以我的问题是:如果随后的命令在传奇中失败,有什么方法可以使命令失败?我知道HTTP请求完全与由传奇触发的任何后续命令断开连接,但是我想知道是否有人已经在使用事件或此处的其他方法来复制此数据流?

我使用CQRS的原因之一,除了具有用于微服务之间的数据交互的更清晰的代码之外,还能够在任何链式命令失败的情况下回滚存储库操作,这很好用。 但是我需要一种方法向最终用户表明该链遇到了问题并已回滚。

UserController.ts

@Post('createUser')
async createUser(@Body() createUserDto: CreateUserDto): Promise<{user: IAuthUser, token: string}> {
  const { authUser } = await this.authService.createAuthUser(createUserDto);
  // this is executed after resolve() in CreateUserCommand
  return {user: authUser, token: this.authService.createAccessTokenFromUser(authUser)};
}

UserService.ts

async createAuthUser(createUserDto: CreateUserDto): Promise<{authUser: IAuthUser}> {
  return await this.commandBus
    .execute(new CreateAuthUserCommand(createUserDto))
    .catch(error => { throw new HttpException(error, error.status); });
}

CreateUserCommand.ts

async execute(command: CreateAuthUserCommand, resolve: (value?) => void) {
    const { createUserDto } = command;
    const createAuthUserDto: CreateAuthUserDto = {
      email: createUserDto.email,
      password: createUserDto.password,
      phoneNumber: createUserDto.phoneNumber,
    };

    try {
      const user = this.publisher.mergeObjectContext(
        await this.client
          .send<IAuthUser>({ cmd: 'createAuthUser' }, createAuthUserDto)
          .toPromise()
          .then((dbUser: IAuthUser) => {
            const {password, passwordConfirm, ...publicUser} = Object.assign(dbUser, createUserDto);
            return new AuthUser(publicUser);
          }),
      );
      user.notifyCreated();
      user.commit();
      resolve(user); // <== This makes the HTTP request return its reponse
    } catch (error) {
      resolve(Promise.reject(error));
    }
  }

UserSagas.ts

authUserCreated = (event$: EventObservable<any>): Observable<ICommand> => {
    return event$
      .ofType(AuthUserCreatedEvent)
      .pipe(
        map(event => {
          const createUserProfileDto: CreateUserProfileDto = {
            avatarUrl: '',
            firstName: event.authUser.firstName,
            lastName: event.authUser.lastName,
            nationality: '',
            userId: event.authUser.id,
            username: event.authUser.username,
          };
          return new CreateUserProfileCommand(createUserProfileDto);
        }),
      );
  }

CreateUserProfileCommand.ts

async execute(command: CreateUserProfileCommand, resolve: (value?) => void) {
    const { createUserProfileDto } = command;

    try {
      const userProfile = this.publisher.mergeObjectContext(
        await this.client
          .send<IUserProfile>({ cmd: 'createUserProfile' }, createUserProfileDto)
          .toPromise()
          .then((dbUserProfile: IUserProfile) => new UserProfile(dbUserProfile)),
      );
      userProfile.notifyCreated();
      userProfile.commit();
      resolve(userProfile);
    } catch (error) {
      const userProfile = this.publisher.mergeObjectContext(new UserProfile({id: createUserProfileDto.userId} as IUserProfile));
      userProfile.notifyFailedToCreate();
      userProfile.commit();
      resolve(Promise.reject(new HttpException(error, 500)).catch(() => {}));
    }
  }

UserProfileSagas.ts

userProfileFailedToCreate = (event$: EventObservable<any>): Observable<ICommand> => {
    return event$
      .ofType(UserProfileFailedToCreateEvent)
      .pipe(
        map(event => {
          return new DeleteAuthUserCommand(event.userProfile);
        }),
      );
  }

DeleteUserCommand.ts

async execute(command: DeleteAuthUserCommand, resolve: (value?) => void) {
    const { deleteAuthUserDto } = command;

    try {
      const user = this.publisher.mergeObjectContext(
        await this.client
          .send<IAuthUser>({ cmd: 'deleteAuthUser' }, deleteAuthUserDto)
          .toPromise()
          .then(() => new AuthUser({} as IAuthUser)),
      );
      user.notifyDeleted();
      user.commit();
      resolve(user);
    } catch (error) {
      resolve(Promise.reject(new HttpException(error, error.status)).catch(() => {}));
    }
  }

1 个答案:

答案 0 :(得分:1)

以DDD术语表示,您创建的UserUserProfile构成了业务交易-一组必须一致的业务操作/规则-spans multiple microservices

在这种情况下,在创建User之前返回数据库UserProfile意味着您以不一致的状态返回数据。这不一定是错误的,但是如果您这样做,则应该在客户端中适当地处理它。

我看到了处理这种情况的三种可能的方法:

  1. 您可以让Sagas运行,直到他们执行了指示业务交易已结束的命令,然后才为客户端解析指示成功或失败的结果(例如,在错误详细信息中,您可以报告哪些步骤成功以及哪些步骤失败了)没有)。因此,您尚未在CreateAuthUserCommand中解决。

  2. 如果创建UserProfile可能花费很长时间(甚至可能需要主持人手动验证),那么您可能想解决{ {1}},然后让客户端订阅与UserProfile相关的事件。您需要一种机制,但是它将客户端与正在运行的事务分离开来,并且它可以做其他事情。

  3. 或者,您可以将业务交易分为两个部分,客户端分别为它们发送请求:一个创建/返回已认证的User,另一个返回创建的CreateAuthUserCommand。尽管User + UserProfile似乎属于同一个有界上下文,但是它们驻留在两个不同的微服务中这一事实可能表明它们不是(在这种情况下,我认为第一个微服务确实是用于身份验证和另一个用于UserProfiles,向我指示不同的有界上下文)。最佳实践是让微服务实现自己的封装有界上下文。

(注意:回答了一个旧问题,希望对其他人有帮助)