From 0261d2fe3c073b9b5f4bfe0a053180810d46285c Mon Sep 17 00:00:00 2001 From: trivernis Date: Sun, 30 Jan 2022 14:25:38 +0100 Subject: [PATCH] Remove remaining models in favour of dao-dto architecture Signed-off-by: trivernis --- .../mediarepo-logic/src/dao/repo/mod.rs | 70 +----- .../mediarepo-logic/src/dao/tag/mod.rs | 22 ++ .../mediarepo-logic/src/file_metadata.rs | 124 ---------- mediarepo-daemon/mediarepo-logic/src/lib.rs | 3 - .../mediarepo-logic/src/namespace.rs | 143 ----------- mediarepo-daemon/mediarepo-logic/src/tag.rs | 227 ------------------ .../mediarepo-socket/src/from_model.rs | 35 --- .../mediarepo-socket/src/namespaces/tags.rs | 21 +- 8 files changed, 37 insertions(+), 608 deletions(-) delete mode 100644 mediarepo-daemon/mediarepo-logic/src/file_metadata.rs delete mode 100644 mediarepo-daemon/mediarepo-logic/src/namespace.rs delete mode 100644 mediarepo-daemon/mediarepo-logic/src/tag.rs diff --git a/mediarepo-daemon/mediarepo-logic/src/dao/repo/mod.rs b/mediarepo-daemon/mediarepo-logic/src/dao/repo/mod.rs index 89945af..9157345 100644 --- a/mediarepo-daemon/mediarepo-logic/src/dao/repo/mod.rs +++ b/mediarepo-daemon/mediarepo-logic/src/dao/repo/mod.rs @@ -1,7 +1,5 @@ - use std::fmt::Debug; - use std::path::PathBuf; use sea_orm::DatabaseConnection; @@ -10,17 +8,10 @@ use mediarepo_core::error::RepoResult; use mediarepo_core::fs::file_hash_store::FileHashStore; use mediarepo_core::fs::thumbnail_store::ThumbnailStore; - -use mediarepo_core::utils::parse_namespace_and_tag; - +use crate::dao::{DaoContext, DaoProvider}; use mediarepo_database::get_database; use mediarepo_database::queries::analysis::{get_all_counts, Counts}; - -use crate::dao::{DaoContext, DaoProvider}; -use crate::namespace::Namespace; -use crate::tag::Tag; - #[derive(Clone)] pub struct Repo { db: DatabaseConnection, @@ -67,65 +58,6 @@ impl Repo { &self.db } - /// Finds all tags that are assigned to the given list of hashes - #[tracing::instrument(level = "debug", skip_all)] - pub async fn find_tags_for_file_identifiers(&self, cds: Vec>) -> RepoResult> { - Tag::for_cd_list(self.db.clone(), cds).await - } - - /// Adds or finds a tag - #[tracing::instrument(level = "debug", skip(self))] - pub async fn add_or_find_tag(&self, tag: S) -> RepoResult { - let (namespace, name) = parse_namespace_and_tag(tag.to_string()); - if let Some(namespace) = namespace { - self.add_or_find_namespaced_tag(name, namespace).await - } else { - self.add_or_find_unnamespaced_tag(name).await - } - } - - /// Adds or finds an unnamespaced tag - #[tracing::instrument(level = "debug", skip(self))] - pub async fn add_or_find_unnamespaced_tag(&self, name: String) -> RepoResult { - if let Some(tag) = Tag::by_name(self.db.clone(), &name, None).await? { - Ok(tag) - } else { - self.add_unnamespaced_tag(name).await - } - } - - /// Adds an unnamespaced tag - #[tracing::instrument(level = "debug", skip(self))] - pub async fn add_unnamespaced_tag(&self, name: String) -> RepoResult { - Tag::add(self.db.clone(), name, None).await - } - - /// Adds or finds a namespaced tag - #[tracing::instrument(level = "debug", skip(self))] - pub async fn add_or_find_namespaced_tag( - &self, - name: String, - namespace: String, - ) -> RepoResult { - if let Some(tag) = Tag::by_name(self.db.clone(), &name, Some(namespace.clone())).await? { - Ok(tag) - } else { - self.add_namespaced_tag(name, namespace).await - } - } - - /// Adds a namespaced tag - #[tracing::instrument(level = "debug", skip(self))] - pub async fn add_namespaced_tag(&self, name: String, namespace: String) -> RepoResult { - let namespace = - if let Some(namespace) = Namespace::by_name(self.db.clone(), &namespace).await? { - namespace - } else { - Namespace::add(self.db.clone(), namespace).await? - }; - Tag::add(self.db.clone(), name, Some(namespace.id())).await - } - /// Returns the size of the main storage #[inline] #[tracing::instrument(level = "debug", skip(self))] diff --git a/mediarepo-daemon/mediarepo-logic/src/dao/tag/mod.rs b/mediarepo-daemon/mediarepo-logic/src/dao/tag/mod.rs index 1403456..bd4a6e9 100644 --- a/mediarepo-daemon/mediarepo-logic/src/dao/tag/mod.rs +++ b/mediarepo-daemon/mediarepo-logic/src/dao/tag/mod.rs @@ -57,6 +57,28 @@ impl TagDao { Ok(namespaces) } + #[tracing::instrument(level = "debug", skip(self))] + pub async fn all_for_cds(&self, cds: Vec>) -> RepoResult> { + let tags = tag::Entity::find() + .find_also_related(namespace::Entity) + .join( + JoinType::LeftJoin, + content_descriptor_tag::Relation::Tag.def().rev(), + ) + .join( + JoinType::InnerJoin, + content_descriptor_tag::Relation::ContentDescriptorId.def(), + ) + .filter(content_descriptor::Column::Descriptor.is_in(cds)) + .all(&self.ctx.db) + .await? + .into_iter() + .map(map_tag_dto) + .collect(); + + Ok(tags) + } + #[tracing::instrument(level = "debug", skip(self))] pub async fn tags_for_cd(&self, cd_id: i64) -> RepoResult> { let tags = tag::Entity::find() diff --git a/mediarepo-daemon/mediarepo-logic/src/file_metadata.rs b/mediarepo-daemon/mediarepo-logic/src/file_metadata.rs deleted file mode 100644 index 0af2fbb..0000000 --- a/mediarepo-daemon/mediarepo-logic/src/file_metadata.rs +++ /dev/null @@ -1,124 +0,0 @@ -use std::fmt::Debug; - -use chrono::{Local, NaiveDateTime}; -use sea_orm::prelude::*; -use sea_orm::{DatabaseConnection, Set}; - -use mediarepo_core::error::RepoResult; -use mediarepo_database::entities::file_metadata; - -#[derive(Clone)] -pub struct FileMetadata { - db: DatabaseConnection, - model: file_metadata::Model, -} - -impl FileMetadata { - #[tracing::instrument(level = "trace")] - pub(crate) fn new(db: DatabaseConnection, model: file_metadata::Model) -> Self { - Self { db, model } - } - - /// Fetches the file by id - #[tracing::instrument(level = "debug", skip(db))] - pub async fn by_id(db: DatabaseConnection, id: i64) -> RepoResult> { - let file_metadata = file_metadata::Entity::find_by_id(id) - .one(&db) - .await? - .map(|m| FileMetadata::new(db, m)); - - Ok(file_metadata) - } - - /// Fetches metadata for all given file ids - #[tracing::instrument(level = "debug", skip(db))] - pub async fn all_by_ids(db: DatabaseConnection, ids: Vec) -> RepoResult> { - let file_metadata = file_metadata::Entity::find() - .filter(file_metadata::Column::FileId.is_in(ids)) - .all(&db) - .await? - .into_iter() - .map(|m| FileMetadata::new(db.clone(), m)) - .collect(); - - Ok(file_metadata) - } - - /// Adds a file with its hash to the database - #[tracing::instrument(level = "debug", skip(db))] - pub(crate) async fn add( - db: DatabaseConnection, - file_id: i64, - size: i64, - creation_time: NaiveDateTime, - change_time: NaiveDateTime, - ) -> RepoResult { - let file = file_metadata::ActiveModel { - file_id: Set(file_id), - size: Set(size), - import_time: Set(Local::now().naive_local()), - creation_time: Set(creation_time), - change_time: Set(change_time), - ..Default::default() - }; - let model = file.insert(&db).await?; - - Ok(Self::new(db, model)) - } - - pub fn file_id(&self) -> i64 { - self.model.file_id - } - - pub fn size(&self) -> i64 { - self.model.size - } - - pub fn name(&self) -> &Option { - &self.model.name - } - - pub fn comment(&self) -> &Option { - &self.model.comment - } - - pub fn import_time(&self) -> &NaiveDateTime { - &self.model.import_time - } - - pub fn creation_time(&self) -> &NaiveDateTime { - &self.model.creation_time - } - - pub fn change_time(&self) -> &NaiveDateTime { - &self.model.change_time - } - - /// Changes the name of the file - #[tracing::instrument(level = "debug", skip(self))] - pub async fn set_name(&mut self, name: S) -> RepoResult<()> { - let mut active_model = self.get_active_model(); - active_model.name = Set(Some(name.to_string())); - self.model = active_model.update(&self.db).await?; - - Ok(()) - } - - /// Changes the comment of the file - #[tracing::instrument(level = "debug", skip(self))] - pub async fn set_comment(&mut self, comment: S) -> RepoResult<()> { - let mut active_file = self.get_active_model(); - active_file.comment = Set(Some(comment.to_string())); - self.model = active_file.update(&self.db).await?; - - Ok(()) - } - - /// Returns the active model of the file with only the id set - fn get_active_model(&self) -> file_metadata::ActiveModel { - file_metadata::ActiveModel { - file_id: Set(self.file_id()), - ..Default::default() - } - } -} diff --git a/mediarepo-daemon/mediarepo-logic/src/lib.rs b/mediarepo-daemon/mediarepo-logic/src/lib.rs index 1d620f5..054053a 100644 --- a/mediarepo-daemon/mediarepo-logic/src/lib.rs +++ b/mediarepo-daemon/mediarepo-logic/src/lib.rs @@ -1,6 +1,3 @@ pub mod dao; pub mod dto; -pub mod file_metadata; -pub mod namespace; -pub mod tag; pub mod type_keys; diff --git a/mediarepo-daemon/mediarepo-logic/src/namespace.rs b/mediarepo-daemon/mediarepo-logic/src/namespace.rs deleted file mode 100644 index 8f173c7..0000000 --- a/mediarepo-daemon/mediarepo-logic/src/namespace.rs +++ /dev/null @@ -1,143 +0,0 @@ -use std::fmt::Debug; - -use sea_orm::{ - Condition, ConnectionTrait, DatabaseBackend, DatabaseConnection, InsertResult, Set, Statement, -}; -use sea_orm::prelude::*; - -use mediarepo_core::error::RepoResult; -use mediarepo_database::entities::namespace; - -#[derive(Clone)] -pub struct Namespace { - #[allow(dead_code)] - db: DatabaseConnection, - model: namespace::Model, -} - -impl Namespace { - #[tracing::instrument(level = "trace")] - pub(crate) fn new(db: DatabaseConnection, model: namespace::Model) -> Self { - Self { db, model } - } - - /// Retrieves a list of all namespaces - #[tracing::instrument(level = "debug", skip(db))] - pub async fn all(db: DatabaseConnection) -> RepoResult> { - let namespaces = namespace::Entity::find() - .all(&db) - .await? - .into_iter() - .map(|model| Self::new(db.clone(), model)) - .collect(); - - Ok(namespaces) - } - - /// Retrieves the namespace by id - #[tracing::instrument(level = "debug", skip(db))] - pub async fn by_id(db: DatabaseConnection, id: i64) -> RepoResult> { - let namespace = namespace::Entity::find_by_id(id) - .one(&db) - .await? - .map(|model| Self::new(db, model)); - - Ok(namespace) - } - - /// Retrieves a namespace by its name - #[tracing::instrument(level = "debug", skip(db))] - pub async fn by_name + Debug>( - db: DatabaseConnection, - name: S, - ) -> RepoResult> { - let namespace = namespace::Entity::find() - .filter(namespace::Column::Name.eq(name.as_ref())) - .one(&db) - .await? - .map(|model| Self::new(db, model)); - - Ok(namespace) - } - - /// Returns all namespaces by name - #[tracing::instrument(level = "debug", skip(db))] - pub async fn all_by_name(db: DatabaseConnection, names: Vec) -> RepoResult> { - if names.is_empty() { - return Ok(Vec::with_capacity(0)); - } - let mut condition = Condition::any(); - for name in names { - condition = condition.add(namespace::Column::Name.eq(name)); - } - - let namespaces = namespace::Entity::find() - .filter(condition) - .all(&db) - .await? - .into_iter() - .map(|model| Self::new(db.clone(), model)) - .collect(); - - Ok(namespaces) - } - - /// Adds all namespaces to the database - #[tracing::instrument(level = "debug", skip(db))] - pub async fn add_all(db: DatabaseConnection, names: Vec) -> RepoResult> { - if names.is_empty() { - return Ok(vec![]); - } - let models: Vec = names - .into_iter() - .map(|name| namespace::ActiveModel { - name: Set(name), - ..Default::default() - }) - .collect(); - let txn = db.begin().await?; - let last_id = txn - .query_one(Statement::from_string( - DatabaseBackend::Sqlite, - r#"SELECT MAX(id) AS "max_id" FROM namespaces;"#.to_owned(), - )) - .await? - .and_then(|result| result.try_get("", "max_id").ok()) - .unwrap_or(-1); - let result: InsertResult = - namespace::Entity::insert_many(models).exec(&txn).await?; - - let namespaces = namespace::Entity::find() - .filter(namespace::Column::Id.between(last_id, result.last_insert_id + 1)) - .all(&txn) - .await? - .into_iter() - .map(|model| Self::new(db.clone(), model)) - .collect(); - txn.commit().await?; - - Ok(namespaces) - } - - /// Adds a namespace to the database - #[tracing::instrument(level = "debug", skip(db))] - pub async fn add(db: DatabaseConnection, name: S) -> RepoResult { - let active_model = namespace::ActiveModel { - name: Set(name.to_string()), - ..Default::default() - }; - let model = active_model.insert(&db).await?; - - Ok(Self::new(db, model)) - } - - /// The ID of the namespace - pub fn id(&self) -> i64 { - self.model.id - } - - /// The name of the namespace - pub fn name(&self) -> &String { - &self.model.name - } -} diff --git a/mediarepo-daemon/mediarepo-logic/src/tag.rs b/mediarepo-daemon/mediarepo-logic/src/tag.rs deleted file mode 100644 index 378c854..0000000 --- a/mediarepo-daemon/mediarepo-logic/src/tag.rs +++ /dev/null @@ -1,227 +0,0 @@ -use std::fmt::Debug; - -use sea_orm::{Condition, DatabaseBackend, DatabaseConnection, JoinType, Set, Statement}; -use sea_orm::{InsertResult, QuerySelect}; -use sea_orm::prelude::*; -use sea_orm::query::ConnectionTrait; -use sea_orm::sea_query::Expr; - -use mediarepo_core::error::RepoResult; -use mediarepo_database::entities::content_descriptor; -use mediarepo_database::entities::content_descriptor_tag; -use mediarepo_database::entities::namespace; -use mediarepo_database::entities::tag; - -use crate::namespace::Namespace; - -#[derive(Clone)] -pub struct Tag { - db: DatabaseConnection, - model: tag::Model, - namespace: Option, -} - -impl Tag { - #[tracing::instrument(level = "trace")] - pub(crate) fn new( - db: DatabaseConnection, - model: tag::Model, - namespace: Option, - ) -> Self { - Self { - db, - model, - namespace, - } - } - - /// Returns all tags stored in the database - #[tracing::instrument(level = "debug", skip(db))] - pub async fn all(db: DatabaseConnection) -> RepoResult> { - let tags: Vec = tag::Entity::find() - .left_join(namespace::Entity) - .select_also(namespace::Entity) - .all(&db) - .await? - .into_iter() - .map(|(tag, namespace)| Self::new(db.clone(), tag, namespace)) - .collect(); - - Ok(tags) - } - - /// Returns the tag by id - #[tracing::instrument(level = "debug", skip(db))] - pub async fn by_id(db: DatabaseConnection, id: i64) -> RepoResult> { - let tag = tag::Entity::find_by_id(id) - .find_also_related(namespace::Entity) - .one(&db) - .await? - .map(|(model, namespace)| Self::new(db, model, namespace)); - - Ok(tag) - } - - /// Returns one tag by name and namespace - #[tracing::instrument(level = "debug", skip(db))] - pub async fn by_name( - db: DatabaseConnection, - name: S1, - namespace: Option, - ) -> RepoResult> { - let mut entries = Self::all_by_name(db, vec![(namespace, name.to_string())]).await?; - - Ok(entries.pop()) - } - - /// Retrieves the namespaced tags by name and namespace - #[tracing::instrument(level = "debug", skip(db))] - pub async fn all_by_name( - db: DatabaseConnection, - namespaces_with_names: Vec<(Option, String)>, - ) -> RepoResult> { - if namespaces_with_names.is_empty() { - return Ok(vec![]); - } - let mut or_condition = Condition::any(); - - for (namespace, name) in namespaces_with_names { - let mut all_condition = Condition::all(); - if !name.ends_with('*') { - all_condition = all_condition.add(tag::Column::Name.eq(name)) - } else if name.len() > 1 { - all_condition = all_condition - .add(tag::Column::Name.like(&*format!("{}%", name.trim_end_matches("*")))) - } else if namespace.is_none() { - continue; // would result in an empty condition otherwise - } - - all_condition = if let Some(namespace) = namespace { - all_condition.add(namespace::Column::Name.eq(namespace)) - } else { - all_condition.add(Expr::tbl(tag::Entity, tag::Column::NamespaceId).is_null()) - }; - or_condition = or_condition.add(all_condition); - } - - let tags: Vec = tag::Entity::find() - .find_also_related(namespace::Entity) - .filter(or_condition) - .group_by(tag::Column::Id) - .all(&db) - .await? - .into_iter() - .map(|(t, n)| Self::new(db.clone(), t, n)) - .collect(); - - Ok(tags) - } - - /// Returns all tags that are assigned to any of the passed hashes - #[tracing::instrument(level = "debug", skip_all)] - pub async fn for_cd_list(db: DatabaseConnection, cds: Vec>) -> RepoResult> { - let tags: Vec = tag::Entity::find() - .find_also_related(namespace::Entity) - .join( - JoinType::LeftJoin, - content_descriptor_tag::Relation::Tag.def().rev(), - ) - .join( - JoinType::InnerJoin, - content_descriptor_tag::Relation::ContentDescriptorId.def(), - ) - .filter(content_descriptor::Column::Descriptor.is_in(cds)) - .group_by(tag::Column::Id) - .all(&db) - .await? - .into_iter() - .map(|(t, n)| Self::new(db.clone(), t, n)) - .collect(); - - Ok(tags) - } - - pub async fn add_all( - db: DatabaseConnection, - namespaces_with_names: Vec<(Option, String)>, - ) -> RepoResult> { - if namespaces_with_names.is_empty() { - return Ok(vec![]); - } - let models: Vec = namespaces_with_names - .into_iter() - .map(|(namespace_id, name)| tag::ActiveModel { - name: Set(name), - namespace_id: Set(namespace_id), - ..Default::default() - }) - .collect(); - let txn = db.begin().await?; - let last_id: i64 = txn - .query_one(Statement::from_string( - DatabaseBackend::Sqlite, - r#"SELECT MAX(id) as "max_id" FROM tags"#.to_owned(), - )) - .await? - .and_then(|res| res.try_get("", "max_id").ok()) - .unwrap_or(-1); - - let result: InsertResult = - tag::Entity::insert_many(models).exec(&txn).await?; - let tags: Vec = tag::Entity::find() - .find_also_related(namespace::Entity) - .filter(tag::Column::Id.between(last_id, result.last_insert_id + 1)) - .all(&txn) - .await? - .into_iter() - .map(|(t, n)| Self::new(db.clone(), t, n)) - .collect(); - txn.commit().await?; - - Ok(tags) - } - - /// Adds a new tag to the database - #[tracing::instrument(level = "debug", skip(db))] - pub async fn add( - db: DatabaseConnection, - name: S, - namespace_id: Option, - ) -> RepoResult { - let active_model = tag::ActiveModel { - name: Set(name.to_string()), - namespace_id: Set(namespace_id), - ..Default::default() - }; - let model: tag::Model = active_model.insert(&db).await?; - let namespace = model.find_related(namespace::Entity).one(&db).await?; - - Ok(Self::new(db, model, namespace)) - } - - /// The ID of the tag - pub fn id(&self) -> i64 { - self.model.id - } - - /// The name of the tag - pub fn name(&self) -> &String { - &self.model.name - } - - /// The namespace of the tag - pub fn namespace(&self) -> Option { - self.namespace - .clone() - .map(|n| Namespace::new(self.db.clone(), n)) - } - - /// Returns the normalized name of the tag (namespace:tag) - pub fn normalized_name(&self) -> String { - if let Some(namespace) = &self.namespace { - format!("{}:{}", namespace.name, self.model.name) - } else { - self.model.name.to_owned() - } - } -} diff --git a/mediarepo-daemon/mediarepo-socket/src/from_model.rs b/mediarepo-daemon/mediarepo-socket/src/from_model.rs index eaa6e7e..eeffd64 100644 --- a/mediarepo-daemon/mediarepo-socket/src/from_model.rs +++ b/mediarepo-daemon/mediarepo-socket/src/from_model.rs @@ -5,27 +5,11 @@ use mediarepo_core::mediarepo_api::types::tags::{NamespaceResponse, TagResponse} use mediarepo_logic::dto::{ FileDto, FileMetadataDto, FileStatus as FileStatusModel, NamespaceDto, TagDto, ThumbnailDto, }; -use mediarepo_logic::file_metadata::FileMetadata; -use mediarepo_logic::namespace::Namespace; -use mediarepo_logic::tag::Tag; pub trait FromModel { fn from_model(model: M) -> Self; } -impl FromModel for FileMetadataResponse { - fn from_model(metadata: FileMetadata) -> Self { - Self { - file_id: metadata.file_id(), - name: metadata.name().to_owned(), - comment: metadata.comment().to_owned(), - creation_time: metadata.creation_time().to_owned(), - change_time: metadata.change_time().to_owned(), - import_time: metadata.import_time().to_owned(), - } - } -} - impl FromModel for FileMetadataResponse { fn from_model(model: FileMetadataDto) -> Self { Self { @@ -60,16 +44,6 @@ impl FromModel for FileStatus { } } -impl FromModel for TagResponse { - fn from_model(model: Tag) -> Self { - Self { - id: model.id(), - namespace: model.namespace().map(|n| n.name().to_owned()), - name: model.name().to_owned(), - } - } -} - impl FromModel for TagResponse { fn from_model(model: TagDto) -> Self { Self { @@ -91,15 +65,6 @@ impl FromModel for ThumbnailMetadataResponse { } } -impl FromModel for NamespaceResponse { - fn from_model(model: Namespace) -> Self { - Self { - id: model.id(), - name: model.name().to_owned(), - } - } -} - impl FromModel for NamespaceResponse { fn from_model(model: NamespaceDto) -> Self { Self { diff --git a/mediarepo-daemon/mediarepo-socket/src/namespaces/tags.rs b/mediarepo-daemon/mediarepo-socket/src/namespaces/tags.rs index 1bfde4c..bfd6895 100644 --- a/mediarepo-daemon/mediarepo-socket/src/namespaces/tags.rs +++ b/mediarepo-daemon/mediarepo-socket/src/namespaces/tags.rs @@ -6,7 +6,9 @@ use mediarepo_core::mediarepo_api::types::files::{GetFileTagsRequest, GetFilesTa use mediarepo_core::mediarepo_api::types::tags::{ ChangeFileTagsRequest, NamespaceResponse, TagResponse, }; +use mediarepo_core::utils::parse_namespace_and_tag; use mediarepo_logic::dao::DaoProvider; +use mediarepo_logic::dto::AddTagDto; use crate::from_model::FromModel; use crate::utils::{file_by_identifier, get_repo_from_context}; @@ -85,7 +87,8 @@ impl TagsNamespace { let repo = get_repo_from_context(ctx).await; let request = event.payload::()?; let tag_responses: Vec = repo - .find_tags_for_file_identifiers( + .tag() + .all_for_cds( request .cds .into_par_iter() @@ -102,17 +105,21 @@ impl TagsNamespace { Ok(()) } - /// Creates all tags given as input or returns the existing tag + /// Creates all tags given as input or returns the existing tags #[tracing::instrument(skip_all)] async fn create_tags(ctx: &Context, event: Event) -> IPCResult<()> { let repo = get_repo_from_context(ctx).await; let tags = event.payload::>()?; - let mut created_tags = Vec::new(); + let created_tags = repo + .tag() + .add_all( + tags.into_iter() + .map(parse_namespace_and_tag) + .map(AddTagDto::from_tuple) + .collect(), + ) + .await?; - for tag in tags { - let created_tag = repo.add_or_find_tag(tag).await?; - created_tags.push(created_tag); - } let responses: Vec = created_tags .into_iter() .map(TagResponse::from_model)