cqrs icon indicating copy to clipboard operation
cqrs copied to clipboard

Correct usage of the query bus to get a strongly-typed result? (Maybe feature request.)

Open ldiego08 opened this issue 5 years ago • 13 comments

I'm submitting a...


[ ] Regression 
[ ] Bug report
[x] Feature request?
[x] Documentation issue or request?
[ ] Support request => Please do not submit support request here, instead post your question on Stack Overflow.

Current behavior

Getting a strongly-typed result from QueryBus.execute() involves specifying the TRes type parameter, but since it's the second one, you also have to specify the type of the query (inferred when no type params are specified).

According to this answer https://github.com/nestjs/cqrs/issues/167#issuecomment-628462104, considering the following query...

class UsersQuery {
   constructor(
      readonly pageSize: number
   ) {}
}

class UsersQueryHandler implements IQueryHandler<UsersQuery> {
    async execute(): Promise<User[]> {
        // ..
    }
}

... the intended use if you want a strongly-typed result would be:

const users = await queryBus.execute<UsersQuery, User[]>(new UsersQuery(10));

Expected behavior

If TRes was the first parameter, it would be less verbose:

const users = await queryBus.execute<User[]>(new UsersQuery(10));

Examining a bit deeper, I started wondering why is not the result type inferred along with the query type, i.e.:

class UsersQuery implements IQuery<User[]> {
   constructor(
      readonly pageSize: number
   ) {}
}

class UsersQueryHandler implements IQueryHandler<UsersQuery> {
    async execute(): Promise<User[]> {
        // ..
    }
}

execute could potentially infer the type if its definition was:

async execute<TResult = any, TQuery extends IQuery<TResult>>(query TQuery): Promise<TResult>;

I'm aware that QueryBase is already being used as the generic type for the entire QueryBus class. Does that mean I should inject as many query buses as query types I'm intending to execute?

constructor(
    readonly usersQueryBus: QueryBus<UsersQuery>,
    readonly rolesQueryBus: QueryBus<RolesQuery>,
) {}

While the implementation hints so, docs specify otherwise: the QueryBus is a generic class. With all the above said, I have some questions:

  • Is there a particular reason QueryBase is a class-level type parameter in QueryBus instead of a method-level one in execute?

  • Would it be okay to move the TRes type param first?

Environment


Nest version: 7.5.0

 
For Tooling issues:
- Node version: 14
- Platform:  MacOS

ldiego08 avatar Nov 06 '20 23:11 ldiego08

I have checked and also asked for Generics, i think what you are asking is a basic thing that everyone would benefit and shouldn't be hard to implement but there are some PR's in standby that i see no one from staff paying attention to

underfisk avatar Dec 03 '20 02:12 underfisk

// a bit of helpers
export class Command<T> implements ICommand {
  private $resultType!: T
}

export class Query<T> implements IQuery {
  private resultType!: T
}
// declarations.d.ts - extension to @nestjs/cqrs
declare module '@nestjs/cqrs/dist/query-bus' {
  export interface QueryBus {
    execute<X>(query: Query<X>): Promise<X>
  }

  export type IInferringQueryHandler<
    QueryType extends Query<unknown>
  > = QueryType extends Query<infer ResultType>
    ? IQueryHandler<QueryType, ResultType>
    : never
}

declare module '@nestjs/cqrs/dist/command-bus' {
  export interface CommandBus {
    execute<X>(command: Command<X>): Promise<X>
  }

  export type IInferringCommandHandler<
    CommandType extends Command<unknown>
  > = CommandType extends Command<infer ResultType>
    ? {
        execute(command: CommandType): Promise<ResultType>
      }
    : never
}

Then

class SomeQuery extends Query<Dto[]> {
  constructor() {
    super()
  }
}


@QueryHandler(SomeQuery)
export class SomeQueryHandler
  implements IInferringQueryHandler<SomeQuery> { ... }

We often use such enhancement/path to not add additional generics but just letting to infer the result type.

(code credits to @Dyostiq)

kgajowy avatar Mar 24 '21 07:03 kgajowy

@ldiego08 and others:

we have released the package that may be useful to achieve the above:

https://github.com/valueadd-poland/nestjs-packages/tree/master/packages/typed-cqrs

kgajowy avatar Apr 27 '21 09:04 kgajowy

@kamilmysliwiec the solution provided by @kgajowy can be easily added to the nestjs/cqrs package with backward compatibility using overloading.

PoC https://github.com/nestjs-architects/cqrs/commit/053ebff8e6a3f95be9514d0d219d989a3585051b#diff-077f9c78eb3526c090ed295ae929fd52c7a9348c6b858566daafab0dc2bc70e3

Sikora00 avatar Oct 22 '21 08:10 Sikora00

MediatR (.NET) has this and it is sorely missed in my NodeJS projects.

mattheiler avatar Nov 24 '21 23:11 mattheiler

This would be extremely handy for project I am now working on. I think this should be bumped to higher priority, it would be a very well-received feature for reducing verbosity.

aeoncleanse avatar Dec 22 '21 13:12 aeoncleanse

@aeoncleanse you can already use this addon https://github.com/valueadd-poland/nestjs-packages/tree/master/packages/typed-cqrs

Sikora00 avatar Dec 22 '21 13:12 Sikora00

Out of curiosity, Now that nestJS 8 is here are you planning to release a new version of this or it's supported ? @Sikora00

mohit-singh-pepper avatar Feb 15 '22 08:02 mohit-singh-pepper

@mohit-singh-pepper I can release it this week. Basically it's just a change to the package.json. It works the same with --force install

Sikora00 avatar Feb 15 '22 09:02 Sikora00

@mohit-singh-pepper v1.0.0 released :) I removed dependency on any specific version of @nestjs/cqrs

Sikora00 avatar Feb 20 '22 10:02 Sikora00

In the meantime, I've been accomplishing this with the following (admittedly verbose) typing:

queryBus.execute<
  FooQuery,
  Awaited<ReturnType<FooQueryHandler['execute']>>
>(new FooQuery(fooId))

Awaited type comes from TypeScript 4.5.

chrislambe avatar May 23 '22 18:05 chrislambe

Is there any update here? We are really missing this functionality in our applications and would rather not depend on an external library for this. Seems like a very good option to add. I can also create the PR if needed.

lennartquerter avatar Oct 24 '23 13:10 lennartquerter

So was this abandoned or can we expect an update any time soon?

alexandru-dodon avatar Jan 21 '24 18:01 alexandru-dodon