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

@@ -2,7 +2,7 @@ use std::sync::Arc;
use domain::entry::MoodEntry;
use domain::events::{DomainEvent, EventEnvelope};
use domain::ports::{EventPublisherPort, MoodEntryCommandPort};
use domain::ports::{EntryDimensionPort, EventPublisherPort, MoodEntryCommandPort};
use crate::errors::ApplicationError;
@@ -10,19 +10,18 @@ use super::super::commands::CreateEntryCommand;
pub struct Deps {
pub entries: Arc<dyn MoodEntryCommandPort>,
pub dimensions: Vec<Arc<dyn EntryDimensionPort>>,
pub events: Arc<dyn EventPublisherPort>,
}
#[tracing::instrument(skip(deps))]
pub async fn execute(cmd: CreateEntryCommand, deps: &Deps) -> Result<MoodEntry, ApplicationError> {
let mut entry = MoodEntry::new(cmd.user_id, cmd.mood, cmd.logged_at);
entry.set_content(cmd.content);
entry.set_activities(cmd.activities);
entry.set_photos(cmd.photos);
entry.set_voice_memos(cmd.voice_memos);
let entry = MoodEntry::new(cmd.user_id, cmd.mood, cmd.logged_at);
deps.entries.save(&entry).await?;
for port in &deps.dimensions {
port.save(entry.id(), &cmd.dimensions).await?;
}
let event = DomainEvent::EntryCreated {
entry_id: entry.id().clone(),

View File

@@ -1,13 +1,18 @@
use std::sync::Arc;
use domain::dimension::ComposedEntry;
use domain::entry::DateRange;
use domain::ports::{CascadeDeletePort, MediaStoragePort};
use domain::ports::{CascadeDeletePort, EntryDimensionPort, MediaStoragePort, MoodEntryQueryPort};
use domain::user::UserId;
use crate::entry::composition::EntryComposer;
use crate::errors::ApplicationError;
use crate::media::cleanup::delete_media_for;
pub struct Deps {
pub cascade: Arc<dyn CascadeDeletePort>,
pub query: Arc<dyn MoodEntryQueryPort>,
pub dimensions: Vec<Arc<dyn EntryDimensionPort>>,
pub media_storage: Arc<dyn MediaStoragePort>,
}
@@ -17,23 +22,26 @@ pub async fn execute(
range: &DateRange,
deps: &Deps,
) -> Result<u64, ApplicationError> {
let entries = deps
let media = media_of_entries_about_to_be_deleted(&user_id, range, deps).await?;
let deleted = deps
.cascade
.delete_entries_in_range(&user_id, range)
.await?;
for entry in &entries {
for photo_id in entry.photos() {
if let Err(e) = deps.media_storage.delete_photo(photo_id).await {
tracing::warn!(%e, "failed to delete photo blob");
}
}
for memo_id in entry.voice_memos() {
if let Err(e) = deps.media_storage.delete_voice_memo(memo_id).await {
tracing::warn!(%e, "failed to delete voice memo blob");
}
}
}
delete_media_for(&media, &deps.media_storage).await;
Ok(entries.len() as u64)
Ok(deleted.len() as u64)
}
async fn media_of_entries_about_to_be_deleted(
user_id: &UserId,
range: &DateRange,
deps: &Deps,
) -> Result<Vec<ComposedEntry>, ApplicationError> {
let doomed = deps.query.find_by_date_range(user_id, range).await?;
EntryComposer::new(deps.dimensions.clone())
.compose(doomed)
.await
}

View File

@@ -4,16 +4,20 @@ use domain::entry::MoodEntryId;
use domain::errors::DomainError;
use domain::events::{DomainEvent, EventEnvelope};
use domain::ports::{
EventPublisherPort, MediaStoragePort, MoodEntryCommandPort, MoodEntryQueryPort,
EntryDimensionPort, EventPublisherPort, MediaStoragePort, MoodEntryCommandPort,
MoodEntryQueryPort,
};
use domain::user::UserId;
use crate::authorization::verify_ownership;
use crate::entry::composition::EntryComposer;
use crate::errors::ApplicationError;
use crate::media::cleanup::delete_media_for;
pub struct Deps {
pub command: Arc<dyn MoodEntryCommandPort>,
pub query: Arc<dyn MoodEntryQueryPort>,
pub dimensions: Vec<Arc<dyn EntryDimensionPort>>,
pub events: Arc<dyn EventPublisherPort>,
pub media_storage: Arc<dyn MediaStoragePort>,
}
@@ -32,23 +36,15 @@ pub async fn execute(
verify_ownership(entry.user_id(), &caller_id)?;
for photo_id in entry.photos() {
if let Err(e) = deps.media_storage.delete_photo(photo_id).await {
tracing::warn!(%e, "failed to delete photo blob");
}
}
for memo_id in entry.voice_memos() {
if let Err(e) = deps.media_storage.delete_voice_memo(memo_id).await {
tracing::warn!(%e, "failed to delete voice memo blob");
}
}
let user_id = entry.user_id().clone();
let composed = EntryComposer::new(deps.dimensions.clone())
.compose(vec![entry])
.await?;
delete_media_for(&composed, &deps.media_storage).await;
deps.command.delete(&entry_id).await?;
let event = DomainEvent::EntryDeleted {
entry_id,
user_id: entry.user_id().clone(),
};
let event = DomainEvent::EntryDeleted { entry_id, user_id };
deps.events.publish(EventEnvelope::wrap(event)).await?;
Ok(())

View File

@@ -1,31 +0,0 @@
use std::sync::Arc;
use domain::activity::ActivityId;
use domain::entry::DateRange;
use domain::ports::MoodEntryQueryPort;
use domain::services::MoodAnalyzerService;
use domain::user::UserId;
use crate::errors::ApplicationError;
pub struct Deps {
pub query: Arc<dyn MoodEntryQueryPort>,
}
#[tracing::instrument(skip(deps))]
pub async fn execute(
user_id: UserId,
activity_id: ActivityId,
range: Option<DateRange>,
deps: &Deps,
) -> Result<Option<f64>, ApplicationError> {
let entries = match range {
Some(range) => deps.query.find_by_date_range(&user_id, &range).await?,
None => deps.query.find_by_user(&user_id, None, None).await?,
};
Ok(MoodAnalyzerService::activity_mood_correlation(
&entries,
&activity_id,
))
}

View File

@@ -1,22 +1,34 @@
use std::collections::BTreeMap;
use std::sync::Arc;
use chrono::NaiveDate;
use domain::entry::{DateRange, Mood, MoodEntry};
use domain::ports::MoodEntryQueryPort;
use domain::cycle::CycleDay;
use domain::dimension::ComposedEntry;
use domain::entry::{Date, DateRange, DayMood};
use domain::ports::{
CycleStartQueryPort, EntryDimensionPort, MoodEntryQueryPort, UserPreferencesQueryPort,
UserQueryPort,
};
use domain::user::UserId;
use crate::cycle::use_cases::read_cycle::calendar_of;
use crate::day::{date_of, timezone_for};
use crate::entry::composition::EntryComposer;
use crate::errors::ApplicationError;
use crate::user::preferences::preferences_of;
pub struct CalendarDay {
pub date: NaiveDate,
pub entries: Vec<MoodEntry>,
pub dominant_mood: Option<Mood>,
pub date: Date,
pub entries: Vec<ComposedEntry>,
pub day_mood: Option<DayMood>,
pub cycle_day: Option<CycleDay>,
}
pub struct Deps {
pub query: Arc<dyn MoodEntryQueryPort>,
pub dimensions: Vec<Arc<dyn EntryDimensionPort>>,
pub cycles: Arc<dyn CycleStartQueryPort>,
pub preferences: Arc<dyn UserPreferencesQueryPort>,
pub users: Arc<dyn UserQueryPort>,
}
#[tracing::instrument(skip(deps))]
@@ -26,21 +38,34 @@ pub async fn execute(
deps: &Deps,
) -> Result<Vec<CalendarDay>, ApplicationError> {
let entries = deps.query.find_by_date_range(&user_id, &range).await?;
let entries = EntryComposer::new(deps.dimensions.clone())
.compose(entries)
.await?;
let mut by_date: BTreeMap<NaiveDate, Vec<MoodEntry>> = BTreeMap::new();
let timezone = timezone_for(&user_id, &deps.users).await?;
let mut by_date: BTreeMap<Date, Vec<ComposedEntry>> = BTreeMap::new();
for entry in entries {
let date = entry.logged_at().date_naive();
let date = date_of(&entry.entry, &timezone);
by_date.entry(date).or_default().push(entry);
}
let cycles = tracked_cycle(&user_id, deps).await?;
let days = by_date
.into_iter()
.map(|(date, entries)| {
let dominant_mood = find_dominant_mood(&entries);
let day_mood = day_mood_of(&entries);
let cycle_day = cycles
.as_ref()
.and_then(|calendar| calendar.position_on(&date))
.map(|position| position.day());
CalendarDay {
date,
entries,
dominant_mood,
day_mood,
cycle_day,
}
})
.collect();
@@ -48,21 +73,21 @@ pub async fn execute(
Ok(days)
}
fn find_dominant_mood(entries: &[MoodEntry]) -> Option<Mood> {
if entries.is_empty() {
return None;
async fn tracked_cycle(
user_id: &domain::user::UserId,
deps: &Deps,
) -> Result<Option<domain::cycle::CycleCalendar>, ApplicationError> {
let preferences = preferences_of(user_id, &deps.preferences).await?;
if !preferences.tracks_cycle() {
return Ok(None);
}
let mut counts = [0u32; 5];
for entry in entries {
counts[entry.mood().value() as usize - 1] += 1;
}
let max_index = counts
.iter()
.enumerate()
.max_by_key(|(_, count)| *count)
.map(|(i, _)| i)?;
Mood::try_from(max_index as u8 + 1).ok()
Ok(Some(calendar_of(user_id, &deps.cycles).await?))
}
fn day_mood_of(entries: &[ComposedEntry]) -> Option<DayMood> {
let moods: Vec<_> = entries.iter().map(|entry| entry.entry.mood()).collect();
DayMood::of(&moods)
}

View File

@@ -1,9 +1,10 @@
use std::sync::Arc;
use domain::entry::Mood;
use domain::ports::MoodEntryQueryPort;
use domain::ports::{MoodEntryQueryPort, UserQueryPort};
use domain::services::MoodAnalyzerService;
use crate::day::{date_of, timezone_for, today_in};
use crate::errors::ApplicationError;
use super::super::queries::MoodStatsQuery;
@@ -17,6 +18,7 @@ pub struct MoodStats {
pub struct Deps {
pub query: Arc<dyn MoodEntryQueryPort>,
pub users: Arc<dyn UserQueryPort>,
}
#[tracing::instrument(skip(deps))]
@@ -30,10 +32,16 @@ pub async fn execute(query: MoodStatsQuery, deps: &Deps) -> Result<MoodStats, Ap
None => deps.query.find_by_user(&query.user_id, None, None).await?,
};
let timezone = timezone_for(&query.user_id, &deps.users).await?;
let dates: Vec<_> = entries
.iter()
.map(|entry| date_of(entry, &timezone))
.collect();
Ok(MoodStats {
average: MoodAnalyzerService::average_mood(&entries),
frequency: MoodAnalyzerService::mood_frequency(&entries),
current_streak: MoodAnalyzerService::current_streak(&entries),
current_streak: MoodAnalyzerService::current_streak(&dates, today_in(&timezone)),
total_entries: entries.len(),
})
}

View File

@@ -3,7 +3,6 @@ pub mod delete_entries_by_date_range;
pub mod delete_entry;
pub mod filter_by_activity;
pub mod filter_by_mood;
pub mod get_activity_correlation;
pub mod get_calendar;
pub mod get_entry;
pub mod get_mood_stats;

View File

@@ -1,20 +1,24 @@
use std::sync::Arc;
use domain::dimension::lookup;
use domain::entry::MoodEntry;
use domain::errors::DomainError;
use domain::events::{DomainEvent, EventEnvelope};
use domain::ports::{
EventPublisherPort, MediaStoragePort, MoodEntryCommandPort, MoodEntryQueryPort,
EntryDimensionPort, EventPublisherPort, MediaStoragePort, MoodEntryCommandPort,
MoodEntryQueryPort,
};
use domain::user::UserId;
use crate::authorization::verify_ownership;
use crate::entry::composition::EntryComposer;
use crate::errors::ApplicationError;
use super::super::commands::UpdateEntryCommand;
pub struct Deps {
pub command: Arc<dyn MoodEntryCommandPort>,
pub dimensions: Vec<Arc<dyn EntryDimensionPort>>,
pub query: Arc<dyn MoodEntryQueryPort>,
pub media_storage: Arc<dyn MediaStoragePort>,
pub events: Arc<dyn EventPublisherPort>,
@@ -34,25 +38,29 @@ pub async fn execute(
verify_ownership(entry.user_id(), &caller_id)?;
let old_photos = entry.photos().to_vec();
let old_memos = entry.voice_memos().to_vec();
let previous = EntryComposer::new(deps.dimensions.clone())
.compose(vec![entry.clone()])
.await?;
let old_photos = previous[0].photos().to_vec();
let old_memos = previous[0].voice_memos().to_vec();
entry.update_mood(cmd.mood);
entry.update_logged_at(cmd.logged_at);
entry.set_content(cmd.content);
entry.set_activities(cmd.activities);
entry.set_photos(cmd.photos);
entry.set_voice_memos(cmd.voice_memos);
if let Some(logged_at) = cmd.logged_at {
entry.update_logged_at(logged_at);
}
let new_photos = lookup::photos_in(&cmd.dimensions);
let new_memos = lookup::voice_memos_in(&cmd.dimensions);
for photo_id in &old_photos {
if !entry.photos().contains(photo_id)
if !new_photos.contains(photo_id)
&& let Err(e) = deps.media_storage.delete_photo(photo_id).await
{
tracing::warn!(%e, "failed to delete removed photo blob");
}
}
for memo_id in &old_memos {
if !entry.voice_memos().contains(memo_id)
if !new_memos.contains(memo_id)
&& let Err(e) = deps.media_storage.delete_voice_memo(memo_id).await
{
tracing::warn!(%e, "failed to delete removed voice memo blob");
@@ -60,6 +68,9 @@ pub async fn execute(
}
deps.command.save(&entry).await?;
for port in &deps.dimensions {
port.save(entry.id(), &cmd.dimensions).await?;
}
let event = DomainEvent::EntryUpdated {
entry_id: entry.id().clone(),