Move tag adding and retrieval by name to dao

Signed-off-by: trivernis <trivernis@protonmail.com>
pull/5/head
trivernis 2 years ago
parent 744475dd1e
commit 31addcda87
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

@ -16,6 +16,7 @@ use mediarepo_core::utils::parse_namespace_and_tag;
use mediarepo_database::get_database;
use mediarepo_database::queries::analysis::{get_all_counts, Counts};
use crate::dao::tag::by_name::TagByNameQuery;
use crate::dao::{DaoContext, DaoProvider};
use crate::namespace::Namespace;
use crate::tag::Tag;
@ -66,78 +67,12 @@ impl Repo {
&self.db
}
/// Converts a list of tag names to tag ids
#[tracing::instrument(level = "debug", skip(self))]
pub async fn tag_names_to_ids(&self, tags: Vec<String>) -> RepoResult<HashMap<String, i64>> {
let parsed_tags = tags
.iter()
.map(|tag| parse_namespace_and_tag(tag.clone()))
.unique()
.collect();
let db_tags = self.tags_by_names(parsed_tags).await?;
let tag_map: HashMap<String, i64> =
HashMap::from_iter(db_tags.into_iter().map(|t| (t.normalized_name(), t.id())));
Ok(tag_map)
}
/// Finds all tags by name
#[tracing::instrument(level = "debug", skip(self))]
pub async fn tags_by_names(&self, tags: Vec<(Option<String>, String)>) -> RepoResult<Vec<Tag>> {
Tag::all_by_name(self.db.clone(), tags).await
}
/// 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<Vec<u8>>) -> RepoResult<Vec<Tag>> {
Tag::for_cd_list(self.db.clone(), cds).await
}
/// Adds all tags that are not in the database to the database and returns the ones already existing as well
#[tracing::instrument(level = "debug", skip_all)]
pub async fn add_all_tags(&self, tags: Vec<(Option<String>, String)>) -> RepoResult<Vec<Tag>> {
let mut tags_to_add = tags.into_iter().unique().collect_vec();
let mut namespaces_to_add = tags_to_add
.iter()
.filter_map(|(namespace, _)| namespace.clone())
.unique()
.collect_vec();
let mut existing_namespaces =
Namespace::all_by_name(self.db.clone(), namespaces_to_add.clone()).await?;
{
let existing_namespaces_set = existing_namespaces
.iter()
.map(|n| n.name().clone())
.collect::<HashSet<String>>();
namespaces_to_add.retain(|namespace| !existing_namespaces_set.contains(namespace));
}
existing_namespaces
.append(&mut Namespace::add_all(self.db.clone(), namespaces_to_add).await?);
let mut existing_tags = self.tags_by_names(tags_to_add.clone()).await?;
{
let existing_tags_set = existing_tags
.iter()
.map(|t| (t.namespace().map(|n| n.name().clone()), t.name().clone()))
.collect::<HashSet<(Option<String>, String)>>();
tags_to_add.retain(|t| !existing_tags_set.contains(t));
}
let namespace_map = existing_namespaces
.into_iter()
.map(|namespace| (namespace.name().clone(), namespace.id()))
.collect::<HashMap<String, i64>>();
let tags_to_add = tags_to_add
.into_iter()
.map(|(nsp, name)| (nsp.and_then(|n| namespace_map.get(&n)).map(|i| *i), name))
.collect_vec();
existing_tags.append(&mut Tag::add_all(self.db.clone(), tags_to_add).await?);
Ok(existing_tags)
}
/// Adds or finds a tag
#[tracing::instrument(level = "debug", skip(self))]
pub async fn add_or_find_tag<S: ToString + Debug>(&self, tag: S) -> RepoResult<Tag> {

@ -0,0 +1,120 @@
use crate::dao::tag::{map_tag_dto, TagDao};
use crate::dto::{AddTagDto, NamespaceDto, TagDto};
use mediarepo_core::error::RepoResult;
use mediarepo_database::entities::{namespace, tag};
use sea_orm::prelude::*;
use sea_orm::ActiveValue::Set;
use sea_orm::{Condition, ConnectionTrait, DatabaseTransaction};
use std::collections::HashMap;
use std::iter::FromIterator;
impl TagDao {
#[tracing::instrument(level = "debug", skip(self))]
pub async fn add_all(&self, mut tags: Vec<AddTagDto>) -> RepoResult<Vec<TagDto>> {
let namespaces = tags.iter().filter_map(|t| t.namespace.clone()).collect();
let trx = self.ctx.db.begin().await?;
let existing_tags = tags_by_name(&trx, tags.clone()).await?;
if existing_tags.len() == tags.len() {
return Ok(existing_tags);
}
let existing_tag_map: HashMap<String, TagDto> =
HashMap::from_iter(existing_tags.into_iter().map(|t| (t.normalized_name(), t)));
let namespace_map = add_or_get_all_namespaces(&trx, namespaces).await?;
tags.retain(|dto| !existing_tag_map.contains_key(&dto.normalized_name()));
let tag_models: Vec<tag::ActiveModel> = tags
.iter()
.map(|t| tag::ActiveModel {
name: Set(t.name.to_owned()),
namespace_id: Set(t
.namespace
.as_ref()
.and_then(|n| namespace_map.get(n))
.map(|n| n.id())),
..Default::default()
})
.collect();
tag::Entity::insert_many(tag_models).exec(&trx).await?;
let mut tag_dtos = tags_by_name(&trx, tags).await?;
trx.commit().await?;
tag_dtos.append(&mut existing_tag_map.into_iter().map(|(_, dto)| dto).collect());
Ok(tag_dtos)
}
}
async fn add_or_get_all_namespaces(
trx: &DatabaseTransaction,
mut namespaces: Vec<String>,
) -> RepoResult<HashMap<String, NamespaceDto>> {
let existing_namespaces = namespaces_by_name(trx, namespaces.clone()).await?;
let mut namespace_map = HashMap::from_iter(
existing_namespaces
.into_iter()
.map(|nsp| (nsp.name().to_owned(), nsp)),
);
if namespaces.len() == namespace_map.len() {
return Ok(namespace_map);
}
namespaces.retain(|nsp| !namespace_map.contains_key(nsp));
let namespace_models: Vec<namespace::ActiveModel> = namespaces
.iter()
.map(|nsp| namespace::ActiveModel {
name: Set(nsp.to_owned()),
..Default::default()
})
.collect();
namespace::Entity::insert_many(namespace_models)
.exec(trx)
.await?;
let additional_namespaces = namespaces_by_name(trx, namespaces.clone()).await?;
for nsp in additional_namespaces {
namespace_map.insert(nsp.name().to_owned(), nsp);
}
Ok(namespace_map)
}
async fn namespaces_by_name(
trx: &DatabaseTransaction,
names: Vec<String>,
) -> RepoResult<Vec<NamespaceDto>> {
let namespaces: Vec<NamespaceDto> = namespace::Entity::find()
.filter(namespace::Column::Name.is_in(names))
.all(trx)
.await?
.into_iter()
.map(NamespaceDto::new)
.collect();
Ok(namespaces)
}
async fn tags_by_name(trx: &DatabaseTransaction, tags: Vec<AddTagDto>) -> RepoResult<Vec<TagDto>> {
let condition = tags
.into_iter()
.map(build_tag_condition)
.fold(Condition::any(), Condition::add);
let tags = tag::Entity::find()
.find_also_related(namespace::Entity)
.filter(condition)
.all(trx)
.await?
.into_iter()
.map(map_tag_dto)
.collect();
Ok(tags)
}
fn build_tag_condition(tag: AddTagDto) -> Condition {
if let Some(namespace) = tag.namespace {
Condition::all()
.add(tag::Column::Name.eq(tag.name))
.add(namespace::Column::Name.eq(namespace))
} else {
Condition::all().add(tag::Column::Name.eq(tag.name))
}
}

@ -0,0 +1,63 @@
use crate::dao::tag::{map_tag_dto, TagDao};
use crate::dto::TagDto;
use mediarepo_core::error::RepoResult;
use mediarepo_database::entities::{namespace, tag};
use sea_orm::prelude::*;
use sea_orm::sea_query::Expr;
use sea_orm::Condition;
#[derive(Clone, Debug)]
pub struct TagByNameQuery {
pub namespace: Option<String>,
pub name: String,
}
impl TagDao {
/// Filters all tags by names
/// wildcards are supported
#[tracing::instrument(level = "debug", skip(self))]
pub async fn all_by_name(&self, names: Vec<TagByNameQuery>) -> RepoResult<Vec<TagDto>> {
let mut condition_count = 0;
let condition = names
.into_iter()
.filter_map(name_query_to_condition)
.inspect(|_| condition_count += 1)
.fold(Condition::any(), Condition::add);
if condition_count == 0 {
return Ok(vec![]);
}
let tags = tag::Entity::find()
.find_also_related(namespace::Entity)
.filter(condition)
.all(&self.ctx.db)
.await?
.into_iter()
.map(map_tag_dto)
.collect();
Ok(tags)
}
}
fn name_query_to_condition(query: TagByNameQuery) -> Option<Condition> {
let TagByNameQuery { namespace, name } = query;
let mut condition = Condition::all();
if !name.ends_with('*') {
condition = condition.add(tag::Column::Name.eq(name))
} else if name.len() > 1 {
condition =
condition.add(tag::Column::Name.like(&*format!("{}%", name.trim_end_matches("*"))))
} else if namespace.is_none() {
return None;
}
condition = if let Some(namespace) = namespace {
condition.add(namespace::Column::Name.eq(namespace))
} else {
condition.add(Expr::tbl(tag::Entity, tag::Column::NamespaceId).is_null())
};
Some(condition)
}

@ -1,13 +1,20 @@
use sea_orm::prelude::*;
use sea_orm::JoinType;
use sea_orm::QuerySelect;
use std::collections::HashMap;
use std::iter::FromIterator;
use mediarepo_core::error::RepoResult;
use mediarepo_core::mediarepo_api::types::filtering::TagQuery;
use mediarepo_core::utils::parse_namespace_and_tag;
use mediarepo_database::entities::{content_descriptor, content_descriptor_tag, namespace, tag};
use crate::dao::tag::by_name::TagByNameQuery;
use crate::dao::{DaoContext, DaoProvider};
use crate::dto::{NamespaceDto, TagDto};
pub mod add;
pub mod by_name;
pub mod mappings;
pub struct TagDao {
@ -71,6 +78,26 @@ impl TagDao {
Ok(tags)
}
/// Returns a map mapping tag names to ids
#[tracing::instrument(level = "debug", skip(self))]
pub async fn normalized_tags_to_ids(
&self,
names: Vec<String>,
) -> RepoResult<HashMap<String, i64>> {
let queries = names
.into_iter()
.map(parse_namespace_and_tag)
.map(|(namespace, name)| TagByNameQuery { namespace, name })
.collect();
let tags = self.all_by_name(queries).await?;
let tag_map = HashMap::from_iter(
tags.into_iter()
.map(|tag| (tag.normalized_name(), tag.id())),
);
Ok(tag_map)
}
}
fn map_tag_dto(result: (tag::Model, Option<namespace::Model>)) -> TagDto {

@ -28,4 +28,35 @@ impl TagDto {
pub fn namespace(&self) -> Option<&NamespaceDto> {
self.namespace.as_ref()
}
/// 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.name())
} else {
self.name().to_owned()
}
}
}
#[derive(Clone, Debug)]
pub struct AddTagDto {
pub namespace: Option<String>,
pub name: String,
}
impl AddTagDto {
pub fn from_tuple(tuple: (Option<String>, String)) -> Self {
let (namespace, name) = tuple;
Self { namespace, name }
}
/// Returns the normalized name of the tag (namespace:tag)
pub fn normalized_name(&self) -> String {
if let Some(namespace) = &self.namespace {
format!("{}:{}", namespace, &self.name)
} else {
self.name.to_owned()
}
}
}

