changes
All checks were successful
CI / ci (push) Successful in 19m38s

This commit is contained in:
2026-08-26 20:55:30 +02:00
parent a557c183e9
commit 23d052278a
523 changed files with 24448 additions and 2005 deletions

View File

@@ -0,0 +1,94 @@
use std::collections::HashMap;
use sqlx::SqlitePool;
use domain::dimension::{DimensionKind, DimensionValue};
use domain::entry::{Content, MoodEntryId};
use domain::errors::DomainError;
use super::super::shared::db_err;
pub struct SqliteContentDimensionRepository {
pool: SqlitePool,
}
impl SqliteContentDimensionRepository {
pub fn new(pool: SqlitePool) -> Self {
Self { pool }
}
}
#[derive(sqlx::FromRow)]
struct ContentRow {
entry_id: String,
content: String,
}
#[async_trait::async_trait]
impl domain::ports::EntryDimensionPort for SqliteContentDimensionRepository {
async fn load(
&self,
entry_ids: &[MoodEntryId],
) -> Result<HashMap<MoodEntryId, DimensionValue>, DomainError> {
if entry_ids.is_empty() {
return Ok(HashMap::new());
}
let placeholders = vec!["?"; entry_ids.len()].join(",");
let sql = format!(
"SELECT entry_id, content FROM entry_content WHERE entry_id IN ({placeholders})"
);
let mut query = sqlx::query_as::<_, ContentRow>(sqlx::AssertSqlSafe(sql));
for id in entry_ids {
query = query.bind(id.value().to_string());
}
let rows = query.fetch_all(&self.pool).await.map_err(db_err)?;
Ok(rows
.into_iter()
.filter_map(|row| {
let id = row.entry_id.parse().ok()?;
Some((
MoodEntryId::from_uuid(id),
DimensionValue::Content(Content::from_persistence(row.content)),
))
})
.collect())
}
async fn save(
&self,
entry_id: &MoodEntryId,
values: &[DimensionValue],
) -> Result<(), DomainError> {
let id = entry_id.value().to_string();
match values
.iter()
.find(|value| value.kind() == DimensionKind::Content)
{
Some(DimensionValue::Content(content)) => {
sqlx::query(
"INSERT INTO entry_content (entry_id, content) VALUES (?, ?)
ON CONFLICT(entry_id) DO UPDATE SET content = excluded.content",
)
.bind(&id)
.bind(content.value())
.execute(&self.pool)
.await
.map_err(db_err)?;
}
_ => {
sqlx::query("DELETE FROM entry_content WHERE entry_id = ?")
.bind(&id)
.execute(&self.pool)
.await
.map_err(db_err)?;
}
}
Ok(())
}
}

View File

@@ -0,0 +1,101 @@
use std::collections::HashMap;
use sqlx::SqlitePool;
use domain::dimension::{DimensionKind, DimensionValue};
use domain::entry::MoodEntryId;
use domain::errors::DomainError;
use domain::location::Coordinates;
use super::super::shared::db_err;
pub struct SqliteLocationDimensionRepository {
pool: SqlitePool,
}
impl SqliteLocationDimensionRepository {
pub fn new(pool: SqlitePool) -> Self {
Self { pool }
}
}
#[derive(sqlx::FromRow)]
struct LocationRow {
entry_id: String,
latitude: f64,
longitude: f64,
}
#[async_trait::async_trait]
impl domain::ports::EntryDimensionPort for SqliteLocationDimensionRepository {
async fn load(
&self,
entry_ids: &[MoodEntryId],
) -> Result<HashMap<MoodEntryId, DimensionValue>, DomainError> {
if entry_ids.is_empty() {
return Ok(HashMap::new());
}
let placeholders = vec!["?"; entry_ids.len()].join(",");
let sql = format!(
"SELECT entry_id, latitude, longitude FROM entry_location WHERE entry_id IN ({placeholders})"
);
let mut query = sqlx::query_as::<_, LocationRow>(sqlx::AssertSqlSafe(sql));
for id in entry_ids {
query = query.bind(id.value().to_string());
}
let rows = query.fetch_all(&self.pool).await.map_err(db_err)?;
Ok(rows
.into_iter()
.filter_map(|row| {
let entry_id = row.entry_id.parse().ok()?;
Some((
MoodEntryId::from_uuid(entry_id),
DimensionValue::Location(Coordinates::from_persistence(
row.latitude,
row.longitude,
)),
))
})
.collect())
}
async fn save(
&self,
entry_id: &MoodEntryId,
values: &[DimensionValue],
) -> Result<(), DomainError> {
let id = entry_id.value().to_string();
match values
.iter()
.find(|value| value.kind() == DimensionKind::Location)
{
Some(DimensionValue::Location(coordinates)) => {
sqlx::query(
"INSERT INTO entry_location (entry_id, latitude, longitude) VALUES (?, ?, ?)
ON CONFLICT(entry_id) DO UPDATE SET
latitude = excluded.latitude, longitude = excluded.longitude",
)
.bind(&id)
.bind(coordinates.latitude().value())
.bind(coordinates.longitude().value())
.execute(&self.pool)
.await
.map_err(db_err)?;
}
_ => {
sqlx::query("DELETE FROM entry_location WHERE entry_id = ?")
.bind(&id)
.execute(&self.pool)
.await
.map_err(db_err)?;
}
}
Ok(())
}
}

