Skip to content

Commit

Permalink
Interfaces and unions
Browse files Browse the repository at this point in the history
  • Loading branch information
rafalp committed Oct 8, 2024
1 parent 3b90f74 commit 9866dab
Show file tree
Hide file tree
Showing 14 changed files with 352 additions and 7 deletions.
2 changes: 1 addition & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[MESSAGES CONTROL]
disable=C0114, C0115, C0116, W0613, W0622
disable=C0114, C0115, C0116, R0801, R0903, W0613, W0622
58 changes: 57 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ This API aims to use most features from GraphQL Modules v2. It can also be used
Replacing `list` with `Iterable` may be enough to fix this.


## Resolvers need to be decorated with `@classmethod` or `@staticmethod` to keep linters happy.
## Resolvers need to be decorated with `@classmethod` or `@staticmethod` to keep linters happy

Linters will scream that resolver method decorated with `@ObectType.resolver` is missing `self` first attribute.

Expand All @@ -34,3 +34,59 @@ class ConcatMutation(GraphQLMutation):
```

This would be more intuitive way to define mutations than current approach of having `MutationType(GraphQLObject)` with multiple methods.


## Object type docs don't document interface usage for objects with schema

According to interface docs, this is valid:

```python
class PostType(GraphQLObject, SearchResultInterface):
__schema__ = gql(
"""
type Post {
id: ID!
content: String!
category: Category
poster: User
}
"""
)
```

But it will fail to validate with following error:

```
ValueError: Class 'PostType' defines '__schema__' attribute with declaration for an invalid GraphQL type. ('ObjectTypeDefinitionNode' != 'InterfaceTypeDefinitionNode')
```

This points to `SearchResultInterface` validation logic overriding `GraphQLObject`.

Docs also need to be updated to show example for `GraphQLObject` with interface.

Interface's docs are also mentioning `subscription` in its parts which needs to be edited out.

Interface's docs should also have example of usage with `GraphQLObject`, even if only as "See the `GraphQLObject` docs for usage example." link somewhere in it.


## Interfaces need to be explicitly passed to `make_executable_schema`

Given GraphQL object definition:

```python
class PostType(GraphQLObject, SearchResultInterface):
...
```

`make_executable_schema` will fail with:

```
TypeError: Unknown type 'SearchResultInterface'.
```

`GraphQLObject`'s model creation could see if parent types include subclasses of `GraphQLInterface` and include them.


## `GraphQLUnion` docs should have example of union type implementing `resolve_type`

We do this already for `GraphQLInterface`, it would help if `GraphQLUnion` also did it.
Empty file added example/interfaces/__init__.py
Empty file.
24 changes: 24 additions & 0 deletions example/interfaces/search_result.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from typing import Any

from ariadne_graphql_modules import GraphQLInterface

from ..models.group import Group
from ..models.post import Post
from ..models.user import User


class SearchResultInterface(GraphQLInterface):
summary: str

@staticmethod
def resolve_type(obj: Any, *_) -> str:
if isinstance(obj, Group):
return "Group"

if isinstance(obj, Post):
return "Post"

if isinstance(obj, User):
return "User"

raise TypeError(f"Unsupported type: {type(obj)}")
4 changes: 3 additions & 1 deletion example/queries/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from typing import Any

from . import calendar, categories, groups, hello, posts, users
from . import calendar, categories, groups, hello, models, posts, search, users

queries: Any = [
calendar.Query,
categories.Query,
groups.Query,
hello.Query,
models.Query,
posts.Query,
search.Query,
users.Query,
]
21 changes: 21 additions & 0 deletions example/queries/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from typing import Any

from ariadne_graphql_modules import GraphQLObject
from graphql import GraphQLResolveInfo

from ..database import db
from ..unions.model import Model


class Query(GraphQLObject):
@GraphQLObject.field(graphql_type=list[Model])
@staticmethod
async def models(obj, info: GraphQLResolveInfo) -> list[Any]:
results: list = []

results += await db.get_all("categories")
results += await db.get_all("groups")
results += await db.get_all("posts")
results += await db.get_all("users")

return results
32 changes: 32 additions & 0 deletions example/queries/search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from typing import Any

from ariadne_graphql_modules import GraphQLObject
from graphql import GraphQLResolveInfo

from ..database import db
from ..interfaces.search_result import SearchResultInterface


class Query(GraphQLObject):
@GraphQLObject.field(graphql_type=list[SearchResultInterface])
@staticmethod
async def search(obj, info: GraphQLResolveInfo, *, query: str) -> list[Any]:
query = query.strip()
if not query:
return []

results: list = []

for group in await db.get_all("groups"):
if query in group.name.lower():
results.append(group)

for post in await db.get_all("posts"):
if query in post.message.lower():
results.append(post)

for user in await db.get_all("users"):
if query in user.username.lower():
results.append(user)

return results
2 changes: 2 additions & 0 deletions example/schema.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from ariadne_graphql_modules import make_executable_schema

from .interfaces.search_result import SearchResultInterface
from .mutations import mutations
from .queries import queries
from .subscriptions import subscriptions
Expand All @@ -9,5 +10,6 @@
queries,
mutations,
subscriptions,
SearchResultInterface,
convert_names_case=True,
)
8 changes: 7 additions & 1 deletion example/types/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
from graphql import GraphQLResolveInfo

from ..database import db
from ..interfaces.search_result import SearchResultInterface
from ..models.group import Group

if TYPE_CHECKING:
from .user import UserType


class GroupType(GraphQLObject):
class GroupType(GraphQLObject, SearchResultInterface):
id: GraphQLID
name: str
is_admin: bool
Expand All @@ -19,3 +20,8 @@ class GroupType(GraphQLObject):
@staticmethod
async def users(obj: Group, info: GraphQLResolveInfo):
return await db.get_all("users", group_id=obj.id)

@GraphQLObject.resolver("summary")
@staticmethod
async def resolve_summary(obj: Group, info: GraphQLResolveInfo):
return f"#{obj.id}: {obj.name}"
11 changes: 9 additions & 2 deletions example/types/post.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
from graphql import GraphQLResolveInfo

from ..database import db
from ..interfaces.search_result import SearchResultInterface
from ..models.category import Category
from ..models.post import Post
from ..models.user import User

from .category import CategoryType

if TYPE_CHECKING:
Expand All @@ -18,15 +18,17 @@
class PostType(GraphQLObject):
__schema__ = gql(
"""
type Post {
type Post implements SearchResultInterface {
id: ID!
content: String!
category: Category
poster: User
summary: String!
}
"""
)
__aliases__ = {"content": "message"}
__requires__ = [SearchResultInterface]

@GraphQLObject.resolver("category", CategoryType)
@staticmethod
Expand All @@ -42,3 +44,8 @@ async def resolve_poster(obj: Post, info: GraphQLResolveInfo) -> User | None:
return None

return await db.get_row("users", id=obj.poster_id)

@GraphQLObject.resolver("summary")
@staticmethod
async def resolve_summary(obj: Post, info: GraphQLResolveInfo):
return f"#{obj.id}: {obj.message}"
8 changes: 7 additions & 1 deletion example/types/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from graphql import GraphQLResolveInfo

from ..database import db
from ..interfaces.search_result import SearchResultInterface
from ..enums.role import RoleEnum
from ..models.group import Group
from ..models.post import Post
Expand All @@ -14,7 +15,7 @@
from .group import GroupType


class UserType(GraphQLObject):
class UserType(GraphQLObject, SearchResultInterface):
id: GraphQLID
username: str
group: Annotated["GroupType", deferred(".group")]
Expand All @@ -30,3 +31,8 @@ async def resolve_group(user: User, info: GraphQLResolveInfo) -> Group:
@staticmethod
async def resolve_posts(user: User, info: GraphQLResolveInfo) -> list[Post]:
return await db.get_all("posts", poster_id=user.id)

@GraphQLObject.resolver("summary")
@staticmethod
async def resolve_summary(obj: User, info: GraphQLResolveInfo):
return f"#{obj.id}: {obj.username}"
Empty file added example/unions/__init__.py
Empty file.
32 changes: 32 additions & 0 deletions example/unions/model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from typing import Any

from ariadne_graphql_modules import GraphQLUnion

from ..models.category import Category
from ..models.group import Group
from ..models.post import Post
from ..models.user import User
from ..types.category import CategoryType
from ..types.group import GroupType
from ..types.post import PostType
from ..types.user import UserType


class Model(GraphQLUnion):
__types__ = (CategoryType, GroupType, PostType, UserType)

@staticmethod
def resolve_type(obj: Any, *_) -> str:
if isinstance(obj, Category):
return "Category"

if isinstance(obj, Group):
return "Group"

if isinstance(obj, Post):
return "Post"

if isinstance(obj, User):
return "User"

raise TypeError(f"Unsupported type: {type(obj)}")
Loading

0 comments on commit 9866dab

Please sign in to comment.