Skip to content

Commit fceeca9

Browse files
authored
Merge pull request #25 from toggle-corp/feature/graphql-and-login
Graphql Setup and login modules
2 parents 2df863e + 47e6966 commit fceeca9

26 files changed

+2080
-469
lines changed

common/dataloaders.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import typing
2+
3+
from django.db import models
4+
5+
DjangoModel = typing.TypeVar("DjangoModel", bound=models.Model)
6+
7+
8+
def load_model_objects(
9+
Model: typing.Type[DjangoModel],
10+
keys: list[int],
11+
) -> list[DjangoModel]:
12+
qs = Model.objects.filter(id__in=keys)
13+
_map = {obj.pk: obj for obj in qs}
14+
return [_map[key] for key in keys]

main/graphql/__init__.py

Whitespace-only changes.

main/graphql/context.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from dataclasses import dataclass
2+
3+
from strawberry.django.context import StrawberryDjangoContext
4+
from strawberry.types import Info as _Info
5+
6+
from .dataloaders import GlobalDataLoader
7+
8+
9+
@dataclass
10+
class GraphQLContext(StrawberryDjangoContext):
11+
dl: GlobalDataLoader
12+
13+
14+
# NOTE: This is for type support only, There is a better way?
15+
class Info(_Info):
16+
context: GraphQLContext # type: ignore[reportIncompatibleMethodOverride]

main/graphql/dataloaders.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from django.utils.functional import cached_property
2+
3+
from user.dataloaders import UserDataLoader
4+
5+
6+
class GlobalDataLoader:
7+
8+
@cached_property
9+
def user(self):
10+
return UserDataLoader()

main/graphql/enums.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import dataclasses
2+
3+
import strawberry
4+
5+
from user.enums import enum_map as user_enum_map
6+
7+
ENUM_TO_STRAWBERRY_ENUM_MAP: dict[str, type] = {
8+
**user_enum_map,
9+
}
10+
11+
12+
class AppEnumData:
13+
def __init__(self, enum):
14+
self.enum = enum
15+
16+
@property
17+
def key(self):
18+
return self.enum
19+
20+
@property
21+
def label(self):
22+
return str(self.enum.label)
23+
24+
25+
def generate_app_enum_collection_data(name):
26+
return type(
27+
name,
28+
(),
29+
{field_name: [AppEnumData(e) for e in enum] for field_name, enum in ENUM_TO_STRAWBERRY_ENUM_MAP.items()},
30+
)
31+
32+
33+
AppEnumCollectionData = generate_app_enum_collection_data("AppEnumCollectionData")
34+
35+
36+
def generate_type_for_enum(name, Enum):
37+
return strawberry.type(
38+
dataclasses.make_dataclass(
39+
f"AppEnumCollection{name}",
40+
[
41+
("key", Enum),
42+
("label", str),
43+
],
44+
)
45+
)
46+
47+
48+
def _enum_type(name, Enum):
49+
EnumType = generate_type_for_enum(name, Enum)
50+
51+
@strawberry.field
52+
def _field() -> list[EnumType]: # type: ignore[reportGeneralTypeIssues]
53+
return [
54+
EnumType(
55+
key=e,
56+
label=e.label,
57+
)
58+
for e in Enum
59+
]
60+
61+
return list[EnumType], _field
62+
63+
64+
def generate_type_for_enums():
65+
enum_fields = [
66+
(
67+
enum_field_name,
68+
*_enum_type(enum_field_name, enum),
69+
)
70+
for enum_field_name, enum in ENUM_TO_STRAWBERRY_ENUM_MAP.items()
71+
]
72+
return strawberry.type(
73+
dataclasses.make_dataclass(
74+
"AppEnumCollection",
75+
enum_fields,
76+
)
77+
)
78+
79+
80+
AppEnumCollection = generate_type_for_enums()

main/graphql/permissions.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import typing
2+
3+
from asgiref.sync import sync_to_async
4+
from strawberry.permission import BasePermission
5+
from strawberry.types import Info
6+
7+
8+
class IsAuthenticated(BasePermission):
9+
message = "User is not authenticated"
10+
11+
@sync_to_async
12+
def has_permission(self, source: typing.Any, info: Info, **_) -> bool:
13+
user = info.context.request.user
14+
return bool(user and user.is_authenticated)

