pylon icon indicating copy to clipboard operation
pylon copied to clipboard

Resolver Chains

Open LowLifeArcade opened this issue 11 months ago • 10 comments

Is your feature request related to a problem? Please describe. The ability to create resolver chains prevents over fetching data

Describe the solution you'd like create a resolver that is for a nested data type that we can conditionally get data for based on the query

type Details = {
    rank: number;
}

type User = {
    name: string;
    details?: Details;
}
export const graphql = {
    Query: {
        users(type) {
            return db.getUsers(type);
        },
    },
    Details: {
        rank(context) {
            return db.getUserRank(context.id)
        },
    },
}

// this won't fetch details and prevents over fetching data

query MyQuery {
  users(type: "players") {
    name
  }
}

Describe alternatives you've considered This comes from resolver chains in Apollo: https://www.apollographql.com/docs/apollo-server/data/resolvers#resolver-chains

Additional context I've started integrating Pylon in a large production echo system as I wanted to use hono and this was an obvious solution for that. I'm finding this part tricky as the problem I'm trying to solve is we have a lot of data we're pulling and the goal is to seperate it all out in chunks based on the query so we don't overfetch data.

LowLifeArcade avatar Dec 24 '24 02:12 LowLifeArcade

This is a bit tricky to solve with resolver chains because for that you have to explicitly set / know the typename of the type.

In Pylon, the schema is generated based on the signature of the queries. So in order to get the details relation, the db.getUsers(type) must resolve the details in the first place.

In order to resolve over fetching, Pylon allows you to use functions, that are only executed when also queried.

So take a look at the following example:

export const graphql = {
    Query: {
        user: {
            return {
               "name": "John",
               details: () => {
                   return {
                      "rank": 1
                   }
               }
            }
        },
    },
}

Here, the details is only executed when also defined in the graphql query.

Going back to your example, the db.getUsers(type) could return a array of user objects where each object also contains the details function.

schettn avatar Dec 24 '24 12:12 schettn

So the idea would be more like this:

export const graphql = {
    users: {
        return [
            {
                "name": "John",
                id: 11,
            },
            {
                "name": "Bob",
                id: 12,
            },
        ]
    },
    Details: {
        details: (context) => {
            const rank = db.getUserRank(context.id);

            return {
                rank,
            }
        }
    },
}

Which is dealing with nested objects in an array of users.

But from what you're showing me, it sounds like this would work and not call getUserRank unless in the query.

export const graphql = {
    users: {
        return [
            {
                "name": "John",
                id: 11,
                details: {
                   rank: () => db.getUserRank(11)
                },
            },
            {
                "name": "Bob",
                id: 12,
                details: {
                   rank: () => db.getUserRank(12)
                },
            },
        ]
    },
}

But more specifically would this code work?:

export const graphql = {
    users: async (type: string) => {
        const users = db.getUsers(type);

        return users.map(user => ({
            ...user,
            details: {
               rank: () => db.getUserRank(user.id)
            },
        }))
    },
}

This is the usecase of my problem as we're getting lots of records with nested objects I don't want to collect unless in the query.

EDIT: I tested it and it does only call getUserRank if rank is in the query.

LowLifeArcade avatar Dec 24 '24 17:12 LowLifeArcade

I'm not really finding this as a solution for my needs. There is a case where we have a large record in a table in JSON I want to pick certain fields from in a sql query. Something like this (very contrived example):

