introduce graphql

This commit is contained in:
2026-05-19 00:24:45 +08:00
parent 78f15c80a1
commit 9c55cc675e
10 changed files with 203 additions and 11 deletions
+3 -2
View File
@@ -5,7 +5,7 @@ prompts:
preset: "下面的代码来自项目相对路径 {{path}}\n---\n{{content}}" preset: "下面的代码来自项目相对路径 {{path}}\n---\n{{content}}"
preset: preset:
auth: auth:
- "app/auth.py" - "app/auth.py"
- "app/auth/*" - "app/auth/*"
@@ -18,4 +18,5 @@ preset:
db_model: db_model:
- "app/models/*" - "app/models/*"
- "app/db.py" - "app/db.py"
- "app/auth.py" - "app/auth.py"
- "app/main.py"
+5 -5
View File
@@ -164,11 +164,11 @@ async def auth_callback(request: Request, code: str, state: str, background_task
"access_token": jwt_token, "access_token": jwt_token,
"refresh_token": refresh_token, "refresh_token": refresh_token,
"token_type": "bearer", "token_type": "bearer",
"user": { # "user": {
"email": user.email, # "email": user.email,
"name": user.name, # "name": user.name,
"avatar": get_image_url(user.avatar), # "avatar": get_image_url(user.avatar),
} # }
} }
@router.get("/me") @router.get("/me")
View File
+26
View File
@@ -0,0 +1,26 @@
from fastapi import Request
from strawberry.fastapi import BaseContext
from app.db import SessionLocal
from app.auth.deps import get_current_user
from app.models import User
from typing import AsyncGenerator
class CustomContext(BaseContext):
def __init__(self, request: Request):
super().__init__(request)
self.db = SessionLocal()
self.request = request
async def get_current_user(self) -> User | None:
# 复用现有的 auth 逻辑
try:
return await get_current_user(self.request)
except Exception:
return None
async def get_context(request: Request) -> AsyncGenerator[CustomContext, None]:
context = CustomContext(request)
try:
yield context
finally:
context.db.close() # 确保请求结束时关闭数据库连接
+18
View File
@@ -0,0 +1,18 @@
import strawberry
from app.auth.jwt import create_jwt_token, create_refresh_token
@strawberry.type
class AuthResponse:
access_token: str
refresh_token: str
token_type: str
@strawberry.type
class Mutation:
@strawberry.mutation
def logout(self) -> str:
return "ok"
# 提示:像 Google OAuth 这种涉及第三方重定向的流程,
# 依然保留现有的 RESTful (/login/google 和 /auth/callback) 会更简单。
# 登录成功后重定向回前端,前端拿到 token,后续所有数据交互全部走 GraphQL。
+18
View File
@@ -0,0 +1,18 @@
import strawberry
from typing import Optional
from app.graphql.types import UserType
from app.models import User
@strawberry.type
class Query:
@strawberry.field
async def me(self, info) -> Optional[UserType]:
# 从 context 获取当前登录用户
user = await info.context.get_current_user()
# Strawberry 会自动将 SQLAlchemy 实例映射为 UserType
return user
@strawberry.field
def get_user_by_handle(self, info, handle: str) -> Optional[UserType]:
db = info.context.db
return db.query(User).filter(User.handle == handle).first()
+44
View File
@@ -0,0 +1,44 @@
import strawberry
from typing import List, Optional
from datetime import datetime
from app.utils.format import get_image_url
@strawberry.type
class UserType:
id: strawberry.ID
email: str
name: Optional[str]
handle: Optional[str]
avatar: Optional[str]
following_count: int
follower_count: int
created_at: datetime
# 动态解析字段:例如自动处理头像的绝对路径
@strawberry.field
def avatar_url(self) -> Optional[str]:
return get_image_url(self.avatar) if self.avatar else None
@strawberry.type
class WorkspaceType:
id: strawberry.ID
name: str
slug: str
logo: Optional[str]
description: Optional[str]
storage_used: int
# 支持前端按需加载关联的成员
@strawberry.field
def members(self, info) -> List["WorkspaceMemberType"]:
from app.models import WorkspaceMember
db = info.context.db
return db.query(WorkspaceMember).filter(WorkspaceMember.workspace_id == self.id).all()
@strawberry.type
class WorkspaceMemberType:
id: strawberry.ID
workspace_id: str
user_id: Optional[str]
role: str
status: str
+25 -4
View File
@@ -1,10 +1,31 @@
from fastapi import FastAPI from fastapi import FastAPI
from app.auth import router as auth_router from strawberry import Schema
from app.models import Base from strawberry.fastapi import GraphQLRouter
from app.db import engine
from app.auth import router as auth_router
from app.db import engine
from app.models import Base
# 导入 GraphQL 组件
from app.graphql.queries import Query
from app.graphql.mutations import Mutation
from app.graphql.context import get_context
# 自动建表
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
# 创建 FastAPI 实例
app = FastAPI() app = FastAPI()
app.include_router(auth_router) # 组装 GraphQL Schema
schema = Schema(query=Query, mutation=Mutation)
# 创建 GraphQL 路由并注入上下文
graphql_router = GraphQLRouter(schema, context_getter=get_context)
# 挂载路由
# 保留现有的 auth 路由供 Google OAuth 回调使用
app.include_router(auth_router)
# 挂载 GraphQL 终点,前端以后只需要请求 /graphql
app.include_router(graphql_router, prefix="/graphql")
+1
View File
@@ -13,6 +13,7 @@ dependencies = [
"psycopg2-binary>=2.9.12", "psycopg2-binary>=2.9.12",
"python-jose>=3.5.0", "python-jose>=3.5.0",
"sqlalchemy>=2.0.49", "sqlalchemy>=2.0.49",
"strawberry-graphql[fastapi]>=0.315.5",
"ulid-py>=1.1.0", "ulid-py>=1.1.0",
"uvicorn>=0.47.0", "uvicorn>=0.47.0",
] ]
Generated
+63
View File
@@ -151,6 +151,7 @@ dependencies = [
{ name = "psycopg2-binary" }, { name = "psycopg2-binary" },
{ name = "python-jose" }, { name = "python-jose" },
{ name = "sqlalchemy" }, { name = "sqlalchemy" },
{ name = "strawberry-graphql", extra = ["fastapi"] },
{ name = "ulid-py" }, { name = "ulid-py" },
{ name = "uvicorn" }, { name = "uvicorn" },
] ]
@@ -165,6 +166,7 @@ requires-dist = [
{ name = "psycopg2-binary", specifier = ">=2.9.12" }, { name = "psycopg2-binary", specifier = ">=2.9.12" },
{ name = "python-jose", specifier = ">=3.5.0" }, { name = "python-jose", specifier = ">=3.5.0" },
{ name = "sqlalchemy", specifier = ">=2.0.49" }, { name = "sqlalchemy", specifier = ">=2.0.49" },
{ name = "strawberry-graphql", extras = ["fastapi"], specifier = ">=0.315.5" },
{ name = "ulid-py", specifier = ">=1.1.0" }, { name = "ulid-py", specifier = ">=1.1.0" },
{ name = "uvicorn", specifier = ">=0.47.0" }, { name = "uvicorn", specifier = ">=0.47.0" },
] ]
@@ -237,6 +239,18 @@ version = "1.7"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6b/b0/e595ce2a2527e169c3bcd6c33d2473c1918e0b7f6826a043ca1245dd4e5b/crcmod-1.7.tar.gz", hash = "sha256:dc7051a0db5f2bd48665a990d3ec1cc305a466a77358ca4492826f41f283601e", size = 89670, upload-time = "2010-06-27T14:35:29.538Z" } sdist = { url = "https://files.pythonhosted.org/packages/6b/b0/e595ce2a2527e169c3bcd6c33d2473c1918e0b7f6826a043ca1245dd4e5b/crcmod-1.7.tar.gz", hash = "sha256:dc7051a0db5f2bd48665a990d3ec1cc305a466a77358ca4492826f41f283601e", size = 89670, upload-time = "2010-06-27T14:35:29.538Z" }
[[package]]
name = "cross-web"
version = "0.6.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ad/83/b5ef04565acc065387dda3a4fbf0c4cfb6bab805c81b66b2bc5b5ac9a282/cross_web-0.6.0.tar.gz", hash = "sha256:ae90570802615365ca1a781117b43bfd0d6cd3bf611649d24c3a206a82a693c9", size = 331315, upload-time = "2026-04-13T14:29:12.718Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/35/a2/dab06d9b80cb76c700883186a9a2e6fd103342c9b4def4d88f5787796e17/cross_web-0.6.0-py3-none-any.whl", hash = "sha256:bdebf0c08d02f3a48cf67b6904d3a6d8fd8cab2cd905592ab96ab00b259cd582", size = 24820, upload-time = "2026-04-13T14:29:11.198Z" },
]
[[package]] [[package]]
name = "cryptography" name = "cryptography"
version = "48.0.0" version = "48.0.0"
@@ -318,6 +332,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f", size = 117683, upload-time = "2026-04-23T16:49:42.437Z" }, { url = "https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f", size = 117683, upload-time = "2026-04-23T16:49:42.437Z" },
] ]
[[package]]
name = "graphql-core"
version = "3.2.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/68/c5/36aa96205c3ecbb3d34c7c24189e4553c7ca2ebc7e1dd07432339b980272/graphql_core-3.2.8.tar.gz", hash = "sha256:015457da5d996c924ddf57a43f4e959b0b94fb695b85ed4c29446e508ed65cf3", size = 513181, upload-time = "2026-03-05T19:55:37.332Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/86/41/cb887d9afc5dabd78feefe6ccbaf83ff423c206a7a1b7aeeac05120b2125/graphql_core-3.2.8-py3-none-any.whl", hash = "sha256:cbee07bee1b3ed5e531723685369039f32ff815ef60166686e0162f540f1520c", size = 207349, upload-time = "2026-03-05T19:55:35.911Z" },
]
[[package]] [[package]]
name = "greenlet" name = "greenlet"
version = "3.5.0" version = "3.5.0"
@@ -452,6 +475,15 @@ dependencies = [
] ]
sdist = { url = "https://files.pythonhosted.org/packages/df/b5/f2cb1950dda46ac2284d6c950489fdacd0e743c2d79a347924d3cc44b86f/oss2-2.19.1.tar.gz", hash = "sha256:a8ab9ee7eb99e88a7e1382edc6ea641d219d585a7e074e3776e9dec9473e59c1", size = 298845, upload-time = "2024-10-25T11:37:46.638Z" } sdist = { url = "https://files.pythonhosted.org/packages/df/b5/f2cb1950dda46ac2284d6c950489fdacd0e743c2d79a347924d3cc44b86f/oss2-2.19.1.tar.gz", hash = "sha256:a8ab9ee7eb99e88a7e1382edc6ea641d219d585a7e074e3776e9dec9473e59c1", size = 298845, upload-time = "2024-10-25T11:37:46.638Z" }
[[package]]
name = "packaging"
version = "26.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
]
[[package]] [[package]]
name = "psycopg2-binary" name = "psycopg2-binary"
version = "2.9.12" version = "2.9.12"
@@ -590,6 +622,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d9/c3/0bd11992072e6a1c513b16500a5d07f91a24017c5909b02c72c62d7ad024/python_jose-3.5.0-py2.py3-none-any.whl", hash = "sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771", size = 34624, upload-time = "2025-05-28T17:31:52.802Z" }, { url = "https://files.pythonhosted.org/packages/d9/c3/0bd11992072e6a1c513b16500a5d07f91a24017c5909b02c72c62d7ad024/python_jose-3.5.0-py2.py3-none-any.whl", hash = "sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771", size = 34624, upload-time = "2025-05-28T17:31:52.802Z" },
] ]
[[package]]
name = "python-multipart"
version = "0.0.29"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4e/fe/70bd71a6738b09a0bdf6480ca6436b167469ca4578b2a0efbe390b4b0e70/python_multipart-0.0.29.tar.gz", hash = "sha256:643e93849196645e2dbdd81a0f8829a23123ad7f797a84a364c6fb3563f18904", size = 45678, upload-time = "2026-05-17T17:29:47.654Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/cb/769cfc37177252872a45a71f3fbdde9d51b471a3f3c14bfe95dde3407386/python_multipart-0.0.29-py3-none-any.whl", hash = "sha256:2ddcc971cef266225f54f552d8fa10bcfbb1f14446caec199060daac59ff2d69", size = 29640, upload-time = "2026-05-17T17:29:45.69Z" },
]
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.34.2" version = "2.34.2"
@@ -676,6 +717,28 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" },
] ]
[[package]]
name = "strawberry-graphql"
version = "0.315.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cross-web" },
{ name = "graphql-core" },
{ name = "packaging" },
{ name = "python-dateutil" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/18/9b/101940ee899959d4d220cc8beb4f41bce9c58830d1872df06f35cf92c457/strawberry_graphql-0.315.5.tar.gz", hash = "sha256:29a2f04479aba29f9f30ecdfce1ef9ee04acee3c434a4f6019249474cd649492", size = 222724, upload-time = "2026-05-14T10:46:16.222Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/19/e389140b3b50faf803b4817ae5a8c3c2d6737f062f4a0fe6778e929a405d/strawberry_graphql-0.315.5-py3-none-any.whl", hash = "sha256:073bc818a5f55951a9a6fbab40bbfa07c418d2c4151cc2aab24399bd14d9a51a", size = 325062, upload-time = "2026-05-14T10:46:18.672Z" },
]
[package.optional-dependencies]
fastapi = [
{ name = "fastapi" },
{ name = "python-multipart" },
]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.15.0" version = "4.15.0"