feat(api): cursor listing contract — PageCursor trait + resource field

- Add src/application/dtos/cursor.rs with three shared types:
  · PageCursor trait  — default base64url+JSON encode/decode; one bare
    impl line per cursor struct
  · CursorQuery struct — standard limit/cursor/sort_by query params with
    limit_clamped() and decode_cursor<C>() helpers; compose via flatten
  · CursorListResponse<T> — standard {items, next_cursor?} envelope with
    from_oversized() and with_cursor() builders

- Migrate GrantCursor to impl PageCursor (remove duplicate encode/decode)

- Update GET /api/grants/incoming/resources:
  · SharedWithMeQuery now embeds CursorQuery via #[serde(flatten)]
  · Replace file/folder nullable pair with ResourceContentDto (untagged
    enum) under a single always-present 'resource' field
  · SharedWithMeDto is now a type alias for CursorListResponse<SharedWithMeItemDto>
  · Handler uses q.paging.limit_clamped() and decode_cursor<GrantCursor>()

- Add docs/architecture/resource-listing.md — authoritative contract for
  all listing endpoints (cursor design, SQL keyset WHERE, sort_by naming,
  Rust + JS skeletons, compliance table, migration guide)

- Register doc in VitePress sidebar and architecture index

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Edouard Vanbelle
2026-05-26 17:52:28 +02:00
parent 9a2782b67e
commit c65f2b5385
10 changed files with 565 additions and 53 deletions
+3 -15
View File
@@ -5,7 +5,7 @@
//! `AuthorizationEngine` port consumes them and the `PgAclEngine` implementation
//! maps them to / from `storage.access_grants` rows.
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use crate::application::dtos::cursor::PageCursor;
use std::fmt;
use uuid::Uuid;
@@ -265,20 +265,8 @@ pub struct GrantCursor {
pub resource_id: Uuid,
}
impl GrantCursor {
/// Encode as a URL-safe base64 JSON string (no padding).
pub fn encode(&self) -> String {
let json = serde_json::to_vec(self).unwrap_or_default();
URL_SAFE_NO_PAD.encode(&json)
}
/// Decode from a URL-safe base64 JSON string. Returns `None` on any
/// parse failure — callers treat a bad cursor as "start from the top".
pub fn decode(s: &str) -> Option<Self> {
let bytes = URL_SAFE_NO_PAD.decode(s).ok()?;
serde_json::from_slice(&bytes).ok()
}
}
/// Delegate encode/decode to the shared [`PageCursor`] trait.
impl PageCursor for GrantCursor {}
#[cfg(test)]
mod tests {