From f9c1a12a11d565f22fdb05c52acb66737000a9a6 Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Thu, 16 Jan 2025 15:34:52 +0100 Subject: [PATCH 1/3] Add `model` top level module and move `TranslatedString` into it This is the start of a restructuring where we move several types that currently randomly live in one submodule, despite interacting with several. --- backend/src/api/model/acl.rs | 2 +- backend/src/api/model/known_roles.rs | 2 +- backend/src/auth/config.rs | 2 +- backend/src/cmd/known_groups.rs | 3 ++- backend/src/config/general.rs | 3 ++- backend/src/config/mod.rs | 2 -- backend/src/config/theme.rs | 3 ++- backend/src/main.rs | 1 + backend/src/model/mod.rs | 14 ++++++++++++++ backend/src/{config => model}/translated_string.rs | 0 10 files changed, 24 insertions(+), 8 deletions(-) create mode 100644 backend/src/model/mod.rs rename backend/src/{config => model}/translated_string.rs (100%) diff --git a/backend/src/api/model/acl.rs b/backend/src/api/model/acl.rs index 530d2e0a0..56bcd1394 100644 --- a/backend/src/api/model/acl.rs +++ b/backend/src/api/model/acl.rs @@ -2,7 +2,7 @@ use juniper::{GraphQLInputObject, GraphQLObject}; use postgres_types::BorrowToSql; use serde::Serialize; -use crate::{api::{err::ApiResult, Context}, config::TranslatedString, db::util::select}; +use crate::{api::{err::ApiResult, Context}, model::TranslatedString, db::util::select}; diff --git a/backend/src/api/model/known_roles.rs b/backend/src/api/model/known_roles.rs index d6ea6732d..c207c3348 100644 --- a/backend/src/api/model/known_roles.rs +++ b/backend/src/api/model/known_roles.rs @@ -3,7 +3,7 @@ use serde::Deserialize; use crate::{ api::{err::ApiResult, Context}, - config::TranslatedString, + model::TranslatedString, db::util::{impl_from_db, select}, prelude::*, }; diff --git a/backend/src/auth/config.rs b/backend/src/auth/config.rs index de7808077..d6c6b2423 100644 --- a/backend/src/auth/config.rs +++ b/backend/src/auth/config.rs @@ -4,7 +4,7 @@ use hyper::{http::HeaderName, Uri}; use secrecy::SecretString; use serde::{Deserialize, Deserializer, de::Error}; -use crate::{config::{parse_normal_http_uri, TranslatedString}, prelude::*}; +use crate::{config::parse_normal_http_uri, model::TranslatedString, prelude::*}; use super::JwtConfig; diff --git a/backend/src/cmd/known_groups.rs b/backend/src/cmd/known_groups.rs index 69cb1074b..6b4bd03ad 100644 --- a/backend/src/cmd/known_groups.rs +++ b/backend/src/cmd/known_groups.rs @@ -6,7 +6,8 @@ use serde_json::json; use crate::{ api::model::known_roles::KnownGroup, - config::{Config, TranslatedString}, + config::Config, + model::TranslatedString, db, prelude::*, }; diff --git a/backend/src/config/general.rs b/backend/src/config/general.rs index 51e4dc6fe..da72df5c6 100644 --- a/backend/src/config/general.rs +++ b/backend/src/config/general.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; -use super::{HttpHost, TranslatedString}; +use crate::model::TranslatedString; +use super::HttpHost; #[derive(Debug, confique::Config)] diff --git a/backend/src/config/mod.rs b/backend/src/config/mod.rs index 086d186e4..bc3c08688 100644 --- a/backend/src/config/mod.rs +++ b/backend/src/config/mod.rs @@ -12,14 +12,12 @@ use crate::prelude::*; mod color; mod general; mod theme; -mod translated_string; mod matomo; mod opencast; mod player; mod upload; pub(crate) use self::{ - translated_string::TranslatedString, theme::{ThemeConfig, LogoDef}, matomo::MatomoConfig, opencast::OpencastConfig, diff --git a/backend/src/config/theme.rs b/backend/src/config/theme.rs index fd62fa01b..2d1dbebb4 100644 --- a/backend/src/config/theme.rs +++ b/backend/src/config/theme.rs @@ -1,7 +1,8 @@ use std::{collections::HashMap, fmt, path::PathBuf}; use serde::{Deserialize, Serialize}; -use super::{color::ColorConfig, translated_string::LangKey}; +use crate::model::LangKey; +use super::{color::ColorConfig}; #[derive(Debug, confique::Config)] diff --git a/backend/src/main.rs b/backend/src/main.rs index f2ebc7f48..115efbeac 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -24,6 +24,7 @@ mod db; mod http; mod logger; mod metrics; +mod model; mod prelude; mod search; mod sync; diff --git a/backend/src/model/mod.rs b/backend/src/model/mod.rs new file mode 100644 index 000000000..cba770b77 --- /dev/null +++ b/backend/src/model/mod.rs @@ -0,0 +1,14 @@ +//! Items that define the domain data model and logic. +//! +//! There are many types that represent "user-visible data", i.e. data that +//! directly models the application domain and not technical helpers (like a DB +//! pool). These are big high level types like `Event`, but also things like +//! `EventTrack` and `TranslatedString`. These commonly don't neatly fit into +//! either of `db`, `api` or any other submodule as they are used in multiple +//! situations (loading from DB, exposing via API, ...). + +mod translated_string; + +pub(crate) use self::{ + translated_string::{LangKey, TranslatedString}, +}; diff --git a/backend/src/config/translated_string.rs b/backend/src/model/translated_string.rs similarity index 100% rename from backend/src/config/translated_string.rs rename to backend/src/model/translated_string.rs From 10aef3cec01602089501eef58703f739208a1682 Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Thu, 16 Jan 2025 15:47:56 +0100 Subject: [PATCH 2/3] Move `Key` from `db::types` to `model` --- backend/src/api/id.rs | 51 +------------ backend/src/api/model/block/mod.rs | 3 +- backend/src/api/model/block/mutations.rs | 3 +- backend/src/api/model/event.rs | 3 +- backend/src/api/model/playlist/mod.rs | 3 +- backend/src/api/model/realm/mod.rs | 3 +- backend/src/api/model/realm/mutations.rs | 2 +- backend/src/api/model/series.rs | 3 +- backend/src/db/tests/mod.rs | 2 +- backend/src/db/tests/search_queue.rs | 2 +- backend/src/db/tests/util.rs | 2 +- backend/src/db/types.rs | 45 +----------- backend/src/model/key.rs | 94 ++++++++++++++++++++++++ backend/src/model/mod.rs | 2 + backend/src/rss.rs | 3 +- backend/src/search/event.rs | 7 +- backend/src/search/mod.rs | 2 +- backend/src/search/playlist.rs | 3 +- backend/src/search/realm.rs | 2 +- backend/src/search/series.rs | 3 +- backend/src/search/update.rs | 3 +- backend/src/search/user.rs | 3 +- backend/src/sync/text/mod.rs | 3 +- 23 files changed, 136 insertions(+), 111 deletions(-) create mode 100644 backend/src/model/key.rs diff --git a/backend/src/api/id.rs b/backend/src/api/id.rs index a19302227..7ac32c810 100644 --- a/backend/src/api/id.rs +++ b/backend/src/api/id.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use static_assertions::const_assert; use std::fmt; -use crate::{db::types::Key, util::{base64_decode, BASE64_DIGITS}}; +use crate::model::Key; /// An opaque, globally-unique identifier for all "nodes" that the GraphQL API @@ -138,30 +138,6 @@ impl Id { } } -impl Key { - pub(crate) fn from_base64(s: &str) -> Option { - if s.len() != 11 { - return None; - } - - decode_base64(s.as_bytes()) - } - - pub(crate) fn to_base64<'a>(&self, out: &'a mut [u8; 11]) -> &'a str { - // Base64 encoding. After this loop, `n` is always 0, because `u64::MAX` - // divided by 64 eleven times is 0. - let mut n = self.0; - for i in (0..out.len()).rev() { - out[i] = BASE64_DIGITS[(n % 64) as usize]; - n /= 64; - } - debug_assert!(n == 0); - - std::str::from_utf8(out) - .expect("bug: base64 did produce non-ASCII character") - } -} - impl std::str::FromStr for Id { type Err = &'static str; @@ -191,31 +167,10 @@ impl fmt::Display for Id { } } - -fn decode_base64(src: &[u8]) -> Option { - let src: [u8; 11] = src.try_into().ok()?; - - // Make sure the string doesn't decode to a number > `u64::MAX`. Luckily, - // checking that is easy. `u64::MAX` encodes to `P__________`, so the next - // higher number would carry through and make the highest digit a `Q`. So we - // just make sure the first digit is between 'A' and 'P'. - if src[0] > b'P' || src[0] < b'A' { - return None; - } - - src.iter() - .rev() - .enumerate() - .map(|(i, &d)| base64_decode(d).map(|n| n as u64 * 64u64.pow(i as u32))) - .sum::>() - .map(Key) -} - - #[cfg(test)] mod tests { use std::str::FromStr; - use super::{Id, Key, BASE64_DIGITS}; + use super::{Id, Key}; #[test] fn simple() { @@ -265,7 +220,7 @@ mod tests { let id = Id { kind: Id::REALM_KIND, key: Key((n as u64) << shift) }; let s = id.to_string(); assert_eq!(s[..2].as_bytes(), Id::REALM_KIND); - assert!(s[2..].bytes().all(|d| BASE64_DIGITS.contains(&d))); + assert!(s[2..].bytes().all(|d| crate::util::BASE64_DIGITS.contains(&d))); } } } diff --git a/backend/src/api/model/block/mod.rs b/backend/src/api/model/block/mod.rs index fdfc59300..b134d73bb 100644 --- a/backend/src/api/model/block/mod.rs +++ b/backend/src/api/model/block/mod.rs @@ -15,7 +15,8 @@ use crate::{ Context, Id, }, - db::{types::Key, util::impl_from_db}, + model::Key, + db::util::impl_from_db, prelude::*, }; diff --git a/backend/src/api/model/block/mutations.rs b/backend/src/api/model/block/mutations.rs index 06e3c4f7b..f038ad699 100644 --- a/backend/src/api/model/block/mutations.rs +++ b/backend/src/api/model/block/mutations.rs @@ -2,7 +2,8 @@ use juniper::{GraphQLInputObject, GraphQLObject}; use crate::{ api::{Context, Id, err::{ApiResult, invalid_input}, model::realm::Realm}, - db::{types::Key, util::select}, + db::util::select, + model::Key, prelude::*, }; use super::{BlockValue, VideoListOrder, VideoListLayout}; diff --git a/backend/src/api/model/event.rs b/backend/src/api/model/event.rs index 541ab7d33..2b16b5854 100644 --- a/backend/src/api/model/event.rs +++ b/backend/src/api/model/event.rs @@ -24,9 +24,10 @@ use crate::{ }, }, db::{ - types::{EventCaption, EventSegment, EventState, EventTrack, ExtraMetadata, Key, Credentials}, + types::{EventCaption, EventSegment, EventState, EventTrack, ExtraMetadata, Credentials}, util::{impl_from_db, select}, }, + model::Key, prelude::*, }; diff --git a/backend/src/api/model/playlist/mod.rs b/backend/src/api/model/playlist/mod.rs index 010ce4152..55de1b8fe 100644 --- a/backend/src/api/model/playlist/mod.rs +++ b/backend/src/api/model/playlist/mod.rs @@ -5,7 +5,8 @@ use crate::{ api::{ common::NotAllowed, err::ApiResult, Context, Id, Node, NodeValue }, - db::{types::Key, util::{impl_from_db, select}}, + db::util::{impl_from_db, select}, + model::Key, prelude::*, }; diff --git a/backend/src/api/model/realm/mod.rs b/backend/src/api/model/realm/mod.rs index 34b1c1511..8414d5793 100644 --- a/backend/src/api/model/realm/mod.rs +++ b/backend/src/api/model/realm/mod.rs @@ -11,7 +11,8 @@ use crate::{ NodeValue, }, auth::AuthContext, - db::{types::Key, util::{impl_from_db, select}}, + db::util::{impl_from_db, select}, + model::Key, prelude::*, }; use super::block::BlockValue; diff --git a/backend/src/api/model/realm/mutations.rs b/backend/src/api/model/realm/mutations.rs index 5263017d2..5b34144be 100644 --- a/backend/src/api/model/realm/mutations.rs +++ b/backend/src/api/model/realm/mutations.rs @@ -8,7 +8,7 @@ use crate::{ model::block::RemovedBlock, }, auth::AuthContext, - db::types::Key, + model::Key, prelude::*, }; use super::{Realm, RealmOrder}; diff --git a/backend/src/api/model/series.rs b/backend/src/api/model/series.rs index d91a32c26..e5294797a 100644 --- a/backend/src/api/model/series.rs +++ b/backend/src/api/model/series.rs @@ -8,7 +8,8 @@ use crate::{ err::{invalid_input, ApiResult}, model::{event::AuthorizedEvent, realm::Realm}, }, - db::{types::{ExtraMetadata, Key, SeriesState as State}, util::impl_from_db}, + db::{types::{ExtraMetadata, SeriesState as State}, util::impl_from_db}, + model::Key, prelude::*, }; diff --git a/backend/src/db/tests/mod.rs b/backend/src/db/tests/mod.rs index 70d2f184a..b576f86a3 100644 --- a/backend/src/db/tests/mod.rs +++ b/backend/src/db/tests/mod.rs @@ -1,4 +1,4 @@ -use crate::{prelude::*, db::types::Key}; +use crate::{prelude::*, model::Key}; use super::DbConfig; use self::util::TestDb; diff --git a/backend/src/db/tests/search_queue.rs b/backend/src/db/tests/search_queue.rs index 805351ff7..16a0627d8 100644 --- a/backend/src/db/tests/search_queue.rs +++ b/backend/src/db/tests/search_queue.rs @@ -1,4 +1,4 @@ -use crate::{prelude::*, db::types::Key, search::IndexItemKind}; +use crate::{prelude::*, model::Key, search::IndexItemKind}; use super::util::TestDb; diff --git a/backend/src/db/tests/util.rs b/backend/src/db/tests/util.rs index eb1f9a616..a4531deb4 100644 --- a/backend/src/db/tests/util.rs +++ b/backend/src/db/tests/util.rs @@ -2,7 +2,7 @@ use std::{ops::Deref, collections::HashSet}; use secrecy::ExposeSecret; use tokio_postgres::{Client, NoTls}; -use crate::{prelude::*, db::types::Key, search::IndexItemKind}; +use crate::{prelude::*, model::Key, search::IndexItemKind}; use super::DbConfig; diff --git a/backend/src/db/types.rs b/backend/src/db/types.rs index 405ca60b3..dcb57102a 100644 --- a/backend/src/db/types.rs +++ b/backend/src/db/types.rs @@ -1,4 +1,4 @@ -use std::{fmt, collections::HashMap}; +use std::collections::HashMap; use bytes::BytesMut; use chrono::{DateTime, Utc}; @@ -6,48 +6,7 @@ use juniper::GraphQLEnum; use postgres_types::{FromSql, ToSql}; use serde::{Deserialize, Serialize}; - -/// Our primary database ID type, which we call "key". In the database, it's a -/// `bigint` (`i64`), but we have a separate Rust type for it for several -/// reasons. Implements `ToSql` and `FromSql` by casting to/from `i64`. -#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub(crate) struct Key(pub(crate) u64); - -impl ToSql for Key { - fn to_sql( - &self, - ty: &postgres_types::Type, - out: &mut BytesMut, - ) -> Result> { - (self.0 as i64).to_sql(ty, out) - } - - fn accepts(ty: &postgres_types::Type) -> bool { - ::accepts(ty) - } - - postgres_types::to_sql_checked!(); -} - -impl<'a> FromSql<'a> for Key { - fn from_sql( - ty: &postgres_types::Type, - raw: &'a [u8], - ) -> Result> { - i64::from_sql(ty, raw).map(|i| Key(i as u64)) - } - - fn accepts(ty: &postgres_types::Type) -> bool { - ::accepts(ty) - } -} - -impl fmt::Debug for Key { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut buf = [0; 11]; - write!(f, "Key({} :: {})", self.0 as i64, self.to_base64(&mut buf)) - } -} +use crate::model::Key; /// Represents the `event_track` type defined in `5-events.sql`. diff --git a/backend/src/model/key.rs b/backend/src/model/key.rs new file mode 100644 index 000000000..f8f77d80f --- /dev/null +++ b/backend/src/model/key.rs @@ -0,0 +1,94 @@ +use std::fmt; + +use bytes::BytesMut; +use postgres_types::{FromSql, ToSql}; +use serde::{Deserialize, Serialize}; + +use crate::util::{BASE64_DIGITS, base64_decode}; + + + +/// Our primary ID type, which we call "key". In the database, it's a +/// `bigint` (`i64`), but we have a separate Rust type for it for several +/// reasons. Implements `ToSql` and `FromSql` by casting to/from `i64`. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub(crate) struct Key(pub(crate) u64); + +impl Key { + pub(crate) fn from_base64(s: &str) -> Option { + if s.len() != 11 { + return None; + } + + decode_base64(s.as_bytes()) + } + + pub(crate) fn to_base64<'a>(&self, out: &'a mut [u8; 11]) -> &'a str { + // Base64 encoding. After this loop, `n` is always 0, because `u64::MAX` + // divided by 64 eleven times is 0. + let mut n = self.0; + for i in (0..out.len()).rev() { + out[i] = BASE64_DIGITS[(n % 64) as usize]; + n /= 64; + } + debug_assert!(n == 0); + + std::str::from_utf8(out) + .expect("bug: base64 did produce non-ASCII character") + } +} + +fn decode_base64(src: &[u8]) -> Option { + let src: [u8; 11] = src.try_into().ok()?; + + // Make sure the string doesn't decode to a number > `u64::MAX`. Luckily, + // checking that is easy. `u64::MAX` encodes to `P__________`, so the next + // higher number would carry through and make the highest digit a `Q`. So we + // just make sure the first digit is between 'A' and 'P'. + if src[0] > b'P' || src[0] < b'A' { + return None; + } + + src.iter() + .rev() + .enumerate() + .map(|(i, &d)| base64_decode(d).map(|n| n as u64 * 64u64.pow(i as u32))) + .sum::>() + .map(Key) +} + +impl ToSql for Key { + fn to_sql( + &self, + ty: &postgres_types::Type, + out: &mut BytesMut, + ) -> Result> { + (self.0 as i64).to_sql(ty, out) + } + + fn accepts(ty: &postgres_types::Type) -> bool { + ::accepts(ty) + } + + postgres_types::to_sql_checked!(); +} + +impl<'a> FromSql<'a> for Key { + fn from_sql( + ty: &postgres_types::Type, + raw: &'a [u8], + ) -> Result> { + i64::from_sql(ty, raw).map(|i| Key(i as u64)) + } + + fn accepts(ty: &postgres_types::Type) -> bool { + ::accepts(ty) + } +} + +impl fmt::Debug for Key { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut buf = [0; 11]; + write!(f, "Key({} :: {})", self.0 as i64, self.to_base64(&mut buf)) + } +} diff --git a/backend/src/model/mod.rs b/backend/src/model/mod.rs index cba770b77..cf9df96ac 100644 --- a/backend/src/model/mod.rs +++ b/backend/src/model/mod.rs @@ -7,8 +7,10 @@ //! either of `db`, `api` or any other submodule as they are used in multiple //! situations (loading from DB, exposing via API, ...). +mod key; mod translated_string; pub(crate) use self::{ + key::Key, translated_string::{LangKey, TranslatedString}, }; diff --git a/backend/src/rss.rs b/backend/src/rss.rs index 2ce02806f..0c8f51397 100644 --- a/backend/src/rss.rs +++ b/backend/src/rss.rs @@ -6,8 +6,9 @@ use futures::TryStreamExt; use ogrim::xml; use crate::{ - db::{types::{EventTrack, Key}, self, util::{impl_from_db, FromDb, dbargs}}, + db::{types::EventTrack, self, util::{impl_from_db, FromDb, dbargs}}, http::{Context, response::{bad_request, self, not_found}, Response}, + model::Key, config::HttpHost, prelude::*, }; diff --git a/backend/src/search/event.rs b/backend/src/search/event.rs index 8fa8e6400..13859ed7b 100644 --- a/backend/src/search/event.rs +++ b/backend/src/search/event.rs @@ -9,8 +9,11 @@ use tokio_postgres::GenericClient; use crate::{ api::model::search::{ByteSpan, TextMatch}, - db::{types::{Key, TextAssetType, TimespanText}, - util::{collect_rows_mapped, impl_from_db}}, + db::{ + types::{TextAssetType, TimespanText}, + util::{collect_rows_mapped, impl_from_db} + }, + model::Key, prelude::*, util::{base64_decode, BASE64_DIGITS}, }; diff --git a/backend/src/search/mod.rs b/backend/src/search/mod.rs index c42f33d43..ad75f15d2 100644 --- a/backend/src/search/mod.rs +++ b/backend/src/search/mod.rs @@ -12,7 +12,7 @@ use secrecy::{SecretString, ExposeSecret}; use serde::{Deserialize, Serialize}; use crate::{ - db::types::Key, + model::Key, prelude::*, config::HttpHost, }; diff --git a/backend/src/search/playlist.rs b/backend/src/search/playlist.rs index 62b276f79..30ff9d344 100644 --- a/backend/src/search/playlist.rs +++ b/backend/src/search/playlist.rs @@ -5,7 +5,8 @@ use tokio_postgres::GenericClient; use crate::{ prelude::*, - db::{types::Key, util::{collect_rows_mapped, impl_from_db}}, + model::Key, + db::util::{collect_rows_mapped, impl_from_db}, }; use super::{realm::Realm, util::{self, FieldAbilities}, IndexItem, IndexItemKind, SearchId}; diff --git a/backend/src/search/realm.rs b/backend/src/search/realm.rs index d238cfd22..6efc82374 100644 --- a/backend/src/search/realm.rs +++ b/backend/src/search/realm.rs @@ -3,7 +3,7 @@ use postgres_types::FromSql; use serde::{Serialize, Deserialize}; use tokio_postgres::GenericClient; -use crate::{prelude::*, db::{types::Key, util::{collect_rows_mapped, impl_from_db}}}; +use crate::{prelude::*, model::Key, db::util::{collect_rows_mapped, impl_from_db}}; use super::{util::{self, FieldAbilities}, IndexItem, IndexItemKind, SearchId}; diff --git a/backend/src/search/series.rs b/backend/src/search/series.rs index f2acdce0c..b26bc15d5 100644 --- a/backend/src/search/series.rs +++ b/backend/src/search/series.rs @@ -5,7 +5,8 @@ use tokio_postgres::GenericClient; use crate::{ prelude::*, - db::{types::{Key, SearchThumbnailInfo}, util::{collect_rows_mapped, impl_from_db}}, + model::Key, + db::{types::SearchThumbnailInfo, util::{collect_rows_mapped, impl_from_db}}, }; use super::{realm::Realm, util::{self, FieldAbilities}, IndexItem, IndexItemKind, SearchId}; diff --git a/backend/src/search/update.rs b/backend/src/search/update.rs index 035d82466..6a18cd4a2 100644 --- a/backend/src/search/update.rs +++ b/backend/src/search/update.rs @@ -5,7 +5,8 @@ use std::{ }; use crate::{ - db::{DbConnection, types::Key, util::select}, + db::{DbConnection, util::select}, + model::Key, prelude::*, util::Never, }; diff --git a/backend/src/search/user.rs b/backend/src/search/user.rs index a34f87ff7..5a974a5f9 100644 --- a/backend/src/search/user.rs +++ b/backend/src/search/user.rs @@ -4,7 +4,8 @@ use tokio_postgres::GenericClient; use crate::{ prelude::*, - db::{types::Key, util::{collect_rows_mapped, impl_from_db}}, + db::util::{collect_rows_mapped, impl_from_db}, + model::Key, }; use super::{util::{self, FieldAbilities}, IndexItem, IndexItemKind, SearchId}; diff --git a/backend/src/sync/text/mod.rs b/backend/src/sync/text/mod.rs index 8c3fd2842..9a54746a9 100644 --- a/backend/src/sync/text/mod.rs +++ b/backend/src/sync/text/mod.rs @@ -10,10 +10,11 @@ use crate::{ config::Config, db::{ self, - types::{EventCaption, EventTextsQueueRecord, Key, TextAssetType, TimespanText}, + types::{EventCaption, EventTextsQueueRecord, TextAssetType, TimespanText}, util::{collect_rows_mapped, select}, DbConnection, }, + model::Key, dbargs, prelude::*, }; From af600ea62bb82e24e9262c32a59b31b0e8befecf Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Thu, 16 Jan 2025 15:59:25 +0100 Subject: [PATCH 3/3] Move `ExtraMetadata` from `db::types` to `model` --- backend/src/api/common.rs | 44 ------------- backend/src/api/model/event.rs | 4 +- backend/src/api/model/series.rs | 4 +- backend/src/db/types.rs | 51 --------------- backend/src/model/extra_metadata.rs | 98 ++++++++++++++++++++++++++++ backend/src/model/mod.rs | 2 + backend/src/sync/harvest/response.rs | 5 +- 7 files changed, 108 insertions(+), 100 deletions(-) create mode 100644 backend/src/model/extra_metadata.rs diff --git a/backend/src/api/common.rs b/backend/src/api/common.rs index 7c55a5b83..570898ba9 100644 --- a/backend/src/api/common.rs +++ b/backend/src/api/common.rs @@ -17,7 +17,6 @@ use crate::{ }, prelude::*, search::Playlist as SearchPlaylist, - db::types::ExtraMetadata, }; @@ -100,46 +99,3 @@ impl Cursor { Ok(Self(s.into())) } } - -// TODO: This uses `graphql_scalar` instead of `derive(GraphQLScalar)` because -// the type `ExtraMetadata` is defined in the `db` module and adding GraphQL -// code there seems wrong. However, I feel like we should move some types -// around anyway since we encountered problems like this before. -#[juniper::graphql_scalar( - name = "ExtraMetadata", - description = "Arbitrary metadata for events/series. Serialized as JSON object.", - with = Self, - parse_token(String), -)] -#[allow(dead_code)] -pub type ApiExtraMetadata = ExtraMetadata; - -impl ExtraMetadata { - fn to_output(&self) -> juniper::Value { - use juniper::Value; - - std::iter::once(("dcterms", &self.dcterms)) - .chain(self.extended.iter().map(|(k, v)| (&**k, v))) - .map(|(k, v)| { - let value = v.iter() - .map(|(k, v)| { - let elements = v.iter() - .map(|s| Value::Scalar(S::from(s.clone()))) - .collect(); - (k, Value::List(elements)) - }) - .collect::>(); - - (k, Value::Object(value)) - }) - .collect::>() - .pipe(Value::Object) - } - - fn from_input(input: &InputValue) -> Result { - // I did not want to waste time implementing this now, given that we - // likely never use it. - let _ = input; - todo!("ExtraMetadata cannot be used as input value yet") - } -} diff --git a/backend/src/api/model/event.rs b/backend/src/api/model/event.rs index 2b16b5854..f6e21fd2a 100644 --- a/backend/src/api/model/event.rs +++ b/backend/src/api/model/event.rs @@ -24,10 +24,10 @@ use crate::{ }, }, db::{ - types::{EventCaption, EventSegment, EventState, EventTrack, ExtraMetadata, Credentials}, + types::{EventCaption, EventSegment, EventState, EventTrack, Credentials}, util::{impl_from_db, select}, }, - model::Key, + model::{Key, ExtraMetadata}, prelude::*, }; diff --git a/backend/src/api/model/series.rs b/backend/src/api/model/series.rs index e5294797a..ab6e5b495 100644 --- a/backend/src/api/model/series.rs +++ b/backend/src/api/model/series.rs @@ -8,8 +8,8 @@ use crate::{ err::{invalid_input, ApiResult}, model::{event::AuthorizedEvent, realm::Realm}, }, - db::{types::{ExtraMetadata, SeriesState as State}, util::impl_from_db}, - model::Key, + db::{types::{SeriesState as State}, util::impl_from_db}, + model::{Key, ExtraMetadata}, prelude::*, }; diff --git a/backend/src/db/types.rs b/backend/src/db/types.rs index dcb57102a..961c3a70d 100644 --- a/backend/src/db/types.rs +++ b/backend/src/db/types.rs @@ -112,57 +112,6 @@ pub enum TextAssetType { } -/// Represents extra metadata in the DB. Is a map from "namespace" to a -/// `string -> string array` map. -/// -/// Each namespace key is a URL pointing to an XML namespace definition OR -/// `"dcterms"` for the dc terms (most common namespace). The value for each -/// namespace is a simple string-key map where each value is an array of string -/// values. -#[derive(Debug, Serialize, Deserialize, Default)] -#[cfg_attr(test, derive(PartialEq, Eq))] -pub(crate) struct ExtraMetadata { - /// Metadata of the dcterms - #[serde(default)] - pub(crate) dcterms: MetadataMap, - - /// Extended metadata. - #[serde(flatten)] - pub(crate) extended: HashMap, -} - -type MetadataMap = HashMap>; - -impl ToSql for ExtraMetadata { - fn to_sql( - &self, - ty: &postgres_types::Type, - out: &mut BytesMut, - ) -> Result> { - serde_json::to_value(self) - .expect("failed to convert `ExtraMetadata` to JSON value") - .to_sql(ty, out) - } - - fn accepts(ty: &postgres_types::Type) -> bool { - ::accepts(ty) - } - - postgres_types::to_sql_checked!(); -} - -impl<'a> FromSql<'a> for ExtraMetadata { - fn from_sql( - ty: &postgres_types::Type, - raw: &'a [u8], - ) -> Result> { - serde_json::from_value(<_>::from_sql(ty, raw)?).map_err(Into::into) - } - - fn accepts(ty: &postgres_types::Type) -> bool { - ::accepts(ty) - } -} /// Represents the type for the `custom_action_roles` field from `32-custom-actions.sql`. /// This holds a mapping of actions to lists holding roles that are allowed diff --git a/backend/src/model/extra_metadata.rs b/backend/src/model/extra_metadata.rs new file mode 100644 index 000000000..a851218e8 --- /dev/null +++ b/backend/src/model/extra_metadata.rs @@ -0,0 +1,98 @@ +use std::collections::HashMap; + +use bytes::BytesMut; +use postgres_types::{FromSql, ToSql}; +use serde::{Deserialize, Serialize}; +use juniper::{GraphQLScalar, InputValue, ScalarValue}; + +use crate::prelude::*; + + + +/// Represents extra metadata in the DB. Is a map from "namespace" to a +/// `string -> string array` map. +/// +/// Each namespace key is a URL pointing to an XML namespace definition OR +/// `"dcterms"` for the dc terms (most common namespace). The value for each +/// namespace is a simple string-key map where each value is an array of string +/// values. +#[derive(Debug, Serialize, Deserialize, Default, GraphQLScalar)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[graphql( + description = "Arbitrary metadata for events/series. Serialized as JSON object.", + with = Self, + parse_token(String), +)] +pub(crate) struct ExtraMetadata { + /// Metadata of the dcterms + #[serde(default)] + pub(crate) dcterms: MetadataMap, + + /// Extended metadata. + #[serde(flatten)] + pub(crate) extended: HashMap, +} + +type MetadataMap = HashMap>; + +impl ToSql for ExtraMetadata { + fn to_sql( + &self, + ty: &postgres_types::Type, + out: &mut BytesMut, + ) -> Result> { + serde_json::to_value(self) + .expect("failed to convert `ExtraMetadata` to JSON value") + .to_sql(ty, out) + } + + fn accepts(ty: &postgres_types::Type) -> bool { + ::accepts(ty) + } + + postgres_types::to_sql_checked!(); +} + +impl<'a> FromSql<'a> for ExtraMetadata { + fn from_sql( + ty: &postgres_types::Type, + raw: &'a [u8], + ) -> Result> { + serde_json::from_value(<_>::from_sql(ty, raw)?).map_err(Into::into) + } + + fn accepts(ty: &postgres_types::Type) -> bool { + ::accepts(ty) + } +} + +impl ExtraMetadata { + fn to_output(&self) -> juniper::Value { + use juniper::Value; + + std::iter::once(("dcterms", &self.dcterms)) + .chain(self.extended.iter().map(|(k, v)| (&**k, v))) + .map(|(k, v)| { + let value = v.iter() + .map(|(k, v)| { + let elements = v.iter() + .map(|s| Value::Scalar(S::from(s.clone()))) + .collect(); + (k, Value::List(elements)) + }) + .collect::>(); + + (k, Value::Object(value)) + }) + .collect::>() + .pipe(Value::Object) + } + + fn from_input(input: &InputValue) -> Result { + // I did not want to waste time implementing this now, given that we + // likely never use it. + let _ = input; + todo!("ExtraMetadata cannot be used as input value yet") + } +} + diff --git a/backend/src/model/mod.rs b/backend/src/model/mod.rs index cf9df96ac..eaa0ce23f 100644 --- a/backend/src/model/mod.rs +++ b/backend/src/model/mod.rs @@ -7,10 +7,12 @@ //! either of `db`, `api` or any other submodule as they are used in multiple //! situations (loading from DB, exposing via API, ...). +mod extra_metadata; mod key; mod translated_string; pub(crate) use self::{ + extra_metadata::ExtraMetadata, key::Key, translated_string::{LangKey, TranslatedString}, }; diff --git a/backend/src/sync/harvest/response.rs b/backend/src/sync/harvest/response.rs index 1de2b4726..90fe709c3 100644 --- a/backend/src/sync/harvest/response.rs +++ b/backend/src/sync/harvest/response.rs @@ -1,7 +1,10 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use crate::db::types::{CustomActions, EventCaption, EventTrack, EventSegment, ExtraMetadata}; +use crate::{ + db::types::{CustomActions, EventCaption, EventTrack, EventSegment}, + model::ExtraMetadata, +}; /// What the harvesting API returns.