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

Develop [Test] #18

Closed
wants to merge 11 commits into from
Closed
27 changes: 11 additions & 16 deletions examples/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@


def get_file_by_id(id):
return File(**{'id': id, 'name': 'test_name'})
return File(**{"id": id, "name": "test_name"})


class Author(graphene.ObjectType):
id = graphene.ID(required=True)
name = graphene.String(required=True)


@key(fields='id')
@key(fields='id author { name }')
@key(fields='id author { id name }')
@key(fields="id")
@key(fields="id author { name }")
@key(fields="id author { id name }")
class File(graphene.ObjectType):
id = graphene.Int(required=True)
name = graphene.String()
Expand All @@ -38,18 +38,18 @@ def resolve_file(self, **kwargs):

schema = build_schema(Query)

query = '''
query = """
query getSDL {
_service {
sdl
}
}
'''
"""
result = schema.execute(query)
print(result.data)
# {'_service': {'sdl': 'type Query {\n file: File\n}\n\ntype File @key(fields: "id") {\n id: Int!\n name: String\n}'}}

query = '''
query = """
query entities($_representations: [_Any!]!) {
_entities(representations: $_representations) {
... on File {
Expand All @@ -59,15 +59,10 @@ def resolve_file(self, **kwargs):
}
}

'''
"""

result = schema.execute(query, variables={
"_representations": [
{
"__typename": "File",
"id": 1
}
]
})
result = schema.execute(
query, variables={"_representations": [{"__typename": "File", "id": 1}]}
)
print(result.data)
# {'_entities': [{'id': 1, 'name': 'test_name'}]}
6 changes: 3 additions & 3 deletions examples/extend.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from graphene_federation import build_schema, extend, external


@extend(fields='id')
@extend(fields="id")
class Message(graphene.ObjectType):
id = external(graphene.Int(required=True))

Expand All @@ -19,13 +19,13 @@ def resolve_file(self, **kwargs):

schema = build_schema(Query)

query = '''
query = """
query getSDL {
_service {
sdl
}
}
'''
"""
result = schema.execute(query)
print(result.data)
# {'sdl': 'type Query {\n message: Message\n}\n\nextend type Message @key(fields: "id") {\n id: Int! @external\n}'}}
17 changes: 13 additions & 4 deletions examples/inaccessible.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import graphene

from graphene_federation import inaccessible, external, provides, key, override, shareable
from graphene_federation import (
inaccessible,
external,
provides,
key,
override,
shareable,
)

from graphene_federation import build_schema

Expand Down Expand Up @@ -56,15 +63,17 @@ class Query(graphene.ObjectType):
position = graphene.Field(Position)


schema = build_schema(Query, enable_federation_2=True, types=(ReviewInterface, SearchResult, Review))
schema = build_schema(
Query, enable_federation_2=True, types=(ReviewInterface, SearchResult, Review)
)

query = '''
query = """
query getSDL {
_service {
sdl
}
}
'''
"""
result = schema.execute(query)
print(result.data)
# {'_service': {'sdl': 'extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@external", "@key", "@override", "@provides", "@inaccessible"])\ntype Query {\n position: Position\n}\n\ntype Position @key(fields: "x") {\n x: Int!\n y: Int! @external\n z: Int! @inaccessible\n a: Int @provides(fields: "x")\n b: Int! @override(from: "h")\n}'}}
13 changes: 10 additions & 3 deletions examples/override.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import graphene

from graphene_federation import build_schema, shareable, external, key, override, inaccessible
from graphene_federation import (
build_schema,
shareable,
external,
key,
override,
inaccessible,
)


@key(fields="id")
Expand All @@ -16,13 +23,13 @@ class Query(graphene.ObjectType):

schema = build_schema(Query, enable_federation_2=True)

query = '''
query = """
query getSDL {
_service {
sdl
}
}
'''
"""
result = schema.execute(query)
print(result.data)
# {'_service': {'sdl': 'extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@override", "@inaccessible"])\ntype Query {\n position: Product\n}\n\ntype Product @key(fields: "id") {\n id: ID!\n inStock: Boolean! @override(from: "Products")\n outStock: Boolean! @inaccessible\n}'}}
4 changes: 2 additions & 2 deletions examples/shareable.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ class Query(graphene.ObjectType):

schema = build_schema(Query, enable_federation_2=True, types=(SearchResult,))

query = '''
query = """
query getSDL {
_service {
sdl
}
}
'''
"""
result = schema.execute(query)
print(result.data)
# {'_service': {'sdl': 'extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@shareable"])\ntype Query {\n position: Position\n}\n\ntype Position @shareable {\n x: Int!\n y: Int! @shareable\n}'}}
4 changes: 2 additions & 2 deletions examples/tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ class Query(graphene.ObjectType):

schema = build_schema(Query, enable_federation_2=True)

query = '''
query = """
query getSDL {
_service {
sdl
}
}
'''
"""
result = schema.execute(query)
print(result.data)
# {'_service': {'sdl': 'extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@inaccessible", "@shareable", "@tag"])\ntype Query {\n position: Product\n}\n\ntype Product {\n id: ID!\n inStock: Boolean! @tag(name: "Products")\n outStock: Boolean! @shareable\n isListed: Boolean! @inaccessible\n}'}}
1 change: 1 addition & 0 deletions graphene_federation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
from .inaccessible import inaccessible
from .provides import provides
from .override import override
from .compose_directive import mark_composable, is_composable
64 changes: 64 additions & 0 deletions graphene_federation/compose_directive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from typing import Optional

from graphql import GraphQLDirective


def is_composable(directive: GraphQLDirective) -> bool:
"""
Checks if the directive will be composed to supergraph.
Validates the presence of _compose_import_url attribute
"""
return hasattr(directive, "_compose_import_url")


def mark_composable(
directive: GraphQLDirective, import_url: str, import_as: Optional[str] = None
) -> GraphQLDirective:
"""
Marks directive with _compose_import_url and _compose_import_as
Enables Identification of directives which are to be composed to supergraph
"""
setattr(directive, "_compose_import_url", import_url)
if import_as:
setattr(directive, "_compose_import_as", import_as)
return directive


def compose_directive_schema_extensions(directives: list[GraphQLDirective]):
"""
Generates schema extends string for ComposeDirective
"""
link_schema = ""
compose_directive_schema = ""
# Using dictionary to generate cleaner schema when multiple directives imports from same URL.
links: dict = {}

for directive in directives:
# TODO: Replace with walrus operator when dropping Python 3.8 support
if hasattr(directive, "_compose_import_url"):
compose_import_url = getattr(directive, "_compose_import_url")
if hasattr(directive, "_compose_import_as"):
compose_import_as = getattr(directive, "_compose_import_as")
import_value = (
f'{{ name: "@{directive.name}, as: "@{compose_import_as}" }}'
)
imported_name = compose_import_as
else:
import_value = f'"@{directive.name}"'
imported_name = directive.name

import_url = compose_import_url

if links.get(import_url):
links[import_url] = links[import_url].append(import_value)
else:
links[import_url] = [import_value]

compose_directive_schema += (
f' @composeDirective(name: "@{imported_name}")\n'
)

for import_url in links:
link_schema += f' @link(url: "{import_url}", import: [{",".join(value for value in links[import_url])}])\n'

return link_schema + compose_directive_schema
53 changes: 46 additions & 7 deletions graphene_federation/entity.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
from __future__ import annotations

import collections.abc
from typing import Any, Callable, Dict

from graphene import List, NonNull, Union

from graphene import Field, List, NonNull, ObjectType, Union
from graphene.types.schema import Schema
from graphene.types.schema import TypeMap

from .types import _Any
from .utils import (
field_name_to_type_attribute,
check_fields_exist_on_type,
field_name_to_type_attribute,
is_valid_compound_key,
)

import collections.abc


def update(d, u):
for k, v in u.items():
Expand Down Expand Up @@ -80,7 +78,7 @@ class EntityQuery:
required=True,
)

def resolve_entities(self, info, representations):
def resolve_entities(self, info, representations, sub_field_resolution=False):
entities = []
for representation in representations:
type_ = schema.graphql_schema.get_type(representation["__typename"])
Expand All @@ -92,12 +90,53 @@ def resolve_entities(self, info, representations):
model_arguments = {
get_model_attr(k): v for k, v in model_arguments.items()
}

# convert subfields of models from dict to a corresponding graphql type
for model_field, value in model_arguments.items():
if not hasattr(model, model_field):
continue

field = getattr(model, model_field)
if isinstance(field, Field) and isinstance(value, dict):
if value.get("__typename") is None:
value["__typename"] = field.type.of_type._meta.name
model_arguments[model_field] = EntityQuery.resolve_entities(
self,
info,
representations=[value],
sub_field_resolution=True,
).pop()
elif all(
[
isinstance(field, List),
isinstance(value, list),
any(
[
(
hasattr(field, "of_type")
and issubclass(field.of_type, ObjectType)
),
(
hasattr(field, "of_type")
and issubclass(field.of_type, Union)
),
]
),
]
):
for sub_value in value:
if sub_value.get("__typename") is None:
sub_value["__typename"] = field.type.of_type._meta.name
model_arguments[model_field] = EntityQuery.resolve_entities(
self, info, representations=value, sub_field_resolution=True
)

model_instance = model(**model_arguments)

resolver = getattr(
model, "_%s__resolve_reference" % model.__name__, None
) or getattr(model, "_resolve_reference", None)
if resolver:
if resolver and not sub_field_resolution:
model_instance = resolver(model_instance, info)

entities.append(model_instance)
Expand Down
18 changes: 11 additions & 7 deletions graphene_federation/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,20 @@ def _get_query(schema: Schema, query_cls: Optional[ObjectType] = None) -> Object
def build_schema(
query: Optional[ObjectType] = None,
mutation: Optional[ObjectType] = None,
enable_federation_2=False,
federation_version: Optional[float] = None,
enable_federation_2: bool = False,
schema: Optional[Schema] = None,
**kwargs
) -> Schema:
schema = schema or Schema(query=query, mutation=mutation, **kwargs)
schema.auto_camelcase = kwargs.get("auto_camelcase", True)
schema.federation_version = 2 if enable_federation_2 else 1
federation_query = _get_query(schema, schema.query if schema else query)
return Schema(
query=federation_query,
mutation=schema.mutation if schema else mutation,
**kwargs
schema.federation_version = float(
(federation_version or 2) if (enable_federation_2 or federation_version) else 1
)
federation_query = _get_query(schema, schema.query)
# Use shallow copy to prevent recursion error
kwargs = schema.__dict__.copy()
kwargs.pop("query")
kwargs.pop("graphql_schema")
kwargs.pop("federation_version")
return type(schema)(query=federation_query, **kwargs)
Loading
Loading