feat: pagination
This commit is contained in:
@@ -27,6 +27,8 @@ from .models import (
|
|||||||
StorageGetResponse,
|
StorageGetResponse,
|
||||||
StorageListRequest,
|
StorageListRequest,
|
||||||
StorageListResponse,
|
StorageListResponse,
|
||||||
|
StoragePaginatedRequest,
|
||||||
|
StoragePaginatedResponse,
|
||||||
StorageSetRequest,
|
StorageSetRequest,
|
||||||
StorageSetResponse,
|
StorageSetResponse,
|
||||||
WatchPaymentRequest,
|
WatchPaymentRequest,
|
||||||
@@ -162,6 +164,20 @@ class ExtensionAPI:
|
|||||||
async def storage_list(self, request: StorageListRequest) -> StorageListResponse:
|
async def storage_list(self, request: StorageListRequest) -> StorageListResponse:
|
||||||
self._raise_unwired_runtime("storage_list")
|
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(
|
@extension_api_method(
|
||||||
method_id="storage.delete",
|
method_id="storage.delete",
|
||||||
namespace="storage",
|
namespace="storage",
|
||||||
|
|||||||
@@ -51,6 +51,33 @@ class StorageListResponse(BaseModel):
|
|||||||
rows_json: str = "[]"
|
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):
|
class StorageDeleteRequest(BaseModel):
|
||||||
table: str = Field(..., min_length=1, max_length=128)
|
table: str = Field(..., min_length=1, max_length=128)
|
||||||
id: str = Field(..., min_length=1, max_length=512)
|
id: str = Field(..., min_length=1, max_length=512)
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ from .models import (
|
|||||||
StorageGetResponse,
|
StorageGetResponse,
|
||||||
StorageListRequest,
|
StorageListRequest,
|
||||||
StorageListResponse,
|
StorageListResponse,
|
||||||
|
StoragePaginatedRequest,
|
||||||
|
StoragePaginatedResponse,
|
||||||
StorageSetRequest,
|
StorageSetRequest,
|
||||||
StorageSetResponse,
|
StorageSetResponse,
|
||||||
UserWalletSummary,
|
UserWalletSummary,
|
||||||
@@ -23,6 +25,7 @@ from .models import (
|
|||||||
)
|
)
|
||||||
from .storage import (
|
from .storage import (
|
||||||
storage_delete_row,
|
storage_delete_row,
|
||||||
|
storage_get_paginated_rows,
|
||||||
storage_get_row,
|
storage_get_row,
|
||||||
storage_list_rows,
|
storage_list_rows,
|
||||||
storage_set_row,
|
storage_set_row,
|
||||||
@@ -70,6 +73,26 @@ class InMemoryExtensionAPI(ExtensionAPI):
|
|||||||
)
|
)
|
||||||
return StorageListResponse(rows_json=json.dumps(rows))
|
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(
|
async def storage_delete(
|
||||||
self, request: StorageDeleteRequest
|
self, request: StorageDeleteRequest
|
||||||
) -> StorageDeleteResponse:
|
) -> StorageDeleteResponse:
|
||||||
|
|||||||
@@ -86,6 +86,47 @@ async def storage_list_rows(
|
|||||||
return [_row_from_db(table_schema, row) for row in 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(
|
async def storage_delete_row(
|
||||||
ext_id: str,
|
ext_id: str,
|
||||||
table: 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(
|
def _row_from_db(
|
||||||
table_schema: dict[str, Any],
|
table_schema: dict[str, Any],
|
||||||
row: dict[str, Any],
|
row: dict[str, Any],
|
||||||
|
|||||||
Reference in New Issue
Block a user