feat: pagination

This commit is contained in:
Vlad Stan
2026-07-09 16:30:18 +03:00
parent 3d4202b495
commit d18f225757
4 changed files with 149 additions and 0 deletions
+16
View File
@@ -27,6 +27,8 @@ from .models import (
StorageGetResponse,
StorageListRequest,
StorageListResponse,
StoragePaginatedRequest,
StoragePaginatedResponse,
StorageSetRequest,
StorageSetResponse,
WatchPaymentRequest,
@@ -162,6 +164,20 @@ class ExtensionAPI:
async def storage_list(self, request: StorageListRequest) -> StorageListResponse:
self._raise_unwired_runtime("storage_list")
@extension_api_method(
method_id="storage.get_paginated",
namespace="storage",
name="Get paginated storage rows",
host_name="storage_get_paginated",
sdk_name="getPaginated",
description="Get filtered, searched, sorted, paginated storage rows.",
required_permission="ext.storage.read_write",
)
async def storage_get_paginated(
self, request: StoragePaginatedRequest
) -> StoragePaginatedResponse:
self._raise_unwired_runtime("storage_get_paginated")
@extension_api_method(
method_id="storage.delete",
namespace="storage",
+27
View File
@@ -51,6 +51,33 @@ class StorageListResponse(BaseModel):
rows_json: str = "[]"
class StoragePaginatedRequest(BaseModel):
table: str = Field(..., min_length=1, max_length=128)
filters: dict[str, Any] = Field(default_factory=dict)
search: str | None = Field(None, max_length=256)
search_fields: list[str] = Field(default_factory=list)
sort_by: str | None = Field(None, min_length=1, max_length=128)
descending: bool = False
limit: int = Field(25, ge=1, le=1000)
offset: int = Field(0, ge=0)
@root_validator(pre=True)
def parse_json_fields(cls, values: dict[str, Any]) -> dict[str, Any]:
filters_json = values.get("filters_json")
if filters_json is not None and "filters" not in values:
values["filters"] = json.loads(filters_json)
search_fields_json = values.get("search_fields_json")
if search_fields_json is not None and "search_fields" not in values:
values["search_fields"] = json.loads(search_fields_json)
return values
class StoragePaginatedResponse(BaseModel):
rows_json: str = "[]"
total: int = 0
class StorageDeleteRequest(BaseModel):
table: str = Field(..., min_length=1, max_length=128)
id: str = Field(..., min_length=1, max_length=512)
+23
View File
@@ -15,6 +15,8 @@ from .models import (
StorageGetResponse,
StorageListRequest,
StorageListResponse,
StoragePaginatedRequest,
StoragePaginatedResponse,
StorageSetRequest,
StorageSetResponse,
UserWalletSummary,
@@ -23,6 +25,7 @@ from .models import (
)
from .storage import (
storage_delete_row,
storage_get_paginated_rows,
storage_get_row,
storage_list_rows,
storage_set_row,
@@ -70,6 +73,26 @@ class InMemoryExtensionAPI(ExtensionAPI):
)
return StorageListResponse(rows_json=json.dumps(rows))
async def storage_get_paginated(
self, request: StoragePaginatedRequest
) -> StoragePaginatedResponse:
self.require_permission("ext.storage.read_write")
page = await storage_get_paginated_rows(
self.extension_id,
request.table,
request.filters,
search=request.search,
search_fields=request.search_fields,
sort_by=request.sort_by,
descending=request.descending,
limit=request.limit,
offset=request.offset,
)
return StoragePaginatedResponse(
rows_json=json.dumps(page["data"]),
total=page["total"],
)
async def storage_delete(
self, request: StorageDeleteRequest
) -> StorageDeleteResponse:
+83
View File
@@ -86,6 +86,47 @@ async def storage_list_rows(
return [_row_from_db(table_schema, row) for row in rows]
async def storage_get_paginated_rows(
ext_id: str,
table: str,
filters: dict[str, Any],
*,
search: str | None,
search_fields: list[str],
sort_by: str | None,
descending: bool,
limit: int,
offset: int,
) -> dict[str, Any]:
table_schema = _load_table_schema(ext_id, table)
where_sql, values = _where_sql(table_schema, filters, search, search_fields)
order_sql = _order_sql(table_schema, sort_by, descending)
count_values = dict(values)
values.update({"limit": min(limit, 1000), "offset": offset})
table_ref = _table_ref_for_schema(ext_id, table)
rows_query = f"""
SELECT * FROM {table_ref}
{where_sql}
{order_sql}
LIMIT :limit
OFFSET :offset
""" # noqa: S608
count_query = f"""
SELECT COUNT(*) AS count FROM {table_ref}
{where_sql}
""" # noqa: S608
async with Database(f"ext_{ext_id}").connect() as conn:
rows = await conn.fetchall(rows_query, values)
count_row = await conn.fetchone(count_query, count_values)
return {
"data": [_row_from_db(table_schema, row) for row in rows],
"total": int(count_row["count"]) if count_row else 0,
}
async def storage_delete_row(
ext_id: str,
table: str,
@@ -319,6 +360,48 @@ def _filters_to_db(
}
def _where_sql(
table_schema: dict[str, Any],
filters: dict[str, Any],
search: str | None,
search_fields: list[str],
) -> tuple[str, dict[str, Any]]:
clean_filters = _filters_to_db(table_schema, filters)
clauses = [f"{field} = :filter_{field}" for field in clean_filters]
values = {f"filter_{field}": value for field, value in clean_filters.items()}
clean_search = search.strip().lower() if search else ""
if clean_search:
fields = _fields_by_name(table_schema)
invalid_fields = sorted(set(search_fields) - set(fields))
if invalid_fields:
raise ValueError(
"WASM storage search has unknown fields: " + ", ".join(invalid_fields)
)
if search_fields:
search_clause = " OR ".join(
f"LOWER(CAST({field} AS TEXT)) LIKE :search" for field in search_fields
)
clauses.append(f"({search_clause})")
values["search"] = f"%{clean_search}%"
return ("WHERE " + " AND ".join(clauses), values) if clauses else ("", values)
def _order_sql(
table_schema: dict[str, Any],
sort_by: str | None,
descending: bool,
) -> str:
if not sort_by:
return ""
fields = _fields_by_name(table_schema)
if sort_by not in fields:
raise ValueError(f"WASM storage sort field is unknown: {sort_by}")
direction = "DESC" if descending else "ASC"
return f"ORDER BY {sort_by} {direction}"
def _row_from_db(
table_schema: dict[str, Any],
row: dict[str, Any],