@ -15,7 +15,7 @@ use mediarepo_core::mediarepo_api::types::identifier::FileIdentifier;
use mediarepo_core::thumbnailer::ThumbnailSize;
use mediarepo_core::utils::parse_namespace_and_tag;
use mediarepo_logic::dao::DaoProvider;
use mediarepo_logic::dto::{AddFileDto, UpdateFileDto, UpdateFileMetadataDto};
use mediarepo_logic::dto::{AddFileDto, AddTagDto, UpdateFileDto, UpdateFileMetadataDto};
use crate::from_model::FromModel;
use crate::namespaces::files::searching::find_files_for_filters;
@ -171,7 +171,13 @@ impl FilesNamespace {
};
let tags = repo
.add_all_tags(tags.into_iter().map(parse_namespace_and_tag).collect())
.tag()
.add_all(
tags.into_iter()
.map(parse_namespace_and_tag)
.map(AddTagDto::from_tuple)
.collect(),
)
.await?;
let tag_ids: Vec<i64> = tags.into_iter().map(|t| t.id()).unique().collect();
repo.tag()

@ -6,10 +6,10 @@ use mediarepo_core::mediarepo_api::types::files::FileStatus as ApiFileStatus;
use mediarepo_core::mediarepo_api::types::filtering::{
FilterExpression, FilterQuery, PropertyQuery, TagQuery, ValueComparator,
};
use mediarepo_logic::dao::DaoProvider;
use mediarepo_logic::dao::file::find::{FilterFileProperty, FilterProperty, OrderingComparator};
use mediarepo_logic::dao::file::find::NegatableComparator::{Is, IsNot};
use mediarepo_logic::dao::file::find::{FilterFileProperty, FilterProperty, OrderingComparator};
use mediarepo_logic::dao::repo::Repo;
use mediarepo_logic::dao::DaoProvider;
use mediarepo_logic::dto::{FileDto, FileStatus};
#[tracing::instrument(level = "debug", skip(repo))]
@ -18,7 +18,7 @@ pub async fn find_files_for_filters(
expressions: Vec<FilterExpression>,
) -> RepoResult<Vec<FileDto>> {
let tag_names = get_tag_names_from_expressions(&expressions);
let tag_id_map = repo.tag_names_to_ids(tag_names).await?;
let tag_id_map = repo.tag().normalized_tags_to_ids(tag_names).await?;
let filters = build_filters_from_expressions(expressions, &tag_id_map);
repo.file().find(filters).await

Loading…
Cancel
Save