Repository: IgnisDa/ryot (language: rust)

Issue:
Allow removing media item from list pages
I do not see any straightforward way to delete custom entries. 


Discussion / hints:
Once you remove all seen history and collections from a metadata, it should be automatically removed by the nightly cron job.
I looked into this and it seems unnecessarily cumbersome. Say one had a entry assigned to custom, inprogress and monitoring and had logged progress to 79%. That adds up to a total of five clicks on a custom entry to remove all seen history and collections. 
When it comes to regular entries, the burden increases even more since each episode/chapter is individually added to history. So for a series with 26 episodes one would have to make 29 clicks which would make the experience very cumbersome.
I see you have responded to this in #487 but I hope you will reconsider considering the above.
What's your expectation from a feature like this?
My expectation would be to reduce the above described process to one step. 
I can think of multiple scenario's where this would be useful.
1. Adding the wrong series and marking it as watched 
2. Adding a custom entry and messing up some metadata (since one cannot edit custom metadata currently, the only recourse is to delete and start over)
Makes sense. Will add a btn to do this in "More Actions"

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 6252ad16ac..71feea8736 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1714,6 +1714,7 @@ dependencies = [
  "sea-orm",
  "tracing",
  "user-models",
+ "uuid",
 ]
 
 [[package]]
diff --git a/apps/frontend/app/components/media.tsx b/apps/frontend/app/components/media.tsx
index dc6243c7a8..4741541017 100644
--- a/apps/frontend/app/components/media.tsx
+++ b/apps/frontend/app/components/media.tsx
@@ -486,7 +486,6 @@ export const ToggleMediaMonitorMenuItem = (props: {
 			<input readOnly hidden name="creatorUserId" value={userDetails.id} />
 			<Menu.Item
 				type="submit"
-				color={isMonitored ? "red" : undefined}
 				onClick={async (e) => {
 					const form = e.currentTarget.form;
 					if (form) {
diff --git a/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx b/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx
index e5093574b3..b943a24f1a 100644
--- a/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx
@@ -27,7 +27,7 @@ import {
 	Title,
 } from "@mantine/core";
 import { DateInput } from "@mantine/dates";
-import { useDisclosure } from "@mantine/hooks";
+import { useDidUpdate, useDisclosure, useInViewport } from "@mantine/hooks";
 import { unstable_defineAction, unstable_defineLoader } from "@remix-run/node";
 import {
 	Form,
@@ -38,6 +38,7 @@ import {
 import {
 	DeleteSeenItemDocument,
 	DeployUpdateMetadataJobDocument,
+	DisassociateMetadataDocument,
 	EntityLot,
 	MediaLot,
 	MediaSource,
@@ -204,6 +205,18 @@ export const action = unstable_defineAction(async ({ request }) => {
 				}),
 			});
 		},
+		removeItem: async () => {
+			const submission = processSubmission(formData, MetadataIdSchema);
+			await serverGqlService.authenticatedRequest(
+				request,
+				DisassociateMetadataDocument,
+				submission,
+			);
+			return redirectWithToast($path("/"), {
+				type: "success",
+				message: "Removed item successfully",
+			});
+		},
 	});
 });
 
@@ -882,23 +895,21 @@ export default function Page() {
 										Post a review
 									</Button>
 								) : null}
-								<>
-									<Button
-										variant="outline"
-										onClick={() => {
-											setAddEntityToCollectionData({
-												entityId: loaderData.metadataId,
-												entityLot: EntityLot.Metadata,
-												alreadyInCollections:
-													loaderData.userMetadataDetails.collections.map(
-														(c) => c.id,
-													),
-											});
-										}}
-									>
-										Add to collection
-									</Button>
-								</>
+								<Button
+									variant="outline"
+									onClick={() => {
+										setAddEntityToCollectionData({
+											entityId: loaderData.metadataId,
+											entityLot: EntityLot.Metadata,
+											alreadyInCollections:
+												loaderData.userMetadataDetails.collections.map(
+													(c) => c.id,
+												),
+										});
+									}}
+								>
+									Add to collection
+								</Button>
 								<Menu shadow="md">
 									<Menu.Target>
 										<Button variant="outline">More actions</Button>
