Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolver Chains #58

Open
LowLifeArcade opened this issue Dec 24, 2024 · 10 comments
Open

Resolver Chains #58

LowLifeArcade opened this issue Dec 24, 2024 · 10 comments
Assignees
Labels
💬 talk: discussion Open for discussions and feedback

Comments

@LowLifeArcade
Copy link

LowLifeArcade commented Dec 24, 2024

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.

@schettn
Copy link
Contributor

schettn commented Dec 24, 2024

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 schettn self-assigned this Dec 24, 2024
@schettn schettn added the 💬 talk: discussion Open for discussions and feedback label Dec 24, 2024
@LowLifeArcade
Copy link
Author

LowLifeArcade commented Dec 24, 2024

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
Copy link
Author

LowLifeArcade commented Dec 25, 2024

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?

@schettn
Copy link
Contributor

schettn commented Dec 26, 2024

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.

@LowLifeArcade
Copy link
Author

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
Copy link
Author

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

@schettn
Copy link
Contributor

schettn commented Dec 26, 2024

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
Copy link
Contributor

schettn commented Dec 26, 2024

@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

@LowLifeArcade
Copy link
Author

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
Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
💬 talk: discussion Open for discussions and feedback
Projects
None yet
Development

No branches or pull requests

2 participants