From 36a6347586ca1e7b0354e281f3d8832cfd5e8a56 Mon Sep 17 00:00:00 2001 From: "Tjeerd.Verschragen" Date: Thu, 3 Aug 2023 11:44:51 +0200 Subject: [PATCH 1/5] Add graphql documentation for the model auto registration --- docs/architecture/application/graphql.md | 64 ++++++++++++++++++++++++ orchestrator/graphql/__init__.py | 2 + 2 files changed, 66 insertions(+) diff --git a/docs/architecture/application/graphql.md b/docs/architecture/application/graphql.md index f6b3ad39a..54fd5060e 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,66 @@ 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 can lead to an infinite loop. +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 +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_refrence_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 infinite loop 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. + +Here's an example of how to add a new scalar: + +```python +import strawberry +from typing import NewType +from orchestrator.graphql import SCALAR_OVERRIDESs + +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-core/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. 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", From 683c4ef4628d8b17c1dbe9694dc7219306ea9ee4 Mon Sep 17 00:00:00 2001 From: "Tjeerd.Verschragen" Date: Thu, 3 Aug 2023 14:16:47 +0200 Subject: [PATCH 2/5] Add graphql docs for federating the autoregistered types --- docs/architecture/application/graphql.md | 138 +++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/docs/architecture/application/graphql.md b/docs/architecture/application/graphql.md index 54fd5060e..38d5c6040 100644 --- a/docs/architecture/application/graphql.md +++ b/docs/architecture/application/graphql.md @@ -101,3 +101,141 @@ You can find more examples of scalar usage in the `orchestrator-core/orchestrato 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. From 57f127f1ff24293f4b45bb25e1480d453c112499 Mon Sep 17 00:00:00 2001 From: "Tjeerd.Verschragen" Date: Thu, 3 Aug 2023 14:50:28 +0200 Subject: [PATCH 3/5] Fix graphql docs with incorrect issue for self reference product blocks - also fix typo/formatting. --- docs/architecture/application/graphql.md | 26 ++++++++++++++++-------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/docs/architecture/application/graphql.md b/docs/architecture/application/graphql.md index 38d5c6040..f80406e06 100644 --- a/docs/architecture/application/graphql.md +++ b/docs/architecture/application/graphql.md @@ -41,15 +41,23 @@ 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. +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 can lead to an infinite loop. -To handle this situation, you must manually create the GraphQL type for that ProductBlock and add it to the GRAPHQL_MODELS list. +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 an example of the expected error with self referenced `ProductBlock`: + +``` +trawberry.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 @@ -60,7 +68,7 @@ from orchestrator.graphql import GRAPHQL_MODELS @strawberry.experimental.pydantic.type(model=ProductBlock) class ProductBlockGraphql: name: strawberry.auto - self_refrence_block: Annotated[ + self_reference_block: Annotated[ "ProductBlockGraphql", strawberry.lazy(".product_block_file") ] | None = None ... @@ -70,19 +78,19 @@ class ProductBlockGraphql: 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 infinite loop scenarios and enable smooth integration of domain models with GraphQL. +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. +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_OVERRIDESs +from orchestrator.graphql import SCALAR_OVERRIDES VlanRangesType = strawberry.scalar( NewType("VlanRangesType", str), @@ -97,7 +105,7 @@ SCALAR_OVERRIDES = { } ``` -You can find more examples of scalar usage in the `orchestrator-core/orchestrator/graphql/types.py` file. +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. From 37afb85ebc09977a89eb22a75bb056d39737939e Mon Sep 17 00:00:00 2001 From: tjeerddie Date: Mon, 7 Aug 2023 15:57:59 +0200 Subject: [PATCH 4/5] Fix typo of trawberry to strawberry Co-authored-by: Daniel Marjenburgh --- docs/architecture/application/graphql.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture/application/graphql.md b/docs/architecture/application/graphql.md index f80406e06..3fdd12133 100644 --- a/docs/architecture/application/graphql.md +++ b/docs/architecture/application/graphql.md @@ -49,7 +49,7 @@ However, there is a potential issue when dealing with a `ProductBlock` that refe Here an example of the expected error with self referenced `ProductBlock`: ``` -trawberry.experimental.pydantic.exceptions.UnregisteredTypeException: Cannot find a Strawberry Type for did you forget to register it? +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. From 45d5545312db63d0865789d386f092bf2422c015 Mon Sep 17 00:00:00 2001 From: tjeerddie Date: Mon, 7 Aug 2023 16:00:23 +0200 Subject: [PATCH 5/5] Update docs/architecture/application/graphql.md Co-authored-by: Daniel Marjenburgh --- docs/architecture/application/graphql.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture/application/graphql.md b/docs/architecture/application/graphql.md index 3fdd12133..5d8b90ebd 100644 --- a/docs/architecture/application/graphql.md +++ b/docs/architecture/application/graphql.md @@ -46,7 +46,7 @@ The registration process iterates through the list, starting from the deepest pr 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 an example of the expected error with self referenced `ProductBlock`: +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?