From 9d9e190328f25e1a3fb1fcee5d238e6561a97852 Mon Sep 17 00:00:00 2001 From: Agzamus Date: Mon, 12 Oct 2026 17:12:27 +0300 Subject: [PATCH] =?UTF-8?q?feat:=209.5.=20=D0=A1=D0=B1=D0=BE=D1=80=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=B8=20=D0=BC=D0=BE=D0=BD=D1=82=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/graphql/resolvers.py | 12 ++++++++++- app/graphql/schema.py | 44 ++++++++++++++++++++++++++++++++++++++++ app/main.py | 2 ++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 app/graphql/schema.py diff --git a/app/graphql/resolvers.py b/app/graphql/resolvers.py index 50f3e6c..e0f38a8 100644 --- a/app/graphql/resolvers.py +++ b/app/graphql/resolvers.py @@ -1,7 +1,8 @@ +import json from typing import Any from uuid import uuid4 -from ariadne import MutationType, QueryType, convert_kwargs_to_snake_case +from ariadne import MutationType, ObjectType, QueryType, convert_kwargs_to_snake_case from graphql import GraphQLError from sqlalchemy import select @@ -19,6 +20,8 @@ from app.models import AuditEvent, OAuthClient, User query = QueryType() mutation = MutationType() +audit_event = ObjectType("AuditEvent") + # me – возвращает `info.context["user"]`; @query.field("me") @@ -128,3 +131,10 @@ def resolve_disable_user(_, info: Any, user_id: int) -> User: actor = info.context.get("user") write_event(db, "user.disabled", actor_id=actor.id if actor else None) return user + + +@audit_event.field("metadata") +def resolve_metadata(obj: AuditEvent, info: Any) -> str | None: + if obj.metadata_json is None: + return None + return json.dumps(obj.metadata_json, ensure_ascii=False) diff --git a/app/graphql/schema.py b/app/graphql/schema.py new file mode 100644 index 0000000..32b16e0 --- /dev/null +++ b/app/graphql/schema.py @@ -0,0 +1,44 @@ +from datetime import datetime +from pathlib import Path +from typing import Any + +from ariadne import ScalarType, load_schema_from_path, make_executable_schema +from ariadne import snake_case_fallback_resolvers +from ariadne.asgi import GraphQL + +from app.graphql.context import get_context_value +from app.graphql.directives import IsAdminDirective, IsAuthenticatedDirective +from app.graphql.resolvers import audit_event, mutation, query + +datetime_scalar = ScalarType("DateTime") + + +@datetime_scalar.serializer +def _serialize_datetime(value: Any) -> str: + if isinstance(value, datetime): + return value.isoformat() + return str(value) + + +# Загрузить SDL-схему из файла (в Ariadne есть функция load_schema_from_path) +type_defs = load_schema_from_path(str(Path(__file__).parent / "schema.graphql")) + +# Собрать исполняемую схему через `make_executable_schema`, передав: +schema = make_executable_schema( + # типы; + type_defs, + # объекты `query` и `mutation` из `resolvers.py`; + query, + mutation, + audit_event, + datetime_scalar, + snake_case_fallback_resolvers, + # словарь директив `{"isAuthenticated": ..., "isAdmin": ...}`. + directives={ + "isAuthenticated": IsAuthenticatedDirective, + "isAdmin": IsAdminDirective, + }, +) + +# Обернуть схему в ASGI-приложение `GraphQL(schema, context_value=..., debug=True)`. +graphql_app = GraphQL(schema, context_value=get_context_value, debug=True) diff --git a/app/main.py b/app/main.py index 4d31700..c0e0c74 100644 --- a/app/main.py +++ b/app/main.py @@ -2,7 +2,9 @@ from fastapi import FastAPI from app.auth.router import router as auth_router from app.config import settings +from app.graphql.schema import graphql_app app = FastAPI(title=settings.app_name) app.include_router(auth_router) +app.mount("/graphql", graphql_app)