-
-
Notifications
You must be signed in to change notification settings - Fork 535
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
Relay implementation proposal #1573
Comments
@bellini666 this is a really nice relay implementation! However I think that explicitly declaring connections is better than automatically providing them! Also, to name relay mutation inputs as Apart from that, the implementation looks really good to me! |
Thank you! :) What do you mean specifically by "explicitly declaring connections"? Don't know if I fully understood the comment.
Oh yeah, I went that road for the "v0.1" lets say. Adding an optional name should not be hard though, maye an optional |
Or what about not transforming the inputs at all? We could leave it as it is so that everything becomes simple to the user. We dont need to create inputs implicitly @strawberry.input
class CreateUserInput:
name: str
@relay.mutation
def create_user(input: CreateUserInput) -> bool:
pass |
@aryaniyaps ohhh, I see what you mean now. The idea of the The reasoning is for people who uses only relay-style mutations (i.e. the ones where the only argument for the mutation is an input), but still want to take advantage of the simplicity of function args -> graphql args typing, without having to create an input for every mutation. I'm one of those for example =P But If you guys prefer not to have that for strawberry, I don't have any issues at all with removing it from the future-to-come PR. |
A few questions:
The issue I see with most attempts at abstracting connections, is that they miss that most of the heavy lifting has to be done in user-land (unless you're making major assumptions, such as using Django's ORM), there's actually relatively little to do in library code. |
Hey @AndrewIngram , thanks for the feedback!
Having said that, I disagree that it depends on major assumptions, such as using Django's ORM. You can actually paginate anything as long as the
For example, one could easily implement a "paginator result" through a REST api by returning an object whose But, I can see one small change that, if we allow the |
Note that i'm using limit/offset to refer to any pattern where you're fetching a page by its index and page size, regardless of whether that's hitting a REST API or running a database query directly. The Relay pagination spec is kind of designed around the assumption that you're not using limit/offset, because limit/offset is inefficient, that's why it's focused on the more efficient cursor-based pagination (of which keyset pagination is the most common, because it's the easiest to implement). Keyset pagination works based on filtering out all the rows before the cursor (the cursor contains all the necessary data to do this), which means the resolver needs access to at least the cursor being used. Whilst it's possible to implement limit/offset within the cursor pagination spec, it's not really the right choice for it. I'd implement a separate (but similar) pattern as discussed here: https://andrewingram.net/posts/demystifying-graphql-connections/ One use case the cursor pagination spec is particularly trying to solve well is infinite scrolling, as commonly seen in feed-based apps. This can't be done well with limit/offset because it results in unstable paging and duplicate rows. Cursor-based pagination doesn't suffer from this problem, which is it's preferred. There are downsides to this approach, the primary one being that you can't do direct page access, e.g. starting from page 3 (you don't know its cursor until you fetch it); which is why I like to leave the door open for different pagination systems.
If you overfetch by one, you can satisfy hasNextPage/hasPreviousPage for whichever direction you're traversing the data. Though you technically can't answer the opposite, because you filtered out all those rows. Even the presence of a cursor doesn't mean anything, because the row before that cursor could've been deleted. |
Hey @AndrewIngram . I totally get your point and agree with you that limit/offset is not the most efficient cursor-based pagination. My point is that it is still good enough for most cases (for some it is on par) and is probably what most people is going to do anyway. This is a v1 (or even v0.1) solution that can for sure benefit from a refactoring on the way the pagination works to make it easy to do something different, like keyset. I personally would like to implement that possibility now in my solution. I'm going to think about how to do that in a nice way and am also open to suggestions! :) |
Probably the best thing to do now is to not make any assumptions of how users would implement pagination logic. We should leave all of that to the user, as @AndrewIngram suggested. I think we will be left with little code on the strawberry side, but that's okay! |
Also, I think that the relay.mutation field should add a clientMutationId field for every mutation. |
I would still argue that limit/offset is the most common kind of implementation, but I'm fine with not making assumptions at all. I'm still thinking in a way to make this more generic and less "assumptious" (does that word exist?)
I also thought about that at the beginning, and after searching for a while I discovered that it was removed from relay and the spec. |
Thank you @bellini666 for this work! |
Cool, looks like it has been removed between versions 7 and 8: https://relay.dev/docs/v7.0.0/graphql-server-specification/ https://relay.dev/docs/v8.0.0/graphql-server-specification/ What is the best practice now? I am currently using
|
Hey guys, It's been a while since I opened this PR. Since them the implementation has evolved a bit and some issues that were discussed here were resolved. For example, one of the main issues was that the base implementation was using a limit/offset approach and it was not easy to override that implementation. It is now possible to define a custom The implementation is still self contained and has not dependencies on django specific parts of the lib. Would like to revive this discussion to see if it makes sense to try to contribute it here to strawberry or if it should become its own library, like cc @strawberry-graphql/core |
I would very much like to see proper support for relay in strawberry, so +1 to make this happen :) |
@kaos how would you see that working? Would we need to provide something like I want to make sure we have a relay implementation that's flexible enough to work with many tool, but I'm not sure exactly what that would look like 😊 |
I've not looked into the particular wrgt strawberry yet. I've used graphene in the past and that worked out well enough, if that helps..? Given that relay works with generic types (the global Node type) and the like, I imagine that having some basic building blocks for Node types Connections and PageInfo would be enough. I can play with the implementation presented by @bellini666 to see how that works out. It looks pretty much like what I'm hoping for at a quick glance :) From a user perspective, paging is a real easy endeavour--you get a request for a range of objects that's it. The rest is added complexity to make it efficient and adaptable to many different uses of paginating data which I would be happy to not have to implement/care too much about ;) |
My current implementation does create a connection from any iterable and paginates it using limit/offset. But one can basically subclass that @strawberry.type
class MyConnection(Connection):
@classmethod
def from_nodes(
cls,
nodes: Iterable[Any],
*,
total_count: Optional[int] = None,
before: Optional[str] = None,
after: Optional[str] = None,
first: Optional[int] = None,
last: Optional[int] = None,
):
... # custom implementation
@strawberry.type
class MyType(Node):
CONNECTION_CLASS = MyConnection Obviously this can be improved when merging into strawberry by either receiving the
I kept it in a single file and without any django dependency specifically to make it easier to contribute it back here later, so it should (hopefully) be easy to understand what's going on (the django specific code for handling querysets and retrieving node objects are injected when creating the django type). The module has the basic building blocks with some basic funcionality built-in (like the limit/offset pagination, but with the possibility of easily overriding it) and some fields (to be used as @strawberry.type
class Mutation:
@strawberry.field
def create_person(self, name: str, age: int) -> Person:
...
# generates:
"""
type Mutation {
createPerson(
name: String!
age: Int!
): Person
}
""" Would get translated to: @strawberry.type
class Mutation:
@relay.input_mutation
def create_person(self, name: str, age: int) -> Person:
...
# generates:
"""
input CreatePersonInput {
name: String!
age: Int!
}
type Mutation {
createPerson(input: CreatePersonInput!): Person
}
""" |
Hey guys, After my last message and curiously, after coincidently having to define a custom Those changes make it easier to define custom connections (and thus, add fields to it or define custom pagination methods), and to use them it is just a matter of typing the field with it (no need to define I also wrote some tests for the relay module, which also contains an example of a custom pagination. It can give you a nice idea on how to use it: https://github.com/blb-ventures/strawberry-django-plus/blob/main/tests/test_relay.py For curiosity, the schema defined in that test module prints like this: https://github.com/blb-ventures/strawberry-django-plus/blob/main/tests/data/relay_schema.gql I don't know if there's anything else that needs to be improved in the implementation. There are some code that can be simplified if the integration is merged here, like:
cc @patrick91 @kaos |
@bellini666 thanks for the update. Hoping to have some feedback for this in the coming week after trying it out in a project I'm working on. |
This works beautifully, and mostly does what I would expect.
However, I tried with this:
But my schema gives me two fields, the first one is a valid Connection, but the second skips the connection part and is just a list directly:
Not sure if it's a bug or my mistake, but it's close, I can feel it! :) The diff overall to turn my regular query over to a relay paged connection was very easy. Kudos! My vote is to get started on a PR to get this relay implementation properly tested and integrated with strawberry 💯 |
Hey @kaos , Thank you so much for testing it! :)
That's so strange. I tried to reproduce the issue in my tests to try to fix it and wrote two custom resolvers: https://github.com/blb-ventures/strawberry-django-plus/blob/main/tests/test_relay.py#L122 . One that returns an The only issue that I found when i wrote tests for it was that the one returning a generator has problems with the pagination slice, so I made a fix for it to use
😊 I think I'll start a PR soon, so any remaining issues to be resolved can be suggested in the PR itself. Still open to suggestions/issue reports in the meantime |
Hmm.. interesting. I copied your example verbatim with the fruitsCustomResolverReturningList(
before: String = null
after: String = null
first: Int = null
last: Int = null
nameEndswith: String = null
): [Fruit!]! Your schema looks correct though, so I'm guessing there may be some issue in the Looking forward to the PR! :) |
It might be it! Well, when I open the PR I'll port the current tests, and write some new ones, so I should be able to fix any corner case you are hitting there :) I think I'll have some time next weekend and try to do that! |
Hey guys. Just created a PR with the initial implementation in here: #2511 :) |
Hi. I benefitted from a gist you created years ago where you implemented proper cursor based pagination for the django graphene library. Do you have something similar for strawberry? (I started to use the relay support it offers assuming it would be using proper cursors but it seems it's just masking the index position of the item in the queryset like django graphene). |
Hey guys,
I saw that
relay
support is something planned here and I have created a relay implementation that I would like to know if you would be interested as a contribution.You might have seen that I posted on discord at django channel about this lib I wrote: https://github.com/blb-ventures/strawberry-django-plus. In it I created a generic relay implementation:
I made it really generic and no attached to django at all because, not only I would like to have it be useful to non-django related types, but also because my intention was to either contribute it back to you or create a separated project for it (strawberry-relay maybe?). It is also in a single file for that purpose.
The way I did it, you just implement the
Node
interface by inheriting at the type you are creating and define at least 2 specific methods, one to resolve a single node given its it, and another one to resolve a list of nodes (optionally given a list of ids or none at all, in which the implementation is supposed to return an iterator of all nodes). There are other resolvers that can be overridden by the implementer, but they have a default implementation already.With that, one can create a connection with
some_connection: Connection[NodeType] = relay.connection()
. A resolver can also be passed to that (by argument or by decorating a method) which is supposed to return an iterator of theNodeType
, and that will be paginated by the field itself. Any arguments added to that resolver will be included together with thefirst
,last
,before
andafter
arguments.There's is a
some_node: NodeType = relay.node()
which creates a field that expects a global id and returns the node.And lastly, there's an input mutation field that basically converts all of the arguments passed from the resolver function to an input
Input
that is automatically created for the mutation, and them converts them back to arguments when calling the function, so for the user it still defines the resolver the same way as a common mutation, without having to create a type for each one and specify it as the single input argument. The only thing I did not do here was to create thePayload
type because I did not know how to do that the best way...A complete example would be:
The generated schema would be:
ORM integrations can implement those
resolve_node
andresolve_nodes
to automatically provide them. I've done that with my lib for the django ORM. I also did override theresolve_id
andresolve_connection
to make their implementation async-safe, since django orm is not.The text was updated successfully, but these errors were encountered: