-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Support for multiple @key directives in federation (reworked) #1723
Support for multiple @key directives in federation (reworked) #1723
Conversation
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.
Looks pretty good to me. Do you feel like you're ready for review at this point? I can ask @MiguelCastillo if he has any more suggestions before merging.
So @MiguelCastillo had some specific issues he found in the last one, so happy to wait for his input. I pinged him on discord about it. I'll remove the "draft" status and we can go from there. |
plugin/federation/federation.go
Outdated
// | ||
for _, keyField := range r.KeyFields { | ||
if len(keyField.Field) == 0 { | ||
fmt.Println("skipping field " + keyField.Definition.Name + " in " + r.ResolverName + " in " + e.Def.Name) |
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.
Is this output needed?
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.
i'd think so. I think it would be useful to log why we are skipping a @key
. tho I would treat this more as an error, but leaving as is for now is good idea.
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.
@carldunham let's add the word key
in the message again. It makes it more clear what it is we are skipping. Below in line 217 we include requires
to disambiguate.
4ccca87
to
61de5e9
Compare
Thanks for the ping! I will give it a look later today. |
61de5e9
to
26a7283
Compare
if err != nil { | ||
return errors.New(fmt.Sprintf("Field %s undefined in schema.", "id")) | ||
entity, err := func() (*model.Product, error) { | ||
id0, err := ec.unmarshalNString2string(ctx, rep["manufacturer"].(map[string]interface{})["id"]) |
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.
This issue is quite tricky. If the query entity was for FindProductByUpc
and there is no manufacturer
representation, than this will fail because we would be trying to read id
from nil
.
We definitely want plugin/federation/federation_entityresolver_test.go
tests to verify that we can have a nested key and query entities with all the resolvers.
Like here [1] we could add something like
type World @key(fields: "hello { name } foo ") @key(fields: "bar") {
foo: String!
bar: Int!
hello: Hello
}
And the tests can have representations like:
{
"__typename": "World",
"bar": 1,
}, {
"__typename": "World",
"hello": map[string]interface{}{
"name": "world name - 1",
},
"foo": "foo 1",
},
type World @key(fields: "hello { name } foo ") {
https://go.dev/play/p/yLWsp2g0gyR
@@ -4,3 +4,6 @@ exec: | |||
filename: testdata/entities/generated/exec.go | |||
federation: | |||
filename: testdata/entities/generated/federation.go | |||
|
|||
autobind: | |||
- "github.com/99designs/gqlgen/plugin/federation/test_data/model" |
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.
It would be good to have a separate test with autobind. Maybe have a different nokey-autobind.yml
so that we can ensure these tests work with model autogen as well as autobinding. WDYT?
Thanks for tackling this complicated issue. The code itself to enable this looks good to me. The issues are more around the generated code:
I think that we can address both issues together tho! What if we generated a function that for a given type and representation, it would determine what resolver to call.
Then in
Let me know if this makes sense and or if you would like more specific implementation details. Again, thanks for tackling this complicated issue! |
Smart. Let me take a run at this and see where it goes. Getting rid of the imports would definitely clean things up, and this seems like a good way to do it. |
26a7283
to
3407f7a
Compare
So... @duckbrain are you still working on this, or do you want a re-review as is? |
I'm still working on this between other things, should have something to review in the next day or two. |
aa2b044
to
f46c6bb
Compare
@MiguelCastillo @StevenACoffman (and @duckbrain, if you care 🙂), I have updated this based on comments. Still missing a couple of things, noted in the commit comments. Happy to continue, or address in a follow-on. Also willing to squash and clean up before you merge. Thanks! |
(Oh so sorry duckbrain... I just typed |
@carldunham i am planning on taking another look either today or tomorrow. thank you for your patience and your work on this! |
@MiguelCastillo Thank you for looking into this. We just adopted gqlgen and I was wondering when I had multiple @key described no entity resolvers would be created after the first initial @key. I believe this also has to do with this open issue from last year. |
Looking forward to this PR getting merged. Thanks! |
@MiguelCastillo I would appreciate it a lot if you could continue this. This is a huge factor of gqlgen. If you're not able to could you tell me what else needs to be done so I could contribute? |
f46c6bb
to
da38c74
Compare
da38c74
to
3ad58d5
Compare
plugin/federation/federation.go
Outdated
if len(keys) > 1 { | ||
// TODO: support multiple keys -- multiple resolvers per Entity | ||
panic("only one @key directive currently supported") | ||
if len(keys) == 0 { |
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.
Food for thought perhaps for later PRs. One of the things that makes this code kinda hard to grok unless you are very familiar with federation internals is the lack of human readable conditionals. Like I'd love to have a helper function here that's perhaps called something is isFederatedEntity
or something. And that checks schemeType.Kind == ast.Object
and ensure that there are federated keys.
But yeah not for this PR. Just thoughts I have had about how to make this code more readable and accessible to folks.
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.
Went ahead and added this. Hopefully returning the keys isn't more confusing.
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.
Yeah I think its great. The only nit is that functions that start with is
usually are just predicate functions. I would have called this instead perhaps getFederatedEntityKeys
or something and that can return they keys and ok...
but this is perfect. We can always iterate on this nits.
plugin/federation/federation.go
Outdated
|
||
// Let's process custom entity resolver settings. | ||
dir := schemaType.Directives.ForName("entityResolver") | ||
if dir != nil { |
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.
Same sort of idea here. Like a helper function like configureEntityResolverDirective
... And configureKeyDirective
... These are TODOs in my head. Hoping to get more cycles for this over the break.
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.
Left this for you 🙂
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.
Thank you :)
plugin/federation/federation.go
Outdated
InputType: resolverFieldsToGo + "Input", | ||
}) | ||
|
||
e.Requires = []*Requires{} |
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.
I think we want to process requires
directives outside of the loop for processing all the keys
to avoid reprocessing them.
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.
Yeah, dumb oversight on my part!
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.
all good! these changes were very tricky. and hard to catch in unit tests since the generated should still work correctly. 🤷
plugin/federation/federation.gotpl
Outdated
|
||
list[idx[i]] = entity | ||
return nil | ||
{{ if eq (len .Resolvers) 1 }} |
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.
I think it should be OK to always generate the switch statement. There isn't a huge runtime performance penalty and it keep this already giant template more manageable and less error prone since you have to make any template changes in multiple places.
Or is there a reason you'd prefer to having a different generated code when there is only 1 entity resolver vs multiple?
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.
Only that it made the generated code cleaner. But you're right, it comes at the cost of a more complicated template, which will probably need to be comprehended more often. I'm removing the ==1 case.
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.
There were two places where this could be done, hopefully strikes a good balance.
plugin/federation/federation.gotpl
Outdated
@@ -163,7 +195,8 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati | |||
return nil | |||
{{ end }} | |||
{{ end }} | |||
{{- end }} | |||
{{ end }} | |||
{{- end }} |
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.
😅 templates tags are so hard to match up!!! Did an {{ end }}
sneak in or is there a misalignment that makes it harder to spot where the opening tag is? I think the new {{ end }}
tag is for line 161?
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.
Hopefully, it's a little more aligned and clear now. I think indenting at the case statements might make things clearer? But not doing that yet.
plugin/federation/federation.gotpl
Outdated
var ( | ||
ErrUnknownType = errors.New("unknown type") | ||
ErrTypeNotFound = errors.New("type not found") | ||
ErrUnableToResolveEntity = errors.New("unable to resolve Entity") |
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.
Is ErrUnableToResolveEntity being used? It seems like it isn't...
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.
Not any more :)
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.
yay!
plugin/federation/federation.gotpl
Outdated
{{ range $_, $entity := .Entities }} | ||
{{- if .Resolvers }} | ||
|
||
func (ec *executionContext) entityResolverNameFor{{$entity.Name}}(ctx context.Context, rep map[string]interface{}) (string, error) { |
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.
From a mental model standpoint, I think it would be a lil better if ec
was passed in as an argument to entityResolverNameFor
... Not a major thing, so if you feel strongly about keeping it as is then no problem. But in this generated code the execution engine mostly really care about being able to call __resolve__service
and __resolve_entities
as well as providing an interface for marshaling fields.
Tho - what do you think about not calling any marshaling functions in the generated entityResolverNameFor
functions? We are primarily interested in matching up the shapes of rep
to determine what resolver to call. We don't care as much about marshaling the values. Especially since rep
values are getting marshaled again before calling the entity resolvers. That would do two things: 1. Avoid double marshaling for values. 2. Can allow us to not even pass ec
into these generated function.
Thoughts?
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.
The only weird cases would be where an invalid representation in the map[string]interface{} would result in that resolver being chosen, rather than another that had proper reps. Of course, the reverse could happen also, and not be caught. I'll modify it and we can see how it looks.
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.
I think the generated turned out great! We can definitely tweak as we get more use of this new functionality tho.
@carldunham this is absolute awesome! Im really excited for these changes to land, and so are lots of other folks. 👏 |
Thanks, @MiguelCastillo. Great feedback! I'll attack this over the weekend, if not today. It looks like mostly moving and removing, should clean things up nicely! |
Thank you for your offer @LockedThread! 💟 Happily I was able to squeeze in some cycles today for this. I think we are very close to landing. And it's always helpful to have other folks eyes on PRs, so if you go thru the changes and spot anything - call it out! It can only help :) |
Alright, I'll stay up-to-date on your changes. Good luck! |
- reworked code generation for federation.go - better checking for missing/incorrect parameters to entity resolver functions - better tests for generated entity resolvers Still missing: - suggested test for autobind vs non-autobind generation - could probably clean up generated code spacing, etc - not sure @multi cases are handled completely (if they apply)
3ad58d5
to
e9c4229
Compare
@MiguelCastillo @LockedThread ready for re-review! |
} | ||
|
||
// make sure order remains stable across multiple builds | ||
sort.Slice(f.Entities, func(i, j int) bool { | ||
return f.Entities[i].Name < f.Entities[j].Name | ||
}) | ||
} | ||
|
||
func isFederatedEntity(schemaType *ast.Definition) ([]*ast.Directive, bool) { |
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.
Ah thank you! Yeah this is is great. We can tweak the function name as we use it more.
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.
Woohoo! This is great, @carldunham. Thank you for all your work with this.
Reworked support for multiple @key and @requires directives in federation.
Would like feedback in general, and specifically for real use cases.
I have: