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
+2 -3
View File
@@ -297,14 +297,13 @@
/**
* One item returned by `GET /api/grants/incoming/resources`.
* Exactly one of `file` / `folder` is populated (indicated by `resource_type`).
* `resource_type` discriminates the shape of `resource`.
* @typedef {Object} SharedWithMeItem
* @property {ResourceTypeEnum} resource_type
* @property {PermissionTypeEnum[]} permissions - All permissions the caller holds on this resource.
* @property {string} granted_at - ISO-8601 timestamp of the earliest grant.
* @property {string} granted_by - UUID of the user who created the grant.
* @property {FileItem|undefined} [file] - Populated when resource_type === 'file'.
* @property {FolderItem|undefined} [folder] - Populated when resource_type === 'folder'.
* @property {FileItem|FolderItem} resource - Full resource details; shape follows resource_type.
*/
/**
@@ -150,8 +150,8 @@ const sharedWithMeView = {
const ownerMap = new Map();
for (const item of items) {
if (item.resource_type === 'folder' && item.folder) {
const f = item.folder;
if (item.resource_type === 'folder') {
const f = /** @type {FolderItem} */ (item.resource);
folders.push(
/** @type {FolderItem} */ ({
id: f.id,
@@ -168,8 +168,8 @@ const sharedWithMeView = {
})
);
ownerMap.set(f.id, item.granted_by);
} else if (item.resource_type === 'file' && item.file) {
const f = item.file;
} else if (item.resource_type === 'file') {
const f = /** @type {FileItem} */ (item.resource);
files.push(
/** @type {FileItem} */ ({
id: f.id,