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

Adding contrib for GraphQL Relay #1214

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

Adding contrib for GraphQL Relay #1214

wants to merge 8 commits into from

Conversation

pkucmus
Copy link

@pkucmus pkucmus commented Jan 24, 2025

Related to #1213.

Important

The schema used for this example comes from https://relay.dev/docs/guides/graphql-server-specification/#schema

Doc strings with examples are yet to come so here's a brief manual on how to use this:

from ariadne.contrib.relay import (
    ConnectionArgumentsUnion,
    RelayConnection,
    RelayObjectType,
    RelayQueryType,
)

RelayQueryType

Use the QueryType that enforces the node GQL logic while providing some utility:

query = RelayQueryType()


@query.field("node")
async def resolve_node(_, info, bid: str):
    ships = [{"__typename": "Ship", **ship} for ship in SHIPS if ship["id"] == bid]
    return ships[0]


@query.node.type_resolver
def resolve_node_type(obj, *_):
    return obj["__typename"]

or if you have the node resolvers already:

query = RelayQueryType(resolve_node_type, resolve_node)

You can map to a different id field if your Node interface specifies something else than id, like bid:

@query.node.field("bid")
def resolve_id(obj, *_):
    return obj["id"]

RelayObjectType and RelayConnection

There's the RelayObjectType which is similar to the standard ObjectType but has the relay "pagination" logic encapsulated in the connection decorator. The decorator takes a resolver that will later be invoked with an additional connection_arguments argument that holds your first, last, after, before. The resolver is expected to fetch a slide of data from the resource's storage and return it, encapsulated in a RelayConnection instance with additional pagination data - in gist - the resolver is responsible for the page calculations.

faction = RelayObjectType("Faction")


@faction.connection("ships")
async def resolve_ships(
    faction_obj,
    info,
    connection_arguments: ConnectionArguments,
    **kwargs,
):
	# This is a poor man's implementation of a storage - the ships are stored in a 
	# list of dicts here. I'm leaving it here for the example to have more sense, 
 	# but it's not critical for the overall feature.
	# This would normally be a call to a remote resource, Django queries, SQLAlchemy queries, 
    # Dataloader calls, etc
    ships = [ship for ship in SHIPS if ship["factionId"] == faction_obj["id"]]
    total = len(ships)
    if connection_arguments.after:
        after_index = (
            ships.index(
                next(ship for ship in ships if ship["id"] == connection_arguments.after)
            )
            + 1
        )
    else:
        after_index = 0
    ships_slice = ships[after_index : after_index + connection_arguments.first]

	# This return is the important part.
    return RelayConnection(
        edges=ships_slice,
        total=total,
        has_next_page=after_index + connection_arguments.first < total,
        has_previous_page=after_index > 0,
    )

RelayConnection is something one would want to overload to provide some repeatable utility, maybe you'd like a DjangoRelayConnection to operate on a Django ORM QuerySet and maybe calculate the paging there to reduce the work that's needed to be done in the resolver. Overloading get_cursor, get_page_info, get_edges on RelayConnection should enable one to achieve a lot but here it's important to listen to the use cases people might have.

I'm open to feedback, thanks :)

@pkucmus
Copy link
Author

pkucmus commented Jan 28, 2025

Based on the request from #1213 (comment) I introduced the following way to provide utility for Query.node object resolving:

By default the RelayQueryType object will instantiate a new RelayNodeInterfaceType object with a node_resolver decorator capable of registering node object resolvers.

query = RelayQueryType()

...

@query.node.node_resolver("Ship")   # special node_resolver decorator
async def resolve_ship(_, info, bid: str):
    ships = [{"__typename": "Ship", **ship} for ship in SHIPS if ship["id"] == bid]
    return ships[0]

The above would be the minimal one need to do to achieve a query.node(id: ID) resolution - assuming a few defaults which seem to be healthy in terms of the Relay Spec like having a Base64 encoded {type_name}:{id} global ID scheme with the IDs incoming in the id kwarg.

Others will have the ability to provide means for a more customized behavior, like having a custom global ID field:

def decode_global_id(kwargs) -> GlobalIDTuple:
    return GlobalIDTuple(*b64decode(kwargs["bid"]).decode().split(":"))


node = RelayNodeInterfaceType(
    global_id_decoder=decode_global_id,
)
query = RelayQueryType(
    node=node,
)

Finally one can just reset the node field resolver to get rid of all and any logic delivered by this module, allowing full control:

query = RelayQueryType()

@query.field("node")     # standard Aridane field decorator
async def resolve_node(_, info, bid: str):
    ships = [{"__typename": "Ship", **ship} for ship in SHIPS if ship["id"] == bid]
    return ships[0]

@pkucmus
Copy link
Author

pkucmus commented Jan 30, 2025

This newest version allows one to use the RelayObjectTypes to provide a query.node resolution method:

query = RelayQueryType(
    global_id_decoder=decode_global_id,
)
ship = RelayObjectType("Ship")


@ship.node_resolver
async def resolve_ship(_, info, bid: str):
    ships = [{"__typename": "Ship", **ship} for ship in SHIPS if ship["id"] == bid]
    return ships[0]

Thanks @reallistic!

@pkucmus pkucmus marked this pull request as ready for review January 30, 2025 14:22
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.

1 participant