diff --git a/CLAUDE.md b/CLAUDE.md index afe75ae..41dab9f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,9 +2,14 @@ ## 项目定位 -sqlmodel-nexus:从 SQLModel 类自动生成 GraphQL API,并提供 Core API 模式构建用例响应的 Python 库。 -- **GraphQL 模式**:SDL 自动生成 + DataLoader 批量关系加载 + MCP 服务集成 -- **Core API 模式**:DefineSubset DTO + ErManager + Resolver 模型驱动的响应构建 +sqlmodel-nexus:从 SQLModel 实体模型出发,自动生成 GraphQL / REST / MCP 三种 API 的 Python 库。 + +核心理念:定义一次数据模型,获得三种 API 输出,N+1 查询自动防护。 + +三条使用路径: +- **GraphQL 模式**:在实体上写 `@query`/`@mutation` → 自动生成 SDL → GraphQLHandler 执行查询 +- **Core API 模式**:`DefineSubset` DTO → `ErManager` + `Resolver` → 构建 REST 响应 / 业务 DTO +- **UseCase 模式**:`UseCaseService` 业务类 → 同时输出 MCP(AI Agent)和 FastAPI(REST) ## 技术栈 diff --git a/docs/advanced/mcp_service.md b/docs/advanced/mcp_service.md index b5f4c3d..b5f04aa 100644 --- a/docs/advanced/mcp_service.md +++ b/docs/advanced/mcp_service.md @@ -2,6 +2,10 @@ Expose SQLModel APIs to AI agents — create an MCP service with a single line of code. +> **Prerequisites**: SQLModel entities with `@query`/`@mutation` methods and a `session_factory`. This path covers the **GraphQL→MCP** bridge — for the **UseCase→MCP** path see [UseCase Service](./use_case_service.md). +> +> **Live demo**: [`demo/mcp_server_simple.py`](https://github.com/allmonday/sqlmodel-nexus/blob/master/demo/mcp_server_simple.py) (single-app) and [`demo/mcp_server.py`](https://github.com/allmonday/sqlmodel-nexus/blob/master/demo/mcp_server.py) (multi-app). + ## Installation ```bash diff --git a/docs/advanced/use_case_fastapi.md b/docs/advanced/use_case_fastapi.md index 52857a5..2875cf9 100644 --- a/docs/advanced/use_case_fastapi.md +++ b/docs/advanced/use_case_fastapi.md @@ -2,6 +2,10 @@ Using the same UseCaseService class in FastAPI — routes are thin wrappers, business logic stays in the service. +> **Prerequisites**: [UseCase Service](./use_case_service.md). You should have a UseCaseService subclass with `@query`/`@mutation` methods before wiring it into FastAPI. +> +> **Prerequisite demo**: [`demo/use_case/mcp_server.py`](https://github.com/allmonday/sqlmodel-nexus/blob/master/demo/use_case/mcp_server.py) — defines the services. [`demo/use_case/fastapi.py`](https://github.com/allmonday/sqlmodel-nexus/blob/master/demo/use_case/fastapi.py) — the same services as REST. + ## Route Definitions ```python diff --git a/docs/advanced/use_case_service.md b/docs/advanced/use_case_service.md index e0934c4..4bdd9aa 100644 --- a/docs/advanced/use_case_service.md +++ b/docs/advanced/use_case_service.md @@ -2,6 +2,10 @@ UseCaseService lets you define business logic as service classes that serve both MCP and web frameworks — one definition, two presentations. +> **Prerequisites**: [Core API Mode](../guide/core_api.md) — ErManager + DefineSubset + Resolver. UseCaseService wraps Core API DTOs into business methods. +> +> **Live demo**: See [`demo/use_case/mcp_server.py`](https://github.com/allmonday/sqlmodel-nexus/blob/master/demo/use_case/mcp_server.py) for a working MCP server — and [`demo/use_case/fastapi.py`](https://github.com/allmonday/sqlmodel-nexus/blob/master/demo/use_case/fastapi.py) for the same services exposed as REST. + ## Design Philosophy ``` diff --git a/docs/advanced/voyager.md b/docs/advanced/voyager.md index df9716f..dbcc4d7 100644 --- a/docs/advanced/voyager.md +++ b/docs/advanced/voyager.md @@ -2,6 +2,8 @@ sqlmodel-nexus includes a built-in Voyager module that provides interactive UseCase service graphs and ER entity relationship visualization. +> **Prerequisites**: [UseCase Service](./use_case_service.md) (for service graph) and optionally [Core API Mode](../guide/core_api.md) (for ER diagram integration via `er_manager`). + ## create_use_case_voyager ```python diff --git a/docs/guide/core_api.md b/docs/guide/core_api.md index 132d4d4..3001754 100644 --- a/docs/guide/core_api.md +++ b/docs/guide/core_api.md @@ -2,7 +2,11 @@ The Core API mode is for scenarios beyond GraphQL — FastAPI REST endpoints, service layer response assembly, or any use-case DTO. Same DataLoader batch loading, same N+1 prevention. -Core concepts progress in order: **Implicit auto-loading → resolve_\* → post_\* → Cross-layer data flow**. +> **Prerequisites**: SQLModel entities defined with `Relationship` / `Field(foreign_key=...)`. +> +> **Live demo**: See [`demo/core_api/`](https://github.com/allmonday/sqlmodel-nexus/tree/master/demo/core_api) for a complete REST server with Sprint/Task/User models. The DTOs in [`dtos.py`](https://github.com/allmonday/sqlmodel-nexus/blob/master/demo/core_api/dtos.py) progress through 5 levels of complexity. + +Core concepts progress in order: **Implicit auto-loading → resolve\_\* → post\_\* → Cross-layer data flow**. ## Step 1: DefineSubset + Implicit Auto-Loading @@ -24,6 +28,8 @@ class SprintDTO(DefineSubset): tasks: list[TaskDTO] = [] # Name matches Sprint.tasks relationship → auto-loaded ``` +> ⬆ This is **Level 2** in the demo's [`dtos.py`](https://github.com/allmonday/sqlmodel-nexus/blob/master/demo/core_api/dtos.py#L30). + ## ErManager Initialization ```python @@ -48,49 +54,23 @@ app = FastAPI() async def get_sprints(): async with async_session() as session: sprints = (await session.exec(select(Sprint))).all() - dtos = [SprintDTO(id=s.id, name=s.name) for s in sprints] - return await Resolver().resolve(dtos) + dtos = [SprintDTO(**s.model_dump()) for s in sprints] + return await Resolver().resolve(dtos) # tasks + owner auto-loaded ``` -## Four Conditions for Implicit Auto-Loading - -The Resolver automatically loads relationship fields (no need to write `resolve_*`) when all conditions are met: - -1. The field has no corresponding `resolve_*` method -2. The field is an extra field (not in the `__subset__` definition) -3. The field name matches a registered ORM/custom relationship -4. The field type is a BaseModel DTO compatible with the relationship target entity +> **Tip**: For more efficient field-selective queries, use `build_dto_select(SprintDTO)` — it generates a SQLAlchemy select() that only fetches the fields declared in `__subset__`. See [`demo/core_api/dtos.py` → `TaskService.list_tasks`](https://github.com/allmonday/sqlmodel-nexus/blob/master/demo/use_case/mcp_server.py#L63). -## DefineSubset Rules +## How Auto-Loading Works -- `__subset__` accepts a tuple `(Entity, ('field1', 'field2'))` -- FK fields (e.g., `owner_id`) are automatically hidden from serialization output, but remain internally accessible in `resolve_*` -- Relationship fields are declared in the class body (not in `__subset__`), and must use DTO types - -## How It Works - -``` -SprintDTO(id=1, name="Sprint 1") - → Implicit auto-load: tasks → [TaskDTO(...), TaskDTO(...)] - → Each TaskDTO: Implicit auto-load: owner → UserDTO(...) - → Result: complete nested response tree -``` +Implicit auto-loading triggers when **all** conditions are true: -Each relationship executes only one DataLoader query, regardless of how many Sprints or Tasks exist. - -## DTO Type Constraint - -```python -# Wrong — direct use of SQLModel entity is prohibited -class TaskDTO(DefineSubset): - owner: User | None = None # TypeError! - -# Correct — use DTO type -class TaskDTO(DefineSubset): - owner: UserDTO | None = None # OK -``` +1. The field has no corresponding `resolve_*` method +2. The field is an extra field (not in `__subset__` fields) +3. The field name matches a registered ORM or custom relationship +4. The field type is a DefineSubset DTO compatible with the relationship target ## Next Steps -- [Core API Advanced](./core_api_advanced.md) — resolve_*/post_*/cross-layer data flow -- [Custom Relationships](./custom_relationship.md) — Non-ORM relationship declarations +- [Core API Advanced](./core_api_advanced.md) — resolve_*, post_*, cross-layer data flow +- [Custom Relationships](./custom_relationship.md) — non-ORM relationships +- [UseCase Service](../advanced/use_case_service.md) — wrap Core API DTOs in business services diff --git a/docs/guide/core_api_advanced.md b/docs/guide/core_api_advanced.md index 7b2b1a5..20b224e 100644 --- a/docs/guide/core_api_advanced.md +++ b/docs/guide/core_api_advanced.md @@ -2,6 +2,10 @@ When implicit auto-loading is not enough, Core API provides three progressive capabilities: `resolve_*` for custom loading, `post_*` for derived field computation, and cross-layer data flow. +> **Prerequisites**: [Core API Mode](./core_api.md) — specifically DefineSubset, ErManager, and implicit auto-loading. +> +> **Live demo**: The advanced concepts below correspond to Levels 3–5 in [`demo/core_api/dtos.py`](https://github.com/allmonday/sqlmodel-nexus/blob/master/demo/core_api/dtos.py). + ## resolve_*: Custom Loading When the field name doesn't match a relationship, or custom logic is needed, use `resolve_*`: @@ -58,6 +62,8 @@ class SprintDTO(DefineSubset): return sorted({t.owner.name for t in self.tasks if t.owner}) ``` +> ⬆ This is **Level 3** in the demo: [`SprintSummary`](https://github.com/allmonday/sqlmodel-nexus/blob/master/demo/core_api/dtos.py#L53). + Execution order: 1. Implicit auto-loading → `tasks` populated with TaskDTO list @@ -78,60 +84,72 @@ Execution order: Use when parent and child nodes need cross-layer collaboration. Only necessary when the tree structure truly matters. -### ExposeAs: Ancestor → Descendant +**Key tools**: -```python -from typing import Annotated -from sqlmodel_nexus import ExposeAs +- `ExposeAs`: Parent exposes a value to all descendants via `ancestor_context` +- `SendTo`: Child sends a value upward to ancestor Collector +- `Collector`: Aggregates all values sent via SendTo -class SprintDTO(DefineSubset): - __subset__ = (Sprint, ("id", "name")) - name: Annotated[str, ExposeAs('sprint_name')] # Expose to descendants - tasks: list[TaskDTO] = [] -``` +```python +from sqlmodel_nexus import Collector, DefineSubset, SubsetConfig -### SendTo + Collector: Descendant → Ancestor +class TaskDTO(DefineSubset): + __subset__ = SubsetConfig( + kls=Task, fields=["id", "title"], + send_to=[("owner", "contributors")], # Send owner to ancestor collector + ) + owner: UserDTO | None = None -```python -from sqlmodel_nexus import SendTo, Collector + def post_full_title(self, ancestor_context=None): + sprint_name = (ancestor_context or {}).get("sprint_name", "unknown") + return f"{sprint_name} / {self.title}" class SprintDTO(DefineSubset): - __subset__ = (Sprint, ("id", "name")) - name: Annotated[str, ExposeAs('sprint_name')] + __subset__ = SubsetConfig( + kls=Sprint, fields=["id", "name"], + expose_as=[("name", "sprint_name")], # Expose name to descendants + ) tasks: list[TaskDTO] = [] contributors: list[UserDTO] = [] - def post_contributors(self, collector=Collector('contributors')): - return collector.values() # Collect values sent by descendants - -class TaskDTO(DefineSubset): - __subset__ = (Task, ("id", "title", "owner_id")) - owner: Annotated[UserDTO | None, SendTo('contributors')] = None # Send to ancestor - full_title: str = "" - - def post_full_title(self, ancestor_context): - return f"{ancestor_context['sprint_name']} / {self.title}" + def post_contributors(self, collector=Collector("contributors")): + return collector.values() ``` -Applicable scenarios: +> ⬆ This is **Level 4** in the demo: [`SprintDetail`](https://github.com/allmonday/sqlmodel-nexus/blob/master/demo/core_api/dtos.py#L107). -- Child nodes need ancestor context (sprint name, permission info, tenant configuration) -- Parent nodes need to aggregate results from multiple descendants (contributors, tags) +## Custom Non-ORM Relationships -## Resolver Options +For data sources that don't use ORM relationships (external APIs, caches, computed edges), declare custom `Relationship` on SQLModel entities with a hand-written async loader: ```python -result = await Resolver( - context={"user_id": 42}, # Pass global context - loader_params={}, # DataLoader extra parameters -).resolve(dtos) +from sqlmodel_nexus import Relationship as CustomRelationship + +async def tags_loader(task_ids: list[int]) -> list[list[Tag]]: + async with get_session() as session: + stmt = select(Tag, TaskTag.task_id).join(TaskTag).where(TaskTag.task_id.in_(task_ids)) + rows = (await session.exec(stmt)).all() + return build_list(rows, task_ids, lambda r: r[1], lambda r: r[0]) + +class Task(SQLModel, table=True): + __relationships__ = [ + CustomRelationship(fk="id", target=list[Tag], name="tags", loader=tags_loader) + ] ``` -## Loader Dependency Name Rule +DTO auto-loads custom relationships the same way as ORM ones: + +```python +class TaskDTO(DefineSubset): + __subset__ = (Task, ("id", "title")) + tags: list[TagDTO] = [] # Auto-loaded from the custom relationship +``` -`Loader('author')` requires a relationship named `author` in ErManager. When using implicit auto-loading, you typically don't need to write Loaders manually. +> ⬆ This is **Level 5** in the demo: [`TaskWithTags`](https://github.com/allmonday/sqlmodel-nexus/blob/master/demo/core_api/dtos.py#L140). -## Next Steps +## Execution Order (Full) -- [Custom Relationships](./custom_relationship.md) — Non-ORM relationship declarations -- [MCP Service](../advanced/mcp_service.md) — Expose APIs to AI agents +1. Execute all `resolve_*` methods on current node (load relationship data) +2. Traverse existing object/relationship fields recursively (depth-first) +3. Execute all `post_*` methods on current node (compute derived fields) +4. Collect SendTo values upward to ancestor Collectors diff --git a/docs/guide/graphql_mode.md b/docs/guide/graphql_mode.md index d269066..3c89705 100644 --- a/docs/guide/graphql_mode.md +++ b/docs/guide/graphql_mode.md @@ -2,6 +2,10 @@ From SQLModel entities to a complete GraphQL API — SDL auto-generation, automatic relationship resolution, and DataLoader batch loading. +> **Prerequisites**: You should be familiar with [SQLModel](https://sqlmodel.tiangolo.com/) entity definitions (SQLModel + Field + Relationship). +> +> **Live demo**: See [`demo/app.py`](https://github.com/allmonday/sqlmodel-nexus/blob/master/demo/app.py) for a working GraphQL server (User/Post/Comment). + ## GraphQLHandler Configuration ```python diff --git a/docs/guide/quick_start.md b/docs/guide/quick_start.md index 0c29cb8..e43fee7 100644 --- a/docs/guide/quick_start.md +++ b/docs/guide/quick_start.md @@ -1,103 +1,138 @@ # Quick Start -From SQLModel entities to a working GraphQL API in 30 seconds. +Pick your path — three ways to use sqlmodel-nexus from the same SQLModel entities. -## Installation +All paths assume you have: ```bash pip install sqlmodel-nexus ``` -## 1. Define SQLModel Entities +--- + +## 🟣 Path 1: GraphQL API + +Define entities, add `@query` / `@mutation`, create a `GraphQLHandler` — done. ```python from sqlmodel import SQLModel, Field, Relationship, select +from sqlmodel_nexus import query, mutation, GraphQLHandler class User(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) name: str - email: str posts: list["Post"] = Relationship(back_populates="author") + @query + async def get_all(cls, limit: int = 10) -> list["User"]: + async with get_session() as session: + return (await session.exec(select(cls).limit(limit))).all() + class Post(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) title: str author_id: int = Field(foreign_key="user.id") author: User | None = Relationship(back_populates="posts") -``` - -## 2. Add @query and @mutation - -```python -from sqlmodel_nexus import query, mutation - -class Post(SQLModel, table=True): - # ... fields as above ... - - @query - async def get_all(cls, limit: int = 10) -> list['Post']: - """Get all posts.""" - async with get_session() as session: - return (await session.exec(select(cls).limit(limit))).all() @mutation - async def create(cls, title: str, author_id: int) -> 'Post': - """Create a new post.""" + async def create(cls, title: str, author_id: int) -> "Post": async with get_session() as session: post = cls(title=title, author_id=author_id) session.add(post) await session.commit() await session.refresh(post) return post + +handler = GraphQLHandler(base=SQLModel, session_factory=async_session) ``` -## 3. Create GraphQLHandler +```graphql +# Query +{ userGetAll(limit: 5) { id name posts { title } } } -```python -from fastapi import FastAPI -from fastapi.responses import HTMLResponse -from pydantic import BaseModel -from sqlmodel_nexus import GraphQLHandler +# Mutation +mutation { postCreate(title: "Hello", authorId: 1) { id title } } +``` -handler = GraphQLHandler(base=SQLModel, session_factory=async_session) +📖 Learn more: [GraphQL Mode](./guide/graphql_mode.md) -class GraphQLRequest(BaseModel): - query: str +--- -app = FastAPI() +## 🟢 Path 2: REST API (Core API) -@app.get("/graphql", response_class=HTMLResponse) -async def graphiql(): - return handler.get_graphiql_html() +Define `DefineSubset` DTOs — relationship fields auto-load via DataLoader. -@app.post("/graphql") -async def graphql(req: GraphQLRequest): - return await handler.execute(req.query) +```python +from sqlmodel_nexus import DefineSubset, ErManager + +# 1. Define DTOs +class UserDTO(DefineSubset): + __subset__ = (User, ("id", "name")) + +class PostDTO(DefineSubset): + __subset__ = (Post, ("id", "title", "author_id")) + author: UserDTO | None = None # auto-loaded! No resolve_* needed + +# 2. Create Resolver +er = ErManager(base=SQLModel, session_factory=async_session) +Resolver = er.create_resolver() + +# 3. In your FastAPI endpoint +@app.get("/posts") +async def get_posts(): + posts = (await session.exec(select(Post).limit(10))).all() + dtos = [PostDTO(**p.model_dump()) for p in posts] # or use build_dto_select + return await Resolver().resolve(dtos) # resolves author automatically ``` -## 4. Run and Query +Output JSON: -```bash -uvicorn app:app -# Visit http://localhost:8000/graphql +```json +[ + { "id": 1, "title": "Hello", "author": { "id": 1, "name": "Alice" } }, + { "id": 2, "title": "World", "author": { "id": 2, "name": "Bob" } } +] ``` -```graphql -{ - postGetAll(limit: 5) { - id - title - author { name email } - } -} -``` +📖 Learn more: [Core API Mode](./guide/core_api.md) -**Automatic relationship resolution**: The framework traverses the GraphQL selection tree, collects FK values layer by layer, and batch-loads relationships via DataLoader. No matter how many records are returned, each relationship requires only one query. +--- -## Core Mental Model +## 🟡 Path 3: MCP for AI Agents (UseCase) +Write business logic once — serve both MCP and FastAPI from the same `UseCaseService` class. + +```python +from sqlmodel_nexus import UseCaseService, UseCaseAppConfig, create_use_case_mcp_server + +class PostService(UseCaseService): + @query + async def list_posts(cls, limit: int = 10) -> list[PostDTO]: + stmt = build_dto_select(PostDTO) + async with async_session() as session: + rows = (await session.exec(stmt)).all() + dtos = [PostDTO(**dict(row._mapping)) for row in rows] + return await Resolver().resolve(dtos) + +mcp = create_use_case_mcp_server(apps=[ + UseCaseAppConfig(name="blog", services=[PostService]), +]) +mcp.run() # AI agents can: list_apps → list_services → describe_service → call ``` -SQLModel entities + @query decorators → GraphQL API (SDL + DataLoader auto-generated) -``` -Next, learn about the full capabilities of [GraphQL Mode](./graphql_mode.md). +📖 Learn more: [UseCase Service](./advanced/use_case_service.md) | [FastAPI Integration](./advanced/use_case_fastapi.md) + +--- + +## What's Next + +| Topic | Guide | +|-------|-------| +| How DTOs auto-load relationships | [Core API Mode](./guide/core_api.md) | +| Derived fields via `post_*` | [Core API Advanced](./guide/core_api_advanced.md) | +| Pagination in GraphQL | [GraphQL Pagination](./guide/graphql_pagination.md) | +| Non-ORM relationships | [Custom Relationships](./guide/custom_relationship.md) | +| Five-level progressive demo | [`demo/core_api/dtos.py`](../demo/core_api/dtos.py) | +| Full project template | [`skill/template/`](../skill/template/) | + +The remaining guides and API references are listed in the [index](index.md). diff --git a/docs/index.md b/docs/index.md index bce9d81..7134d8c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,72 +4,132 @@ template: home.html # sqlmodel-nexus -**sqlmodel-nexus** is a progressive SQLModel extension library. Start from ORM entities, extend with non-ORM relationships, auto-generate GraphQL APIs, and use `DefineSubset` to declaratively build response DTOs. Visualize entity relationships and data flows through ER diagrams. +**Define your SQLModel entities once. Get GraphQL, REST, and MCP APIs — with N+1 protection built in.** -## What sqlmodel-nexus Solves +```mermaid +flowchart LR + M[SQLModel Entities] --> G[GraphQL API] + M --> R[REST API] + M --> MCP[MCP for AI Agents] + style M fill:#4a90d9,color:#fff + style G fill:#e535ab,color:#fff + style R fill:#22c55e,color:#fff + style MCP fill:#f59e0b,color:#fff +``` -| Need | What You Write | What the Framework Handles | -|------|----------------|---------------------------| -| GraphQL API | `@query` / `@mutation` decorators | Auto-generate SDL, DataLoader batch-loading relationships | -| REST / Use-case DTOs | `DefineSubset` + field declarations | Implicit auto-loading, N+1 prevention, ORM→DTO conversion | -| Derived fields | `post_*` methods | Auto-execute after nested data is ready | -| Cross-layer data flow | `ExposeAs`, `SendTo`, `Collector` | Pass context downward or aggregate results upward | -| Non-ORM relationships | `Relationship(...)` | Same DataLoader infrastructure, supports auto-loading | -| AI-ready API | `config_simple_mcp_server(base=...)` | Progressive MCP tool exposure | +--- -## Use Cases +## Pick Your Path -- **Backend developers**: Quickly build GraphQL and REST APIs from SQLModel entities -- **Teams**: Auto-generate APIs after models stabilize, reducing hand-written schemas -- **Projects**: Support both GraphQL for validation and REST for delivery -- **AI integration**: Expose the same models to AI agents via MCP +| | GraphQL | REST (Core API) | MCP / UseCase | +|---|---------|-----------------|---------------| +| **What you get** | Auto-generated SDL + DataLoader-executed GraphQL endpoint | Pure Pydantic DTOs assembled via Resolver tree traversal | Four-layer progressive MCP tools for AI agents | +| **Key API** | `@query` / `@mutation` + `GraphQLHandler` | `DefineSubset` + `ErManager` + `Resolver` | `UseCaseService` + `create_use_case_mcp_server` | +| **Best for** | Frontend apps needing flexible queries | REST APIs, service-layer DTOs | AI agent integration (Claude, Cursor, etc.) | +| **Demo** | [`demo/app.py`](https://github.com/allmonday/sqlmodel-nexus/blob/master/demo/app.py) | [`demo/core_api/`](https://github.com/allmonday/sqlmodel-nexus/blob/master/demo/core_api/) | [`demo/use_case/`](https://github.com/allmonday/sqlmodel-nexus/blob/master/demo/use_case/) | +| **Quick Start** | ↓ [GraphQL Quick Start](#graphql-path) | ↓ [Core API Quick Start](#core-api-path) | ↓ [UseCase Quick Start](#usecase-path) | -## Learning Path +--- -```mermaid -flowchart LR - p1["P1: ER Diagram
SQLModel entities + non-ORM relationships
+ visualized ER diagram"] - --> p2["P2: GraphQL API
@query / @mutation
SDL auto-generation + DataLoader"] - --> p3["P3: Core API
DefineSubset DTOs
Implicit auto-loading + post_*"] - --> p4["MCP / UseCase
AI agents + business services"] + + +## 🟣 Path 1: GraphQL API + +Mark entity methods with `@query` / `@mutation`, get a full GraphQL schema + DataLoader: + +```python +from sqlmodel import SQLModel, Field, Relationship, select +from sqlmodel_nexus import query, mutation, GraphQLHandler + +class User(SQLModel, table=True): + id: int | None = Field(default=None, primary_key=True) + name: str + posts: list["Post"] = Relationship(back_populates="author") + + @query + async def get_users(cls, limit: int = 10) -> list["User"]: + async with get_session() as session: + return (await session.exec(select(cls).limit(limit))).all() + +handler = GraphQLHandler(base=SQLModel, session_factory=async_session) +# handler.execute("{ userGetAll(limit: 5) { id name posts { title } } }") ``` -The guide reuses the same business scenario: +📖 Full guide: [GraphQL Mode](./guide/graphql_mode.md) · [Pagination](./guide/graphql_pagination.md) · [Auto Query](./guide/graphql_auto_query.md) -```mermaid -erDiagram - Sprint ||--o{ Task : "has many" - Task }o--|| User : "owner" +--- + + + +## 🟢 Path 2: REST API (Core API) + +Define `DefineSubset` DTOs — relationship fields auto-load via DataLoader. No SQL injection, no N+1: + +```python +from sqlmodel_nexus import DefineSubset, ErManager + +class UserDTO(DefineSubset): + __subset__ = (User, ("id", "name")) + +class TaskDTO(DefineSubset): + __subset__ = (Task, ("id", "title", "owner_id")) + owner: UserDTO | None = None # Auto-loaded! + +er = ErManager(base=SQLModel, session_factory=async_session) +Resolver = er.create_resolver() +result = await Resolver().resolve(dtos) # Tree traversal resolves all relationships +``` + +📖 Full guide: [Core API Mode](./guide/core_api.md) · [Advanced](./guide/core_api_advanced.md) · [Custom Relationships](./guide/custom_relationship.md) + +--- + + + +## 🟡 Path 3: MCP for AI Agents (UseCase) + +Write business logic once in `UseCaseService` — serve both MCP and FastAPI from the same class: + +```python +from sqlmodel_nexus import UseCaseService, UseCaseAppConfig, create_use_case_mcp_server + +class SprintService(UseCaseService): + @query + async def list_sprints(cls) -> list[SprintSummary]: + dtos = [SprintSummary(**dict(row._mapping)) for row in rows] + return await Resolver().resolve(dtos) + +mcp = create_use_case_mcp_server(apps=[ + UseCaseAppConfig(name="project", services=[SprintService], ...), +]) +mcp.run() # Exposes 4-layer MCP tools: list_apps → list_services → describe_service → call ``` -### Guide (Tutorial Path) - -| Page | Main Question Answered | -|------|------------------------| -| [Quick Start](./guide/quick_start.md) | How to get a GraphQL API running with minimal code? | -| [ER Diagram & Non-ORM Relationships](./guide/er_diagram.md) | How to declare and visualize entity relationships? | -| [GraphQL Mode](./guide/graphql_mode.md) | What is the full workflow from SQLModel to GraphQL API? | -| [GraphQL Pagination](./guide/graphql_pagination.md) | How to paginate list relationships? | -| [Auto Query](./guide/graphql_auto_query.md) | How to skip @query and auto-generate by_id / by_filter? | -| [Core API Mode](./guide/core_api.md) | How do DefineSubset + implicit auto-loading work? | -| [Core API Advanced](./guide/core_api_advanced.md) | How to use resolve_* / post_* / cross-layer data flow? | -| [Custom Relationships](./guide/custom_relationship.md) | How to declare and use non-ORM relationships? | -| [ER Diagram Visualization](./guide/er_diagram_visual.md) | How to generate and embed Mermaid ER diagrams? | - -### Advanced Guides - -| Page | Topic | -|------|-------| -| [MCP Service](./advanced/mcp_service.md) | Expose SQLModel APIs to AI agents | -| [UseCase Service](./advanced/use_case_service.md) | Define business services serving both MCP and REST | -| [UseCase + FastAPI](./advanced/use_case_fastapi.md) | Embed the same service class into FastAPI routes | -| [Voyager Visualization](./advanced/voyager.md) | Interactive ERD browsing | - -### API Reference - -- [GraphQLHandler](./api/api_graphql_handler.md) — GraphQL entry point + SDL generation -- [Core API](./api/api_core.md) — ErManager / Resolver / DefineSubset / Loader -- [Cross-layer Data Flow](./api/api_cross_layer.md) — ExposeAs / SendTo / Collector -- [Relationships & ER Diagram](./api/api_relationship.md) — Relationship / ErDiagram -- [MCP API](./api/api_mcp.md) — MCP service configuration -- [UseCase API](./api/api_use_case.md) — UseCaseService / create_use_case_mcp_server +📖 Full guide: [UseCase Service](./advanced/use_case_service.md) · [FastAPI](./advanced/use_case_fastapi.md) · [Voyager](./advanced/voyager.md) · [MCP Service](./advanced/mcp_service.md) + +--- + +## Complete Tutorial + +For a step-by-step walkthrough with the same Sprint/Task model used across all demos, see [`demo/core_api/`](https://github.com/allmonday/sqlmodel-nexus/blob/master/demo/core_api/). The DTOs progress through 5 levels of complexity: + +| Level | What | File | +|-------|------|------| +| 1 | Basic field selection + FK hiding | [`dtos.py` → `UserSummary`](https://github.com/allmonday/sqlmodel-nexus/blob/master/demo/core_api/dtos.py#L21) | +| 2 | Implicit relationship auto-loading | [`dtos.py` → `TaskSummary`](https://github.com/allmonday/sqlmodel-nexus/blob/master/demo/core_api/dtos.py#L30) | +| 3 | `post_*` derived field computation | [`dtos.py` → `SprintSummary`](https://github.com/allmonday/sqlmodel-nexus/blob/master/demo/core_api/dtos.py#L53) | +| 4 | Cross-layer data flow (`ExposeAs` / `SendTo` / `Collector`) | [`dtos.py` → `SprintDetail`](https://github.com/allmonday/sqlmodel-nexus/blob/master/demo/core_api/dtos.py#L107) | +| 5 | Custom non-ORM relationships | [`dtos.py` → `TaskWithTags`](https://github.com/allmonday/sqlmodel-nexus/blob/master/demo/core_api/dtos.py#L140) | + +--- + +## API Reference + +| Module | Key exports | +|--------|-------------| +| [GraphQLHandler](./api/api_graphql_handler.md) | `GraphQLHandler`, `SDLGenerator`, `AutoQueryConfig` | +| [Core API](./api/api_core.md) | `ErManager`, `Resolver`, `DefineSubset`, `SubsetConfig`, `Loader` | +| [Cross-layer Data Flow](./api/api_cross_layer.md) | `ExposeAs`, `SendTo`, `Collector` | +| [Relationships & ER Diagram](./api/api_relationship.md) | `Relationship`, `ErDiagram` | +| [MCP API](./api/api_mcp.md) | `config_simple_mcp_server`, `create_mcp_server` | +| [UseCase API](./api/api_use_case.md) | `UseCaseService`, `UseCaseAppConfig`, `create_use_case_mcp_server`, `create_use_case_router` | diff --git a/src/sqlmodel_nexus/__init__.py b/src/sqlmodel_nexus/__init__.py index 5897082..08ca391 100644 --- a/src/sqlmodel_nexus/__init__.py +++ b/src/sqlmodel_nexus/__init__.py @@ -1,12 +1,21 @@ -"""SQLModel Nexus - GraphQL SDL generation and Core API response building. - -This package provides: -- Automatic GraphQL SDL generation from SQLModel classes -- @query/@mutation decorators for defining GraphQL operations -- DataLoader-based relationship resolution -- Per-relationship pagination support -- DefineSubset for creating independent DTO models from SQLModel entities -- ErManager for entity-relationship management and Resolver creation +"""sqlmodel-nexus — from SQLModel entities to GraphQL / REST / MCP APIs. + +Define your data model once as SQLModel entities; get three API outputs +automatically with built-in N+1 prevention via DataLoader batch loading. + +┌─ GraphQL mode ─────────── @query/@mutation → SDL auto-generation → GraphQLHandler +├─ Core API (REST) ──────── DefineSubset DTO → ErManager → Resolver → FastAPI +└─ UseCase (MCP) ────────── UseCaseService → create_use_case_mcp_server → AI agents + +Core capabilities: +- @query/@mutation decorators: mark entity methods as GraphQL operations +- SDLGenerator + GraphQLHandler: auto-generate SDL and execute queries +- DataLoader batch loading: per-relationship, N+1-proof, optional pagination +- DefineSubset: create pure Pydantic DTOs from SQLModel entities +- ErManager + Resolver: model-driven tree traversal with implicit auto-loading +- UseCaseService: one business logic class, two outputs (MCP + FastAPI) +- Relationship: declare non-ORM relationships with custom DataLoaders +- ErDiagram + Voyager: visualize entity graphs and service topologies Example (GraphQL mode): ```python