diff --git a/docs/architecture/application/graphql.md b/docs/architecture/application/graphql.md index f6b3ad39a..5d8b90ebd 100644 --- a/docs/architecture/application/graphql.md +++ b/docs/architecture/application/graphql.md @@ -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 @@ -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 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. diff --git a/orchestrator/graphql/__init__.py b/orchestrator/graphql/__init__.py index 39f647292..1a96fbac9 100644 --- a/orchestrator/graphql/__init__.py +++ b/orchestrator/graphql/__init__.py @@ -27,9 +27,11 @@ get_context, graphql_router, ) +from orchestrator.graphql.types import SCALAR_OVERRIDES __all__ = [ "GRAPHQL_MODELS", + "SCALAR_OVERRIDES", "Query", "Mutation", "OrchestratorGraphqlRouter",