2 Commits

Author SHA1 Message Date
a3ccea2e91 chore(deps): update nginx docker tag to v1.30 2026-04-16 00:03:01 +00:00
a37cafc53c update fastapi pagination article
All checks were successful
Build Blog Docker Image / build docker (push) Successful in 1m55s
2026-04-09 03:59:56 -04:00
2 changed files with 58 additions and 49 deletions

View File

@@ -1,5 +1,5 @@
# Build Stage # Build Stage
FROM hugomods/hugo:0.160.0 AS build FROM hugomods/hugo:0.155.3 AS build
ARG BLOWFISH_VERSION ARG BLOWFISH_VERSION
@@ -11,7 +11,7 @@ RUN git submodule update --init --recursive && \
RUN hugo RUN hugo
# Publish Stage # Publish Stage
FROM nginx:1.29-alpine FROM nginx:1.30-alpine
WORKDIR /usr/share/nginx/html WORKDIR /usr/share/nginx/html
COPY --from=build /opt/blog/public /usr/share/nginx/html/ COPY --from=build /opt/blog/public /usr/share/nginx/html/

View File

@@ -31,9 +31,13 @@ GET /articles?page=2&items_per_page=20
```json ```json
{ {
"items": [...], "items": [...],
"total": 143, "pagination": {
"total_count": 143,
"items_per_page": 20,
"page": 2, "page": 2,
"total_pages": 8 "has_more": true,
"pages": 8
}
} }
``` ```
@@ -52,8 +56,12 @@ GET /articles?cursor=eyJpZCI6IjEyMyJ9&items_per_page=20
```json ```json
{ {
"items": [...], "items": [...],
"next_cursor": "eyJpZCI6IjE0MyJ9", "pagination": {
"has_next": true "next_cursor": "eyJjcmVhdGVkX2F0IjogIjIwMjYtMDMtMTBUMDg6MTQ6MDBaIn0=",
"prev_cursor": null,
"items_per_page": 20,
"has_more": true
}
} }
``` ```
@@ -207,22 +215,19 @@ With the CRUD factory declared, routes become thin wrappers. Each route uses [Ar
async def list_articles_offset( async def list_articles_offset(
session: SessionDep, session: SessionDep,
params: Annotated[ params: Annotated[
dict[str, Any], dict,
Depends(ArticleCrud.offset_params(default_page_size=20, max_page_size=100)), Depends(
ArticleCrud.offset_paginate_params(
default_page_size=20,
max_page_size=100,
default_order_field=Article.created_at,
)
),
], ],
filter_by: Annotated[dict[str, list[str]], Depends(ArticleCrud.filter_params())],
order_by: Annotated[
OrderByClause | None,
Depends(ArticleCrud.order_params(default_field=Article.created_at)),
],
search: str | None = None,
) -> OffsetPaginatedResponse[ArticleRead]: ) -> OffsetPaginatedResponse[ArticleRead]:
return await ArticleCrud.offset_paginate( return await ArticleCrud.offset_paginate(
session=session, session=session,
**params, **params,
search=search,
filter_by=filter_by or None,
order_by=order_by,
schema=ArticleRead, schema=ArticleRead,
) )
``` ```
@@ -231,7 +236,7 @@ async def list_articles_offset(
**Example request:** **Example request:**
``` ```
GET /articles/offset?page=2&items_per_page=2&search=fastapi&filter_by[status]=published&order_by=created_at&order_dir=desc GET /articles/offset?page=2&items_per_page=2&search=fastapi&status=published&order_by=created_at&order_dir=desc
``` ```
**Example response:** **Example response:**
@@ -259,13 +264,16 @@ GET /articles/offset?page=2&items_per_page=2&search=fastapi&filter_by[status]=pu
"total_count": 47, "total_count": 47,
"items_per_page": 2, "items_per_page": 2,
"page": 2, "page": 2,
"has_more": true "has_more": true,
"pages": 24
}, },
"pagination_type": "offset", "pagination_type": "offset",
"filter_attributes": { "filter_attributes": {
"status": ["draft", "published", "archived"], "status": ["draft", "published", "archived"],
"category__name": ["Python", "DevOps", "Architecture"] "category__name": ["Python", "DevOps", "Architecture"]
} },
"search_columns": ["title", "body", "category__name"],
"order_columns": ["title", "created_at"]
} }
``` ```
@@ -276,22 +284,19 @@ GET /articles/offset?page=2&items_per_page=2&search=fastapi&filter_by[status]=pu
async def list_articles_cursor( async def list_articles_cursor(
session: SessionDep, session: SessionDep,
params: Annotated[ params: Annotated[
dict[str, Any], dict,
Depends(ArticleCrud.cursor_params(default_page_size=20, max_page_size=100)), Depends(
ArticleCrud.cursor_paginate_params(
default_page_size=20,
max_page_size=100,
default_order_field=Article.created_at,
)
),
], ],
filter_by: Annotated[dict[str, list[str]], Depends(ArticleCrud.filter_params())],
order_by: Annotated[
OrderByClause | None,
Depends(ArticleCrud.order_params(default_field=Article.created_at)),
],
search: str | None = None,
) -> CursorPaginatedResponse[ArticleRead]: ) -> CursorPaginatedResponse[ArticleRead]:
return await ArticleCrud.cursor_paginate( return await ArticleCrud.cursor_paginate(
session=session, session=session,
**params, **params,
search=search,
filter_by=filter_by or None,
order_by=order_by,
schema=ArticleRead, schema=ArticleRead,
) )
``` ```
@@ -300,7 +305,7 @@ async def list_articles_cursor(
**Example request (first page):** **Example request (first page):**
``` ```
GET /articles/cursor?items_per_page=2&search=fastapi&filter_by[status]=published GET /articles/cursor?items_per_page=2&search=fastapi&status=published
``` ```
**Example response:** **Example response:**
@@ -334,7 +339,9 @@ GET /articles/cursor?items_per_page=2&search=fastapi&filter_by[status]=published
"filter_attributes": { "filter_attributes": {
"status": ["draft", "published", "archived"], "status": ["draft", "published", "archived"],
"category__name": ["Python", "DevOps", "Architecture"] "category__name": ["Python", "DevOps", "Architecture"]
} },
"search_columns": ["title", "body", "category__name"],
"order_columns": ["title", "created_at"]
} }
``` ```
@@ -352,22 +359,19 @@ You can also expose a single endpoint that supports both strategies via a `pagin
async def list_articles( async def list_articles(
session: SessionDep, session: SessionDep,
params: Annotated[ params: Annotated[
dict[str, Any], dict,
Depends(ArticleCrud.paginate_params(default_page_size=20, max_page_size=100)), Depends(
ArticleCrud.paginate_params(
default_page_size=20,
max_page_size=100,
default_order_field=Article.created_at,
)
),
], ],
filter_by: Annotated[dict[str, list[str]], Depends(ArticleCrud.filter_params())],
order_by: Annotated[
OrderByClause | None,
Depends(ArticleCrud.order_params(default_field=Article.created_at)),
],
search: str | None = None,
) -> PaginatedResponse[ArticleRead]: ) -> PaginatedResponse[ArticleRead]:
return await ArticleCrud.paginate( return await ArticleCrud.paginate(
session, session,
**params, **params,
search=search,
filter_by=filter_by or None,
order_by=order_by,
schema=ArticleRead, schema=ArticleRead,
) )
``` ```
@@ -376,7 +380,7 @@ async def list_articles(
The response shape adapts to the chosen strategy. With `pagination_type=offset` (default): The response shape adapts to the chosen strategy. With `pagination_type=offset` (default):
``` ```
GET /articles/?pagination_type=offset&page=1&items_per_page=2&filter_by[status]=published GET /articles/?pagination_type=offset&page=1&items_per_page=2&status=published
``` ```
```json ```json
{ {
@@ -386,18 +390,21 @@ GET /articles/?pagination_type=offset&page=1&items_per_page=2&filter_by[status]=
"items_per_page": 2, "items_per_page": 2,
"page": 1, "page": 1,
"has_more": true "has_more": true
"pages": 24,
}, },
"pagination_type": "offset", "pagination_type": "offset",
"filter_attributes": { "filter_attributes": {
"status": ["draft", "published", "archived"], "status": ["draft", "published", "archived"],
"category__name": ["Python", "DevOps", "Architecture"] "category__name": ["Python", "DevOps", "Architecture"]
} },
"search_columns": ["title", "body", "category__name"],
"order_columns": ["title", "created_at"]
} }
``` ```
With `pagination_type=cursor`: With `pagination_type=cursor`:
``` ```
GET /articles/?pagination_type=cursor&items_per_page=2&filter_by[status]=published GET /articles/?pagination_type=cursor&items_per_page=2&status=published
``` ```
```json ```json
{ {
@@ -412,7 +419,9 @@ GET /articles/?pagination_type=cursor&items_per_page=2&filter_by[status]=publish
"filter_attributes": { "filter_attributes": {
"status": ["draft", "published", "archived"], "status": ["draft", "published", "archived"],
"category__name": ["Python", "DevOps", "Architecture"] "category__name": ["Python", "DevOps", "Architecture"]
} },
"search_columns": ["title", "body", "category__name"],
"order_columns": ["title", "created_at"]
} }
``` ```