init
Some checks failed
CI / ci (push) Failing after 1m48s

This commit is contained in:
2026-08-25 23:24:36 +02:00
commit 95739892de
466 changed files with 33918 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
use std::sync::Arc;
use domain::entry::MoodEntry;
use domain::events::{DomainEvent, EventEnvelope};
use domain::ports::{EventPublisherPort, MoodEntryCommandPort};
use crate::errors::ApplicationError;
use super::super::commands::CreateEntryCommand;
pub struct Deps {
pub entries: Arc<dyn MoodEntryCommandPort>,
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);
deps.entries.save(&entry).await?;
let event = DomainEvent::EntryCreated {
entry_id: entry.id().clone(),
user_id: entry.user_id().clone(),
};
deps.events.publish(EventEnvelope::wrap(event)).await?;
Ok(entry)
}

View File

@@ -0,0 +1,39 @@
use std::sync::Arc;
use domain::entry::DateRange;
use domain::ports::{CascadeDeletePort, MediaStoragePort};
use domain::user::UserId;
use crate::errors::ApplicationError;
pub struct Deps {
pub cascade: Arc<dyn CascadeDeletePort>,
pub media_storage: Arc<dyn MediaStoragePort>,
}
#[tracing::instrument(skip(deps))]
pub async fn execute(
user_id: UserId,
range: &DateRange,
deps: &Deps,
) -> Result<u64, ApplicationError> {
let entries = 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");
}
}
}
Ok(entries.len() as u64)
}

View File

@@ -0,0 +1,55 @@
use std::sync::Arc;
use domain::entry::MoodEntryId;
use domain::errors::DomainError;
use domain::events::{DomainEvent, EventEnvelope};
use domain::ports::{
EventPublisherPort, MediaStoragePort, MoodEntryCommandPort, MoodEntryQueryPort,
};
use domain::user::UserId;
use crate::authorization::verify_ownership;
use crate::errors::ApplicationError;
pub struct Deps {
pub command: Arc<dyn MoodEntryCommandPort>,
pub query: Arc<dyn MoodEntryQueryPort>,
pub events: Arc<dyn EventPublisherPort>,
pub media_storage: Arc<dyn MediaStoragePort>,
}
#[tracing::instrument(skip(deps))]
pub async fn execute(
entry_id: MoodEntryId,
caller_id: UserId,
deps: &Deps,
) -> Result<(), ApplicationError> {
let entry = deps
.query
.find_by_id(&entry_id)
.await?
.ok_or_else(|| DomainError::NotFound("mood entry not found".into()))?;
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");
}
}
deps.command.delete(&entry_id).await?;
let event = DomainEvent::EntryDeleted {
entry_id,
user_id: entry.user_id().clone(),
};
deps.events.publish(EventEnvelope::wrap(event)).await?;
Ok(())
}

View File

@@ -0,0 +1,24 @@
use std::sync::Arc;
use domain::entry::MoodEntry;
use domain::ports::MoodEntryQueryPort;
use crate::errors::ApplicationError;
use super::super::queries::FilterByActivityQuery;
pub struct Deps {
pub query: Arc<dyn MoodEntryQueryPort>,
}
#[tracing::instrument(skip(deps))]
pub async fn execute(
query: FilterByActivityQuery,
deps: &Deps,
) -> Result<Vec<MoodEntry>, ApplicationError> {
let entries = deps
.query
.find_by_activity(&query.user_id, &query.activity_id)
.await?;
Ok(entries)
}

View File

@@ -0,0 +1,21 @@
use std::sync::Arc;
use domain::entry::MoodEntry;
use domain::ports::MoodEntryQueryPort;
use crate::errors::ApplicationError;
use super::super::queries::FilterByMoodQuery;
pub struct Deps {
pub query: Arc<dyn MoodEntryQueryPort>,
}
#[tracing::instrument(skip(deps))]
pub async fn execute(
query: FilterByMoodQuery,
deps: &Deps,
) -> Result<Vec<MoodEntry>, ApplicationError> {
let entries = deps.query.find_by_mood(&query.user_id, query.mood).await?;
Ok(entries)
}

View File

@@ -0,0 +1,31 @@
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

