You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: .cursor/rules/backend/advanced_alchemy_integration.mdc
+81-87Lines changed: 81 additions & 87 deletions
Original file line number
Diff line number
Diff line change
@@ -3,112 +3,106 @@ description:
3
3
globs:
4
4
alwaysApply: false
5
5
---
6
-
# Advanced Alchemy Integration
6
+
# Advanced Alchemy and Litestar Service/Repository/Model Patterns (with msgspec)
7
7
8
8
## Objective
9
-
To ensure robust, maintainable, and efficient database interactions by consistently applying Advanced Alchemy patterns for SQLAlchemy models, repositories, and services within the Litestar application. All Litestar components involving SQLAlchemy (schemas, services with repositories) MUST use constructs found in Advanced Alchemy.
9
+
To ensure robust, maintainable, and efficient database interactions by consistently applying the project's established Advanced Alchemy patterns for SQLAlchemy models, Litestar services (with inner repositories), and mandatory `msgspec.Struct` DTOs. **The patterns found in `UserService`, `TeamService`, and the `User`, `Team`, `Tag` models are the de-facto standard.**
- **Key Concepts**: Declarative Base Models (e.g., `UUIDBase`, `BigIntBase`, `IdentityAuditBase`, `CommonTableAttributes`), Repository Pattern (`SQLAlchemyAsyncRepository`), Service Layer Pattern integration.
15
-
- **Project Files**: Model definitions (`src/py/app/db/models/`), base model setup (e.g., `src/py/app/lib/db/base.py`), service implementations (`src/py/app/services/`), repository implementations (`src/py/app/lib/repositories/` or similar), Litestar DB plugin configuration.
- Services define an inner `Repo` class inheriting `litestar.plugins.sqlalchemy.repository.SQLAlchemyAsyncRepository` (or `...SlugRepository`).
17
+
- Models use Advanced Alchemy base classes (e.g., `UUIDAuditBase`) and mixins (e.g., `SlugKey`, `UniqueMixin`).
18
+
- `msgspec.Struct` is **mandatory** for API DTOs.
19
+
- **Project Files**: Model definitions (`src/py/app/db/models/`), service implementations (`src/py/app/services/`), `msgspec.Struct` definitions (`src/py/app/schemas/`).
16
20
17
21
## Rules
18
22
19
23
### SQLAlchemy Models (`src/py/app/db/models/`)
20
-
- **Base Models**: All SQLAlchemy declarative models MUST inherit from an appropriate Advanced Alchemy base model (e.g., `UUIDBase`, `UUIDAuditBase`, `BigIntPrimaryKey`, `IdentityBase`). Choose the base model that fits the primary key strategy (UUID, BigInt, Identity) and auditing requirements (`AuditColumns`).
21
-
- The project primarily uses UUIDs, so `UUIDBase` or `UUIDAuditBase` (which includes `AuditColumns` and `Timestamps`) are common choices.
24
+
- **Base Models**: All SQLAlchemy declarative models MUST inherit from an appropriate Advanced Alchemy base model (e.g., `UUIDAuditBase` as seen in `User` and `Team`).
25
+
- ✅ `class User(UUIDAuditBase): ...`
26
+
- ✅ `class Team(UUIDAuditBase, SlugKey): ...`
27
+
- **Mixins**: Utilize Advanced Alchemy mixins like `SlugKey` (as in `Team`, `Tag`) and `UniqueMixin` (as in `Tag`) when their functionality is required.
28
+
- `UniqueMixin` is crucial for "get-or-create" M2M scenarios (e.g., tags). It requires implementing `unique_hash` and `unique_filter` methods on the model.
22
29
- ✅
23
30
```python
24
-
# src/py/app/lib/db/base.py (or a similar central location)
25
-
from advanced_alchemy.base import UUIDAuditBase # Recommended for UUID PKs with audit fields
26
-
from sqlalchemy.orm import DeclarativeBase
31
+
# In models/tag.py
32
+
from advanced_alchemy.mixins import UniqueMixin, SlugKey
33
+
from advanced_alchemy.base import UUIDAuditBase
34
+
from advanced_alchemy.utils.text import slugify
35
+
from sqlalchemy import ColumnElement # type: ignore[attr-defined]
email: Mapped[str] = mapped_column(String(255), unique=True) # Example
44
-
# Other fields as needed
45
52
```
46
-
- ❌ Defining models directly from `sqlalchemy.orm.DeclarativeBase` without incorporating Advanced Alchemy base classes or mixins for PKs, audit trails, etc.
47
-
- **Mixins**: Utilize Advanced Alchemy mixins like `AuditColumns`, `SlugKey`, `Timestamps` explicitly if not already included in the chosen base model and if that functionality is desired.
48
-
- Most comprehensive AA bases like `UUIDAuditBase` already include `AuditColumns` and `Timestamps`.
49
-
- **Typing**: Use `sqlalchemy.orm.Mapped` and `mapped_column` for all model attributes for proper type hinting and SQLAlchemy 2.0 compatibility.
50
-
- **Advanced Alchemy Types**: Consider using custom column types from `advanced_alchemy.types` (e.g., `EncryptedString`, `GUID`, `JSONB`) where appropriate.
53
+
- **Typing**: Use `sqlalchemy.orm.Mapped` and `mapped_column` for all model attributes.
54
+
- **Relationships**: Define relationships using `sqlalchemy.orm.relationship`. Prefer `lazy="selectin"` for frequently accessed relationships to avoid N+1 queries, as seen in `User.roles` and `User.teams`.
51
55
52
-
### Repository Pattern (`src/py/app/lib/repositories/` or similar)
53
-
- **`SQLAlchemyAsyncRepository`**: All repository classes MUST inherit from `advanced_alchemy.repository.SQLAlchemyAsyncRepository` (for async operations, which is standard in this project).
56
+
### Service Layer (`src/py/app/services/`)
57
+
- **Base Service Class**: All services MUST inherit from `litestar.plugins.sqlalchemy.service.SQLAlchemyAsyncRepositoryService[ModelType]` (e.g., `UserService(service.SQLAlchemyAsyncRepositoryService[m.User])`).
58
+
- **Inner Repository Class**: Each service MUST define an inner class named `Repo` that inherits from `litestar.plugins.sqlalchemy.repository.SQLAlchemyAsyncRepository[ModelType]` (e.g., `UserService.Repo`) or `SQLAlchemyAsyncSlugRepository[ModelType]` if slug functionality is needed (e.g., `TeamService.Repo`).
59
+
- The service's `repository_type` attribute MUST be set to this inner `Repo` class.
54
60
- ✅
55
61
```python
56
-
from advanced_alchemy.repository import SQLAlchemyAsyncRepository
57
-
from sqlalchemy.ext.asyncio import AsyncSession
58
-
from app.db.models import User # Your SQLAlchemy model
59
-
60
-
class UserRepository(SQLAlchemyAsyncRepository[User]):
61
-
model_type = User
62
-
63
-
# Constructor is usually not needed if only session is passed by DI
from litestar.plugins.sqlalchemy import repository, service
64
+
from app.db import models as m
65
+
66
+
class UserService(service.SQLAlchemyAsyncRepositoryService[m.User]):
67
+
class Repo(repository.SQLAlchemyAsyncRepository[m.User]):
68
+
model_type = m.User
69
+
repository_type = Repo
70
+
# ... rest of the service ...
66
71
```
67
-
- ❌ Implementing custom repository methods for basic CRUD operations (add, get, list, update, delete) already provided by `SQLAlchemyAsyncRepository` unless specific pre/post processing is needed for those basic operations.
68
-
- **Custom Repository Methods**: Add custom query methods to the repository layer for complex data retrieval logic beyond the standard CRUD. These methods should use `self.session` and SQLAlchemy query constructs.
69
-
- **Dependency Injection for Session**: Repositories will receive the `AsyncSession` via dependency injection, managed by Litestar and the Advanced Alchemy plugin.
70
-
71
-
### Service Layer (`src/py/app/services/`)
72
-
- **Purpose**: Business logic, orchestrating calls to one or more repositories, handling DTO transformations.
73
-
- **Repository Usage**: Services MUST use repositories (derived from `SQLAlchemyAsyncRepository`) for all data access.
74
72
- ✅
75
73
```python
76
-
from sqlalchemy.ext.asyncio import AsyncSession
77
-
from litestar.exceptions import NotFoundException # Or your custom exceptions
78
-
from uuid import UUID
79
-
80
-
from app.lib.repositories import UserRepository # Your repository
81
-
from app.db.models import User
82
-
from app.schemas import UserCreateDTO, UserReadDTO # Your Pydantic DTOs
83
-
84
-
class UserService:
85
-
def __init__(self, db_session: AsyncSession): # Session injected by Litestar
async def create_user(self, data: UserCreateDTO) -> User: # Return model or DTO as per convention
89
-
# Business logic before creating, if any
90
-
user = User(**data.model_dump(exclude_unset=True)) # Or a more sophisticated mapping
91
-
created_user = await self._repository.add(user)
92
-
# Business logic after creating, if any (e.g., sending email)
93
-
return created_user # Or map to UserReadDTO
94
-
95
-
async def get_user(self, user_id: UUID) -> User:
96
-
user = await self._repository.get_one_or_none(id=user_id)
97
-
if not user:
98
-
raise NotFoundException(f"User with ID {user_id} not found.")
99
-
return user
100
-
# ... other service methods (update, delete, custom logic)
74
+
# In services/_teams.py
75
+
from litestar.plugins.sqlalchemy import repository, service
76
+
from app.db import models as m
77
+
78
+
class TeamService(service.SQLAlchemyAsyncRepositoryService[m.Team]):
79
+
class Repo(repository.SQLAlchemyAsyncSlugRepository[m.Team]): # Note SlugRepository
80
+
model_type = m.Team
81
+
repository_type = Repo
82
+
# ... rest of the service ...
101
83
```
102
-
- ❌ Services directly using `AsyncSession` to execute queries instead of going through a repository layer.
103
-
- **DTOs**: Services should operate with DTOs for input from controllers and can return DTOs or domain models based on the internal convention. Controllers are then responsible for the final response model mapping if needed.
84
+
- **Data Transformation Hooks**: Utilize `to_model_on_create`, `to_model_on_update`, and `to_model_on_upsert` service hooks for data manipulation and preparation *before* data is passed to the repository. This is where `msgspec.Struct` data from controllers is processed or mapped to model-compatible dictionaries if needed.
85
+
- These hooks often call internal `_populate_*` helper methods within the service (e.g., `_populate_slug`, `_populate_with_hashed_password`).
86
+
- **Mapping `msgspec.Struct` to Model**: Within these hooks, or before calling repository methods, convert incoming `msgspec.Struct` data to a dictionary suitable for model instantiation or repository operations if the service base class doesn't handle `msgspec.Struct` directly. `msgspec.to_builtins(struct_instance)` can be used, or direct field access.
87
+
```python
88
+
# Example within a to_model_on_create hook or service method
89
+
# data: UserCreateStruct (msgspec.Struct)
90
+
# model_data_dict = msgspec.to_builtins(data) # Converts to dict
91
+
# # Now model_data_dict can be used to create a SQLAlchemy model instance
92
+
# # or passed to repository methods that expect dicts.
93
+
# # The SQLAlchemyAsyncRepositoryService might also directly handle attribute access
94
+
# # from msgspec.Struct if field names align.
95
+
```
96
+
- **Business Logic**: Implement specific business logic methods within the service (e.g., `authenticate`, `update_password` in `UserService`; handling M2M tag logic using `Tag.as_unique_async` in `TeamService`).
97
+
- **`msgspec.Struct` for I/O**: Services should expect `msgspec.Struct` instances as input for create/update operations (coming from Litestar controllers) and are responsible for mapping SQLAlchemy model instances to `msgspec.Struct` instances for output if the controller doesn't handle this explicitly.
98
+
99
+
### Litestar Integration (SQLAlchemyPlugin)
100
+
- The `SQLAlchemyPlugin` from `advanced_alchemy.extensions.litestar` MUST be correctly configured in the Litestar application setup (e.g., in `src/py/app/plugins/db.py` or `asgi.py`).
101
+
- This plugin provides the `AsyncSession` for dependency injection into services, which then pass it to their repositories (typically via `self.repository.session`).
104
102
105
-
### Litestar Integration & Schemas
106
-
- **SQLAlchemyPlugin**: The `SQLAlchemyPlugin` from `advanced_alchemy.extensions.litestar` MUST be correctly configured in the Litestar application setup.
107
-
- This plugin provides the `AsyncSession` for dependency injection and manages transaction lifecycles (e.g., `before_send_handler="autocommit"`).
108
-
- **Schemas (`src/py/app/schemas/`)**: Pydantic schemas (DTOs) used for API request/response validation and serialization should align with the SQLAlchemy models.
109
-
- Advanced Alchemy doesn't directly generate Pydantic schemas from models, but Litestar's DTO capabilities or manual Pydantic model definition should be used.
110
-
- When using Litestar DTOs with `DTOConfig(exclude=...)`, ensure all fields related to Advanced Alchemy base models (like `id`, `created_at`, `updated_at`) are handled correctly (included or excluded as intended).
103
+
## Mandatory `msgspec` Usage
104
+
- All Data Transfer Objects (DTOs) used at the API boundary (controller request/response types) MUST be `msgspec.Struct` instances. See `.cursor/rules/domain/data_validation.mdc` for `msgspec.Struct` definition guidelines.
111
105
112
106
## Exceptions
113
-
- Direct use of `AsyncSession` for highly complex or performance-critical queries that are difficult to express via the repository pattern might be permissible in rare, well-justified cases, but this should be an exception, not the rule, and ideally still encapsulated within a repository method.
114
-
- Database seeding scripts (e.g., in `app.cli.commands.seed`) can directly use repositories and sessions for bulk operations, following Advanced Alchemy examples if applicable.
107
+
- Alembic migration scripts operate directly with SQLAlchemy Core/ORM and do not involve services or `msgspec` DTOs.
108
+
- CLI commands might interact directly with services, but their input parsing is distinct from API DTO validation.
0 commit comments