Graphback: Parent parameter to resolver does not correctly return relation in selection set

Created on 28 Jul 2020  路  20Comments  路  Source: aerogear/graphback

To get certain of my schema directives to work properly, I need to modify the selection set of incoming queries and add some selections. I am currently doing this by intercepting all resolvers with a simple proxy function. In that function, I take the info object and modify the AST to add in the selections that I want. I don't know any other way of modifying the selection set before a resolver resolves. This worked just fine for me when I did this with Prisma, but with Graphback things are slightly broken. I can modify the selection set just fine (essentially I just modify info.fieldNodes and change the AST there), but any selections with relations seem to not return properly.

For example, if I have the following incoming query:

query {
  findCompanies {
    id
  }
}

I need to modify it to return the organization and the owner, before it hits the resolvers. So I modify the info object's AST to reflect the following query:

query {
  findCompanies {
    id
    organization {
      owner {
        id
      }
    }
  }
}

The parent variable in my schema directives should then have an object representing the selection set. But, the parent variable returns an object that looks like this:

{
  id: "1",
  organization_id: "1"
}

The relation isn't properly retrieved for some reason. I've tried this with one or two other relations, and the same thing happens. Only the id of the relation is returned.

I believe this is a bug with Graphback, but also, if there is a better way of changing the selection set of a query before it is resolved and before the schema directives are called, I would love to know.

This is a very urgent issue for me.

triage

All 20 comments

Actually, the issue might be even simpler than I explained above. If I do a query without modifying the info object, I seem to get the same result. In my schema directive, the parent variable value does not have the relation I want, it only returns the relation's id.

Can I get an ETA on this or some more information about a possible fix? We really need this fixed ASAP, if the team doesn't have time for it, would you accept a pull request?

We have talked about this issue today with the team. We have a feature to build selection sets based on info objects, however, if you are using subscription this feature is disabled and you will get the entire root object that is then later filtered out by graphql selection sets provided by client. Editing info inside resolvers is very risky as it affects only that particular resolvers.

As for relationships, I believe that override we are doing should happen in relation to resolver rather than root resolver.
Company: {organization:...} from here you can control what is being fetched, but I believe that for simplification we always query entire object in the relationship.

Graphback offers amazing support for plugins - if you use client-side generator you can add your extra selections based on directives to the client side queries. You can also extend selection sets directly. Using an info object is not the recommended way to do it as it is works only for local context (current resolver). This explains why your relationship is not working (as this is handled by relationship resolvers that we need to override as well.)

What you are getting is the id that is being passed to a batch method that prevents from issuing extra queries (dataloader) and that has original info object (not the one that is modified.

Generally, this seems like xy problem so we need to get more information on how we can help. Graphback is the most extensible platform that was created so I'm pretty sure we will be able to handle your case.
It is not recommended to override resolvers where we can actually create plugin to handle resolvers based on your annotations/directives.

Recommended options supported by our team at the moment

Override schema plugin to add extra selections for whatever is needed based on
annotations: mhttps://github.com/aerogear/graphback/blob/master/packages/graphback-codegen-schema/src/SchemaCRUDPlugin.ts#L507

We can have plugin without package. Just create new class in the code and do what you want to do. The advantage of the plugin is that you can override the entire implementation or reuse some of the pars of it, add extra fields etc.

Alternative for generic queries

Client crud plugin that you can extend to add extra selection sets
https://github.com/aerogear/graphback/blob/master/packages/graphback-codegen-client/src/ClientCRUDPlugin.ts

Plugin docs:
https://graphback.dev/docs/plugins/create

I can create plugin for you if needed if we will know how this should work.]

We can also chat how we can help you by changing code our our side

Thanks a lot! I'll parse this and get back with questions if I have any

Alright, so I'm thinking the plugin route is best. An example from you would really help. Here's part of what I am trying to accomplish, as an example.

In my schema directive resolvers, I need the parent object's selection set to be augmented on certain queries. For company queries, getCompanies and findCompanies, I always need

organization {
  owner {
    id
  }
}

added to the selection set before it reaches the schema directive resolvers.

So in this case, for a client-side query that looks like this:

query {
  getCompanies(id: "1") {
    id
  }
}

The parent variable in the schema directive resolver would look like this:

{
  "id": "1"
  "organization": {
    "owner": {
       "id": "5"
    }
  }
}

As another example, for calls queries, getCalls and findCalls, I always need the selection set augmented like so before reaching the schema directive resolvers:

companies {
  company {
    organization {
      owner {
        id
      }
    }
  }
}

These are just some examples. I cannot foresee in all of the ways I will need to augment the selection set before my schema resolvers are called, I just need a general purpose way of augmenting the selection set for each of my object types. Doing this statically is fine, as far as I know I will always know how the selection set needs to be augmented before runtime.

Ok. I will push some changes in branch so you can try this out.

Why not augument this in the client?
I have created example here on how schema/resolver can be extended:
https://github.com/aerogear/graphback/pull/1770/files

There is also a batching function on that class

I really appreciate the help. I have a couple questions about https://github.com/aerogear/graphback/pull/1770/files.

How do I import SchemaCRUDPlugin, SchemaCRUDPluginConfig, the import in the example is relative. Are these bindings exported somewhere where I can easily import them?

I'm still a little lost on where I modify the selection set to grab the relation instead of just the relation's id. I see this:

      // extend info
      // https://github.com/graphql/graphql-js/blob/7b3241329e1ff49fb647b043b80568f0cf9e1a7c/src/execution/execute.js#L680
      // info = buildResolveInfo(...)
      return context.graphback.services[modelName].findBy(args.filter, { ...context, graphback }, args.page, args.orderBy)

So should I still be updating the info object? Am I supposed to use buildResolveInfo to do it? What do I do with the info object once I have created a new one? Or am I supposed to just mutate the current info object?

This issue it detached from graphback itself - it is trying to override graphql internal selection sets. I have suggested plugins as way to handle this - preferably on client side. On server side we will need to extend this info no matter what but this is risky and will require us to extend relationship info objects as well.
I wanted to make sure that we are getting all the possibilities that we could have here.

So should I still be updating the info object? Am I supposed to use buildResolveInfo to do it?

I haven't done that so just assumed that you were using buildResolveInfo or something to extend info and added it as an example to the resolver.

Why not augument this in the client?

There are at least two reasons I can think of. The most important is that it doesn't work. Like I mentioned above:

Actually, the issue might be even simpler than I explained above. If I do a query without modifying the info object, I seem to get the same result. In my schema directive, the parent variable value does not have the relation I want, it only returns the relation's id.

Even if I don't modify the info object, and I have a query with a relation in it, the parent object does not have the relation's selection set in the schema directive resolver.

Also, I want these selection set additions added at the server, because I want them enforced without clients having to worry about them. The selection set info is not always needed by the client, it's for authorization purposes server-side, so the client might be requesting info in the selection set that they don't need. This will lead to unnecessary code and confusion for clients. I also can't foresee all of the clients we will be serving, and I want to enable simple GraphQL queries across a communication medium like http, I don't want to have to ship a library or specialized queries to my clients.

This issue it detached from graphback itself

Because the selection set is not being properly returned in the parent object for schema directives, this seems like a Graphback issue. Even if I don't attempt to modify the info object, this problem persists.

If the parent object in my schema directives at least had the entire relation selection set from the client-side query, I could probably work with that for now.

Because the selection set is not being properly returned in the parent object for schema directives

I do not understand it fully. I do not see any directives etc.
I will act on the info provided above.

I see some examples above on adding selection sets (adding to info object)
So the way to go forward would be to:

A) Return all data inside root query (including relationship) for this specific operations and extending info object (overriding what graphback offers)
B) Provide wrapper for graphback root and batch queries - relationship resolver info object. Literally what was done but we need to add an extra wrapper for relationship query to extend info.

I'll add some more information just to be safe.

Example model schema:

""" @model """
type User {
  id: ID!
  email: String! @read
  company: Company!
}

""" @model """
type Company {
  id: ID!
  name: String!
}

Schema directive for @read:

import { SchemaDirectiveVisitor } from 'graphql-tools';
import { defaultFieldResolver } from 'graphql';

export class ReadDirective extends SchemaDirectiveVisitor {
    visitFieldDefinition(field: any, details: any) {
        const originalResolver = field.resolve || defaultFieldResolver;
        field.resolve = async (
            parent,
            args,
            context,
            info
        ) => {
           console.log('parent', parent);
           // The parent object should have a company property with the name property

           // do authorization work or whatever else here

           return await originalResolver(parent, args, context, info);
        };    
    }
}

Example query coming from the client:

query {
  getUser(id: 1) {
    email
    company {
      name
    }
  }
}

In the process of the above query being resolved, the resolver defined in ReadDirective.visitFieldDefinition should log the parent like so:

{
  "email": "[email protected]",
  "company": {
    "name: "Company 1"
  }
}

But based on my testing so far, the parent object looks like the following:

{
  "email": "[email protected]",
  "company_id": "1"
}

This is without me trying to change the info object in any way. It seems that as Graphback is right now, the parent object in a schema directive resolver does not have relationship info, but I would like that info to be there in whatever way is best.

{
  "email": "[email protected]",
  "company_id": "1"
}

This is the only result for the current resolver.
Relationship resolver will be hit and it will use parent.company_id.
This is very classical dataLoader stuff so if we have relationship we cannot assume that query will be executing only root function - individual fields will be executed as well.

A) Return all data inside root query (including relationship) for this specific operations and extending info object (overriding what graphback offers)
B) Provide wrapper for graphback root and batch queries - relationship resolver info object. Literally what was done but we need to add an extra wrapper for relationship query to extend info.

Do these two choices remain the best options in your opinion?

For B, can you provide an example of adding that extra wrapper for the relationship query to extend info?

Do these two choices remain the best options in your opinion?

While in graphback we want to help. This will be the same problem if you will develop your own resolvers with data loader support. So with that in mind, we can ask around for the best option as while we want to help here - this is not specifically graphback problem. Graphback is only giving some resolvers out of the box.

as for example of B.
This is classical dataloader problem, meaning if your query operates on User object and there is company relationship graphback will generate relationship resolver.

```ts
{
User: {
company: (parent, args, context, info){
// Relationship will have it's own selection set/info
info = someInfoMagic();
return db.find(parent.company_id)
};
},
getUser(parent, args, context, info){
// if we wrap only this we not going to hit relationship.
info = someInfoMagic();
return {
"email": "[email protected]",
"company_id": "1"
}
};
}

I'm starting to see now, thank you for working through this with me

Do you know how I can access the final resolved result before it is sent back to the client? I thought that the resolver for something like getUser would have the final result, but it still has unresolved relations. I'm just wondering how to access the final totally resolved result, not sure which resolver would return that

So I switched from my own custom permissions system to using graphql-shield. As you have mentioned, this problem is not graphback-specific, but specific to the dataloader patten. Indeed the problem rears its head when using graphql-shield as well: https://github.com/maticzav/graphql-shield/issues/879

I'd like to raise the idea of creating a performant way to add fragments with relations to certain queries. Otherwise, certain classes of permissions that use resolvers, such as using schema directives or graphql-shield rules, just lose an entire class of permissions. The issue I linked to above has an example of the types of permissions I'm trying to implement.

Closing this as it is not a Graphback problem and appears to be resolved on our side. Feel free to reopen!

Was this page helpful?
0 / 5 - 0 ratings