Merge pull request #641 from EdouardVanbelle/refactor/front-resource-list
feat(fileDto, folderDto): add is_favorite + is_shared
This commit is contained in:
@@ -132,6 +132,14 @@ pub struct FavoriteResourceRow {
|
||||
pub created_by: Option<Uuid>,
|
||||
/// §14 provenance — who last touched the row.
|
||||
pub updated_by: Option<Uuid>,
|
||||
/// Caller-scoped favorite state — `TRUE` by construction on this
|
||||
/// listing (every row IS a favorite). The listing SQL hardcodes
|
||||
/// `TRUE AS is_favorite`; this field stays here so the DTO builder
|
||||
/// signature stays symmetric with the other listings.
|
||||
pub is_favorite: bool,
|
||||
/// Resource-scoped: `true` when the row has any `storage.role_grants`
|
||||
/// entry. Computed by a per-row `EXISTS` in the listing SQL.
|
||||
pub is_shared: bool,
|
||||
/// `true` when `owner_id == requesting user_id`.
|
||||
pub is_owner: bool,
|
||||
pub favorited_at: DateTime<Utc>,
|
||||
|
||||
@@ -83,6 +83,25 @@ pub struct FileDto {
|
||||
/// stub/legacy files.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub updated_by: Option<Uuid>,
|
||||
|
||||
/// Caller-scoped: `true` when the requesting user has favorited
|
||||
/// this file. **Wire contract: always present**, never null and
|
||||
/// never absent — the SPA reads it as a required `boolean` with no
|
||||
/// nullish branch. Every emission path (listing endpoints inline
|
||||
/// via a per-row `EXISTS` in the listing SQL; single-item endpoints
|
||||
/// via the shared `caller_flags` helper on the favorites port) is
|
||||
/// responsible for populating this before the DTO reaches the
|
||||
/// wire. WebDAV/CalDAV/CardDAV DTOs default to `false` — the XML
|
||||
/// property serializer drops the field entirely, so a stale
|
||||
/// default is never observable on those surfaces.
|
||||
pub is_favorite: bool,
|
||||
|
||||
/// Resource-scoped: `true` when the file has ANY explicit
|
||||
/// role-grant on it (link share via `subject_type = 'token'`,
|
||||
/// user/group grant, any role). "Someone was given access to
|
||||
/// this beyond drive membership." Same wire contract as
|
||||
/// `is_favorite` — always present.
|
||||
pub is_shared: bool,
|
||||
}
|
||||
|
||||
impl From<File> for FileDto {
|
||||
@@ -132,6 +151,11 @@ impl From<File> for FileDto {
|
||||
etag,
|
||||
created_by: parts.created_by,
|
||||
updated_by: parts.updated_by,
|
||||
// `From<File>` has no caller context. Callers that will
|
||||
// emit the DTO to the SPA MUST override these before
|
||||
// Json emission via the `caller_flags` helper.
|
||||
is_favorite: false,
|
||||
is_shared: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,6 +213,8 @@ impl FileDto {
|
||||
sort_date: None,
|
||||
created_by: None,
|
||||
updated_by: None,
|
||||
is_favorite: false,
|
||||
is_shared: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,17 @@ pub struct FolderDto {
|
||||
/// stub/legacy folders.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub updated_by: Option<Uuid>,
|
||||
|
||||
/// Caller-scoped: `true` when the requesting user has favorited
|
||||
/// this folder. See `FileDto::is_favorite` for the full wire
|
||||
/// contract note (always present, never null; enrichment path
|
||||
/// covers listing rows via inline `EXISTS` and single-item
|
||||
/// endpoints via the `caller_flags` helper).
|
||||
pub is_favorite: bool,
|
||||
|
||||
/// Resource-scoped: `true` when the folder has ANY explicit
|
||||
/// role-grant on it. Same wire contract as `is_favorite`.
|
||||
pub is_shared: bool,
|
||||
}
|
||||
|
||||
impl From<Folder> for FolderDto {
|
||||
@@ -127,6 +138,11 @@ impl From<Folder> for FolderDto {
|
||||
etag,
|
||||
created_by: parts.created_by,
|
||||
updated_by: parts.updated_by,
|
||||
// `From<Folder>` has no caller context. Handlers that
|
||||
// emit to the SPA MUST override via `caller_flags` before
|
||||
// Json response.
|
||||
is_favorite: false,
|
||||
is_shared: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,6 +195,8 @@ impl FolderDto {
|
||||
etag: String::new(),
|
||||
created_by: None,
|
||||
updated_by: None,
|
||||
is_favorite: false,
|
||||
is_shared: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,6 +244,16 @@ pub struct FolderResourceRow {
|
||||
pub created_by: Option<Uuid>,
|
||||
/// §14 provenance — who last touched the row.
|
||||
pub updated_by: Option<Uuid>,
|
||||
/// Caller-scoped: `true` when the requesting user has favorited
|
||||
/// this row. Populates `FileDto::is_favorite` / `FolderDto::is_favorite`
|
||||
/// on the listing without a follow-up query. Computed by the
|
||||
/// per-row `EXISTS` in `list_resources_paged`.
|
||||
pub is_favorite: bool,
|
||||
/// Resource-scoped: `true` when the row has any `storage.role_grants`
|
||||
/// entry — link share (`subject_type = 'token'`), user grant, group
|
||||
/// grant, or any role. Populates `FileDto::is_shared` /
|
||||
/// `FolderDto::is_shared`.
|
||||
pub is_shared: bool,
|
||||
// Pre-computed sort fields — returned by the SQL for cursor construction.
|
||||
/// `LOWER(name)` used by `name`/`type` sorts.
|
||||
pub sort_str: String,
|
||||
|
||||
@@ -122,6 +122,14 @@ pub struct RecentResourceRow {
|
||||
/// consumed by the UI but surfaced for API parity with the other
|
||||
/// listing endpoints.
|
||||
pub updated_by: Option<Uuid>,
|
||||
/// Caller-scoped: `true` when the requesting user has favorited
|
||||
/// this row. Populates `FileDto::is_favorite` /
|
||||
/// `FolderDto::is_favorite` on this listing via a per-row
|
||||
/// `EXISTS` in the SQL.
|
||||
pub is_favorite: bool,
|
||||
/// Resource-scoped: `true` when the row has any
|
||||
/// `storage.role_grants` entry.
|
||||
pub is_shared: bool,
|
||||
/// `true` when `owner_id == requesting user_id`.
|
||||
pub is_owner: bool,
|
||||
pub accessed_at: DateTime<Utc>,
|
||||
|
||||
@@ -77,6 +77,13 @@ pub struct TrashResourceRow {
|
||||
/// §14 provenance — who last touched the row (includes the trash
|
||||
/// action itself, which stamps `updated_by = caller_id`).
|
||||
pub updated_by: Option<Uuid>,
|
||||
/// Caller-scoped: `true` when the caller has favorited this
|
||||
/// trashed item.
|
||||
pub is_favorite: bool,
|
||||
/// Resource-scoped: `true` when the row has any
|
||||
/// `storage.role_grants` entry (surviving trash — grants are
|
||||
/// GC'd by `purge_expired_grants` on the 15-day grace).
|
||||
pub is_shared: bool,
|
||||
pub trashed_at: DateTime<Utc>,
|
||||
pub deletion_date: DateTime<Utc>,
|
||||
/// Original location path (for folders: `path`; for files: `parent.path || '/' || name`).
|
||||
|
||||
@@ -90,4 +90,26 @@ pub trait FavoritesRepositoryPort: Send + Sync + 'static {
|
||||
kinds: Option<&[ResourceKind]>,
|
||||
reverse: bool,
|
||||
) -> Result<Vec<FavoriteResourceRow>>;
|
||||
|
||||
/// Caller-scoped inline state flags for a single resource — the
|
||||
/// shared enrichment helper called by every single-item handler
|
||||
/// that emits `FileDto` / `FolderDto` to the SPA (get / rename /
|
||||
/// move / upload / delta upload / photos / bulk get by ids).
|
||||
///
|
||||
/// Runs one SQL round trip with two `EXISTS` in the SELECT:
|
||||
/// * `is_favorite` — `EXISTS on auth.user_favorites` for the
|
||||
/// `(caller_id, resource_id, resource_type)` triple.
|
||||
/// * `is_shared` — `EXISTS on storage.role_grants` for the
|
||||
/// `(resource_id, resource_type)` pair, regardless of role /
|
||||
/// subject_type / granter. Link shares live in `role_grants`
|
||||
/// as `subject_type = 'token'` under the unified model, so
|
||||
/// one EXISTS covers link shares + user grants + group grants.
|
||||
///
|
||||
/// `resource_type` MUST be `"file"` or `"folder"`.
|
||||
async fn caller_flags(
|
||||
&self,
|
||||
caller_id: Uuid,
|
||||
resource_type: &str,
|
||||
resource_id: Uuid,
|
||||
) -> Result<(bool, bool)>;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,26 @@ impl FavoritesService {
|
||||
) -> Result<HashSet<String>> {
|
||||
self.repo.batch_check_favorites(user_id, items).await
|
||||
}
|
||||
|
||||
/// Shared enrichment helper — computes `is_favorite` + `is_shared`
|
||||
/// for a single resource so single-item handlers (get / rename /
|
||||
/// move / upload / delta upload / photos / bulk get by ids) can
|
||||
/// populate the two wire-contract flags on FileDto / FolderDto
|
||||
/// before Json emission. Delegates straight to the repository
|
||||
/// port; kept on `FavoritesService` because the port already
|
||||
/// lives on that service and callers already hold it in DI.
|
||||
///
|
||||
/// `resource_type` MUST be `"file"` or `"folder"`.
|
||||
pub async fn caller_flags(
|
||||
&self,
|
||||
caller_id: Uuid,
|
||||
resource_type: &str,
|
||||
resource_id: Uuid,
|
||||
) -> Result<(bool, bool)> {
|
||||
self.repo
|
||||
.caller_flags(caller_id, resource_type, resource_id)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl FavoritesUseCase for FavoritesService {
|
||||
|
||||
@@ -828,7 +828,15 @@ impl FolderService {
|
||||
// 2. Fetch limit+1 rows so we can detect has_next
|
||||
let mut rows = self
|
||||
.folder_storage
|
||||
.list_resources_paged(pid, limit + 1, cursor.as_ref(), order_by, kinds, reverse)
|
||||
.list_resources_paged(
|
||||
pid,
|
||||
caller_id,
|
||||
limit + 1,
|
||||
cursor.as_ref(),
|
||||
order_by,
|
||||
kinds,
|
||||
reverse,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// 3. Detect has_next, build encoded next cursor
|
||||
|
||||
@@ -824,6 +824,7 @@ impl TrashService {
|
||||
.trash_repository
|
||||
.list_resources_paged(
|
||||
&drive_ids,
|
||||
user_id,
|
||||
limit + 1,
|
||||
cursor.as_ref(),
|
||||
order_by,
|
||||
@@ -890,6 +891,8 @@ fn row_to_item_dto(row: TrashResourceRow) -> TrashResourceItemDto {
|
||||
category: intern_display("Folder"),
|
||||
created_by: row.created_by,
|
||||
updated_by: row.updated_by,
|
||||
is_favorite: row.is_favorite,
|
||||
is_shared: row.is_shared,
|
||||
};
|
||||
TrashResourceItemDto {
|
||||
resource_type: ResourceTypeDto::Folder,
|
||||
@@ -933,6 +936,8 @@ fn row_to_item_dto(row: TrashResourceRow) -> TrashResourceItemDto {
|
||||
etag,
|
||||
created_by: row.created_by,
|
||||
updated_by: row.updated_by,
|
||||
is_favorite: row.is_favorite,
|
||||
is_shared: row.is_shared,
|
||||
};
|
||||
TrashResourceItemDto {
|
||||
resource_type: ResourceTypeDto::File,
|
||||
|
||||
Reference in New Issue
Block a user