pylon
pylon copied to clipboard
Resolver Chains
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.
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.
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.
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?
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.
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.
@schettn do you want me to write a ticket for that?
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")
...
@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
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 🙏
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.