View File

@@ -0,0 +1,11 @@
mod content;
mod location;
mod relation;
mod song;
mod weather;
pub use content::SqliteContentDimensionRepository;
pub use location::SqliteLocationDimensionRepository;
pub use relation::SqliteRelationDimensionRepository;
pub use song::SqliteSongDimensionRepository;
pub use weather::SqliteWeatherDimensionRepository;

View File

@@ -0,0 +1,166 @@
use std::collections::HashMap;
use sqlx::SqlitePool;
use uuid::Uuid;
use domain::activity::ActivityId;
use domain::attachment::{PhotoId, VoiceMemoId};
use domain::dimension::{DimensionKind, DimensionValue};
use domain::entry::MoodEntryId;
use domain::errors::DomainError;
use super::super::shared::db_err;
pub struct SqliteRelationDimensionRepository {
pool: SqlitePool,
table: &'static str,
column: &'static str,
kind: DimensionKind,
}
impl SqliteRelationDimensionRepository {
pub fn activities(pool: SqlitePool) -> Self {
Self {
pool,
table: "entry_activities",
column: "activity_id",
kind: DimensionKind::Activities,
}
}
pub fn photos(pool: SqlitePool) -> Self {
Self {
pool,
table: "entry_photos",
column: "photo_id",
kind: DimensionKind::Photos,
}
}
pub fn voice_memos(pool: SqlitePool) -> Self {
Self {
pool,
table: "entry_voice_memos",
column: "voice_memo_id",
kind: DimensionKind::VoiceMemos,
}
}
fn to_value(&self, ids: Vec<Uuid>) -> DimensionValue {
match self.kind {
DimensionKind::Activities => {
DimensionValue::Activities(ids.into_iter().map(ActivityId::from_uuid).collect())
}
DimensionKind::Photos => {
DimensionValue::Photos(ids.into_iter().map(PhotoId::from_uuid).collect())
}
DimensionKind::VoiceMemos => {
DimensionValue::VoiceMemos(ids.into_iter().map(VoiceMemoId::from_uuid).collect())
}
DimensionKind::Content
| DimensionKind::Location
| DimensionKind::Song
| DimensionKind::Weather => {
unreachable!("relation repository serves only id-list dimensions")
}
}
}
}
fn related_ids(value: &DimensionValue) -> Vec<String> {
match value {
DimensionValue::Activities(ids) => ids.iter().map(|id| id.value().to_string()).collect(),
DimensionValue::Photos(ids) => ids.iter().map(|id| id.value().to_string()).collect(),
DimensionValue::VoiceMemos(ids) => ids.iter().map(|id| id.value().to_string()).collect(),
DimensionValue::Content(_)
| DimensionValue::Location(_)
| DimensionValue::Song(_)
| DimensionValue::Weather(_) => Vec::new(),
}
}
#[derive(sqlx::FromRow)]
struct RelationRow {
entry_id: String,
related_id: String,
}
#[async_trait::async_trait]
impl domain::ports::EntryDimensionPort for SqliteRelationDimensionRepository {
async fn load(
&self,
entry_ids: &[MoodEntryId],
) -> Result<HashMap<MoodEntryId, DimensionValue>, DomainError> {
if entry_ids.is_empty() {
return Ok(HashMap::new());
}
let placeholders = vec!["?"; entry_ids.len()].join(",");
let sql = format!(
"SELECT entry_id, {} AS related_id FROM {} WHERE entry_id IN ({placeholders})",
self.column, self.table
);
let mut query = sqlx::query_as::<_, RelationRow>(sqlx::AssertSqlSafe(sql));
for id in entry_ids {
query = query.bind(id.value().to_string());
}
let rows = query.fetch_all(&self.pool).await.map_err(db_err)?;
let mut grouped: HashMap<MoodEntryId, Vec<Uuid>> = HashMap::new();
for row in rows {
let (Ok(entry_id), Ok(related_id)) =
(row.entry_id.parse::<Uuid>(), row.related_id.parse::<Uuid>())
else {
continue;
};
grouped
.entry(MoodEntryId::from_uuid(entry_id))
.or_default()
.push(related_id);
}
Ok(grouped
.into_iter()
.map(|(entry_id, ids)| (entry_id, self.to_value(ids)))
.collect())
}
async fn save(
&self,
entry_id: &MoodEntryId,
values: &[DimensionValue],
) -> Result<(), DomainError> {
let id = entry_id.value().to_string();
let mut tx = self.pool.begin().await.map_err(db_err)?;
sqlx::query(sqlx::AssertSqlSafe(format!(
"DELETE FROM {} WHERE entry_id = ?",
self.table
)))
.bind(&id)
.execute(&mut *tx)
.await
.map_err(db_err)?;
if let Some(value) = values.iter().find(|value| value.kind() == self.kind) {
let insert = format!(
"INSERT OR IGNORE INTO {} (entry_id, {}) VALUES (?, ?)",
self.table, self.column
);
for related in related_ids(value) {
sqlx::query(sqlx::AssertSqlSafe(insert.clone()))
.bind(&id)
.bind(related)
.execute(&mut *tx)
.await
.map_err(db_err)?;
}
}
tx.commit().await.map_err(db_err)?;
Ok(())
}
}

