mirror of
https://github.com/d3vyce/fastapi-toolsets.git
synced 2026-04-16 14:46:24 +02:00
* chore: remove deprecated code * docs: update v3 migration guide * fix: pytest warnings * Version 3.0.0 * fix: docs workflows
181 lines
6.5 KiB
Markdown
181 lines
6.5 KiB
Markdown
# Migrating to v3.0
|
|
|
|
This page covers every breaking change introduced in **v3.0** and the steps required to update your code.
|
|
|
|
---
|
|
|
|
## CRUD
|
|
|
|
### Facet keys now always use the full relationship chain
|
|
|
|
In `v2`, relationship facet fields used only the terminal column key (e.g. `"name"` for `Role.name`) and only prepended the relationship name when two facet fields shared the same column key. In `v3`, facet keys **always** include the full relationship chain joined by `__`, regardless of collisions.
|
|
|
|
=== "Before (`v2`)"
|
|
|
|
```
|
|
User.status -> status
|
|
(User.role, Role.name) -> name
|
|
(User.role, Role.permission, Permission.name) -> name
|
|
```
|
|
|
|
=== "Now (`v3`)"
|
|
|
|
```
|
|
User.status -> status
|
|
(User.role, Role.name) -> role__name
|
|
(User.role, Role.permission, Permission.name) -> role__permission__name
|
|
```
|
|
|
|
---
|
|
|
|
### `*_params` dependencies consolidated into per-paginate methods
|
|
|
|
The six individual dependency methods (`offset_params`, `cursor_params`, `paginate_params`, `filter_params`, `search_params`, `order_params`) have been **removed** and replaced by three consolidated methods that bundle pagination, search, filter, and order into a single `Depends()` call.
|
|
|
|
| Removed | Replacement |
|
|
|---|---|
|
|
| `offset_params()` + `filter_params()` + `search_params()` + `order_params()` | `offset_paginate_params()` |
|
|
| `cursor_params()` + `filter_params()` + `search_params()` + `order_params()` | `cursor_paginate_params()` |
|
|
| `paginate_params()` + `filter_params()` + `search_params()` + `order_params()` | `paginate_params()` |
|
|
|
|
Each new method accepts `search`, `filter`, and `order` boolean toggles (all `True` by default) to disable features you don't need.
|
|
|
|
=== "Before (`v2`)"
|
|
|
|
```python
|
|
from fastapi_toolsets.crud import OrderByClause
|
|
|
|
@router.get("/offset")
|
|
async def list_articles_offset(
|
|
session: SessionDep,
|
|
params: Annotated[dict, Depends(ArticleCrud.offset_params(default_page_size=20))],
|
|
filter_by: Annotated[dict, Depends(ArticleCrud.filter_params())],
|
|
order_by: Annotated[OrderByClause | None, Depends(ArticleCrud.order_params(default_field=Article.created_at))],
|
|
search: str | None = None,
|
|
) -> OffsetPaginatedResponse[ArticleRead]:
|
|
return await ArticleCrud.offset_paginate(
|
|
session=session,
|
|
**params,
|
|
search=search,
|
|
filter_by=filter_by or None,
|
|
order_by=order_by,
|
|
schema=ArticleRead,
|
|
)
|
|
```
|
|
|
|
=== "Now (`v3`)"
|
|
|
|
```python
|
|
@router.get("/offset")
|
|
async def list_articles_offset(
|
|
session: SessionDep,
|
|
params: Annotated[
|
|
dict,
|
|
Depends(
|
|
ArticleCrud.offset_paginate_params(
|
|
default_page_size=20,
|
|
default_order_field=Article.created_at,
|
|
)
|
|
),
|
|
],
|
|
) -> OffsetPaginatedResponse[ArticleRead]:
|
|
return await ArticleCrud.offset_paginate(session=session, **params, schema=ArticleRead)
|
|
```
|
|
|
|
The same pattern applies to `cursor_paginate_params()` and `paginate_params()`. To disable a feature, pass the toggle:
|
|
|
|
```python
|
|
# No search or ordering, only pagination + filtering
|
|
ArticleCrud.offset_paginate_params(search=False, order=False)
|
|
```
|
|
|
|
---
|
|
|
|
## Models
|
|
|
|
The lifecycle event system has been rewritten. Callbacks are now registered with a module-level [`listens_for`](../reference/models.md#fastapi_toolsets.models.listens_for) decorator and dispatched by [`EventSession`](../reference/models.md#fastapi_toolsets.models.EventSession), replacing the mixin-based approach from `v2`.
|
|
|
|
### `WatchedFieldsMixin` and `@watch` removed
|
|
|
|
Importing `WatchedFieldsMixin` or `watch` will raise `ImportError`.
|
|
|
|
Model method callbacks (`on_create`, `on_delete`, `on_update`) and the `@watch` decorator are replaced by:
|
|
|
|
1. **`__watched_fields__`** — a plain class attribute to restrict which field changes trigger `UPDATE` events (replaces `@watch`).
|
|
2. **`@listens_for`** — a module-level decorator to register callbacks for one or more [`ModelEvent`](../reference/models.md#fastapi_toolsets.models.ModelEvent) types (replaces `on_create` / `on_delete` / `on_update` methods).
|
|
|
|
=== "Before (`v2`)"
|
|
|
|
```python
|
|
from fastapi_toolsets.models import WatchedFieldsMixin, watch
|
|
|
|
@watch("status")
|
|
class Order(Base, UUIDMixin, WatchedFieldsMixin):
|
|
__tablename__ = "orders"
|
|
|
|
status: Mapped[str]
|
|
|
|
async def on_create(self):
|
|
await notify_new_order(self.id)
|
|
|
|
async def on_update(self, changes):
|
|
if "status" in changes:
|
|
await notify_status_change(self.id, changes["status"])
|
|
|
|
async def on_delete(self):
|
|
await notify_order_cancelled(self.id)
|
|
```
|
|
|
|
=== "Now (`v3`)"
|
|
|
|
```python
|
|
from fastapi_toolsets.models import ModelEvent, UUIDMixin, listens_for
|
|
|
|
class Order(Base, UUIDMixin):
|
|
__tablename__ = "orders"
|
|
__watched_fields__ = ("status",)
|
|
|
|
status: Mapped[str]
|
|
|
|
@listens_for(Order, [ModelEvent.CREATE])
|
|
async def on_order_created(order: Order, event_type: ModelEvent, changes: None):
|
|
await notify_new_order(order.id)
|
|
|
|
@listens_for(Order, [ModelEvent.UPDATE])
|
|
async def on_order_updated(order: Order, event_type: ModelEvent, changes: dict):
|
|
if "status" in changes:
|
|
await notify_status_change(order.id, changes["status"])
|
|
|
|
@listens_for(Order, [ModelEvent.DELETE])
|
|
async def on_order_deleted(order: Order, event_type: ModelEvent, changes: None):
|
|
await notify_order_cancelled(order.id)
|
|
```
|
|
|
|
### `EventSession` now required
|
|
|
|
Without `EventSession`, lifecycle callbacks will silently stop firing.
|
|
|
|
Callbacks are now dispatched inside `EventSession.commit()` rather than via background tasks. Pass it as the session class when creating your session factory:
|
|
|
|
=== "Before (`v2`)"
|
|
|
|
```python
|
|
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
|
|
engine = create_async_engine("postgresql+asyncpg://...")
|
|
SessionLocal = async_sessionmaker(engine, expire_on_commit=False)
|
|
```
|
|
|
|
=== "Now (`v3`)"
|
|
|
|
```python
|
|
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
from fastapi_toolsets.models import EventSession
|
|
|
|
engine = create_async_engine("postgresql+asyncpg://...")
|
|
SessionLocal = async_sessionmaker(engine, expire_on_commit=False, class_=EventSession)
|
|
```
|
|
|
|
!!! note
|
|
If you use `create_db_session` from `fastapi_toolsets.pytest`, the session already uses `EventSession` — no changes needed in tests.
|