@@ -0,0 +1,68 @@
use std::collections::BTreeMap;
use std::sync::Arc;
use chrono::NaiveDate;
use domain::entry::{DateRange, Mood, MoodEntry};
use domain::ports::MoodEntryQueryPort;
use domain::user::UserId;
use crate::errors::ApplicationError;
pub struct CalendarDay {
pub date: NaiveDate,
pub entries: Vec<MoodEntry>,
pub dominant_mood: Option<Mood>,
}
pub struct Deps {
pub query: Arc<dyn MoodEntryQueryPort>,
}
#[tracing::instrument(skip(deps))]
pub async fn execute(
user_id: UserId,
range: DateRange,
deps: &Deps,
) -> Result<Vec<CalendarDay>, ApplicationError> {
let entries = deps.query.find_by_date_range(&user_id, &range).await?;
let mut by_date: BTreeMap<NaiveDate, Vec<MoodEntry>> = BTreeMap::new();
for entry in entries {
let date = entry.logged_at().date_naive();
by_date.entry(date).or_default().push(entry);
}
let days = by_date
.into_iter()
.map(|(date, entries)| {
let dominant_mood = find_dominant_mood(&entries);
CalendarDay {
date,
entries,
dominant_mood,
}
})
.collect();
Ok(days)
}
fn find_dominant_mood(entries: &[MoodEntry]) -> Option<Mood> {
if entries.is_empty() {
return 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()
}

View File

@@ -0,0 +1,29 @@
use std::sync::Arc;
use domain::entry::{MoodEntry, MoodEntryId};
use domain::errors::DomainError;
use domain::ports::MoodEntryQueryPort;
use domain::user::UserId;
use crate::authorization::verify_ownership;
use crate::errors::ApplicationError;
pub struct Deps {
pub query: Arc<dyn MoodEntryQueryPort>,
}
#[tracing::instrument(skip(deps))]
pub async fn execute(
entry_id: MoodEntryId,
caller_id: UserId,
deps: &Deps,
) -> Result<MoodEntry, ApplicationError> {
let entry = deps
.query
.find_by_id(&entry_id)
.await?
.ok_or_else(|| DomainError::NotFound("mood entry not found".into()))?;
verify_ownership(entry.user_id(), &caller_id)?;
Ok(entry)
}

View File

@@ -0,0 +1,39 @@
use std::sync::Arc;
use domain::entry::Mood;
use domain::ports::MoodEntryQueryPort;
use domain::services::MoodAnalyzerService;
use crate::errors::ApplicationError;
use super::super::queries::MoodStatsQuery;
pub struct MoodStats {
pub average: Option<f64>,
pub frequency: Vec<(Mood, usize)>,
pub current_streak: usize,
pub total_entries: usize,
}
pub struct Deps {
pub query: Arc<dyn MoodEntryQueryPort>,
}
#[tracing::instrument(skip(deps))]
pub async fn execute(query: MoodStatsQuery, deps: &Deps) -> Result<MoodStats, ApplicationError> {
let entries = match query.range {
Some(range) => {
deps.query
.find_by_date_range(&query.user_id, &range)
.await?
}
None => deps.query.find_by_user(&query.user_id, None, None).await?,
};
Ok(MoodStats {
average: MoodAnalyzerService::average_mood(&entries),
frequency: MoodAnalyzerService::mood_frequency(&entries),
current_streak: MoodAnalyzerService::current_streak(&entries),
total_entries: entries.len(),
})
}

View File

@@ -0,0 +1,33 @@
use std::sync::Arc;
use domain::entry::MoodEntry;
use domain::ports::MoodEntryQueryPort;
use crate::errors::ApplicationError;
use super::super::queries::ListEntriesQuery;
pub struct Deps {
pub query: Arc<dyn MoodEntryQueryPort>,
}
#[tracing::instrument(skip(deps))]
pub async fn execute(
query: ListEntriesQuery,
deps: &Deps,
) -> Result<Vec<MoodEntry>, ApplicationError> {
let entries = match query.range {
Some(range) => {
deps.query
.find_by_date_range(&query.user_id, &range)
.await?
}
None => {
deps.query
.find_by_user(&query.user_id, query.limit, query.offset)
.await?
}
};
Ok(entries)
}

View File

@@ -0,0 +1,12 @@
pub mod create_entry;
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;
pub mod list_entries;
pub mod replace_activity;
pub mod update_entry;

View File

@@ -0,0 +1,37 @@
use std::sync::Arc;
use domain::activity::ActivityId;
use domain::errors::DomainError;
use domain::ports::{ActivityQueryPort, MoodEntryCommandPort};
use domain::user::UserId;
use crate::authorization::verify_ownership;
use crate::errors::ApplicationError;
pub struct Deps {
pub entry_command: Arc<dyn MoodEntryCommandPort>,
pub activity_query: Arc<dyn ActivityQueryPort>,
}
#[tracing::instrument(skip(deps))]
pub async fn execute(
user_id: UserId,
old_activity_id: ActivityId,
new_activity_id: ActivityId,
deps: &Deps,
) -> Result<u64, ApplicationError> {
let new_activity = deps
.activity_query
.find_by_id(&new_activity_id)
.await?
.ok_or_else(|| DomainError::NotFound("target activity not found".into()))?;
verify_ownership(new_activity.user_id(), &user_id)?;
let updated = deps
.entry_command
.replace_activity(&user_id, &old_activity_id, &new_activity_id)
.await?;
Ok(updated)
}

View File

@@ -0,0 +1,71 @@
use std::sync::Arc;
use domain::entry::MoodEntry;
use domain::errors::DomainError;
use domain::events::{DomainEvent, EventEnvelope};
use domain::ports::{
EventPublisherPort, MediaStoragePort, MoodEntryCommandPort, MoodEntryQueryPort,
};
use domain::user::UserId;
use crate::authorization::verify_ownership;
use crate::errors::ApplicationError;
use super::super::commands::UpdateEntryCommand;
pub struct Deps {
pub command: Arc<dyn MoodEntryCommandPort>,
pub query: Arc<dyn MoodEntryQueryPort>,
pub media_storage: Arc<dyn MediaStoragePort>,
pub events: Arc<dyn EventPublisherPort>,
}
#[tracing::instrument(skip(deps))]
pub async fn execute(
cmd: UpdateEntryCommand,
caller_id: UserId,
deps: &Deps,
) -> Result<MoodEntry, ApplicationError> {
let mut entry = deps
.query
.find_by_id(&cmd.entry_id)
.await?
.ok_or_else(|| DomainError::NotFound("mood entry not found".into()))?;
verify_ownership(entry.user_id(), &caller_id)?;
let old_photos = entry.photos().to_vec();
let old_memos = entry.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);
for photo_id in &old_photos {
if !entry.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)
&& let Err(e) = deps.media_storage.delete_voice_memo(memo_id).await
{
tracing::warn!(%e, "failed to delete removed voice memo blob");
}
}
deps.command.save(&entry).await?;
let event = DomainEvent::EntryUpdated {
entry_id: entry.id().clone(),
user_id: entry.user_id().clone(),
};
deps.events.publish(EventEnvelope::wrap(event)).await?;
Ok(entry)
}