diff --git a/docs/module/crud.md b/docs/module/crud.md index c62ddcd..15bc7c8 100644 --- a/docs/module/crud.md +++ b/docs/module/crud.md @@ -255,7 +255,7 @@ The cursor column is set once on [`CrudFactory`](../reference/crud.md#fastapi_to !!! note `cursor_column` is required. Calling [`cursor_paginate`](../reference/crud.md#fastapi_toolsets.crud.factory.AsyncCrud.cursor_paginate) on a CRUD class that has no `cursor_column` configured raises a `ValueError`. -The cursor value is base64-encoded when returned to the client and decoded back to the correct Python type on the next request. The following SQLAlchemy column types are supported: +The cursor value is URL-safe base64-encoded (no padding) when returned to the client and decoded back to the correct Python type on the next request. The following SQLAlchemy column types are supported: | SQLAlchemy type | Python type | |---|---| diff --git a/src/fastapi_toolsets/crud/factory.py b/src/fastapi_toolsets/crud/factory.py index dd571b0..fdeced0 100644 --- a/src/fastapi_toolsets/crud/factory.py +++ b/src/fastapi_toolsets/crud/factory.py @@ -58,15 +58,20 @@ class _CursorDirection(str, Enum): def _encode_cursor( value: Any, *, direction: _CursorDirection = _CursorDirection.NEXT ) -> str: - """Encode a cursor column value and navigation direction as a base64 string.""" - return base64.b64encode( - json.dumps({"val": str(value), "dir": direction}).encode() - ).decode() + """Encode a cursor column value and navigation direction as a URL-safe base64 string.""" + return ( + base64.urlsafe_b64encode( + json.dumps({"val": str(value), "dir": direction}).encode() + ) + .decode() + .rstrip("=") + ) def _decode_cursor(cursor: str) -> tuple[str, _CursorDirection]: - """Decode a cursor base64 string into ``(raw_value, direction)``.""" - payload = json.loads(base64.b64decode(cursor.encode()).decode()) + """Decode a URL-safe base64 cursor string into ``(raw_value, direction)``.""" + padded = cursor + "=" * (-len(cursor) % 4) + payload = json.loads(base64.urlsafe_b64decode(padded).decode()) return payload["val"], _CursorDirection(payload["dir"])