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

Support different arguments for different aliases #482

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

Conversation

kryops
Copy link

@kryops kryops commented May 17, 2022

Hi there,

after experimenting with different workarounds in #481, I believe I may have found a proper fix for #126.

Please let me know what you think ☺️

I published the changes to npm as @kryops/join-monster@3.1.1-1 if anyone wants to try it out with their schema.

Description

The general strategy is as follows:

  • join-monster tries to detect conflicting aliases to the same field
  • If a conflict is detected, it will set multiple properties on the output object instead of overwriting the fieldName:

The query

{
  users {
    following { fullName }
    matts: following(name: "matt") { fullName }
  }
}

generates per user:

{
  following$: [
    { fullName: 'andrew carlson' },
    { fullName: 'matt elder' }
  ],
  following$matts: [
    { fullName: 'matt elder' }
  ],
  following: (args, context, info) => { /* ... */ }
}

GraphQL's default resolver supports functions as properties, and will call them with the information necessary to return the correct property.

This is a breaking change when using custom resolvers: Instead of always accessing source[fieldName], GraphQL's defaultFieldResolver has to be used to get the raw value in order to support multiple conflicting aliases:

import { defaultFieldResolver } from 'graphql'

// ...

resolve: (source, args, context, info) => {
  const rawValue = defaultFieldResolver(source, args, context, info)
  return processUsers(rawValue)
}

Note that this should be only actually breaking in situations that likely used to return wrong data. For normal fields and non-conflicting aliases the same format is used as before.

References

Fixes #126

Fixes #146

Checklist

  • I have added documentation for new/changed functionality in this PR via comments and by updating the change log
  • I'm not entirely sure when to consider aliases as "conflicting". For now I have implemented the following logic:
    • Some types of nodes are never conflicting because join-monster does not use the args, thus they should point to the same data anyway (e.g. normal columns)
    • Some types of nodes are always conflicting because even using the same args, they can have nested conflicting aliases inside them (e.g. tables, unions)
    • For other types of nodes (e.g. sqlExpr), the args may be used by join-monster, so aliases are conflicting if they use different args
  • Are the generated property keys "unique enough"? What if a schema contains both comments and comments$?
  • We should probably add a separate test for aliases with paginated schemas. Maybe Post.comments or User.comments?
  • We should probably add a separate test for aliases with unions and interfaces. Any ideas how to best do it in the test schema? (should we add args to writtenMaterial1 or writtenMaterial2?)
  • I wasn't sure where to add the new behavior in the documentation. For now, I added it to the warnings page.

@DanielSWolf
Copy link

Are the generated property keys "unique enough"? What if a schema contains both comments and comments$?

According to the GraphQL spec, the $ character cannot be used in a GraphQL identifier. So it is my understanding that the keys you generate are always unique.

test/aliases.js Outdated Show resolved Hide resolved
@kryops kryops force-pushed the alias-pr branch 2 times, most recently from 0010f49 to e41f974 Compare December 5, 2023 07:24
@kryops
Copy link
Author

kryops commented Dec 5, 2023

Just rebased on v3.3.2.

Any chance of moving this forward?

@nicoabie
Copy link
Contributor

@bueche can you take a look into this one?

@nicoabie
Copy link
Contributor

nicoabie commented May 4, 2024

@kryops This week I become maintainer of this project and I would love helping you merge this PR if you are still interested.

Let me know, have a great weekend!

following: {
// ...
resolve: (source, args, context, info) => {
const rawValue = defaultFieldResolver(source, args, context, info)
Copy link
Contributor

@bueche bueche May 5, 2024

Choose a reason for hiding this comment

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

assuming this is only required for "non-trivial" field usage. Please confirm. Also, perhaps it's useful to explain in more detail how this helps. Isn't this defaultFieldResolver() called under-the-hood?

Copy link
Author

Choose a reason for hiding this comment

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

Yes, defaultFieldResolver is only required for non-trivial fields.

The problem is that GraphQL only uses its defaultFieldResolver if you do not specify a custom resolve function. As soon as you do, you need to handle this yourself.

I extended the section on custom resolvers in the docs and added a link to it from the changelog, please let me know if this covers it better now 😅

// ...
resolve: (source, args, context, info) => {
const rawValue = defaultFieldResolver(source, args, context, info)
return processUsers(rawValue)
Copy link
Contributor

Choose a reason for hiding this comment

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

and what do you mean by "rawValue" here? raw vs. what??

Copy link
Author

Choose a reason for hiding this comment

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

Good point, it's just the value of the field - changed it to value

docs/warnings.md Outdated

The `source.following` property is now a function that will return the correct value. GraphQL's default resolver will detect the function and do the right thing.

Therefore, we recommend getting the raw value through GraphQL's default resolver first when using a custom resolver on a non-trivial field:
Copy link
Contributor

Choose a reason for hiding this comment

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

  • "non-trivial field" ... this should have a more precise definition at least as best as you understand it.
  • "recommend" ? ... perhaps too softly worded. What happens if you don't use the default resolver ... and (again) why are we calling the default resolver explicitly when it was previously called implicitly?

Copy link
Author

Choose a reason for hiding this comment

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

True, it might throw an error or do weird things if you don't use the default resolver and a query uses multiple aliases, because source[fieldName] would contain a function instead of the field value.

I changed the wording to "need to", and added a section explaining "non-trivial" values in more detail.

@@ -275,6 +275,9 @@ const User = new GraphQLObjectType({
},
writtenMaterial1: {
type: new GraphQLList(AuthoredUnion),
args: {
search: { type: GraphQLString },
Copy link
Contributor

Choose a reason for hiding this comment

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

Excellent to add to the tests ... can you explain what is being added to test this?

Copy link
Author

Choose a reason for hiding this comment

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

This was done to be able to test multiple aliases with different args on union and interface types. I added a comment explaining this.

test/aliases.js Show resolved Hide resolved
@kryops
Copy link
Author

kryops commented May 6, 2024

Hi all, yes I'm still interested in getting this merged.

Thanks for the feedback, I'll try to find some time this week to address it 😅

@nicoabie
Copy link
Contributor

I'm planning to do a v3.3.5 with the small fixes currently in master and then create a v3.4.0 release with this.
@bueche what are your thoughts?

@kryops
Copy link
Author

kryops commented May 27, 2024

Should this rather be a v4.0.0 as it is a breaking change for custom resolvers?

@nicoabie
Copy link
Contributor

I understood it is only a breaking change for users that are using broken aliases but yeah we can for sure make it v4.0.0

@bueche
Copy link
Contributor

bueche commented May 28, 2024 via email

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

Successfully merging this pull request may close these issues.

Problem with aliases on paginated fields Several batch queries of one table
4 participants