From 78b423b8bf12fc5ca0959d1767043f7d7917af21 Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Fri, 26 Jul 2024 12:30:11 +0200 Subject: [PATCH 1/2] support async_client with custom operations --- ariadne_codegen/client_generators/client.py | 132 ++++-- ariadne_codegen/client_generators/package.py | 6 +- .../expected_client/__init__.py | 24 ++ .../expected_client/base_client.py | 209 ++++++++++ .../expected_client/base_model.py | 27 ++ .../expected_client/base_operation.py | 156 +++++++ .../expected_client/client.py | 105 +++++ .../expected_client/custom_fields.py | 384 ++++++++++++++++++ .../expected_client/custom_mutations.py | 15 + .../expected_client/custom_queries.py | 55 +++ .../expected_client/custom_typing_fields.py | 95 +++++ .../expected_client/enums.py | 9 + .../expected_client/exceptions.py | 83 ++++ .../expected_client/input_types.py | 0 .../custom_sync_query_builder/pyproject.toml | 6 + .../custom_sync_query_builder/schema.graphql | 249 ++++++++++++ tests/main/test_main.py | 8 + 17 files changed, 1521 insertions(+), 42 deletions(-) create mode 100644 tests/main/clients/custom_sync_query_builder/expected_client/__init__.py create mode 100644 tests/main/clients/custom_sync_query_builder/expected_client/base_client.py create mode 100644 tests/main/clients/custom_sync_query_builder/expected_client/base_model.py create mode 100644 tests/main/clients/custom_sync_query_builder/expected_client/base_operation.py create mode 100644 tests/main/clients/custom_sync_query_builder/expected_client/client.py create mode 100644 tests/main/clients/custom_sync_query_builder/expected_client/custom_fields.py create mode 100644 tests/main/clients/custom_sync_query_builder/expected_client/custom_mutations.py create mode 100644 tests/main/clients/custom_sync_query_builder/expected_client/custom_queries.py create mode 100644 tests/main/clients/custom_sync_query_builder/expected_client/custom_typing_fields.py create mode 100644 tests/main/clients/custom_sync_query_builder/expected_client/enums.py create mode 100644 tests/main/clients/custom_sync_query_builder/expected_client/exceptions.py create mode 100644 tests/main/clients/custom_sync_query_builder/expected_client/input_types.py create mode 100644 tests/main/clients/custom_sync_query_builder/pyproject.toml create mode 100644 tests/main/clients/custom_sync_query_builder/schema.graphql diff --git a/ariadne_codegen/client_generators/client.py b/ariadne_codegen/client_generators/client.py index 54b0fc0a..4c0831a5 100644 --- a/ariadne_codegen/client_generators/client.py +++ b/ariadne_codegen/client_generators/client.py @@ -505,7 +505,28 @@ def create_build_operation_ast_method(self): return_type=generate_name("DocumentNode"), ) - def create_execute_custom_operation_method(self): + def create_execute_custom_operation_method(self, async_client: bool): + execute_call = generate_call( + func=generate_attribute(value=generate_name("self"), attr="execute"), + args=[ + generate_call( + func=generate_name("print_ast"), + args=[generate_name("operation_ast")], + ) + ], + keywords=[ + generate_keyword( + arg="variables", value=generate_name('combined_variables["values"]') + ), + generate_keyword( + arg="operation_name", value=generate_name("operation_name") + ), + ], + ) + response_value = ( + generate_await(value=execute_call) if async_client else execute_call + ) + method_body = [ generate_assign( targets=["selections"], @@ -549,54 +570,31 @@ def create_execute_custom_operation_method(self): ], ), ), - generate_assign( - targets=["response"], - value=generate_await( - value=generate_call( - func=generate_attribute( - value=generate_name("self"), - attr="execute", - ), - args=[ - generate_call( - func=generate_name("print_ast"), - args=[generate_name("operation_ast")], - ) - ], - keywords=[ - generate_keyword( - arg="variables", - value=generate_name('combined_variables["values"]'), - ), - generate_keyword( - arg="operation_name", - value=generate_name("operation_name"), - ), - ], - ) - ), - ), + generate_assign(targets=["response"], value=response_value), generate_return( value=generate_call( func=generate_attribute( - value=generate_name("self"), - attr="get_data", + value=generate_name("self"), attr="get_data" ), args=[generate_name("response")], ) ), ] - return generate_async_method_definition( + + method_definition = ( + generate_async_method_definition + if async_client + else generate_method_definition + ) + + return method_definition( name="execute_custom_operation", arguments=generate_arguments( args=[ generate_arg("self"), generate_arg("*fields", annotation=generate_name("GraphQLField")), generate_arg( - "operation_type", - annotation=generate_name( - "OperationType", - ), + "operation_type", annotation=generate_name("OperationType") ), generate_arg("operation_name", annotation=generate_name("str")), ] @@ -655,7 +653,7 @@ def create_build_selection_set(self): ), ) - def add_execute_custom_operation_method(self): + def add_execute_custom_operation_method(self, async_client: bool): self._add_import( generate_import_from( [ @@ -679,13 +677,20 @@ def add_execute_custom_operation_method(self): ) self._add_import(generate_import_from([DICT, TUPLE, LIST, ANY], "typing")) - self._class_def.body.append(self.create_execute_custom_operation_method()) + self._class_def.body.append( + self.create_execute_custom_operation_method(async_client) + ) self._class_def.body.append(self.create_combine_variables_method()) self._class_def.body.append(self.create_build_variable_definitions_method()) self._class_def.body.append(self.create_build_operation_ast_method()) self._class_def.body.append(self.create_build_selection_set()) - def create_custom_operation_method(self, name, operation_type): + def create_custom_operation_method( + self, + name: str, + operation_type: str, + async_client: bool, + ): self._add_import( generate_import_from( [ @@ -694,6 +699,55 @@ def create_custom_operation_method(self, name, operation_type): GRAPHQL_MODULE, ) ) + if async_client: + def_query = self._create_async_operation_method(name, operation_type) + else: + def_query = self._create_sync_operation_method(name, operation_type) + self._class_def.body.append(def_query) + + def _create_sync_operation_method(self, name: str, operation_type: str): + body_return = generate_return( + value=generate_call( + func=generate_attribute( + value=generate_name("self"), + attr="execute_custom_operation", + ), + args=[ + generate_name("*fields"), + ], + keywords=[ + generate_keyword( + arg="operation_type", + value=generate_attribute( + value=generate_name("OperationType"), + attr=operation_type, + ), + ), + generate_keyword( + arg="operation_name", value=generate_name("operation_name") + ), + ], + ) + ) + + def_query = generate_method_definition( + name=name, + arguments=generate_arguments( + args=[ + generate_arg("self"), + generate_arg("*fields", annotation=generate_name("GraphQLField")), + generate_arg("operation_name", annotation=generate_name("str")), + ], + ), + body=[body_return], + return_type=generate_subscript( + generate_name(DICT), + generate_tuple([generate_name("str"), generate_name("Any")]), + ), + ) + return def_query + + def _create_async_operation_method(self, name: str, operation_type: str): body_return = generate_return( value=generate_await( value=generate_call( @@ -734,7 +788,7 @@ def create_custom_operation_method(self, name, operation_type): generate_tuple([generate_name("str"), generate_name("Any")]), ), ) - self._class_def.body.append(async_def_query) + return async_def_query def get_variable_names(self, arguments: ast.arguments) -> Dict[str, str]: mapped_variable_names = [ diff --git a/ariadne_codegen/client_generators/package.py b/ariadne_codegen/client_generators/package.py index a05cc594..5c91aa8e 100644 --- a/ariadne_codegen/client_generators/package.py +++ b/ariadne_codegen/client_generators/package.py @@ -156,16 +156,16 @@ def generate(self) -> List[str]: if self.enable_custom_operations: self._generate_custom_fields_typing() self._generate_custom_fields() - self.client_generator.add_execute_custom_operation_method() + self.client_generator.add_execute_custom_operation_method(self.async_client) if self.custom_query_generator: self._generate_custom_queries() self.client_generator.create_custom_operation_method( - "query", OperationType.QUERY.value.upper() + "query", OperationType.QUERY.value.upper(), self.async_client ) if self.custom_mutation_generator: self._generate_custom_mutations() self.client_generator.create_custom_operation_method( - "mutation", OperationType.MUTATION.value.upper() + "mutation", OperationType.MUTATION.value.upper(), self.async_client ) self._generate_client() diff --git a/tests/main/clients/custom_sync_query_builder/expected_client/__init__.py b/tests/main/clients/custom_sync_query_builder/expected_client/__init__.py new file mode 100644 index 00000000..c16eb212 --- /dev/null +++ b/tests/main/clients/custom_sync_query_builder/expected_client/__init__.py @@ -0,0 +1,24 @@ +from .base_client import BaseClient +from .base_model import BaseModel, Upload +from .client import Client +from .enums import MetadataErrorCode +from .exceptions import ( + GraphQLClientError, + GraphQLClientGraphQLError, + GraphQLClientGraphQLMultiError, + GraphQLClientHttpError, + GraphQLClientInvalidResponseError, +) + +__all__ = [ + "BaseClient", + "BaseModel", + "Client", + "GraphQLClientError", + "GraphQLClientGraphQLError", + "GraphQLClientGraphQLMultiError", + "GraphQLClientHttpError", + "GraphQLClientInvalidResponseError", + "MetadataErrorCode", + "Upload", +] diff --git a/tests/main/clients/custom_sync_query_builder/expected_client/base_client.py b/tests/main/clients/custom_sync_query_builder/expected_client/base_client.py new file mode 100644 index 00000000..76e3ac1f --- /dev/null +++ b/tests/main/clients/custom_sync_query_builder/expected_client/base_client.py @@ -0,0 +1,209 @@ +import json +from typing import IO, Any, Dict, List, Optional, Tuple, TypeVar, cast + +import httpx +from pydantic import BaseModel +from pydantic_core import to_jsonable_python + +from .base_model import UNSET, Upload +from .exceptions import ( + GraphQLClientGraphQLMultiError, + GraphQLClientHttpError, + GraphQLClientInvalidResponseError, +) + +Self = TypeVar("Self", bound="BaseClient") + + +class BaseClient: + def __init__( + self, + url: str = "", + headers: Optional[Dict[str, str]] = None, + http_client: Optional[httpx.Client] = None, + ) -> None: + self.url = url + self.headers = headers + + self.http_client = http_client if http_client else httpx.Client(headers=headers) + + def __enter__(self: Self) -> Self: + return self + + def __exit__( + self, + exc_type: object, + exc_val: object, + exc_tb: object, + ) -> None: + self.http_client.close() + + def execute( + self, + query: str, + operation_name: Optional[str] = None, + variables: Optional[Dict[str, Any]] = None, + **kwargs: Any, + ) -> httpx.Response: + processed_variables, files, files_map = self._process_variables(variables) + + if files and files_map: + return self._execute_multipart( + query=query, + operation_name=operation_name, + variables=processed_variables, + files=files, + files_map=files_map, + **kwargs, + ) + + return self._execute_json( + query=query, + operation_name=operation_name, + variables=processed_variables, + **kwargs, + ) + + def get_data(self, response: httpx.Response) -> Dict[str, Any]: + if not response.is_success: + raise GraphQLClientHttpError( + status_code=response.status_code, response=response + ) + + try: + response_json = response.json() + except ValueError as exc: + raise GraphQLClientInvalidResponseError(response=response) from exc + + if (not isinstance(response_json, dict)) or ( + "data" not in response_json and "errors" not in response_json + ): + raise GraphQLClientInvalidResponseError(response=response) + + data = response_json.get("data") + errors = response_json.get("errors") + + if errors: + raise GraphQLClientGraphQLMultiError.from_errors_dicts( + errors_dicts=errors, data=data + ) + + return cast(Dict[str, Any], data) + + def _process_variables( + self, variables: Optional[Dict[str, Any]] + ) -> Tuple[ + Dict[str, Any], Dict[str, Tuple[str, IO[bytes], str]], Dict[str, List[str]] + ]: + if not variables: + return {}, {}, {} + + serializable_variables = self._convert_dict_to_json_serializable(variables) + return self._get_files_from_variables(serializable_variables) + + def _convert_dict_to_json_serializable( + self, dict_: Dict[str, Any] + ) -> Dict[str, Any]: + return { + key: self._convert_value(value) + for key, value in dict_.items() + if value is not UNSET + } + + def _convert_value(self, value: Any) -> Any: + if isinstance(value, BaseModel): + return value.model_dump(by_alias=True, exclude_unset=True) + if isinstance(value, list): + return [self._convert_value(item) for item in value] + return value + + def _get_files_from_variables( + self, variables: Dict[str, Any] + ) -> Tuple[ + Dict[str, Any], Dict[str, Tuple[str, IO[bytes], str]], Dict[str, List[str]] + ]: + files_map: Dict[str, List[str]] = {} + files_list: List[Upload] = [] + + def separate_files(path: str, obj: Any) -> Any: + if isinstance(obj, list): + nulled_list = [] + for index, value in enumerate(obj): + value = separate_files(f"{path}.{index}", value) + nulled_list.append(value) + return nulled_list + + if isinstance(obj, dict): + nulled_dict = {} + for key, value in obj.items(): + value = separate_files(f"{path}.{key}", value) + nulled_dict[key] = value + return nulled_dict + + if isinstance(obj, Upload): + if obj in files_list: + file_index = files_list.index(obj) + files_map[str(file_index)].append(path) + else: + file_index = len(files_list) + files_list.append(obj) + files_map[str(file_index)] = [path] + return None + + return obj + + nulled_variables = separate_files("variables", variables) + files: Dict[str, Tuple[str, IO[bytes], str]] = { + str(i): (file_.filename, cast(IO[bytes], file_.content), file_.content_type) + for i, file_ in enumerate(files_list) + } + return nulled_variables, files, files_map + + def _execute_multipart( + self, + query: str, + operation_name: Optional[str], + variables: Dict[str, Any], + files: Dict[str, Tuple[str, IO[bytes], str]], + files_map: Dict[str, List[str]], + **kwargs: Any, + ) -> httpx.Response: + data = { + "operations": json.dumps( + { + "query": query, + "operationName": operation_name, + "variables": variables, + }, + default=to_jsonable_python, + ), + "map": json.dumps(files_map, default=to_jsonable_python), + } + + return self.http_client.post(url=self.url, data=data, files=files, **kwargs) + + def _execute_json( + self, + query: str, + operation_name: Optional[str], + variables: Dict[str, Any], + **kwargs: Any, + ) -> httpx.Response: + headers: Dict[str, str] = {"Content-Type": "application/json"} + headers.update(kwargs.get("headers", {})) + + merged_kwargs: Dict[str, Any] = kwargs.copy() + merged_kwargs["headers"] = headers + + return self.http_client.post( + url=self.url, + content=json.dumps( + { + "query": query, + "operationName": operation_name, + "variables": variables, + }, + default=to_jsonable_python, + ), + **merged_kwargs, + ) diff --git a/tests/main/clients/custom_sync_query_builder/expected_client/base_model.py b/tests/main/clients/custom_sync_query_builder/expected_client/base_model.py new file mode 100644 index 00000000..ccde3975 --- /dev/null +++ b/tests/main/clients/custom_sync_query_builder/expected_client/base_model.py @@ -0,0 +1,27 @@ +from io import IOBase + +from pydantic import BaseModel as PydanticBaseModel, ConfigDict + + +class UnsetType: + def __bool__(self) -> bool: + return False + + +UNSET = UnsetType() + + +class BaseModel(PydanticBaseModel): + model_config = ConfigDict( + populate_by_name=True, + validate_assignment=True, + arbitrary_types_allowed=True, + protected_namespaces=(), + ) + + +class Upload: + def __init__(self, filename: str, content: IOBase, content_type: str): + self.filename = filename + self.content = content + self.content_type = content_type diff --git a/tests/main/clients/custom_sync_query_builder/expected_client/base_operation.py b/tests/main/clients/custom_sync_query_builder/expected_client/base_operation.py new file mode 100644 index 00000000..9f9c7660 --- /dev/null +++ b/tests/main/clients/custom_sync_query_builder/expected_client/base_operation.py @@ -0,0 +1,156 @@ +from typing import Any, Dict, List, Optional, Set, Tuple, Union + +from graphql import ( + ArgumentNode, + FieldNode, + InlineFragmentNode, + NamedTypeNode, + NameNode, + SelectionSetNode, + VariableNode, +) + + +class GraphQLArgument: + """ + Represents a GraphQL argument and allows conversion to an AST structure. + """ + + def __init__(self, argument_name: str, argument_value: Any) -> None: + self._name = argument_name + self._value = argument_value + + def to_ast(self) -> ArgumentNode: + """Converts the argument to an ArgumentNode AST object.""" + return ArgumentNode( + name=NameNode(value=self._name), + value=VariableNode(name=NameNode(value=self._value)), + ) + + +class GraphQLField: + """ + Represents a GraphQL field with its name, arguments, subfields, alias, + and inline fragments. + + Attributes: + formatted_variables (Dict[str, Dict[str, Any]]): The formatted arguments + of the GraphQL field. + """ + + def __init__( + self, field_name: str, arguments: Optional[Dict[str, Dict[str, Any]]] = None + ) -> None: + self._field_name = field_name + self._variables = arguments or {} + self.formatted_variables: Dict[str, Dict[str, Any]] = {} + self._subfields: List[GraphQLField] = [] + self._alias: Optional[str] = None + self._inline_fragments: Dict[str, Tuple[GraphQLField, ...]] = {} + + def alias(self, alias: str) -> "GraphQLField": + """Sets an alias for the GraphQL field and returns the instance.""" + self._alias = alias + return self + + def _build_field_name(self) -> str: + """Builds the field name, including the alias if present.""" + return f"{self._alias}: {self._field_name}" if self._alias else self._field_name + + def _build_selections( + self, idx: int, used_names: Set[str] + ) -> List[Union[FieldNode, InlineFragmentNode]]: + """Builds the selection set for the current GraphQL field, + including subfields and inline fragments.""" + # Create selections from subfields + selections: List[Union[FieldNode, InlineFragmentNode]] = [ + subfield.to_ast(idx, used_names) for subfield in self._subfields + ] + + # Add inline fragments + for name, subfields in self._inline_fragments.items(): + selections.append( + InlineFragmentNode( + type_condition=NamedTypeNode(name=NameNode(value=name)), + selection_set=SelectionSetNode( + selections=[ + subfield.to_ast(idx, used_names) for subfield in subfields + ] + ), + ) + ) + + return selections + + def _format_variable_name( + self, idx: int, var_name: str, used_names: Set[str] + ) -> str: + """Generates a unique variable name by appending an index and, + if necessary, an additional counter to avoid duplicates.""" + base_name = f"{var_name}_{idx}" + unique_name = base_name + counter = 1 + + # Ensure the generated name is unique + while unique_name in used_names: + unique_name = f"{base_name}_{counter}" + counter += 1 + + # Add the unique name to the set of used names + used_names.add(unique_name) + + return unique_name + + def _collect_all_variables(self, idx: int, used_names: Set[str]) -> None: + """ + Collects and formats all variables for the current GraphQL field, + ensuring unique names. + """ + self.formatted_variables = {} + + for k, v in self._variables.items(): + unique_name = self._format_variable_name(idx, k, used_names) + self.formatted_variables[unique_name] = { + "name": k, + "type": v["type"], + "value": v["value"], + } + + def to_ast(self, idx: int, used_names: Optional[Set[str]] = None) -> FieldNode: + """Converts the current GraphQL field to an AST (Abstract Syntax Tree) node.""" + if used_names is None: + used_names = set() + + self._collect_all_variables(idx, used_names) + + return FieldNode( + name=NameNode(value=self._build_field_name()), + arguments=[ + GraphQLArgument(v["name"], k).to_ast() + for k, v in self.formatted_variables.items() + ], + selection_set=( + SelectionSetNode(selections=self._build_selections(idx, used_names)) + if self._subfields or self._inline_fragments + else None + ), + ) + + def get_formatted_variables(self) -> Dict[str, Dict[str, Any]]: + """ + Retrieves all formatted variables for the current GraphQL field, + including those from subfields and inline fragments. + """ + formatted_variables = self.formatted_variables.copy() + + # Collect variables from subfields + for subfield in self._subfields: + subfield.get_formatted_variables() + formatted_variables.update(subfield.formatted_variables) + + # Collect variables from inline fragments + for subfields in self._inline_fragments.values(): + for subfield in subfields: + subfield.get_formatted_variables() + formatted_variables.update(subfield.formatted_variables) + return formatted_variables diff --git a/tests/main/clients/custom_sync_query_builder/expected_client/client.py b/tests/main/clients/custom_sync_query_builder/expected_client/client.py new file mode 100644 index 00000000..d3493439 --- /dev/null +++ b/tests/main/clients/custom_sync_query_builder/expected_client/client.py @@ -0,0 +1,105 @@ +from typing import Any, Dict, List, Tuple + +from graphql import ( + DocumentNode, + NamedTypeNode, + NameNode, + OperationDefinitionNode, + OperationType, + SelectionNode, + SelectionSetNode, + VariableDefinitionNode, + VariableNode, + print_ast, +) + +from .base_client import BaseClient +from .base_operation import GraphQLField + + +def gql(q: str) -> str: + return q + + +class Client(BaseClient): + def execute_custom_operation( + self, *fields: GraphQLField, operation_type: OperationType, operation_name: str + ) -> Dict[str, Any]: + selections = self._build_selection_set(fields) + combined_variables = self._combine_variables(fields) + variable_definitions = self._build_variable_definitions( + combined_variables["types"] + ) + operation_ast = self._build_operation_ast( + selections, operation_type, operation_name, variable_definitions + ) + response = self.execute( + print_ast(operation_ast), + variables=combined_variables["values"], + operation_name=operation_name, + ) + return self.get_data(response) + + def _combine_variables( + self, fields: Tuple[GraphQLField, ...] + ) -> Dict[str, Dict[str, Any]]: + variables_types_combined = {} + processed_variables_combined = {} + for field in fields: + formatted_variables = field.get_formatted_variables() + variables_types_combined.update( + {k: v["type"] for k, v in formatted_variables.items()} + ) + processed_variables_combined.update( + {k: v["value"] for k, v in formatted_variables.items()} + ) + return { + "types": variables_types_combined, + "values": processed_variables_combined, + } + + def _build_variable_definitions( + self, variables_types_combined: Dict[str, str] + ) -> List[VariableDefinitionNode]: + return [ + VariableDefinitionNode( + variable=VariableNode(name=NameNode(value=var_name)), + type=NamedTypeNode(name=NameNode(value=var_value)), + ) + for var_name, var_value in variables_types_combined.items() + ] + + def _build_operation_ast( + self, + selections: List[SelectionNode], + operation_type: OperationType, + operation_name: str, + variable_definitions: List[VariableDefinitionNode], + ) -> DocumentNode: + return DocumentNode( + definitions=[ + OperationDefinitionNode( + operation=operation_type, + name=NameNode(value=operation_name), + variable_definitions=variable_definitions, + selection_set=SelectionSetNode(selections=selections), + ) + ] + ) + + def _build_selection_set( + self, fields: Tuple[GraphQLField, ...] + ) -> List[SelectionNode]: + return [field.to_ast(idx) for idx, field in enumerate(fields)] + + def query(self, *fields: GraphQLField, operation_name: str) -> Dict[str, Any]: + return self.execute_custom_operation( + *fields, operation_type=OperationType.QUERY, operation_name=operation_name + ) + + def mutation(self, *fields: GraphQLField, operation_name: str) -> Dict[str, Any]: + return self.execute_custom_operation( + *fields, + operation_type=OperationType.MUTATION, + operation_name=operation_name + ) diff --git a/tests/main/clients/custom_sync_query_builder/expected_client/custom_fields.py b/tests/main/clients/custom_sync_query_builder/expected_client/custom_fields.py new file mode 100644 index 00000000..799b8391 --- /dev/null +++ b/tests/main/clients/custom_sync_query_builder/expected_client/custom_fields.py @@ -0,0 +1,384 @@ +from typing import Any, Dict, Optional, Union + +from .base_operation import GraphQLField +from .custom_typing_fields import ( + AppGraphQLField, + CollectionTranslatableContentGraphQLField, + MetadataErrorGraphQLField, + MetadataItemGraphQLField, + ObjectWithMetadataGraphQLField, + PageInfoGraphQLField, + ProductCountableConnectionGraphQLField, + ProductCountableEdgeGraphQLField, + ProductGraphQLField, + ProductTranslatableContentGraphQLField, + ProductTypeCountableConnectionGraphQLField, + TranslatableItemConnectionGraphQLField, + TranslatableItemEdgeGraphQLField, + TranslatableItemUnion, + UpdateMetadataGraphQLField, +) + + +class AppFields(GraphQLField): + id: "AppGraphQLField" = AppGraphQLField("id") + + def fields(self, *subfields: AppGraphQLField) -> "AppFields": + """Subfields should come from the AppFields class""" + self._subfields.extend(subfields) + return self + + def alias(self, alias: str) -> "AppFields": + self._alias = alias + return self + + +class CollectionTranslatableContentFields(GraphQLField): + id: "CollectionTranslatableContentGraphQLField" = ( + CollectionTranslatableContentGraphQLField("id") + ) + collection_id: "CollectionTranslatableContentGraphQLField" = ( + CollectionTranslatableContentGraphQLField("collectionId") + ) + seo_title: "CollectionTranslatableContentGraphQLField" = ( + CollectionTranslatableContentGraphQLField("seoTitle") + ) + seo_description: "CollectionTranslatableContentGraphQLField" = ( + CollectionTranslatableContentGraphQLField("seoDescription") + ) + name: "CollectionTranslatableContentGraphQLField" = ( + CollectionTranslatableContentGraphQLField("name") + ) + description: "CollectionTranslatableContentGraphQLField" = ( + CollectionTranslatableContentGraphQLField("description") + ) + + def fields( + self, *subfields: CollectionTranslatableContentGraphQLField + ) -> "CollectionTranslatableContentFields": + """Subfields should come from the CollectionTranslatableContentFields class""" + self._subfields.extend(subfields) + return self + + def alias(self, alias: str) -> "CollectionTranslatableContentFields": + self._alias = alias + return self + + +class MetadataErrorFields(GraphQLField): + field: "MetadataErrorGraphQLField" = MetadataErrorGraphQLField("field") + message: "MetadataErrorGraphQLField" = MetadataErrorGraphQLField("message") + code: "MetadataErrorGraphQLField" = MetadataErrorGraphQLField("code") + + def fields(self, *subfields: MetadataErrorGraphQLField) -> "MetadataErrorFields": + """Subfields should come from the MetadataErrorFields class""" + self._subfields.extend(subfields) + return self + + def alias(self, alias: str) -> "MetadataErrorFields": + self._alias = alias + return self + + +class MetadataItemFields(GraphQLField): + key: "MetadataItemGraphQLField" = MetadataItemGraphQLField("key") + value: "MetadataItemGraphQLField" = MetadataItemGraphQLField("value") + + def fields(self, *subfields: MetadataItemGraphQLField) -> "MetadataItemFields": + """Subfields should come from the MetadataItemFields class""" + self._subfields.extend(subfields) + return self + + def alias(self, alias: str) -> "MetadataItemFields": + self._alias = alias + return self + + +class ObjectWithMetadataInterface(GraphQLField): + @classmethod + def private_metadata(cls) -> "MetadataItemFields": + return MetadataItemFields("private_metadata") + + @classmethod + def private_metafield(cls, key: str) -> "ObjectWithMetadataGraphQLField": + arguments: Dict[str, Dict[str, Any]] = { + "key": {"type": "String!", "value": key} + } + cleared_arguments = { + key: value for key, value in arguments.items() if value["value"] is not None + } + return ObjectWithMetadataGraphQLField( + "private_metafield", arguments=cleared_arguments + ) + + @classmethod + def metadata(cls) -> "MetadataItemFields": + return MetadataItemFields("metadata") + + @classmethod + def metafield(cls, key: str) -> "ObjectWithMetadataGraphQLField": + arguments: Dict[str, Dict[str, Any]] = { + "key": {"type": "String!", "value": key} + } + cleared_arguments = { + key: value for key, value in arguments.items() if value["value"] is not None + } + return ObjectWithMetadataGraphQLField("metafield", arguments=cleared_arguments) + + def fields( + self, *subfields: Union[ObjectWithMetadataGraphQLField, "MetadataItemFields"] + ) -> "ObjectWithMetadataInterface": + """Subfields should come from the ObjectWithMetadataInterface class""" + self._subfields.extend(subfields) + return self + + def alias(self, alias: str) -> "ObjectWithMetadataInterface": + self._alias = alias + return self + + def on( + self, type_name: str, *subfields: GraphQLField + ) -> "ObjectWithMetadataInterface": + self._inline_fragments[type_name] = subfields + return self + + +class PageInfoFields(GraphQLField): + has_next_page: "PageInfoGraphQLField" = PageInfoGraphQLField("hasNextPage") + has_previous_page: "PageInfoGraphQLField" = PageInfoGraphQLField("hasPreviousPage") + start_cursor: "PageInfoGraphQLField" = PageInfoGraphQLField("startCursor") + end_cursor: "PageInfoGraphQLField" = PageInfoGraphQLField("endCursor") + + def fields(self, *subfields: PageInfoGraphQLField) -> "PageInfoFields": + """Subfields should come from the PageInfoFields class""" + self._subfields.extend(subfields) + return self + + def alias(self, alias: str) -> "PageInfoFields": + self._alias = alias + return self + + +class ProductFields(GraphQLField): + id: "ProductGraphQLField" = ProductGraphQLField("id") + slug: "ProductGraphQLField" = ProductGraphQLField("slug") + name: "ProductGraphQLField" = ProductGraphQLField("name") + + @classmethod + def private_metadata(cls) -> "MetadataItemFields": + return MetadataItemFields("private_metadata") + + @classmethod + def private_metafield(cls, key: str) -> "ProductGraphQLField": + arguments: Dict[str, Dict[str, Any]] = { + "key": {"type": "String!", "value": key} + } + cleared_arguments = { + key: value for key, value in arguments.items() if value["value"] is not None + } + return ProductGraphQLField("private_metafield", arguments=cleared_arguments) + + @classmethod + def metadata(cls) -> "MetadataItemFields": + return MetadataItemFields("metadata") + + @classmethod + def metafield(cls, key: str) -> "ProductGraphQLField": + arguments: Dict[str, Dict[str, Any]] = { + "key": {"type": "String!", "value": key} + } + cleared_arguments = { + key: value for key, value in arguments.items() if value["value"] is not None + } + return ProductGraphQLField("metafield", arguments=cleared_arguments) + + def fields( + self, *subfields: Union[ProductGraphQLField, "MetadataItemFields"] + ) -> "ProductFields": + """Subfields should come from the ProductFields class""" + self._subfields.extend(subfields) + return self + + def alias(self, alias: str) -> "ProductFields": + self._alias = alias + return self + + +class ProductCountableConnectionFields(GraphQLField): + @classmethod + def edges(cls) -> "ProductCountableEdgeFields": + return ProductCountableEdgeFields("edges") + + @classmethod + def page_info(cls) -> "PageInfoFields": + return PageInfoFields("page_info") + + total_count: "ProductCountableConnectionGraphQLField" = ( + ProductCountableConnectionGraphQLField("totalCount") + ) + + def fields( + self, + *subfields: Union[ + ProductCountableConnectionGraphQLField, + "PageInfoFields", + "ProductCountableEdgeFields", + ] + ) -> "ProductCountableConnectionFields": + """Subfields should come from the ProductCountableConnectionFields class""" + self._subfields.extend(subfields) + return self + + def alias(self, alias: str) -> "ProductCountableConnectionFields": + self._alias = alias + return self + + +class ProductCountableEdgeFields(GraphQLField): + @classmethod + def node(cls) -> "ProductFields": + return ProductFields("node") + + cursor: "ProductCountableEdgeGraphQLField" = ProductCountableEdgeGraphQLField( + "cursor" + ) + + def fields( + self, *subfields: Union[ProductCountableEdgeGraphQLField, "ProductFields"] + ) -> "ProductCountableEdgeFields": + """Subfields should come from the ProductCountableEdgeFields class""" + self._subfields.extend(subfields) + return self + + def alias(self, alias: str) -> "ProductCountableEdgeFields": + self._alias = alias + return self + + +class ProductTranslatableContentFields(GraphQLField): + id: "ProductTranslatableContentGraphQLField" = ( + ProductTranslatableContentGraphQLField("id") + ) + product_id: "ProductTranslatableContentGraphQLField" = ( + ProductTranslatableContentGraphQLField("productId") + ) + seo_title: "ProductTranslatableContentGraphQLField" = ( + ProductTranslatableContentGraphQLField("seoTitle") + ) + seo_description: "ProductTranslatableContentGraphQLField" = ( + ProductTranslatableContentGraphQLField("seoDescription") + ) + name: "ProductTranslatableContentGraphQLField" = ( + ProductTranslatableContentGraphQLField("name") + ) + description: "ProductTranslatableContentGraphQLField" = ( + ProductTranslatableContentGraphQLField("description") + ) + + def fields( + self, *subfields: ProductTranslatableContentGraphQLField + ) -> "ProductTranslatableContentFields": + """Subfields should come from the ProductTranslatableContentFields class""" + self._subfields.extend(subfields) + return self + + def alias(self, alias: str) -> "ProductTranslatableContentFields": + self._alias = alias + return self + + +class ProductTypeCountableConnectionFields(GraphQLField): + @classmethod + def page_info(cls) -> "PageInfoFields": + return PageInfoFields("page_info") + + def fields( + self, + *subfields: Union[ProductTypeCountableConnectionGraphQLField, "PageInfoFields"] + ) -> "ProductTypeCountableConnectionFields": + """Subfields should come from the ProductTypeCountableConnectionFields class""" + self._subfields.extend(subfields) + return self + + def alias(self, alias: str) -> "ProductTypeCountableConnectionFields": + self._alias = alias + return self + + +class TranslatableItemConnectionFields(GraphQLField): + @classmethod + def page_info(cls) -> "PageInfoFields": + return PageInfoFields("page_info") + + @classmethod + def edges(cls) -> "TranslatableItemEdgeFields": + return TranslatableItemEdgeFields("edges") + + total_count: "TranslatableItemConnectionGraphQLField" = ( + TranslatableItemConnectionGraphQLField("totalCount") + ) + + def fields( + self, + *subfields: Union[ + TranslatableItemConnectionGraphQLField, + "PageInfoFields", + "TranslatableItemEdgeFields", + ] + ) -> "TranslatableItemConnectionFields": + """Subfields should come from the TranslatableItemConnectionFields class""" + self._subfields.extend(subfields) + return self + + def alias(self, alias: str) -> "TranslatableItemConnectionFields": + self._alias = alias + return self + + +class TranslatableItemEdgeFields(GraphQLField): + node: "TranslatableItemUnion" = TranslatableItemUnion("node") + cursor: "TranslatableItemEdgeGraphQLField" = TranslatableItemEdgeGraphQLField( + "cursor" + ) + + def fields( + self, + *subfields: Union[TranslatableItemEdgeGraphQLField, "TranslatableItemUnion"] + ) -> "TranslatableItemEdgeFields": + """Subfields should come from the TranslatableItemEdgeFields class""" + self._subfields.extend(subfields) + return self + + def alias(self, alias: str) -> "TranslatableItemEdgeFields": + self._alias = alias + return self + + +class UpdateMetadataFields(GraphQLField): + @classmethod + def metadata_errors(cls) -> "MetadataErrorFields": + return MetadataErrorFields("metadata_errors") + + @classmethod + def errors(cls) -> "MetadataErrorFields": + return MetadataErrorFields("errors") + + @classmethod + def item(cls) -> "ObjectWithMetadataInterface": + return ObjectWithMetadataInterface("item") + + def fields( + self, + *subfields: Union[ + UpdateMetadataGraphQLField, + "MetadataErrorFields", + "ObjectWithMetadataInterface", + ] + ) -> "UpdateMetadataFields": + """Subfields should come from the UpdateMetadataFields class""" + self._subfields.extend(subfields) + return self + + def alias(self, alias: str) -> "UpdateMetadataFields": + self._alias = alias + return self diff --git a/tests/main/clients/custom_sync_query_builder/expected_client/custom_mutations.py b/tests/main/clients/custom_sync_query_builder/expected_client/custom_mutations.py new file mode 100644 index 00000000..f4836ddb --- /dev/null +++ b/tests/main/clients/custom_sync_query_builder/expected_client/custom_mutations.py @@ -0,0 +1,15 @@ +from typing import Any, Dict, Optional + +from .custom_fields import UpdateMetadataFields + + +class Mutation: + @classmethod + def update_metadata(cls, id: str) -> UpdateMetadataFields: + arguments: Dict[str, Dict[str, Any]] = {"id": {"type": "ID!", "value": id}} + cleared_arguments = { + key: value for key, value in arguments.items() if value["value"] is not None + } + return UpdateMetadataFields( + field_name="updateMetadata", arguments=cleared_arguments + ) diff --git a/tests/main/clients/custom_sync_query_builder/expected_client/custom_queries.py b/tests/main/clients/custom_sync_query_builder/expected_client/custom_queries.py new file mode 100644 index 00000000..d14cf4db --- /dev/null +++ b/tests/main/clients/custom_sync_query_builder/expected_client/custom_queries.py @@ -0,0 +1,55 @@ +from typing import Any, Dict, Optional + +from .custom_fields import ( + AppFields, + ProductCountableConnectionFields, + ProductTypeCountableConnectionFields, + TranslatableItemConnectionFields, +) + + +class Query: + @classmethod + def products( + cls, *, channel: Optional[str] = None, first: Optional[int] = None + ) -> ProductCountableConnectionFields: + arguments: Dict[str, Dict[str, Any]] = { + "channel": {"type": "String", "value": channel}, + "first": {"type": "Int", "value": first}, + } + cleared_arguments = { + key: value for key, value in arguments.items() if value["value"] is not None + } + return ProductCountableConnectionFields( + field_name="products", arguments=cleared_arguments + ) + + @classmethod + def app(cls) -> AppFields: + return AppFields(field_name="app") + + @classmethod + def product_types(cls) -> ProductTypeCountableConnectionFields: + return ProductTypeCountableConnectionFields(field_name="productTypes") + + @classmethod + def translations( + cls, + *, + before: Optional[str] = None, + after: Optional[str] = None, + first: Optional[int] = None, + last: Optional[int] = None + ) -> TranslatableItemConnectionFields: + arguments: Dict[str, Dict[str, Any]] = { + "before": {"type": "String", "value": before}, + "after": {"type": "String", "value": after}, + "first": {"type": "Int", "value": first}, + "last": {"type": "Int", "value": last}, + } + cleared_arguments = { + key: value for key, value in arguments.items() if value["value"] is not None + } + return TranslatableItemConnectionFields( + field_name="translations", arguments=cleared_arguments + ) diff --git a/tests/main/clients/custom_sync_query_builder/expected_client/custom_typing_fields.py b/tests/main/clients/custom_sync_query_builder/expected_client/custom_typing_fields.py new file mode 100644 index 00000000..91be62aa --- /dev/null +++ b/tests/main/clients/custom_sync_query_builder/expected_client/custom_typing_fields.py @@ -0,0 +1,95 @@ +from .base_operation import GraphQLField + + +class ProductGraphQLField(GraphQLField): + def alias(self, alias: str) -> "ProductGraphQLField": + self._alias = alias + return self + + +class ProductCountableEdgeGraphQLField(GraphQLField): + def alias(self, alias: str) -> "ProductCountableEdgeGraphQLField": + self._alias = alias + return self + + +class ProductCountableConnectionGraphQLField(GraphQLField): + def alias(self, alias: str) -> "ProductCountableConnectionGraphQLField": + self._alias = alias + return self + + +class AppGraphQLField(GraphQLField): + def alias(self, alias: str) -> "AppGraphQLField": + self._alias = alias + return self + + +class ProductTypeCountableConnectionGraphQLField(GraphQLField): + def alias(self, alias: str) -> "ProductTypeCountableConnectionGraphQLField": + self._alias = alias + return self + + +class PageInfoGraphQLField(GraphQLField): + def alias(self, alias: str) -> "PageInfoGraphQLField": + self._alias = alias + return self + + +class ObjectWithMetadataGraphQLField(GraphQLField): + def alias(self, alias: str) -> "ObjectWithMetadataGraphQLField": + self._alias = alias + return self + + +class MetadataItemGraphQLField(GraphQLField): + def alias(self, alias: str) -> "MetadataItemGraphQLField": + self._alias = alias + return self + + +class UpdateMetadataGraphQLField(GraphQLField): + def alias(self, alias: str) -> "UpdateMetadataGraphQLField": + self._alias = alias + return self + + +class MetadataErrorGraphQLField(GraphQLField): + def alias(self, alias: str) -> "MetadataErrorGraphQLField": + self._alias = alias + return self + + +class TranslatableItemConnectionGraphQLField(GraphQLField): + def alias(self, alias: str) -> "TranslatableItemConnectionGraphQLField": + self._alias = alias + return self + + +class TranslatableItemEdgeGraphQLField(GraphQLField): + def alias(self, alias: str) -> "TranslatableItemEdgeGraphQLField": + self._alias = alias + return self + + +class TranslatableItemUnion(GraphQLField): + def on(self, type_name: str, *subfields: GraphQLField) -> "TranslatableItemUnion": + self._inline_fragments[type_name] = subfields + return self + + def alias(self, alias: str) -> "TranslatableItemUnion": + self._alias = alias + return self + + +class ProductTranslatableContentGraphQLField(GraphQLField): + def alias(self, alias: str) -> "ProductTranslatableContentGraphQLField": + self._alias = alias + return self + + +class CollectionTranslatableContentGraphQLField(GraphQLField): + def alias(self, alias: str) -> "CollectionTranslatableContentGraphQLField": + self._alias = alias + return self diff --git a/tests/main/clients/custom_sync_query_builder/expected_client/enums.py b/tests/main/clients/custom_sync_query_builder/expected_client/enums.py new file mode 100644 index 00000000..b6a853d5 --- /dev/null +++ b/tests/main/clients/custom_sync_query_builder/expected_client/enums.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class MetadataErrorCode(str, Enum): + GRAPHQL_ERROR = "GRAPHQL_ERROR" + INVALID = "INVALID" + NOT_FOUND = "NOT_FOUND" + REQUIRED = "REQUIRED" + NOT_UPDATED = "NOT_UPDATED" diff --git a/tests/main/clients/custom_sync_query_builder/expected_client/exceptions.py b/tests/main/clients/custom_sync_query_builder/expected_client/exceptions.py new file mode 100644 index 00000000..b34acfe1 --- /dev/null +++ b/tests/main/clients/custom_sync_query_builder/expected_client/exceptions.py @@ -0,0 +1,83 @@ +from typing import Any, Dict, List, Optional, Union + +import httpx + + +class GraphQLClientError(Exception): + """Base exception.""" + + +class GraphQLClientHttpError(GraphQLClientError): + def __init__(self, status_code: int, response: httpx.Response) -> None: + self.status_code = status_code + self.response = response + + def __str__(self) -> str: + return f"HTTP status code: {self.status_code}" + + +class GraphQLClientInvalidResponseError(GraphQLClientError): + def __init__(self, response: httpx.Response) -> None: + self.response = response + + def __str__(self) -> str: + return "Invalid response format." + + +class GraphQLClientGraphQLError(GraphQLClientError): + def __init__( + self, + message: str, + locations: Optional[List[Dict[str, int]]] = None, + path: Optional[List[str]] = None, + extensions: Optional[Dict[str, object]] = None, + orginal: Optional[Dict[str, object]] = None, + ): + self.message = message + self.locations = locations + self.path = path + self.extensions = extensions + self.orginal = orginal + + def __str__(self) -> str: + return self.message + + @classmethod + def from_dict(cls, error: Dict[str, Any]) -> "GraphQLClientGraphQLError": + return cls( + message=error["message"], + locations=error.get("locations"), + path=error.get("path"), + extensions=error.get("extensions"), + orginal=error, + ) + + +class GraphQLClientGraphQLMultiError(GraphQLClientError): + def __init__( + self, + errors: List[GraphQLClientGraphQLError], + data: Optional[Dict[str, Any]] = None, + ): + self.errors = errors + self.data = data + + def __str__(self) -> str: + return "; ".join(str(e) for e in self.errors) + + @classmethod + def from_errors_dicts( + cls, errors_dicts: List[Dict[str, Any]], data: Optional[Dict[str, Any]] = None + ) -> "GraphQLClientGraphQLMultiError": + return cls( + errors=[GraphQLClientGraphQLError.from_dict(e) for e in errors_dicts], + data=data, + ) + + +class GraphQLClientInvalidMessageFormat(GraphQLClientError): + def __init__(self, message: Union[str, bytes]) -> None: + self.message = message + + def __str__(self) -> str: + return "Invalid message format." diff --git a/tests/main/clients/custom_sync_query_builder/expected_client/input_types.py b/tests/main/clients/custom_sync_query_builder/expected_client/input_types.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/main/clients/custom_sync_query_builder/pyproject.toml b/tests/main/clients/custom_sync_query_builder/pyproject.toml new file mode 100644 index 00000000..ef0f49f9 --- /dev/null +++ b/tests/main/clients/custom_sync_query_builder/pyproject.toml @@ -0,0 +1,6 @@ +[tool.ariadne-codegen] +schema_path = "schema.graphql" +include_comments = "none" +target_package_name = "example_client" +enable_custom_operations = true +async_client = false diff --git a/tests/main/clients/custom_sync_query_builder/schema.graphql b/tests/main/clients/custom_sync_query_builder/schema.graphql new file mode 100644 index 00000000..4e873d7c --- /dev/null +++ b/tests/main/clients/custom_sync_query_builder/schema.graphql @@ -0,0 +1,249 @@ +schema { + query: Query + mutation: Mutation +} + +type Query { + products(channel: String, first: Int): ProductCountableConnection + app: App + productTypes: ProductTypeCountableConnection + translations( + """ + Return the elements in the list that come before the specified cursor. + """ + before: String + + """ + Return the elements in the list that come after the specified cursor. + """ + after: String + + """ + Retrieve the first n elements from the list. Note that the system only allows fetching a maximum of 100 objects in a single query. + """ + first: Int + + """ + Retrieve the last n elements from the list. Note that the system only allows fetching a maximum of 100 objects in a single query. + """ + last: Int + ): TranslatableItemConnection +} + +type Mutation { + updateMetadata( + """ + ID or token (for Order and Checkout) of an object to update. + """ + id: ID! + ): UpdateMetadata +} + +type Product implements ObjectWithMetadata { + id: ID! + slug: String! + name: String! +} + +type ProductCountableEdge { + node: Product! + cursor: String! +} + +type ProductCountableConnection { + edges: [ProductCountableEdge!]! + pageInfo: PageInfo! + totalCount: Int +} + +type App { + id: ID! +} + +type ProductTypeCountableConnection { + pageInfo: PageInfo! +} + +type PageInfo { + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + endCursor: String +} + +interface ObjectWithMetadata { + """ + List of private metadata items. Requires staff permissions to access. + """ + privateMetadata: [MetadataItem!]! + + """ + A single key from private metadata. Requires staff permissions to access. + + Tip: Use GraphQL aliases to fetch multiple keys. + """ + privateMetafield(key: String!): String + + """ + List of public metadata items. Can be accessed without permissions. + """ + metadata: [MetadataItem!]! + + """ + A single key from public metadata. + + Tip: Use GraphQL aliases to fetch multiple keys. + """ + metafield(key: String!): String +} + +type MetadataItem { + """ + Key of a metadata item. + """ + key: String! + + """ + Value of a metadata item. + """ + value: String! +} + +type UpdateMetadata { + metadataErrors: [MetadataError!]! + @deprecated( + reason: "This field will be removed in Saleor 4.0. Use `errors` field instead." + ) + errors: [MetadataError!]! + item: ObjectWithMetadata +} +type MetadataError { + """ + Name of a field that caused the error. A value of `null` indicates that the error isn't associated with a particular field. + """ + field: String + + """ + The error message. + """ + message: String + + """ + The error code. + """ + code: MetadataErrorCode! +} + +""" +An enumeration. +""" +enum MetadataErrorCode { + GRAPHQL_ERROR + INVALID + NOT_FOUND + REQUIRED + NOT_UPDATED +} + +type TranslatableItemConnection { + """ + Pagination data for this connection. + """ + pageInfo: PageInfo! + edges: [TranslatableItemEdge!]! + + """ + A total count of items in the collection. + """ + totalCount: Int +} + +type TranslatableItemEdge { + """ + The item at the end of the edge. + """ + node: TranslatableItem! + + """ + A cursor for use in pagination. + """ + cursor: String! +} + +union TranslatableItem = + ProductTranslatableContent + | CollectionTranslatableContent + +type ProductTranslatableContent @doc(category: "Products") { + """ + The ID of the product translatable content. + """ + id: ID! + + """ + The ID of the product to translate. + + Added in Saleor 3.14. + """ + productId: ID! + + """ + SEO title to translate. + """ + seoTitle: String + + """ + SEO description to translate. + """ + seoDescription: String + + """ + Product's name to translate. + """ + name: String! + + """ + Product's description to translate. + + Rich text format. For reference see https://editorjs.io/ + """ + description: JSONString +} + +type CollectionTranslatableContent @doc(category: "Products") { + """ + The ID of the collection translatable content. + """ + id: ID! + + """ + The ID of the collection to translate. + + Added in Saleor 3.14. + """ + collectionId: ID! + + """ + SEO title to translate. + """ + seoTitle: String + + """ + SEO description to translate. + """ + seoDescription: String + + """ + Collection's name to translate. + """ + name: String! + + """ + Collection's description to translate. + + Rich text format. For reference see https://editorjs.io/ + """ + description: JSONString +} + +scalar JSONString diff --git a/tests/main/test_main.py b/tests/main/test_main.py index 2182a1de..3cf9f1d3 100644 --- a/tests/main/test_main.py +++ b/tests/main/test_main.py @@ -205,6 +205,14 @@ def test_main_shows_version(): "example_client", CLIENTS_PATH / "custom_query_builder" / "expected_client", ), + ( + ( + CLIENTS_PATH / "custom_sync_query_builder" / "pyproject.toml", + (CLIENTS_PATH / "custom_sync_query_builder" / "schema.graphql",), + ), + "example_client", + CLIENTS_PATH / "custom_sync_query_builder" / "expected_client", + ), ], indirect=["project_dir"], ) From e46e0a024309b7885509a2bde9e59ef129e95d0a Mon Sep 17 00:00:00 2001 From: Damian Czajkowski Date: Fri, 26 Jul 2024 12:43:04 +0200 Subject: [PATCH 2/2] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index da53c93d..86483f1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.14.1 (UNRELEASED) - Changed code typing to satisfy MyPy 1.11.0 version +- Added support for `async_client=false` to work with `enable_custom_operations=true` ## 0.14.0 (2024-07-17)