def test_error_with_non_strawberry_type(): @dataclass class A: a: int with pytest.raises(InvalidUnionType, match="Type `A` cannot be used in a GraphQL Union"): strawberry.union("Result", (A, ))
def test_error_with_non_strawberry_type(): @dataclass class A: a: int with pytest.raises(InvalidUnionType, match="Union type `A` is not a Strawberry type"): strawberry.union("Result", (A, ))
def test_union_optional_with_or_operator(): """ Verify that the `|` operator is supported when annotating unions as optional in schemas. """ @strawberry.type class Cat: name: str @strawberry.type class Dog: name: str animal_union = strawberry.union("Animal", (Cat, Dog)) @strawberry.type class Query: @strawberry.field def animal(self) -> animal_union | None: return None schema = strawberry.Schema(query=Query) query = """{ animal { __typename } }""" result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["animal"] is None
def test_union_explicit_type_resolution(): @dataclass class ADataclass: a: int @strawberry.type class A: a: int @classmethod def is_type_of(cls, obj, _info) -> bool: return isinstance(obj, ADataclass) @strawberry.type class B: b: int MyUnion = strawberry.union("MyUnion", types=(A, B)) @strawberry.type class Query: @strawberry.field def my_field(self) -> MyUnion: return ADataclass(a=1) # type: ignore schema = strawberry.Schema(query=Query) query = "{ myField { __typename, ... on A { a }, ... on B { b } } }" result = schema.execute_sync(query) assert not result.errors assert result.data == {"myField": {"__typename": "A", "a": 1}}
def test_union_used_multiple_times(): @strawberry.type class A: a: int @strawberry.type class B: b: int MyUnion = strawberry.union("MyUnion", types=(A, B)) @strawberry.type class Query: field1: MyUnion field2: MyUnion schema = strawberry.Schema(query=Query) assert schema.as_str() == dedent( """\ type A { a: Int! } type B { b: Int! } union MyUnion = A | B type Query { field1: MyUnion! field2: MyUnion! }""" )
def test_union_inside_generics(): @strawberry.type class Dog: name: str @strawberry.type class Cat: name: str @strawberry.type class Connection(Generic[T]): nodes: List[T] DogCat = strawberry.union("DogCat", (Dog, Cat)) @strawberry.type class Query: connection: Connection[DogCat] query_definition = Query._type_definition assert query_definition.type_params == [] [connection_field] = query_definition.fields assert connection_field.python_name == "connection" assert not isinstance(connection_field, StrawberryOptional) dog_cat_connection_definition = connection_field.type._type_definition [node_field] = dog_cat_connection_definition.fields assert isinstance(node_field.type, StrawberryList) union = dog_cat_connection_definition.fields[0].type.of_type assert isinstance(union, StrawberryUnion)
def test_cannot_use_union_directly(): @strawberry.type class A: a: int @strawberry.type class B: b: int Result = strawberry.union("Result", (A, B)) with pytest.raises(ValueError, match=r"Cannot use union type directly"): Result()
def test_named_union(): @strawberry.type class A: a: int @strawberry.type class B: b: int Result = strawberry.union("Result", (A, B)) union_type_definition = Result._union_definition assert union_type_definition.name == "Result" assert union_type_definition.types == (A, B)
def test_named_union(): @strawberry.type class A: a: int @strawberry.type class B: b: int Result = strawberry.union("Result", (A, B)) union_type_definition = Result assert isinstance(union_type_definition, StrawberryUnion) assert union_type_definition.name == "Result" assert union_type_definition.types == (A, B)
def test_union_as_an_argument_type(): with pytest.raises(InvalidFieldArgument): @strawberry.type class Noun: text: str @strawberry.type class Verb: text: str Word = strawberry.union("Word", types=(Noun, Verb)) @strawberry.field def add_word(word: Word) -> bool: return True
def test_union_as_an_argument_type(): error_message = 'Argument "word" on field "add_word" cannot be of type "Union"' with pytest.raises(InvalidFieldArgument, match=error_message): @strawberry.type class Noun: text: str @strawberry.type class Verb: text: str Word = strawberry.union("Word", types=(Noun, Verb)) @strawberry.field def add_word(word: Word) -> bool: return True
def test_named_union_description(): global Result @strawberry.type class A: a: int @strawberry.type class B: b: int Result = strawberry.union("Result", (A, B), description="Example Result") @strawberry.type class Query: ab: Result = A(a=5) schema = strawberry.Schema(query=Query) query = """{ __type(name: "Result") { kind description } ab { __typename, ... on A { a } } }""" result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["ab"] == {"__typename": "A", "a": 5} assert result.data["__type"] == { "kind": "UNION", "description": "Example Result" } del Result
def test_union_inside_generics(): global Connection, DogCat @strawberry.type class Dog: name: str @strawberry.type class Cat: name: str @strawberry.type class Connection(Generic[T]): nodes: List[T] DogCat = strawberry.union("DogCat", (Dog, Cat)) @strawberry.type class Query: connection: Connection[DogCat] definition = Query._type_definition assert definition.name == "Query" assert len(definition.fields) == 1 assert definition.type_params == {} assert definition.fields[0].name == "connection" assert definition.fields[0].is_optional is False type_definition = definition.fields[0].type._type_definition assert type_definition.name == "DogCatConnection" assert len(type_definition.fields) == 1 assert type_definition.fields[0].is_list is True union_definition = type_definition.fields[0].child.type assert isinstance(union_definition, StrawberryUnion) assert union_definition.types[0]._type_definition.name == "Dog" assert union_definition.types[1]._type_definition.name == "Cat" del Connection, DogCat
def test_union(): @strawberry.type class Europe: name: str @strawberry.type class UK: name: str EU = strawberry.union("EU", types=(Europe, UK)) @strawberry.type class WishfulThinking: desire: EU # TODO: Remove reference to ._type_definition with StrawberryObject field: StrawberryField = WishfulThinking._type_definition.fields[0] assert field.type is EU
def test_union_with_generic(): T = TypeVar("T") @strawberry.type class Error: message: str @strawberry.type class Edge(Generic[T]): node: T Result = strawberry.union("Result", (Error, Edge[str])) union_type_definition = Result assert isinstance(union_type_definition, StrawberryUnion) assert union_type_definition.name == "Result" assert union_type_definition.types[0] == Error assert union_type_definition.types[1]._type_definition.is_generic is False assert union_type_definition.types[1]._type_definition.name == "StrEdge"
def test_can_use_union_in_generics(): global Result @strawberry.type class A: a: int @strawberry.type class B: b: int Result = strawberry.union("Result", (A, B)) @strawberry.type class Query: ab: Optional[Result] = None schema = strawberry.Schema(query=Query) query = """{ __type(name: "Result") { kind description } ab { __typename, ... on A { a } } }""" result = schema.execute_sync(query, root_value=Query()) assert not result.errors assert result.data["ab"] is None del Result
def test_named_union(): @strawberry.type class A: a: int @strawberry.type class B: b: int Result = strawberry.union("Result", (A, B)) @strawberry.type class Query: ab: Result = A(a=5) schema = strawberry.Schema(query=Query) query = """{ __type(name: "Result") { kind description } ab { __typename, ... on A { a } } }""" result = graphql_sync(schema, query, root_value=Query()) assert not result.errors assert result.data["ab"] == {"__typename": "A", "a": 5} assert result.data["__type"] == {"kind": "UNION", "description": None}
@strawberry.type class UpdateProfileErrors: name: PydanticError = None full_name: PydanticError = None gender: PydanticError = None open_to_recruiting: PydanticError = None open_to_newsletter: PydanticError = None date_birth: PydanticError = None country: PydanticError = None UpdateProfileValidationError = create_validation_error_type( "UpdateProfile", UpdateProfileErrors) UpdateProfileResult = strawberry.union("UpdateProfileResult", (User, UpdateProfileValidationError)) @strawberry.mutation(permission_classes=[IsAuthenticated]) async def update_profile(input: UpdateProfileInput, info: Info) -> UpdateProfileResult: try: input_model = input.to_pydantic() except pydantic.ValidationError as exc: return UpdateProfileValidationError.from_validation_error(exc) user = await service_update_profile( int(info.context.request.user.id), input_model, users_repository=info.context.users_repository, )
@strawberry.type class RegisterSuccess: user: User @classmethod def from_domain(cls, user: entities.User) -> RegisterSuccess: return cls(user=User.from_domain(user)) @strawberry.type class EmailAlreadyUsed: message: str = "Email specified is already used" RegisterResult = strawberry.union( "RegisterResult", (RegisterSuccess, EmailAlreadyUsed, RegisterValidationError) ) @strawberry.mutation async def register(info: Info, input: RegisterInput) -> RegisterResult: try: input_model = input.to_pydantic() except pydantic.ValidationError as exc: return RegisterValidationError.from_validation_error(exc) try: user = await services.register( input_model, users_repository=info.context.users_repository ) except EmailAlreadyUsedError:
@strawberry.type class WrongEmailOrPassword: message: str = "Invalid username/password combination" @strawberry.type class LoginErrors: email: PydanticError = None password: PydanticError = None LoginValidationError = create_validation_error_type("Login", LoginErrors) LoginResult = strawberry.union( "LoginResult", (LoginSuccess, WrongEmailOrPassword, LoginValidationError)) @strawberry.mutation async def login(info: Info, input: LoginInput) -> LoginResult: try: input_model = input.to_pydantic() except pydantic.ValidationError as exc: return LoginValidationError.from_validation_error(exc) try: user = await services.login( input_model, users_repository=info.context.users_repository) except (WrongEmailOrPasswordError, UserIsNotActiveError): return WrongEmailOrPassword()
class NoSubscription: message: str = "No subscription to manage" @strawberry.type class NotSubscribedViaStripe: message: str = "Not subscribed via Stripe" @strawberry.type class CustomerPortalResponse: billing_portal_url: str CustomerPortalResult = strawberry.union( "CustomerPortalResult", (CustomerPortalResponse, NoSubscription, NotSubscribedViaStripe), ) @strawberry.mutation(permission_classes=[IsAuthenticated]) async def manage_user_subscription( info: Info[Context, Any]) -> CustomerPortalResult: try: billing_portal_url = await service_manage_user_association_subscription( info.context.request.user, association_repository=info.context.association_repository, ) return CustomerPortalResponse(billing_portal_url=billing_portal_url) except (exceptions.CustomerNotAvailable, exceptions.NoSubscriptionAvailable): return NoSubscription()
def __init_subclass__(cls): name = cls.__name__ form_class = cls.Meta.form_class form = form_class() form_fields = form.fields graphql_fields = convert_form_fields_to_fields(form_fields) input_type = None if len(graphql_fields) > 0: input_type = create_input_type(name, graphql_fields) error_type = create_error_type(name, graphql_fields) output_types = ( cls.Meta.output_types if hasattr(cls.Meta, "output_types") else () ) output = strawberry.union( f"{name}Output", (error_type, *output_types), description="Output" ) def _mutate(root, info, input: input_type) -> output: # Add the mutation input in the context so we can access it inside the permissions if info: setattr(info.context, "input", input) # Once we implement the permission in strawberry we can remove this :) if hasattr(cls.Meta, "permission_classes") and cls.Meta.permission_classes: for permission in cls.Meta.permission_classes: if not permission().has_permission(root, info): raise GraphQLError(permission.message) input_as_dict = input if dataclasses.is_dataclass(input): input_as_dict = dataclasses.asdict(input) form_kwargs = cls.get_form_kwargs(root, info, input_as_dict) form_kwargs["data"] = convert_enums_to_values(form_kwargs["data"]) if "instance" in form_kwargs["data"] and issubclass(form_class, ModelForm): instance = form_kwargs["data"].pop("instance") instance = ( form_class.get_instance(instance) if hasattr(form_class, "get_instance") else form_class.Meta.model.objects.get(id=instance) ) form = form_class(instance=instance, **form_kwargs) form.fields.pop("instance", None) else: form = form_class(**form_kwargs) error = cls.validate_form(form) if error: return error result = form.save() if hasattr(cls, "transform"): result = cls.transform(result) return result if input_type: def mutate(root, info, input: input_type) -> output: return _mutate(root, info, input) else: def mutate(root, info) -> output: return _mutate(root, info, {}) # Hack because something changed and now the name contains `Mutation` # as the classname, which makes sense but why it didn't happen before? lol name = name.replace("Mutation", "") mutate = strawberry.mutation(mutate, name=f"{name[0].lower()}{name[1:]}") cls.Mutation = mutate cls.Meta.error_type = error_type
def test_error_with_scalar_types(): with pytest.raises( InvalidUnionType, match="Scalar type `int` cannot be used in a GraphQL Union"): strawberry.union("Result", (int, ))
def test_error_with_empty_type_list(): with pytest.raises(TypeError, match="No types passed to `union`"): strawberry.union("Result", [])
message: str = "Reset password token expired" @strawberry.type class ResetPasswordTokenInvalid: message: str = "Reset password token invalid" ResetPasswordValidationError = create_validation_error_type( "ResetPassword", ResetPasswordErrors) ResetPasswordResult = strawberry.union( "ResetPasswordResult", ( ResetPasswordValidationError, OperationSuccess, ResetPasswordTokenExpired, ResetPasswordTokenInvalid, ), ) @strawberry.mutation async def reset_password(info: Info, input: ResetPasswordInput) -> ResetPasswordResult: try: input_model = input.to_pydantic() except pydantic.ValidationError as exc: return ResetPasswordValidationError.from_validation_error(exc) try:
@strawberry.input class AddOpenPositionInput: company_name: str position: OpenPositionInput @strawberry.type class AddOpenPositionCreated: company: Company @strawberry.type class AddOpenPositionFailed: error: str suggestion: str = "Fix the error and retry" AddOpenPositionResult = strawberry.union("AddOpenPositionResult", (AddOpenPositionCreated, AddOpenPositionFailed), description="Union of possible outcomes when adding a position to a company") class MutationsEnabled(BasePermission): message = "You are not authorized" def has_permission(self, source, info, **kwargs): return os.getenv('MUTATIONS_ENABLED', False) ADD_OPEN_POSITION_EXAMPLE = """ ``` mutation AgregarBusqueda { addOpenPosition(input: { companyName: "CompanyName", position: {title: "Architect", url: "www.url.com"}
@strawberry.type class CheckoutSession: stripe_session_id: str @classmethod def from_domain(cls, stripe_session_id: str) -> CheckoutSession: return cls( stripe_session_id=stripe_session_id, ) SubscribeUserResult = strawberry.union( "SubscribeUserResult", ( CheckoutSession, AlreadySubscribed, ), ) @strawberry.mutation(permission_classes=[IsAuthenticated]) async def subscribe_user_to_association( info: Info[Context, Any] ) -> SubscribeUserResult: try: checkout_session_id = await service_subscribe_user_to_association( info.context.request.user, association_repository=info.context.association_repository, ) return CheckoutSession.from_domain(checkout_session_id)