main/graphql/schema.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import strawberry
2+
from strawberry.django.views import AsyncGraphQLView
3+
4+
from user import mutations as user_mutations
5+
from user import queries as user_queries
6+
7+
from .context import GraphQLContext
8+
from .dataloaders import GlobalDataLoader
9+
from .enums import AppEnumCollection, AppEnumCollectionData
10+
from .permissions import IsAuthenticated
11+
12+
13+
class CustomAsyncGraphQLView(AsyncGraphQLView):
14+
async def get_context(self, *args, **kwargs) -> GraphQLContext:
15+
return GraphQLContext(
16+
*args,
17+
**kwargs,
18+
dl=GlobalDataLoader(),
19+
)
20+
21+
22+
@strawberry.type
23+
class PublicQuery(
24+
user_queries.PublicQuery,
25+
):
26+
id: strawberry.ID = strawberry.ID("public")
27+
28+
29+
@strawberry.type
30+
class PrivateQuery(
31+
user_queries.PrivateQuery,
32+
):
33+
id: strawberry.ID = strawberry.ID("private")
34+
35+
36+
@strawberry.type
37+
class PublicMutation(
38+
user_mutations.PublicMutation,
39+
):
40+
id: strawberry.ID = strawberry.ID("public")
41+
42+
43+
@strawberry.type
44+
class PrivateMutation:
45+
id: strawberry.ID = strawberry.ID("private")
46+
47+
48+
@strawberry.type
49+
class Query:
50+
public: PublicQuery = strawberry.field(resolver=lambda: PublicQuery())
51+
private: PrivateQuery = strawberry.field(permission_classes=[IsAuthenticated], resolver=lambda: PrivateQuery())
52+
enums: AppEnumCollection = strawberry.field( # type: ignore[reportGeneralTypeIssues]
53+
resolver=lambda: AppEnumCollectionData()
54+
)
55+
56+
57+
@strawberry.type
58+
class Mutation:
59+
public: PublicMutation = strawberry.field(resolver=lambda: PublicMutation())
60+
private: PrivateMutation = strawberry.field(
61+
resolver=lambda: PrivateMutation(),
62+
permission_classes=[IsAuthenticated],
63+
)
64+
65+
66+
schema = strawberry.Schema(
67+
query=Query,
68+
mutation=Mutation,
69+
)

main/settings.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,19 +151,59 @@
151151
"content",
152152
"organization",
153153
"rest_framework",
154+
"corsheaders",
154155
"chat",
155156
]
156157

157158
MIDDLEWARE = [
158159
"django.middleware.security.SecurityMiddleware",
159160
"django.contrib.sessions.middleware.SessionMiddleware",
161+
"corsheaders.middleware.CorsMiddleware",
160162
"django.middleware.common.CommonMiddleware",
161163
"django.middleware.csrf.CsrfViewMiddleware",
162164
"django.contrib.auth.middleware.AuthenticationMiddleware",
163165
"django.contrib.messages.middleware.MessageMiddleware",
164166
"django.middleware.clickjacking.XFrameOptionsMiddleware",
165167
]
166168

169+
# CORS
170+
if not env("DJANGO_CORS_ORIGIN_REGEX_WHITELIST"):
171+
CORS_ORIGIN_ALLOW_ALL = True
172+
else:
173+
# Example ^https://[\w-]+\.mapswipe\.org$
174+
CORS_ORIGIN_REGEX_WHITELIST = env("DJANGO_CORS_ORIGIN_REGEX_WHITELIST")
175+
176+
CORS_ALLOW_CREDENTIALS = True
177+
CORS_URLS_REGEX = r"(^/media/.*$)|(^/graphql/$)"
178+
CORS_ALLOW_METHODS = (
179+
"DELETE",
180+
"GET",
181+
"OPTIONS",
182+
"PATCH",
183+
"POST",
184+
"PUT",
185+
)
186+
187+
CORS_ALLOW_HEADERS = (
188+
"accept",
189+
"accept-encoding",
190+
"authorization",
191+
"content-type",
192+
"dnt",
193+
"origin",
194+
"user-agent",
195+
"x-csrftoken",
196+
"x-requested-with",
197+
"sentry-trace",
198+
)
199+
200+
201+
# Strawberry
202+
# -- Pagination
203+
STRAWBERRY_ENUM_TO_STRAWBERRY_ENUM_MAP = "main.graphql.enums.ENUM_TO_STRAWBERRY_ENUM_MAP"
204+
STRAWBERRY_DEFAULT_PAGINATION_LIMIT = 50
205+
STRAWBERRY_MAX_PAGINATION_LIMIT = 100
206+
167207
ROOT_URLCONF = "main.urls"
168208

169209
TEMPLATES = [

main/urls.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,15 @@
1919
from django.conf.urls.static import static
2020
from django.contrib import admin
2121
from django.urls import path
22+
from django.views.decorators.csrf import csrf_exempt
2223

2324
from content.views import UserQuery
25+
from main.graphql.schema import CustomAsyncGraphQLView
26+
from main.graphql.schema import schema as graphql_schema
2427

2528
urlpatterns = [path("admin/", admin.site.urls), path("chat_message", UserQuery.as_view())]
2629
if settings.DEBUG:
30+
urlpatterns.append(path("graphiql/", csrf_exempt(CustomAsyncGraphQLView.as_view(schema=graphql_schema))))
2731

2832
# Static and media file URLs
2933
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

0 commit comments

Comments
 (0)