-
Notifications
You must be signed in to change notification settings - Fork 152
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
Feature: Add Required Fields For Custom Resolver #842
Feature: Add Required Fields For Custom Resolver #842
Conversation
This is a really innovative solution @dmoree, excited to take a look at this during the week! |
Before I even jump into code review, it looks like this sentence hasn't been removed from the docs in this PR. Can you find and remove it? 🙂 |
Thanks for the documentation updates. The preview documentation has now been torn down - reopening this PR will republish it. |
I was working on something similar for a while, but had to stop because other things came up. One thing I had in my implementation was to also be able to require nested fields: type Offer {
price: Money! // @relationship etc
vat: Money! // @relationship etc
gross: Int! @ignore(requires: [ "price.amount", "vat.amount" ])
}
type Money {
amount: Int!
currency: String!
} This is just an example of course, but it would make a few use cases we have possible. My code is here, but it's really not finished or cleaned up (and it might contain some other things I was working on). |
Thanks for the suggestion @mhlz. I really like the I considered something similar, but was using a selection set instead of the array in the PR. It would have looked something like: type User {
firstName: String!
lastName: String!
fullName: String!
@ignore(
require: """
{
firstName
lastName
}
"""
)
} This generalization would allow for similar use cases that you describe. Such that, if the schema is: type User {
id: ID!
username: String!
posts: [Post!]! @relationship(type: "HAS_POST", direction: OUT)
}
type Post {
id: ID!
title: String!
content: String!
author: User! @relationship(type: "HAS_POST", direction: IN)
likes: [User!]! @relationship(type: "LIKED_POST", direction: IN)
} one could add a field with a custom resolver that can traverse the graph: extend type User {
customResolver: String!
@ignore(
require: """
{
id
posts {
id
title
likes {
id
}
}
}
"""
)
} I'm not sure why one would want this particular selection, but in any case the resolver: const resolvers = {
User: {
customResolver: (source) => {
// get unique user ids of those that have liked any post
const userIds = Array.from(
new Set(
source.posts.map((post) => post.likes.map((like) => like.id)).flat()
)
)
// do something with userIds
},
},
} There were two immediate issues (probably more): validation and arguments. Both, I think, are solvable. ValidationMy attempt at validation was to use the const ignoreValidationErrors = nodes
.filter((node) => node.ignoredFields.length)
.map((node) =>
node.ignoredFields
.filter((ignoredField) => ignoredField.require)
.map((ignoredField) => validate(schema, gql`query { ${node.plural} ${ignoredField.require} }`))
.flat()
)
.flat();
if (ignoreValidationErrors.length) {
throw new Error(ignoreValidationErrors[0].message);
} The drawback was that this is relying on a root This was the approach because I couldn't find a way to validate a selection set against a particular type. It seems hacky. Ideally, it would be something like If validation is accounted for, then ArgumentsIf one were to query query ($postTitle: String!) {
users {
id
username
posts(where: { title: $postTitle }) {
id
title
content
likes {
id
}
}
customResolver
}
} This should throw an error because I apologize for the long post, but as you can see it ended up being a nontrivial problem. I don't want to trivialize this PR, but it is relatively straightforward and satisfies the initial request in #204. Perhaps in the future this could be revisited. |
Using SelectionSets is also a nice idea! And I also agree that this doesn't have to be part of this PR, just wanted to put the idea somewhere :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great job. It is almost good to go IMO.
I've made a few comments regarding some areas where code complexity could be reduced, would love to get your thoughts on these before merging @dmoree
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
I'm not sure I like the naming of Is it only me? Personally I think |
It's not just you. I went back and forth on |
I think the real issue for me here is
is much more clear imo. Sorry for the curve ball here @angrykoala and @dmoree. |
No worries at all. Naming is important. If the directive were to change, it should be more along the lines of type Person {
firstName: String!
lastName: String!
customField: String! @computed
} seems like the library should be doing something as opposed to the user taking responsibility for the field. (I always thought |
Yeah, you're right on the Not sure what the team thinks though. |
@dmoree @oskarhane |
This is tricky! Whilst we do have 3.0.0 coming up, we should still keep breaking changes to a minimum if possible, and renaming I always look at our directives as "instructions" to our library - so always saw it that the |
I think this may be slightly confusing here because it is true that I like the concept of directives as "instructions", I'm not so sure if |
Hi @dmoree , @oskarhane @darrellwarde As of now, it seems that |
…thub.com/dmoree/graphql into feature/ignore-directive-required-fields
@angrykoala @darrellwarde @oskarhane |
Co-authored-by: angrykoala <angrykoala@outlook.es>
Description
Proposed solution for #204 that adds a
require
argument to@ignore
thereby making the definition:If any field that is marked with
@ignore(require: ["field1", ...])
is found in the selection set of the parent node, then the fields in therequire
argument will be added to the fields in theresolveTree
that ultimately get projected and only if those fields are not already in the selection set. This ensures that regardless if all fields are selected, they will be available on the parent object in the resolver.Current Implementation of
@ignore
Taking the example from the documentation:
It is accompanied by a note:
As @litewarp pointed out in the request, this is inefficient from a GraphQL perspective.
Proposed Implementation of
@ignore
If the
typeDefs
were then defined as:The resolvers will then have access to the fields on the
source
objectWhich means even if the query were for example:
The field
fullName
would resolve correctly.Issue
Checklist
The following requirements should have been met (depending on the changes in the branch):