async getUserDetails(userId: number): Promise<Details> {
    const details = { // available fields
        rank: 'managed.profile',
        foo: 'sub.profile',
        bar: 'sub.profile',
    };

    let items  = Object.keys(details)
        .map((item) => `JSON_VALUE(detail, '$.${details[item]}.${item}') AS ${item}`)
        .join(', ');

    const sql = `
        SELECT ${items}
        FROM user_details
        WHERE id ?`;

    // graphQL query only selecting rank and foo outputs: "SELECT JSON_VALUE(detail, '$.managed.profile.rank') AS rank, JSON_VALUE(detail, '$.sub.profile.foo') AS foo FROM user WHERE id = ?"

    return await this.query(sql, [userId]);
}
export const graphql = {
    Query: {
        users: async (name: string) => {
            const users = await db.getUsers(name); // gets fuzzy match

            return users.map(async (person) => {
                return {
                    ...person,
                    details: async () => await db.getUserDetails(person.id), // this call would do the logic above
                };
            });
        },
    },
};

results in: query only selecting rank and foo outputs: "SELECT JSON_VALUE(detail, '$.managed.profile.rank') AS rank, JSON_VALUE(detail, '$.sub.profile.foo') AS foo FROM user WHERE id = ?"

It seems the only way I can generate that query is to know the graphQL query ahead of time. Is this even possible to do something like this with GraphQL let alone Pylon?

LowLifeArcade avatar Dec 25 '24 02:12 LowLifeArcade

Are multiple separate sql calls an option? If so, you could do something like that:

return users.map(async (person) => {
                return {
                    ...person,
                    details:  {
                       rank: async () => await db.getUserDetails(person.id, "rank"),
                       foo: async () => await db.getUserDetails(person.id, "foo"),
                       ...
                    }
                };
            });

A optimal solution, but more complex, would be to use the GraphQLResolveInfo. For example: https://medium.com/@shulha.y/how-to-utilize-your-graphql-query-to-improve-your-database-query-cfc1b483712f

I could add a feature to Pylon that allows you to access the info object.

schettn avatar Dec 26 '24 12:12 schettn

A optimal solution, but more complex, would be to use the GraphQLResolveInfo. For example: https://medium.com/@shulha.y/how-to-utilize-your-graphql-query-to-improve-your-database-query-cfc1b483712f

I could add a feature to Pylon that allows you to access the info object.

Oh wow. That is actually exactly what I was looking for. That would be amazing if you could add that.

LowLifeArcade avatar Dec 26 '24 16:12 LowLifeArcade

@schettn do you want me to write a ticket for that?

LowLifeArcade avatar Dec 26 '24 19:12 LowLifeArcade

Sure, that would be nice. I have just started to work on this.

I will expose the info via the context for now. You have to implement the helper method from the blog post yourself. At some point I think I might add this helper function to pylon.

Will the following for you?:

import {getContext} from "@getcronit/pylon"

...
const ctx = getContext()
const info = ctx.get("graphqlResolveInfo")
...

schettn avatar Dec 26 '24 19:12 schettn

@LowLifeArcade I will also create a new example that showcases this new feature and the helper methods from the blog.

import { app } from '@getcronit/pylon'
import { getResolvedFields } from './get-resolved-fields'

const getUser = (): {
  firstName: string,
  lastName: string,
  username: string
} => {
  const fields = getResolvedFields()

  return {
    firstName: fields.nestedFields.user.flatFields.includes("firstName") ? "John" : "",
    lastName: fields.nestedFields.user.flatFields.includes("lastName") ? "Doe" : "",
    username: fields.nestedFields.user.flatFields.includes("username") ? "johndoe" : ""
  }
}

export const graphql = {
  Query: {
    data: () => {

      const user = getUser()

      console.log("Got user", user)

      return {
        user,
      }
    }
  },
  Mutation: {}
}

export default app

schettn avatar Dec 26 '24 20:12 schettn

That looks great @schettn. And exposing through the context object looks like it will work nicely. I appreciate that.

I'll create a ticket for you for the full feature you're working on 🙏

LowLifeArcade avatar Dec 26 '24 20:12 LowLifeArcade

Wow. I didn't expect you to deploy so soon after making the change. Thank you for turning this all around so fast! This helps me so so much. I really appreciate this.

LowLifeArcade avatar Dec 26 '24 23:12 LowLifeArcade