From bee71e8215b44824a08a8f9dd41137e904e168ff Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Wed, 10 Apr 2024 12:54:34 +0200 Subject: [PATCH 1/5] fixed-variables-process-to-handle-objects-and-list-variables --- ariadne_graphql_proxy/query_filter.py | 29 +++++-- tests/conftest.py | 49 +++++++++++ tests/test_proxy_schema.py | 112 ++++++++++++++++++++++++++ 3 files changed, 184 insertions(+), 6 deletions(-) diff --git a/ariadne_graphql_proxy/query_filter.py b/ariadne_graphql_proxy/query_filter.py index 3751cac..2a6d3d9 100644 --- a/ariadne_graphql_proxy/query_filter.py +++ b/ariadne_graphql_proxy/query_filter.py @@ -7,7 +7,9 @@ FragmentSpreadNode, GraphQLSchema, InlineFragmentNode, + ListValueNode, NameNode, + ObjectValueNode, OperationDefinitionNode, SelectionNode, SelectionSetNode, @@ -147,18 +149,33 @@ def filter_operation_node( ), ) + def extract_variables( + self, + value: VariableNode | ListValueNode | ObjectValueNode, + context: QueryFilterContext, + ): + if isinstance(value, VariableNode): + context.variables.add(value.name.value) + elif isinstance(value, ObjectValueNode): + for field in value.fields: + self.extract_variables(field.value, context) # type: ignore + elif isinstance(value, ListValueNode): + for item in value.values: + self.extract_variables(item, context) # type: ignore + + def update_context_variables( + self, field_node: FieldNode, context: QueryFilterContext + ): + for argument in field_node.arguments: + self.extract_variables(argument.value, context) # type: ignore + def filter_field_node( self, field_node: FieldNode, schema_obj: str, context: QueryFilterContext, ) -> Optional[FieldNode]: - context.variables.update( - argument.value.name.value - for argument in field_node.arguments - if isinstance(argument.value, VariableNode) - ) - + self.update_context_variables(field_node, context) if not field_node.selection_set: return field_node diff --git a/tests/conftest.py b/tests/conftest.py index 9f251f3..d39a621 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -322,3 +322,52 @@ def search_root_value(): @pytest.fixture def gql(): return lambda x: x + + +@pytest.fixture +def car_schema(): + return make_executable_schema( + """ + type Query { + carsByIds(ids: [ID!]!): [Car!]! + carsByCriteria(input: SearchInput!): [Car!]! + } + + type Car { + id: ID! + make: String! + model: String! + year: Int! + } + + input SearchInput { + search: SearchCriteria + } + + input SearchCriteria { + make: String + model: String + year: Int + } + """ + ) + + +@pytest.fixture +def car_schema_json(car_schema): + schema_data = graphql_sync(car_schema, get_introspection_query()).data + return {"data": schema_data} + + +@pytest.fixture +def car_root_value(): + return { + "carsByIds": [ + {"id": "car1", "make": "Toyota", "model": "Corolla", "year": 2020}, + {"id": "car2", "make": "Honda", "model": "Civic", "year": 2019}, + ], + "carsByCriteria": [ + {"id": "car3", "make": "Ford", "model": "Mustang", "year": 2018}, + {"id": "car4", "make": "Chevrolet", "model": "Camaro", "year": 2017}, + ], + } diff --git a/tests/test_proxy_schema.py b/tests/test_proxy_schema.py index 068b282..7a46cc1 100644 --- a/tests/test_proxy_schema.py +++ b/tests/test_proxy_schema.py @@ -743,6 +743,118 @@ async def test_proxy_schema_splits_variables_between_schemas( } +@pytest.mark.asyncio +async def test_proxy_schema_handles_object_variables_correctly( + httpx_mock, + car_schema_json, + car_root_value, +): + httpx_mock.add_response( + url="http://graphql.example.com/cars/", json=car_schema_json + ) + httpx_mock.add_response( + url="http://graphql.example.com/cars/", + json={"data": car_root_value["carsByCriteria"]}, + ) + + proxy_schema = ProxySchema() + proxy_schema.add_remote_schema("http://graphql.example.com/cars/") + proxy_schema.get_final_schema() + + await proxy_schema.root_resolver( + {}, + "CarsByCriteriaQuery", + {"criteria": {"make": "Toyota", "model": "Corolla", "year": 2020}}, + parse( + """ + query CarsByCriteriaQuery($criteria: SearchCriteria!) { + carsByCriteria(input: { criteria: $criteria }) { + id + make + model + year + } + } + """ + ), + ) + + cars_request = httpx_mock.get_requests(url="http://graphql.example.com/cars/")[-1] + + assert json.loads(cars_request.content) == { + "operationName": "CarsByCriteriaQuery", + "variables": {"criteria": {"make": "Toyota", "model": "Corolla", "year": 2020}}, + "query": dedent( + """ + query CarsByCriteriaQuery($criteria: SearchCriteria!) { + carsByCriteria(input: {criteria: $criteria}) { + id + make + model + year + } + } + """ + ).strip(), + } + + +@pytest.mark.asyncio +async def test_proxy_schema_handles_list_variables_correctly( + httpx_mock, + car_schema_json, + car_root_value, +): + httpx_mock.add_response( + url="http://graphql.example.com/cars/", json=car_schema_json + ) + httpx_mock.add_response( + url="http://graphql.example.com/cars/", + json={"data": car_root_value["carsByIds"]}, + ) + + proxy_schema = ProxySchema() + proxy_schema.add_remote_schema("http://graphql.example.com/cars/") + proxy_schema.get_final_schema() + + await proxy_schema.root_resolver( + {}, + "CarsQuery", + {"id": "car2"}, + parse( + """ + query CarsQuery($id: ID!) { + carsByIds(ids: [$id]) { + id + make + model + year + } + } + """ + ), + ) + + cars_request = httpx_mock.get_requests(url="http://graphql.example.com/cars/")[-1] + + assert json.loads(cars_request.content) == { + "operationName": "CarsQuery", + "variables": {"id": "car2"}, + "query": dedent( + """ + query CarsQuery($id: ID!) { + carsByIds(ids: [$id]) { + id + make + model + year + } + } + """ + ).strip(), + } + + @pytest.mark.asyncio async def test_proxy_schema_splits_variables_from_fragments_between_schemas( httpx_mock, From 4f96aaba71ee23cd87218322c9c4e76973da7fbe Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Wed, 10 Apr 2024 13:06:37 +0200 Subject: [PATCH 2/5] reorganize-function-placement --- ariadne_graphql_proxy/query_filter.py | 40 +++++++++++++-------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/ariadne_graphql_proxy/query_filter.py b/ariadne_graphql_proxy/query_filter.py index 2a6d3d9..5625a2c 100644 --- a/ariadne_graphql_proxy/query_filter.py +++ b/ariadne_graphql_proxy/query_filter.py @@ -149,26 +149,6 @@ def filter_operation_node( ), ) - def extract_variables( - self, - value: VariableNode | ListValueNode | ObjectValueNode, - context: QueryFilterContext, - ): - if isinstance(value, VariableNode): - context.variables.add(value.name.value) - elif isinstance(value, ObjectValueNode): - for field in value.fields: - self.extract_variables(field.value, context) # type: ignore - elif isinstance(value, ListValueNode): - for item in value.values: - self.extract_variables(item, context) # type: ignore - - def update_context_variables( - self, field_node: FieldNode, context: QueryFilterContext - ): - for argument in field_node.arguments: - self.extract_variables(argument.value, context) # type: ignore - def filter_field_node( self, field_node: FieldNode, @@ -408,3 +388,23 @@ def get_type_fields_dependencies( return self.dependencies[schema_id][type_name] return None + + def update_context_variables( + self, field_node: FieldNode, context: QueryFilterContext + ): + for argument in field_node.arguments: + self.extract_variables(argument.value, context) # type: ignore + + def extract_variables( + self, + value: VariableNode | ListValueNode | ObjectValueNode, + context: QueryFilterContext, + ): + if isinstance(value, VariableNode): + context.variables.add(value.name.value) + elif isinstance(value, ObjectValueNode): + for field in value.fields: + self.extract_variables(field.value, context) # type: ignore + elif isinstance(value, ListValueNode): + for item in value.values: + self.extract_variables(item, context) # type: ignore From 050a6eb7c1a708ae81c2920809d188edcfbd1745 Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Thu, 11 Apr 2024 09:26:27 +0200 Subject: [PATCH 3/5] Add missing changelog entry --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8218960..fc2429e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## 0.4.0 (UNRELEASED) +- Improved handling of nested variables in objects and lists. + + ## 0.3.0 (2024-03-26) - Added `CacheSerializer`, `NoopCacheSerializer` and `JSONCacheSerializer`. Changed `CacheBackend`, `InMemoryCache`, `CloudflareCacheBackend` and `DynamoDBCacheBackend` to accept `serializer` initialization option. From aa4c74a07ed4219d84af68817064924f8c258e10 Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Thu, 11 Apr 2024 10:39:53 +0200 Subject: [PATCH 4/5] Update changelog entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc2429e..9ddc54f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # CHANGELOG ## 0.4.0 (UNRELEASED) -- Improved handling of nested variables in objects and lists. +- Fixed handling of nested variables in objects and lists. ## 0.3.0 (2024-03-26) From e646d0691e7b354090b6c0dda3595e6011c3af08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Pito=C5=84?= Date: Thu, 11 Apr 2024 14:16:31 +0200 Subject: [PATCH 5/5] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ddc54f..06f9bba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # CHANGELOG ## 0.4.0 (UNRELEASED) + - Fixed handling of nested variables in objects and lists.