当前位置: 首页 > 知识库问答 >
问题:

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

黄扬
2023-03-14

我使用NestJS CQRS配方来管理两个实体之间的交互:User和UserProfile。该架构是一个API网关NestJS服务器+每个微服务(User、UserProfile等)的NestJS服务器。

我已经通过API Gateway上的User和UserProfile模块用它们自己的saga/events/命令建立了基本的交互:

    null

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

一切都很好。

如果CreateUser命令失败,I解析(promise.reject(new HttpException(error,error.status)),它向最终用户指示在用户创建过程中出了问题。

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));
    }
  }
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);
        }),
      );
  }
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个答案

裴泰平
2023-03-14

用DDD术语来说,您对useruserprofile的创建构成了一个跨多个微服务的业务事务--一组必须一致的业务操作/规则。

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

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

>

  • 您让SAGA运行,直到它们执行指示业务事务已结束的命令,然后才为客户机解析指示成功或失败的结果(例如,在错误详细信息中,您可以报告哪些步骤成功了,哪些步骤没有成功)。因此,您还没有在CreateAuthUserCommand中进行解析。

    如果创建UserProfile可能需要很长时间(甚至需要由主持人手动验证),那么您可能需要在CreateAuthUserCommand中解析UserProfile中的UserProfile,然后让客户机订阅与UserProfile相关的事件。您需要一种机制来实现这一点,但它将客户机与正在运行的事务解耦,并且可以执行其他操作。

    或者,您可以将业务事务分成两个部分,客户机为其发送单独的请求:一个创建/返回经过身份验证的User,另一个返回创建的UserProfile。虽然User+UserProfile似乎属于相同的有界上下文,但它们驻留在两个不同的微服务中的事实可能表明它们不是(在这种情况下,我认为第一个微服务实际上是用于身份验证的,另一个用于用户配置文件,它对我来说指示了不同的有界上下文)。最佳实践是让微服务实现它们自己的封装的有界上下文。

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

  •  类似资料:
    • 我这里少了点什么。我做错什么了吗?

    • 问题内容: 我在jenkins中使用了一些Windows批处理命令,其中每个命令都可能失败。为了使詹金斯工作在每个步骤上失败,这些批处理命令如下所示: 换句话说:我似乎要检查每个批处理命令是否失败,然后在必要时以错误代码1退出。有没有更有意义/更方便/更好的方法来实现这一目标? 如果我不检查每个步骤并退出,jenkins将执行以下批处理命令。 问题答案: 可悲的是,您不能激活in bash之类的标

    • 默认情况下, 只要有任务调用失败, Gradle就会中断执行. 这可能会使调用过程更快, 但那些后面隐藏的错误就没有办法发现了. 所以你可以使用 --continue 选项在一次调用中尽可能多的发现所有问题. 采用 --continue 选项, Gralde 会调用每一个任务以及它们依赖的任务. 而不是一旦出现错误就会中断执行.所有错误信息都会在最后被列出来. 一旦某个任务执行失败,那么所有依赖于

    • 问题内容: 我正在尝试引发异常(不使用try catch块),并且程序在引发异常后立即完成。有没有一种方法可以在我引发异常之后继续执行程序?我抛出了在另一个类中定义的InvalidEmployeeTypeException,但是我希望程序在抛出该异常后继续执行。 问题答案: 试试这个:

    • 问题内容: 我试图按照Heroku的建议通过WhiteNoise提供静态文件。当我在开发环境中运行时,会发生以下情况: 当我在设置中将此行注释掉时,静态收集命令运行不会发生意外: 这里出了什么问题,我该如何解决?我已经尝试清空静态文件输出文件夹。它运行平稳,直到开始处理一个特定文件为止。 问题答案: 这里的问题是引用文件,该文件在预期位置不存在。 当您使用该存储后端运行时,Django会尝试重写C

    • 我使用android studio preview beta 4,打开project it build时从https://github.com/drklo/Telegram获得Telegram源代码,并出现以下错误: 我使用最新的NDK android-ndk-r16-beta1-windows-x86。add-application.mk的第100行中有错误,它是: 所以我使用的add-appl