feat(drive): remove all user_id ref in file or folder
This commit is contained in:
@@ -0,0 +1,113 @@
|
|||||||
|
-- ─────────────────────────────────────────────────────────────────────────
|
||||||
|
-- D7 step 6 — drop `user_id` from `storage.files` and `storage.folders`.
|
||||||
|
--
|
||||||
|
-- Companion / final step to:
|
||||||
|
-- • `20260902000000_files_folders_user_id_nullable.sql` — dropped NOT NULL,
|
||||||
|
-- swapped uniqueness indexes to drive-scoped, retired the user_id-leading
|
||||||
|
-- indexes.
|
||||||
|
-- • `20260902000001_copy_folder_tree_drop_user_id.sql` — stopped writing
|
||||||
|
-- the column from `storage.copy_folder_tree`.
|
||||||
|
--
|
||||||
|
-- All Rust writers already omit `user_id` from INSERTs (step 4). Every read
|
||||||
|
-- has been rewritten to drive-membership predicates (step 5). This migration
|
||||||
|
-- removes the column entirely so no future accidental read/write can bind it.
|
||||||
|
--
|
||||||
|
-- Ownership continues to live in `storage.role_grants` (drive-Owner role);
|
||||||
|
-- provenance in `created_by` / `updated_by` (§14).
|
||||||
|
--
|
||||||
|
-- ── Dependencies to unpin before ALTER ───────────────────────────────────
|
||||||
|
--
|
||||||
|
-- `storage.trash_items` is a VIEW that projects both `f.user_id` and
|
||||||
|
-- `fo.user_id`. `CREATE OR REPLACE VIEW` can only APPEND columns, never
|
||||||
|
-- drop or reorder — see `bug_create_or_replace_view_column_order`. So we
|
||||||
|
-- DROP the view, then recreate it without user_id after the column drop.
|
||||||
|
--
|
||||||
|
-- All remaining pre-D7 indexes that referenced `user_id`
|
||||||
|
-- (`idx_files_trashed`, `idx_files_media_timeline`, and any legacy
|
||||||
|
-- uniqueness holdovers) are dropped implicitly by `ALTER TABLE DROP
|
||||||
|
-- COLUMN`. The D0/D7 drive-keyed successors already exist
|
||||||
|
-- (`idx_files_media_timeline_by_drive`,
|
||||||
|
-- `idx_files_unique_name_in_folder`, `idx_files_unique_name_at_root`,
|
||||||
|
-- etc.), so the hot paths retain their O(LIMIT) shape.
|
||||||
|
|
||||||
|
-- ── 1. Drop dependent view so the column drop can proceed ────────────────
|
||||||
|
|
||||||
|
DROP VIEW IF EXISTS storage.trash_items;
|
||||||
|
|
||||||
|
-- ── 2. Drop the column ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
ALTER TABLE storage.files DROP COLUMN IF EXISTS user_id;
|
||||||
|
ALTER TABLE storage.folders DROP COLUMN IF EXISTS user_id;
|
||||||
|
|
||||||
|
-- ── 3. Recreate the trash view without user_id ───────────────────────────
|
||||||
|
--
|
||||||
|
-- `drive_id` is still projected (D2b introduced it) and is the scope
|
||||||
|
-- column for per-drive trash listing; `caller_group_ids($1)` fans it
|
||||||
|
-- out to the caller's group memberships via role_grants.
|
||||||
|
|
||||||
|
CREATE VIEW storage.trash_items AS
|
||||||
|
SELECT f.id, f.name, 'file' AS item_type, f.trashed_at,
|
||||||
|
f.original_folder_id AS original_parent_id, f.created_at,
|
||||||
|
f.drive_id
|
||||||
|
FROM storage.files f
|
||||||
|
WHERE f.is_trashed = TRUE
|
||||||
|
AND (f.folder_id IS NULL
|
||||||
|
OR NOT EXISTS (
|
||||||
|
SELECT 1 FROM storage.folders p
|
||||||
|
WHERE p.id = f.folder_id AND p.is_trashed = TRUE))
|
||||||
|
UNION ALL
|
||||||
|
SELECT fo.id, fo.name, 'folder' AS item_type, fo.trashed_at,
|
||||||
|
fo.original_parent_id, fo.created_at,
|
||||||
|
fo.drive_id
|
||||||
|
FROM storage.folders fo
|
||||||
|
WHERE fo.is_trashed = TRUE
|
||||||
|
AND (fo.parent_id IS NULL
|
||||||
|
OR NOT EXISTS (
|
||||||
|
SELECT 1 FROM storage.folders p
|
||||||
|
WHERE p.id = fo.parent_id AND p.is_trashed = TRUE));
|
||||||
|
|
||||||
|
COMMENT ON VIEW storage.trash_items IS
|
||||||
|
'Unified view of all trashed files and folders. Post-D7: `user_id` '
|
||||||
|
'projection removed — the source column is gone. Scope is `drive_id` '
|
||||||
|
'via role_grants membership (see TrashDbRepository::get_trash_items).';
|
||||||
|
|
||||||
|
-- ── 4. Post-flight sanity ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
DO $BODY$
|
||||||
|
DECLARE
|
||||||
|
files_has_col BOOLEAN;
|
||||||
|
folders_has_col BOOLEAN;
|
||||||
|
view_has_col BOOLEAN;
|
||||||
|
BEGIN
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'storage'
|
||||||
|
AND table_name = 'files'
|
||||||
|
AND column_name = 'user_id'
|
||||||
|
) INTO files_has_col;
|
||||||
|
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'storage'
|
||||||
|
AND table_name = 'folders'
|
||||||
|
AND column_name = 'user_id'
|
||||||
|
) INTO folders_has_col;
|
||||||
|
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_schema = 'storage'
|
||||||
|
AND table_name = 'trash_items'
|
||||||
|
AND column_name = 'user_id'
|
||||||
|
) INTO view_has_col;
|
||||||
|
|
||||||
|
IF files_has_col THEN
|
||||||
|
RAISE EXCEPTION 'storage.files.user_id column did not drop';
|
||||||
|
END IF;
|
||||||
|
IF folders_has_col THEN
|
||||||
|
RAISE EXCEPTION 'storage.folders.user_id column did not drop';
|
||||||
|
END IF;
|
||||||
|
IF view_has_col THEN
|
||||||
|
RAISE EXCEPTION 'storage.trash_items still projects user_id — view recreate skipped';
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$BODY$;
|
||||||
@@ -8,6 +8,8 @@
|
|||||||
//! materialized path column), so no recursive CTEs or N+1 queries are needed.
|
//! materialized path column), so no recursive CTEs or N+1 queries are needed.
|
||||||
|
|
||||||
/// Row shape returned by media-file queries (avoids `clippy::type_complexity`).
|
/// Row shape returned by media-file queries (avoids `clippy::type_complexity`).
|
||||||
|
/// Post-D7-step-6: `storage.files.user_id` dropped, so it's no
|
||||||
|
/// longer projected.
|
||||||
type MediaFileRow = (
|
type MediaFileRow = (
|
||||||
String, // id
|
String, // id
|
||||||
String, // name
|
String, // name
|
||||||
@@ -18,7 +20,6 @@ type MediaFileRow = (
|
|||||||
i64, // created_at
|
i64, // created_at
|
||||||
i64, // updated_at
|
i64, // updated_at
|
||||||
String, // blob_hash
|
String, // blob_hash
|
||||||
Option<Uuid>, // user_id
|
|
||||||
Option<Uuid>, // created_by (§14 provenance)
|
Option<Uuid>, // created_by (§14 provenance)
|
||||||
Option<Uuid>, // updated_by (§14 provenance)
|
Option<Uuid>, // updated_by (§14 provenance)
|
||||||
i64, // sort_date
|
i64, // sort_date
|
||||||
@@ -76,8 +77,11 @@ const CALLER_CAN_READ_DRIVE: &str = "EXISTS (\
|
|||||||
|
|
||||||
/// Type alias for file metadata rows from SQL queries.
|
/// Type alias for file metadata rows from SQL queries.
|
||||||
/// Fields: id, name, folder_id, folder_path, size, mime_type,
|
/// Fields: id, name, folder_id, folder_path, size, mime_type,
|
||||||
/// created_at, updated_at, blob_hash, user_id, created_by, updated_by.
|
/// created_at, updated_at, blob_hash, created_by, updated_by.
|
||||||
/// `created_by` / `updated_by` are the §14 provenance columns.
|
/// `created_by` / `updated_by` are the §14 provenance columns.
|
||||||
|
/// Post-D7-step-6: `storage.files.user_id` dropped, so it's no
|
||||||
|
/// longer part of the tuple; `row_to_file` populates the entity's
|
||||||
|
/// legacy `user_id` field with `None`.
|
||||||
type FileRow = (
|
type FileRow = (
|
||||||
String,
|
String,
|
||||||
String,
|
String,
|
||||||
@@ -90,7 +94,6 @@ type FileRow = (
|
|||||||
String,
|
String,
|
||||||
Option<Uuid>,
|
Option<Uuid>,
|
||||||
Option<Uuid>,
|
Option<Uuid>,
|
||||||
Option<Uuid>,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Append the optional type/date/size filters from `criteria` to
|
/// Append the optional type/date/size filters from `criteria` to
|
||||||
@@ -271,7 +274,7 @@ impl FileBlobReadRepository {
|
|||||||
EXTRACT(EPOCH FROM fi.created_at)::bigint, \
|
EXTRACT(EPOCH FROM fi.created_at)::bigint, \
|
||||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint, \
|
EXTRACT(EPOCH FROM fi.updated_at)::bigint, \
|
||||||
fi.blob_hash, \
|
fi.blob_hash, \
|
||||||
fi.user_id, \
|
\
|
||||||
fi.created_by, fi.updated_by \
|
fi.created_by, fi.updated_by \
|
||||||
FROM storage.files fi \
|
FROM storage.files fi \
|
||||||
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id \
|
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id \
|
||||||
@@ -292,10 +295,8 @@ impl FileBlobReadRepository {
|
|||||||
|
|
||||||
rows.into_iter()
|
rows.into_iter()
|
||||||
.map(
|
.map(
|
||||||
|(id, name, fid, fpath, size, mime, ca, ma, blob_hash, uid, cb, ub)| {
|
|(id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub)| {
|
||||||
Self::row_to_file(
|
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub)
|
||||||
id, name, fid, fpath, size, mime, ca, ma, blob_hash, uid, cb, ub,
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.collect::<Result<Vec<_>, _>>()
|
.collect::<Result<Vec<_>, _>>()
|
||||||
@@ -323,7 +324,7 @@ impl FileBlobReadRepository {
|
|||||||
EXTRACT(EPOCH FROM fi.created_at)::bigint, \
|
EXTRACT(EPOCH FROM fi.created_at)::bigint, \
|
||||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint, \
|
EXTRACT(EPOCH FROM fi.updated_at)::bigint, \
|
||||||
fi.blob_hash, \
|
fi.blob_hash, \
|
||||||
fi.user_id, \
|
\
|
||||||
fi.created_by, fi.updated_by \
|
fi.created_by, fi.updated_by \
|
||||||
FROM storage.files fi \
|
FROM storage.files fi \
|
||||||
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id \
|
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id \
|
||||||
@@ -338,10 +339,8 @@ impl FileBlobReadRepository {
|
|||||||
|
|
||||||
rows.into_iter()
|
rows.into_iter()
|
||||||
.map(
|
.map(
|
||||||
|(id, name, fid, fpath, size, mime, ca, ma, blob_hash, uid, cb, ub)| {
|
|(id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub)| {
|
||||||
Self::row_to_file(
|
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub)
|
||||||
id, name, fid, fpath, size, mime, ca, ma, blob_hash, uid, cb, ub,
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.collect::<Result<Vec<_>, _>>()
|
.collect::<Result<Vec<_>, _>>()
|
||||||
@@ -398,6 +397,8 @@ impl FileBlobReadRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Post-D7-step-6: `storage.files.user_id` dropped; the entity's
|
||||||
|
/// legacy `user_id` field is populated with `None` here.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn row_to_file(
|
fn row_to_file(
|
||||||
id: String,
|
id: String,
|
||||||
@@ -409,7 +410,6 @@ impl FileBlobReadRepository {
|
|||||||
created_at: i64,
|
created_at: i64,
|
||||||
modified_at: i64,
|
modified_at: i64,
|
||||||
blob_hash: String,
|
blob_hash: String,
|
||||||
owner_id: Option<Uuid>,
|
|
||||||
created_by: Option<Uuid>,
|
created_by: Option<Uuid>,
|
||||||
updated_by: Option<Uuid>,
|
updated_by: Option<Uuid>,
|
||||||
) -> Result<File, DomainError> {
|
) -> Result<File, DomainError> {
|
||||||
@@ -423,7 +423,7 @@ impl FileBlobReadRepository {
|
|||||||
folder_id,
|
folder_id,
|
||||||
created_at as u64,
|
created_at as u64,
|
||||||
modified_at as u64,
|
modified_at as u64,
|
||||||
owner_id,
|
None, // Post-D7: `files.user_id` column dropped.
|
||||||
blob_hash,
|
blob_hash,
|
||||||
created_by,
|
created_by,
|
||||||
updated_by,
|
updated_by,
|
||||||
@@ -504,7 +504,7 @@ impl FileBlobReadRepository {
|
|||||||
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
||||||
fi.blob_hash,
|
fi.blob_hash,
|
||||||
fi.user_id,
|
|
||||||
fi.created_by, fi.updated_by,
|
fi.created_by, fi.updated_by,
|
||||||
EXTRACT(EPOCH FROM fi.media_sort_date)::bigint AS sort_date,
|
EXTRACT(EPOCH FROM fi.media_sort_date)::bigint AS sort_date,
|
||||||
fm.width, fm.height
|
fm.width, fm.height
|
||||||
@@ -544,9 +544,9 @@ impl FileBlobReadRepository {
|
|||||||
let mut sort_dates = Vec::with_capacity(rows.len());
|
let mut sort_dates = Vec::with_capacity(rows.len());
|
||||||
let mut dims = Vec::with_capacity(rows.len());
|
let mut dims = Vec::with_capacity(rows.len());
|
||||||
|
|
||||||
for (id, name, fid, fpath, size, mime, ca, ma, blob_hash, uid, cb, ub, sd, w, h) in rows {
|
for (id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub, sd, w, h) in rows {
|
||||||
files.push(Self::row_to_file(
|
files.push(Self::row_to_file(
|
||||||
id, name, fid, fpath, size, mime, ca, ma, blob_hash, uid, cb, ub,
|
id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub,
|
||||||
)?);
|
)?);
|
||||||
sort_dates.push(sd);
|
sort_dates.push(sd);
|
||||||
dims.push((w, h));
|
dims.push((w, h));
|
||||||
@@ -641,7 +641,6 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
i64, // created_at
|
i64, // created_at
|
||||||
i64, // updated_at
|
i64, // updated_at
|
||||||
String, // blob_hash
|
String, // blob_hash
|
||||||
Option<Uuid>, // user_id (owner)
|
|
||||||
Option<Uuid>, // created_by (§14)
|
Option<Uuid>, // created_by (§14)
|
||||||
Option<Uuid>, // updated_by (§14)
|
Option<Uuid>, // updated_by (§14)
|
||||||
),
|
),
|
||||||
@@ -652,7 +651,6 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
||||||
fi.blob_hash,
|
fi.blob_hash,
|
||||||
fi.user_id,
|
|
||||||
fi.created_by, fi.updated_by
|
fi.created_by, fi.updated_by
|
||||||
FROM storage.files fi
|
FROM storage.files fi
|
||||||
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
||||||
@@ -670,7 +668,7 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
self.hash_cache.insert(id.to_string(), row.8.clone());
|
self.hash_cache.insert(id.to_string(), row.8.clone());
|
||||||
|
|
||||||
Self::row_to_file(
|
Self::row_to_file(
|
||||||
row.0, row.1, row.2, row.3, row.4, row.5, row.6, row.7, row.8, row.9, row.10, row.11,
|
row.0, row.1, row.2, row.3, row.4, row.5, row.6, row.7, row.8, row.9, row.10,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -690,7 +688,6 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
i64,
|
i64,
|
||||||
i64,
|
i64,
|
||||||
String,
|
String,
|
||||||
Option<Uuid>,
|
|
||||||
Option<Uuid>, // created_by (§14)
|
Option<Uuid>, // created_by (§14)
|
||||||
Option<Uuid>, // updated_by (§14)
|
Option<Uuid>, // updated_by (§14)
|
||||||
),
|
),
|
||||||
@@ -701,7 +698,6 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
||||||
fi.blob_hash,
|
fi.blob_hash,
|
||||||
fi.user_id,
|
|
||||||
fi.created_by, fi.updated_by
|
fi.created_by, fi.updated_by
|
||||||
FROM storage.files fi
|
FROM storage.files fi
|
||||||
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
||||||
@@ -716,7 +712,7 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
|
|
||||||
self.hash_cache.insert(id.to_string(), row.8.clone());
|
self.hash_cache.insert(id.to_string(), row.8.clone());
|
||||||
Self::row_to_file(
|
Self::row_to_file(
|
||||||
row.0, row.1, row.2, row.3, row.4, row.5, row.6, row.7, row.8, row.9, row.10, row.11,
|
row.0, row.1, row.2, row.3, row.4, row.5, row.6, row.7, row.8, row.9, row.10,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -730,7 +726,7 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
||||||
fi.blob_hash,
|
fi.blob_hash,
|
||||||
fi.user_id,
|
|
||||||
fi.created_by, fi.updated_by
|
fi.created_by, fi.updated_by
|
||||||
FROM storage.files fi
|
FROM storage.files fi
|
||||||
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
||||||
@@ -749,7 +745,7 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
||||||
fi.blob_hash,
|
fi.blob_hash,
|
||||||
fi.user_id,
|
|
||||||
fi.created_by, fi.updated_by
|
fi.created_by, fi.updated_by
|
||||||
FROM storage.files fi
|
FROM storage.files fi
|
||||||
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
||||||
@@ -764,10 +760,8 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
|
|
||||||
rows.into_iter()
|
rows.into_iter()
|
||||||
.map(
|
.map(
|
||||||
|(id, name, fid, fpath, size, mime, ca, ma, blob_hash, uid, cb, ub)| {
|
|(id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub)| {
|
||||||
Self::row_to_file(
|
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub)
|
||||||
id, name, fid, fpath, size, mime, ca, ma, blob_hash, uid, cb, ub,
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.collect()
|
.collect()
|
||||||
@@ -796,7 +790,7 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
||||||
fi.blob_hash,
|
fi.blob_hash,
|
||||||
fi.user_id,
|
|
||||||
fi.created_by, fi.updated_by
|
fi.created_by, fi.updated_by
|
||||||
FROM storage.files fi
|
FROM storage.files fi
|
||||||
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
||||||
@@ -818,7 +812,7 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
||||||
fi.blob_hash,
|
fi.blob_hash,
|
||||||
fi.user_id,
|
|
||||||
fi.created_by, fi.updated_by
|
fi.created_by, fi.updated_by
|
||||||
FROM storage.files fi
|
FROM storage.files fi
|
||||||
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
||||||
@@ -836,10 +830,8 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
|
|
||||||
rows.into_iter()
|
rows.into_iter()
|
||||||
.map(
|
.map(
|
||||||
|(id, name, fid, fpath, size, mime, ca, ma, blob_hash, uid, cb, ub)| {
|
|(id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub)| {
|
||||||
Self::row_to_file(
|
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub)
|
||||||
id, name, fid, fpath, size, mime, ca, ma, blob_hash, uid, cb, ub,
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.collect()
|
.collect()
|
||||||
@@ -989,7 +981,6 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
i64,
|
i64,
|
||||||
i64,
|
i64,
|
||||||
String,
|
String,
|
||||||
Option<Uuid>,
|
|
||||||
Option<Uuid>, // created_by (§14)
|
Option<Uuid>, // created_by (§14)
|
||||||
Option<Uuid>, // updated_by (§14)
|
Option<Uuid>, // updated_by (§14)
|
||||||
),
|
),
|
||||||
@@ -1000,8 +991,7 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
||||||
fi.blob_hash,
|
fi.blob_hash,
|
||||||
fi.user_id,
|
fi.created_by, fi.updated_by
|
||||||
fi.created_by, fi.updated_by
|
|
||||||
FROM storage.files fi
|
FROM storage.files fi
|
||||||
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
||||||
WHERE fi.name = $1 AND fi.folder_id IS NULL
|
WHERE fi.name = $1 AND fi.folder_id IS NULL
|
||||||
@@ -1029,7 +1019,6 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
i64,
|
i64,
|
||||||
i64,
|
i64,
|
||||||
String,
|
String,
|
||||||
Option<Uuid>,
|
|
||||||
Option<Uuid>, // created_by (§14)
|
Option<Uuid>, // created_by (§14)
|
||||||
Option<Uuid>, // updated_by (§14)
|
Option<Uuid>, // updated_by (§14)
|
||||||
),
|
),
|
||||||
@@ -1040,8 +1029,7 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
||||||
fi.blob_hash,
|
fi.blob_hash,
|
||||||
fi.user_id,
|
fi.created_by, fi.updated_by
|
||||||
fi.created_by, fi.updated_by
|
|
||||||
FROM storage.files fi
|
FROM storage.files fi
|
||||||
JOIN storage.folders fo ON fo.id = fi.folder_id
|
JOIN storage.folders fo ON fo.id = fi.folder_id
|
||||||
WHERE fo.path = $1 AND fi.name = $2
|
WHERE fo.path = $1 AND fi.name = $2
|
||||||
@@ -1058,7 +1046,7 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
|
|
||||||
match row {
|
match row {
|
||||||
Some(r) => Ok(Some(Self::row_to_file(
|
Some(r) => Ok(Some(Self::row_to_file(
|
||||||
r.0, r.1, r.2, r.3, r.4, r.5, r.6, r.7, r.8, r.9, r.10, r.11,
|
r.0, r.1, r.2, r.3, r.4, r.5, r.6, r.7, r.8, r.9, r.10,
|
||||||
)?)),
|
)?)),
|
||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
}
|
}
|
||||||
@@ -1078,8 +1066,8 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
let stream = async_stream::try_stream! {
|
let stream = async_stream::try_stream! {
|
||||||
let mut row_stream = sqlx::query_as::<_, (
|
let mut row_stream = sqlx::query_as::<_, (
|
||||||
String, String, Option<String>, Option<String>,
|
String, String, Option<String>, Option<String>,
|
||||||
i64, String, i64, i64, String, Option<Uuid>,
|
i64, String, i64, i64, String,
|
||||||
Option<Uuid>, Option<Uuid>,
|
Option<Uuid>, Option<Uuid>, // created_by, updated_by (§14)
|
||||||
)>(
|
)>(
|
||||||
r#"
|
r#"
|
||||||
SELECT fi.id::text, fi.name, fi.folder_id::text, fo.path,
|
SELECT fi.id::text, fi.name, fi.folder_id::text, fo.path,
|
||||||
@@ -1087,8 +1075,7 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
||||||
fi.blob_hash,
|
fi.blob_hash,
|
||||||
fi.user_id,
|
fi.created_by, fi.updated_by
|
||||||
fi.created_by, fi.updated_by
|
|
||||||
FROM storage.files fi
|
FROM storage.files fi
|
||||||
JOIN storage.folders fo ON fo.id = fi.folder_id
|
JOIN storage.folders fo ON fo.id = fi.folder_id
|
||||||
WHERE fo.lpath <@ (SELECT lpath FROM storage.folders WHERE id = $1::uuid)
|
WHERE fo.lpath <@ (SELECT lpath FROM storage.folders WHERE id = $1::uuid)
|
||||||
@@ -1102,9 +1089,9 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
while let Some(row) = row_stream.try_next().await.map_err(|e| {
|
while let Some(row) = row_stream.try_next().await.map_err(|e| {
|
||||||
DomainError::internal_error("FileBlobRead", format!("subtree stream: {e}"))
|
DomainError::internal_error("FileBlobRead", format!("subtree stream: {e}"))
|
||||||
})? {
|
})? {
|
||||||
let (id, name, fid, fpath, size, mime, ca, ma, blob_hash, uid, cb, ub) = row;
|
let (id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub) = row;
|
||||||
let file = FileBlobReadRepository::row_to_file(
|
let file = FileBlobReadRepository::row_to_file(
|
||||||
id, name, fid, fpath, size, mime, ca, ma, blob_hash, uid, cb, ub,
|
id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub,
|
||||||
)?;
|
)?;
|
||||||
yield file;
|
yield file;
|
||||||
}
|
}
|
||||||
@@ -1171,7 +1158,7 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
EXTRACT(EPOCH FROM fi.created_at)::bigint, \
|
EXTRACT(EPOCH FROM fi.created_at)::bigint, \
|
||||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint, \
|
EXTRACT(EPOCH FROM fi.updated_at)::bigint, \
|
||||||
fi.blob_hash, \
|
fi.blob_hash, \
|
||||||
fi.user_id, \
|
\
|
||||||
fi.created_by, fi.updated_by, \
|
fi.created_by, fi.updated_by, \
|
||||||
COUNT(*) OVER() AS total_count \
|
COUNT(*) OVER() AS total_count \
|
||||||
FROM storage.files fi \
|
FROM storage.files fi \
|
||||||
@@ -1194,10 +1181,9 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
i64,
|
i64,
|
||||||
i64,
|
i64,
|
||||||
String,
|
String,
|
||||||
Option<Uuid>,
|
|
||||||
Option<Uuid>, // created_by (§14)
|
Option<Uuid>, // created_by (§14)
|
||||||
Option<Uuid>, // updated_by (§14)
|
Option<Uuid>, // updated_by (§14)
|
||||||
i64,
|
i64, // total_count
|
||||||
),
|
),
|
||||||
>(&sql)
|
>(&sql)
|
||||||
.bind(caller_id);
|
.bind(caller_id);
|
||||||
@@ -1219,15 +1205,13 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
.map_err(|e| DomainError::internal_error("FileBlobRead", format!("search: {e}")))?;
|
.map_err(|e| DomainError::internal_error("FileBlobRead", format!("search: {e}")))?;
|
||||||
|
|
||||||
// total_count is the same in every row; 0 when result set is empty.
|
// total_count is the same in every row; 0 when result set is empty.
|
||||||
let total_count = rows.first().map_or(0, |r| r.12) as usize;
|
let total_count = rows.first().map_or(0, |r| r.11) as usize;
|
||||||
|
|
||||||
let files = rows
|
let files = rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(
|
.map(
|
||||||
|(id, name, fid, fpath, size, mime, ca, ma, blob_hash, uid, cb, ub, _total)| {
|
|(id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub, _total)| {
|
||||||
Self::row_to_file(
|
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub)
|
||||||
id, name, fid, fpath, size, mime, ca, ma, blob_hash, uid, cb, ub,
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.collect::<Result<Vec<_>, _>>()
|
.collect::<Result<Vec<_>, _>>()
|
||||||
@@ -1306,7 +1290,7 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
EXTRACT(EPOCH FROM fi.created_at)::bigint, \
|
EXTRACT(EPOCH FROM fi.created_at)::bigint, \
|
||||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint, \
|
EXTRACT(EPOCH FROM fi.updated_at)::bigint, \
|
||||||
fi.blob_hash, \
|
fi.blob_hash, \
|
||||||
fi.user_id, \
|
\
|
||||||
fi.created_by, fi.updated_by, \
|
fi.created_by, fi.updated_by, \
|
||||||
COUNT(*) OVER() AS total_count \
|
COUNT(*) OVER() AS total_count \
|
||||||
FROM storage.files fi \
|
FROM storage.files fi \
|
||||||
@@ -1329,10 +1313,9 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
i64,
|
i64,
|
||||||
i64,
|
i64,
|
||||||
String,
|
String,
|
||||||
Option<Uuid>,
|
|
||||||
Option<Uuid>, // created_by (§14)
|
Option<Uuid>, // created_by (§14)
|
||||||
Option<Uuid>, // updated_by (§14)
|
Option<Uuid>, // updated_by (§14)
|
||||||
i64,
|
i64, // total_count
|
||||||
),
|
),
|
||||||
>(&sql)
|
>(&sql)
|
||||||
.bind(caller_id)
|
.bind(caller_id)
|
||||||
@@ -1352,15 +1335,13 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
DomainError::internal_error("FileBlobRead", format!("subtree search: {e}"))
|
DomainError::internal_error("FileBlobRead", format!("subtree search: {e}"))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let total_count = rows.first().map_or(0, |r| r.12) as usize;
|
let total_count = rows.first().map_or(0, |r| r.11) as usize;
|
||||||
|
|
||||||
let files = rows
|
let files = rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(
|
.map(
|
||||||
|(id, name, fid, fpath, size, mime, ca, ma, blob_hash, uid, cb, ub, _total)| {
|
|(id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub, _total)| {
|
||||||
Self::row_to_file(
|
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub)
|
||||||
id, name, fid, fpath, size, mime, ca, ma, blob_hash, uid, cb, ub,
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.collect::<Result<Vec<_>, _>>()
|
.collect::<Result<Vec<_>, _>>()
|
||||||
@@ -1402,7 +1383,7 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
||||||
fi.blob_hash,
|
fi.blob_hash,
|
||||||
fi.user_id,
|
|
||||||
fi.created_by, fi.updated_by
|
fi.created_by, fi.updated_by
|
||||||
FROM storage.files fi
|
FROM storage.files fi
|
||||||
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
||||||
@@ -1432,7 +1413,7 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
EXTRACT(EPOCH FROM fi.created_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
EXTRACT(EPOCH FROM fi.updated_at)::bigint,
|
||||||
fi.blob_hash,
|
fi.blob_hash,
|
||||||
fi.user_id,
|
|
||||||
fi.created_by, fi.updated_by
|
fi.created_by, fi.updated_by
|
||||||
FROM storage.files fi
|
FROM storage.files fi
|
||||||
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
LEFT JOIN storage.folders fo ON fo.id = fi.folder_id
|
||||||
@@ -1458,10 +1439,8 @@ impl FileReadPort for FileBlobReadRepository {
|
|||||||
|
|
||||||
rows.into_iter()
|
rows.into_iter()
|
||||||
.map(
|
.map(
|
||||||
|(id, name, fid, fpath, size, mime, ca, ma, blob_hash, uid, cb, ub)| {
|
|(id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub)| {
|
||||||
Self::row_to_file(
|
Self::row_to_file(id, name, fid, fpath, size, mime, ca, ma, blob_hash, cb, ub)
|
||||||
id, name, fid, fpath, size, mime, ca, ma, blob_hash, uid, cb, ub,
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.collect()
|
.collect()
|
||||||
|
|||||||
@@ -422,7 +422,6 @@ impl FileBlobWriteRepository {
|
|||||||
_,
|
_,
|
||||||
(
|
(
|
||||||
String,
|
String,
|
||||||
Option<Uuid>,
|
|
||||||
String,
|
String,
|
||||||
i64,
|
i64,
|
||||||
i64,
|
i64,
|
||||||
@@ -433,7 +432,7 @@ impl FileBlobWriteRepository {
|
|||||||
),
|
),
|
||||||
>(
|
>(
|
||||||
r#"
|
r#"
|
||||||
SELECT f.id::text, f.user_id, fo.path,
|
SELECT f.id::text, fo.path,
|
||||||
EXTRACT(EPOCH FROM f.created_at)::bigint,
|
EXTRACT(EPOCH FROM f.created_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM f.updated_at)::bigint,
|
EXTRACT(EPOCH FROM f.updated_at)::bigint,
|
||||||
f.created_by, f.updated_by, f.size, f.mime_type
|
f.created_by, f.updated_by, f.size, f.mime_type
|
||||||
@@ -457,7 +456,6 @@ impl FileBlobWriteRepository {
|
|||||||
|
|
||||||
let Some((
|
let Some((
|
||||||
id,
|
id,
|
||||||
user_id,
|
|
||||||
folder_path,
|
folder_path,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
@@ -479,7 +477,7 @@ impl FileBlobWriteRepository {
|
|||||||
mime_type,
|
mime_type,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
user_id,
|
None,
|
||||||
blob_hash.to_string(),
|
blob_hash.to_string(),
|
||||||
created_by,
|
created_by,
|
||||||
updated_by,
|
updated_by,
|
||||||
@@ -532,11 +530,10 @@ impl FileWritePort for FileBlobWriteRepository {
|
|||||||
>(
|
>(
|
||||||
r#"
|
r#"
|
||||||
WITH dest AS (
|
WITH dest AS (
|
||||||
SELECT user_id, drive_id FROM storage.folders WHERE id = $1::uuid
|
SELECT drive_id FROM storage.folders WHERE id = $1::uuid
|
||||||
)
|
)
|
||||||
UPDATE storage.files f
|
UPDATE storage.files f
|
||||||
SET folder_id = $1::uuid,
|
SET folder_id = $1::uuid,
|
||||||
user_id = COALESCE((SELECT user_id FROM dest), f.user_id),
|
|
||||||
drive_id = COALESCE((SELECT drive_id FROM dest), f.drive_id),
|
drive_id = COALESCE((SELECT drive_id FROM dest), f.drive_id),
|
||||||
updated_at = NOW(),
|
updated_at = NOW(),
|
||||||
updated_by = $3
|
updated_by = $3
|
||||||
|
|||||||
@@ -21,23 +21,22 @@ use crate::domain::services::authorization::ResourceKind;
|
|||||||
use crate::domain::services::path_service::StoragePath;
|
use crate::domain::services::path_service::StoragePath;
|
||||||
|
|
||||||
/// Type alias for folder metadata rows from SQL queries.
|
/// Type alias for folder metadata rows from SQL queries.
|
||||||
/// Tuple order: id, name, path, parent_id, user_id, drive_id,
|
/// Tuple order: id, name, path, parent_id, drive_id, created_at,
|
||||||
/// created_at, modified_at, tree_modified_at, created_by, updated_by.
|
/// modified_at, tree_modified_at, created_by, updated_by.
|
||||||
/// The trailing `tree_modified_at` feeds [`Folder::etag`] — every
|
/// The trailing `tree_modified_at` feeds [`Folder::etag`] — every
|
||||||
/// SELECT here must include `EXTRACT(EPOCH FROM tree_modified_at)::bigint`.
|
/// SELECT here must include `EXTRACT(EPOCH FROM tree_modified_at)::bigint`.
|
||||||
/// `drive_id` is the post-D0 `NOT NULL` scope axis for path-based
|
/// `drive_id` is the post-D0 `NOT NULL` scope axis for path-based
|
||||||
/// lookups. `created_by` / `updated_by` are the §14 provenance
|
/// lookups. `created_by` / `updated_by` are the §14 provenance
|
||||||
/// columns, nullable because the FK is `ON DELETE SET NULL`.
|
/// columns, nullable because the FK is `ON DELETE SET NULL`.
|
||||||
///
|
///
|
||||||
/// Post-D7 `user_id` is `Option<Uuid>` — the column is nullable and
|
/// Post-D7-step-6: `storage.folders.user_id` dropped, so the tuple
|
||||||
/// left NULL on new rows (see migration
|
/// no longer carries it. The domain entity's `user_id` field is
|
||||||
/// `20260902000000_files_folders_user_id_nullable.sql`).
|
/// populated with `None` at `row_to_folder` construction.
|
||||||
type FolderRow = (
|
type FolderRow = (
|
||||||
String,
|
String,
|
||||||
String,
|
String,
|
||||||
String,
|
String,
|
||||||
Option<String>,
|
Option<String>,
|
||||||
Option<Uuid>,
|
|
||||||
Uuid,
|
Uuid,
|
||||||
i64,
|
i64,
|
||||||
i64,
|
i64,
|
||||||
@@ -48,13 +47,12 @@ type FolderRow = (
|
|||||||
|
|
||||||
/// Type alias for paginated folder rows (includes total_count as
|
/// Type alias for paginated folder rows (includes total_count as
|
||||||
/// the last element after the §14 provenance columns). Same
|
/// the last element after the §14 provenance columns). Same
|
||||||
/// nullability semantics as [`FolderRow`].
|
/// column set as [`FolderRow`] plus the trailing count.
|
||||||
type FolderRowPaginated = (
|
type FolderRowPaginated = (
|
||||||
String,
|
String,
|
||||||
String,
|
String,
|
||||||
String,
|
String,
|
||||||
Option<String>,
|
Option<String>,
|
||||||
Option<Uuid>,
|
|
||||||
Uuid,
|
Uuid,
|
||||||
i64,
|
i64,
|
||||||
i64,
|
i64,
|
||||||
@@ -64,23 +62,6 @@ type FolderRowPaginated = (
|
|||||||
i64,
|
i64,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Type alias for folder rows with optional user_id — kept as a
|
|
||||||
/// separate name for the search-shape SELECTs.
|
|
||||||
/// Includes the §14 provenance columns `created_by` / `updated_by`.
|
|
||||||
type FolderRowOptUser = (
|
|
||||||
String,
|
|
||||||
String,
|
|
||||||
String,
|
|
||||||
Option<String>,
|
|
||||||
Option<Uuid>,
|
|
||||||
Uuid,
|
|
||||||
i64,
|
|
||||||
i64,
|
|
||||||
i64,
|
|
||||||
Option<Uuid>,
|
|
||||||
Option<Uuid>,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// SQL `EXISTS (…)` predicate — true when the caller (bound to `$1`) has
|
/// SQL `EXISTS (…)` predicate — true when the caller (bound to `$1`) has
|
||||||
/// any active `role_grants` on the drive owning `fo` (the aliased folder
|
/// any active `role_grants` on the drive owning `fo` (the aliased folder
|
||||||
/// row). Group memberships (direct + transitive) are expanded inline via
|
/// row). Group memberships (direct + transitive) are expanded inline via
|
||||||
@@ -148,13 +129,17 @@ impl FolderDbRepository {
|
|||||||
/// extra queries needed. `created_by` / `updated_by` carry the
|
/// extra queries needed. `created_by` / `updated_by` carry the
|
||||||
/// §14 provenance signal through the entity layer; both are
|
/// §14 provenance signal through the entity layer; both are
|
||||||
/// `Option<Uuid>` because the FK is `ON DELETE SET NULL`.
|
/// `Option<Uuid>` because the FK is `ON DELETE SET NULL`.
|
||||||
|
///
|
||||||
|
/// Post-D7-step-6: the `storage.folders.user_id` column is gone;
|
||||||
|
/// the entity's legacy `user_id` field is populated with `None`
|
||||||
|
/// at construction time (removed in the follow-up entity
|
||||||
|
/// cleanup PR).
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn row_to_folder(
|
fn row_to_folder(
|
||||||
id: String,
|
id: String,
|
||||||
name: String,
|
name: String,
|
||||||
path: String,
|
path: String,
|
||||||
parent_id: Option<String>,
|
parent_id: Option<String>,
|
||||||
user_id: Option<Uuid>,
|
|
||||||
drive_id: Uuid,
|
drive_id: Uuid,
|
||||||
created_at: i64,
|
created_at: i64,
|
||||||
modified_at: i64,
|
modified_at: i64,
|
||||||
@@ -168,7 +153,7 @@ impl FolderDbRepository {
|
|||||||
name,
|
name,
|
||||||
storage_path,
|
storage_path,
|
||||||
parent_id,
|
parent_id,
|
||||||
user_id,
|
None, // Post-D7: `folders.user_id` column dropped.
|
||||||
drive_id,
|
drive_id,
|
||||||
created_at as u64,
|
created_at as u64,
|
||||||
modified_at as u64,
|
modified_at as u64,
|
||||||
@@ -192,7 +177,7 @@ impl FolderDbRepository {
|
|||||||
|
|
||||||
let rows = sqlx::query_as::<_, FolderRow>(
|
let rows = sqlx::query_as::<_, FolderRow>(
|
||||||
r#"
|
r#"
|
||||||
SELECT id::text, name, path, parent_id::text, user_id, drive_id,
|
SELECT id::text, name, path, parent_id::text, drive_id,
|
||||||
EXTRACT(EPOCH FROM created_at)::bigint,
|
EXTRACT(EPOCH FROM created_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM updated_at)::bigint,
|
EXTRACT(EPOCH FROM updated_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM tree_modified_at)::bigint,
|
EXTRACT(EPOCH FROM tree_modified_at)::bigint,
|
||||||
@@ -207,7 +192,7 @@ impl FolderDbRepository {
|
|||||||
.map_err(|e| DomainError::internal_error("FolderDb", format!("get_folders_by_ids: {e}")))?;
|
.map_err(|e| DomainError::internal_error("FolderDb", format!("get_folders_by_ids: {e}")))?;
|
||||||
|
|
||||||
rows.into_iter()
|
rows.into_iter()
|
||||||
.map(|r| Self::row_to_folder(r.0, r.1, r.2, r.3, r.4, r.5, r.6, r.7, r.8, r.9, r.10))
|
.map(|r| Self::row_to_folder(r.0, r.1, r.2, r.3, r.4, r.5, r.6, r.7, r.8, r.9))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -290,9 +275,7 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
Self::row_to_folder(
|
Self::row_to_folder(
|
||||||
row.0, name, row.1, parent_id,
|
row.0, name, row.1, parent_id, drive_id, row.2, row.3, row.4,
|
||||||
None, // Post-D7: `folders.user_id` no longer written on new rows.
|
|
||||||
drive_id, row.2, row.3, row.4,
|
|
||||||
// Fresh from RETURNING — caller_id was bound to both columns.
|
// Fresh from RETURNING — caller_id was bound to both columns.
|
||||||
row.5, row.6,
|
row.5, row.6,
|
||||||
)
|
)
|
||||||
@@ -301,7 +284,7 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
async fn get_folder(&self, id: &str) -> Result<Folder, DomainError> {
|
async fn get_folder(&self, id: &str) -> Result<Folder, DomainError> {
|
||||||
let row = sqlx::query_as::<_, FolderRow>(
|
let row = sqlx::query_as::<_, FolderRow>(
|
||||||
r#"
|
r#"
|
||||||
SELECT id::text, name, path, parent_id::text, user_id, drive_id,
|
SELECT id::text, name, path, parent_id::text, drive_id,
|
||||||
EXTRACT(EPOCH FROM created_at)::bigint,
|
EXTRACT(EPOCH FROM created_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM updated_at)::bigint,
|
EXTRACT(EPOCH FROM updated_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM tree_modified_at)::bigint,
|
EXTRACT(EPOCH FROM tree_modified_at)::bigint,
|
||||||
@@ -317,7 +300,7 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
.ok_or_else(|| DomainError::not_found("Folder", id))?;
|
.ok_or_else(|| DomainError::not_found("Folder", id))?;
|
||||||
|
|
||||||
Self::row_to_folder(
|
Self::row_to_folder(
|
||||||
row.0, row.1, row.2, row.3, row.4, row.5, row.6, row.7, row.8, row.9, row.10,
|
row.0, row.1, row.2, row.3, row.4, row.5, row.6, row.7, row.8, row.9,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,7 +326,7 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
// wrapper scoping post-D0).
|
// wrapper scoping post-D0).
|
||||||
let row = sqlx::query_as::<_, FolderRow>(
|
let row = sqlx::query_as::<_, FolderRow>(
|
||||||
r#"
|
r#"
|
||||||
SELECT id::text, name, path, parent_id::text, user_id, drive_id,
|
SELECT id::text, name, path, parent_id::text, drive_id,
|
||||||
EXTRACT(EPOCH FROM created_at)::bigint,
|
EXTRACT(EPOCH FROM created_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM updated_at)::bigint,
|
EXTRACT(EPOCH FROM updated_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM tree_modified_at)::bigint,
|
EXTRACT(EPOCH FROM tree_modified_at)::bigint,
|
||||||
@@ -360,7 +343,7 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
.ok_or_else(|| DomainError::not_found("Folder", lookup))?;
|
.ok_or_else(|| DomainError::not_found("Folder", lookup))?;
|
||||||
|
|
||||||
Self::row_to_folder(
|
Self::row_to_folder(
|
||||||
row.0, row.1, row.2, row.3, row.4, row.5, row.6, row.7, row.8, row.9, row.10,
|
row.0, row.1, row.2, row.3, row.4, row.5, row.6, row.7, row.8, row.9,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,7 +352,7 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
let rows: Vec<FolderRow> = if let Some(pid) = parent_id {
|
let rows: Vec<FolderRow> = if let Some(pid) = parent_id {
|
||||||
sqlx::query_as(
|
sqlx::query_as(
|
||||||
r#"
|
r#"
|
||||||
SELECT id::text, name, path, parent_id::text, user_id, drive_id,
|
SELECT id::text, name, path, parent_id::text, drive_id,
|
||||||
EXTRACT(EPOCH FROM created_at)::bigint,
|
EXTRACT(EPOCH FROM created_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM updated_at)::bigint,
|
EXTRACT(EPOCH FROM updated_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM tree_modified_at)::bigint,
|
EXTRACT(EPOCH FROM tree_modified_at)::bigint,
|
||||||
@@ -385,7 +368,7 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
} else {
|
} else {
|
||||||
sqlx::query_as(
|
sqlx::query_as(
|
||||||
r#"
|
r#"
|
||||||
SELECT id::text, name, path, parent_id::text, user_id, drive_id,
|
SELECT id::text, name, path, parent_id::text, drive_id,
|
||||||
EXTRACT(EPOCH FROM created_at)::bigint,
|
EXTRACT(EPOCH FROM created_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM updated_at)::bigint,
|
EXTRACT(EPOCH FROM updated_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM tree_modified_at)::bigint,
|
EXTRACT(EPOCH FROM tree_modified_at)::bigint,
|
||||||
@@ -401,8 +384,8 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
.map_err(|e| DomainError::internal_error("FolderDb", format!("list: {e}")))?;
|
.map_err(|e| DomainError::internal_error("FolderDb", format!("list: {e}")))?;
|
||||||
|
|
||||||
rows.into_iter()
|
rows.into_iter()
|
||||||
.map(|(id, name, path, pid, uid, did, ca, ma, tma, cb, ub)| {
|
.map(|(id, name, path, pid, did, ca, ma, tma, cb, ub)| {
|
||||||
Self::row_to_folder(id, name, path, pid, uid, did, ca, ma, tma, cb, ub)
|
Self::row_to_folder(id, name, path, pid, did, ca, ma, tma, cb, ub)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -429,7 +412,7 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
// `/api/drives::caller_role` via `folder.drive_id`.
|
// `/api/drives::caller_role` via `folder.drive_id`.
|
||||||
let sql = format!(
|
let sql = format!(
|
||||||
"SELECT fo.id::text, fo.name, fo.path, fo.parent_id::text, \
|
"SELECT fo.id::text, fo.name, fo.path, fo.parent_id::text, \
|
||||||
fo.user_id, fo.drive_id, \
|
fo.drive_id, \
|
||||||
EXTRACT(EPOCH FROM fo.created_at)::bigint, \
|
EXTRACT(EPOCH FROM fo.created_at)::bigint, \
|
||||||
EXTRACT(EPOCH FROM fo.updated_at)::bigint, \
|
EXTRACT(EPOCH FROM fo.updated_at)::bigint, \
|
||||||
EXTRACT(EPOCH FROM fo.tree_modified_at)::bigint, \
|
EXTRACT(EPOCH FROM fo.tree_modified_at)::bigint, \
|
||||||
@@ -449,8 +432,8 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
rows.into_iter()
|
rows.into_iter()
|
||||||
.map(|(id, name, path, pid, uid, did, ca, ma, tma, cb, ub)| {
|
.map(|(id, name, path, pid, did, ca, ma, tma, cb, ub)| {
|
||||||
Self::row_to_folder(id, name, path, pid, uid, did, ca, ma, tma, cb, ub)
|
Self::row_to_folder(id, name, path, pid, did, ca, ma, tma, cb, ub)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -469,7 +452,7 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
let rows: Vec<FolderRowPaginated> = if let Some(pid) = parent_id {
|
let rows: Vec<FolderRowPaginated> = if let Some(pid) = parent_id {
|
||||||
sqlx::query_as(
|
sqlx::query_as(
|
||||||
r#"
|
r#"
|
||||||
SELECT id::text, name, path, parent_id::text, user_id, drive_id,
|
SELECT id::text, name, path, parent_id::text, drive_id,
|
||||||
EXTRACT(EPOCH FROM created_at)::bigint,
|
EXTRACT(EPOCH FROM created_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM updated_at)::bigint,
|
EXTRACT(EPOCH FROM updated_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM tree_modified_at)::bigint,
|
EXTRACT(EPOCH FROM tree_modified_at)::bigint,
|
||||||
@@ -489,7 +472,7 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
} else {
|
} else {
|
||||||
sqlx::query_as(
|
sqlx::query_as(
|
||||||
r#"
|
r#"
|
||||||
SELECT id::text, name, path, parent_id::text, user_id, drive_id,
|
SELECT id::text, name, path, parent_id::text, drive_id,
|
||||||
EXTRACT(EPOCH FROM created_at)::bigint,
|
EXTRACT(EPOCH FROM created_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM updated_at)::bigint,
|
EXTRACT(EPOCH FROM updated_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM tree_modified_at)::bigint,
|
EXTRACT(EPOCH FROM tree_modified_at)::bigint,
|
||||||
@@ -510,18 +493,16 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
|
|
||||||
// total_count is identical in every row; 0 when the result set is empty.
|
// total_count is identical in every row; 0 when the result set is empty.
|
||||||
let total = if include_total {
|
let total = if include_total {
|
||||||
Some(rows.first().map_or(0, |r| r.11) as usize)
|
Some(rows.first().map_or(0, |r| r.10) as usize)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let folders: Result<Vec<Folder>, DomainError> = rows
|
let folders: Result<Vec<Folder>, DomainError> = rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(
|
.map(|(id, name, path, pid, did, ca, ma, tma, cb, ub, _total)| {
|
||||||
|(id, name, path, pid, uid, did, ca, ma, tma, cb, ub, _total)| {
|
Self::row_to_folder(id, name, path, pid, did, ca, ma, tma, cb, ub)
|
||||||
Self::row_to_folder(id, name, path, pid, uid, did, ca, ma, tma, cb, ub)
|
})
|
||||||
},
|
|
||||||
)
|
|
||||||
.collect();
|
.collect();
|
||||||
Ok((folders?, total))
|
Ok((folders?, total))
|
||||||
}
|
}
|
||||||
@@ -539,7 +520,7 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
) -> Result<(Vec<Folder>, Option<usize>), DomainError> {
|
) -> Result<(Vec<Folder>, Option<usize>), DomainError> {
|
||||||
let sql = format!(
|
let sql = format!(
|
||||||
"SELECT fo.id::text, fo.name, fo.path, fo.parent_id::text, \
|
"SELECT fo.id::text, fo.name, fo.path, fo.parent_id::text, \
|
||||||
fo.user_id, fo.drive_id, \
|
fo.drive_id, \
|
||||||
EXTRACT(EPOCH FROM fo.created_at)::bigint, \
|
EXTRACT(EPOCH FROM fo.created_at)::bigint, \
|
||||||
EXTRACT(EPOCH FROM fo.updated_at)::bigint, \
|
EXTRACT(EPOCH FROM fo.updated_at)::bigint, \
|
||||||
EXTRACT(EPOCH FROM fo.tree_modified_at)::bigint, \
|
EXTRACT(EPOCH FROM fo.tree_modified_at)::bigint, \
|
||||||
@@ -563,18 +544,16 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
let total = if include_total {
|
let total = if include_total {
|
||||||
Some(rows.first().map_or(0, |r| r.11) as usize)
|
Some(rows.first().map_or(0, |r| r.10) as usize)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let folders: Result<Vec<Folder>, DomainError> = rows
|
let folders: Result<Vec<Folder>, DomainError> = rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(
|
.map(|(id, name, path, pid, did, ca, ma, tma, cb, ub, _total)| {
|
||||||
|(id, name, path, pid, uid, did, ca, ma, tma, cb, ub, _total)| {
|
Self::row_to_folder(id, name, path, pid, did, ca, ma, tma, cb, ub)
|
||||||
Self::row_to_folder(id, name, path, pid, uid, did, ca, ma, tma, cb, ub)
|
})
|
||||||
},
|
|
||||||
)
|
|
||||||
.collect();
|
.collect();
|
||||||
Ok((folders?, total))
|
Ok((folders?, total))
|
||||||
}
|
}
|
||||||
@@ -602,7 +581,7 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
UPDATE storage.folders
|
UPDATE storage.folders
|
||||||
SET name = $1, updated_at = NOW(), updated_by = $3
|
SET name = $1, updated_at = NOW(), updated_by = $3
|
||||||
WHERE id = $2::uuid AND NOT is_trashed
|
WHERE id = $2::uuid AND NOT is_trashed
|
||||||
RETURNING id::text, name, path, parent_id::text, user_id, drive_id,
|
RETURNING id::text, name, path, parent_id::text, drive_id,
|
||||||
EXTRACT(EPOCH FROM created_at)::bigint,
|
EXTRACT(EPOCH FROM created_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM updated_at)::bigint,
|
EXTRACT(EPOCH FROM updated_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM tree_modified_at)::bigint,
|
EXTRACT(EPOCH FROM tree_modified_at)::bigint,
|
||||||
@@ -626,7 +605,7 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
.ok_or_else(|| DomainError::not_found("Folder", id))?;
|
.ok_or_else(|| DomainError::not_found("Folder", id))?;
|
||||||
|
|
||||||
Self::row_to_folder(
|
Self::row_to_folder(
|
||||||
row.0, row.1, row.2, row.3, row.4, row.5, row.6, row.7, row.8, row.9, row.10,
|
row.0, row.1, row.2, row.3, row.4, row.5, row.6, row.7, row.8, row.9,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -663,7 +642,7 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
updated_at = NOW(),
|
updated_at = NOW(),
|
||||||
updated_by = $3
|
updated_by = $3
|
||||||
WHERE f.id = $2::uuid AND NOT f.is_trashed
|
WHERE f.id = $2::uuid AND NOT f.is_trashed
|
||||||
RETURNING f.id::text, f.name, f.path, f.parent_id::text, f.user_id, f.drive_id,
|
RETURNING f.id::text, f.name, f.path, f.parent_id::text, f.drive_id,
|
||||||
EXTRACT(EPOCH FROM f.created_at)::bigint,
|
EXTRACT(EPOCH FROM f.created_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM f.updated_at)::bigint,
|
EXTRACT(EPOCH FROM f.updated_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM f.tree_modified_at)::bigint,
|
EXTRACT(EPOCH FROM f.tree_modified_at)::bigint,
|
||||||
@@ -680,7 +659,7 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
.ok_or_else(|| DomainError::not_found("Folder", id))?;
|
.ok_or_else(|| DomainError::not_found("Folder", id))?;
|
||||||
|
|
||||||
Self::row_to_folder(
|
Self::row_to_folder(
|
||||||
row.0, row.1, row.2, row.3, row.4, row.5, row.6, row.7, row.8, row.9, row.10,
|
row.0, row.1, row.2, row.3, row.4, row.5, row.6, row.7, row.8, row.9,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -963,7 +942,7 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
async fn list_subtree_folders(&self, folder_id: &str) -> Result<Vec<Folder>, DomainError> {
|
async fn list_subtree_folders(&self, folder_id: &str) -> Result<Vec<Folder>, DomainError> {
|
||||||
let sql = "SELECT fo.id::text, fo.name, fo.path, fo.parent_id::text, \
|
let sql = "SELECT fo.id::text, fo.name, fo.path, fo.parent_id::text, \
|
||||||
fo.user_id, fo.drive_id, \
|
fo.drive_id, \
|
||||||
EXTRACT(EPOCH FROM fo.created_at)::bigint, \
|
EXTRACT(EPOCH FROM fo.created_at)::bigint, \
|
||||||
EXTRACT(EPOCH FROM fo.updated_at)::bigint, \
|
EXTRACT(EPOCH FROM fo.updated_at)::bigint, \
|
||||||
EXTRACT(EPOCH FROM fo.tree_modified_at)::bigint, \
|
EXTRACT(EPOCH FROM fo.tree_modified_at)::bigint, \
|
||||||
@@ -973,7 +952,7 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
AND fo.lpath <@ (SELECT lpath FROM storage.folders WHERE id = $1::uuid) \
|
AND fo.lpath <@ (SELECT lpath FROM storage.folders WHERE id = $1::uuid) \
|
||||||
ORDER BY fo.path";
|
ORDER BY fo.path";
|
||||||
|
|
||||||
let rows: Vec<FolderRowOptUser> = sqlx::query_as(sql)
|
let rows: Vec<FolderRow> = sqlx::query_as(sql)
|
||||||
.bind(folder_id)
|
.bind(folder_id)
|
||||||
.fetch_all(self.pool())
|
.fetch_all(self.pool())
|
||||||
.await
|
.await
|
||||||
@@ -982,8 +961,8 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
rows.into_iter()
|
rows.into_iter()
|
||||||
.map(|(id, name, path, pid, uid, did, ca, ma, tma, cb, ub)| {
|
.map(|(id, name, path, pid, did, ca, ma, tma, cb, ub)| {
|
||||||
Self::row_to_folder(id, name, path, pid, uid, did, ca, ma, tma, cb, ub)
|
Self::row_to_folder(id, name, path, pid, did, ca, ma, tma, cb, ub)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -1030,7 +1009,7 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
// Recursive, no folder scope → ALL folders in caller's readable drives
|
// Recursive, no folder scope → ALL folders in caller's readable drives
|
||||||
let sql = format!(
|
let sql = format!(
|
||||||
"SELECT fo.id::text, fo.name, fo.path, fo.parent_id::text, \
|
"SELECT fo.id::text, fo.name, fo.path, fo.parent_id::text, \
|
||||||
fo.user_id, fo.drive_id, \
|
fo.drive_id, \
|
||||||
EXTRACT(EPOCH FROM fo.created_at)::bigint, \
|
EXTRACT(EPOCH FROM fo.created_at)::bigint, \
|
||||||
EXTRACT(EPOCH FROM fo.updated_at)::bigint, \
|
EXTRACT(EPOCH FROM fo.updated_at)::bigint, \
|
||||||
EXTRACT(EPOCH FROM fo.tree_modified_at)::bigint, \
|
EXTRACT(EPOCH FROM fo.tree_modified_at)::bigint, \
|
||||||
@@ -1042,7 +1021,7 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
ORDER BY fo.name"
|
ORDER BY fo.name"
|
||||||
);
|
);
|
||||||
|
|
||||||
let rows: Vec<FolderRowOptUser> = if let Some(ref pattern) = name_pattern {
|
let rows: Vec<FolderRow> = if let Some(ref pattern) = name_pattern {
|
||||||
sqlx::query_as(&sql)
|
sqlx::query_as(&sql)
|
||||||
.bind(caller_id)
|
.bind(caller_id)
|
||||||
.bind(pattern)
|
.bind(pattern)
|
||||||
@@ -1058,8 +1037,8 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
|
|
||||||
return rows
|
return rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(id, name, path, pid, uid, did, ca, ma, tma, cb, ub)| {
|
.map(|(id, name, path, pid, did, ca, ma, tma, cb, ub)| {
|
||||||
Self::row_to_folder(id, name, path, pid, uid, did, ca, ma, tma, cb, ub)
|
Self::row_to_folder(id, name, path, pid, did, ca, ma, tma, cb, ub)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
@@ -1069,7 +1048,7 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
let sql = if parent_id.is_some() {
|
let sql = if parent_id.is_some() {
|
||||||
format!(
|
format!(
|
||||||
"SELECT fo.id::text, fo.name, fo.path, fo.parent_id::text, \
|
"SELECT fo.id::text, fo.name, fo.path, fo.parent_id::text, \
|
||||||
fo.user_id, fo.drive_id, \
|
fo.drive_id, \
|
||||||
EXTRACT(EPOCH FROM fo.created_at)::bigint, \
|
EXTRACT(EPOCH FROM fo.created_at)::bigint, \
|
||||||
EXTRACT(EPOCH FROM fo.updated_at)::bigint, \
|
EXTRACT(EPOCH FROM fo.updated_at)::bigint, \
|
||||||
EXTRACT(EPOCH FROM fo.tree_modified_at)::bigint, \
|
EXTRACT(EPOCH FROM fo.tree_modified_at)::bigint, \
|
||||||
@@ -1089,7 +1068,7 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
};
|
};
|
||||||
format!(
|
format!(
|
||||||
"SELECT fo.id::text, fo.name, fo.path, fo.parent_id::text, \
|
"SELECT fo.id::text, fo.name, fo.path, fo.parent_id::text, \
|
||||||
fo.user_id, fo.drive_id, \
|
fo.drive_id, \
|
||||||
EXTRACT(EPOCH FROM fo.created_at)::bigint, \
|
EXTRACT(EPOCH FROM fo.created_at)::bigint, \
|
||||||
EXTRACT(EPOCH FROM fo.updated_at)::bigint, \
|
EXTRACT(EPOCH FROM fo.updated_at)::bigint, \
|
||||||
EXTRACT(EPOCH FROM fo.tree_modified_at)::bigint, \
|
EXTRACT(EPOCH FROM fo.tree_modified_at)::bigint, \
|
||||||
@@ -1103,7 +1082,7 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let rows: Vec<FolderRowOptUser> = if let Some(pid) = parent_id {
|
let rows: Vec<FolderRow> = if let Some(pid) = parent_id {
|
||||||
if let Some(ref pattern) = name_pattern {
|
if let Some(ref pattern) = name_pattern {
|
||||||
sqlx::query_as(&sql)
|
sqlx::query_as(&sql)
|
||||||
.bind(caller_id)
|
.bind(caller_id)
|
||||||
@@ -1133,8 +1112,8 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
.map_err(|e| DomainError::internal_error("FolderDb", format!("search_folders: {e}")))?;
|
.map_err(|e| DomainError::internal_error("FolderDb", format!("search_folders: {e}")))?;
|
||||||
|
|
||||||
rows.into_iter()
|
rows.into_iter()
|
||||||
.map(|(id, name, path, pid, uid, did, ca, ma, tma, cb, ub)| {
|
.map(|(id, name, path, pid, did, ca, ma, tma, cb, ub)| {
|
||||||
Self::row_to_folder(id, name, path, pid, uid, did, ca, ma, tma, cb, ub)
|
Self::row_to_folder(id, name, path, pid, did, ca, ma, tma, cb, ub)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -1162,7 +1141,7 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
|
|
||||||
let sql = format!(
|
let sql = format!(
|
||||||
"SELECT fo.id::text, fo.name, fo.path, fo.parent_id::text, \
|
"SELECT fo.id::text, fo.name, fo.path, fo.parent_id::text, \
|
||||||
fo.user_id, fo.drive_id, \
|
fo.drive_id, \
|
||||||
EXTRACT(EPOCH FROM fo.created_at)::bigint, \
|
EXTRACT(EPOCH FROM fo.created_at)::bigint, \
|
||||||
EXTRACT(EPOCH FROM fo.updated_at)::bigint, \
|
EXTRACT(EPOCH FROM fo.updated_at)::bigint, \
|
||||||
EXTRACT(EPOCH FROM fo.tree_modified_at)::bigint, \
|
EXTRACT(EPOCH FROM fo.tree_modified_at)::bigint, \
|
||||||
@@ -1176,7 +1155,7 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
ORDER BY fo.name"
|
ORDER BY fo.name"
|
||||||
);
|
);
|
||||||
|
|
||||||
let rows: Vec<FolderRowOptUser> = if let Some(ref pattern) = name_pattern {
|
let rows: Vec<FolderRow> = if let Some(ref pattern) = name_pattern {
|
||||||
sqlx::query_as(&sql)
|
sqlx::query_as(&sql)
|
||||||
.bind(caller_id)
|
.bind(caller_id)
|
||||||
.bind(folder_id)
|
.bind(folder_id)
|
||||||
@@ -1193,8 +1172,8 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
.map_err(|e| DomainError::internal_error("FolderDb", format!("descendant search: {e}")))?;
|
.map_err(|e| DomainError::internal_error("FolderDb", format!("descendant search: {e}")))?;
|
||||||
|
|
||||||
rows.into_iter()
|
rows.into_iter()
|
||||||
.map(|(id, name, path, pid, uid, did, ca, ma, tma, cb, ub)| {
|
.map(|(id, name, path, pid, did, ca, ma, tma, cb, ub)| {
|
||||||
Self::row_to_folder(id, name, path, pid, uid, did, ca, ma, tma, cb, ub)
|
Self::row_to_folder(id, name, path, pid, did, ca, ma, tma, cb, ub)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -1212,7 +1191,7 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
let rows: Vec<FolderRow> = if let Some(pid) = parent_id {
|
let rows: Vec<FolderRow> = if let Some(pid) = parent_id {
|
||||||
sqlx::query_as(
|
sqlx::query_as(
|
||||||
r#"
|
r#"
|
||||||
SELECT id::text, name, path, parent_id::text, user_id, drive_id,
|
SELECT id::text, name, path, parent_id::text, drive_id,
|
||||||
EXTRACT(EPOCH FROM created_at)::bigint,
|
EXTRACT(EPOCH FROM created_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM updated_at)::bigint,
|
EXTRACT(EPOCH FROM updated_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM tree_modified_at)::bigint,
|
EXTRACT(EPOCH FROM tree_modified_at)::bigint,
|
||||||
@@ -1239,7 +1218,7 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
} else {
|
} else {
|
||||||
sqlx::query_as(
|
sqlx::query_as(
|
||||||
r#"
|
r#"
|
||||||
SELECT id::text, name, path, parent_id::text, user_id, drive_id,
|
SELECT id::text, name, path, parent_id::text, drive_id,
|
||||||
EXTRACT(EPOCH FROM created_at)::bigint,
|
EXTRACT(EPOCH FROM created_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM updated_at)::bigint,
|
EXTRACT(EPOCH FROM updated_at)::bigint,
|
||||||
EXTRACT(EPOCH FROM tree_modified_at)::bigint,
|
EXTRACT(EPOCH FROM tree_modified_at)::bigint,
|
||||||
@@ -1266,8 +1245,8 @@ impl FolderRepository for FolderDbRepository {
|
|||||||
.map_err(|e| DomainError::internal_error("FolderDb", format!("suggest: {e}")))?;
|
.map_err(|e| DomainError::internal_error("FolderDb", format!("suggest: {e}")))?;
|
||||||
|
|
||||||
rows.into_iter()
|
rows.into_iter()
|
||||||
.map(|(id, name, path, pid, uid, did, ca, ma, tma, cb, ub)| {
|
.map(|(id, name, path, pid, did, ca, ma, tma, cb, ub)| {
|
||||||
Self::row_to_folder(id, name, path, pid, uid, did, ca, ma, tma, cb, ub)
|
Self::row_to_folder(id, name, path, pid, did, ca, ma, tma, cb, ub)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,31 +123,19 @@ impl TrashRepository for TrashDbRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get_trash_items(&self, user_id: &Uuid) -> Result<Vec<TrashedItem>> {
|
async fn get_trash_items(&self, user_id: &Uuid) -> Result<Vec<TrashedItem>> {
|
||||||
// Post-D7: the `WHERE t.user_id = $1` filter no longer works —
|
// Post-D7: the `WHERE t.user_id = $1` filter is gone — the
|
||||||
// new rows land with `user_id = NULL`, so the trash view's
|
// `user_id` column was dropped from `storage.{files,folders}`
|
||||||
// `user_id` projection is nullable and can't be the scope
|
// and the view no longer projects it. Scope is drive-membership
|
||||||
// axis any more. Filter by drive-membership instead: any
|
// via role_grants; group memberships expand inline through
|
||||||
// trashed item in a drive the caller has any role_grant on.
|
|
||||||
// Group memberships expand inline via
|
|
||||||
// `storage.caller_group_ids`. Same predicate shape as
|
// `storage.caller_group_ids`. Same predicate shape as
|
||||||
// `list_root_folders_for_caller` / the file listings.
|
// `list_root_folders_for_caller` / the file listings.
|
||||||
//
|
//
|
||||||
// Legacy method — the paginated `list_resources_paged` is
|
// Legacy method — the paginated `list_resources_paged` is the
|
||||||
// the modern shape and takes explicit drive_ids from the
|
// modern shape and takes explicit drive_ids from the service
|
||||||
// service layer.
|
// layer.
|
||||||
let rows = sqlx::query_as::<
|
let rows = sqlx::query_as::<_, (Uuid, String, String, Option<DateTime<Utc>>, String)>(
|
||||||
_,
|
|
||||||
(
|
|
||||||
Uuid,
|
|
||||||
String,
|
|
||||||
String,
|
|
||||||
Option<Uuid>,
|
|
||||||
Option<DateTime<Utc>>,
|
|
||||||
String,
|
|
||||||
),
|
|
||||||
>(
|
|
||||||
r#"
|
r#"
|
||||||
SELECT t.id, t.name, t.item_type, t.user_id, t.trashed_at,
|
SELECT t.id, t.name, t.item_type, t.trashed_at,
|
||||||
COALESCE(p.path || '/' || t.name, t.name) AS original_path
|
COALESCE(p.path || '/' || t.name, t.name) AS original_path
|
||||||
FROM storage.trash_items t
|
FROM storage.trash_items t
|
||||||
LEFT JOIN storage.folders p ON p.id = t.original_parent_id
|
LEFT JOIN storage.folders p ON p.id = t.original_parent_id
|
||||||
@@ -172,15 +160,8 @@ impl TrashRepository for TrashDbRepository {
|
|||||||
|
|
||||||
Ok(rows
|
Ok(rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(id, name, item_type, uid, trashed_at, path)| {
|
.map(|(id, name, item_type, trashed_at, path)| {
|
||||||
self.row_to_trashed_item(
|
self.row_to_trashed_item(id, name, item_type, *user_id, trashed_at, path)
|
||||||
id,
|
|
||||||
name,
|
|
||||||
item_type,
|
|
||||||
uid.unwrap_or(*user_id),
|
|
||||||
trashed_at,
|
|
||||||
path,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
@@ -192,24 +173,15 @@ impl TrashRepository for TrashDbRepository {
|
|||||||
// The drive precheck in `pg_acl_engine` then resolves Owner-on-drive
|
// The drive precheck in `pg_acl_engine` then resolves Owner-on-drive
|
||||||
// → Delete-permission for items in shared drives.
|
// → Delete-permission for items in shared drives.
|
||||||
//
|
//
|
||||||
// Post-D7: `t.user_id` is nullable (new rows land NULL). The
|
// Post-D7: `t.user_id` no longer exists — the column is dropped
|
||||||
// entity's `user_id` field is still non-optional; fall back to
|
// from `storage.{files,folders}` and no longer projected by the
|
||||||
// `Uuid::nil()` when the view row is NULL. AuthZ decisions
|
// view. The entity's `user_id` field is still non-optional;
|
||||||
// don't consult this field — they've already resolved the
|
// synthesize `Uuid::nil()`. AuthZ decisions don't consult this
|
||||||
// caller's role on the target's drive.
|
// field — they've already resolved the caller's role on the
|
||||||
let row = sqlx::query_as::<
|
// target's drive.
|
||||||
_,
|
let row = sqlx::query_as::<_, (Uuid, String, String, Option<DateTime<Utc>>, String)>(
|
||||||
(
|
|
||||||
Uuid,
|
|
||||||
String,
|
|
||||||
String,
|
|
||||||
Option<Uuid>,
|
|
||||||
Option<DateTime<Utc>>,
|
|
||||||
String,
|
|
||||||
),
|
|
||||||
>(
|
|
||||||
r#"
|
r#"
|
||||||
SELECT t.id, t.name, t.item_type, t.user_id, t.trashed_at,
|
SELECT t.id, t.name, t.item_type, t.trashed_at,
|
||||||
COALESCE(p.path || '/' || t.name, t.name) AS original_path
|
COALESCE(p.path || '/' || t.name, t.name) AS original_path
|
||||||
FROM storage.trash_items t
|
FROM storage.trash_items t
|
||||||
LEFT JOIN storage.folders p ON p.id = t.original_parent_id
|
LEFT JOIN storage.folders p ON p.id = t.original_parent_id
|
||||||
@@ -221,15 +193,8 @@ impl TrashRepository for TrashDbRepository {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| DomainError::internal_error("TrashDb", format!("get: {e}")))?;
|
.map_err(|e| DomainError::internal_error("TrashDb", format!("get: {e}")))?;
|
||||||
|
|
||||||
Ok(row.map(|(id, name, item_type, uid, trashed_at, path)| {
|
Ok(row.map(|(id, name, item_type, trashed_at, path)| {
|
||||||
self.row_to_trashed_item(
|
self.row_to_trashed_item(id, name, item_type, Uuid::nil(), trashed_at, path)
|
||||||
id,
|
|
||||||
name,
|
|
||||||
item_type,
|
|
||||||
uid.unwrap_or_else(Uuid::nil),
|
|
||||||
trashed_at,
|
|
||||||
path,
|
|
||||||
)
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ impl PathResolverService {
|
|||||||
String, // name
|
String, // name
|
||||||
String, // path
|
String, // path
|
||||||
Option<String>, // parent_id
|
Option<String>, // parent_id
|
||||||
Option<String>, // user_id
|
|
||||||
Uuid, // drive_id
|
Uuid, // drive_id
|
||||||
i64, // created_at
|
i64, // created_at
|
||||||
i64, // modified_at
|
i64, // modified_at
|
||||||
@@ -90,7 +89,7 @@ impl PathResolverService {
|
|||||||
),
|
),
|
||||||
>(
|
>(
|
||||||
r#"
|
r#"
|
||||||
SELECT resource_type, id, name, path, parent_id, user_id, drive_id,
|
SELECT resource_type, id, name, path, parent_id, drive_id,
|
||||||
created_at, modified_at, size, mime_type, folder_id,
|
created_at, modified_at, size, mime_type, folder_id,
|
||||||
blob_hash, tree_modified_at
|
blob_hash, tree_modified_at
|
||||||
FROM (
|
FROM (
|
||||||
@@ -99,7 +98,6 @@ impl PathResolverService {
|
|||||||
fo.name,
|
fo.name,
|
||||||
fo.path,
|
fo.path,
|
||||||
fo.parent_id::text,
|
fo.parent_id::text,
|
||||||
fo.user_id::text,
|
|
||||||
fo.drive_id,
|
fo.drive_id,
|
||||||
EXTRACT(EPOCH FROM fo.created_at)::bigint AS created_at,
|
EXTRACT(EPOCH FROM fo.created_at)::bigint AS created_at,
|
||||||
EXTRACT(EPOCH FROM fo.updated_at)::bigint AS modified_at,
|
EXTRACT(EPOCH FROM fo.updated_at)::bigint AS modified_at,
|
||||||
@@ -123,7 +121,6 @@ impl PathResolverService {
|
|||||||
ELSE fi.name
|
ELSE fi.name
|
||||||
END AS path,
|
END AS path,
|
||||||
NULL::text AS parent_id,
|
NULL::text AS parent_id,
|
||||||
fi.user_id::text,
|
|
||||||
fi.drive_id,
|
fi.drive_id,
|
||||||
EXTRACT(EPOCH FROM fi.created_at)::bigint AS created_at,
|
EXTRACT(EPOCH FROM fi.created_at)::bigint AS created_at,
|
||||||
EXTRACT(EPOCH FROM fi.updated_at)::bigint AS modified_at,
|
EXTRACT(EPOCH FROM fi.updated_at)::bigint AS modified_at,
|
||||||
@@ -160,7 +157,6 @@ impl PathResolverService {
|
|||||||
name,
|
name,
|
||||||
res_path,
|
res_path,
|
||||||
parent_id,
|
parent_id,
|
||||||
_uid, // Post-D7: `user_id` column no longer flowed into the DTO.
|
|
||||||
drive_id,
|
drive_id,
|
||||||
created_at,
|
created_at,
|
||||||
modified_at,
|
modified_at,
|
||||||
|
|||||||
@@ -243,21 +243,16 @@ impl ContentIndexWorker {
|
|||||||
// Authoritative state re-read: a queued 'upsert' whose row vanished
|
// Authoritative state re-read: a queued 'upsert' whose row vanished
|
||||||
// or got trashed in the meantime becomes a delete.
|
// or got trashed in the meantime becomes a delete.
|
||||||
//
|
//
|
||||||
// Post-D7: `fi.user_id` is nullable and new rows land NULL.
|
// Post-D7: `fi.user_id` is dropped — no longer projected. The
|
||||||
// The projected `user_id::text` therefore comes back as
|
// Tantivy `user_id` field survives as defence-in-depth but now
|
||||||
// `Option<String>`; we normalise to `""` at the tuple boundary
|
// always indexes `""`. Every query is Must-scoped by `drive_id`.
|
||||||
// so the downstream indexing code doesn't have to change.
|
// (file_id, drive_id, name, blob_hash, mime, size).
|
||||||
// The Tantivy `user_id` field is defence-in-depth only — every
|
type FileIndexRow = (Uuid, String, String, String, String, i64);
|
||||||
// query is Must-scoped by `drive_id`.
|
|
||||||
// (file_id, user_id, drive_id, name, blob_hash, mime, size).
|
|
||||||
// `user_id` is `Option<String>` because post-D7 `storage.files.user_id`
|
|
||||||
// is nullable — new rows land NULL.
|
|
||||||
type FileIndexRow = (Uuid, Option<String>, String, String, String, String, i64);
|
|
||||||
let files: Vec<FileIndexRow> = if upsert_candidates.is_empty() {
|
let files: Vec<FileIndexRow> = if upsert_candidates.is_empty() {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
} else {
|
} else {
|
||||||
sqlx::query_as(
|
sqlx::query_as(
|
||||||
"SELECT fi.id, fi.user_id::text, fi.drive_id::text, fi.name,
|
"SELECT fi.id, fi.drive_id::text, fi.name,
|
||||||
fi.blob_hash, fi.mime_type, fi.size
|
fi.blob_hash, fi.mime_type, fi.size
|
||||||
FROM storage.files fi
|
FROM storage.files fi
|
||||||
WHERE fi.id = ANY($1) AND NOT fi.is_trashed",
|
WHERE fi.id = ANY($1) AND NOT fi.is_trashed",
|
||||||
@@ -272,10 +267,10 @@ impl ContentIndexWorker {
|
|||||||
// Per-blob text: batch-read the extraction cache, extract misses.
|
// Per-blob text: batch-read the extraction cache, extract misses.
|
||||||
let wanted_hashes: Vec<String> = files
|
let wanted_hashes: Vec<String> = files
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, _, _, name, _, mime, size)| {
|
.filter(|(_, _, name, _, mime, size)| {
|
||||||
text_extractor::supports(name, mime) && *size as u64 <= self.max_extract_file_bytes
|
text_extractor::supports(name, mime) && *size as u64 <= self.max_extract_file_bytes
|
||||||
})
|
})
|
||||||
.map(|f| f.4.clone())
|
.map(|f| f.3.clone())
|
||||||
.collect();
|
.collect();
|
||||||
let mut text_by_hash: HashMap<String, Option<String>> = HashMap::new();
|
let mut text_by_hash: HashMap<String, Option<String>> = HashMap::new();
|
||||||
if !wanted_hashes.is_empty() {
|
if !wanted_hashes.is_empty() {
|
||||||
@@ -292,7 +287,7 @@ impl ContentIndexWorker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut records = Vec::with_capacity(files.len());
|
let mut records = Vec::with_capacity(files.len());
|
||||||
for (file_id, user_id, drive_id, name, blob_hash, mime, size) in files {
|
for (file_id, drive_id, name, blob_hash, mime, size) in files {
|
||||||
let supported = text_extractor::supports(&name, &mime);
|
let supported = text_extractor::supports(&name, &mime);
|
||||||
let content = if !supported {
|
let content = if !supported {
|
||||||
None
|
None
|
||||||
@@ -311,7 +306,7 @@ impl ContentIndexWorker {
|
|||||||
.map(|t| truncate_on_char(t, PREVIEW_BYTES));
|
.map(|t| truncate_on_char(t, PREVIEW_BYTES));
|
||||||
records.push(IndexDocRecord {
|
records.push(IndexDocRecord {
|
||||||
file_id: file_id.to_string(),
|
file_id: file_id.to_string(),
|
||||||
user_id: user_id.unwrap_or_default(),
|
user_id: String::new(),
|
||||||
drive_id,
|
drive_id,
|
||||||
name,
|
name,
|
||||||
content,
|
content,
|
||||||
|
|||||||
@@ -75,9 +75,11 @@ BEGIN
|
|||||||
VALUES ('personal', admin_id, NULL)
|
VALUES ('personal', admin_id, NULL)
|
||||||
RETURNING id INTO drive_id;
|
RETURNING id INTO drive_id;
|
||||||
|
|
||||||
|
-- Post-D7: `storage.folders.user_id` dropped. Ownership lives on the
|
||||||
|
-- drive-Owner role_grant below; provenance in `created_by`/`updated_by`.
|
||||||
INSERT INTO storage.folders
|
INSERT INTO storage.folders
|
||||||
(name, parent_id, user_id, drive_id, created_by, updated_by)
|
(name, parent_id, drive_id, created_by, updated_by)
|
||||||
VALUES ('Personal', NULL, admin_id, drive_id, admin_id, admin_id)
|
VALUES ('Personal', NULL, drive_id, admin_id, admin_id)
|
||||||
RETURNING id INTO folder_id;
|
RETURNING id INTO folder_id;
|
||||||
|
|
||||||
UPDATE storage.drives SET root_folder_id = folder_id WHERE id = drive_id;
|
UPDATE storage.drives SET root_folder_id = folder_id WHERE id = drive_id;
|
||||||
|
|||||||
Reference in New Issue
Block a user