Skip to content

Commit 8b5e1e7

Browse files
arun-sureshkumararunsureshkumaradarshdigievopatrick91
authored
Federation v2 support added (#4)
* Federation v2 support added * Minor fix in link * Minor fix in @provides * Minor fix in @keys * Revert patch in @keys * Remove Print * add override @OverRide to @link * fix @link * fix @link * fix @link * Test case passed * Add @key (multi) support * Fix Test Cases * Fix Test Cases * Fix Lint Error * Update README.md * Fix Lint Error * Multi fields support @extend * Federation v2 support added * Minor fix in link * Minor fix in @provides * Minor fix in @keys * Revert patch in @keys * Remove Print * add override @OverRide to @link * fix @link * fix @link * fix @link * Test case passed * Add @key (multi) support * Fix Test Cases * Fix Test Cases * Fix Lint Error * Update README.md * Fix Lint Error * Multi fields support @extend * Fix Lint error * mark PageInfo @Shareable * Refactor: @OverRide directive argument renamed to from_ * Add: resolvable argument to @key directive * WIP: Toggle for federation v2 * Fix: correct resolvable attribute of entity * Fix: correct entity set building Add: federation v2 arg to test cases * Fix: resolvable argument addition to schema * Refactor: Correct docstrings * Add: enable_federation_2 to examples * Add: test case for compound keys Refactor: Docstring and checks in inaccessible directive * Fix: Lint issues * Fix: Remove duplicated definition of User type in test_key * Add: Compound key validation * Add: test for compound keys * Docs: Correct known issues in readme as compound keys are now working * Fix: lint * Update graphene_federation/shareable.py fix documentation Co-authored-by: Patrick Arminio <[email protected]> * Update graphene_federation/inaccessible.py remove the usage of builtin Co-authored-by: Patrick Arminio <[email protected]> * Fix: Correct output comments in examples * Fix: Use graphene_type._meta.fields for getting valid fields in type * Fix: Add resolvable argument only for federation v2 * Refactor: Rename variable Type to type_ * Doc: Correct comment * Remove: unused _shareable list in shareable.py * Lint: service.py * Add: utility function get_attributed_fields * Add: tests for federation v1 * LInt: fix lint error * Fix: optimise imports * Change: implementation of utility function is_valid_compound_key optimised * Change: combined logic for adding _inaccessible attribute to fields and types * Change: Compound key validation logic using graphql core parse() * Fix: Auto camelcase field names if enabled in schema * Add: Advanced test cases for Compound keys * Lint test_key.py * Support @inaccessible on graphene Interface * Fix: Lint Error * Fix: Gateway Dockerfile build error * Add Support: @inaccessible & @Shareable to graphene.Union * Update: Dependencies, Dockerfile Fix: Lint Error Passed: Test Cases * Add: Test case for @Shareable & @inaccessible * Fix: Lint Error --------- Co-authored-by: Arun Suresh Kumar <[email protected]> Co-authored-by: Adarsh Divakaran <[email protected]> Co-authored-by: Patrick Arminio <[email protected]>
1 parent a68d491 commit 8b5e1e7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+3439
-7520
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,6 @@ There is also a cool [example](https://github.com/preply/graphene-federation/iss
171171
## Known issues
172172

173173
1. decorators will not work properly on fields with custom names for example `some_field = String(name='another_name')`
174-
1. `@key` decorator will not work on [compound primary key](https://www.apollographql.com/docs/federation/entities-advanced/#compound-keys)
175174

176175
------------------------
177176

examples/entities.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def resolve_file(self, **kwargs):
3939
'''
4040
result = schema.execute(query)
4141
print(result.data)
42-
# OrderedDict([('_service', OrderedDict([('sdl', ' type File @key(fields: "id") { id: Int! name: String } extend type Query { hello: String file: File } ')]))])
42+
# {'_service': {'sdl': 'type Query {\n file: File\n}\n\ntype File @key(fields: "id") {\n id: Int!\n name: String\n}'}}
4343

4444
query ='''
4545
query entities($_representations: [_Any!]!) {
@@ -62,4 +62,4 @@ def resolve_file(self, **kwargs):
6262
]
6363
})
6464
print(result.data)
65-
# OrderedDict([('_entities', [OrderedDict([('id', 1), ('name', 'test_name')])])])
65+
# {'_entities': [{'id': 1, 'name': 'test_name'}]}

examples/extend.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,4 @@ def resolve_file(self, **kwargs):
2828
'''
2929
result = schema.execute(query)
3030
print(result.data)
31-
# OrderedDict([('_service', OrderedDict([('sdl', ' extend type Message @key(fields: "id") { id: Int! @external } type Query { message: Message } ')]))])
31+
# {'sdl': 'type Query {\n message: Message\n}\n\nextend type Message @key(fields: "id") {\n id: Int! @external\n}'}}

examples/inaccessible.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import graphene
2+
3+
from graphene_federation import inaccessible, external, provides, key, override, shareable
4+
5+
from graphene_federation import build_schema
6+
7+
8+
@key(fields="x")
9+
class Position(graphene.ObjectType):
10+
x = graphene.Int(required=True)
11+
y = external(graphene.Int(required=True))
12+
z = inaccessible(graphene.Int(required=True))
13+
a = provides(graphene.Int(), fields="x")
14+
b = override(graphene.Int(required=True), from_="h")
15+
16+
17+
@inaccessible
18+
class ReviewInterface(graphene.Interface):
19+
interfaced_body = graphene.String(required=True)
20+
21+
22+
@inaccessible
23+
class Review(graphene.ObjectType):
24+
class Meta:
25+
interfaces = (ReviewInterface,)
26+
27+
id = graphene.Int(required=True)
28+
body = graphene.String(required=True)
29+
30+
31+
@inaccessible
32+
class Human(graphene.ObjectType):
33+
name = graphene.String()
34+
born_in = graphene.String()
35+
36+
37+
@inaccessible
38+
class Droid(graphene.ObjectType):
39+
name = graphene.String()
40+
primary_function = graphene.String()
41+
42+
43+
@inaccessible
44+
class Starship(graphene.ObjectType):
45+
name = graphene.String()
46+
length = graphene.Int()
47+
48+
49+
@inaccessible
50+
class SearchResult(graphene.Union):
51+
class Meta:
52+
types = (Human, Droid, Starship)
53+
54+
55+
class Query(graphene.ObjectType):
56+
position = graphene.Field(Position)
57+
58+
59+
schema = build_schema(Query, enable_federation_2=True, types=(ReviewInterface, SearchResult, Review))
60+
61+
query = '''
62+
query getSDL {
63+
_service {
64+
sdl
65+
}
66+
}
67+
'''
68+
result = schema.execute(query)
69+
print(result.data)
70+
# {'_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}'}}

examples/override.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import graphene
2+
3+
from graphene_federation import build_schema, shareable, external, key, override, inaccessible
4+
5+
6+
@key(fields="id")
7+
class Product(graphene.ObjectType):
8+
id = graphene.ID(required=True)
9+
in_stock = override(graphene.Boolean(required=True), "Products")
10+
out_stock = inaccessible(graphene.Boolean(required=True))
11+
12+
13+
class Query(graphene.ObjectType):
14+
position = graphene.Field(Product)
15+
16+
17+
schema = build_schema(Query, enable_federation_2=True)
18+
19+
query = '''
20+
query getSDL {
21+
_service {
22+
sdl
23+
}
24+
}
25+
'''
26+
result = schema.execute(query)
27+
print(result.data)
28+
# {'_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}'}}

examples/shareable.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import graphene
2+
from graphene import Interface
3+
4+
from graphene_federation.shareable import shareable
5+
6+
from graphene_federation import build_schema
7+
8+
9+
@shareable
10+
class Position(graphene.ObjectType):
11+
x = graphene.Int(required=True)
12+
y = shareable(graphene.Int(required=True))
13+
14+
15+
@shareable
16+
class Human(graphene.ObjectType):
17+
name = graphene.String()
18+
born_in = graphene.String()
19+
20+
21+
@shareable
22+
class Droid(graphene.ObjectType):
23+
name = shareable(graphene.String())
24+
primary_function = graphene.String()
25+
26+
27+
@shareable
28+
class Starship(graphene.ObjectType):
29+
name = graphene.String()
30+
length = shareable(graphene.Int())
31+
32+
33+
@shareable
34+
class SearchResult(graphene.Union):
35+
class Meta:
36+
types = (Human, Droid, Starship)
37+
38+
39+
class Query(graphene.ObjectType):
40+
position = graphene.Field(Position)
41+
42+
43+
schema = build_schema(Query, enable_federation_2=True, types=(SearchResult,))
44+
45+
query = '''
46+
query getSDL {
47+
_service {
48+
sdl
49+
}
50+
}
51+
'''
52+
result = schema.execute(query)
53+
print(result.data)
54+
# {'_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}'}}

examples/tag.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import graphene
2+
3+
from graphene_federation import build_schema, key, inaccessible, shareable
4+
from graphene_federation.tag import tag
5+
6+
7+
class Product(graphene.ObjectType):
8+
id = graphene.ID(required=True)
9+
in_stock = tag(graphene.Boolean(required=True), "Products")
10+
out_stock = shareable(graphene.Boolean(required=True))
11+
is_listed = inaccessible(graphene.Boolean(required=True))
12+
13+
14+
class Query(graphene.ObjectType):
15+
position = graphene.Field(Product)
16+
17+
18+
schema = build_schema(Query, enable_federation_2=True)
19+
20+
query = '''
21+
query getSDL {
22+
_service {
23+
sdl
24+
}
25+
}
26+
'''
27+
result = schema.execute(query)
28+
print(result.data)
29+
# {'_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}'}}

graphene_federation/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
from .main import build_schema
22
from .entity import key
3-
from .extend import extend, external, requires
3+
from .extend import extend
4+
from .external import external
5+
from .requires import requires
6+
from .shareable import shareable
7+
from .inaccessible import inaccessible
48
from .provides import provides
9+
from .override import override

graphene_federation/entity.py

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,22 @@
88
from graphene.types.schema import TypeMap
99

1010
from .types import _Any
11-
from .utils import field_name_to_type_attribute
11+
from .utils import (
12+
field_name_to_type_attribute,
13+
check_fields_exist_on_type,
14+
is_valid_compound_key,
15+
)
16+
17+
import collections.abc
18+
19+
20+
def update(d, u):
21+
for k, v in u.items():
22+
if isinstance(v, collections.abc.Mapping):
23+
d[k] = update(d.get(k, {}), v)
24+
else:
25+
d[k] = v
26+
return d
1227

1328

1429
def get_entities(schema: Schema) -> Dict[str, Any]:
@@ -24,6 +39,14 @@ def get_entities(schema: Schema) -> Dict[str, Any]:
2439
continue
2540
if getattr(type_.graphene_type, "_keys", None):
2641
entities[type_name] = type_.graphene_type
42+
43+
# Validation for compound keys
44+
key_str = " ".join(type_.graphene_type._keys)
45+
type_name = type_.graphene_type._meta.name
46+
if "{" in key_str: # checking for subselection to identify compound key
47+
assert is_valid_compound_key(
48+
type_name, key_str, schema
49+
), f'Invalid compound key definition for type "{type_name}"'
2750
return entities
2851

2952

@@ -83,27 +106,31 @@ def resolve_entities(self, info, representations):
83106
return EntityQuery
84107

85108

86-
def key(fields: str) -> Callable:
109+
def key(fields: str, resolvable: bool = True) -> Callable:
87110
"""
88111
Take as input a field that should be used as key for that entity.
89112
See specification: https://www.apollographql.com/docs/federation/federation-spec/#key
90-
91-
If the input contains a space it means it's a [compound primary key](https://www.apollographql.com/docs/federation/entities/#defining-a-compound-primary-key)
92-
which is not yet supported.
93113
"""
94-
if " " in fields:
95-
raise NotImplementedError("Compound primary keys are not supported.")
96114

97-
def decorator(Type):
115+
def decorator(type_):
98116
# Check the provided fields actually exist on the Type.
99-
assert (
100-
fields in Type._meta.fields
101-
), f'Field "{fields}" does not exist on type "{Type._meta.name}"'
102-
103-
keys = getattr(Type, "_keys", [])
117+
if " " not in fields:
118+
assert (
119+
fields in type_._meta.fields
120+
), f'Field "{fields}" does not exist on type "{type_._meta.name}"'
121+
if "{" not in fields:
122+
# Skip valid fields check if the key is a compound key. The validation for compound keys
123+
# is done on calling get_entities()
124+
fields_set = set(fields.replace(" ", "").split(","))
125+
assert check_fields_exist_on_type(
126+
fields=fields_set, type_=type_
127+
), f'Field "{fields}" does not exist on type "{type_._meta.name}"'
128+
129+
keys = getattr(type_, "_keys", [])
104130
keys.append(fields)
105-
setattr(Type, "_keys", keys)
131+
setattr(type_, "_keys", keys)
132+
setattr(type_, "_resolvable", resolvable)
106133

107-
return Type
134+
return type_
108135

109136
return decorator

0 commit comments

Comments
 (0)