Skip to content

Commit 6ddd208

Browse files
Add graphql documentation for the model auto registration (#324)
* Add graphql documentation for the model auto registration * Add graphql docs for federating the autoregistered types * Fix graphql docs with incorrect issue for self reference product blocks - also fix typo/formatting. * Fix typo of trawberry to strawberry Co-authored-by: Daniel Marjenburgh <[email protected]> * Update docs/architecture/application/graphql.md Co-authored-by: Daniel Marjenburgh <[email protected]> --------- Co-authored-by: Tjeerd.Verschragen <[email protected]> Co-authored-by: Daniel Marjenburgh <[email protected]>
1 parent 07062ec commit 6ddd208

File tree

2 files changed

+212
-0
lines changed

2 files changed

+212
-0
lines changed

docs/architecture/application/graphql.md

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ OrchestratorCore comes with a graphql interface that can to be registered after
44
If you add it after registering your `SUBSCRIPTION_MODEL_REGISTRY` it will automatically create graphql types for them.
55

66
example:
7+
78
```python
89
app = OrchestratorCore(base_settings=AppSettings())
910
# register SUBSCRIPTION_MODEL_REGISTRY
@@ -37,3 +38,212 @@ app = OrchestratorCore(base_settings=AppSettings())
3738
# register SUBSCRIPTION_MODEL_REGISTRY
3839
app.register_graphql(query=NewQuery)
3940
```
41+
42+
## Domain Models Auto Registration for GraphQL
43+
44+
When using the `register_graphql()` function, all products in the `SUBSCRIPTION_MODEL_REGISTRY` will be automatically converted into GraphQL types.
45+
The registration process iterates through the list, starting from the deepest product block and working its way back up to the product level.
46+
47+
However, there is a potential issue when dealing with a `ProductBlock` that references itself, as it leads to an error expecting the `ProductBlock` type to exist.
48+
49+
Here is an example of the expected error with a self referenced `ProductBlock`:
50+
51+
```
52+
strawberry.experimental.pydantic.exceptions.UnregisteredTypeException: Cannot find a Strawberry Type for <class 'products.product_blocks.product_block_file.ProductBlock'> did you forget to register it?
53+
```
54+
55+
To handle this situation, you must manually create the GraphQL type for that `ProductBlock` and add it to the `GRAPHQL_MODELS` list.
56+
57+
Here's an example of how to do it:
58+
59+
```python
60+
# product_block_file.py
61+
import strawberry
62+
from typing import Annotated
63+
from app.product_blocks import ProductBlock
64+
from orchestrator.graphql import GRAPHQL_MODELS
65+
66+
67+
# It is necessary to use pydantic type, so that other product blocks can recognize it when typing to GraphQL.
68+
@strawberry.experimental.pydantic.type(model=ProductBlock)
69+
class ProductBlockGraphql:
70+
name: strawberry.auto
71+
self_reference_block: Annotated[
72+
"ProductBlockGraphql", strawberry.lazy(".product_block_file")
73+
] | None = None
74+
...
75+
76+
77+
# Add the ProductBlockGraphql type to GRAPHQL_MODELS, which is used in auto-registration for already created product blocks.
78+
GRAPHQL_MODELS.update({"ProductBlockGraphql": ProductBlockGraphql})
79+
```
80+
81+
By following this example, you can effectively create the necessary GraphQL type for `ProductBlock` and ensure proper registration with `register_graphql()`. This will help you avoid any `Cannot find a Strawberry Type` scenarios and enable smooth integration of domain models with GraphQL.
82+
83+
### Scalars for Auto Registration
84+
85+
When working with special types such as `VlanRanges` or `IPv4Interface` in the core module, scalar types are essential for the auto registration process.
86+
Scalar types enable smooth integration of these special types into the GraphQL schema, They need to be initialized before `register_graphql()`.
87+
88+
Here's an example of how to add a new scalar:
89+
90+
```python
91+
import strawberry
92+
from typing import NewType
93+
from orchestrator.graphql import SCALAR_OVERRIDES
94+
95+
VlanRangesType = strawberry.scalar(
96+
NewType("VlanRangesType", str),
97+
description="Represent the Orchestrator VlanRanges data type",
98+
serialize=lambda v: v.to_list_of_tuples(),
99+
parse_value=lambda v: v,
100+
)
101+
102+
# Add the scalar to the SCALAR_OVERRIDES dictionary, with the type in the product block as the key and the scalar as the value
103+
SCALAR_OVERRIDES = {
104+
VlanRanges: VlanRangesType,
105+
}
106+
```
107+
108+
You can find more examples of scalar usage in the `orchestrator/graphql/types.py` file.
109+
For additional information on Scalars, please refer to the Strawberry documentation on Scalars: https://strawberry.rocks/docs/types/scalars.
110+
111+
By using scalar types for auto registration, you can seamlessly incorporate special types into your GraphQL schema, making it easier to work with complex data in the Orchestrator application.
112+
113+
### Federating with Autogenerated Types
114+
115+
To enable federation, set the `FEDERATION_ENABLED` environment variable to `True`.
116+
117+
Federation allows you to federate with subscriptions using the `subscriptionId` and with product blocks inside the subscription by utilizing any property that includes `_id` in its name.
118+
119+
Below is an example of a GraphQL app that extends the `SubscriptionInterface`:
120+
121+
```python
122+
from typing import Any
123+
124+
import strawberry
125+
from starlette.applications import Starlette
126+
from starlette.routing import Route
127+
from strawberry.asgi import GraphQL
128+
from uuid import UUID
129+
130+
131+
@strawberry.federation.interface_object(keys=["subscriptionId"])
132+
class SubscriptionInterface:
133+
subscription_id: UUID
134+
new_value: str
135+
136+
@classmethod
137+
async def resolve_reference(cls, **data: Any) -> "SubscriptionInterface":
138+
if not (subscription_id := data.get("subscriptionId")):
139+
raise ValueError(
140+
f"Need 'subscriptionId' to resolve reference. Found keys: {list(data.keys())}"
141+
)
142+
143+
value = new_value_resolver(subscription_id)
144+
return SubscriptionInterface(subscription_id=subscription_id, new_value=value)
145+
146+
147+
@strawberry.type
148+
class Query:
149+
hi: str = strawberry.field(resolver=lambda: "query for other graphql")
150+
151+
152+
# Add `SubscriptionInterface` in types array.
153+
schema = strawberry.federation.Schema(
154+
query=Query,
155+
types=[SubscriptionInterface],
156+
enable_federation_2=True,
157+
)
158+
159+
app = Starlette(debug=True, routes=[Route("/", GraphQL(schema, graphiql=True))])
160+
```
161+
162+
To run this example, execute the following command:
163+
164+
```bash
165+
uvicorn app:app --port 4001 --host 0.0.0.0 --reload
166+
```
167+
168+
In the `supergraph.yaml` file, you can federate the GraphQL endpoints together as shown below:
169+
170+
```yaml
171+
federation_version: 2
172+
subgraphs:
173+
orchestrator:
174+
routing_url: https://orchestrator-graphql-endpoint
175+
schema:
176+
subgraph_url: https://orchestrator-graphql-endpoint
177+
new_graphql:
178+
routing_url: http://localhost:4001
179+
schema:
180+
subgraph_url: http://localhost:4001
181+
```
182+
183+
When both GraphQL endpoints are available, you can compose the supergraph schema using the following command:
184+
185+
```bash
186+
rover supergraph compose --config ./supergraph.yaml > supergraph-schema.graphql
187+
```
188+
189+
The command will return errors if incorrect keys or other issues are present.
190+
Then, you can run the federation with the following command:
191+
192+
```bash
193+
./router --supergraph supergraph-schema.graphql
194+
```
195+
196+
Now you can query the endpoint to obtain `newValue` from all subscriptions using the payload below:
197+
198+
```json
199+
{
200+
"rationName": "ExampleQuery",
201+
"query": "query ExampleQuery {\n subscriptions {\n page {\n newValue\n }\n }\n}\n",
202+
"variables": {}
203+
}
204+
```
205+
206+
#### Federating with Specific Subscriptions
207+
208+
To federate with specific subscriptions, you need to make a few changes. Here's an example of a specific subscription:
209+
210+
```python
211+
# `type` instead of `interface_object` and name the class exactly the same as the one in orchestrator.
212+
@strawberry.federation.type(keys=["subscriptionId"])
213+
class YourProductSubscription:
214+
subscription_id: UUID
215+
new_value: str
216+
217+
@classmethod
218+
async def resolve_reference(cls, **data: Any) -> "SubscriptionInterface":
219+
if not (subscription_id := data.get("subscriptionId")):
220+
raise ValueError(
221+
f"Need 'subscriptionId' to resolve reference. Found keys: {list(data.keys())}"
222+
)
223+
224+
value = new_value_resolver(subscription_id)
225+
return SubscriptionInterface(subscription_id=subscription_id, new_value=value)
226+
```
227+
228+
#### Federating with Specific Subscription Product Blocks
229+
230+
You can also federate a ProductBlock. In this case, the `subscriptionInstanceId` can be replaced with any product block property containing `Id`:
231+
232+
```python
233+
@strawberry.federation.interface_object(keys=["subscriptionInstanceId"])
234+
class YourProductBlock:
235+
subscription_instance_id: UUID
236+
new_value: str
237+
238+
@classmethod
239+
async def resolve_reference(cls, **data: Any) -> "YourProductBlock":
240+
if not (subscription_id := data.get("subscriptionInstanceId")):
241+
raise ValueError(
242+
f"Need 'subscriptionInstanceId' to resolve reference. Found keys: {list(data.keys())}"
243+
)
244+
245+
value = "new value"
246+
return YourProductBlock(subscription_id=subscription_id, new_value="new value")
247+
```
248+
249+
By following these examples, you can effectively federate autogenerated types (`subscriptions` and `product blocks`) enabling seamless integration across multiple GraphQL endpoints.

orchestrator/graphql/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@
2727
get_context,
2828
graphql_router,
2929
)
30+
from orchestrator.graphql.types import SCALAR_OVERRIDES
3031

3132
__all__ = [
3233
"GRAPHQL_MODELS",
34+
"SCALAR_OVERRIDES",
3335
"Query",
3436
"Mutation",
3537
"OrchestratorGraphqlRouter",

0 commit comments

Comments
 (0)