@@ -940,6 +951,33 @@ export default function Page() {
 										</Menu.Item>
 									</Menu.Dropdown>
 								</Menu>
+								<Form
+									method="POST"
+									action={withQuery("", { intent: "removeItem" })}
+								>
+									<input
+										hidden
+										name="metadataId"
+										defaultValue={loaderData.metadataId}
+									/>
+									<Button
+										w="100%"
+										variant="outline"
+										onClick={async (e) => {
+											const form = e.currentTarget.form;
+											if (form) {
+												e.preventDefault();
+												const conf = await confirmWrapper({
+													confirmation:
+														"Are you sure you want to remove this item? This will remove it from all collections and delete all history and reviews.",
+												});
+												if (conf && form) submit(form);
+											}
+										}}
+									>
+										Remove item
+									</Button>
+								</Form>
 							</SimpleGrid>
 						</MediaScrollArea>
 					</Tabs.Panel>
@@ -1092,27 +1130,11 @@ export default function Page() {
 							<MediaScrollArea>
 								<Stack>
 									{loaderData.metadataDetails.assets.videos.map((v) => (
-										<Box key={v.videoId}>
-											<iframe
-												width="100%"
-												height={200}
-												src={
-													match(v.source)
-														.with(
-															MetadataVideoSource.Youtube,
-															() => "https://www.youtube.com/embed/",
-														)
-														.with(
-															MetadataVideoSource.Dailymotion,
-															() => "https://www.dailymotion.com/embed/video/",
-														)
-														.with(MetadataVideoSource.Custom, () => "")
-														.exhaustive() + v.videoId
-												}
-												title="YouTube video player"
-												allowFullScreen
-											/>
-										</Box>
+										<VideoIframe
+											key={v.videoId}
+											videoId={v.videoId}
+											videoSource={v.source}
+										/>
 									))}
 								</Stack>
 							</MediaScrollArea>
@@ -1165,6 +1187,44 @@ export default function Page() {
 	);
 }
 
+const VideoIframe = (props: {
+	videoId: string;
+	videoSource: MetadataVideoSource;
+}) => {
+	const [isMounted, setIsMounted] = useState(false);
+	const { ref, inViewport } = useInViewport();
+
+	useDidUpdate(() => {
+		if (inViewport) setIsMounted(true);
+	}, [inViewport]);
+
+	return (
+		<Box ref={ref}>
+			{isMounted ? (
+				<iframe
+					width="100%"
+					height={200}
+					src={
+						match(props.videoSource)
+							.with(
+								MetadataVideoSource.Youtube,
+								() => "https://www.youtube.com/embed/",
+							)
+							.with(
+								MetadataVideoSource.Dailymotion,
+								() => "https://www.dailymotion.com/embed/video/",
+							)
+							.with(MetadataVideoSource.Custom, () => "")
+							.exhaustive() + props.videoId
+					}
+					title="Video player"
+					allowFullScreen
+				/>
+			) : null}
+		</Box>
+	);
+};
+
 const MetadataCreator = (props: {
 	name: string;
 	image?: string | null;
diff --git a/crates/resolvers/miscellaneous/src/lib.rs b/crates/resolvers/miscellaneous/src/lib.rs
index 466f55a436..6d6d262edd 100644
--- a/crates/resolvers/miscellaneous/src/lib.rs
+++ b/crates/resolvers/miscellaneous/src/lib.rs
@@ -325,6 +325,18 @@ impl MiscellaneousMutation {
             .await
     }
 
+    /// Delete all history and reviews for a given media item and remove it from all
+    /// collections for the user.
+    async fn disassociate_metadata(
+        &self,
+        gql_ctx: &Context<'_>,
+        metadata_id: String,
+    ) -> Result<bool> {
+        let service = gql_ctx.data_unchecked::<Arc<MiscellaneousService>>();
+        let user_id = self.user_id_from_ctx(gql_ctx).await?;
+        service.disassociate_metadata(user_id, metadata_id).await
+    }
+
     /// Fetch details about a media and create a media item in the database.
     async fn commit_metadata(
         &self,
diff --git a/crates/services/collection/src/lib.rs b/crates/services/collection/src/lib.rs
index f12835a99e..d264a0f15b 100644
--- a/crates/services/collection/src/lib.rs
+++ b/crates/services/collection/src/lib.rs
@@ -299,7 +299,7 @@ impl CollectionService {
         let reviews = item_reviews(
             &self.db,
             &collection.user_id,
-            input.collection_id,
+            &input.collection_id,
             EntityLot::Collection,
         )
         .await?;
diff --git a/crates/services/exporter/src/lib.rs b/crates/services/exporter/src/lib.rs
index b2bfe88591..1321f844fb 100644
--- a/crates/services/exporter/src/lib.rs
+++ b/crates/services/exporter/src/lib.rs
@@ -11,15 +11,15 @@ use database_models::{
     seen, user_to_entity, workout,
 };
 use database_utils::{
-    entity_in_collections, get_review_export_item, item_reviews, user_measurements_list,
-    workout_details,
+    entity_in_collections, item_reviews, user_measurements_list, workout_details,
 };
 use enums::EntityLot;
 use file_storage_service::FileStorageService;
 use fitness_models::UserMeasurementsListInput;
 use media_models::{
-    ImportOrExportExerciseItem, ImportOrExportMediaGroupItem, ImportOrExportMediaItem,
-    ImportOrExportMediaItemSeen, ImportOrExportPersonItem,
+    ImportOrExportExerciseItem, ImportOrExportItemRating, ImportOrExportItemReview,
+    ImportOrExportMediaGroupItem, ImportOrExportMediaItem, ImportOrExportMediaItemSeen,
+    ImportOrExportPersonItem, ReviewItem,
 };
 use nanoid::nanoid;
 use reqwest::{
@@ -225,12 +225,12 @@ impl ExporterService {
                     }
                 })
                 .collect();
-            let reviews = item_reviews(&self.db, user_id, m.id.clone(), EntityLot::Metadata)
+            let reviews = item_reviews(&self.db, user_id, &m.id, EntityLot::Metadata)
                 .await?
                 .into_iter()
-                .map(get_review_export_item)
+                .map(|r| self.get_review_export_item(r))
                 .collect();
-            let collections = entity_in_collections(&self.db, user_id, m.id, EntityLot::Metadata)
+            let collections = entity_in_collections(&self.db, user_id, &m.id, EntityLot::Metadata)
                 .await?
                 .into_iter()
                 .map(|c| c.name)
@@ -267,13 +267,13 @@ impl ExporterService {
                 .await
                 .unwrap()
                 .unwrap();
-            let reviews = item_reviews(&self.db, user_id, m.id.clone(), EntityLot::MetadataGroup)
+            let reviews = item_reviews(&self.db, user_id, &m.id, EntityLot::MetadataGroup)
                 .await?
                 .into_iter()
-                .map(get_review_export_item)
+                .map(|r| self.get_review_export_item(r))
                 .collect();
             let collections =
-                entity_in_collections(&self.db, user_id, m.id, EntityLot::MetadataGroup)
+                entity_in_collections(&self.db, user_id, &m.id, EntityLot::MetadataGroup)
                     .await?
                     .into_iter()
                     .map(|c| c.name)
@@ -309,12 +309,12 @@ impl ExporterService {
                 .await
                 .unwrap()
                 .unwrap();
-            let reviews = item_reviews(&self.db, user_id, p.id.clone(), EntityLot::Person)
+            let reviews = item_reviews(&self.db, user_id, &p.id, EntityLot::Person)
                 .await?
                 .into_iter()
-                .map(get_review_export_item)
+                .map(|r| self.get_review_export_item(r))
                 .collect();
-            let collections = entity_in_collections(&self.db, user_id, p.id, EntityLot::Person)
+            let collections = entity_in_collections(&self.db, user_id, &p.id, EntityLot::Person)
                 .await?
                 .into_iter()
                 .map(|c| c.name)
@@ -391,17 +391,16 @@ impl ExporterService {
                 .await
                 .unwrap()
                 .unwrap();
-            let reviews = item_reviews(&self.db, user_id, e.id.clone(), EntityLot::Exercise)
+            let reviews = item_reviews(&self.db, user_id, &e.id, EntityLot::Exercise)
                 .await?
                 .into_iter()
-                .map(get_review_export_item)
+                .map(|r| self.get_review_export_item(r))
+                .collect();
+            let collections = entity_in_collections(&self.db, user_id, &e.id, EntityLot::Exercise)
+                .await?
+                .into_iter()
+                .map(|c| c.name)
                 .collect();
-            let collections =
-                entity_in_collections(&self.db, user_id, e.id.clone(), EntityLot::Exercise)
-                    .await?
-                    .into_iter()
-                    .map(|c| c.name)
-                    .collect();
             let exp = ImportOrExportExerciseItem {
                 name: e.id,
                 collections,
@@ -411,4 +410,32 @@ impl ExporterService {
         }
         Ok(())
     }
+
+    fn get_review_export_item(&self, rev: ReviewItem) -> ImportOrExportItemRating {
+        let (show_season_number, show_episode_number) = match rev.show_extra_information {
+            Some(d) => (Some(d.season), Some(d.episode)),
+            None => (None, None),
+        };
+        let podcast_episode_number = rev.podcast_extra_information.map(|d| d.episode);
+        let anime_episode_number = rev.anime_extra_information.and_then(|d| d.episode);
+        let manga_chapter_number = rev.manga_extra_information.and_then(|d| d.chapter);
+        ImportOrExportItemRating {
+            review: Some(ImportOrExportItemReview {
+                visibility: Some(rev.visibility),
+                date: Some(rev.posted_on),
+                spoiler: Some(rev.is_spoiler),
+                text: rev.text_original,
+            }),
+            rating: rev.rating,
+            show_season_number,
+            show_episode_number,
+            podcast_episode_number,
+            anime_episode_number,
+            manga_chapter_number,
+            comments: match rev.comments.is_empty() {
+                true => None,
+                false => Some(rev.comments),
+            },
+        }
+    }
 }
diff --git a/crates/services/fitness/src/lib.rs b/crates/services/fitness/src/lib.rs
index b8a2576db9..732054d208 100644
--- a/crates/services/fitness/src/lib.rs
+++ b/crates/services/fitness/src/lib.rs
@@ -161,10 +161,8 @@ impl ExerciseService {
         exercise_id: String,
     ) -> Result<UserExerciseDetails> {
         let collections =
-            entity_in_collections(&self.db, &user_id, exercise_id.clone(), EntityLot::Exercise)
-                .await?;
-        let reviews =
-            item_reviews(&self.db, &user_id, exercise_id.clone(), EntityLot::Exercise).await?;
+            entity_in_collections(&self.db, &user_id, &exercise_id, EntityLot::Exercise).await?;
+        let reviews = item_reviews(&self.db, &user_id, &exercise_id, EntityLot::Exercise).await?;
         let mut resp = UserExerciseDetails {
             details: None,
             history: None,
diff --git a/crates/services/miscellaneous/src/lib.rs b/crates/services/miscellaneous/src/lib.rs
index 8fdc213b58..d4cf0e9cb9 100644
--- a/crates/services/miscellaneous/src/lib.rs
+++ b/crates/services/miscellaneous/src/lib.rs
@@ -36,8 +36,9 @@ use database_models::{
 };
 use database_utils::{
     add_entity_to_collection, admin_account_guard, apply_collection_filter,
-    deploy_job_to_calculate_user_activities_and_summary, entity_in_collections, ilike_sql,
-    item_reviews, remove_entity_from_collection, user_by_id, user_preferences_by_id,
+    deploy_job_to_calculate_user_activities_and_summary, entity_in_collections,
+    entity_in_collections_with_collection_to_entity_ids, ilike_sql, item_reviews,
+    remove_entity_from_collection, user_by_id, user_preferences_by_id,
 };
 use dependent_models::{
     CoreDetails, GenreDetails, MetadataBaseData, MetadataGroupDetails, PersonDetails,
@@ -490,10 +491,8 @@ impl MiscellaneousService {
     ) -> Result<UserMetadataDetails> {
         let media_details = self.generic_metadata(&metadata_id).await?;
         let collections =
-            entity_in_collections(&self.db, &user_id, metadata_id.clone(), EntityLot::Metadata)
-                .await?;
-        let reviews =
-            item_reviews(&self.db, &user_id, metadata_id.clone(), EntityLot::Metadata).await?;
+            entity_in_collections(&self.db, &user_id, &metadata_id, EntityLot::Metadata).await?;
+        let reviews = item_reviews(&self.db, &user_id, &metadata_id, EntityLot::Metadata).await?;
         let (_, history) = self
             .is_metadata_finished_by_user(&user_id, &media_details)
             .await?;
@@ -679,10 +678,9 @@ impl MiscellaneousService {
         user_id: String,
         person_id: String,
     ) -> Result<UserPersonDetails> {
-        let reviews =
-            item_reviews(&self.db, &user_id, person_id.clone(), EntityLot::Person).await?;
+        let reviews = item_reviews(&self.db, &user_id, &person_id, EntityLot::Person).await?;
         let collections =
-            entity_in_collections(&self.db, &user_id, person_id, EntityLot::Person).await?;
+            entity_in_collections(&self.db, &user_id, &person_id, EntityLot::Person).await?;
         Ok(UserPersonDetails {
             reviews,
             collections,
@@ -697,14 +695,14 @@ impl MiscellaneousService {
         let collections = entity_in_collections(
             &self.db,
             &user_id,
-            metadata_group_id.clone(),
+            &metadata_group_id,
             EntityLot::MetadataGroup,
         )
         .await?;
         let reviews = item_reviews(
             &self.db,
             &user_id,
-            metadata_group_id,
+            &metadata_group_id,
             EntityLot::MetadataGroup,
         )
         .await?;
@@ -1411,13 +1409,13 @@ impl MiscellaneousService {
                 .unwrap();
             let all_user_to_entities = UserToEntity::find()
                 .filter(user_to_entity::Column::NeedsToBeUpdated.eq(true))
-                .filter(user_to_entity::Column::UserId.eq(user_id))
+                .filter(user_to_entity::Column::UserId.eq(&user_id))
                 .all(&self.db)
                 .await
                 .unwrap();
             for ute in all_user_to_entities {
                 let mut new_reasons = HashSet::new();
-                if let Some(metadata_id) = ute.metadata_id.clone() {
+                let (entity_id, entity_lot) = if let Some(metadata_id) = ute.metadata_id.clone() {
                     let metadata = self.generic_metadata(&metadata_id).await?;
                     let (is_finished, seen_history) = self
                         .is_metadata_finished_by_user(&ute.user_id, &metadata)
@@ -1428,27 +1426,22 @@ impl MiscellaneousService {
                     if !seen_history.is_empty() && is_finished {
                         new_reasons.insert(UserToMediaReason::Finished);
                     }
-                } else if ute.person_id.is_some() || ute.metadata_group_id.is_some() {
+                    (metadata_id, EntityLot::Metadata)
+                } else if let Some(person_id) = ute.person_id.clone() {
+                    (person_id, EntityLot::Person)
+                } else if let Some(metadata_group_id) = ute.metadata_group_id.clone() {
+                    (metadata_group_id, EntityLot::MetadataGroup)
                 } else {
                     ryot_log!(debug, "Skipping user_to_entity = {:?}", ute.id);
                     continue;
                 };
 
-                let collections_part_of = CollectionToEntity::find()
-                    .select_only()
-                    .column(collection_to_entity::Column::CollectionId)
-                    .filter(
-                        collection_to_entity::Column::MetadataId
-                            .eq(ute.metadata_id.clone())
-                            .or(collection_to_entity::Column::PersonId.eq(ute.person_id.clone()))
-                            .or(collection_to_entity::Column::MetadataGroupId
-                                .eq(ute.metadata_group_id.clone())),
-                    )
-                    .filter(collection_to_entity::Column::CollectionId.is_not_null())
-                    .into_tuple::<String>()
-                    .all(&self.db)
-                    .await
-                    .unwrap();
+                let collections_part_of =
+                    entity_in_collections(&self.db, &user_id, &entity_id, entity_lot)
+                        .await?
+                        .into_iter()
+                        .map(|c| c.id)
+                        .collect_vec();
                 if Review::find()
                     .filter(review::Column::UserId.eq(&ute.user_id))
                     .filter(
@@ -2138,6 +2131,49 @@ impl MiscellaneousService {
         Ok(true)
     }
 
+    pub async fn disassociate_metadata(
+        &self,
+        user_id: String,
+        metadata_id: String,
+    ) -> Result<bool> {
+        let delete_review = Review::delete_many()
+            .filter(review::Column::MetadataId.eq(&metadata_id))
+            .filter(review::Column::UserId.eq(&user_id))
+            .exec(&self.db)
+            .await?;
+        ryot_log!(debug, "Deleted {} reviews", delete_review.rows_affected);
+        let delete_seen = Seen::delete_many()
+            .filter(seen::Column::MetadataId.eq(&metadata_id))
+            .filter(seen::Column::UserId.eq(&user_id))
+            .exec(&self.db)
+            .await?;
+        ryot_log!(debug, "Deleted {} seen items", delete_seen.rows_affected);
+        let collections_part_of = entity_in_collections_with_collection_to_entity_ids(
+            &self.db,
+            &user_id,
+            &metadata_id,
+            EntityLot::Metadata,
+        )
+        .await?
+        .into_iter()
+        .map(|(_, id)| id);
+        let delete_collections = CollectionToEntity::delete_many()
+            .filter(collection_to_entity::Column::Id.is_in(collections_part_of))
+            .exec(&self.db)
+            .await?;
+        ryot_log!(
+            debug,
+            "Deleted {} collections",
+            delete_collections.rows_affected
+        );
+        UserToEntity::delete_many()
+            .filter(user_to_entity::Column::MetadataId.eq(metadata_id))
+            .filter(user_to_entity::Column::UserId.eq(user_id))
+            .exec(&self.db)
+            .await?;
+        Ok(true)
+    }
+
     pub async fn metadata_search(
         &self,
         user_id: &String,
diff --git a/crates/utils/database/Cargo.toml b/crates/utils/database/Cargo.toml
index 2b9acb6eb9..56c206ef4b 100644
--- a/crates/utils/database/Cargo.toml
+++ b/crates/utils/database/Cargo.toml
@@ -26,6 +26,7 @@ rust_decimal_macros = { workspace = true }
 sea-orm = { workspace = true }
 tracing = { workspace = true }
 user-models = { path = "../../models/user" }
+uuid = { workspace = true }
 
 [package.metadata.cargo-machete]
 ignored = ["rust_decimal", "tracing"]
diff --git a/crates/utils/database/src/lib.rs b/crates/utils/database/src/lib.rs
index f8103705fd..3ab89cd17d 100644
--- a/crates/utils/database/src/lib.rs
+++ b/crates/utils/database/src/lib.rs
@@ -24,9 +24,7 @@ use file_storage_service::FileStorageService;
 use fitness_models::UserMeasurementsListInput;
 use itertools::Itertools;
 use markdown::to_html as markdown_to_html;
-use media_models::{
-    CreateOrUpdateCollectionInput, ImportOrExportItemRating, ImportOrExportItemReview, ReviewItem,
-};
+use media_models::{CreateOrUpdateCollectionInput, ReviewItem};
 use migrations::AliasedCollectionToEntity;
 use rust_decimal_macros::dec;
 use sea_orm::{
@@ -36,6 +34,7 @@ use sea_orm::{
     QueryFilter, QueryOrder, QuerySelect, QueryTrait, Select, TransactionTrait,
 };
 use user_models::{UserPreferences, UserReviewScale};
+use uuid::Uuid;
 
 pub fn ilike_sql(value: &str) -> String {
     format!("%{value}%")
@@ -114,12 +113,12 @@ fn get_cte_column_from_lot(entity_lot: EntityLot) -> collection_to_entity::Colum
     }
 }
 
-pub async fn entity_in_collections(
+pub async fn entity_in_collections_with_collection_to_entity_ids(
     db: &DatabaseConnection,
     user_id: &String,
-    entity_id: String,
+    entity_id: &String,
     entity_lot: EntityLot,
-) -> Result<Vec<collection::Model>> {
+) -> Result<Vec<(collection::Model, Uuid)>> {
     let user_collections = Collection::find()
         .left_join(UserToCollection)
         .filter(user_to_collection::Column::UserId.eq(user_id))
@@ -137,36 +136,23 @@ pub async fn entity_in_collections(
         .all(db)
         .await
         .unwrap();
-    let resp = mtc.into_iter().flat_map(|(_, b)| b).collect_vec();
+    let resp = mtc
+        .into_iter()
+        .map(|(cte, col)| (col.unwrap(), cte.id))
+        .collect_vec();
     Ok(resp)
 }
 
-pub fn get_review_export_item(rev: ReviewItem) -> ImportOrExportItemRating {
-    let (show_season_number, show_episode_number) = match rev.show_extra_information {
-        Some(d) => (Some(d.season), Some(d.episode)),
-        None => (None, None),
-    };
-    let podcast_episode_number = rev.podcast_extra_information.map(|d| d.episode);
-    let anime_episode_number = rev.anime_extra_information.and_then(|d| d.episode);
-    let manga_chapter_number = rev.manga_extra_information.and_then(|d| d.chapter);
-    ImportOrExportItemRating {
-        review: Some(ImportOrExportItemReview {
-            visibility: Some(rev.visibility),
-            date: Some(rev.posted_on),
-            spoiler: Some(rev.is_spoiler),
-            text: rev.text_original,
-        }),
-        rating: rev.rating,
-        show_season_number,
-        show_episode_number,
-        podcast_episode_number,
-        anime_episode_number,
-        manga_chapter_number,
-        comments: match rev.comments.is_empty() {
-            true => None,
-            false => Some(rev.comments),
-        },
-    }
+pub async fn entity_in_collections(
+    db: &DatabaseConnection,
+    user_id: &String,
+    entity_id: &String,
+    entity_lot: EntityLot,
+) -> Result<Vec<collection::Model>> {
+    let eic =
+        entity_in_collections_with_collection_to_entity_ids(db, user_id, entity_id, entity_lot)
+            .await?;
+    Ok(eic.into_iter().map(|(c, _)| c).collect_vec())
 }
 
 pub async fn workout_details(
@@ -185,7 +171,7 @@ pub async fn workout_details(
         )),
         Some(e) => {
             let collections =
-                entity_in_collections(db, user_id, workout_id, EntityLot::Workout).await?;
+                entity_in_collections(db, user_id, &workout_id, EntityLot::Workout).await?;
             let details = e.graphql_representation(file_storage_service).await?;
             Ok(UserWorkoutDetails {
                 details,
@@ -327,7 +313,7 @@ pub async fn remove_entity_from_collection(
 pub async fn item_reviews(
     db: &DatabaseConnection,
     user_id: &String,
-    entity_id: String,
+    entity_id: &String,
     entity_lot: EntityLot,
 ) -> Result<Vec<ReviewItem>> {
     let column = match entity_lot {
diff --git a/libs/generated/src/graphql/backend/gql.ts b/libs/generated/src/graphql/backend/gql.ts
index 857e684e4d..07daeda7b0 100644
--- a/libs/generated/src/graphql/backend/gql.ts
+++ b/libs/generated/src/graphql/backend/gql.ts
@@ -13,7 +13,7 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/
  * Therefore it is highly recommended to use the babel or swc plugin for production.
  */
 const documents = {
-    "mutation RegisterUser($input: RegisterUserInput!) {\n  registerUser(input: $input) {\n    __typename\n    ... on RegisterError {\n      error\n    }\n    ... on StringIdObject {\n      id\n    }\n  }\n}\n\nmutation LoginUser($input: AuthUserInput!) {\n  loginUser(input: $input) {\n    __typename\n    ... on LoginError {\n      error\n    }\n    ... on LoginResponse {\n      apiKey\n    }\n  }\n}\n\nmutation AddEntityToCollection($input: ChangeCollectionToEntityInput!) {\n  addEntityToCollection(input: $input)\n}\n\nmutation CommitMetadata($input: CommitMediaInput!) {\n  commitMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CommitMetadataGroup($input: CommitMediaInput!) {\n  commitMetadataGroup(input: $input) {\n    id\n  }\n}\n\nmutation CommitPerson($input: CommitPersonInput!) {\n  commitPerson(input: $input) {\n    id\n  }\n}\n\nmutation CreateCustomExercise($input: ExerciseInput!) {\n  createCustomExercise(input: $input)\n}\n\nmutation UpdateCustomExercise($input: UpdateCustomExerciseInput!) {\n  updateCustomExercise(input: $input)\n}\n\nmutation UpdateUserIntegration($input: UpdateUserIntegrationInput!) {\n  updateUserIntegration(input: $input)\n}\n\nmutation CreateCustomMetadata($input: CreateCustomMetadataInput!) {\n  createCustomMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CreateOrUpdateCollection($input: CreateOrUpdateCollectionInput!) {\n  createOrUpdateCollection(input: $input) {\n    id\n  }\n}\n\nmutation CreateReviewComment($input: CreateReviewCommentInput!) {\n  createReviewComment(input: $input)\n}\n\nmutation CreateUserMeasurement($input: UserMeasurementInput!) {\n  createUserMeasurement(input: $input)\n}\n\nmutation CreateUserNotificationPlatform($input: CreateUserNotificationPlatformInput!) {\n  createUserNotificationPlatform(input: $input)\n}\n\nmutation CreateUserIntegration($input: CreateUserIntegrationInput!) {\n  createUserIntegration(input: $input) {\n    id\n  }\n}\n\nmutation CreateUserWorkout($input: UserWorkoutInput!) {\n  createUserWorkout(input: $input)\n}\n\nmutation DeleteCollection($collectionName: String!) {\n  deleteCollection(collectionName: $collectionName)\n}\n\nmutation DeleteReview($reviewId: String!) {\n  deleteReview(reviewId: $reviewId)\n}\n\nmutation DeleteS3Object($key: String!) {\n  deleteS3Object(key: $key)\n}\n\nmutation DeleteSeenItem($seenId: String!) {\n  deleteSeenItem(seenId: $seenId) {\n    id\n  }\n}\n\nmutation DeleteUser($toDeleteUserId: String!) {\n  deleteUser(toDeleteUserId: $toDeleteUserId)\n}\n\nmutation DeleteUserIntegration($integrationId: String!) {\n  deleteUserIntegration(integrationId: $integrationId)\n}\n\nmutation DeleteUserMeasurement($timestamp: DateTime!) {\n  deleteUserMeasurement(timestamp: $timestamp)\n}\n\nmutation DeleteUserNotificationPlatform($notificationId: String!) {\n  deleteUserNotificationPlatform(notificationId: $notificationId)\n}\n\nmutation DeleteUserWorkout($workoutId: String!) {\n  deleteUserWorkout(workoutId: $workoutId)\n}\n\nmutation DeployBackgroundJob($jobName: BackgroundJob!) {\n  deployBackgroundJob(jobName: $jobName)\n}\n\nmutation DeployBulkProgressUpdate($input: [ProgressUpdateInput!]!) {\n  deployBulkProgressUpdate(input: $input)\n}\n\nmutation DeployExportJob {\n  deployExportJob\n}\n\nmutation DeployImportJob($input: DeployImportJobInput!) {\n  deployImportJob(input: $input)\n}\n\nmutation DeployUpdateMetadataJob($metadataId: String!) {\n  deployUpdateMetadataJob(metadataId: $metadataId)\n}\n\nmutation DeployUpdatePersonJob($personId: String!) {\n  deployUpdatePersonJob(personId: $personId)\n}\n\nmutation UpdateSeenItem($input: UpdateSeenItemInput!) {\n  updateSeenItem(input: $input)\n}\n\nmutation UpdateUserNotificationPlatform($input: UpdateUserNotificationPlatformInput!) {\n  updateUserNotificationPlatform(input: $input)\n}\n\nmutation UpdateUserWorkout($input: UpdateUserWorkoutInput!) {\n  updateUserWorkout(input: $input)\n}\n\nmutation GenerateAuthToken {\n  generateAuthToken\n}\n\nmutation MergeMetadata($mergeFrom: String!, $mergeInto: String!) {\n  mergeMetadata(mergeFrom: $mergeFrom, mergeInto: $mergeInto)\n}\n\nmutation PostReview($input: PostReviewInput!) {\n  postReview(input: $input) {\n    id\n  }\n}\n\nmutation PresignedPutS3Url($input: PresignedPutUrlInput!) {\n  presignedPutS3Url(input: $input) {\n    key\n    uploadUrl\n  }\n}\n\nmutation RemoveEntityFromCollection($input: ChangeCollectionToEntityInput!) {\n  removeEntityFromCollection(input: $input) {\n    id\n  }\n}\n\nmutation TestUserNotificationPlatforms {\n  testUserNotificationPlatforms\n}\n\nmutation UpdateUser($input: UpdateUserInput!) {\n  updateUser(input: $input) {\n    id\n  }\n}\n\nmutation UpdateUserPreference($input: UpdateUserPreferenceInput!) {\n  updateUserPreference(input: $input)\n}": types.RegisterUserDocument,
+    "mutation RegisterUser($input: RegisterUserInput!) {\n  registerUser(input: $input) {\n    __typename\n    ... on RegisterError {\n      error\n    }\n    ... on StringIdObject {\n      id\n    }\n  }\n}\n\nmutation LoginUser($input: AuthUserInput!) {\n  loginUser(input: $input) {\n    __typename\n    ... on LoginError {\n      error\n    }\n    ... on LoginResponse {\n      apiKey\n    }\n  }\n}\n\nmutation AddEntityToCollection($input: ChangeCollectionToEntityInput!) {\n  addEntityToCollection(input: $input)\n}\n\nmutation CommitMetadata($input: CommitMediaInput!) {\n  commitMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CommitMetadataGroup($input: CommitMediaInput!) {\n  commitMetadataGroup(input: $input) {\n    id\n  }\n}\n\nmutation CommitPerson($input: CommitPersonInput!) {\n  commitPerson(input: $input) {\n    id\n  }\n}\n\nmutation CreateCustomExercise($input: ExerciseInput!) {\n  createCustomExercise(input: $input)\n}\n\nmutation UpdateCustomExercise($input: UpdateCustomExerciseInput!) {\n  updateCustomExercise(input: $input)\n}\n\nmutation UpdateUserIntegration($input: UpdateUserIntegrationInput!) {\n  updateUserIntegration(input: $input)\n}\n\nmutation CreateCustomMetadata($input: CreateCustomMetadataInput!) {\n  createCustomMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CreateOrUpdateCollection($input: CreateOrUpdateCollectionInput!) {\n  createOrUpdateCollection(input: $input) {\n    id\n  }\n}\n\nmutation CreateReviewComment($input: CreateReviewCommentInput!) {\n  createReviewComment(input: $input)\n}\n\nmutation CreateUserMeasurement($input: UserMeasurementInput!) {\n  createUserMeasurement(input: $input)\n}\n\nmutation CreateUserNotificationPlatform($input: CreateUserNotificationPlatformInput!) {\n  createUserNotificationPlatform(input: $input)\n}\n\nmutation CreateUserIntegration($input: CreateUserIntegrationInput!) {\n  createUserIntegration(input: $input) {\n    id\n  }\n}\n\nmutation CreateUserWorkout($input: UserWorkoutInput!) {\n  createUserWorkout(input: $input)\n}\n\nmutation DeleteCollection($collectionName: String!) {\n  deleteCollection(collectionName: $collectionName)\n}\n\nmutation DeleteReview($reviewId: String!) {\n  deleteReview(reviewId: $reviewId)\n}\n\nmutation DeleteS3Object($key: String!) {\n  deleteS3Object(key: $key)\n}\n\nmutation DeleteSeenItem($seenId: String!) {\n  deleteSeenItem(seenId: $seenId) {\n    id\n  }\n}\n\nmutation DeleteUser($toDeleteUserId: String!) {\n  deleteUser(toDeleteUserId: $toDeleteUserId)\n}\n\nmutation DeleteUserIntegration($integrationId: String!) {\n  deleteUserIntegration(integrationId: $integrationId)\n}\n\nmutation DeleteUserMeasurement($timestamp: DateTime!) {\n  deleteUserMeasurement(timestamp: $timestamp)\n}\n\nmutation DeleteUserNotificationPlatform($notificationId: String!) {\n  deleteUserNotificationPlatform(notificationId: $notificationId)\n}\n\nmutation DeleteUserWorkout($workoutId: String!) {\n  deleteUserWorkout(workoutId: $workoutId)\n}\n\nmutation DeployBackgroundJob($jobName: BackgroundJob!) {\n  deployBackgroundJob(jobName: $jobName)\n}\n\nmutation DeployBulkProgressUpdate($input: [ProgressUpdateInput!]!) {\n  deployBulkProgressUpdate(input: $input)\n}\n\nmutation DeployExportJob {\n  deployExportJob\n}\n\nmutation DeployImportJob($input: DeployImportJobInput!) {\n  deployImportJob(input: $input)\n}\n\nmutation DeployUpdateMetadataJob($metadataId: String!) {\n  deployUpdateMetadataJob(metadataId: $metadataId)\n}\n\nmutation DeployUpdatePersonJob($personId: String!) {\n  deployUpdatePersonJob(personId: $personId)\n}\n\nmutation UpdateSeenItem($input: UpdateSeenItemInput!) {\n  updateSeenItem(input: $input)\n}\n\nmutation UpdateUserNotificationPlatform($input: UpdateUserNotificationPlatformInput!) {\n  updateUserNotificationPlatform(input: $input)\n}\n\nmutation UpdateUserWorkout($input: UpdateUserWorkoutInput!) {\n  updateUserWorkout(input: $input)\n}\n\nmutation GenerateAuthToken {\n  generateAuthToken\n}\n\nmutation MergeMetadata($mergeFrom: String!, $mergeInto: String!) {\n  mergeMetadata(mergeFrom: $mergeFrom, mergeInto: $mergeInto)\n}\n\nmutation DisassociateMetadata($metadataId: String!) {\n  disassociateMetadata(metadataId: $metadataId)\n}\n\nmutation PostReview($input: PostReviewInput!) {\n  postReview(input: $input) {\n    id\n  }\n}\n\nmutation PresignedPutS3Url($input: PresignedPutUrlInput!) {\n  presignedPutS3Url(input: $input) {\n    key\n    uploadUrl\n  }\n}\n\nmutation RemoveEntityFromCollection($input: ChangeCollectionToEntityInput!) {\n  removeEntityFromCollection(input: $input) {\n    id\n  }\n}\n\nmutation TestUserNotificationPlatforms {\n  testUserNotificationPlatforms\n}\n\nmutation UpdateUser($input: UpdateUserInput!) {\n  updateUser(input: $input) {\n    id\n  }\n}\n\nmutation UpdateUserPreference($input: UpdateUserPreferenceInput!) {\n  updateUserPreference(input: $input)\n}": types.RegisterUserDocument,
     "query CollectionContents($input: CollectionContentsInput!) {\n  collectionContents(input: $input) {\n    user {\n      id\n      name\n    }\n    reviews {\n      ...ReviewItemPart\n    }\n    results {\n      details {\n        total\n        nextPage\n      }\n      items {\n        entityId\n        entityLot\n      }\n    }\n    details {\n      name\n      description\n      createdOn\n    }\n  }\n}": types.CollectionContentsDocument,
     "query CoreDetails {\n  coreDetails {\n    isPro\n    version\n    docsLink\n    pageLimit\n    websiteUrl\n    smtpEnabled\n    oidcEnabled\n    signupAllowed\n    repositoryLink\n    localAuthDisabled\n    fileStorageEnabled\n    tokenValidForDays\n  }\n}": types.CoreDetailsDocument,
     "query ExerciseDetails($exerciseId: String!) {\n  exerciseDetails(exerciseId: $exerciseId) {\n    id\n    lot\n    source\n    level\n    force\n    mechanic\n    equipment\n    muscles\n    createdByUserId\n    attributes {\n      instructions\n      images\n    }\n  }\n}": types.ExerciseDetailsDocument,
@@ -60,7 +60,7 @@ export function graphql(source: string): unknown;
 /**
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
  */
-export function graphql(source: "mutation RegisterUser($input: RegisterUserInput!) {\n  registerUser(input: $input) {\n    __typename\n    ... on RegisterError {\n      error\n    }\n    ... on StringIdObject {\n      id\n    }\n  }\n}\n\nmutation LoginUser($input: AuthUserInput!) {\n  loginUser(input: $input) {\n    __typename\n    ... on LoginError {\n      error\n    }\n    ... on LoginResponse {\n      apiKey\n    }\n  }\n}\n\nmutation AddEntityToCollection($input: ChangeCollectionToEntityInput!) {\n  addEntityToCollection(input: $input)\n}\n\nmutation CommitMetadata($input: CommitMediaInput!) {\n  commitMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CommitMetadataGroup($input: CommitMediaInput!) {\n  commitMetadataGroup(input: $input) {\n    id\n  }\n}\n\nmutation CommitPerson($input: CommitPersonInput!) {\n  commitPerson(input: $input) {\n    id\n  }\n}\n\nmutation CreateCustomExercise($input: ExerciseInput!) {\n  createCustomExercise(input: $input)\n}\n\nmutation UpdateCustomExercise($input: UpdateCustomExerciseInput!) {\n  updateCustomExercise(input: $input)\n}\n\nmutation UpdateUserIntegration($input: UpdateUserIntegrationInput!) {\n  updateUserIntegration(input: $input)\n}\n\nmutation CreateCustomMetadata($input: CreateCustomMetadataInput!) {\n  createCustomMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CreateOrUpdateCollection($input: CreateOrUpdateCollectionInput!) {\n  createOrUpdateCollection(input: $input) {\n    id\n  }\n}\n\nmutation CreateReviewComment($input: CreateReviewCommentInput!) {\n  createReviewComment(input: $input)\n}\n\nmutation CreateUserMeasurement($input: UserMeasurementInput!) {\n  createUserMeasurement(input: $input)\n}\n\nmutation CreateUserNotificationPlatform($input: CreateUserNotificationPlatformInput!) {\n  createUserNotificationPlatform(input: $input)\n}\n\nmutation CreateUserIntegration($input: CreateUserIntegrationInput!) {\n  createUserIntegration(input: $input) {\n    id\n  }\n}\n\nmutation CreateUserWorkout($input: UserWorkoutInput!) {\n  createUserWorkout(input: $input)\n}\n\nmutation DeleteCollection($collectionName: String!) {\n  deleteCollection(collectionName: $collectionName)\n}\n\nmutation DeleteReview($reviewId: String!) {\n  deleteReview(reviewId: $reviewId)\n}\n\nmutation DeleteS3Object($key: String!) {\n  deleteS3Object(key: $key)\n}\n\nmutation DeleteSeenItem($seenId: String!) {\n  deleteSeenItem(seenId: $seenId) {\n    id\n  }\n}\n\nmutation DeleteUser($toDeleteUserId: String!) {\n  deleteUser(toDeleteUserId: $toDeleteUserId)\n}\n\nmutation DeleteUserIntegration($integrationId: String!) {\n  deleteUserIntegration(integrationId: $integrationId)\n}\n\nmutation DeleteUserMeasurement($timestamp: DateTime!) {\n  deleteUserMeasurement(timestamp: $timestamp)\n}\n\nmutation DeleteUserNotificationPlatform($notificationId: String!) {\n  deleteUserNotificationPlatform(notificationId: $notificationId)\n}\n\nmutation DeleteUserWorkout($workoutId: String!) {\n  deleteUserWorkout(workoutId: $workoutId)\n}\n\nmutation DeployBackgroundJob($jobName: BackgroundJob!) {\n  deployBackgroundJob(jobName: $jobName)\n}\n\nmutation DeployBulkProgressUpdate($input: [ProgressUpdateInput!]!) {\n  deployBulkProgressUpdate(input: $input)\n}\n\nmutation DeployExportJob {\n  deployExportJob\n}\n\nmutation DeployImportJob($input: DeployImportJobInput!) {\n  deployImportJob(input: $input)\n}\n\nmutation DeployUpdateMetadataJob($metadataId: String!) {\n  deployUpdateMetadataJob(metadataId: $metadataId)\n}\n\nmutation DeployUpdatePersonJob($personId: String!) {\n  deployUpdatePersonJob(personId: $personId)\n}\n\nmutation UpdateSeenItem($input: UpdateSeenItemInput!) {\n  updateSeenItem(input: $input)\n}\n\nmutation UpdateUserNotificationPlatform($input: UpdateUserNotificationPlatformInput!) {\n  updateUserNotificationPlatform(input: $input)\n}\n\nmutation UpdateUserWorkout($input: UpdateUserWorkoutInput!) {\n  updateUserWorkout(input: $input)\n}\n\nmutation GenerateAuthToken {\n  generateAuthToken\n}\n\nmutation MergeMetadata($mergeFrom: String!, $mergeInto: String!) {\n  mergeMetadata(mergeFrom: $mergeFrom, mergeInto: $mergeInto)\n}\n\nmutation PostReview($input: PostReviewInput!) {\n  postReview(input: $input) {\n    id\n  }\n}\n\nmutation PresignedPutS3Url($input: PresignedPutUrlInput!) {\n  presignedPutS3Url(input: $input) {\n    key\n    uploadUrl\n  }\n}\n\nmutation RemoveEntityFromCollection($input: ChangeCollectionToEntityInput!) {\n  removeEntityFromCollection(input: $input) {\n    id\n  }\n}\n\nmutation TestUserNotificationPlatforms {\n  testUserNotificationPlatforms\n}\n\nmutation UpdateUser($input: UpdateUserInput!) {\n  updateUser(input: $input) {\n    id\n  }\n}\n\nmutation UpdateUserPreference($input: UpdateUserPreferenceInput!) {\n  updateUserPreference(input: $input)\n}"): (typeof documents)["mutation RegisterUser($input: RegisterUserInput!) {\n  registerUser(input: $input) {\n    __typename\n    ... on RegisterError {\n      error\n    }\n    ... on StringIdObject {\n      id\n    }\n  }\n}\n\nmutation LoginUser($input: AuthUserInput!) {\n  loginUser(input: $input) {\n    __typename\n    ... on LoginError {\n      error\n    }\n    ... on LoginResponse {\n      apiKey\n    }\n  }\n}\n\nmutation AddEntityToCollection($input: ChangeCollectionToEntityInput!) {\n  addEntityToCollection(input: $input)\n}\n\nmutation CommitMetadata($input: CommitMediaInput!) {\n  commitMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CommitMetadataGroup($input: CommitMediaInput!) {\n  commitMetadataGroup(input: $input) {\n    id\n  }\n}\n\nmutation CommitPerson($input: CommitPersonInput!) {\n  commitPerson(input: $input) {\n    id\n  }\n}\n\nmutation CreateCustomExercise($input: ExerciseInput!) {\n  createCustomExercise(input: $input)\n}\n\nmutation UpdateCustomExercise($input: UpdateCustomExerciseInput!) {\n  updateCustomExercise(input: $input)\n}\n\nmutation UpdateUserIntegration($input: UpdateUserIntegrationInput!) {\n  updateUserIntegration(input: $input)\n}\n\nmutation CreateCustomMetadata($input: CreateCustomMetadataInput!) {\n  createCustomMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CreateOrUpdateCollection($input: CreateOrUpdateCollectionInput!) {\n  createOrUpdateCollection(input: $input) {\n    id\n  }\n}\n\nmutation CreateReviewComment($input: CreateReviewCommentInput!) {\n  createReviewComment(input: $input)\n}\n\nmutation CreateUserMeasurement($input: UserMeasurementInput!) {\n  createUserMeasurement(input: $input)\n}\n\nmutation CreateUserNotificationPlatform($input: CreateUserNotificationPlatformInput!) {\n  createUserNotificationPlatform(input: $input)\n}\n\nmutation CreateUserIntegration($input: CreateUserIntegrationInput!) {\n  createUserIntegration(input: $input) {\n    id\n  }\n}\n\nmutation CreateUserWorkout($input: UserWorkoutInput!) {\n  createUserWorkout(input: $input)\n}\n\nmutation DeleteCollection($collectionName: String!) {\n  deleteCollection(collectionName: $collectionName)\n}\n\nmutation DeleteReview($reviewId: String!) {\n  deleteReview(reviewId: $reviewId)\n}\n\nmutation DeleteS3Object($key: String!) {\n  deleteS3Object(key: $key)\n}\n\nmutation DeleteSeenItem($seenId: String!) {\n  deleteSeenItem(seenId: $seenId) {\n    id\n  }\n}\n\nmutation DeleteUser($toDeleteUserId: String!) {\n  deleteUser(toDeleteUserId: $toDeleteUserId)\n}\n\nmutation DeleteUserIntegration($integrationId: String!) {\n  deleteUserIntegration(integrationId: $integrationId)\n}\n\nmutation DeleteUserMeasurement($timestamp: DateTime!) {\n  deleteUserMeasurement(timestamp: $timestamp)\n}\n\nmutation DeleteUserNotificationPlatform($notificationId: String!) {\n  deleteUserNotificationPlatform(notificationId: $notificationId)\n}\n\nmutation DeleteUserWorkout($workoutId: String!) {\n  deleteUserWorkout(workoutId: $workoutId)\n}\n\nmutation DeployBackgroundJob($jobName: BackgroundJob!) {\n  deployBackgroundJob(jobName: $jobName)\n}\n\nmutation DeployBulkProgressUpdate($input: [ProgressUpdateInput!]!) {\n  deployBulkProgressUpdate(input: $input)\n}\n\nmutation DeployExportJob {\n  deployExportJob\n}\n\nmutation DeployImportJob($input: DeployImportJobInput!) {\n  deployImportJob(input: $input)\n}\n\nmutation DeployUpdateMetadataJob($metadataId: String!) {\n  deployUpdateMetadataJob(metadataId: $metadataId)\n}\n\nmutation DeployUpdatePersonJob($personId: String!) {\n  deployUpdatePersonJob(personId: $personId)\n}\n\nmutation UpdateSeenItem($input: UpdateSeenItemInput!) {\n  updateSeenItem(input: $input)\n}\n\nmutation UpdateUserNotificationPlatform($input: UpdateUserNotificationPlatformInput!) {\n  updateUserNotificationPlatform(input: $input)\n}\n\nmutation UpdateUserWorkout($input: UpdateUserWorkoutInput!) {\n  updateUserWorkout(input: $input)\n}\n\nmutation GenerateAuthToken {\n  generateAuthToken\n}\n\nmutation MergeMetadata($mergeFrom: String!, $mergeInto: String!) {\n  mergeMetadata(mergeFrom: $mergeFrom, mergeInto: $mergeInto)\n}\n\nmutation PostReview($input: PostReviewInput!) {\n  postReview(input: $input) {\n    id\n  }\n}\n\nmutation PresignedPutS3Url($input: PresignedPutUrlInput!) {\n  presignedPutS3Url(input: $input) {\n    key\n    uploadUrl\n  }\n}\n\nmutation RemoveEntityFromCollection($input: ChangeCollectionToEntityInput!) {\n  removeEntityFromCollection(input: $input) {\n    id\n  }\n}\n\nmutation TestUserNotificationPlatforms {\n  testUserNotificationPlatforms\n}\n\nmutation UpdateUser($input: UpdateUserInput!) {\n  updateUser(input: $input) {\n    id\n  }\n}\n\nmutation UpdateUserPreference($input: UpdateUserPreferenceInput!) {\n  updateUserPreference(input: $input)\n}"];
+export function graphql(source: "mutation RegisterUser($input: RegisterUserInput!) {\n  registerUser(input: $input) {\n    __typename\n    ... on RegisterError {\n      error\n    }\n    ... on StringIdObject {\n      id\n    }\n  }\n}\n\nmutation LoginUser($input: AuthUserInput!) {\n  loginUser(input: $input) {\n    __typename\n    ... on LoginError {\n      error\n    }\n    ... on LoginResponse {\n      apiKey\n    }\n  }\n}\n\nmutation AddEntityToCollection($input: ChangeCollectionToEntityInput!) {\n  addEntityToCollection(input: $input)\n}\n\nmutation CommitMetadata($input: CommitMediaInput!) {\n  commitMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CommitMetadataGroup($input: CommitMediaInput!) {\n  commitMetadataGroup(input: $input) {\n    id\n  }\n}\n\nmutation CommitPerson($input: CommitPersonInput!) {\n  commitPerson(input: $input) {\n    id\n  }\n}\n\nmutation CreateCustomExercise($input: ExerciseInput!) {\n  createCustomExercise(input: $input)\n}\n\nmutation UpdateCustomExercise($input: UpdateCustomExerciseInput!) {\n  updateCustomExercise(input: $input)\n}\n\nmutation UpdateUserIntegration($input: UpdateUserIntegrationInput!) {\n  updateUserIntegration(input: $input)\n}\n\nmutation CreateCustomMetadata($input: CreateCustomMetadataInput!) {\n  createCustomMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CreateOrUpdateCollection($input: CreateOrUpdateCollectionInput!) {\n  createOrUpdateCollection(input: $input) {\n    id\n  }\n}\n\nmutation CreateReviewComment($input: CreateReviewCommentInput!) {\n  createReviewComment(input: $input)\n}\n\nmutation CreateUserMeasurement($input: UserMeasurementInput!) {\n  createUserMeasurement(input: $input)\n}\n\nmutation CreateUserNotificationPlatform($input: CreateUserNotificationPlatformInput!) {\n  createUserNotificationPlatform(input: $input)\n}\n\nmutation CreateUserIntegration($input: CreateUserIntegrationInput!) {\n  createUserIntegration(input: $input) {\n    id\n  }\n}\n\nmutation CreateUserWorkout($input: UserWorkoutInput!) {\n  createUserWorkout(input: $input)\n}\n\nmutation DeleteCollection($collectionName: String!) {\n  deleteCollection(collectionName: $collectionName)\n}\n\nmutation DeleteReview($reviewId: String!) {\n  deleteReview(reviewId: $reviewId)\n}\n\nmutation DeleteS3Object($key: String!) {\n  deleteS3Object(key: $key)\n}\n\nmutation DeleteSeenItem($seenId: String!) {\n  deleteSeenItem(seenId: $seenId) {\n    id\n  }\n}\n\nmutation DeleteUser($toDeleteUserId: String!) {\n  deleteUser(toDeleteUserId: $toDeleteUserId)\n}\n\nmutation DeleteUserIntegration($integrationId: String!) {\n  deleteUserIntegration(integrationId: $integrationId)\n}\n\nmutation DeleteUserMeasurement($timestamp: DateTime!) {\n  deleteUserMeasurement(timestamp: $timestamp)\n}\n\nmutation DeleteUserNotificationPlatform($notificationId: String!) {\n  deleteUserNotificationPlatform(notificationId: $notificationId)\n}\n\nmutation DeleteUserWorkout($workoutId: String!) {\n  deleteUserWorkout(workoutId: $workoutId)\n}\n\nmutation DeployBackgroundJob($jobName: BackgroundJob!) {\n  deployBackgroundJob(jobName: $jobName)\n}\n\nmutation DeployBulkProgressUpdate($input: [ProgressUpdateInput!]!) {\n  deployBulkProgressUpdate(input: $input)\n}\n\nmutation DeployExportJob {\n  deployExportJob\n}\n\nmutation DeployImportJob($input: DeployImportJobInput!) {\n  deployImportJob(input: $input)\n}\n\nmutation DeployUpdateMetadataJob($metadataId: String!) {\n  deployUpdateMetadataJob(metadataId: $metadataId)\n}\n\nmutation DeployUpdatePersonJob($personId: String!) {\n  deployUpdatePersonJob(personId: $personId)\n}\n\nmutation UpdateSeenItem($input: UpdateSeenItemInput!) {\n  updateSeenItem(input: $input)\n}\n\nmutation UpdateUserNotificationPlatform($input: UpdateUserNotificationPlatformInput!) {\n  updateUserNotificationPlatform(input: $input)\n}\n\nmutation UpdateUserWorkout($input: UpdateUserWorkoutInput!) {\n  updateUserWorkout(input: $input)\n}\n\nmutation GenerateAuthToken {\n  generateAuthToken\n}\n\nmutation MergeMetadata($mergeFrom: String!, $mergeInto: String!) {\n  mergeMetadata(mergeFrom: $mergeFrom, mergeInto: $mergeInto)\n}\n\nmutation DisassociateMetadata($metadataId: String!) {\n  disassociateMetadata(metadataId: $metadataId)\n}\n\nmutation PostReview($input: PostReviewInput!) {\n  postReview(input: $input) {\n    id\n  }\n}\n\nmutation PresignedPutS3Url($input: PresignedPutUrlInput!) {\n  presignedPutS3Url(input: $input) {\n    key\n    uploadUrl\n  }\n}\n\nmutation RemoveEntityFromCollection($input: ChangeCollectionToEntityInput!) {\n  removeEntityFromCollection(input: $input) {\n    id\n  }\n}\n\nmutation TestUserNotificationPlatforms {\n  testUserNotificationPlatforms\n}\n\nmutation UpdateUser($input: UpdateUserInput!) {\n  updateUser(input: $input) {\n    id\n  }\n}\n\nmutation UpdateUserPreference($input: UpdateUserPreferenceInput!) {\n  updateUserPreference(input: $input)\n}"): (typeof documents)["mutation RegisterUser($input: RegisterUserInput!) {\n  registerUser(input: $input) {\n    __typename\n    ... on RegisterError {\n      error\n    }\n    ... on StringIdObject {\n      id\n    }\n  }\n}\n\nmutation LoginUser($input: AuthUserInput!) {\n  loginUser(input: $input) {\n    __typename\n    ... on LoginError {\n      error\n    }\n    ... on LoginResponse {\n      apiKey\n    }\n  }\n}\n\nmutation AddEntityToCollection($input: ChangeCollectionToEntityInput!) {\n  addEntityToCollection(input: $input)\n}\n\nmutation CommitMetadata($input: CommitMediaInput!) {\n  commitMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CommitMetadataGroup($input: CommitMediaInput!) {\n  commitMetadataGroup(input: $input) {\n    id\n  }\n}\n\nmutation CommitPerson($input: CommitPersonInput!) {\n  commitPerson(input: $input) {\n    id\n  }\n}\n\nmutation CreateCustomExercise($input: ExerciseInput!) {\n  createCustomExercise(input: $input)\n}\n\nmutation UpdateCustomExercise($input: UpdateCustomExerciseInput!) {\n  updateCustomExercise(input: $input)\n}\n\nmutation UpdateUserIntegration($input: UpdateUserIntegrationInput!) {\n  updateUserIntegration(input: $input)\n}\n\nmutation CreateCustomMetadata($input: CreateCustomMetadataInput!) {\n  createCustomMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CreateOrUpdateCollection($input: CreateOrUpdateCollectionInput!) {\n  createOrUpdateCollection(input: $input) {\n    id\n  }\n}\n\nmutation CreateReviewComment($input: CreateReviewCommentInput!) {\n  createReviewComment(input: $input)\n}\n\nmutation CreateUserMeasurement($input: UserMeasurementInput!) {\n  createUserMeasurement(input: $input)\n}\n\nmutation CreateUserNotificationPlatform($input: CreateUserNotificationPlatformInput!) {\n  createUserNotificationPlatform(input: $input)\n}\n\nmutation CreateUserIntegration($input: CreateUserIntegrationInput!) {\n  createUserIntegration(input: $input) {\n    id\n  }\n}\n\nmutation CreateUserWorkout($input: UserWorkoutInput!) {\n  createUserWorkout(input: $input)\n}\n\nmutation DeleteCollection($collectionName: String!) {\n  deleteCollection(collectionName: $collectionName)\n}\n\nmutation DeleteReview($reviewId: String!) {\n  deleteReview(reviewId: $reviewId)\n}\n\nmutation DeleteS3Object($key: String!) {\n  deleteS3Object(key: $key)\n}\n\nmutation DeleteSeenItem($seenId: String!) {\n  deleteSeenItem(seenId: $seenId) {\n    id\n  }\n}\n\nmutation DeleteUser($toDeleteUserId: String!) {\n  deleteUser(toDeleteUserId: $toDeleteUserId)\n}\n\nmutation DeleteUserIntegration($integrationId: String!) {\n  deleteUserIntegration(integrationId: $integrationId)\n}\n\nmutation DeleteUserMeasurement($timestamp: DateTime!) {\n  deleteUserMeasurement(timestamp: $timestamp)\n}\n\nmutation DeleteUserNotificationPlatform($notificationId: String!) {\n  deleteUserNotificationPlatform(notificationId: $notificationId)\n}\n\nmutation DeleteUserWorkout($workoutId: String!) {\n  deleteUserWorkout(workoutId: $workoutId)\n}\n\nmutation DeployBackgroundJob($jobName: BackgroundJob!) {\n  deployBackgroundJob(jobName: $jobName)\n}\n\nmutation DeployBulkProgressUpdate($input: [ProgressUpdateInput!]!) {\n  deployBulkProgressUpdate(input: $input)\n}\n\nmutation DeployExportJob {\n  deployExportJob\n}\n\nmutation DeployImportJob($input: DeployImportJobInput!) {\n  deployImportJob(input: $input)\n}\n\nmutation DeployUpdateMetadataJob($metadataId: String!) {\n  deployUpdateMetadataJob(metadataId: $metadataId)\n}\n\nmutation DeployUpdatePersonJob($personId: String!) {\n  deployUpdatePersonJob(personId: $personId)\n}\n\nmutation UpdateSeenItem($input: UpdateSeenItemInput!) {\n  updateSeenItem(input: $input)\n}\n\nmutation UpdateUserNotificationPlatform($input: UpdateUserNotificationPlatformInput!) {\n  updateUserNotificationPlatform(input: $input)\n}\n\nmutation UpdateUserWorkout($input: UpdateUserWorkoutInput!) {\n  updateUserWorkout(input: $input)\n}\n\nmutation GenerateAuthToken {\n  generateAuthToken\n}\n\nmutation MergeMetadata($mergeFrom: String!, $mergeInto: String!) {\n  mergeMetadata(mergeFrom: $mergeFrom, mergeInto: $mergeInto)\n}\n\nmutation DisassociateMetadata($metadataId: String!) {\n  disassociateMetadata(metadataId: $metadataId)\n}\n\nmutation PostReview($input: PostReviewInput!) {\n  postReview(input: $input) {\n    id\n  }\n}\n\nmutation PresignedPutS3Url($input: PresignedPutUrlInput!) {\n  presignedPutS3Url(input: $input) {\n    key\n    uploadUrl\n  }\n}\n\nmutation RemoveEntityFromCollection($input: ChangeCollectionToEntityInput!) {\n  removeEntityFromCollection(input: $input) {\n    id\n  }\n}\n\nmutation TestUserNotificationPlatforms {\n  testUserNotificationPlatforms\n}\n\nmutation UpdateUser($input: UpdateUserInput!) {\n  updateUser(input: $input) {\n    id\n  }\n}\n\nmutation UpdateUserPreference($input: UpdateUserPreferenceInput!) {\n  updateUserPreference(input: $input)\n}"];
 /**
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
  */
diff --git a/libs/generated/src/graphql/backend/graphql.ts b/libs/generated/src/graphql/backend/graphql.ts
index 5721ff1268..bacaf00c8c 100644
--- a/libs/generated/src/graphql/backend/graphql.ts
+++ b/libs/generated/src/graphql/backend/graphql.ts
@@ -1083,6 +1083,11 @@ export type MutationRoot = {
    * It is only available in development mode.
    */
   developmentMutation: Scalars['Boolean']['output'];
+  /**
+   * Delete all history and reviews for a given media item and remove it from all
+   * collections for the user.
+   */
+  disassociateMetadata: Scalars['Boolean']['output'];
   /** Generate an auth token without any expiry. */
   generateAuthToken: Scalars['String']['output'];
   /** Login a user using their username and password and return an auth token. */
@@ -1252,6 +1257,11 @@ export type MutationRootDeployUpdatePersonJobArgs = {
 };
 
 
+export type MutationRootDisassociateMetadataArgs = {
+  metadataId: Scalars['String']['input'];
+};
+
+
 export type MutationRootLoginUserArgs = {
   input: AuthUserInput;
 };
@@ -2717,6 +2727,13 @@ export type MergeMetadataMutationVariables = Exact<{
 
 export type MergeMetadataMutation = { mergeMetadata: boolean };
 
+export type DisassociateMetadataMutationVariables = Exact<{
+  metadataId: Scalars['String']['input'];
+}>;
+
+
+export type DisassociateMetadataMutation = { disassociateMetadata: boolean };
+
 export type PostReviewMutationVariables = Exact<{
   input: PostReviewInput;
 }>;
@@ -3103,6 +3120,7 @@ export const UpdateUserNotificationPlatformDocument = {"kind":"Document","defini
 export const UpdateUserWorkoutDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateUserWorkout"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateUserWorkoutInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateUserWorkout"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<UpdateUserWorkoutMutation, UpdateUserWorkoutMutationVariables>;
 export const GenerateAuthTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"GenerateAuthToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"generateAuthToken"}}]}}]} as unknown as DocumentNode<GenerateAuthTokenMutation, GenerateAuthTokenMutationVariables>;
 export const MergeMetadataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"MergeMetadata"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"mergeFrom"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"mergeInto"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"mergeMetadata"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"mergeFrom"},"value":{"kind":"Variable","name":{"kind":"Name","value":"mergeFrom"}}},{"kind":"Argument","name":{"kind":"Name","value":"mergeInto"},"value":{"kind":"Variable","name":{"kind":"Name","value":"mergeInto"}}}]}]}}]} as unknown as DocumentNode<MergeMetadataMutation, MergeMetadataMutationVariables>;
+export const DisassociateMetadataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DisassociateMetadata"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"metadataId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"disassociateMetadata"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"metadataId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"metadataId"}}}]}]}}]} as unknown as DocumentNode<DisassociateMetadataMutation, DisassociateMetadataMutationVariables>;
 export const PostReviewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"PostReview"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PostReviewInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"postReview"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<PostReviewMutation, PostReviewMutationVariables>;
 export const PresignedPutS3UrlDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"PresignedPutS3Url"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PresignedPutUrlInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"presignedPutS3Url"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"uploadUrl"}}]}}]}}]} as unknown as DocumentNode<PresignedPutS3UrlMutation, PresignedPutS3UrlMutationVariables>;
 export const RemoveEntityFromCollectionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RemoveEntityFromCollection"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ChangeCollectionToEntityInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"removeEntityFromCollection"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<RemoveEntityFromCollectionMutation, RemoveEntityFromCollectionMutationVariables>;
diff --git a/libs/graphql/src/backend/mutations/combined.gql b/libs/graphql/src/backend/mutations/combined.gql
index 81c6f628d7..4178cffd36 100644
--- a/libs/graphql/src/backend/mutations/combined.gql
+++ b/libs/graphql/src/backend/mutations/combined.gql
@@ -172,6 +172,10 @@ mutation MergeMetadata($mergeFrom: String!, $mergeInto: String!) {
   mergeMetadata(mergeFrom: $mergeFrom, mergeInto: $mergeInto)
 }
 
+mutation DisassociateMetadata($metadataId: String!) {
+  disassociateMetadata(metadataId: $metadataId)
+}
+
 mutation PostReview($input: PostReviewInput!) {
   postReview(input: $input) {
     id
@@ -204,3 +208,4 @@ mutation UpdateUser($input: UpdateUserInput!) {
 mutation UpdateUserPreference($input: UpdateUserPreferenceInput!) {
   updateUserPreference(input: $input)
 }
+


Test patch: