diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000000..2e94a52763 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,3 @@ +Release type: minor + +Allow strawberry's code generator to build clients from graphql schema files. diff --git a/strawberry/cli/commands/codegen.py b/strawberry/cli/commands/codegen.py index 6e398a54f0..f8ecc4f834 100644 --- a/strawberry/cli/commands/codegen.py +++ b/strawberry/cli/commands/codegen.py @@ -3,15 +3,17 @@ import functools import importlib import inspect -from pathlib import Path # noqa: TCH003 +from pathlib import Path from typing import List, Optional, Type import rich import typer +from graphql.utilities import build_schema from strawberry.cli.app import app from strawberry.cli.utils import load_schema from strawberry.codegen import ConsolePlugin, QueryCodegen, QueryCodegenPlugin +from strawberry.codegen.schema_adapter import GraphQLSchemaWrapper, SchemaLike def _is_codegen_plugin(obj: object) -> bool: @@ -123,7 +125,12 @@ def codegen( if not query: return - schema_symbol = load_schema(schema, app_dir) + schema_symbol: SchemaLike + if schema.endswith(".graphql"): + with Path(schema).open() as input_schema: + schema_symbol = GraphQLSchemaWrapper(build_schema(input_schema.read())) + else: + schema_symbol = load_schema(schema, app_dir) console_plugin_type = _load_plugin(cli_plugin) if cli_plugin else ConsolePlugin console_plugin = console_plugin_type(output_dir) diff --git a/strawberry/codegen/query_codegen.py b/strawberry/codegen/query_codegen.py index 2761ad0a54..71d615045c 100644 --- a/strawberry/codegen/query_codegen.py +++ b/strawberry/codegen/query_codegen.py @@ -101,8 +101,7 @@ VariableDefinitionNode, ) - from strawberry.schema import Schema - + from .schema_adapter import SchemaLike from .types import GraphQLArgumentValue, GraphQLSelection, GraphQLType @@ -302,7 +301,7 @@ def on_end(self, result: CodegenResult) -> None: class QueryCodegen: def __init__( self, - schema: Schema, + schema: SchemaLike, plugins: List[QueryCodegenPlugin], console_plugin: Optional[ConsolePlugin] = None, ): @@ -545,7 +544,7 @@ def _get_field_type( not isinstance(field_type, StrawberryType) and field_type in self.schema.schema_converter.scalar_registry ): - field_type = self.schema.schema_converter.scalar_registry[field_type] # type: ignore + field_type = self.schema.schema_converter.scalar_registry[field_type] if isinstance(field_type, ScalarWrapper): python_type = field_type.wrap @@ -637,9 +636,10 @@ def _field_from_selection( assert field, f"{parent_type.name},{selection.name.value}" field_type = self._get_field_type(field.type) - return GraphQLField( - field.name, selection.alias.value if selection.alias else None, field_type + field.name, + selection.alias.value if selection.alias else None, + field_type, ) def _unwrap_type( @@ -660,8 +660,8 @@ def _unwrap_type( elif isinstance(type_, StrawberryList): type_, wrapper = self._unwrap_type(type_.of_type) wrapper = ( - GraphQLList if wrapper is None else lambda t: GraphQLList(wrapper(t)) # type: ignore[misc] - ) + GraphQLList if wrapper is None else lambda t: GraphQLList(wrapper(t)) + ) # type: ignore[misc] elif isinstance(type_, LazyType): return self._unwrap_type(type_.resolve_type()) @@ -686,12 +686,9 @@ def _field_from_selection_set( # but insertion order is maintained in python3.6+ (for CPython) and # guaranteed for all python implementations in python3.7+, so that # should be pretty safe. - if parent_type.type_var_map: + if getattr(parent_type, "type_var_map", None): parent_type_name = ( - "".join( - c.__name__ # type: ignore[union-attr] - for c in parent_type.type_var_map.values() - ) + "".join(c.__name__ for c in parent_type.type_var_map.values()) # type: ignore[union-attr] + parent_type.name ) diff --git a/strawberry/codegen/schema_adapter.py b/strawberry/codegen/schema_adapter.py new file mode 100644 index 0000000000..a882cefede --- /dev/null +++ b/strawberry/codegen/schema_adapter.py @@ -0,0 +1,251 @@ +from __future__ import annotations + +import functools +from enum import Enum +from typing import Any, Dict, Hashable, Optional, Tuple +from typing_extensions import Protocol + +from graphql.type import ( + GraphQLEnumType, + GraphQLField, + GraphQLInputObjectType, + GraphQLInterfaceType, + GraphQLList, + GraphQLNonNull, + GraphQLObjectType, + GraphQLOutputType, + GraphQLScalarType, + GraphQLSchema, + GraphQLType, + GraphQLUnionType, + GraphQLWrappingType, +) + +from strawberry.custom_scalar import ScalarDefinition +from strawberry.enum import EnumDefinition, EnumValue +from strawberry.field import StrawberryField +from strawberry.type import StrawberryList, StrawberryType +from strawberry.types.types import StrawberryObjectDefinition +from strawberry.union import StrawberryUnion + + +class _ScalarRegistry: + """A simple type registry for the GraphQLScalars that we encounter.""" + + def __init__(self) -> None: + self._cache: Dict[Any, Tuple[bool, Optional[ScalarDefinition]]] = {} + + def _check_populate_cache( + self, obj: Hashable + ) -> Tuple[bool, Optional[ScalarDefinition]]: + if obj in self._cache: + return self._cache[obj] + + is_scalar = False + if isinstance(obj, GraphQLNonNull) and isinstance( + obj.of_type, GraphQLScalarType + ): + is_scalar = True + elif isinstance(obj, GraphQLScalarType): + is_scalar = True + scalar_def = ScalarDefinition( + name=obj.name, + description=obj.description, + specified_by_url=obj.specified_by_url, + serialize=obj.serialize, + parse_value=obj.parse_value, + parse_literal=obj.parse_literal, + ) + + else: + scalar_def = None + if not is_scalar: + self._cache[obj] = (False, None) + self._cache[obj] = (is_scalar, scalar_def) + return self._cache[obj] + + def __contains__(self, obj: Hashable) -> bool: + return self._check_populate_cache(obj)[0] + + def __getitem__(self, obj: Hashable) -> ScalarDefinition: + _, result = self._check_populate_cache(obj) + if result is None: + raise KeyError(obj) + return result + + +class DeferredTypeStrawberryField(StrawberryField): + """A basic strawberry field subclass for deferred resolution of the type property.""" + + def __init__( + self, + graphql_field_type: GraphQLOutputType, + schema_wrapper: GraphQLSchemaWrapper, + **kwargs: Any, + ): + self.graphql_field_type = graphql_field_type + self.schema_wrapper = schema_wrapper + super().__init__(**kwargs) + + @property + def type(self) -> Any: + inner_type = self.graphql_field_type + while isinstance(inner_type, GraphQLWrappingType): + inner_type = inner_type.of_type + + name = getattr(inner_type, "name", None) + if name is not None: + field_type = self.schema_wrapper.get_type_by_name(name) + else: + raise ValueError(f"Unable to find type for {self.graphql_field_type}") + return field_type + + @type.setter + def type(self, val: Any) -> None: + ... + + +class GraphQLSchemaWrapper: + def __init__(self, schema: GraphQLSchema) -> None: + self.schema = schema + self.scalar_registry = _ScalarRegistry() + self._types_by_name: dict[str, Optional[StrawberryType]] = {} + + def get_field_for_type( + field_name: str, type_name: str + ) -> Optional[StrawberryField]: + return self._get_field_for_type(field_name, type_name) + + self.get_field_for_type = functools.lru_cache(maxsize=None)(get_field_for_type) + + def get_type_by_name(self, name: str) -> Optional[StrawberryType]: + if name not in self._types_by_name: + self._types_by_name[name] = self._get_type_by_name(name) + + return self._types_by_name[name] + + def _get_type_by_name(self, name: str) -> Optional[StrawberryType]: + schema_type = self.schema.get_type(name) + if schema_type is None: + return None + return self._strawberry_type_from_graphql_type(schema_type) + + def _strawberry_type_from_graphql_type( + self, graphql_type: GraphQLType + ) -> StrawberryType: + if isinstance(graphql_type, GraphQLNonNull): + graphql_type = graphql_type.of_type + if isinstance(graphql_type, GraphQLEnumType): + wrapped_cls = Enum("name", list(graphql_type.values)) # type: ignore[misc] + return EnumDefinition( + wrapped_cls=wrapped_cls, + name=graphql_type.name, + values=[ + EnumValue(name=name, value=i) + for i, name in enumerate(graphql_type.values) + ], + description=None, + ) + if isinstance( + graphql_type, + (GraphQLObjectType, GraphQLInputObjectType, GraphQLInterfaceType), + ): + obj_def = StrawberryObjectDefinition( + name=graphql_type.name, + is_input=False, + is_interface=False, + interfaces=[], + description=graphql_type.description, + origin=type(graphql_type.name, (), {}), + extend=False, + directives=[], + is_type_of=None, + resolve_type=None, + fields=[], + ) + for graphql_field in graphql_type.fields.values(): + obj_def.fields.append( + self._strawberry_field_from_graphql_field(graphql_field) + ) + # This is just monkey-patching the strawberry-definition with itself. + obj_def.__strawberry_definition__ = obj_def # type:ignore[attr-defined] + return obj_def + if isinstance(graphql_type, GraphQLScalarType): + return self.scalar_registry[graphql_type] + if isinstance(graphql_type, GraphQLList): + return StrawberryList( + of_type=self._strawberry_type_from_graphql_type(graphql_type.of_type) + ) + if isinstance(graphql_type, GraphQLUnionType): + types = [self.get_type_by_name(type_.name) for type_ in graphql_type.types] + return StrawberryUnion( + name=graphql_type.name, type_annotations=tuple(types) + ) + raise ValueError(graphql_type) + + def _strawberry_field_from_graphql_field( + self, graphql_field: GraphQLField + ) -> StrawberryField: + ast_node = graphql_field.ast_node + if ast_node is None: + raise ValueError("GraphQLField must have an AST node to get it's name.") + name = ast_node.name.value + return DeferredTypeStrawberryField( + graphql_field_type=graphql_field.type, + schema_wrapper=self, + python_name=name, + graphql_name=name, + ) + + @property + def schema_converter(self) -> GraphQLSchemaWrapper: + return self + + def _get_field_for_type( + self, field_name: str, type_name: str + ) -> Optional[StrawberryField]: + type_ = self.get_type_by_name(type_name) + if type_ is None: + return None + if not isinstance(type_, StrawberryObjectDefinition): + raise TypeError(f"{type_name!r} does not correspond to an object type!") + return self.get_field(type_, field_name) + + def get_field( + self, parent_type: StrawberryObjectDefinition, field_name: str + ) -> Optional[StrawberryField]: + """Get field of a given type with the given name.""" + if field_name == "__typename": + field = StrawberryField(python_name=field_name, graphql_name=field_name) + field.type = self.get_type_by_name("String") + return field + + return next(fld for fld in parent_type.fields if fld.name == field_name) + + +class Registry(Protocol): + def __contains__(self, key: Hashable) -> bool: + ... + + def __getitem__(self, key: Hashable) -> Any: + ... + + +class SchemaConverterLike(Protocol): + @property + def scalar_registry(self) -> Registry: + ... + + +class SchemaLike(Protocol): + @property + def schema_converter(self) -> SchemaConverterLike: + ... + + def get_type_by_name(self, name: str) -> Optional[StrawberryType]: + ... + + def get_field_for_type( + self, field_name: str, type_name: str + ) -> Optional[StrawberryField]: + ... diff --git a/tests/codegen/snapshots/from_graphql_schema/python/alias.py b/tests/codegen/snapshots/from_graphql_schema/python/alias.py new file mode 100644 index 0000000000..189cb7d826 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/python/alias.py @@ -0,0 +1,11 @@ +class OperationNameResultLazy: + # alias for something + lazy: bool + +class OperationNameResult: + id: str + # alias for id + second_id: str + # alias for float + a_float: float + lazy: OperationNameResultLazy diff --git a/tests/codegen/snapshots/from_graphql_schema/python/basic.py b/tests/codegen/snapshots/from_graphql_schema/python/basic.py new file mode 100644 index 0000000000..f21ee1bf9f --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/python/basic.py @@ -0,0 +1,18 @@ +from uuid import UUID +from datetime import date, datetime, time +from decimal import Decimal + +class OperationNameResultLazy: + something: bool + +class OperationNameResult: + id: str + integer: int + float: float + boolean: bool + uuid: UUID + date: date + datetime: datetime + time: time + decimal: Decimal + lazy: OperationNameResultLazy diff --git a/tests/codegen/snapshots/from_graphql_schema/python/enum.py b/tests/codegen/snapshots/from_graphql_schema/python/enum.py new file mode 100644 index 0000000000..1116307dd7 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/python/enum.py @@ -0,0 +1,9 @@ +from enum import Enum + +class Color(Enum): + RED = "RED" + GREEN = "GREEN" + BLUE = "BLUE" + +class OperationNameResult: + enum: Color diff --git a/tests/codegen/snapshots/from_graphql_schema/python/fragment.py b/tests/codegen/snapshots/from_graphql_schema/python/fragment.py new file mode 100644 index 0000000000..cd3f9205c1 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/python/fragment.py @@ -0,0 +1,6 @@ +class PersonName: + # typename: Person + name: str + +class OperationNameResult: + person: PersonName diff --git a/tests/codegen/snapshots/from_graphql_schema/python/generic_types.py b/tests/codegen/snapshots/from_graphql_schema/python/generic_types.py new file mode 100644 index 0000000000..b498397549 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/python/generic_types.py @@ -0,0 +1,14 @@ +class ListLifeGenericResultListLifeItems1: + name: str + age: int + +class ListLifeGenericResultListLifeItems2: + name: str + age: int + +class ListLifeGenericResultListLife: + items1: ListLifeGenericResultListLifeItems1 + items2: ListLifeGenericResultListLifeItems2 + +class ListLifeGenericResult: + listLife: ListLifeGenericResultListLife diff --git a/tests/codegen/snapshots/from_graphql_schema/python/interface.py b/tests/codegen/snapshots/from_graphql_schema/python/interface.py new file mode 100644 index 0000000000..7e810d3f2c --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/python/interface.py @@ -0,0 +1,5 @@ +class OperationNameResultInterface: + id: str + +class OperationNameResult: + interface: OperationNameResultInterface diff --git a/tests/codegen/snapshots/from_graphql_schema/python/interface_fragments.py b/tests/codegen/snapshots/from_graphql_schema/python/interface_fragments.py new file mode 100644 index 0000000000..eaf5791066 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/python/interface_fragments.py @@ -0,0 +1,16 @@ +from typing import Union + +class OperationNameResultInterfaceBlogPost: + # typename: BlogPost + id: str + title: str + +class OperationNameResultInterfaceImage: + # typename: Image + id: str + url: str + +OperationNameResultInterface = Union[OperationNameResultInterfaceBlogPost, OperationNameResultInterfaceImage] + +class OperationNameResult: + interface: OperationNameResultInterface diff --git a/tests/codegen/snapshots/from_graphql_schema/python/interface_fragments_with_spread.py b/tests/codegen/snapshots/from_graphql_schema/python/interface_fragments_with_spread.py new file mode 100644 index 0000000000..e30940c12f --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/python/interface_fragments_with_spread.py @@ -0,0 +1,20 @@ +from typing import Union + +class PartialBlogPost: + # typename: BlogPost + title: str + +class OperationNameResultInterfaceBlogPost: + # typename: BlogPost + id: str + title: str + +class OperationNameResultInterfaceImage: + # typename: Image + id: str + url: str + +OperationNameResultInterface = Union[OperationNameResultInterfaceBlogPost, OperationNameResultInterfaceImage] + +class OperationNameResult: + interface: OperationNameResultInterface diff --git a/tests/codegen/snapshots/from_graphql_schema/python/interface_single_fragment.py b/tests/codegen/snapshots/from_graphql_schema/python/interface_single_fragment.py new file mode 100644 index 0000000000..61afdcd5bc --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/python/interface_single_fragment.py @@ -0,0 +1,7 @@ +class OperationNameResultInterfaceBlogPost: + # typename: BlogPost + id: str + title: str + +class OperationNameResult: + interface: OperationNameResultInterfaceBlogPost diff --git a/tests/codegen/snapshots/from_graphql_schema/python/multiple_types.py b/tests/codegen/snapshots/from_graphql_schema/python/multiple_types.py new file mode 100644 index 0000000000..44bc7ea3f7 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/python/multiple_types.py @@ -0,0 +1,9 @@ +class OperationNameResultPerson: + name: str + +class OperationNameResultListOfPeople: + name: str + +class OperationNameResult: + person: OperationNameResultPerson + listOfPeople: OperationNameResultListOfPeople diff --git a/tests/codegen/snapshots/from_graphql_schema/python/multiple_types_optional.py b/tests/codegen/snapshots/from_graphql_schema/python/multiple_types_optional.py new file mode 100644 index 0000000000..aa9ed8301c --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/python/multiple_types_optional.py @@ -0,0 +1,5 @@ +class OperationNameResultOptionalPerson: + name: str + +class OperationNameResult: + optionalPerson: OperationNameResultOptionalPerson diff --git a/tests/codegen/snapshots/from_graphql_schema/python/mutation-fragment.py b/tests/codegen/snapshots/from_graphql_schema/python/mutation-fragment.py new file mode 100644 index 0000000000..1fa641132e --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/python/mutation-fragment.py @@ -0,0 +1,9 @@ +class IdFragment: + # typename: BlogPost + id: str + +class addBookResult: + addBook: IdFragment + +class addBookVariables: + input: str diff --git a/tests/codegen/snapshots/from_graphql_schema/python/mutation.py b/tests/codegen/snapshots/from_graphql_schema/python/mutation.py new file mode 100644 index 0000000000..2616cbf519 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/python/mutation.py @@ -0,0 +1,8 @@ +class addBookResultAddBook: + id: str + +class addBookResult: + addBook: addBookResultAddBook + +class addBookVariables: + input: str diff --git a/tests/codegen/snapshots/from_graphql_schema/python/mutation_with_object.py b/tests/codegen/snapshots/from_graphql_schema/python/mutation_with_object.py new file mode 100644 index 0000000000..d0eaa063e1 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/python/mutation_with_object.py @@ -0,0 +1,27 @@ +from enum import Enum +from typing import List + +class AddBlogPostsResultAddBlogPostsPosts: + title: str + +class AddBlogPostsResultAddBlogPosts: + posts: AddBlogPostsResultAddBlogPostsPosts + +class AddBlogPostsResult: + addBlogPosts: AddBlogPostsResultAddBlogPosts + +class Color(Enum): + RED = "RED" + GREEN = "GREEN" + BLUE = "BLUE" + +class BlogPostInput: + title: str + color: Color + pi: float + aBool: bool + anInt: int + anOptionalInt: int + +class AddBlogPostsVariables: + input: List[BlogPostInput] diff --git a/tests/codegen/snapshots/from_graphql_schema/python/optional_and_lists.py b/tests/codegen/snapshots/from_graphql_schema/python/optional_and_lists.py new file mode 100644 index 0000000000..4336802c52 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/python/optional_and_lists.py @@ -0,0 +1,5 @@ +class OperationNameResult: + optionalInt: int + listOfInt: int + listOfOptionalInt: int + optionalListOfOptionalInt: int diff --git a/tests/codegen/snapshots/from_graphql_schema/python/union.py b/tests/codegen/snapshots/from_graphql_schema/python/union.py new file mode 100644 index 0000000000..6e495da8a3 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/python/union.py @@ -0,0 +1,25 @@ +from typing import Union + +class OperationNameResultUnionAnimal: + # typename: Animal + age: int + +class OperationNameResultUnionPerson: + # typename: Person + name: str + +OperationNameResultUnion = Union[OperationNameResultUnionAnimal, OperationNameResultUnionPerson] + +class OperationNameResultOptionalUnionAnimal: + # typename: Animal + age: int + +class OperationNameResultOptionalUnionPerson: + # typename: Person + name: str + +OperationNameResultOptionalUnion = Union[OperationNameResultOptionalUnionAnimal, OperationNameResultOptionalUnionPerson] + +class OperationNameResult: + union: OperationNameResultUnion + optionalUnion: OperationNameResultOptionalUnion diff --git a/tests/codegen/snapshots/from_graphql_schema/python/union_return.py b/tests/codegen/snapshots/from_graphql_schema/python/union_return.py new file mode 100644 index 0000000000..bd07b74816 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/python/union_return.py @@ -0,0 +1,7 @@ +class OperationNameResultGetPersonOrAnimalPerson: + # typename: Person + name: str + age: int + +class OperationNameResult: + getPersonOrAnimal: OperationNameResultGetPersonOrAnimalPerson diff --git a/tests/codegen/snapshots/from_graphql_schema/python/union_with_one_type.py b/tests/codegen/snapshots/from_graphql_schema/python/union_with_one_type.py new file mode 100644 index 0000000000..fa4f3129f3 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/python/union_with_one_type.py @@ -0,0 +1,7 @@ +class OperationNameResultUnionAnimal: + # typename: Animal + age: int + name: str + +class OperationNameResult: + union: OperationNameResultUnionAnimal diff --git a/tests/codegen/snapshots/from_graphql_schema/python/union_with_typename.py b/tests/codegen/snapshots/from_graphql_schema/python/union_with_typename.py new file mode 100644 index 0000000000..6e495da8a3 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/python/union_with_typename.py @@ -0,0 +1,25 @@ +from typing import Union + +class OperationNameResultUnionAnimal: + # typename: Animal + age: int + +class OperationNameResultUnionPerson: + # typename: Person + name: str + +OperationNameResultUnion = Union[OperationNameResultUnionAnimal, OperationNameResultUnionPerson] + +class OperationNameResultOptionalUnionAnimal: + # typename: Animal + age: int + +class OperationNameResultOptionalUnionPerson: + # typename: Person + name: str + +OperationNameResultOptionalUnion = Union[OperationNameResultOptionalUnionAnimal, OperationNameResultOptionalUnionPerson] + +class OperationNameResult: + union: OperationNameResultUnion + optionalUnion: OperationNameResultOptionalUnion diff --git a/tests/codegen/snapshots/from_graphql_schema/python/union_with_typename_and_fragment.py b/tests/codegen/snapshots/from_graphql_schema/python/union_with_typename_and_fragment.py new file mode 100644 index 0000000000..856a21857f --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/python/union_with_typename_and_fragment.py @@ -0,0 +1,21 @@ +from typing import Union + +class AnimalProjection: + # typename: Animal + age: int + +class OperationNameResultUnionPerson: + # typename: Person + name: str + +OperationNameResultUnion = Union[AnimalProjection, OperationNameResultUnionPerson] + +class OperationNameResultOptionalUnionPerson: + # typename: Person + name: str + +OperationNameResultOptionalUnion = Union[AnimalProjection, OperationNameResultOptionalUnionPerson] + +class OperationNameResult: + union: OperationNameResultUnion + optionalUnion: OperationNameResultOptionalUnion diff --git a/tests/codegen/snapshots/from_graphql_schema/python/variables.py b/tests/codegen/snapshots/from_graphql_schema/python/variables.py new file mode 100644 index 0000000000..3c35ff5c85 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/python/variables.py @@ -0,0 +1,23 @@ +from typing import List, Optional + +class OperationNameResult: + withInputs: bool + +class PersonInput: + name: str + age: int + +class ExampleInput: + id: str + name: str + age: int + person: PersonInput + people: PersonInput + optionalPeople: PersonInput + +class OperationNameVariables: + id: Optional[str] + input: ExampleInput + ids: List[str] + ids2: Optional[List[Optional[str]]] + ids3: Optional[List[Optional[List[Optional[str]]]]] diff --git a/tests/codegen/snapshots/from_graphql_schema/python/with_directives.py b/tests/codegen/snapshots/from_graphql_schema/python/with_directives.py new file mode 100644 index 0000000000..3fbc413822 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/python/with_directives.py @@ -0,0 +1,5 @@ +class OperationNameResultPerson: + name: str + +class OperationNameResult: + person: OperationNameResultPerson diff --git a/tests/codegen/snapshots/from_graphql_schema/typescript/alias.ts b/tests/codegen/snapshots/from_graphql_schema/typescript/alias.ts new file mode 100644 index 0000000000..b4888318d7 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/typescript/alias.ts @@ -0,0 +1,13 @@ +type OperationNameResultLazy = { + // alias for something + lazy: boolean +} + +type OperationNameResult = { + id: string + // alias for id + second_id: string + // alias for float + a_float: number + lazy: OperationNameResultLazy +} diff --git a/tests/codegen/snapshots/from_graphql_schema/typescript/basic.ts b/tests/codegen/snapshots/from_graphql_schema/typescript/basic.ts new file mode 100644 index 0000000000..8a89a82b09 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/typescript/basic.ts @@ -0,0 +1,16 @@ +type OperationNameResultLazy = { + something: boolean +} + +type OperationNameResult = { + id: string + integer: number + float: number + boolean: boolean + uuid: string + date: string + datetime: string + time: string + decimal: string + lazy: OperationNameResultLazy +} diff --git a/tests/codegen/snapshots/from_graphql_schema/typescript/enum.ts b/tests/codegen/snapshots/from_graphql_schema/typescript/enum.ts new file mode 100644 index 0000000000..2356061132 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/typescript/enum.ts @@ -0,0 +1,9 @@ +enum Color { + RED = "RED", + GREEN = "GREEN", + BLUE = "BLUE", +} + +type OperationNameResult = { + enum: Color +} diff --git a/tests/codegen/snapshots/from_graphql_schema/typescript/fragment.ts b/tests/codegen/snapshots/from_graphql_schema/typescript/fragment.ts new file mode 100644 index 0000000000..e1cc774a48 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/typescript/fragment.ts @@ -0,0 +1,7 @@ +type PersonName = { + name: string +} + +type OperationNameResult = { + person: PersonName +} diff --git a/tests/codegen/snapshots/from_graphql_schema/typescript/generic_types.ts b/tests/codegen/snapshots/from_graphql_schema/typescript/generic_types.ts new file mode 100644 index 0000000000..2aaa9c9425 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/typescript/generic_types.ts @@ -0,0 +1,18 @@ +type ListLifeGenericResultListLifeItems1 = { + name: string + age: number +} + +type ListLifeGenericResultListLifeItems2 = { + name: string + age: number +} + +type ListLifeGenericResultListLife = { + items1: ListLifeGenericResultListLifeItems1 + items2: ListLifeGenericResultListLifeItems2 +} + +type ListLifeGenericResult = { + listLife: ListLifeGenericResultListLife +} diff --git a/tests/codegen/snapshots/from_graphql_schema/typescript/interface.ts b/tests/codegen/snapshots/from_graphql_schema/typescript/interface.ts new file mode 100644 index 0000000000..6161ec66e8 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/typescript/interface.ts @@ -0,0 +1,7 @@ +type OperationNameResultInterface = { + id: string +} + +type OperationNameResult = { + interface: OperationNameResultInterface +} diff --git a/tests/codegen/snapshots/from_graphql_schema/typescript/interface_fragments.ts b/tests/codegen/snapshots/from_graphql_schema/typescript/interface_fragments.ts new file mode 100644 index 0000000000..36a972825d --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/typescript/interface_fragments.ts @@ -0,0 +1,15 @@ +type OperationNameResultInterfaceBlogPost = { + id: string + title: string +} + +type OperationNameResultInterfaceImage = { + id: string + url: string +} + +type OperationNameResultInterface = OperationNameResultInterfaceBlogPost | OperationNameResultInterfaceImage + +type OperationNameResult = { + interface: OperationNameResultInterface +} diff --git a/tests/codegen/snapshots/from_graphql_schema/typescript/interface_fragments_with_spread.ts b/tests/codegen/snapshots/from_graphql_schema/typescript/interface_fragments_with_spread.ts new file mode 100644 index 0000000000..d94d4c6eb9 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/typescript/interface_fragments_with_spread.ts @@ -0,0 +1,19 @@ +type PartialBlogPost = { + title: string +} + +type OperationNameResultInterfaceBlogPost = { + id: string + title: string +} + +type OperationNameResultInterfaceImage = { + id: string + url: string +} + +type OperationNameResultInterface = OperationNameResultInterfaceBlogPost | OperationNameResultInterfaceImage + +type OperationNameResult = { + interface: OperationNameResultInterface +} diff --git a/tests/codegen/snapshots/from_graphql_schema/typescript/interface_single_fragment.ts b/tests/codegen/snapshots/from_graphql_schema/typescript/interface_single_fragment.ts new file mode 100644 index 0000000000..4ac7922412 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/typescript/interface_single_fragment.ts @@ -0,0 +1,8 @@ +type OperationNameResultInterfaceBlogPost = { + id: string + title: string +} + +type OperationNameResult = { + interface: OperationNameResultInterfaceBlogPost +} diff --git a/tests/codegen/snapshots/from_graphql_schema/typescript/multiple_types.ts b/tests/codegen/snapshots/from_graphql_schema/typescript/multiple_types.ts new file mode 100644 index 0000000000..e8bc8b1cf5 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/typescript/multiple_types.ts @@ -0,0 +1,12 @@ +type OperationNameResultPerson = { + name: string +} + +type OperationNameResultListOfPeople = { + name: string +} + +type OperationNameResult = { + person: OperationNameResultPerson + listOfPeople: OperationNameResultListOfPeople +} diff --git a/tests/codegen/snapshots/from_graphql_schema/typescript/multiple_types_optional.ts b/tests/codegen/snapshots/from_graphql_schema/typescript/multiple_types_optional.ts new file mode 100644 index 0000000000..e32d9c74b6 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/typescript/multiple_types_optional.ts @@ -0,0 +1,7 @@ +type OperationNameResultOptionalPerson = { + name: string +} + +type OperationNameResult = { + optionalPerson: OperationNameResultOptionalPerson +} diff --git a/tests/codegen/snapshots/from_graphql_schema/typescript/mutation-fragment.ts b/tests/codegen/snapshots/from_graphql_schema/typescript/mutation-fragment.ts new file mode 100644 index 0000000000..66fd0a35df --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/typescript/mutation-fragment.ts @@ -0,0 +1,11 @@ +type IdFragment = { + id: string +} + +type addBookResult = { + addBook: IdFragment +} + +type addBookVariables = { + input: string +} diff --git a/tests/codegen/snapshots/from_graphql_schema/typescript/mutation.ts b/tests/codegen/snapshots/from_graphql_schema/typescript/mutation.ts new file mode 100644 index 0000000000..9c0c626d2a --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/typescript/mutation.ts @@ -0,0 +1,11 @@ +type addBookResultAddBook = { + id: string +} + +type addBookResult = { + addBook: addBookResultAddBook +} + +type addBookVariables = { + input: string +} diff --git a/tests/codegen/snapshots/from_graphql_schema/typescript/mutation_with_object.ts b/tests/codegen/snapshots/from_graphql_schema/typescript/mutation_with_object.ts new file mode 100644 index 0000000000..5b3e0c072d --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/typescript/mutation_with_object.ts @@ -0,0 +1,30 @@ +type AddBlogPostsResultAddBlogPostsPosts = { + title: string +} + +type AddBlogPostsResultAddBlogPosts = { + posts: AddBlogPostsResultAddBlogPostsPosts +} + +type AddBlogPostsResult = { + addBlogPosts: AddBlogPostsResultAddBlogPosts +} + +enum Color { + RED = "RED", + GREEN = "GREEN", + BLUE = "BLUE", +} + +type BlogPostInput = { + title: string + color: Color + pi: number + aBool: boolean + anInt: number + anOptionalInt: number +} + +type AddBlogPostsVariables = { + input: BlogPostInput[] +} diff --git a/tests/codegen/snapshots/from_graphql_schema/typescript/optional_and_lists.ts b/tests/codegen/snapshots/from_graphql_schema/typescript/optional_and_lists.ts new file mode 100644 index 0000000000..0849fb2e86 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/typescript/optional_and_lists.ts @@ -0,0 +1,6 @@ +type OperationNameResult = { + optionalInt: number + listOfInt: number + listOfOptionalInt: number + optionalListOfOptionalInt: number +} diff --git a/tests/codegen/snapshots/from_graphql_schema/typescript/union.ts b/tests/codegen/snapshots/from_graphql_schema/typescript/union.ts new file mode 100644 index 0000000000..bd48a02189 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/typescript/union.ts @@ -0,0 +1,24 @@ +type OperationNameResultUnionAnimal = { + age: number +} + +type OperationNameResultUnionPerson = { + name: string +} + +type OperationNameResultUnion = OperationNameResultUnionAnimal | OperationNameResultUnionPerson + +type OperationNameResultOptionalUnionAnimal = { + age: number +} + +type OperationNameResultOptionalUnionPerson = { + name: string +} + +type OperationNameResultOptionalUnion = OperationNameResultOptionalUnionAnimal | OperationNameResultOptionalUnionPerson + +type OperationNameResult = { + union: OperationNameResultUnion + optionalUnion: OperationNameResultOptionalUnion +} diff --git a/tests/codegen/snapshots/from_graphql_schema/typescript/union_return.ts b/tests/codegen/snapshots/from_graphql_schema/typescript/union_return.ts new file mode 100644 index 0000000000..363770e1ae --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/typescript/union_return.ts @@ -0,0 +1,8 @@ +type OperationNameResultGetPersonOrAnimalPerson = { + name: string + age: number +} + +type OperationNameResult = { + getPersonOrAnimal: OperationNameResultGetPersonOrAnimalPerson +} diff --git a/tests/codegen/snapshots/from_graphql_schema/typescript/union_with_one_type.ts b/tests/codegen/snapshots/from_graphql_schema/typescript/union_with_one_type.ts new file mode 100644 index 0000000000..da196e141b --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/typescript/union_with_one_type.ts @@ -0,0 +1,8 @@ +type OperationNameResultUnionAnimal = { + age: number + name: string +} + +type OperationNameResult = { + union: OperationNameResultUnionAnimal +} diff --git a/tests/codegen/snapshots/from_graphql_schema/typescript/union_with_typename.ts b/tests/codegen/snapshots/from_graphql_schema/typescript/union_with_typename.ts new file mode 100644 index 0000000000..12e1e5b5a5 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/typescript/union_with_typename.ts @@ -0,0 +1,25 @@ +type OperationNameResultUnionAnimal = { + age: number +} + +type OperationNameResultUnionPerson = { + name: string +} + +type OperationNameResultUnion = OperationNameResultUnionAnimal | OperationNameResultUnionPerson + +type OperationNameResultOptionalUnionAnimal = { + age: number +} + +type OperationNameResultOptionalUnionPerson = { + name: string +} + +type OperationNameResultOptionalUnion = OperationNameResultOptionalUnionAnimal | OperationNameResultOptionalUnionPerson + +type OperationNameResult = { + __typename: string + union: OperationNameResultUnion + optionalUnion: OperationNameResultOptionalUnion +} diff --git a/tests/codegen/snapshots/from_graphql_schema/typescript/union_with_typename_and_fragment.ts b/tests/codegen/snapshots/from_graphql_schema/typescript/union_with_typename_and_fragment.ts new file mode 100644 index 0000000000..ead1ae0379 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/typescript/union_with_typename_and_fragment.ts @@ -0,0 +1,21 @@ +type AnimalProjection = { + age: number +} + +type OperationNameResultUnionPerson = { + name: string +} + +type OperationNameResultUnion = AnimalProjection | OperationNameResultUnionPerson + +type OperationNameResultOptionalUnionPerson = { + name: string +} + +type OperationNameResultOptionalUnion = AnimalProjection | OperationNameResultOptionalUnionPerson + +type OperationNameResult = { + __typename: string + union: OperationNameResultUnion + optionalUnion: OperationNameResultOptionalUnion +} diff --git a/tests/codegen/snapshots/from_graphql_schema/typescript/variables.ts b/tests/codegen/snapshots/from_graphql_schema/typescript/variables.ts new file mode 100644 index 0000000000..ec9193a2a9 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/typescript/variables.ts @@ -0,0 +1,25 @@ +type OperationNameResult = { + withInputs: boolean +} + +type PersonInput = { + name: string + age: number +} + +type ExampleInput = { + id: string + name: string + age: number + person: PersonInput + people: PersonInput + optionalPeople: PersonInput +} + +type OperationNameVariables = { + id: string | undefined + input: ExampleInput + ids: string[] + ids2: (string | undefined)[] | undefined + ids3: ((string | undefined)[] | undefined)[] | undefined +} diff --git a/tests/codegen/snapshots/from_graphql_schema/typescript/with_directives.ts b/tests/codegen/snapshots/from_graphql_schema/typescript/with_directives.ts new file mode 100644 index 0000000000..3cd820f2c3 --- /dev/null +++ b/tests/codegen/snapshots/from_graphql_schema/typescript/with_directives.ts @@ -0,0 +1,7 @@ +type OperationNameResultPerson = { + name: string +} + +type OperationNameResult = { + person: OperationNameResultPerson +} diff --git a/tests/codegen/test_query_codegen.py b/tests/codegen/test_query_codegen.py index 78a3b71259..8d5cdeee84 100644 --- a/tests/codegen/test_query_codegen.py +++ b/tests/codegen/test_query_codegen.py @@ -8,6 +8,7 @@ from typing import Type import pytest +from graphql.utilities import build_schema from pytest_snapshot.plugin import Snapshot from strawberry.codegen import QueryCodegen, QueryCodegenPlugin @@ -18,6 +19,8 @@ ) from strawberry.codegen.plugins.python import PythonPlugin from strawberry.codegen.plugins.typescript import TypeScriptPlugin +from strawberry.codegen.schema_adapter import GraphQLSchemaWrapper +from strawberry.printer import print_schema HERE = Path(__file__).parent QUERIES = list(HERE.glob("queries/*.graphql")) @@ -50,6 +53,50 @@ def test_codegen( snapshot.assert_match(code, f"{query.with_suffix('').stem}.{extension}") +@pytest.mark.parametrize( + ("plugin_class", "plugin_name", "extension"), + [ + (PythonPlugin, "python", "py"), + (TypeScriptPlugin, "typescript", "ts"), + ], + ids=["python", "typescript"], +) +@pytest.mark.parametrize("query", QUERIES, ids=[x.name for x in QUERIES]) +def test_codegen_from_schema_file( + query: Path, + plugin_class: Type[QueryCodegenPlugin], + plugin_name: str, + extension: str, + snapshot: Snapshot, + schema, +): + schema_text = print_schema(schema) + schema_wrapper = GraphQLSchemaWrapper(build_schema(schema_text)) + + skip_snapshot_test = False + if query.name == "custom_scalar.graphql": + # The default plugins don't support custom types built this way + # (because it doesn't know the python on Typscript types). + # However, the query generator should still be able to handle the custom + # types and user-custom plugins could still do something legitimate here. + plugin = QueryCodegenPlugin(query) + skip_snapshot_test = True + else: + plugin = plugin_class(query) + + generator = QueryCodegen(schema_wrapper, plugins=[plugin]) + + result = generator.run(query.read_text()) + + code = result.to_string() + + if skip_snapshot_test: + return + + snapshot.snapshot_dir = HERE / "snapshots" / "from_graphql_schema" / plugin_name + snapshot.assert_match(code, f"{query.with_suffix('').stem}.{extension}") + + def test_codegen_fails_if_no_operation_name(schema, tmp_path): query = tmp_path / "query.graphql" data = "query { hello }"