spa hardening, offline logging, rate limit fixes
server: - backup exporter, auth extractors, error shapes, CONTEXT (prior work) - spa assets served outside the rate limit via route_layer - requests_per_second went to per_second(), which takes an interval not a rate: 50 meant one request per 50s once burst was spent. now converted properly. 15/s, burst 60 spa fixes: - account delete cleared snake_case token keys that were never written - refresh interceptor could retry forever - date ranges used local day boundaries stamped +00:00 - "all" period trend plotted one page; calendar days fabricated mood 3 - chart grid invisible: hsl(var(--border)) against rgba tokens - blob url leak, orphaned media on failed save, devtools in prod bundle - pt-safe/safe-area-pb classes never existed spa features: - offline outbox: entries queue to IndexedDB, replay with backoff, only server refusals count against an entry - drafts persist, quick-log sheet, diary infinite scroll + filters - route error boundary, stale-chunk recovery, no service worker in dev a11y + perf: - mood picker is a radiogroup, activity picker keyboard-operable, text alternatives for colour/emoji, locale week start - dark glass over the bright photo: worst case 1.4:1 -> 4.9-9.6:1 - initial payload 1095->769kB raw, 306->230kB gzip; 38 unused components and 5 deps dropped; fonts 218->133kB 53 tests added (43 spa, 10 server)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
pub mod dimension;
|
||||
pub mod errors;
|
||||
pub mod mappers;
|
||||
pub mod params;
|
||||
pub mod requests;
|
||||
pub mod responses;
|
||||
|
||||
@@ -7,7 +7,12 @@ impl From<ApiToken> for ApiTokenResponse {
|
||||
Self {
|
||||
id: token.id().value().to_string(),
|
||||
name: token.name().value().to_string(),
|
||||
scope: token.scope().name().to_string(),
|
||||
scopes: token
|
||||
.scopes()
|
||||
.names()
|
||||
.iter()
|
||||
.map(|n| n.to_string())
|
||||
.collect(),
|
||||
created_at: *token.created_at(),
|
||||
last_used_at: token.last_used_at().copied(),
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ impl From<CalendarDay> for CalendarDayResponse {
|
||||
date: day.date.value(),
|
||||
day_mood: day.day_mood.map(|mood| mood.value()),
|
||||
mood: day.day_mood.map(|mood| mood.rounded().value()),
|
||||
mood_label: day.day_mood.map(|mood| format!("{:?}", mood.rounded())),
|
||||
mood_label: day.day_mood.map(|mood| mood.rounded().label().to_string()),
|
||||
cycle_day: day.cycle_day.map(|cycle| cycle.value()),
|
||||
entries: day.entries.into_iter().map(EntryResponse::from).collect(),
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
use chrono::Utc;
|
||||
|
||||
use config::EntryConfig;
|
||||
use domain::activity::ActivityId;
|
||||
use domain::dimension::{ComposedEntry, DimensionValue};
|
||||
use domain::entry::{DateRange, Mood, MoodEntryId};
|
||||
use domain::entry::{DateRange, EntrySelection, Mood, MoodEntryId, Pagination};
|
||||
use domain::user::UserId;
|
||||
|
||||
use application::entry::commands::{CreateEntryCommand, UpdateEntryCommand};
|
||||
use application::entry::queries::{
|
||||
FilterByActivityQuery, FilterByMoodQuery, ListEntriesQuery, MoodStatsQuery,
|
||||
};
|
||||
use application::entry::queries::{ListEntriesQuery, MoodStatsQuery};
|
||||
|
||||
use crate::dimension::DimensionPayload;
|
||||
use crate::errors::ApiValidationError;
|
||||
use crate::requests::{CreateEntryRequest, DateRangeParams, ListEntriesParams, UpdateEntryRequest};
|
||||
use crate::params::{DateRangeParams, ListEntriesParams};
|
||||
use crate::requests::{CreateEntryRequest, UpdateEntryRequest};
|
||||
use crate::responses::EntryResponse;
|
||||
|
||||
use super::shared::{parse_datetime, validate_content_length};
|
||||
@@ -45,9 +45,13 @@ impl UpdateEntryRequest {
|
||||
entry_id: MoodEntryId,
|
||||
config: &EntryConfig,
|
||||
) -> Result<UpdateEntryCommand, ApiValidationError> {
|
||||
let mood = Mood::try_from(self.mood)?;
|
||||
let mood = self.mood.map(Mood::try_from).transpose()?;
|
||||
let logged_at = self.logged_at.as_deref().map(parse_datetime).transpose()?;
|
||||
let dimensions = parse_dimensions(self.dimensions, config)?;
|
||||
|
||||
let dimensions = match self.dimensions {
|
||||
Some(named) => Some(parse_dimensions(Some(named), config)?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
Ok(UpdateEntryCommand {
|
||||
entry_id,
|
||||
@@ -59,7 +63,11 @@ impl UpdateEntryRequest {
|
||||
}
|
||||
|
||||
impl ListEntriesParams {
|
||||
pub fn into_query(self, user_id: UserId) -> Result<ListEntriesQuery, ApiValidationError> {
|
||||
pub fn into_query(
|
||||
self,
|
||||
user_id: UserId,
|
||||
config: &EntryConfig,
|
||||
) -> Result<ListEntriesQuery, ApiValidationError> {
|
||||
let range = match (self.from, self.to) {
|
||||
(Some(from), Some(to)) => Some(DateRange::new(
|
||||
parse_datetime(&from)?,
|
||||
@@ -67,15 +75,43 @@ impl ListEntriesParams {
|
||||
)?),
|
||||
_ => None,
|
||||
};
|
||||
Ok(ListEntriesQuery {
|
||||
user_id,
|
||||
range,
|
||||
limit: self.limit,
|
||||
offset: self.offset,
|
||||
})
|
||||
|
||||
let activity = self
|
||||
.activity
|
||||
.as_deref()
|
||||
.map(parse_activity_id)
|
||||
.transpose()?;
|
||||
|
||||
let updated_since = self
|
||||
.updated_since
|
||||
.as_deref()
|
||||
.map(parse_datetime)
|
||||
.transpose()?
|
||||
.map(|instant| instant.with_timezone(&Utc));
|
||||
|
||||
let selection = EntrySelection::everything_of(user_id)
|
||||
.logged_within(range)
|
||||
.of_mood(self.mood.map(Mood::try_from).transpose()?)
|
||||
.tagged_with(activity)
|
||||
.changed_since(updated_since);
|
||||
|
||||
let page = Pagination::new(
|
||||
self.limit.unwrap_or(config.max_entries_per_page),
|
||||
self.offset.unwrap_or(0),
|
||||
config.max_entries_per_page,
|
||||
)?;
|
||||
|
||||
Ok(ListEntriesQuery { selection, page })
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_activity_id(named: &str) -> Result<ActivityId, ApiValidationError> {
|
||||
named
|
||||
.parse()
|
||||
.map(ActivityId::from_uuid)
|
||||
.map_err(|_| ApiValidationError::Invalid(format!("{named} is not an activity id")))
|
||||
}
|
||||
|
||||
impl DateRangeParams {
|
||||
pub fn into_date_range(self) -> Result<DateRange, ApiValidationError> {
|
||||
let from = parse_datetime(&self.from)?;
|
||||
@@ -87,27 +123,6 @@ impl DateRangeParams {
|
||||
let range = Some(self.into_date_range()?);
|
||||
Ok(MoodStatsQuery { user_id, range })
|
||||
}
|
||||
|
||||
pub fn into_mood_filter(
|
||||
self,
|
||||
user_id: UserId,
|
||||
mood: u8,
|
||||
) -> Result<FilterByMoodQuery, ApiValidationError> {
|
||||
let mood = Mood::try_from(mood)?;
|
||||
Ok(FilterByMoodQuery { user_id, mood })
|
||||
}
|
||||
|
||||
pub fn into_activity_filter(
|
||||
self,
|
||||
user_id: UserId,
|
||||
activity_id: &str,
|
||||
) -> Result<FilterByActivityQuery, ApiValidationError> {
|
||||
let activity_id = super::shared::parse_uuid(activity_id)?.into();
|
||||
Ok(FilterByActivityQuery {
|
||||
user_id,
|
||||
activity_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ComposedEntry> for EntryResponse {
|
||||
@@ -118,7 +133,7 @@ impl From<ComposedEntry> for EntryResponse {
|
||||
id: entry.id().value(),
|
||||
user_id: entry.user_id().value(),
|
||||
mood: entry.mood().value(),
|
||||
mood_label: format!("{:?}", entry.mood()),
|
||||
mood_label: entry.mood().label().to_string(),
|
||||
logged_at: *entry.logged_at(),
|
||||
photo_urls: composed
|
||||
.photos()
|
||||
|
||||
19
crates/api-types/src/mappers/job.rs
Normal file
19
crates/api-types/src/mappers/job.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use domain::job::{Job, JobSubject};
|
||||
|
||||
use crate::responses::ExhaustedJobResponse;
|
||||
|
||||
impl From<Job> for ExhaustedJobResponse {
|
||||
fn from(job: Job) -> Self {
|
||||
let JobSubject::Entry(entry_id) = job.subject();
|
||||
|
||||
Self {
|
||||
id: job.id().value().to_string(),
|
||||
kind: job.kind().name().to_string(),
|
||||
subject: entry_id.value().to_string(),
|
||||
attempts: job.attempts(),
|
||||
last_error: job.last_error().map(|reason| reason.to_string()),
|
||||
enqueued_at: *job.enqueued_at(),
|
||||
updated_at: *job.updated_at(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use domain::attachment::{PhotoId, VoiceMemoId};
|
||||
use domain::attachment::{MediaKind, MediaRef, PhotoId, VoiceMemoId};
|
||||
|
||||
use crate::responses::MediaIdResponse;
|
||||
use crate::responses::{MediaIdResponse, OwnedMediaResponse};
|
||||
|
||||
impl From<PhotoId> for MediaIdResponse {
|
||||
fn from(id: PhotoId) -> Self {
|
||||
@@ -13,3 +13,18 @@ impl From<VoiceMemoId> for MediaIdResponse {
|
||||
Self { id: id.value() }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MediaRef> for OwnedMediaResponse {
|
||||
fn from(media: MediaRef) -> Self {
|
||||
let path = match media.kind() {
|
||||
MediaKind::Photo => "photos",
|
||||
MediaKind::VoiceMemo => "voice-memos",
|
||||
};
|
||||
|
||||
Self {
|
||||
url: format!("/api/v1/media/{path}/{}", media.id()),
|
||||
id: media.id().to_string(),
|
||||
kind: media.kind().name().to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ use domain::entry::{Date, DateSpan};
|
||||
use domain::metric::{DailyMetric, MetricKind, MetricValue};
|
||||
|
||||
use crate::errors::ApiValidationError;
|
||||
use crate::requests::{DateSpanParams, MetricPayload};
|
||||
use crate::params::DateSpanParams;
|
||||
use crate::requests::MetricPayload;
|
||||
use crate::responses::DailyMetricResponse;
|
||||
|
||||
impl From<DailyMetric> for DailyMetricResponse {
|
||||
|
||||
@@ -6,9 +6,11 @@ mod correlation;
|
||||
mod cycle;
|
||||
mod entry;
|
||||
mod import_metrics;
|
||||
mod job;
|
||||
mod media;
|
||||
mod metric;
|
||||
mod provider;
|
||||
mod push;
|
||||
mod reminder;
|
||||
mod restore;
|
||||
pub mod shared;
|
||||
|
||||
13
crates/api-types/src/mappers/push.rs
Normal file
13
crates/api-types/src/mappers/push.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use domain::push::PushSubscription;
|
||||
|
||||
use crate::responses::PushSubscriptionResponse;
|
||||
|
||||
impl From<PushSubscription> for PushSubscriptionResponse {
|
||||
fn from(subscription: PushSubscription) -> Self {
|
||||
Self {
|
||||
id: subscription.id().value().to_string(),
|
||||
endpoint: subscription.endpoint().to_string(),
|
||||
created_at: *subscription.created_at(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ impl From<RestoreOutcome> for RestoreOutcomeResponse {
|
||||
activities: outcome.activities,
|
||||
reminders: outcome.reminders,
|
||||
media: outcome.media,
|
||||
skipped: outcome.skipped,
|
||||
unreadable: outcome.unreadable,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use application::entry::use_cases::get_mood_stats::MoodStats;
|
||||
|
||||
use crate::responses::{MoodFrequency, MoodStatsResponse};
|
||||
use crate::responses::{MoodFrequencyResponse, MoodStatsResponse};
|
||||
|
||||
impl From<MoodStats> for MoodStatsResponse {
|
||||
fn from(stats: MoodStats) -> Self {
|
||||
@@ -9,14 +9,16 @@ impl From<MoodStats> for MoodStatsResponse {
|
||||
frequency: stats
|
||||
.frequency
|
||||
.into_iter()
|
||||
.map(|(mood, count)| MoodFrequency {
|
||||
.map(|(mood, count)| MoodFrequencyResponse {
|
||||
mood: mood.value(),
|
||||
mood_label: format!("{mood:?}"),
|
||||
mood_label: mood.label().to_string(),
|
||||
count,
|
||||
})
|
||||
.collect(),
|
||||
current_streak: stats.current_streak,
|
||||
total_entries: stats.total_entries,
|
||||
first_logged_on: stats.first_logged_on.map(|date| date.to_string()),
|
||||
last_logged_on: stats.last_logged_on.map(|date| date.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
5
crates/api-types/src/params/activity.rs
Normal file
5
crates/api-types/src/params/activity.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
#[derive(Debug, Default, serde::Deserialize, utoipa::ToSchema, utoipa::IntoParams)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ListActivitiesParams {
|
||||
pub include_archived: Option<bool>,
|
||||
}
|
||||
18
crates/api-types/src/params/entry.rs
Normal file
18
crates/api-types/src/params/entry.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema, utoipa::IntoParams)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ListEntriesParams {
|
||||
pub from: Option<String>,
|
||||
pub to: Option<String>,
|
||||
pub mood: Option<u8>,
|
||||
pub activity: Option<String>,
|
||||
pub updated_since: Option<String>,
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema, utoipa::IntoParams)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DateRangeParams {
|
||||
pub from: String,
|
||||
pub to: String,
|
||||
}
|
||||
6
crates/api-types/src/params/metric.rs
Normal file
6
crates/api-types/src/params/metric.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema, utoipa::IntoParams)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DateSpanParams {
|
||||
pub from: String,
|
||||
pub to: String,
|
||||
}
|
||||
7
crates/api-types/src/params/mod.rs
Normal file
7
crates/api-types/src/params/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
mod activity;
|
||||
mod entry;
|
||||
mod metric;
|
||||
|
||||
pub use activity::ListActivitiesParams;
|
||||
pub use entry::{DateRangeParams, ListEntriesParams};
|
||||
pub use metric::DateSpanParams;
|
||||
@@ -2,4 +2,5 @@
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MintApiTokenRequest {
|
||||
pub name: String,
|
||||
pub scopes: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -1,33 +1,25 @@
|
||||
use crate::dimension::DimensionPayload;
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateEntryRequest {
|
||||
pub mood: u8,
|
||||
pub logged_at: Option<String>,
|
||||
pub dimensions: Option<Vec<crate::dimension::DimensionPayload>>,
|
||||
pub dimensions: Option<Vec<DimensionPayload>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateEntriesRequest {
|
||||
pub entries: Vec<CreateEntryRequest>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateEntryRequest {
|
||||
pub mood: u8,
|
||||
pub mood: Option<u8>,
|
||||
pub logged_at: Option<String>,
|
||||
pub dimensions: Option<Vec<crate::dimension::DimensionPayload>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema, utoipa::IntoParams)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ListEntriesParams {
|
||||
pub from: Option<String>,
|
||||
pub to: Option<String>,
|
||||
pub limit: Option<i64>,
|
||||
pub offset: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema, utoipa::IntoParams)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DateRangeParams {
|
||||
pub from: String,
|
||||
pub to: String,
|
||||
pub dimensions: Option<Vec<DimensionPayload>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
|
||||
@@ -10,10 +10,3 @@ pub struct MetricPayload {
|
||||
pub kind: String,
|
||||
pub value: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema, utoipa::IntoParams)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DateSpanParams {
|
||||
pub from: String,
|
||||
pub to: String,
|
||||
}
|
||||
|
||||
@@ -13,11 +13,10 @@ pub use activity::{CreateActivityRequest, RenameActivityRequest, SetCategoryRequ
|
||||
pub use api_token::MintApiTokenRequest;
|
||||
pub use cycle::SetPreferencesRequest;
|
||||
pub use entry::{
|
||||
CreateEntryRequest, DateRangeParams, ListEntriesParams, ReplaceActivityRequest,
|
||||
UpdateEntryRequest,
|
||||
CreateEntriesRequest, CreateEntryRequest, ReplaceActivityRequest, UpdateEntryRequest,
|
||||
};
|
||||
pub use import::{ImportDailyMetricsRequest, ImportedDayPayload, ImportedMetricPayload};
|
||||
pub use metric::{DateSpanParams, MetricPayload, SetDailyMetricsRequest};
|
||||
pub use metric::{MetricPayload, SetDailyMetricsRequest};
|
||||
pub use provider::ConnectProviderRequest;
|
||||
pub use push::{PushSubscribeRequest, PushUnsubscribeRequest};
|
||||
pub use reminder::{CreateReminderRequest, UpdateReminderRequest};
|
||||
|
||||
@@ -25,8 +25,9 @@ pub struct PushUnsubscribeRequest {
|
||||
}
|
||||
|
||||
impl PushUnsubscribeRequest {
|
||||
pub fn into_command(self) -> UnsubscribePushCommand {
|
||||
pub fn into_command(self, user_id: UserId) -> UnsubscribePushCommand {
|
||||
UnsubscribePushCommand {
|
||||
user_id,
|
||||
endpoint: self.endpoint,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use chrono::{DateTime, Utc};
|
||||
pub struct ApiTokenResponse {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub scope: String,
|
||||
pub scopes: Vec<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub last_used_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
26
crates/api-types/src/responses/auth.rs
Normal file
26
crates/api-types/src/responses/auth.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
use super::UserResponse;
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SignedInResponse {
|
||||
pub access_token: String,
|
||||
pub refresh_token: String,
|
||||
pub expires_at: DateTime<Utc>,
|
||||
pub user: UserResponse,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RefreshedResponse {
|
||||
pub access_token: String,
|
||||
pub refresh_token: String,
|
||||
pub expires_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct VapidKeyResponse {
|
||||
pub public_key: String,
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BulkActionResponse {
|
||||
pub affected_count: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ImportResultResponse {
|
||||
pub imported: u64,
|
||||
pub skipped: u64,
|
||||
pub errors: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MediaIdResponse {
|
||||
pub id: Uuid,
|
||||
}
|
||||
5
crates/api-types/src/responses/bulk_action.rs
Normal file
5
crates/api-types/src/responses/bulk_action.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BulkActionResponse {
|
||||
pub affected_count: u64,
|
||||
}
|
||||
23
crates/api-types/src/responses/error.rs
Normal file
23
crates/api-types/src/responses/error.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ErrorResponse {
|
||||
pub error: ErrorDetailResponse,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ErrorDetailResponse {
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl ErrorResponse {
|
||||
pub fn new(code: &str, message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
error: ErrorDetailResponse {
|
||||
code: code.to_string(),
|
||||
message: message.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
7
crates/api-types/src/responses/import_result.rs
Normal file
7
crates/api-types/src/responses/import_result.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ImportResultResponse {
|
||||
pub imported: u64,
|
||||
pub skipped: u64,
|
||||
pub errors: Vec<String>,
|
||||
}
|
||||
13
crates/api-types/src/responses/job.rs
Normal file
13
crates/api-types/src/responses/job.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExhaustedJobResponse {
|
||||
pub id: String,
|
||||
pub kind: String,
|
||||
pub subject: String,
|
||||
pub attempts: u32,
|
||||
pub last_error: Option<String>,
|
||||
pub enqueued_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
13
crates/api-types/src/responses/media.rs
Normal file
13
crates/api-types/src/responses/media.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MediaIdResponse {
|
||||
pub id: uuid::Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OwnedMediaResponse {
|
||||
pub id: String,
|
||||
pub kind: String,
|
||||
pub url: String,
|
||||
}
|
||||
@@ -1,31 +1,49 @@
|
||||
mod activity;
|
||||
mod api_token;
|
||||
mod bulk;
|
||||
mod auth;
|
||||
mod bulk_action;
|
||||
mod calendar;
|
||||
mod correlation;
|
||||
mod cycle;
|
||||
mod entry;
|
||||
mod error;
|
||||
mod import;
|
||||
mod import_result;
|
||||
mod job;
|
||||
mod media;
|
||||
mod metric;
|
||||
mod page;
|
||||
mod provider;
|
||||
mod push;
|
||||
mod reminder;
|
||||
mod restore;
|
||||
mod server;
|
||||
mod stats;
|
||||
mod user;
|
||||
|
||||
pub use activity::ActivityResponse;
|
||||
pub use api_token::{ApiTokenResponse, MintedApiTokenResponse};
|
||||
pub use bulk::{BulkActionResponse, ImportResultResponse, MediaIdResponse};
|
||||
pub use auth::{RefreshedResponse, SignedInResponse, VapidKeyResponse};
|
||||
pub use bulk_action::BulkActionResponse;
|
||||
pub use calendar::CalendarDayResponse;
|
||||
pub use correlation::{
|
||||
AgreementResponse, CorrelationInputResponse, CorrelationRowResponse, StrategyScoreResponse,
|
||||
};
|
||||
pub use cycle::{CyclePositionResponse, CycleViewResponse, PreferencesResponse};
|
||||
pub use entry::EntryResponse;
|
||||
pub use error::{ErrorDetailResponse, ErrorResponse};
|
||||
pub use import::{ImportOutcomeResponse, RejectedMetricResponse, RejectionResponse};
|
||||
pub use import_result::ImportResultResponse;
|
||||
pub use job::ExhaustedJobResponse;
|
||||
pub use media::{MediaIdResponse, OwnedMediaResponse};
|
||||
pub use metric::DailyMetricResponse;
|
||||
pub use page::EntryPageResponse;
|
||||
pub use provider::ProviderConnectionResponse;
|
||||
pub use push::PushSubscriptionResponse;
|
||||
pub use reminder::{DayScheduleResponse, ReminderResponse};
|
||||
pub use restore::RestoreOutcomeResponse;
|
||||
pub use stats::{MoodFrequency, MoodStatsResponse};
|
||||
pub use server::{
|
||||
MetricKindResponse, MoodResponse, ServerInfoResponse, ServerLimitsResponse, TokenScopeResponse,
|
||||
};
|
||||
pub use stats::{MoodFrequencyResponse, MoodStatsResponse};
|
||||
pub use user::UserResponse;
|
||||
|
||||
11
crates/api-types/src/responses/page.rs
Normal file
11
crates/api-types/src/responses/page.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use super::EntryResponse;
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EntryPageResponse {
|
||||
pub items: Vec<EntryResponse>,
|
||||
pub total: u64,
|
||||
pub limit: i64,
|
||||
pub offset: i64,
|
||||
pub has_more: bool,
|
||||
}
|
||||
9
crates/api-types/src/responses/push.rs
Normal file
9
crates/api-types/src/responses/push.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PushSubscriptionResponse {
|
||||
pub id: String,
|
||||
pub endpoint: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
@@ -7,5 +7,6 @@ pub struct RestoreOutcomeResponse {
|
||||
pub activities: usize,
|
||||
pub reminders: usize,
|
||||
pub media: usize,
|
||||
pub skipped: usize,
|
||||
pub unreadable: Vec<String>,
|
||||
}
|
||||
|
||||
50
crates/api-types/src/responses/server.rs
Normal file
50
crates/api-types/src/responses/server.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ServerInfoResponse {
|
||||
pub version: String,
|
||||
pub registration_open: bool,
|
||||
pub push_enabled: bool,
|
||||
pub weather_enabled: bool,
|
||||
pub now_playing_provider: Option<String>,
|
||||
pub access_token_ttl_seconds: u64,
|
||||
pub limits: ServerLimitsResponse,
|
||||
pub token_scopes: Vec<TokenScopeResponse>,
|
||||
pub metric_kinds: Vec<MetricKindResponse>,
|
||||
pub moods: Vec<MoodResponse>,
|
||||
pub weather_conditions: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ServerLimitsResponse {
|
||||
pub max_body_size: usize,
|
||||
pub max_content_length: usize,
|
||||
pub max_photos: usize,
|
||||
pub max_voice_memos: usize,
|
||||
pub max_activities_per_entry: usize,
|
||||
pub max_entries_per_page: i64,
|
||||
pub max_import_days: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TokenScopeResponse {
|
||||
pub name: String,
|
||||
pub describes: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MetricKindResponse {
|
||||
pub name: String,
|
||||
pub unit: String,
|
||||
pub minimum: i64,
|
||||
pub maximum: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MoodResponse {
|
||||
pub value: u8,
|
||||
pub label: String,
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MoodFrequency {
|
||||
pub struct MoodFrequencyResponse {
|
||||
pub mood: u8,
|
||||
pub mood_label: String,
|
||||
pub count: usize,
|
||||
@@ -10,7 +10,9 @@ pub struct MoodFrequency {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MoodStatsResponse {
|
||||
pub average: Option<f64>,
|
||||
pub frequency: Vec<MoodFrequency>,
|
||||
pub frequency: Vec<MoodFrequencyResponse>,
|
||||
pub current_streak: usize,
|
||||
pub total_entries: usize,
|
||||
pub first_logged_on: Option<String>,
|
||||
pub last_logged_on: Option<String>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user