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

Add metadata (the graphql.core Node) to GraphQLField and GraphQLObject. #3182

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

mgilson
Copy link
Contributor

@mgilson mgilson commented Oct 30, 2023

Clients can use this metadata do specializilation during the creation of client types.

Description

GraphQL is a very flexible language where custom directives can be added for a large number of purposes. It's possible that in some cases, a custom directive on a query object or field could be used to impact what types are emitted on a client. e.g. a client might want to make fields nullable if they have a@skip or @include directive. Since directives can be as diverse as the people who make them, we should allow that information to be passed to the client generator plugin which can then be used or ignored as necessary.

Since it's unclear what these custom directives would do or how someone might use them in the general case, for now, I'm just passing the whole graphql node for the field or type through. In my concrete use-case, I have reasons to want to supply default values to the generated types from the client -- e.g.:

@dataclass
class Foo:
     field: str = "Hello world"

Rather than

@dataclass
class Foo:
     field: str

which is what gets generated by my plugin currently.

My graphql would look something like:

query MyQuery {
    getFoo {
        field @defaultValue(value: "Hello world")
    }
}

And this is fine as long as the server is taught to ignore the directive (or the directive is stripped before the query actually gets issued which is how my plugin actually works).

Types of Changes

  • Core
  • Bugfix
  • New feature
  • Enhancement/optimization
  • Documentation

Issues Fixed or Closed by This PR

Checklist

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • I have tested the changes and verified that they work and don't break anything (as well as I can manage).

@botberry
Copy link
Member

botberry commented Oct 30, 2023

Thanks for adding the RELEASE.md file!

Here's a preview of the changelog:


Augment the codegen GraphQLObjectType and GraphQLField with the graphql.language.ast.Node that caused the
respective object to be created. This node can be introspected for additional metadata for codegen plugins to use
for specialization of type creation.

Here's the tweet text:

🆕 Release (next) is out! Thanks to Matt Gilson for the PR 👏

Get it here 👉 https://beta.strawberry.rocks/release/(next)

@codecov
Copy link

codecov bot commented Oct 30, 2023

Codecov Report

Merging #3182 (85bb731) into main (101ab96) will increase coverage by 0.00%.
The diff coverage is 97.72%.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3182   +/-   ##
=======================================
  Coverage   96.40%   96.41%           
=======================================
  Files         498      498           
  Lines       31137    31220   +83     
  Branches     3815     3822    +7     
=======================================
+ Hits        30017    30100   +83     
+ Misses        914      912    -2     
- Partials      206      208    +2     

@codspeed-hq
Copy link

codspeed-hq bot commented Oct 30, 2023

CodSpeed Performance Report

Merging #3182 will not alter performance

Comparing mgilson:code-gen-field-metadata (85bb731) with main (101ab96)

Summary

✅ 12 untouched benchmarks

@mgilson mgilson force-pushed the code-gen-field-metadata branch 3 times, most recently from 35577c3 to 590d320 Compare October 30, 2023 21:13
@strawberry-graphql strawberry-graphql deleted a comment from botberry Oct 30, 2023
@@ -32,6 +34,7 @@ class GraphQLField:
alias: Optional[str]
type: GraphQLType
default_value: Optional[GraphQLArgumentValue] = None
definition_node: Optional[Node] = None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you think it would be possible to use the strawberry type here instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do both...

The query is the part that is controlled by the client (they may not control the upstream strawberry type). I actually have a branch that I haven't quite gotten working that tries to build a client from only a graphql schema (then the Python client could be generated against things that don't even use strawberry as the graphql provider).

But... It would be nice to have access to the strawberry type where available. That's probably easier to work with than digging through the graphql AST...

@mgilson mgilson force-pushed the code-gen-field-metadata branch 5 times, most recently from f7631c6 to eb211c8 Compare October 31, 2023 14:29
pyproject.toml Outdated
@@ -1,7 +1,7 @@
[tool.poetry]
name = "strawberry-graphql"
packages = [ { include = "strawberry" } ]
version = "0.211.1"
version = "0.211.1.dev1"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
version = "0.211.1.dev1"
version = "0.211.1"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, that wasn't supposed to make it into the PR 😅

variable.name,
None,
variable_type,
definition_node=variable_definition,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this have strawberry_field as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm ... originally, I didn't think so ... but now that I think about it a little more, possibly ...

This represents the input variables to the operation. I guess this is represented by the query/mutation operation object?

Let me see what I can cook up.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just for more context, I'd like to keep as abstracted as possible 😊 we might change the internal representation (ie moving away from GraphQL) in future

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So ... the more I dig into this, the more that this feels weird (or at least really complex). For example, you could have a query like:

query ($skipTitle: Boolean!, $val: Int!) {
  queryPost(val: $val) {
    id
    title @skip(if: $skipTitle)
    text
  }
}

In this case, the $skipTitle variable field really has nothing to do with any of the types in the backend schema definition. The val field presumably does, but it's buried in the Query.query_post field's base_resolver (I guess you'd introspect the wrapped_func on that resolver to understand the backend metadata about this thing?)

For now, I think that I'll make the graphql_type on the {type}Variables point at the Query or Mutation class and then users can crawl that class however they want in their plugins. If we ever see more concrete use-cases, we can make better decisions on how we want to set these pieces of metadata to support those use-cases at that point.

Does that sound reasonable to you?

Comment on lines 66 to 67
definition_node: Optional[Node] = None
strawberry_type: Optional[StrawberryType] = None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder about the naming here :) I think I'd make something like this:

Suggested change
definition_node: Optional[Node] = None
strawberry_type: Optional[StrawberryType] = None
graphql_node: Optional[Node] = None
graphql_type: Optional[StrawberryType] = None

what do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I'm not picky about the naming here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's do the change then 😊

Copy link
Contributor Author

@mgilson mgilson Jan 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:face_palm: That's a little embarassing 😅

@mgilson mgilson force-pushed the code-gen-field-metadata branch from eb211c8 to c0921ac Compare November 3, 2023 14:57
@mgilson
Copy link
Contributor Author

mgilson commented Nov 3, 2023

It looks like pyright released a new version yesterday -- I'm guessing that's why all the pyright tests are now failing :).

womp womp.

@mgilson mgilson force-pushed the code-gen-field-metadata branch from c0921ac to 2295a61 Compare January 9, 2024 14:22
Copy link
Member

@patrick91 patrick91 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

almost ready to go :)

# (e.g. the ``Query`` or ``Mutation`` class and then we'll let consumers sort
# out how they want to use this metadata).
if variables_type.graphql_type is not None:
raise ValueError(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(same q for the one above) when could this happen?

if it is just a defensive check, let's add a nocover comment 😊

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is defensive to make sure we didn't mess something up.

These are the {Type}Variables objects that I mentioned in the other comments. Basically, at the client level, it's just a dataclass that describes the inputs to a strawberry resolver (rather than being an actual type on the strawberry/python side).

In that case, I made the decision to just set the graphql node to the Operation node and I set the graphql type to the query/mutation type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I think that it's a little more complex ... It maps to the variables in the operation

query FooQuery($id: Int, $fetchBar: Boolean) {
  foo(id: $id) {
    bar @skip(if: $fetchBar) {
      x
      y
    }
  }
}

The client is going to generate an object:

class FooQueryVariables:
    id: int
    fetch_bar: bool

This object doesn't map to any class/field on the strawberry/server side.

Comment on lines +555 to +565
variable.name,
None,
variable_type,
graphql_node=variable_definition,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we use named arguments here?

and could we find where the variable is used and attach that field here? Might not probably worth, but maybe we can leave a comment about that? (The current comment is a bit unclear to me)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried to update the comment to make it more clear. These are for the {Type}Variables objects which generally correspond to something like:

class Query:
    @strawberry.field
    def resolver_function(self, id: int, x: float) -> ReturnType:
        ...

The client is going to make a:

class QueryResolverFunctionVariables:
    id: int
    x: float

type of class (depending on the query generated). But in this case, there isn't a StrawberryField that maps 1-to-1 with the id parameter.

Comment on lines 66 to 67
definition_node: Optional[Node] = None
strawberry_type: Optional[StrawberryType] = None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's do the change then 😊

@@ -120,6 +130,14 @@ def get_person_or_animal(self) -> Union[Person, Animal]:
p_or_a.age = 7
return p_or_a

@strawberry.field
def get_person_with_inputs(self, name: str, age: int = 72) -> Person:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def get_person_with_inputs(self, name: str, age: int = 72) -> Person:
def get_person_with_inputs(self, name: str, age: int = 72) -> Person: # pragma: no cover

@mgilson mgilson force-pushed the code-gen-field-metadata branch 2 times, most recently from 7aab7e1 to 50dd99c Compare January 9, 2024 16:55
@mgilson mgilson force-pushed the code-gen-field-metadata branch from 75a1c7d to 06d3afb Compare March 12, 2024 14:14
… this metadata do specialize the creation (e.g. adding a `@defaultValue` directive to add a default value to the generated objects).
@mgilson mgilson force-pushed the code-gen-field-metadata branch from 796bd90 to 85bb731 Compare March 12, 2024 14:20
@mgilson
Copy link
Contributor Author

mgilson commented Mar 12, 2024

@patrick91 -- I believe that this is ready for another look when you have a few moments. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants