Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 210 additions & 0 deletions docs/architecture/application/graphql.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ OrchestratorCore comes with a graphql interface that can to be registered after
If you add it after registering your `SUBSCRIPTION_MODEL_REGISTRY` it will automatically create graphql types for them.

example:

```python
app = OrchestratorCore(base_settings=AppSettings())
# register SUBSCRIPTION_MODEL_REGISTRY
Expand Down Expand Up @@ -37,3 +38,212 @@ app = OrchestratorCore(base_settings=AppSettings())
# register SUBSCRIPTION_MODEL_REGISTRY
app.register_graphql(query=NewQuery)
```

## Domain Models Auto Registration for GraphQL

When using the `register_graphql()` function, all products in the `SUBSCRIPTION_MODEL_REGISTRY` will be automatically converted into GraphQL types.
The registration process iterates through the list, starting from the deepest product block and working its way back up to the product level.

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.

Here is an example of the expected error with a self referenced `ProductBlock`:

```
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?
```

To handle this situation, you must manually create the GraphQL type for that `ProductBlock` and add it to the `GRAPHQL_MODELS` list.

Here's an example of how to do it:

```python
# product_block_file.py
import strawberry
from typing import Annotated
from app.product_blocks import ProductBlock
from orchestrator.graphql import GRAPHQL_MODELS


# It is necessary to use pydantic type, so that other product blocks can recognize it when typing to GraphQL.
@strawberry.experimental.pydantic.type(model=ProductBlock)
class ProductBlockGraphql:
name: strawberry.auto
self_reference_block: Annotated[
"ProductBlockGraphql", strawberry.lazy(".product_block_file")
] | None = None
...


# Add the ProductBlockGraphql type to GRAPHQL_MODELS, which is used in auto-registration for already created product blocks.
GRAPHQL_MODELS.update({"ProductBlockGraphql": ProductBlockGraphql})
```

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.

### Scalars for Auto Registration

When working with special types such as `VlanRanges` or `IPv4Interface` in the core module, scalar types are essential for the auto registration process.
Scalar types enable smooth integration of these special types into the GraphQL schema, They need to be initialized before `register_graphql()`.

Here's an example of how to add a new scalar:

```python
import strawberry
from typing import NewType
from orchestrator.graphql import SCALAR_OVERRIDES

VlanRangesType = strawberry.scalar(
NewType("VlanRangesType", str),
description="Represent the Orchestrator VlanRanges data type",
serialize=lambda v: v.to_list_of_tuples(),
parse_value=lambda v: v,
)

# Add the scalar to the SCALAR_OVERRIDES dictionary, with the type in the product block as the key and the scalar as the value
SCALAR_OVERRIDES = {
VlanRanges: VlanRangesType,
}
```

You can find more examples of scalar usage in the `orchestrator/graphql/types.py` file.
For additional information on Scalars, please refer to the Strawberry documentation on Scalars: https://strawberry.rocks/docs/types/scalars.

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.

### Federating with Autogenerated Types

To enable federation, set the `FEDERATION_ENABLED` environment variable to `True`.

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.

Below is an example of a GraphQL app that extends the `SubscriptionInterface`:

```python
from typing import Any

import strawberry
from starlette.applications import Starlette
from starlette.routing import Route
from strawberry.asgi import GraphQL
from uuid import UUID


@strawberry.federation.interface_object(keys=["subscriptionId"])
class SubscriptionInterface:
subscription_id: UUID
new_value: str

@classmethod
async def resolve_reference(cls, **data: Any) -> "SubscriptionInterface":
if not (subscription_id := data.get("subscriptionId")):
raise ValueError(
f"Need 'subscriptionId' to resolve reference. Found keys: {list(data.keys())}"
)

value = new_value_resolver(subscription_id)
return SubscriptionInterface(subscription_id=subscription_id, new_value=value)


@strawberry.type
class Query:
hi: str = strawberry.field(resolver=lambda: "query for other graphql")


# Add `SubscriptionInterface` in types array.
schema = strawberry.federation.Schema(
query=Query,
types=[SubscriptionInterface],
enable_federation_2=True,
)

app = Starlette(debug=True, routes=[Route("/", GraphQL(schema, graphiql=True))])
```

To run this example, execute the following command:

```bash
uvicorn app:app --port 4001 --host 0.0.0.0 --reload
```

In the `supergraph.yaml` file, you can federate the GraphQL endpoints together as shown below:

```yaml
federation_version: 2
subgraphs:
orchestrator:
routing_url: https://orchestrator-graphql-endpoint
schema:
subgraph_url: https://orchestrator-graphql-endpoint
new_graphql:
routing_url: http://localhost:4001
schema:
subgraph_url: http://localhost:4001
```

When both GraphQL endpoints are available, you can compose the supergraph schema using the following command:

```bash
rover supergraph compose --config ./supergraph.yaml > supergraph-schema.graphql
```

The command will return errors if incorrect keys or other issues are present.
Then, you can run the federation with the following command:

```bash
./router --supergraph supergraph-schema.graphql
```

Now you can query the endpoint to obtain `newValue` from all subscriptions using the payload below:

```json
{
"rationName": "ExampleQuery",
"query": "query ExampleQuery {\n subscriptions {\n page {\n newValue\n }\n }\n}\n",
"variables": {}
}
```

#### Federating with Specific Subscriptions

To federate with specific subscriptions, you need to make a few changes. Here's an example of a specific subscription:

```python
# `type` instead of `interface_object` and name the class exactly the same as the one in orchestrator.
@strawberry.federation.type(keys=["subscriptionId"])
class YourProductSubscription:
subscription_id: UUID
new_value: str

@classmethod
async def resolve_reference(cls, **data: Any) -> "SubscriptionInterface":
if not (subscription_id := data.get("subscriptionId")):
raise ValueError(
f"Need 'subscriptionId' to resolve reference. Found keys: {list(data.keys())}"
)

value = new_value_resolver(subscription_id)
return SubscriptionInterface(subscription_id=subscription_id, new_value=value)
```

#### Federating with Specific Subscription Product Blocks

You can also federate a ProductBlock. In this case, the `subscriptionInstanceId` can be replaced with any product block property containing `Id`:

```python
@strawberry.federation.interface_object(keys=["subscriptionInstanceId"])
class YourProductBlock:
subscription_instance_id: UUID
new_value: str

@classmethod
async def resolve_reference(cls, **data: Any) -> "YourProductBlock":
if not (subscription_id := data.get("subscriptionInstanceId")):
raise ValueError(
f"Need 'subscriptionInstanceId' to resolve reference. Found keys: {list(data.keys())}"
)

value = "new value"
return YourProductBlock(subscription_id=subscription_id, new_value="new value")
```

By following these examples, you can effectively federate autogenerated types (`subscriptions` and `product blocks`) enabling seamless integration across multiple GraphQL endpoints.
2 changes: 2 additions & 0 deletions orchestrator/graphql/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@
get_context,
graphql_router,
)
from orchestrator.graphql.types import SCALAR_OVERRIDES

__all__ = [
"GRAPHQL_MODELS",
"SCALAR_OVERRIDES",
"Query",
"Mutation",
"OrchestratorGraphqlRouter",
Expand Down