View File

@@ -0,0 +1,109 @@
use std::collections::HashMap;
use sqlx::SqlitePool;
use domain::dimension::{DimensionKind, DimensionValue};
use domain::entry::MoodEntryId;
use domain::errors::DomainError;
use domain::song::{AlbumName, ArtistName, RecordingId, Song, SongTitle};
use super::super::shared::db_err;
pub struct SqliteSongDimensionRepository {
pool: SqlitePool,
}
impl SqliteSongDimensionRepository {
pub fn new(pool: SqlitePool) -> Self {
Self { pool }
}
}
#[derive(sqlx::FromRow)]
struct SongRow {
entry_id: String,
title: String,
artist: String,
album: Option<String>,
recording_id: Option<String>,
}
#[async_trait::async_trait]
impl domain::ports::EntryDimensionPort for SqliteSongDimensionRepository {
async fn load(
&self,
entry_ids: &[MoodEntryId],
) -> Result<HashMap<MoodEntryId, DimensionValue>, DomainError> {
if entry_ids.is_empty() {
return Ok(HashMap::new());
}
let placeholders = vec!["?"; entry_ids.len()].join(",");
let sql = format!(
"SELECT entry_id, title, artist, album, recording_id FROM entry_song WHERE entry_id IN ({placeholders})"
);
let mut query = sqlx::query_as::<_, SongRow>(sqlx::AssertSqlSafe(sql));
for id in entry_ids {
query = query.bind(id.value().to_string());
}
let rows = query.fetch_all(&self.pool).await.map_err(db_err)?;
Ok(rows
.into_iter()
.filter_map(|row| {
let entry_id = row.entry_id.parse().ok()?;
let song = Song::from_persistence(
SongTitle::from_persistence(row.title),
ArtistName::from_persistence(row.artist),
row.album.map(AlbumName::from_persistence),
row.recording_id
.and_then(|id| id.parse().ok())
.map(RecordingId::from_uuid),
);
Some((MoodEntryId::from_uuid(entry_id), DimensionValue::Song(song)))
})
.collect())
}
async fn save(
&self,
entry_id: &MoodEntryId,
values: &[DimensionValue],
) -> Result<(), DomainError> {
let id = entry_id.value().to_string();
match values
.iter()
.find(|value| value.kind() == DimensionKind::Song)
{
Some(DimensionValue::Song(song)) => {
sqlx::query(
"INSERT INTO entry_song (entry_id, title, artist, album, recording_id)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(entry_id) DO UPDATE SET
title = excluded.title, artist = excluded.artist,
album = excluded.album, recording_id = excluded.recording_id",
)
.bind(&id)
.bind(song.title().value())
.bind(song.artist().value())
.bind(song.album().map(|album| album.value().to_string()))
.bind(song.recording_id().map(|id| id.value().to_string()))
.execute(&self.pool)
.await
.map_err(db_err)?;
}
_ => {
sqlx::query("DELETE FROM entry_song WHERE entry_id = ?")
.bind(&id)
.execute(&self.pool)
.await
.map_err(db_err)?;
}
}
Ok(())
}
}

View File

@@ -0,0 +1,109 @@
use std::collections::HashMap;
use sqlx::SqlitePool;
use domain::dimension::{DimensionKind, DimensionValue};
use domain::entry::MoodEntryId;
use domain::errors::DomainError;
use domain::provider::ProviderName;
use domain::weather::{Celsius, Condition, Weather};
use super::super::shared::db_err;
pub struct SqliteWeatherDimensionRepository {
pool: SqlitePool,
}
impl SqliteWeatherDimensionRepository {
pub fn new(pool: SqlitePool) -> Self {
Self { pool }
}
}
#[derive(sqlx::FromRow)]
struct WeatherRow {
entry_id: String,
condition: String,
temperature: f64,
observed_by: String,
}
#[async_trait::async_trait]
impl domain::ports::EntryDimensionPort for SqliteWeatherDimensionRepository {
async fn load(
&self,
entry_ids: &[MoodEntryId],
) -> Result<HashMap<MoodEntryId, DimensionValue>, DomainError> {
if entry_ids.is_empty() {
return Ok(HashMap::new());
}
let placeholders = vec!["?"; entry_ids.len()].join(",");
let sql = format!(
"SELECT entry_id, condition, temperature, observed_by
FROM entry_weather WHERE entry_id IN ({placeholders})"
);
let mut query = sqlx::query_as::<_, WeatherRow>(sqlx::AssertSqlSafe(sql));
for id in entry_ids {
query = query.bind(id.value().to_string());
}
let rows = query.fetch_all(&self.pool).await.map_err(db_err)?;
Ok(rows.iter().filter_map(readable).collect())
}
async fn save(
&self,
entry_id: &MoodEntryId,
values: &[DimensionValue],
) -> Result<(), DomainError> {
let observed = values
.iter()
.find(|value| value.kind() == DimensionKind::Weather);
let Some(DimensionValue::Weather(weather)) = observed else {
return Ok(());
};
sqlx::query(
"INSERT INTO entry_weather (entry_id, condition, temperature, observed_by)
VALUES (?, ?, ?, ?)
ON CONFLICT(entry_id) DO UPDATE SET
condition = excluded.condition,
temperature = excluded.temperature,
observed_by = excluded.observed_by",
)
.bind(entry_id.value().to_string())
.bind(weather.condition().name())
.bind(weather.temperature().value())
.bind(weather.observed_by().value())
.execute(&self.pool)
.await
.map_err(db_err)?;
Ok(())
}
}
fn readable(row: &WeatherRow) -> Option<(MoodEntryId, DimensionValue)> {
let entry_id = MoodEntryId::from_uuid(row.entry_id.parse().ok()?);
let condition = Condition::from_name(&row.condition);
if condition.is_none() {
tracing::warn!(
entry_id = %row.entry_id,
condition = %row.condition,
"skipped stored weather this build cannot read"
);
}
let weather = Weather::new(
condition?,
Celsius::from_persistence(row.temperature),
ProviderName::from_persistence(row.observed_by.clone()),
);
Some((entry_id, DimensionValue::Weather(weather)))
}