TriPSs / nestjs-query

Easy CRUD for GraphQL.
https://tripss.github.io/nestjs-query/
MIT License
152 stars 43 forks source link

Returning Relay-connection style responses from custom resolvers #247

Open rinormaloku opened 5 months ago

rinormaloku commented 5 months ago

How to convert the result of the database into a relay-connection style response? This is a workaround that I am currently using:

@Query(() => ActivitiesViewConnection)
  async activitiesViews(
    @Args() query: ActivitiesViewQuery,
  ): Promise<ConnectionType<ActivitiesView>> {

    // custom and complex querying logic
    let qb = this.filterQueryBuilder.select(query);
    const [result, total] = await qb.getManyAndCount();

    return ActivitiesViewConnection.createFromPromise(
      (q) => Promise.resolve(result),
      query,
      (q) => Promise.resolve(total),
    );
  }

I would prefer a cleaner way of mapping the result into a relay-connection style response

TriPSs commented 5 months ago

This looks about right, usually what I tend to do is something like this:

    return ActivitiesViewConnection.createFromPromise(
      (q) => this.customQueryService.customQuery(q),
      query,
      (q) => this.customQueryService.customCount(q),
    );

That way not all logic goes into the resolver but to it's query service instead.

rinormaloku commented 5 months ago

Thanks @TriPSs, that's good to know. However, there is a need for a utility function to create relay-connection style responses as the one used by nestjs-query

Digging deeper I noticed that resolvers with UnionTypes are not supported, e.g.:

@ObjectType()
export class Book {
  @Field()
  title: string;
}

@ObjectType()
export class Author {
  @Field()
  name: string;
}

export const ResultUnion = createUnionType({
  name: 'ResultUnion',
  types: () => [Author, Book] as const,
});

export const InfoQueryArgs = QueryArgsType(ResultUnion)
export const InfoConnection = InfoQueryArgs.ConnectionType;

You cannot create Query Args Type from the Union Type. This error provides more detail:

Argument of type 'Author | Book' is not assignable to parameter of type 'Class<unknown>'.
  Type 'Author' is not assignable to type 'Class<unknown>'.
    Type 'Author' provides no match for the signature 'new (...args: any[]): unknown'.ts(2345)

It makes sense for this not to work (QueryArgsType function expects DTOs), I just want to point out that there are no options to handle this case and still maintain the same API with the other resolvers?

Additionally, I looked into defining my types for PageInfo, Connection, Edges, and so on, but the issue is that it will clash with the predefined types by nestjs-query, and the internal implementations are not exported.

btw, if this is currently not supported, but you can give me some helpful pointers I can contribute this functionality.

TriPSs commented 5 months ago

Interesting, I think a union type could be supported but then you would need to overwrite a lot of the service etc.

Additionally, I looked into defining my types for PageInfo, Connection, Edges, and so on, but the issue is that it will clash with the predefined types by nestjs-query, and the internal implementations are not exported.

We could checkout what you need exactly and just export them so it will make it easier for ppl to reuse, PR would awesome!

rinormaloku commented 4 months ago

Btw, maybe this is a noob question (this might give you some perspective on how to answer it xD)

Let's start with a fresh plate and put aside the union types.

Using nestjs-query how can we define the following types:

@ObjectType()
class AuthorEdge {
    @Field(type => Author)
    node: Author;

    @Field()
    cursor: string;
}

@ObjectType()
class AuthorConnection {
    @Field(type => PageInfo)  ## ATTENTION
    pageInfo: PageInfo;       ## ATTENTION

    @Field(type => [AuthorEdge])
    edges: AuthorEdge[];
}

The issue is that we don't have access to the PageInfo type (or do we?).

Adding the types manually will fail because they are already defined by nestjs-query, i.e. this is not possible:

@ObjectType()
class PageInfo {
    @Field()
    hasNextPage: boolean;

    @Field()
    hasPreviousPage: boolean;

    @Field({ nullable: true })
    startCursor: string;

    @Field({ nullable: true })
    endCursor: string;
}

So what options do we have for a custom nestjs resolver that doesn't use nestjs-query to return the same response types (Utilizing Connections, Edges, and Nodes)?

Why do we need this? This is for those cases which you are not going to use Nestjs-Query and will create a resolver directly with Nestjs. You might do that for various reasons, for e.g. a feature that's not supported by nestjs-query such as InterfaceTypes, UnionTypes, or because for a specific resolver you don't need the nestjs-query features and you opt to go for vanilla nestjs defined resolvers.

However you still want to expose a uniform API. i.e. pagination should be identical for resolvers defined by nestjs-query and by your custom resolvers in nestjs.

Let me know if I can clarify this, and as soon as you give me some direction, I can get started on contributing.

TriPSs commented 4 months ago

If I understand it correctly, you could just follow Custom Endpoints from the docs? There is explained on how to create the return type with page info etc, you are not required to use the .createFromPromise; how you eventually return that response is up to you as long as it matches the connection.

To answer alteast one of your questions: we indeed to not export PageInfoType, these are created with getOrCreatePageInfoType for example, not sure why we do that as it's just a normal class which we probably could just export and use.