Dynamic TypeORM query building to resolve any GraphQL request

See original GitHub issue

For some weeks, I’ve been wondering if the following would be possible.

The idea

Given a GraphQL request, is it possible to dynamically build a TypeORM query from GraphQLResolveInfo context variable data, that returns all information asked for at once?

One single graphql query, one single resolver, one single sql query. I know that this breaks completely GraphQL philosophy, so I don’t know how useful or efficient this is, if it is. But I find it interesting so I want to know what you guys think about it.

I finally managed to get it working. This is how I did it.

Example

This is what I request:

query {
  getUser(userId: 1) {
    userId
    firstName
    lastName
    posts {
      postId
      title
      pages {
        pageId
        content
      }
    }
  }
}

This is the SQL query that would generate:

query: SELECT
    `User`.`userId` AS `User_userId`,
    `User`.`firstName` AS `User_firstName`,
    `User`.`lastName` AS `User_lastName`,
    `User_posts`.`postId` AS `User_posts_postId`,
    `User_posts`.`userId` AS `User_posts_userId`,
    `User_posts`.`title` AS `User_posts_title`,
    `User_posts`.`postId` AS `User_posts_postId`,
    `User_posts`.`title` AS `User_posts_title`,
    `User_posts_pages`.`pageId` AS `User_posts_pages_pageId`,
    `User_posts_pages`.`postId` AS `User_posts_pages_postId`,
    `User_posts_pages`.`content` AS `User_posts_pages_content`,
    `User_posts_pages`.`pageId` AS `User_posts_pages_pageId`,
    `User_posts_pages`.`content` AS `User_posts_pages_content`,
    `User_posts`.`postId`, `User_posts`.`title`, `User_posts_pages`.`pageId`,
    `User_posts_pages`.`content`
FROM `user` `User`
LEFT JOIN `post` `User_posts` ON `User_posts`.`userId`=`User`.`userId`
LEFT JOIN `page` `User_posts_pages` ON `User_posts_pages`.`postId`=`User_posts`.`postId`
WHERE `User`.`userId` = ? -- PARAMETERS: [1]

Imagine user with id 1 has 100 posts, and each one has 200 pages. This would be 1 + 100 resolvers that will result in 101 queries to the database. Using a dataloader you could reduce it to 101 resolvers and 2 queries. This method returns all data in one single query.

Explanation

  • Firstly, I take GraphQLResolveInfo context object, and I extract the GraphQL query tree I can work with.
  • Secondly, I create a query builder and I recursively build the TypeORM query selecting asked fields and joining relations.
  • Finally I execute the query returning all asked data, and no other resolvers are called.

The generated query is built only with the neccessary data. The idea is similar to TypeORM eager relations, loads relation data when the entity is loaded. The key of this method is that it does the same with all relations (whether or not are eager) depending if they are asked in the graphql query, and with which specific fields.

This allows you to transform dynamically any graphql possible request into a SQL single query.

Source code

Check the code and test it yourself here

Notes

Of course, I’m just trying new things. This method may not work in many other cases, and I don’t pretend to.

What I’m looking for next

Imagine the possibility not only of building the query, but of adding filters and options as it is built.

Example:

query {
  getUser(userId: 1) {
    userId
    firstName
    lastName
    posts(tag: "important", orderAscBy: "postId") {
      postId
      title
      pages(orderDescBy: "pageId") {
        pageId
        content
      }
    }
  }
}

This would generate:

query: SELECT
    `User`.`userId` AS `User_userId`,
    `User`.`firstName` AS `User_firstName`,
    `User`.`lastName` AS `User_lastName`,
    `User_posts`.`postId` AS `User_posts_postId`,
    `User_posts`.`userId` AS `User_posts_userId`,
    `User_posts`.`title` AS `User_posts_title`,
    `User_posts`.`tag` AS `User_posts_tag`,
    `User_posts`.`postId` AS `User_posts_postId`,
    `User_posts`.`title` AS `User_posts_title`,
    `User_posts`.`tag` AS `User_posts_tag`,
    `User_posts_pages`.`pageId` AS `User_posts_pages_pageId`,
    `User_posts_pages`.`postId` AS `User_posts_pages_postId`,
    `User_posts_pages`.`content` AS `User_posts_pages_content`,
    `User_posts_pages`.`pageId` AS `User_posts_pages_pageId`,
    `User_posts_pages`.`content` AS `User_posts_pages_content`,
    `User_posts`.`tag`, `User_posts`.`postId`, `User_posts`.`title`,
    `User_posts_pages`.`pageId`, `User_posts_pages`.`content`
FROM `user` `User`
LEFT JOIN `post` `User_posts` ON `User_posts`.`userId`=`User`.`userId`
LEFT JOIN `page` `User_posts_pages` ON `User_posts_pages`.`postId`=`User_posts`.`postId`
WHERE `User`.`userId` = ? AND `User_posts`.`tag` = ?
ORDER BY `User_posts`.`postId` ASC, `User_posts_pages`.`pageId` DESC -- PARAMETERS: [1,"important"]

I’ve already done this, and it works but I have some problems with it. Firstly, as second, third, etc resolvers levels are not neccessary, I don’t know how to define entity fields arguments. If I create resolvers just for TypeGraphQL to build the schema (resolvers that are supposed to not be used anymore), they are called anyways to get data I already got in my built generated query. The only way I found to define field arguments is in the entity itself.

This way:

@ObjectType()
@Entity()
export class User {

    @Field(() => Int)
    @PrimaryGeneratedColumn({ type: "bigint" })
    public userId: number;

    @Field()
    @Column()
    public firstName: string;

    @Field()
    @Column()
    public lastName: string;

    @Field(() => [Post], { nullable: true })
    @OneToMany(() => Post, (post) => post.author)
    @JoinColumn({ name: "userId", referencedColumnName: "userId" })
    public posts(@Args() {}: PostFilterArgs): Promise<Post[]> { return; }
}

Note that the “fake” resolver it just returns and does nothing. That’s what I don’t like, and it gives errors sometimes I have no idea what to do about.

You can check this anyway at the same code above just with some small modifications here

Conclusion

I don’t know how stupid this idea is, or if is it useful, if already exists, but it worked for me. I want to know your opinion, how could we improve this idea, and if it would be a nice feature.

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Reactions:4
  • Comments:13 (3 by maintainers)

github_iconTop GitHub Comments

17reactions
iamgmdcommented, Oct 10, 2019

Finally I managed to resolve it myself. It works perfect for me now. Thanks anyways.

@david-eos Would really like to know how you resolved this, can you share?

7reactions
david-eoscommented, Aug 23, 2019

Finally I managed to resolve it myself. It works perfect for me now. Thanks anyways.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Typeorm dynamic query builder from structured object
Inside my resolver I feed the args.where through a function to parse the structure and utilize TypeOrm's query builder to convert it to ......
Read more >
Resolvers | NestJS - A progressive Node.js framework
Resolvers provide the instructions for turning a GraphQL operation (a query, mutation, or subscription) into data. They return the same shape of data...
Read more >
Implementing pagination with GraphQL in NestJS
gql allows us to build a GraphQL query with functionality like dynamic variable replacement. The GET_USERS variable is a query that requests a ......
Read more >
Virtual Column solutions for TypeORM | Adrian Pietrzak
This issue has not been officially resolved so far. In this article, I will show you all possible ways to deal with this...
Read more >
API with NestJS #28. Dealing in the N + 1 problem in GraphQL
Graphql query. Above, we request an author for every post. One way to do so would be to use the @ResolveField() decorator that...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found