style: apply cargo fmt to entire codebase
Standardize code formatting across all 173 Rust source files using rustfmt. No functional changes - purely cosmetic. This establishes a consistent code style baseline for the project going forward.
This commit is contained in:
@@ -3,9 +3,11 @@ use chrono::Utc;
|
||||
use sqlx::{PgPool, Row, types::Uuid};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::domain::entities::contact::AddressBook;
|
||||
use crate::domain::repositories::address_book_repository::{AddressBookRepository, AddressBookRepositoryResult};
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::entities::contact::AddressBook;
|
||||
use crate::domain::repositories::address_book_repository::{
|
||||
AddressBookRepository, AddressBookRepositoryResult,
|
||||
};
|
||||
|
||||
pub struct AddressBookPgRepository {
|
||||
pool: Arc<PgPool>,
|
||||
@@ -19,7 +21,10 @@ impl AddressBookPgRepository {
|
||||
|
||||
#[async_trait]
|
||||
impl AddressBookRepository for AddressBookPgRepository {
|
||||
async fn create_address_book(&self, address_book: AddressBook) -> AddressBookRepositoryResult<AddressBook> {
|
||||
async fn create_address_book(
|
||||
&self,
|
||||
address_book: AddressBook,
|
||||
) -> AddressBookRepositoryResult<AddressBook> {
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO carddav.address_books (id, name, owner_id, description, color, is_public, created_at, updated_at)
|
||||
@@ -51,7 +56,10 @@ impl AddressBookRepository for AddressBookPgRepository {
|
||||
))
|
||||
}
|
||||
|
||||
async fn update_address_book(&self, address_book: AddressBook) -> AddressBookRepositoryResult<AddressBook> {
|
||||
async fn update_address_book(
|
||||
&self,
|
||||
address_book: AddressBook,
|
||||
) -> AddressBookRepositoryResult<AddressBook> {
|
||||
let now = Utc::now();
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
@@ -59,7 +67,7 @@ impl AddressBookRepository for AddressBookPgRepository {
|
||||
SET name = $1, description = $2, color = $3, is_public = $4, updated_at = $5
|
||||
WHERE id = $6
|
||||
RETURNING id, name, owner_id, description, color, is_public, created_at, updated_at
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(address_book.name())
|
||||
.bind(address_book.description())
|
||||
@@ -69,7 +77,9 @@ impl AddressBookRepository for AddressBookPgRepository {
|
||||
.bind(address_book.id())
|
||||
.fetch_one(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to update address book: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to update address book: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(AddressBook::from_raw(
|
||||
row.get("id"),
|
||||
@@ -88,59 +98,38 @@ impl AddressBookRepository for AddressBookPgRepository {
|
||||
r#"
|
||||
DELETE FROM carddav.address_books
|
||||
WHERE id = $1
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.execute(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to delete address book: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to delete address book: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_address_book_by_id(&self, id: &Uuid) -> AddressBookRepositoryResult<Option<AddressBook>> {
|
||||
async fn get_address_book_by_id(
|
||||
&self,
|
||||
id: &Uuid,
|
||||
) -> AddressBookRepositoryResult<Option<AddressBook>> {
|
||||
let maybe_row = sqlx::query(
|
||||
r#"
|
||||
SELECT id, name, owner_id, description, color, is_public, created_at, updated_at
|
||||
FROM carddav.address_books
|
||||
WHERE id = $1
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get address book by id: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to get address book by id: {}", e))
|
||||
})?;
|
||||
|
||||
let result = maybe_row.map(|row| AddressBook::from_raw(
|
||||
row.get("id"),
|
||||
row.get("name"),
|
||||
row.get("owner_id"),
|
||||
row.get("description"),
|
||||
row.get("color"),
|
||||
row.get("is_public"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
));
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn get_address_books_by_owner(&self, owner_id: &str) -> AddressBookRepositoryResult<Vec<AddressBook>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT id, name, owner_id, description, color, is_public, created_at, updated_at
|
||||
FROM carddav.address_books
|
||||
WHERE owner_id = $1
|
||||
ORDER BY name
|
||||
"#
|
||||
)
|
||||
.bind(owner_id)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get address books by owner: {}", e)))?;
|
||||
|
||||
let result = rows.into_iter()
|
||||
.map(|row| AddressBook::from_raw(
|
||||
let result = maybe_row.map(|row| {
|
||||
AddressBook::from_raw(
|
||||
row.get("id"),
|
||||
row.get("name"),
|
||||
row.get("owner_id"),
|
||||
@@ -149,13 +138,54 @@ impl AddressBookRepository for AddressBookPgRepository {
|
||||
row.get("is_public"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
))
|
||||
)
|
||||
});
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn get_address_books_by_owner(
|
||||
&self,
|
||||
owner_id: &str,
|
||||
) -> AddressBookRepositoryResult<Vec<AddressBook>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT id, name, owner_id, description, color, is_public, created_at, updated_at
|
||||
FROM carddav.address_books
|
||||
WHERE owner_id = $1
|
||||
ORDER BY name
|
||||
"#,
|
||||
)
|
||||
.bind(owner_id)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to get address books by owner: {}", e))
|
||||
})?;
|
||||
|
||||
let result = rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
AddressBook::from_raw(
|
||||
row.get("id"),
|
||||
row.get("name"),
|
||||
row.get("owner_id"),
|
||||
row.get("description"),
|
||||
row.get("color"),
|
||||
row.get("is_public"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn get_shared_address_books(&self, user_id: &str) -> AddressBookRepositoryResult<Vec<AddressBook>> {
|
||||
async fn get_shared_address_books(
|
||||
&self,
|
||||
user_id: &str,
|
||||
) -> AddressBookRepositoryResult<Vec<AddressBook>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT a.id, a.name, a.owner_id, a.description, a.color, a.is_public, a.created_at, a.updated_at
|
||||
@@ -170,17 +200,20 @@ impl AddressBookRepository for AddressBookPgRepository {
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get shared address books: {}", e)))?;
|
||||
|
||||
let result = rows.into_iter()
|
||||
.map(|row| AddressBook::from_raw(
|
||||
row.get("id"),
|
||||
row.get("name"),
|
||||
row.get("owner_id"),
|
||||
row.get("description"),
|
||||
row.get("color"),
|
||||
row.get("is_public"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
))
|
||||
let result = rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
AddressBook::from_raw(
|
||||
row.get("id"),
|
||||
row.get("name"),
|
||||
row.get("owner_id"),
|
||||
row.get("description"),
|
||||
row.get("color"),
|
||||
row.get("is_public"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
@@ -193,35 +226,45 @@ impl AddressBookRepository for AddressBookPgRepository {
|
||||
FROM carddav.address_books
|
||||
WHERE is_public = true
|
||||
ORDER BY name
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get public address books: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to get public address books: {}", e))
|
||||
})?;
|
||||
|
||||
let result = rows.into_iter()
|
||||
.map(|row| AddressBook::from_raw(
|
||||
row.get("id"),
|
||||
row.get("name"),
|
||||
row.get("owner_id"),
|
||||
row.get("description"),
|
||||
row.get("color"),
|
||||
row.get("is_public"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
))
|
||||
let result = rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
AddressBook::from_raw(
|
||||
row.get("id"),
|
||||
row.get("name"),
|
||||
row.get("owner_id"),
|
||||
row.get("description"),
|
||||
row.get("color"),
|
||||
row.get("is_public"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn share_address_book(&self, address_book_id: &Uuid, user_id: &str, can_write: bool) -> AddressBookRepositoryResult<()> {
|
||||
async fn share_address_book(
|
||||
&self,
|
||||
address_book_id: &Uuid,
|
||||
user_id: &str,
|
||||
can_write: bool,
|
||||
) -> AddressBookRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO carddav.address_book_shares (address_book_id, user_id, can_write)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (address_book_id, user_id) DO UPDATE SET can_write = $3
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(address_book_id)
|
||||
.bind(user_id)
|
||||
@@ -233,40 +276,52 @@ impl AddressBookRepository for AddressBookPgRepository {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn unshare_address_book(&self, address_book_id: &Uuid, user_id: &str) -> AddressBookRepositoryResult<()> {
|
||||
async fn unshare_address_book(
|
||||
&self,
|
||||
address_book_id: &Uuid,
|
||||
user_id: &str,
|
||||
) -> AddressBookRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
DELETE FROM carddav.address_book_shares
|
||||
WHERE address_book_id = $1 AND user_id = $2
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(address_book_id)
|
||||
.bind(user_id)
|
||||
.execute(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to unshare address book: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to unshare address book: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_address_book_shares(&self, address_book_id: &Uuid) -> AddressBookRepositoryResult<Vec<(String, bool)>> {
|
||||
async fn get_address_book_shares(
|
||||
&self,
|
||||
address_book_id: &Uuid,
|
||||
) -> AddressBookRepositoryResult<Vec<(String, bool)>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT user_id, can_write
|
||||
FROM carddav.address_book_shares
|
||||
WHERE address_book_id = $1
|
||||
ORDER BY user_id
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(address_book_id)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get address book shares: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to get address book shares: {}", e))
|
||||
})?;
|
||||
|
||||
let result = rows.into_iter()
|
||||
let result = rows
|
||||
.into_iter()
|
||||
.map(|row| (row.get("user_id"), row.get("can_write")))
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@ use chrono::{DateTime, Utc};
|
||||
use sqlx::{PgPool, Row, types::Uuid};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::domain::entities::calendar_event::CalendarEvent;
|
||||
use crate::domain::repositories::calendar_event_repository::{CalendarEventRepository, CalendarEventRepositoryResult};
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::entities::calendar_event::CalendarEvent;
|
||||
use crate::domain::repositories::calendar_event_repository::{
|
||||
CalendarEventRepository, CalendarEventRepositoryResult,
|
||||
};
|
||||
|
||||
pub struct CalendarEventPgRepository {
|
||||
pool: Arc<PgPool>,
|
||||
@@ -19,11 +21,14 @@ impl CalendarEventPgRepository {
|
||||
|
||||
#[async_trait]
|
||||
impl CalendarEventRepository for CalendarEventPgRepository {
|
||||
async fn create_event(&self, event: CalendarEvent) -> CalendarEventRepositoryResult<CalendarEvent> {
|
||||
async fn create_event(
|
||||
&self,
|
||||
event: CalendarEvent,
|
||||
) -> CalendarEventRepositoryResult<CalendarEvent> {
|
||||
// This method would need a full implementation that builds the CalendarEvent
|
||||
// from the query result, using constructor methods
|
||||
// For this demonstration, we return the same event
|
||||
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO caldav.calendar_events (
|
||||
@@ -31,7 +36,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
||||
all_day, rrule, created_at, updated_at, ical_uid, ical_data
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(event.id())
|
||||
.bind(event.calendar_id())
|
||||
@@ -48,15 +53,20 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
||||
.bind(event.ical_data())
|
||||
.execute(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to create calendar event: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to create calendar event: {}", e))
|
||||
})?;
|
||||
|
||||
// We return the same event instead of a result
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
async fn update_event(&self, event: CalendarEvent) -> CalendarEventRepositoryResult<CalendarEvent> {
|
||||
async fn update_event(
|
||||
&self,
|
||||
event: CalendarEvent,
|
||||
) -> CalendarEventRepositoryResult<CalendarEvent> {
|
||||
let now = Utc::now();
|
||||
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE caldav.calendar_events
|
||||
@@ -70,7 +80,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
||||
ical_data = $8,
|
||||
updated_at = $9
|
||||
WHERE id = $10
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(event.summary())
|
||||
.bind(event.description())
|
||||
@@ -84,7 +94,9 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
||||
.bind(event.id())
|
||||
.execute(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to update calendar event: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to update calendar event: {}", e))
|
||||
})?;
|
||||
|
||||
// In a full implementation, we would retrieve the updated event
|
||||
// For simplicity, we return the same event we received
|
||||
@@ -96,21 +108,23 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
||||
r#"
|
||||
DELETE FROM caldav.calendar_events
|
||||
WHERE id = $1
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.execute(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to delete calendar event: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to delete calendar event: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_events_in_time_range(
|
||||
&self,
|
||||
calendar_id: &Uuid,
|
||||
start: &DateTime<Utc>,
|
||||
end: &DateTime<Utc>
|
||||
&self,
|
||||
calendar_id: &Uuid,
|
||||
start: &DateTime<Utc>,
|
||||
end: &DateTime<Utc>,
|
||||
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
@@ -127,14 +141,16 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
||||
(rrule IS NOT NULL AND end_time >= $2)
|
||||
)
|
||||
ORDER BY start_time
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(calendar_id)
|
||||
.bind(start)
|
||||
.bind(end)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get events in time range: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to get events in time range: {}", e))
|
||||
})?;
|
||||
|
||||
let mut events = Vec::new();
|
||||
for row in rows {
|
||||
@@ -152,10 +168,13 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
||||
row.get("ical_data"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
).map_err(|e| DomainError::database_error(format!("Error creating calendar event: {}", e)))?;
|
||||
)
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Error creating calendar event: {}", e))
|
||||
})?;
|
||||
events.push(event);
|
||||
}
|
||||
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
@@ -168,18 +187,20 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
||||
created_at, updated_at, ical_uid, ical_data
|
||||
FROM caldav.calendar_events
|
||||
WHERE id = $1
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get calendar event by id: {}", e)))?
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to get calendar event by id: {}", e))
|
||||
})?
|
||||
.ok_or_else(|| DomainError::not_found("Calendar Event", id.to_string()))?;
|
||||
|
||||
// In a real implementation, we would build a complete CalendarEvent object
|
||||
// For simplicity, we create an object with default values to
|
||||
// demonstrate the approach without macros
|
||||
|
||||
|
||||
let event = CalendarEvent::with_id(
|
||||
row.get("id"),
|
||||
row.get("calendar_id"),
|
||||
@@ -193,13 +214,19 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
||||
row.get("ical_uid"),
|
||||
row.get("ical_data"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at")
|
||||
).map_err(|e| DomainError::database_error(format!("Error creating calendar event: {}", e)))?;
|
||||
|
||||
row.get("updated_at"),
|
||||
)
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Error creating calendar event: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
async fn list_events_by_calendar(&self, calendar_id: &Uuid) -> CalendarEventRepositoryResult<Vec<CalendarEvent>> {
|
||||
|
||||
async fn list_events_by_calendar(
|
||||
&self,
|
||||
calendar_id: &Uuid,
|
||||
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
@@ -209,12 +236,14 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
||||
FROM caldav.calendar_events
|
||||
WHERE calendar_id = $1
|
||||
ORDER BY start_time
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(calendar_id)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get events by calendar: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to get events by calendar: {}", e))
|
||||
})?;
|
||||
|
||||
let mut events = Vec::new();
|
||||
for row in rows {
|
||||
@@ -232,16 +261,23 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
||||
row.get("ical_data"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
).map_err(|e| DomainError::database_error(format!("Error creating calendar event: {}", e)))?;
|
||||
)
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Error creating calendar event: {}", e))
|
||||
})?;
|
||||
events.push(event);
|
||||
}
|
||||
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
async fn find_events_by_summary(&self, calendar_id: &Uuid, summary: &str) -> CalendarEventRepositoryResult<Vec<CalendarEvent>> {
|
||||
|
||||
async fn find_events_by_summary(
|
||||
&self,
|
||||
calendar_id: &Uuid,
|
||||
summary: &str,
|
||||
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>> {
|
||||
let search_pattern = format!("%{}%", summary);
|
||||
|
||||
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
@@ -251,13 +287,15 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
||||
FROM caldav.calendar_events
|
||||
WHERE calendar_id = $1 AND summary ILIKE $2
|
||||
ORDER BY start_time
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(calendar_id)
|
||||
.bind(&search_pattern)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to find events by summary: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to find events by summary: {}", e))
|
||||
})?;
|
||||
|
||||
let mut events = Vec::new();
|
||||
for row in rows {
|
||||
@@ -275,14 +313,21 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
||||
row.get("ical_data"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
).map_err(|e| DomainError::database_error(format!("Error creating calendar event: {}", e)))?;
|
||||
)
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Error creating calendar event: {}", e))
|
||||
})?;
|
||||
events.push(event);
|
||||
}
|
||||
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
async fn find_event_by_ical_uid(&self, calendar_id: &Uuid, ical_uid: &str) -> CalendarEventRepositoryResult<Option<CalendarEvent>> {
|
||||
|
||||
async fn find_event_by_ical_uid(
|
||||
&self,
|
||||
calendar_id: &Uuid,
|
||||
ical_uid: &str,
|
||||
) -> CalendarEventRepositoryResult<Option<CalendarEvent>> {
|
||||
let row_opt = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
@@ -291,13 +336,15 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
||||
created_at, updated_at, ical_uid, ical_data
|
||||
FROM caldav.calendar_events
|
||||
WHERE calendar_id = $1 AND ical_uid = $2
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(calendar_id)
|
||||
.bind(ical_uid)
|
||||
.fetch_optional(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get calendar event by UID: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to get calendar event by UID: {}", e))
|
||||
})?;
|
||||
|
||||
match row_opt {
|
||||
Some(row) => {
|
||||
@@ -315,49 +362,62 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
||||
row.get("ical_data"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
).map_err(|e| DomainError::database_error(format!("Error creating calendar event: {}", e)))?;
|
||||
)
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Error creating calendar event: {}", e))
|
||||
})?;
|
||||
Ok(Some(event))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
async fn count_events_in_calendar(&self, calendar_id: &Uuid) -> CalendarEventRepositoryResult<i64> {
|
||||
|
||||
async fn count_events_in_calendar(
|
||||
&self,
|
||||
calendar_id: &Uuid,
|
||||
) -> CalendarEventRepositoryResult<i64> {
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
SELECT COUNT(*) as count
|
||||
FROM caldav.calendar_events
|
||||
WHERE calendar_id = $1
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(calendar_id)
|
||||
.fetch_one(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to count events in calendar: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to count events in calendar: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(row.get::<i64, _>("count"))
|
||||
}
|
||||
|
||||
async fn delete_all_events_in_calendar(&self, calendar_id: &Uuid) -> CalendarEventRepositoryResult<i64> {
|
||||
|
||||
async fn delete_all_events_in_calendar(
|
||||
&self,
|
||||
calendar_id: &Uuid,
|
||||
) -> CalendarEventRepositoryResult<i64> {
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
DELETE FROM caldav.calendar_events
|
||||
WHERE calendar_id = $1
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(calendar_id)
|
||||
.execute(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to delete all events in calendar: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to delete all events in calendar: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(result.rows_affected() as i64)
|
||||
}
|
||||
|
||||
|
||||
async fn list_events_by_calendar_paginated(
|
||||
&self,
|
||||
&self,
|
||||
calendar_id: &Uuid,
|
||||
limit: i64,
|
||||
offset: i64
|
||||
offset: i64,
|
||||
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
@@ -369,14 +429,19 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
||||
WHERE calendar_id = $1
|
||||
ORDER BY start_time
|
||||
LIMIT $2 OFFSET $3
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(calendar_id)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get paginated events by calendar: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!(
|
||||
"Failed to get paginated events by calendar: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
let mut events = Vec::new();
|
||||
for row in rows {
|
||||
@@ -394,18 +459,21 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
||||
row.get("ical_data"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
).map_err(|e| DomainError::database_error(format!("Error creating calendar event: {}", e)))?;
|
||||
)
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Error creating calendar event: {}", e))
|
||||
})?;
|
||||
events.push(event);
|
||||
}
|
||||
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
|
||||
async fn find_recurring_events_in_range(
|
||||
&self,
|
||||
calendar_id: &Uuid,
|
||||
start: &DateTime<Utc>,
|
||||
end: &DateTime<Utc>
|
||||
end: &DateTime<Utc>,
|
||||
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
@@ -419,14 +487,16 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
||||
AND end_time >= $2
|
||||
AND start_time <= $3
|
||||
ORDER BY start_time
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(calendar_id)
|
||||
.bind(start)
|
||||
.bind(end)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to find recurring events in range: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to find recurring events in range: {}", e))
|
||||
})?;
|
||||
|
||||
let mut events = Vec::new();
|
||||
for row in rows {
|
||||
@@ -444,10 +514,13 @@ impl CalendarEventRepository for CalendarEventPgRepository {
|
||||
row.get("ical_data"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
).map_err(|e| DomainError::database_error(format!("Error creating calendar event: {}", e)))?;
|
||||
)
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Error creating calendar event: {}", e))
|
||||
})?;
|
||||
events.push(event);
|
||||
}
|
||||
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@ use chrono::Utc;
|
||||
use sqlx::{PgPool, Row, types::Uuid};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::domain::entities::calendar::Calendar;
|
||||
use crate::domain::repositories::calendar_repository::{CalendarRepository, CalendarRepositoryResult};
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::entities::calendar::Calendar;
|
||||
use crate::domain::repositories::calendar_repository::{
|
||||
CalendarRepository, CalendarRepositoryResult,
|
||||
};
|
||||
|
||||
pub struct CalendarPgRepository {
|
||||
pool: Arc<PgPool>,
|
||||
@@ -38,7 +40,7 @@ impl CalendarRepository for CalendarPgRepository {
|
||||
.fetch_one(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to create calendar: {}", e)))?;
|
||||
|
||||
|
||||
// Build the Calendar object using its with_id constructor
|
||||
let result = Calendar::with_id(
|
||||
row.get("id"),
|
||||
@@ -48,7 +50,10 @@ impl CalendarRepository for CalendarPgRepository {
|
||||
row.get("color"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
).map_err(|e| DomainError::database_error(format!("Failed to create calendar object: {}", e)))?;
|
||||
)
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to create calendar object: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -61,7 +66,7 @@ impl CalendarRepository for CalendarPgRepository {
|
||||
SET name = $1, description = $2, color = $3, is_public = $4, updated_at = $5
|
||||
WHERE id = $6
|
||||
RETURNING id, name, owner_id, description, color, is_public, created_at, updated_at
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(calendar.name())
|
||||
.bind(calendar.description())
|
||||
@@ -72,7 +77,7 @@ impl CalendarRepository for CalendarPgRepository {
|
||||
.fetch_one(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to update calendar: {}", e)))?;
|
||||
|
||||
|
||||
// Build the Calendar object using its with_id constructor
|
||||
let result = Calendar::with_id(
|
||||
row.get("id"),
|
||||
@@ -82,7 +87,10 @@ impl CalendarRepository for CalendarPgRepository {
|
||||
row.get("color"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
).map_err(|e| DomainError::database_error(format!("Failed to create calendar object: {}", e)))?;
|
||||
)
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to create calendar object: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -92,7 +100,7 @@ impl CalendarRepository for CalendarPgRepository {
|
||||
r#"
|
||||
DELETE FROM caldav.calendars
|
||||
WHERE id = $1
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.execute(&*self.pool)
|
||||
@@ -108,7 +116,7 @@ impl CalendarRepository for CalendarPgRepository {
|
||||
SELECT id, name, owner_id, description, color, is_public, created_at, updated_at
|
||||
FROM caldav.calendars
|
||||
WHERE id = $1
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(&*self.pool)
|
||||
@@ -124,24 +132,32 @@ impl CalendarRepository for CalendarPgRepository {
|
||||
row.get("color"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
).map_err(|e| DomainError::database_error(format!("Failed to create calendar object: {}", e)))?;
|
||||
)
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to create calendar object: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(calendar)
|
||||
}
|
||||
|
||||
async fn list_calendars_by_owner(&self, owner_id: &str) -> CalendarRepositoryResult<Vec<Calendar>> {
|
||||
async fn list_calendars_by_owner(
|
||||
&self,
|
||||
owner_id: &str,
|
||||
) -> CalendarRepositoryResult<Vec<Calendar>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT id, name, owner_id, description, color, is_public, created_at, updated_at
|
||||
FROM caldav.calendars
|
||||
WHERE owner_id = $1
|
||||
ORDER BY name
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(owner_id)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get calendars by owner: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to get calendars by owner: {}", e))
|
||||
})?;
|
||||
|
||||
let mut calendars = Vec::new();
|
||||
for row in rows {
|
||||
@@ -153,27 +169,38 @@ impl CalendarRepository for CalendarPgRepository {
|
||||
row.get("color"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
).map_err(|e| DomainError::database_error(format!("Failed to create calendar object: {}", e)))?;
|
||||
)
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to create calendar object: {}", e))
|
||||
})?;
|
||||
calendars.push(calendar);
|
||||
}
|
||||
|
||||
Ok(calendars)
|
||||
}
|
||||
|
||||
async fn find_calendar_by_name_and_owner(&self, name: &str, owner_id: &str) -> CalendarRepositoryResult<Calendar> {
|
||||
async fn find_calendar_by_name_and_owner(
|
||||
&self,
|
||||
name: &str,
|
||||
owner_id: &str,
|
||||
) -> CalendarRepositoryResult<Calendar> {
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
SELECT id, name, owner_id, description, color, is_public, created_at, updated_at
|
||||
FROM caldav.calendars
|
||||
WHERE name = $1 AND owner_id = $2
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(name)
|
||||
.bind(owner_id)
|
||||
.fetch_optional(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to find calendar by name and owner: {}", e)))?
|
||||
.ok_or_else(|| DomainError::not_found("Calendar", format!("{} (owned by {})", name, owner_id)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to find calendar by name and owner: {}", e))
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
DomainError::not_found("Calendar", format!("{} (owned by {})", name, owner_id))
|
||||
})?;
|
||||
|
||||
let calendar = Calendar::with_id(
|
||||
row.get("id"),
|
||||
@@ -183,12 +210,18 @@ impl CalendarRepository for CalendarPgRepository {
|
||||
row.get("color"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
).map_err(|e| DomainError::database_error(format!("Failed to create calendar object: {}", e)))?;
|
||||
)
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to create calendar object: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(calendar)
|
||||
}
|
||||
|
||||
async fn list_calendars_shared_with_user(&self, user_id: &str) -> CalendarRepositoryResult<Vec<Calendar>> {
|
||||
async fn list_calendars_shared_with_user(
|
||||
&self,
|
||||
user_id: &str,
|
||||
) -> CalendarRepositoryResult<Vec<Calendar>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT c.id, c.name, c.owner_id, c.description, c.color, c.is_public, c.created_at, c.updated_at
|
||||
@@ -213,14 +246,21 @@ impl CalendarRepository for CalendarPgRepository {
|
||||
row.get("color"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
).map_err(|e| DomainError::database_error(format!("Failed to create calendar object: {}", e)))?;
|
||||
)
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to create calendar object: {}", e))
|
||||
})?;
|
||||
calendars.push(calendar);
|
||||
}
|
||||
|
||||
Ok(calendars)
|
||||
}
|
||||
|
||||
async fn list_public_calendars(&self, limit: i64, offset: i64) -> CalendarRepositoryResult<Vec<Calendar>> {
|
||||
async fn list_public_calendars(
|
||||
&self,
|
||||
limit: i64,
|
||||
offset: i64,
|
||||
) -> CalendarRepositoryResult<Vec<Calendar>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT id, name, owner_id, description, color, is_public, created_at, updated_at
|
||||
@@ -228,13 +268,15 @@ impl CalendarRepository for CalendarPgRepository {
|
||||
WHERE is_public = true
|
||||
ORDER BY name
|
||||
LIMIT $1 OFFSET $2
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get public calendars: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to get public calendars: {}", e))
|
||||
})?;
|
||||
|
||||
let mut calendars = Vec::new();
|
||||
for row in rows {
|
||||
@@ -243,17 +285,24 @@ impl CalendarRepository for CalendarPgRepository {
|
||||
row.get("name"),
|
||||
row.get("owner_id"),
|
||||
row.get("description"),
|
||||
row.get("color"),
|
||||
row.get("color"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
).map_err(|e| DomainError::database_error(format!("Failed to create calendar object: {}", e)))?;
|
||||
)
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to create calendar object: {}", e))
|
||||
})?;
|
||||
calendars.push(calendar);
|
||||
}
|
||||
|
||||
Ok(calendars)
|
||||
}
|
||||
|
||||
async fn user_has_calendar_access(&self, calendar_id: &Uuid, user_id: &str) -> CalendarRepositoryResult<bool> {
|
||||
async fn user_has_calendar_access(
|
||||
&self,
|
||||
calendar_id: &Uuid,
|
||||
user_id: &str,
|
||||
) -> CalendarRepositoryResult<bool> {
|
||||
// Check if the user is the owner of the calendar or has a share
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
@@ -264,31 +313,39 @@ impl CalendarRepository for CalendarPgRepository {
|
||||
SELECT 1 FROM caldav.calendar_shares s
|
||||
WHERE s.calendar_id = $1 AND s.user_id = $2
|
||||
) as has_access
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(calendar_id)
|
||||
.bind(user_id)
|
||||
.fetch_one(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to check calendar access: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to check calendar access: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(row.get::<bool, _>("has_access"))
|
||||
}
|
||||
|
||||
async fn share_calendar(&self, calendar_id: &Uuid, user_id: &str, access_level: &str) -> CalendarRepositoryResult<()> {
|
||||
async fn share_calendar(
|
||||
&self,
|
||||
calendar_id: &Uuid,
|
||||
user_id: &str,
|
||||
access_level: &str,
|
||||
) -> CalendarRepositoryResult<()> {
|
||||
// Validate access level
|
||||
if !["read", "write", "owner"].contains(&access_level) {
|
||||
return Err(DomainError::validation_error(
|
||||
format!("Invalid access level: '{}'. Must be 'read', 'write', or 'owner'", access_level)
|
||||
));
|
||||
return Err(DomainError::validation_error(format!(
|
||||
"Invalid access level: '{}'. Must be 'read', 'write', or 'owner'",
|
||||
access_level
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO caldav.calendar_shares (calendar_id, user_id, access_level)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (calendar_id, user_id) DO UPDATE SET access_level = $3
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(calendar_id)
|
||||
.bind(user_id)
|
||||
@@ -300,12 +357,16 @@ impl CalendarRepository for CalendarPgRepository {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_calendar_sharing(&self, calendar_id: &Uuid, user_id: &str) -> CalendarRepositoryResult<()> {
|
||||
async fn remove_calendar_sharing(
|
||||
&self,
|
||||
calendar_id: &Uuid,
|
||||
user_id: &str,
|
||||
) -> CalendarRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
DELETE FROM caldav.calendar_shares
|
||||
WHERE calendar_id = $1 AND user_id = $2
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(calendar_id)
|
||||
.bind(user_id)
|
||||
@@ -316,19 +377,24 @@ impl CalendarRepository for CalendarPgRepository {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_calendar_shares(&self, calendar_id: &Uuid) -> CalendarRepositoryResult<Vec<(String, String)>> {
|
||||
async fn get_calendar_shares(
|
||||
&self,
|
||||
calendar_id: &Uuid,
|
||||
) -> CalendarRepositoryResult<Vec<(String, String)>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT user_id, access_level
|
||||
FROM caldav.calendar_shares
|
||||
WHERE calendar_id = $1
|
||||
ORDER BY user_id
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(calendar_id)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get calendar shares: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to get calendar shares: {}", e))
|
||||
})?;
|
||||
|
||||
let mut shares = Vec::new();
|
||||
for row in rows {
|
||||
@@ -337,76 +403,100 @@ impl CalendarRepository for CalendarPgRepository {
|
||||
|
||||
Ok(shares)
|
||||
}
|
||||
|
||||
async fn get_calendar_property(&self, calendar_id: &Uuid, property_name: &str) -> CalendarRepositoryResult<Option<String>> {
|
||||
|
||||
async fn get_calendar_property(
|
||||
&self,
|
||||
calendar_id: &Uuid,
|
||||
property_name: &str,
|
||||
) -> CalendarRepositoryResult<Option<String>> {
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
SELECT value
|
||||
FROM caldav.calendar_properties
|
||||
WHERE calendar_id = $1 AND name = $2
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(calendar_id)
|
||||
.bind(property_name)
|
||||
.fetch_optional(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get calendar property: {}", e)))?;
|
||||
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to get calendar property: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(row.map(|r| r.get("value")))
|
||||
}
|
||||
|
||||
async fn set_calendar_property(&self, calendar_id: &Uuid, property_name: &str, property_value: &str) -> CalendarRepositoryResult<()> {
|
||||
|
||||
async fn set_calendar_property(
|
||||
&self,
|
||||
calendar_id: &Uuid,
|
||||
property_name: &str,
|
||||
property_value: &str,
|
||||
) -> CalendarRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO caldav.calendar_properties (calendar_id, name, value)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (calendar_id, name) DO UPDATE SET value = $3
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(calendar_id)
|
||||
.bind(property_name)
|
||||
.bind(property_value)
|
||||
.execute(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to set calendar property: {}", e)))?;
|
||||
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to set calendar property: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_calendar_property(&self, calendar_id: &Uuid, property_name: &str) -> CalendarRepositoryResult<()> {
|
||||
|
||||
async fn remove_calendar_property(
|
||||
&self,
|
||||
calendar_id: &Uuid,
|
||||
property_name: &str,
|
||||
) -> CalendarRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
DELETE FROM caldav.calendar_properties
|
||||
WHERE calendar_id = $1 AND name = $2
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(calendar_id)
|
||||
.bind(property_name)
|
||||
.execute(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to remove calendar property: {}", e)))?;
|
||||
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to remove calendar property: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_calendar_properties(&self, calendar_id: &Uuid) -> CalendarRepositoryResult<std::collections::HashMap<String, String>> {
|
||||
|
||||
async fn get_calendar_properties(
|
||||
&self,
|
||||
calendar_id: &Uuid,
|
||||
) -> CalendarRepositoryResult<std::collections::HashMap<String, String>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT name, value
|
||||
FROM caldav.calendar_properties
|
||||
WHERE calendar_id = $1
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(calendar_id)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get calendar properties: {}", e)))?;
|
||||
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to get calendar properties: {}", e))
|
||||
})?;
|
||||
|
||||
let mut properties = std::collections::HashMap::new();
|
||||
for row in rows {
|
||||
properties.insert(row.get("name"), row.get("value"));
|
||||
}
|
||||
|
||||
|
||||
Ok(properties)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,223 +1,276 @@
|
||||
use async_trait::async_trait;
|
||||
use sqlx::{PgPool, Row, types::Uuid};
|
||||
use std::sync::Arc;
|
||||
use chrono::Utc;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use crate::domain::entities::contact::{Contact, ContactGroup};
|
||||
use crate::domain::repositories::contact_repository::{ContactGroupRepository, ContactRepositoryResult};
|
||||
use crate::common::errors::{DomainError, ErrorKind};
|
||||
use super::contact_persistence_dto::{
|
||||
emails_from_persistence, phones_from_persistence, addresses_from_persistence,
|
||||
EmailPersistenceDto, PhonePersistenceDto, AddressPersistenceDto,
|
||||
};
|
||||
|
||||
pub struct ContactGroupPgRepository {
|
||||
pool: Arc<PgPool>,
|
||||
}
|
||||
|
||||
impl ContactGroupPgRepository {
|
||||
pub fn new(pool: Arc<PgPool>) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ContactGroupRepository for ContactGroupPgRepository {
|
||||
async fn create_group(&self, group: ContactGroup) -> ContactRepositoryResult<ContactGroup> {
|
||||
sqlx::query(
|
||||
"INSERT INTO carddav.contact_groups (id, address_book_id, name, created_at, updated_at) VALUES ($1, $2, $3, $4, $5)"
|
||||
)
|
||||
.bind(group.id())
|
||||
.bind(group.address_book_id())
|
||||
.bind(group.name())
|
||||
.bind(group.created_at())
|
||||
.bind(group.updated_at())
|
||||
.execute(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", format!("Failed to create group: {}", e)))?;
|
||||
|
||||
Ok(group)
|
||||
}
|
||||
|
||||
async fn update_group(&self, group: ContactGroup) -> ContactRepositoryResult<ContactGroup> {
|
||||
sqlx::query(
|
||||
"UPDATE carddav.contact_groups SET name = $1, updated_at = $2 WHERE id = $3"
|
||||
)
|
||||
.bind(group.name())
|
||||
.bind(Utc::now())
|
||||
.bind(group.id())
|
||||
.execute(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", format!("Failed to update group: {}", e)))?;
|
||||
|
||||
Ok(group)
|
||||
}
|
||||
|
||||
async fn delete_group(&self, id: &Uuid) -> ContactRepositoryResult<()> {
|
||||
// Delete memberships first
|
||||
sqlx::query("DELETE FROM carddav.group_memberships WHERE group_id = $1")
|
||||
.bind(id)
|
||||
.execute(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", format!("Failed to delete group memberships: {}", e)))?;
|
||||
|
||||
sqlx::query("DELETE FROM carddav.contact_groups WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", format!("Failed to delete group: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_group_by_id(&self, id: &Uuid) -> ContactRepositoryResult<Option<ContactGroup>> {
|
||||
let row = sqlx::query(
|
||||
"SELECT id, address_book_id, name, created_at, updated_at FROM carddav.contact_groups WHERE id = $1"
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", format!("Failed to get group: {}", e)))?;
|
||||
|
||||
match row {
|
||||
Some(row) => {
|
||||
let group = ContactGroup::from_raw(
|
||||
row.get::<Uuid, _>("id"),
|
||||
row.get::<Uuid, _>("address_book_id"),
|
||||
row.get::<String, _>("name"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
);
|
||||
Ok(Some(group))
|
||||
},
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_groups_by_address_book(&self, address_book_id: &Uuid) -> ContactRepositoryResult<Vec<ContactGroup>> {
|
||||
let rows = sqlx::query(
|
||||
"SELECT id, address_book_id, name, created_at, updated_at FROM carddav.contact_groups WHERE address_book_id = $1 ORDER BY name"
|
||||
)
|
||||
.bind(address_book_id)
|
||||
.fetch_all(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", format!("Failed to list groups: {}", e)))?;
|
||||
|
||||
Ok(rows.into_iter().map(|row| {
|
||||
ContactGroup::from_raw(
|
||||
row.get::<Uuid, _>("id"),
|
||||
row.get::<Uuid, _>("address_book_id"),
|
||||
row.get::<String, _>("name"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
)
|
||||
}).collect())
|
||||
}
|
||||
|
||||
async fn add_contact_to_group(&self, group_id: &Uuid, contact_id: &Uuid) -> ContactRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
"INSERT INTO carddav.group_memberships (group_id, contact_id) VALUES ($1, $2) ON CONFLICT DO NOTHING"
|
||||
)
|
||||
.bind(group_id)
|
||||
.bind(contact_id)
|
||||
.execute(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", format!("Failed to add contact to group: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_contact_from_group(&self, group_id: &Uuid, contact_id: &Uuid) -> ContactRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
"DELETE FROM carddav.group_memberships WHERE group_id = $1 AND contact_id = $2"
|
||||
)
|
||||
.bind(group_id)
|
||||
.bind(contact_id)
|
||||
.execute(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", format!("Failed to remove contact from group: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_contacts_in_group(&self, group_id: &Uuid) -> ContactRepositoryResult<Vec<Contact>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
c.id, c.address_book_id, c.uid, c.full_name, c.first_name, c.last_name, c.nickname,
|
||||
c.email, c.phone, c.address, c.organization, c.title, c.notes, c.photo_url,
|
||||
c.birthday, c.anniversary, c.vcard, c.etag, c.created_at, c.updated_at
|
||||
FROM carddav.contacts c
|
||||
INNER JOIN carddav.group_memberships gm ON c.id = gm.contact_id
|
||||
WHERE gm.group_id = $1
|
||||
ORDER BY c.full_name, c.first_name, c.last_name
|
||||
"#
|
||||
)
|
||||
.bind(group_id)
|
||||
.fetch_all(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", format!("Failed to get contacts in group: {}", e)))?;
|
||||
|
||||
let mut contacts = Vec::new();
|
||||
for row in &rows {
|
||||
let email_json: JsonValue = row.get("email");
|
||||
let phone_json: JsonValue = row.get("phone");
|
||||
let address_json: JsonValue = row.get("address");
|
||||
|
||||
let emails = serde_json::from_value::<Vec<EmailPersistenceDto>>(email_json)
|
||||
.map(emails_from_persistence)
|
||||
.unwrap_or_default();
|
||||
let phones = serde_json::from_value::<Vec<PhonePersistenceDto>>(phone_json)
|
||||
.map(phones_from_persistence)
|
||||
.unwrap_or_default();
|
||||
let addresses = serde_json::from_value::<Vec<AddressPersistenceDto>>(address_json)
|
||||
.map(addresses_from_persistence)
|
||||
.unwrap_or_default();
|
||||
|
||||
contacts.push(Contact::from_raw(
|
||||
row.get("id"),
|
||||
row.get("address_book_id"),
|
||||
row.get("uid"),
|
||||
row.get::<Option<String>, _>("full_name"),
|
||||
row.get::<Option<String>, _>("first_name"),
|
||||
row.get::<Option<String>, _>("last_name"),
|
||||
row.get::<Option<String>, _>("nickname"),
|
||||
emails,
|
||||
phones,
|
||||
addresses,
|
||||
row.get::<Option<String>, _>("organization"),
|
||||
row.get::<Option<String>, _>("title"),
|
||||
row.get::<Option<String>, _>("notes"),
|
||||
row.get::<Option<String>, _>("photo_url"),
|
||||
row.get("birthday"),
|
||||
row.get("anniversary"),
|
||||
row.get("vcard"),
|
||||
row.get("etag"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
));
|
||||
}
|
||||
Ok(contacts)
|
||||
}
|
||||
|
||||
async fn get_groups_for_contact(&self, contact_id: &Uuid) -> ContactRepositoryResult<Vec<ContactGroup>> {
|
||||
let rows = sqlx::query(
|
||||
"SELECT g.id, g.address_book_id, g.name, g.created_at, g.updated_at FROM carddav.contact_groups g INNER JOIN carddav.group_memberships gm ON g.id = gm.group_id WHERE gm.contact_id = $1 ORDER BY g.name"
|
||||
)
|
||||
.bind(contact_id)
|
||||
.fetch_all(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", format!("Failed to get groups for contact: {}", e)))?;
|
||||
|
||||
Ok(rows.into_iter().map(|row| {
|
||||
ContactGroup::from_raw(
|
||||
row.get::<Uuid, _>("id"),
|
||||
row.get::<Uuid, _>("address_book_id"),
|
||||
row.get::<String, _>("name"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
)
|
||||
}).collect())
|
||||
}
|
||||
}
|
||||
use async_trait::async_trait;
|
||||
use chrono::Utc;
|
||||
use serde_json::Value as JsonValue;
|
||||
use sqlx::{PgPool, Row, types::Uuid};
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::contact_persistence_dto::{
|
||||
AddressPersistenceDto, EmailPersistenceDto, PhonePersistenceDto, addresses_from_persistence,
|
||||
emails_from_persistence, phones_from_persistence,
|
||||
};
|
||||
use crate::common::errors::{DomainError, ErrorKind};
|
||||
use crate::domain::entities::contact::{Contact, ContactGroup};
|
||||
use crate::domain::repositories::contact_repository::{
|
||||
ContactGroupRepository, ContactRepositoryResult,
|
||||
};
|
||||
|
||||
pub struct ContactGroupPgRepository {
|
||||
pool: Arc<PgPool>,
|
||||
}
|
||||
|
||||
impl ContactGroupPgRepository {
|
||||
pub fn new(pool: Arc<PgPool>) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ContactGroupRepository for ContactGroupPgRepository {
|
||||
async fn create_group(&self, group: ContactGroup) -> ContactRepositoryResult<ContactGroup> {
|
||||
sqlx::query(
|
||||
"INSERT INTO carddav.contact_groups (id, address_book_id, name, created_at, updated_at) VALUES ($1, $2, $3, $4, $5)"
|
||||
)
|
||||
.bind(group.id())
|
||||
.bind(group.address_book_id())
|
||||
.bind(group.name())
|
||||
.bind(group.created_at())
|
||||
.bind(group.updated_at())
|
||||
.execute(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", format!("Failed to create group: {}", e)))?;
|
||||
|
||||
Ok(group)
|
||||
}
|
||||
|
||||
async fn update_group(&self, group: ContactGroup) -> ContactRepositoryResult<ContactGroup> {
|
||||
sqlx::query("UPDATE carddav.contact_groups SET name = $1, updated_at = $2 WHERE id = $3")
|
||||
.bind(group.name())
|
||||
.bind(Utc::now())
|
||||
.bind(group.id())
|
||||
.execute(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"ContactGroup",
|
||||
format!("Failed to update group: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(group)
|
||||
}
|
||||
|
||||
async fn delete_group(&self, id: &Uuid) -> ContactRepositoryResult<()> {
|
||||
// Delete memberships first
|
||||
sqlx::query("DELETE FROM carddav.group_memberships WHERE group_id = $1")
|
||||
.bind(id)
|
||||
.execute(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"ContactGroup",
|
||||
format!("Failed to delete group memberships: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
sqlx::query("DELETE FROM carddav.contact_groups WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"ContactGroup",
|
||||
format!("Failed to delete group: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_group_by_id(&self, id: &Uuid) -> ContactRepositoryResult<Option<ContactGroup>> {
|
||||
let row = sqlx::query(
|
||||
"SELECT id, address_book_id, name, created_at, updated_at FROM carddav.contact_groups WHERE id = $1"
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", format!("Failed to get group: {}", e)))?;
|
||||
|
||||
match row {
|
||||
Some(row) => {
|
||||
let group = ContactGroup::from_raw(
|
||||
row.get::<Uuid, _>("id"),
|
||||
row.get::<Uuid, _>("address_book_id"),
|
||||
row.get::<String, _>("name"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
);
|
||||
Ok(Some(group))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_groups_by_address_book(
|
||||
&self,
|
||||
address_book_id: &Uuid,
|
||||
) -> ContactRepositoryResult<Vec<ContactGroup>> {
|
||||
let rows = sqlx::query(
|
||||
"SELECT id, address_book_id, name, created_at, updated_at FROM carddav.contact_groups WHERE address_book_id = $1 ORDER BY name"
|
||||
)
|
||||
.bind(address_book_id)
|
||||
.fetch_all(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", format!("Failed to list groups: {}", e)))?;
|
||||
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
ContactGroup::from_raw(
|
||||
row.get::<Uuid, _>("id"),
|
||||
row.get::<Uuid, _>("address_book_id"),
|
||||
row.get::<String, _>("name"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn add_contact_to_group(
|
||||
&self,
|
||||
group_id: &Uuid,
|
||||
contact_id: &Uuid,
|
||||
) -> ContactRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
"INSERT INTO carddav.group_memberships (group_id, contact_id) VALUES ($1, $2) ON CONFLICT DO NOTHING"
|
||||
)
|
||||
.bind(group_id)
|
||||
.bind(contact_id)
|
||||
.execute(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", format!("Failed to add contact to group: {}", e)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_contact_from_group(
|
||||
&self,
|
||||
group_id: &Uuid,
|
||||
contact_id: &Uuid,
|
||||
) -> ContactRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
"DELETE FROM carddav.group_memberships WHERE group_id = $1 AND contact_id = $2",
|
||||
)
|
||||
.bind(group_id)
|
||||
.bind(contact_id)
|
||||
.execute(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"ContactGroup",
|
||||
format!("Failed to remove contact from group: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_contacts_in_group(
|
||||
&self,
|
||||
group_id: &Uuid,
|
||||
) -> ContactRepositoryResult<Vec<Contact>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
c.id, c.address_book_id, c.uid, c.full_name, c.first_name, c.last_name, c.nickname,
|
||||
c.email, c.phone, c.address, c.organization, c.title, c.notes, c.photo_url,
|
||||
c.birthday, c.anniversary, c.vcard, c.etag, c.created_at, c.updated_at
|
||||
FROM carddav.contacts c
|
||||
INNER JOIN carddav.group_memberships gm ON c.id = gm.contact_id
|
||||
WHERE gm.group_id = $1
|
||||
ORDER BY c.full_name, c.first_name, c.last_name
|
||||
"#,
|
||||
)
|
||||
.bind(group_id)
|
||||
.fetch_all(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"ContactGroup",
|
||||
format!("Failed to get contacts in group: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut contacts = Vec::new();
|
||||
for row in &rows {
|
||||
let email_json: JsonValue = row.get("email");
|
||||
let phone_json: JsonValue = row.get("phone");
|
||||
let address_json: JsonValue = row.get("address");
|
||||
|
||||
let emails = serde_json::from_value::<Vec<EmailPersistenceDto>>(email_json)
|
||||
.map(emails_from_persistence)
|
||||
.unwrap_or_default();
|
||||
let phones = serde_json::from_value::<Vec<PhonePersistenceDto>>(phone_json)
|
||||
.map(phones_from_persistence)
|
||||
.unwrap_or_default();
|
||||
let addresses = serde_json::from_value::<Vec<AddressPersistenceDto>>(address_json)
|
||||
.map(addresses_from_persistence)
|
||||
.unwrap_or_default();
|
||||
|
||||
contacts.push(Contact::from_raw(
|
||||
row.get("id"),
|
||||
row.get("address_book_id"),
|
||||
row.get("uid"),
|
||||
row.get::<Option<String>, _>("full_name"),
|
||||
row.get::<Option<String>, _>("first_name"),
|
||||
row.get::<Option<String>, _>("last_name"),
|
||||
row.get::<Option<String>, _>("nickname"),
|
||||
emails,
|
||||
phones,
|
||||
addresses,
|
||||
row.get::<Option<String>, _>("organization"),
|
||||
row.get::<Option<String>, _>("title"),
|
||||
row.get::<Option<String>, _>("notes"),
|
||||
row.get::<Option<String>, _>("photo_url"),
|
||||
row.get("birthday"),
|
||||
row.get("anniversary"),
|
||||
row.get("vcard"),
|
||||
row.get("etag"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
));
|
||||
}
|
||||
Ok(contacts)
|
||||
}
|
||||
|
||||
async fn get_groups_for_contact(
|
||||
&self,
|
||||
contact_id: &Uuid,
|
||||
) -> ContactRepositoryResult<Vec<ContactGroup>> {
|
||||
let rows = sqlx::query(
|
||||
"SELECT g.id, g.address_book_id, g.name, g.created_at, g.updated_at FROM carddav.contact_groups g INNER JOIN carddav.group_memberships gm ON g.id = gm.group_id WHERE gm.contact_id = $1 ORDER BY g.name"
|
||||
)
|
||||
.bind(contact_id)
|
||||
.fetch_all(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| DomainError::new(ErrorKind::InternalError, "ContactGroup", format!("Failed to get groups for contact: {}", e)))?;
|
||||
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
ContactGroup::from_raw(
|
||||
row.get::<Uuid, _>("id"),
|
||||
row.get::<Uuid, _>("address_book_id"),
|
||||
row.get::<String, _>("name"),
|
||||
row.get("created_at"),
|
||||
row.get("updated_at"),
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,129 +1,129 @@
|
||||
//! Persistence DTOs for Contact entities
|
||||
//!
|
||||
//! These DTOs are used for JSONB serialization/deserialization in PostgreSQL.
|
||||
//! They mirror the domain entities but include serde traits required for persistence.
|
||||
//! This keeps the domain layer free of infrastructure concerns (serde dependency).
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::domain::entities::contact::{Email, Phone, Address};
|
||||
|
||||
/// Persistence DTO for Email - used for JSONB serialization
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EmailPersistenceDto {
|
||||
pub email: String,
|
||||
pub r#type: String,
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
||||
impl From<&Email> for EmailPersistenceDto {
|
||||
fn from(email: &Email) -> Self {
|
||||
Self {
|
||||
email: email.email.clone(),
|
||||
r#type: email.r#type.clone(),
|
||||
is_primary: email.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EmailPersistenceDto> for Email {
|
||||
fn from(dto: EmailPersistenceDto) -> Self {
|
||||
Self {
|
||||
email: dto.email,
|
||||
r#type: dto.r#type,
|
||||
is_primary: dto.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Persistence DTO for Phone - used for JSONB serialization
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PhonePersistenceDto {
|
||||
pub number: String,
|
||||
pub r#type: String,
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
||||
impl From<&Phone> for PhonePersistenceDto {
|
||||
fn from(phone: &Phone) -> Self {
|
||||
Self {
|
||||
number: phone.number.clone(),
|
||||
r#type: phone.r#type.clone(),
|
||||
is_primary: phone.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PhonePersistenceDto> for Phone {
|
||||
fn from(dto: PhonePersistenceDto) -> Self {
|
||||
Self {
|
||||
number: dto.number,
|
||||
r#type: dto.r#type,
|
||||
is_primary: dto.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Persistence DTO for Address - used for JSONB serialization
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AddressPersistenceDto {
|
||||
pub street: Option<String>,
|
||||
pub city: Option<String>,
|
||||
pub state: Option<String>,
|
||||
pub postal_code: Option<String>,
|
||||
pub country: Option<String>,
|
||||
pub r#type: String,
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
||||
impl From<&Address> for AddressPersistenceDto {
|
||||
fn from(addr: &Address) -> Self {
|
||||
Self {
|
||||
street: addr.street.clone(),
|
||||
city: addr.city.clone(),
|
||||
state: addr.state.clone(),
|
||||
postal_code: addr.postal_code.clone(),
|
||||
country: addr.country.clone(),
|
||||
r#type: addr.r#type.clone(),
|
||||
is_primary: addr.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AddressPersistenceDto> for Address {
|
||||
fn from(dto: AddressPersistenceDto) -> Self {
|
||||
Self {
|
||||
street: dto.street,
|
||||
city: dto.city,
|
||||
state: dto.state,
|
||||
postal_code: dto.postal_code,
|
||||
country: dto.country,
|
||||
r#type: dto.r#type,
|
||||
is_primary: dto.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper functions to convert collections
|
||||
pub fn emails_to_persistence(emails: &[Email]) -> Vec<EmailPersistenceDto> {
|
||||
emails.iter().map(EmailPersistenceDto::from).collect()
|
||||
}
|
||||
|
||||
pub fn emails_from_persistence(dtos: Vec<EmailPersistenceDto>) -> Vec<Email> {
|
||||
dtos.into_iter().map(Email::from).collect()
|
||||
}
|
||||
|
||||
pub fn phones_to_persistence(phones: &[Phone]) -> Vec<PhonePersistenceDto> {
|
||||
phones.iter().map(PhonePersistenceDto::from).collect()
|
||||
}
|
||||
|
||||
pub fn phones_from_persistence(dtos: Vec<PhonePersistenceDto>) -> Vec<Phone> {
|
||||
dtos.into_iter().map(Phone::from).collect()
|
||||
}
|
||||
|
||||
pub fn addresses_to_persistence(addresses: &[Address]) -> Vec<AddressPersistenceDto> {
|
||||
addresses.iter().map(AddressPersistenceDto::from).collect()
|
||||
}
|
||||
|
||||
pub fn addresses_from_persistence(dtos: Vec<AddressPersistenceDto>) -> Vec<Address> {
|
||||
dtos.into_iter().map(Address::from).collect()
|
||||
}
|
||||
//! Persistence DTOs for Contact entities
|
||||
//!
|
||||
//! These DTOs are used for JSONB serialization/deserialization in PostgreSQL.
|
||||
//! They mirror the domain entities but include serde traits required for persistence.
|
||||
//! This keeps the domain layer free of infrastructure concerns (serde dependency).
|
||||
|
||||
use crate::domain::entities::contact::{Address, Email, Phone};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Persistence DTO for Email - used for JSONB serialization
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EmailPersistenceDto {
|
||||
pub email: String,
|
||||
pub r#type: String,
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
||||
impl From<&Email> for EmailPersistenceDto {
|
||||
fn from(email: &Email) -> Self {
|
||||
Self {
|
||||
email: email.email.clone(),
|
||||
r#type: email.r#type.clone(),
|
||||
is_primary: email.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EmailPersistenceDto> for Email {
|
||||
fn from(dto: EmailPersistenceDto) -> Self {
|
||||
Self {
|
||||
email: dto.email,
|
||||
r#type: dto.r#type,
|
||||
is_primary: dto.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Persistence DTO for Phone - used for JSONB serialization
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PhonePersistenceDto {
|
||||
pub number: String,
|
||||
pub r#type: String,
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
||||
impl From<&Phone> for PhonePersistenceDto {
|
||||
fn from(phone: &Phone) -> Self {
|
||||
Self {
|
||||
number: phone.number.clone(),
|
||||
r#type: phone.r#type.clone(),
|
||||
is_primary: phone.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PhonePersistenceDto> for Phone {
|
||||
fn from(dto: PhonePersistenceDto) -> Self {
|
||||
Self {
|
||||
number: dto.number,
|
||||
r#type: dto.r#type,
|
||||
is_primary: dto.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Persistence DTO for Address - used for JSONB serialization
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AddressPersistenceDto {
|
||||
pub street: Option<String>,
|
||||
pub city: Option<String>,
|
||||
pub state: Option<String>,
|
||||
pub postal_code: Option<String>,
|
||||
pub country: Option<String>,
|
||||
pub r#type: String,
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
||||
impl From<&Address> for AddressPersistenceDto {
|
||||
fn from(addr: &Address) -> Self {
|
||||
Self {
|
||||
street: addr.street.clone(),
|
||||
city: addr.city.clone(),
|
||||
state: addr.state.clone(),
|
||||
postal_code: addr.postal_code.clone(),
|
||||
country: addr.country.clone(),
|
||||
r#type: addr.r#type.clone(),
|
||||
is_primary: addr.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AddressPersistenceDto> for Address {
|
||||
fn from(dto: AddressPersistenceDto) -> Self {
|
||||
Self {
|
||||
street: dto.street,
|
||||
city: dto.city,
|
||||
state: dto.state,
|
||||
postal_code: dto.postal_code,
|
||||
country: dto.country,
|
||||
r#type: dto.r#type,
|
||||
is_primary: dto.is_primary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper functions to convert collections
|
||||
pub fn emails_to_persistence(emails: &[Email]) -> Vec<EmailPersistenceDto> {
|
||||
emails.iter().map(EmailPersistenceDto::from).collect()
|
||||
}
|
||||
|
||||
pub fn emails_from_persistence(dtos: Vec<EmailPersistenceDto>) -> Vec<Email> {
|
||||
dtos.into_iter().map(Email::from).collect()
|
||||
}
|
||||
|
||||
pub fn phones_to_persistence(phones: &[Phone]) -> Vec<PhonePersistenceDto> {
|
||||
phones.iter().map(PhonePersistenceDto::from).collect()
|
||||
}
|
||||
|
||||
pub fn phones_from_persistence(dtos: Vec<PhonePersistenceDto>) -> Vec<Phone> {
|
||||
dtos.into_iter().map(Phone::from).collect()
|
||||
}
|
||||
|
||||
pub fn addresses_to_persistence(addresses: &[Address]) -> Vec<AddressPersistenceDto> {
|
||||
addresses.iter().map(AddressPersistenceDto::from).collect()
|
||||
}
|
||||
|
||||
pub fn addresses_from_persistence(dtos: Vec<AddressPersistenceDto>) -> Vec<Address> {
|
||||
dtos.into_iter().map(Address::from).collect()
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use async_trait::async_trait;
|
||||
use chrono::Utc;
|
||||
use serde_json::Value as JsonValue;
|
||||
use sqlx::{PgPool, Row, types::Uuid};
|
||||
use std::sync::Arc;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use super::contact_persistence_dto::{
|
||||
AddressPersistenceDto, EmailPersistenceDto, PhonePersistenceDto, addresses_from_persistence,
|
||||
addresses_to_persistence, emails_from_persistence, emails_to_persistence,
|
||||
phones_from_persistence, phones_to_persistence,
|
||||
};
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::entities::contact::Contact;
|
||||
use crate::domain::repositories::contact_repository::{ContactRepository, ContactRepositoryResult};
|
||||
use crate::common::errors::DomainError;
|
||||
use super::contact_persistence_dto::{
|
||||
emails_to_persistence, phones_to_persistence, addresses_to_persistence,
|
||||
emails_from_persistence, phones_from_persistence, addresses_from_persistence,
|
||||
EmailPersistenceDto, PhonePersistenceDto, AddressPersistenceDto,
|
||||
};
|
||||
|
||||
pub struct ContactPgRepository {
|
||||
pool: Arc<PgPool>,
|
||||
@@ -70,11 +70,11 @@ impl ContactRepository for ContactPgRepository {
|
||||
let email_dtos = emails_to_persistence(contact.email());
|
||||
let phone_dtos = phones_to_persistence(contact.phone());
|
||||
let address_dtos = addresses_to_persistence(contact.address());
|
||||
|
||||
|
||||
let email_json = serde_json::to_value(&email_dtos).unwrap_or(JsonValue::Null);
|
||||
let phone_json = serde_json::to_value(&phone_dtos).unwrap_or(JsonValue::Null);
|
||||
let address_json = serde_json::to_value(&address_dtos).unwrap_or(JsonValue::Null);
|
||||
|
||||
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO carddav.contacts (
|
||||
@@ -90,7 +90,7 @@ impl ContactRepository for ContactPgRepository {
|
||||
id, address_book_id, uid, full_name, first_name, last_name, nickname,
|
||||
email, phone, address, organization, title, notes, photo_url,
|
||||
birthday, anniversary, vcard, etag, created_at, updated_at
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(contact.id())
|
||||
.bind(contact.address_book_id())
|
||||
@@ -125,15 +125,15 @@ impl ContactRepository for ContactPgRepository {
|
||||
let email_dtos = emails_to_persistence(contact.email());
|
||||
let phone_dtos = phones_to_persistence(contact.phone());
|
||||
let address_dtos = addresses_to_persistence(contact.address());
|
||||
|
||||
|
||||
let email_json = serde_json::to_value(&email_dtos).unwrap_or(JsonValue::Null);
|
||||
let phone_json = serde_json::to_value(&phone_dtos).unwrap_or(JsonValue::Null);
|
||||
let address_json = serde_json::to_value(&address_dtos).unwrap_or(JsonValue::Null);
|
||||
|
||||
|
||||
// Create a clone of the contact with the updated timestamp
|
||||
let mut updated_contact = contact.clone();
|
||||
updated_contact.set_updated_at(now);
|
||||
|
||||
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
UPDATE carddav.contacts
|
||||
@@ -159,7 +159,7 @@ impl ContactRepository for ContactPgRepository {
|
||||
id, address_book_id, uid, full_name, first_name, last_name, nickname,
|
||||
email, phone, address, organization, title, notes, photo_url,
|
||||
birthday, anniversary, vcard, etag, created_at, updated_at
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(updated_contact.full_name_owned())
|
||||
.bind(updated_contact.first_name_owned())
|
||||
@@ -190,7 +190,7 @@ impl ContactRepository for ContactPgRepository {
|
||||
r#"
|
||||
DELETE FROM carddav.contacts
|
||||
WHERE id = $1
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.execute(&*self.pool)
|
||||
@@ -209,7 +209,7 @@ impl ContactRepository for ContactPgRepository {
|
||||
birthday, anniversary, vcard, etag, created_at, updated_at
|
||||
FROM carddav.contacts
|
||||
WHERE id = $1
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(&*self.pool)
|
||||
@@ -222,7 +222,11 @@ impl ContactRepository for ContactPgRepository {
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_contact_by_uid(&self, address_book_id: &Uuid, uid: &str) -> ContactRepositoryResult<Option<Contact>> {
|
||||
async fn get_contact_by_uid(
|
||||
&self,
|
||||
address_book_id: &Uuid,
|
||||
uid: &str,
|
||||
) -> ContactRepositoryResult<Option<Contact>> {
|
||||
let row_opt = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
@@ -231,7 +235,7 @@ impl ContactRepository for ContactPgRepository {
|
||||
birthday, anniversary, vcard, etag, created_at, updated_at
|
||||
FROM carddav.contacts
|
||||
WHERE address_book_id = $1 AND uid = $2
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(address_book_id)
|
||||
.bind(uid)
|
||||
@@ -245,7 +249,10 @@ impl ContactRepository for ContactPgRepository {
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_contacts_by_address_book(&self, address_book_id: &Uuid) -> ContactRepositoryResult<Vec<Contact>> {
|
||||
async fn get_contacts_by_address_book(
|
||||
&self,
|
||||
address_book_id: &Uuid,
|
||||
) -> ContactRepositoryResult<Vec<Contact>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
@@ -255,12 +262,14 @@ impl ContactRepository for ContactPgRepository {
|
||||
FROM carddav.contacts
|
||||
WHERE address_book_id = $1
|
||||
ORDER BY full_name, first_name, last_name
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(address_book_id)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get contacts by address book: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to get contacts by address book: {}", e))
|
||||
})?;
|
||||
|
||||
let mut contacts = Vec::new();
|
||||
for row in &rows {
|
||||
@@ -271,7 +280,7 @@ impl ContactRepository for ContactPgRepository {
|
||||
|
||||
async fn get_contacts_by_email(&self, email: &str) -> ContactRepositoryResult<Vec<Contact>> {
|
||||
let search_pattern = format!("%{}%", email);
|
||||
|
||||
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
@@ -281,12 +290,14 @@ impl ContactRepository for ContactPgRepository {
|
||||
FROM carddav.contacts
|
||||
WHERE email::text ILIKE $1
|
||||
ORDER BY full_name, first_name, last_name
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(&search_pattern)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get contacts by email: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to get contacts by email: {}", e))
|
||||
})?;
|
||||
|
||||
let mut contacts = Vec::new();
|
||||
for row in &rows {
|
||||
@@ -295,7 +306,10 @@ impl ContactRepository for ContactPgRepository {
|
||||
Ok(contacts)
|
||||
}
|
||||
|
||||
async fn get_contacts_by_group(&self, group_id: &Uuid) -> ContactRepositoryResult<Vec<Contact>> {
|
||||
async fn get_contacts_by_group(
|
||||
&self,
|
||||
group_id: &Uuid,
|
||||
) -> ContactRepositoryResult<Vec<Contact>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
@@ -306,12 +320,14 @@ impl ContactRepository for ContactPgRepository {
|
||||
INNER JOIN carddav.group_memberships m ON c.id = m.contact_id
|
||||
WHERE m.group_id = $1
|
||||
ORDER BY c.full_name, c.first_name, c.last_name
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(group_id)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(|e| DomainError::database_error(format!("Failed to get contacts by group: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
DomainError::database_error(format!("Failed to get contacts by group: {}", e))
|
||||
})?;
|
||||
|
||||
let mut contacts = Vec::new();
|
||||
for row in &rows {
|
||||
@@ -320,9 +336,13 @@ impl ContactRepository for ContactPgRepository {
|
||||
Ok(contacts)
|
||||
}
|
||||
|
||||
async fn search_contacts(&self, address_book_id: &Uuid, query: &str) -> ContactRepositoryResult<Vec<Contact>> {
|
||||
async fn search_contacts(
|
||||
&self,
|
||||
address_book_id: &Uuid,
|
||||
query: &str,
|
||||
) -> ContactRepositoryResult<Vec<Contact>> {
|
||||
let search_pattern = format!("%{}%", query);
|
||||
|
||||
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
@@ -341,7 +361,7 @@ impl ContactRepository for ContactPgRepository {
|
||||
OR organization ILIKE $2
|
||||
)
|
||||
ORDER BY full_name, first_name, last_name
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(address_book_id)
|
||||
.bind(&search_pattern)
|
||||
@@ -355,4 +375,4 @@ impl ContactRepository for ContactPgRepository {
|
||||
}
|
||||
Ok(contacts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use sqlx::{PgPool, Row};
|
||||
use std::sync::Arc;
|
||||
use tracing::error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::application::dtos::favorites_dto::FavoriteItemDto;
|
||||
use crate::application::ports::favorites_ports::FavoritesRepositoryPort;
|
||||
use crate::common::errors::{Result, DomainError, ErrorKind};
|
||||
use crate::common::errors::{DomainError, ErrorKind, Result};
|
||||
|
||||
/// PostgreSQL implementation of the favorites persistence port.
|
||||
pub struct FavoritesPgRepository {
|
||||
@@ -42,7 +42,11 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Database error fetching favorites: {}", e);
|
||||
DomainError::new(ErrorKind::InternalError, "Favorites", format!("Failed to fetch favorites: {}", e))
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"Favorites",
|
||||
format!("Failed to fetch favorites: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
let favorites = rows
|
||||
@@ -76,7 +80,11 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Database error adding favorite: {}", e);
|
||||
DomainError::new(ErrorKind::InternalError, "Favorites", format!("Failed to add to favorites: {}", e))
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"Favorites",
|
||||
format!("Failed to add to favorites: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
@@ -98,7 +106,11 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Database error removing favorite: {}", e);
|
||||
DomainError::new(ErrorKind::InternalError, "Favorites", format!("Failed to remove from favorites: {}", e))
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"Favorites",
|
||||
format!("Failed to remove from favorites: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
@@ -122,7 +134,11 @@ impl FavoritesRepositoryPort for FavoritesPgRepository {
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Database error checking favorite status: {}", e);
|
||||
DomainError::new(ErrorKind::InternalError, "Favorites", format!("Failed to check favorite status: {}", e))
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"Favorites",
|
||||
format!("Failed to check favorite status: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(row.try_get("is_favorite").unwrap_or(false))
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
mod address_book_pg_repository;
|
||||
mod calendar_pg_repository;
|
||||
mod calendar_event_pg_repository;
|
||||
mod contact_pg_repository;
|
||||
mod calendar_pg_repository;
|
||||
mod contact_group_pg_repository;
|
||||
mod contact_persistence_dto;
|
||||
mod contact_pg_repository;
|
||||
mod favorites_pg_repository;
|
||||
mod recent_items_pg_repository;
|
||||
mod session_pg_repository;
|
||||
@@ -12,11 +12,11 @@ mod transaction_utils;
|
||||
mod user_pg_repository;
|
||||
|
||||
pub use address_book_pg_repository::AddressBookPgRepository;
|
||||
pub use calendar_pg_repository::CalendarPgRepository;
|
||||
pub use calendar_event_pg_repository::CalendarEventPgRepository;
|
||||
pub use contact_pg_repository::ContactPgRepository;
|
||||
pub use calendar_pg_repository::CalendarPgRepository;
|
||||
pub use contact_group_pg_repository::ContactGroupPgRepository;
|
||||
pub use contact_persistence_dto::*;
|
||||
pub use contact_pg_repository::ContactPgRepository;
|
||||
pub use favorites_pg_repository::FavoritesPgRepository;
|
||||
pub use recent_items_pg_repository::RecentItemsPgRepository;
|
||||
pub use session_pg_repository::SessionPgRepository;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use sqlx::{PgPool, Row};
|
||||
use std::sync::Arc;
|
||||
use tracing::error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::application::dtos::recent_dto::RecentItemDto;
|
||||
use crate::application::ports::recent_ports::RecentItemsRepositoryPort;
|
||||
use crate::common::errors::{Result, DomainError, ErrorKind};
|
||||
use crate::common::errors::{DomainError, ErrorKind, Result};
|
||||
|
||||
/// PostgreSQL implementation of the recent items persistence port.
|
||||
pub struct RecentItemsPgRepository {
|
||||
@@ -44,7 +44,11 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository {
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Database error fetching recent items: {}", e);
|
||||
DomainError::new(ErrorKind::InternalError, "RecentItems", format!("Failed to fetch recent items: {}", e))
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"RecentItems",
|
||||
format!("Failed to fetch recent items: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
let items = rows
|
||||
@@ -79,7 +83,11 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository {
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Database error upserting recent item access: {}", e);
|
||||
DomainError::new(ErrorKind::InternalError, "RecentItems", format!("Failed to record item access: {}", e))
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"RecentItems",
|
||||
format!("Failed to record item access: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
@@ -101,7 +109,11 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository {
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Database error removing recent item: {}", e);
|
||||
DomainError::new(ErrorKind::InternalError, "RecentItems", format!("Failed to remove recent item: {}", e))
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"RecentItems",
|
||||
format!("Failed to remove recent item: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(result.rows_affected() > 0)
|
||||
@@ -121,7 +133,11 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository {
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Database error clearing recent items: {}", e);
|
||||
DomainError::new(ErrorKind::InternalError, "RecentItems", format!("Failed to clear recent items: {}", e))
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"RecentItems",
|
||||
format!("Failed to clear recent items: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
@@ -147,7 +163,11 @@ impl RecentItemsRepositoryPort for RecentItemsPgRepository {
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("Database error pruning old recent items: {}", e);
|
||||
DomainError::new(ErrorKind::InternalError, "RecentItems", format!("Failed to prune recent items: {}", e))
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"RecentItems",
|
||||
format!("Failed to prune recent items: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
use async_trait::async_trait;
|
||||
use sqlx::{PgPool, Row};
|
||||
use std::sync::Arc;
|
||||
use chrono::Utc;
|
||||
use futures::future::BoxFuture;
|
||||
use sqlx::{PgPool, Row};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::domain::entities::session::Session;
|
||||
use crate::domain::repositories::session_repository::{SessionRepository, SessionRepositoryError, SessionRepositoryResult};
|
||||
use crate::application::ports::auth_ports::SessionStoragePort;
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::entities::session::Session;
|
||||
use crate::domain::repositories::session_repository::{
|
||||
SessionRepository, SessionRepositoryError, SessionRepositoryResult,
|
||||
};
|
||||
use crate::infrastructure::repositories::pg::transaction_utils::with_transaction;
|
||||
|
||||
// Implement From<sqlx::Error> for SessionRepositoryError to allow automatic conversions
|
||||
@@ -25,16 +27,14 @@ impl SessionPgRepository {
|
||||
pub fn new(pool: Arc<PgPool>) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
|
||||
// Helper method to map SQL errors to domain errors
|
||||
pub fn map_sqlx_error(err: sqlx::Error) -> SessionRepositoryError {
|
||||
match err {
|
||||
sqlx::Error::RowNotFound => {
|
||||
SessionRepositoryError::NotFound("Session not found".to_string())
|
||||
},
|
||||
_ => SessionRepositoryError::DatabaseError(
|
||||
format!("Database error: {}", err)
|
||||
),
|
||||
}
|
||||
_ => SessionRepositoryError::DatabaseError(format!("Database error: {}", err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,65 +45,66 @@ impl SessionRepository for SessionPgRepository {
|
||||
async fn create_session(&self, session: Session) -> SessionRepositoryResult<Session> {
|
||||
// Create a copy of the session for the closure
|
||||
let session_clone = session.clone();
|
||||
|
||||
with_transaction(
|
||||
&self.pool,
|
||||
"create_session",
|
||||
|tx| {
|
||||
Box::pin(async move {
|
||||
// Insert the session
|
||||
sqlx::query(
|
||||
r#"
|
||||
|
||||
with_transaction(&self.pool, "create_session", |tx| {
|
||||
Box::pin(async move {
|
||||
// Insert the session
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO auth.sessions (
|
||||
id, user_id, refresh_token, expires_at,
|
||||
ip_address, user_agent, created_at, revoked
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8
|
||||
)
|
||||
"#
|
||||
)
|
||||
.bind(session_clone.id())
|
||||
.bind(session_clone.user_id())
|
||||
.bind(session_clone.refresh_token())
|
||||
.bind(session_clone.expires_at())
|
||||
.bind(session_clone.ip_address())
|
||||
.bind(session_clone.user_agent())
|
||||
.bind(session_clone.created_at())
|
||||
.bind(session_clone.is_revoked())
|
||||
.execute(&mut **tx)
|
||||
.await
|
||||
.map_err(Self::map_sqlx_error)?;
|
||||
|
||||
// Optionally, update the user's last login
|
||||
// within the same transaction
|
||||
sqlx::query(
|
||||
r#"
|
||||
"#,
|
||||
)
|
||||
.bind(session_clone.id())
|
||||
.bind(session_clone.user_id())
|
||||
.bind(session_clone.refresh_token())
|
||||
.bind(session_clone.expires_at())
|
||||
.bind(session_clone.ip_address())
|
||||
.bind(session_clone.user_agent())
|
||||
.bind(session_clone.created_at())
|
||||
.bind(session_clone.is_revoked())
|
||||
.execute(&mut **tx)
|
||||
.await
|
||||
.map_err(Self::map_sqlx_error)?;
|
||||
|
||||
// Optionally, update the user's last login
|
||||
// within the same transaction
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE auth.users
|
||||
SET last_login_at = NOW(), updated_at = NOW()
|
||||
WHERE id = $1
|
||||
"#
|
||||
)
|
||||
.bind(session_clone.user_id())
|
||||
.execute(&mut **tx)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
// Convert the error but without interrupting session
|
||||
// creation if the update fails
|
||||
tracing::warn!("Could not update last_login_at for user {}: {}",
|
||||
session_clone.user_id(), e);
|
||||
SessionRepositoryError::DatabaseError(format!(
|
||||
"Session created but could not update last_login_at: {}", e
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(session_clone)
|
||||
}) as BoxFuture<'_, SessionRepositoryResult<Session>>
|
||||
}
|
||||
).await?;
|
||||
|
||||
"#,
|
||||
)
|
||||
.bind(session_clone.user_id())
|
||||
.execute(&mut **tx)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
// Convert the error but without interrupting session
|
||||
// creation if the update fails
|
||||
tracing::warn!(
|
||||
"Could not update last_login_at for user {}: {}",
|
||||
session_clone.user_id(),
|
||||
e
|
||||
);
|
||||
SessionRepositoryError::DatabaseError(format!(
|
||||
"Session created but could not update last_login_at: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(session_clone)
|
||||
}) as BoxFuture<'_, SessionRepositoryResult<Session>>
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
|
||||
/// Gets a session by ID
|
||||
async fn get_session_by_id(&self, id: &str) -> SessionRepositoryResult<Session> {
|
||||
let row = sqlx::query(
|
||||
@@ -113,7 +114,7 @@ impl SessionRepository for SessionPgRepository {
|
||||
ip_address, user_agent, created_at, revoked
|
||||
FROM auth.sessions
|
||||
WHERE id = $1
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_one(&*self.pool)
|
||||
@@ -131,9 +132,12 @@ impl SessionRepository for SessionPgRepository {
|
||||
row.get("revoked"),
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
/// Gets a session by refresh token
|
||||
async fn get_session_by_refresh_token(&self, refresh_token: &str) -> SessionRepositoryResult<Session> {
|
||||
async fn get_session_by_refresh_token(
|
||||
&self,
|
||||
refresh_token: &str,
|
||||
) -> SessionRepositoryResult<Session> {
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
@@ -141,7 +145,7 @@ impl SessionRepository for SessionPgRepository {
|
||||
ip_address, user_agent, created_at, revoked
|
||||
FROM auth.sessions
|
||||
WHERE refresh_token = $1
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(refresh_token)
|
||||
.fetch_one(&*self.pool)
|
||||
@@ -159,9 +163,12 @@ impl SessionRepository for SessionPgRepository {
|
||||
row.get("revoked"),
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
/// Gets all sessions for a user
|
||||
async fn get_sessions_by_user_id(&self, user_id: &str) -> SessionRepositoryResult<Vec<Session>> {
|
||||
async fn get_sessions_by_user_id(
|
||||
&self,
|
||||
user_id: &str,
|
||||
) -> SessionRepositoryResult<Vec<Session>> {
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
@@ -170,14 +177,15 @@ impl SessionRepository for SessionPgRepository {
|
||||
FROM auth.sessions
|
||||
WHERE user_id = $1
|
||||
ORDER BY created_at DESC
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(Self::map_sqlx_error)?;
|
||||
|
||||
let sessions = rows.into_iter()
|
||||
let sessions = rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
Session::from_raw(
|
||||
row.get("id"),
|
||||
@@ -194,90 +202,84 @@ impl SessionRepository for SessionPgRepository {
|
||||
|
||||
Ok(sessions)
|
||||
}
|
||||
|
||||
|
||||
/// Revokes a specific session using a transaction
|
||||
async fn revoke_session(&self, session_id: &str) -> SessionRepositoryResult<()> {
|
||||
let id = session_id.to_string(); // Clone for use in closure
|
||||
|
||||
with_transaction(
|
||||
&self.pool,
|
||||
"revoke_session",
|
||||
|tx| {
|
||||
Box::pin(async move {
|
||||
// Revoke the session
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
|
||||
with_transaction(&self.pool, "revoke_session", |tx| {
|
||||
Box::pin(async move {
|
||||
// Revoke the session
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
UPDATE auth.sessions
|
||||
SET revoked = true
|
||||
WHERE id = $1
|
||||
RETURNING user_id
|
||||
"#
|
||||
)
|
||||
.bind(&id)
|
||||
.fetch_optional(&mut **tx)
|
||||
.await
|
||||
.map_err(Self::map_sqlx_error)?;
|
||||
|
||||
// If we found the session, we can log a security event
|
||||
if let Some(row) = result {
|
||||
let user_id: String = row.try_get("user_id").unwrap_or_default();
|
||||
|
||||
// Log security event (in a security table)
|
||||
// This is optional but shows how additional operations
|
||||
// can be performed in the same transaction
|
||||
tracing::info!("Session with ID {} for user {} revoked", id, user_id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}) as BoxFuture<'_, SessionRepositoryResult<()>>
|
||||
}
|
||||
).await
|
||||
"#,
|
||||
)
|
||||
.bind(&id)
|
||||
.fetch_optional(&mut **tx)
|
||||
.await
|
||||
.map_err(Self::map_sqlx_error)?;
|
||||
|
||||
// If we found the session, we can log a security event
|
||||
if let Some(row) = result {
|
||||
let user_id: String = row.try_get("user_id").unwrap_or_default();
|
||||
|
||||
// Log security event (in a security table)
|
||||
// This is optional but shows how additional operations
|
||||
// can be performed in the same transaction
|
||||
tracing::info!("Session with ID {} for user {} revoked", id, user_id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}) as BoxFuture<'_, SessionRepositoryResult<()>>
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
/// Revokes all sessions for a user using a transaction
|
||||
async fn revoke_all_user_sessions(&self, user_id: &str) -> SessionRepositoryResult<u64> {
|
||||
let user_id_clone = user_id.to_string(); // Clone for use in closure
|
||||
|
||||
with_transaction(
|
||||
&self.pool,
|
||||
"revoke_all_user_sessions",
|
||||
|tx| {
|
||||
Box::pin(async move {
|
||||
// Revoke all sessions for the user
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
|
||||
with_transaction(&self.pool, "revoke_all_user_sessions", |tx| {
|
||||
Box::pin(async move {
|
||||
// Revoke all sessions for the user
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
UPDATE auth.sessions
|
||||
SET revoked = true
|
||||
WHERE user_id = $1 AND revoked = false
|
||||
"#
|
||||
)
|
||||
.bind(&user_id_clone)
|
||||
.execute(&mut **tx)
|
||||
.await
|
||||
.map_err(Self::map_sqlx_error)?;
|
||||
|
||||
let affected = result.rows_affected();
|
||||
|
||||
// Log security event
|
||||
if affected > 0 {
|
||||
tracing::info!("Revoked {} sessions for user {}", affected, user_id_clone);
|
||||
}
|
||||
|
||||
Ok(affected)
|
||||
}) as BoxFuture<'_, SessionRepositoryResult<u64>>
|
||||
}
|
||||
).await
|
||||
"#,
|
||||
)
|
||||
.bind(&user_id_clone)
|
||||
.execute(&mut **tx)
|
||||
.await
|
||||
.map_err(Self::map_sqlx_error)?;
|
||||
|
||||
let affected = result.rows_affected();
|
||||
|
||||
// Log security event
|
||||
if affected > 0 {
|
||||
tracing::info!("Revoked {} sessions for user {}", affected, user_id_clone);
|
||||
}
|
||||
|
||||
Ok(affected)
|
||||
}) as BoxFuture<'_, SessionRepositoryResult<u64>>
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
/// Deletes expired sessions
|
||||
async fn delete_expired_sessions(&self) -> SessionRepositoryResult<u64> {
|
||||
let now = Utc::now();
|
||||
|
||||
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
DELETE FROM auth.sessions
|
||||
WHERE expires_at < $1
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(now)
|
||||
.execute(&*self.pool)
|
||||
@@ -292,22 +294,29 @@ impl SessionRepository for SessionPgRepository {
|
||||
#[async_trait]
|
||||
impl SessionStoragePort for SessionPgRepository {
|
||||
async fn create_session(&self, session: Session) -> Result<Session, DomainError> {
|
||||
SessionRepository::create_session(self, session).await.map_err(DomainError::from)
|
||||
SessionRepository::create_session(self, session)
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn get_session_by_refresh_token(&self, refresh_token: &str) -> Result<Session, DomainError> {
|
||||
|
||||
async fn get_session_by_refresh_token(
|
||||
&self,
|
||||
refresh_token: &str,
|
||||
) -> Result<Session, DomainError> {
|
||||
SessionRepository::get_session_by_refresh_token(self, refresh_token)
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
|
||||
async fn revoke_session(&self, session_id: &str) -> Result<(), DomainError> {
|
||||
SessionRepository::revoke_session(self, session_id).await.map_err(DomainError::from)
|
||||
SessionRepository::revoke_session(self, session_id)
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
|
||||
async fn revoke_all_user_sessions(&self, user_id: &str) -> Result<u64, DomainError> {
|
||||
SessionRepository::revoke_all_user_sessions(self, user_id)
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,88 +1,102 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use async_trait::async_trait;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::domain::repositories::settings_repository::SettingsRepository;
|
||||
use crate::common::errors::{DomainError, ErrorKind};
|
||||
|
||||
pub struct SettingsPgRepository {
|
||||
pool: Arc<PgPool>,
|
||||
}
|
||||
|
||||
impl SettingsPgRepository {
|
||||
pub fn new(pool: Arc<PgPool>) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SettingsRepository for SettingsPgRepository {
|
||||
async fn get(&self, key: &str) -> Result<Option<String>, DomainError> {
|
||||
let row = sqlx::query_scalar::<_, String>(
|
||||
"SELECT value FROM auth.admin_settings WHERE key = $1"
|
||||
)
|
||||
.bind(key)
|
||||
.fetch_optional(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| DomainError::new(
|
||||
ErrorKind::InternalError, "Settings", format!("DB error: {}", e),
|
||||
))?;
|
||||
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
async fn get_by_category(&self, category: &str) -> Result<HashMap<String, String>, DomainError> {
|
||||
let rows = sqlx::query_as::<_, (String, String)>(
|
||||
"SELECT key, value FROM auth.admin_settings WHERE category = $1"
|
||||
)
|
||||
.bind(category)
|
||||
.fetch_all(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| DomainError::new(
|
||||
ErrorKind::InternalError, "Settings", format!("DB error: {}", e),
|
||||
))?;
|
||||
|
||||
Ok(rows.into_iter().collect())
|
||||
}
|
||||
|
||||
async fn set(
|
||||
&self,
|
||||
key: &str,
|
||||
value: &str,
|
||||
category: &str,
|
||||
is_secret: bool,
|
||||
updated_by: Option<&str>,
|
||||
) -> Result<(), DomainError> {
|
||||
sqlx::query(
|
||||
"INSERT INTO auth.admin_settings (key, value, category, is_secret, updated_by, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, NOW())
|
||||
ON CONFLICT (key) DO UPDATE
|
||||
SET value = $2, category = $3, is_secret = $4, updated_by = $5, updated_at = NOW()"
|
||||
)
|
||||
.bind(key)
|
||||
.bind(value)
|
||||
.bind(category)
|
||||
.bind(is_secret)
|
||||
.bind(updated_by)
|
||||
.execute(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| DomainError::new(
|
||||
ErrorKind::InternalError, "Settings", format!("DB error: {}", e),
|
||||
))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete(&self, key: &str) -> Result<(), DomainError> {
|
||||
sqlx::query("DELETE FROM auth.admin_settings WHERE key = $1")
|
||||
.bind(key)
|
||||
.execute(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| DomainError::new(
|
||||
ErrorKind::InternalError, "Settings", format!("DB error: {}", e),
|
||||
))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
use async_trait::async_trait;
|
||||
use sqlx::PgPool;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::common::errors::{DomainError, ErrorKind};
|
||||
use crate::domain::repositories::settings_repository::SettingsRepository;
|
||||
|
||||
pub struct SettingsPgRepository {
|
||||
pool: Arc<PgPool>,
|
||||
}
|
||||
|
||||
impl SettingsPgRepository {
|
||||
pub fn new(pool: Arc<PgPool>) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SettingsRepository for SettingsPgRepository {
|
||||
async fn get(&self, key: &str) -> Result<Option<String>, DomainError> {
|
||||
let row =
|
||||
sqlx::query_scalar::<_, String>("SELECT value FROM auth.admin_settings WHERE key = $1")
|
||||
.bind(key)
|
||||
.fetch_optional(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"Settings",
|
||||
format!("DB error: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
async fn get_by_category(
|
||||
&self,
|
||||
category: &str,
|
||||
) -> Result<HashMap<String, String>, DomainError> {
|
||||
let rows = sqlx::query_as::<_, (String, String)>(
|
||||
"SELECT key, value FROM auth.admin_settings WHERE category = $1",
|
||||
)
|
||||
.bind(category)
|
||||
.fetch_all(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"Settings",
|
||||
format!("DB error: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(rows.into_iter().collect())
|
||||
}
|
||||
|
||||
async fn set(
|
||||
&self,
|
||||
key: &str,
|
||||
value: &str,
|
||||
category: &str,
|
||||
is_secret: bool,
|
||||
updated_by: Option<&str>,
|
||||
) -> Result<(), DomainError> {
|
||||
sqlx::query(
|
||||
"INSERT INTO auth.admin_settings (key, value, category, is_secret, updated_by, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, NOW())
|
||||
ON CONFLICT (key) DO UPDATE
|
||||
SET value = $2, category = $3, is_secret = $4, updated_by = $5, updated_at = NOW()"
|
||||
)
|
||||
.bind(key)
|
||||
.bind(value)
|
||||
.bind(category)
|
||||
.bind(is_secret)
|
||||
.bind(updated_by)
|
||||
.execute(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| DomainError::new(
|
||||
ErrorKind::InternalError, "Settings", format!("DB error: {}", e),
|
||||
))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete(&self, key: &str) -> Result<(), DomainError> {
|
||||
sqlx::query("DELETE FROM auth.admin_settings WHERE key = $1")
|
||||
.bind(key)
|
||||
.execute(self.pool.as_ref())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"Settings",
|
||||
format!("DB error: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use sqlx::{PgPool, Transaction, Postgres, Error as SqlxError};
|
||||
use sqlx::{Error as SqlxError, PgPool, Postgres, Transaction};
|
||||
use std::sync::Arc;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
@@ -13,17 +13,19 @@ pub async fn with_transaction<F, T, E>(
|
||||
operation: F,
|
||||
) -> Result<T, E>
|
||||
where
|
||||
F: for<'c> FnOnce(&'c mut Transaction<'_, Postgres>) -> futures::future::BoxFuture<'c, Result<T, E>>,
|
||||
F: for<'c> FnOnce(
|
||||
&'c mut Transaction<'_, Postgres>,
|
||||
) -> futures::future::BoxFuture<'c, Result<T, E>>,
|
||||
E: From<SqlxError> + std::fmt::Display,
|
||||
{
|
||||
debug!("Starting database transaction for: {}", operation_name);
|
||||
|
||||
|
||||
// Begin transaction
|
||||
let mut tx = pool.begin().await.map_err(|e| {
|
||||
error!("Failed to begin transaction for {}: {}", operation_name, e);
|
||||
E::from(e)
|
||||
})?;
|
||||
|
||||
|
||||
// Execute the operation within the transaction
|
||||
match operation(&mut tx).await {
|
||||
Ok(result) => {
|
||||
@@ -32,17 +34,20 @@ where
|
||||
Ok(_) => {
|
||||
debug!("Transaction committed successfully for: {}", operation_name);
|
||||
Ok(result)
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to commit transaction for {}: {}", operation_name, e);
|
||||
Err(E::from(e))
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
// If operation fails, rollback the transaction
|
||||
if let Err(rollback_err) = tx.rollback().await {
|
||||
error!("Failed to rollback transaction for {}: {}", operation_name, rollback_err);
|
||||
error!(
|
||||
"Failed to rollback transaction for {}: {}",
|
||||
operation_name, rollback_err
|
||||
);
|
||||
// Still return the original error
|
||||
} else {
|
||||
info!("Transaction rolled back for {}: {}", operation_name, e);
|
||||
@@ -50,4 +55,4 @@ where
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use async_trait::async_trait;
|
||||
use futures::future::BoxFuture;
|
||||
use sqlx::{PgPool, Row};
|
||||
use std::sync::Arc;
|
||||
use futures::future::BoxFuture;
|
||||
|
||||
use crate::domain::entities::user::{User, UserRole};
|
||||
use crate::domain::repositories::user_repository::{UserRepository, UserRepositoryError, UserRepositoryResult, StorageStats};
|
||||
use crate::application::ports::auth_ports::UserStoragePort;
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::entities::user::{User, UserRole};
|
||||
use crate::domain::repositories::user_repository::{
|
||||
StorageStats, UserRepository, UserRepositoryError, UserRepositoryResult,
|
||||
};
|
||||
use crate::infrastructure::repositories::pg::transaction_utils::with_transaction;
|
||||
|
||||
// Implement From<sqlx::Error> for UserRepositoryError to allow automatic conversions
|
||||
@@ -24,28 +26,20 @@ impl UserPgRepository {
|
||||
pub fn new(pool: Arc<PgPool>) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
|
||||
// Helper method to map SQL errors to domain errors
|
||||
pub fn map_sqlx_error(err: sqlx::Error) -> UserRepositoryError {
|
||||
match err {
|
||||
sqlx::Error::RowNotFound => {
|
||||
UserRepositoryError::NotFound("User not found".to_string())
|
||||
},
|
||||
sqlx::Error::RowNotFound => UserRepositoryError::NotFound("User not found".to_string()),
|
||||
sqlx::Error::Database(db_err) => {
|
||||
if db_err.code().is_some_and(|code| code == "23505") {
|
||||
// PostgreSQL uniqueness violation code
|
||||
UserRepositoryError::AlreadyExists(
|
||||
"User or email already exists".to_string()
|
||||
)
|
||||
UserRepositoryError::AlreadyExists("User or email already exists".to_string())
|
||||
} else {
|
||||
UserRepositoryError::DatabaseError(
|
||||
format!("Database error: {}", db_err)
|
||||
)
|
||||
UserRepositoryError::DatabaseError(format!("Database error: {}", db_err))
|
||||
}
|
||||
},
|
||||
_ => UserRepositoryError::DatabaseError(
|
||||
format!("Database error: {}", err)
|
||||
),
|
||||
}
|
||||
_ => UserRepositoryError::DatabaseError(format!("Database error: {}", err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,21 +50,18 @@ impl UserRepository for UserPgRepository {
|
||||
async fn create_user(&self, user: User) -> UserRepositoryResult<User> {
|
||||
// Create a copy of the user for the closure
|
||||
let user_clone = user.clone();
|
||||
|
||||
with_transaction(
|
||||
&self.pool,
|
||||
"create_user",
|
||||
|tx| {
|
||||
// We need to move the closure into a BoxFuture to return inside
|
||||
// the with_transaction call
|
||||
Box::pin(async move {
|
||||
// Use getters to extract the values
|
||||
// Convert user.role() to string to pass it as plain text
|
||||
let role_str = user_clone.role().to_string();
|
||||
|
||||
// Modify the SQL to do an explicit cast to the auth.userrole type
|
||||
let _result = sqlx::query(
|
||||
r#"
|
||||
|
||||
with_transaction(&self.pool, "create_user", |tx| {
|
||||
// We need to move the closure into a BoxFuture to return inside
|
||||
// the with_transaction call
|
||||
Box::pin(async move {
|
||||
// Use getters to extract the values
|
||||
// Convert user.role() to string to pass it as plain text
|
||||
let role_str = user_clone.role().to_string();
|
||||
|
||||
// Modify the SQL to do an explicit cast to the auth.userrole type
|
||||
let _result = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO auth.users (
|
||||
id, username, email, password_hash, role,
|
||||
storage_quota_bytes, storage_used_bytes,
|
||||
@@ -81,36 +72,36 @@ impl UserRepository for UserPgRepository {
|
||||
$12, $13
|
||||
)
|
||||
RETURNING *
|
||||
"#
|
||||
)
|
||||
.bind(user_clone.id())
|
||||
.bind(user_clone.username())
|
||||
.bind(user_clone.email())
|
||||
.bind(user_clone.password_hash())
|
||||
.bind(&role_str) // Convert to string but with explicit cast in SQL
|
||||
.bind(user_clone.storage_quota_bytes())
|
||||
.bind(user_clone.storage_used_bytes())
|
||||
.bind(user_clone.created_at())
|
||||
.bind(user_clone.updated_at())
|
||||
.bind(user_clone.last_login_at())
|
||||
.bind(user_clone.is_active())
|
||||
.bind(user_clone.oidc_provider())
|
||||
.bind(user_clone.oidc_subject())
|
||||
.execute(&mut **tx)
|
||||
.await
|
||||
.map_err(Self::map_sqlx_error)?;
|
||||
|
||||
// We could perform additional operations here,
|
||||
// such as configuring permissions, roles, etc.
|
||||
|
||||
Ok(user_clone)
|
||||
}) as BoxFuture<'_, UserRepositoryResult<User>>
|
||||
}
|
||||
).await?;
|
||||
|
||||
"#,
|
||||
)
|
||||
.bind(user_clone.id())
|
||||
.bind(user_clone.username())
|
||||
.bind(user_clone.email())
|
||||
.bind(user_clone.password_hash())
|
||||
.bind(&role_str) // Convert to string but with explicit cast in SQL
|
||||
.bind(user_clone.storage_quota_bytes())
|
||||
.bind(user_clone.storage_used_bytes())
|
||||
.bind(user_clone.created_at())
|
||||
.bind(user_clone.updated_at())
|
||||
.bind(user_clone.last_login_at())
|
||||
.bind(user_clone.is_active())
|
||||
.bind(user_clone.oidc_provider())
|
||||
.bind(user_clone.oidc_subject())
|
||||
.execute(&mut **tx)
|
||||
.await
|
||||
.map_err(Self::map_sqlx_error)?;
|
||||
|
||||
// We could perform additional operations here,
|
||||
// such as configuring permissions, roles, etc.
|
||||
|
||||
Ok(user_clone)
|
||||
}) as BoxFuture<'_, UserRepositoryResult<User>>
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(user) // Return the original user for simplicity
|
||||
}
|
||||
|
||||
|
||||
/// Gets a user by ID
|
||||
async fn get_user_by_id(&self, id: &str) -> UserRepositoryResult<User> {
|
||||
let row = sqlx::query(
|
||||
@@ -122,7 +113,7 @@ impl UserRepository for UserPgRepository {
|
||||
oidc_provider, oidc_subject
|
||||
FROM auth.users
|
||||
WHERE id = $1
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_one(&*self.pool)
|
||||
@@ -135,7 +126,7 @@ impl UserRepository for UserPgRepository {
|
||||
Some("admin") => UserRole::Admin,
|
||||
_ => UserRole::User,
|
||||
};
|
||||
|
||||
|
||||
Ok(User::from_data_full(
|
||||
row.get("id"),
|
||||
row.get("username"),
|
||||
@@ -152,7 +143,7 @@ impl UserRepository for UserPgRepository {
|
||||
row.get("oidc_subject"),
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
/// Gets a user by username
|
||||
async fn get_user_by_username(&self, username: &str) -> UserRepositoryResult<User> {
|
||||
let row = sqlx::query(
|
||||
@@ -164,7 +155,7 @@ impl UserRepository for UserPgRepository {
|
||||
oidc_provider, oidc_subject
|
||||
FROM auth.users
|
||||
WHERE username = $1
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(username)
|
||||
.fetch_one(&*self.pool)
|
||||
@@ -177,7 +168,7 @@ impl UserRepository for UserPgRepository {
|
||||
Some("admin") => UserRole::Admin,
|
||||
_ => UserRole::User,
|
||||
};
|
||||
|
||||
|
||||
Ok(User::from_data_full(
|
||||
row.get("id"),
|
||||
row.get("username"),
|
||||
@@ -194,7 +185,7 @@ impl UserRepository for UserPgRepository {
|
||||
row.get("oidc_subject"),
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
/// Gets a user by email
|
||||
async fn get_user_by_email(&self, email: &str) -> UserRepositoryResult<User> {
|
||||
let row = sqlx::query(
|
||||
@@ -206,7 +197,7 @@ impl UserRepository for UserPgRepository {
|
||||
oidc_provider, oidc_subject
|
||||
FROM auth.users
|
||||
WHERE email = $1
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(email)
|
||||
.fetch_one(&*self.pool)
|
||||
@@ -219,7 +210,7 @@ impl UserRepository for UserPgRepository {
|
||||
Some("admin") => UserRole::Admin,
|
||||
_ => UserRole::User,
|
||||
};
|
||||
|
||||
|
||||
Ok(User::from_data_full(
|
||||
row.get("id"),
|
||||
row.get("username"),
|
||||
@@ -236,20 +227,17 @@ impl UserRepository for UserPgRepository {
|
||||
row.get("oidc_subject"),
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
/// Updates an existing user using a transaction
|
||||
async fn update_user(&self, user: User) -> UserRepositoryResult<User> {
|
||||
// Create a copy of the user for the closure
|
||||
let user_clone = user.clone();
|
||||
|
||||
with_transaction(
|
||||
&self.pool,
|
||||
"update_user",
|
||||
|tx| {
|
||||
Box::pin(async move {
|
||||
// Update the user
|
||||
sqlx::query(
|
||||
r#"
|
||||
|
||||
with_transaction(&self.pool, "update_user", |tx| {
|
||||
Box::pin(async move {
|
||||
// Update the user
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE auth.users
|
||||
SET
|
||||
username = $2,
|
||||
@@ -262,35 +250,39 @@ impl UserRepository for UserPgRepository {
|
||||
last_login_at = $9,
|
||||
active = $10
|
||||
WHERE id = $1
|
||||
"#
|
||||
)
|
||||
.bind(user_clone.id())
|
||||
.bind(user_clone.username())
|
||||
.bind(user_clone.email())
|
||||
.bind(user_clone.password_hash())
|
||||
.bind(user_clone.role().to_string())
|
||||
.bind(user_clone.storage_quota_bytes())
|
||||
.bind(user_clone.storage_used_bytes())
|
||||
.bind(user_clone.updated_at())
|
||||
.bind(user_clone.last_login_at())
|
||||
.bind(user_clone.is_active())
|
||||
.execute(&mut **tx)
|
||||
.await
|
||||
.map_err(Self::map_sqlx_error)?;
|
||||
|
||||
// We could perform additional operations here inside
|
||||
// the same transaction, such as updating permissions, etc.
|
||||
|
||||
Ok(user_clone)
|
||||
}) as BoxFuture<'_, UserRepositoryResult<User>>
|
||||
}
|
||||
).await?;
|
||||
|
||||
"#,
|
||||
)
|
||||
.bind(user_clone.id())
|
||||
.bind(user_clone.username())
|
||||
.bind(user_clone.email())
|
||||
.bind(user_clone.password_hash())
|
||||
.bind(user_clone.role().to_string())
|
||||
.bind(user_clone.storage_quota_bytes())
|
||||
.bind(user_clone.storage_used_bytes())
|
||||
.bind(user_clone.updated_at())
|
||||
.bind(user_clone.last_login_at())
|
||||
.bind(user_clone.is_active())
|
||||
.execute(&mut **tx)
|
||||
.await
|
||||
.map_err(Self::map_sqlx_error)?;
|
||||
|
||||
// We could perform additional operations here inside
|
||||
// the same transaction, such as updating permissions, etc.
|
||||
|
||||
Ok(user_clone)
|
||||
}) as BoxFuture<'_, UserRepositoryResult<User>>
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
|
||||
/// Updates only the storage usage of a user
|
||||
async fn update_storage_usage(&self, user_id: &str, usage_bytes: i64) -> UserRepositoryResult<()> {
|
||||
async fn update_storage_usage(
|
||||
&self,
|
||||
user_id: &str,
|
||||
usage_bytes: i64,
|
||||
) -> UserRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE auth.users
|
||||
@@ -298,7 +290,7 @@ impl UserRepository for UserPgRepository {
|
||||
storage_used_bytes = $2,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(usage_bytes)
|
||||
@@ -308,7 +300,7 @@ impl UserRepository for UserPgRepository {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Updates the last login date
|
||||
async fn update_last_login(&self, user_id: &str) -> UserRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
@@ -318,7 +310,7 @@ impl UserRepository for UserPgRepository {
|
||||
last_login_at = NOW(),
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.execute(&*self.pool)
|
||||
@@ -327,7 +319,7 @@ impl UserRepository for UserPgRepository {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Lists users with pagination
|
||||
async fn list_users(&self, limit: i64, offset: i64) -> UserRepositoryResult<Vec<User>> {
|
||||
let rows = sqlx::query(
|
||||
@@ -340,7 +332,7 @@ impl UserRepository for UserPgRepository {
|
||||
FROM auth.users
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $1 OFFSET $2
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
@@ -348,7 +340,8 @@ impl UserRepository for UserPgRepository {
|
||||
.await
|
||||
.map_err(Self::map_sqlx_error)?;
|
||||
|
||||
let users = rows.into_iter()
|
||||
let users = rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
// Convert role string to UserRole enum for each row
|
||||
let role_str: Option<String> = row.try_get("role_text").unwrap_or(None);
|
||||
@@ -356,7 +349,7 @@ impl UserRepository for UserPgRepository {
|
||||
Some("admin") => UserRole::Admin,
|
||||
_ => UserRole::User,
|
||||
};
|
||||
|
||||
|
||||
User::from_data_full(
|
||||
row.get("id"),
|
||||
row.get("username"),
|
||||
@@ -377,9 +370,13 @@ impl UserRepository for UserPgRepository {
|
||||
|
||||
Ok(users)
|
||||
}
|
||||
|
||||
|
||||
/// Activates or deactivates a user
|
||||
async fn set_user_active_status(&self, user_id: &str, active: bool) -> UserRepositoryResult<()> {
|
||||
async fn set_user_active_status(
|
||||
&self,
|
||||
user_id: &str,
|
||||
active: bool,
|
||||
) -> UserRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE auth.users
|
||||
@@ -387,7 +384,7 @@ impl UserRepository for UserPgRepository {
|
||||
active = $2,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(active)
|
||||
@@ -397,9 +394,13 @@ impl UserRepository for UserPgRepository {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Changes a user's password
|
||||
async fn change_password(&self, user_id: &str, password_hash: &str) -> UserRepositoryResult<()> {
|
||||
async fn change_password(
|
||||
&self,
|
||||
user_id: &str,
|
||||
password_hash: &str,
|
||||
) -> UserRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE auth.users
|
||||
@@ -407,7 +408,7 @@ impl UserRepository for UserPgRepository {
|
||||
password_hash = $2,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(password_hash)
|
||||
@@ -417,12 +418,12 @@ impl UserRepository for UserPgRepository {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Changes a user's role
|
||||
async fn change_role(&self, user_id: &str, role: UserRole) -> UserRepositoryResult<()> {
|
||||
// Convert the role to string for the binding
|
||||
let role_str = role.to_string();
|
||||
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE auth.users
|
||||
@@ -430,7 +431,7 @@ impl UserRepository for UserPgRepository {
|
||||
role = $2::auth.userrole,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(&role_str)
|
||||
@@ -440,7 +441,7 @@ impl UserRepository for UserPgRepository {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Lists users by role
|
||||
async fn list_users_by_role(&self, role: &str) -> UserRepositoryResult<Vec<User>> {
|
||||
let rows = sqlx::query(
|
||||
@@ -453,14 +454,15 @@ impl UserRepository for UserPgRepository {
|
||||
FROM auth.users
|
||||
WHERE role::text = $1
|
||||
ORDER BY created_at DESC
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(role)
|
||||
.fetch_all(&*self.pool)
|
||||
.await
|
||||
.map_err(Self::map_sqlx_error)?;
|
||||
|
||||
let users = rows.into_iter()
|
||||
let users = rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
// Convert role string to UserRole enum for each row
|
||||
let role_str: Option<String> = row.try_get("role_text").unwrap_or(None);
|
||||
@@ -468,7 +470,7 @@ impl UserRepository for UserPgRepository {
|
||||
Some("admin") => UserRole::Admin,
|
||||
_ => UserRole::User,
|
||||
};
|
||||
|
||||
|
||||
User::from_data_full(
|
||||
row.get("id"),
|
||||
row.get("username"),
|
||||
@@ -489,14 +491,14 @@ impl UserRepository for UserPgRepository {
|
||||
|
||||
Ok(users)
|
||||
}
|
||||
|
||||
|
||||
/// Deletes a user
|
||||
async fn delete_user(&self, user_id: &str) -> UserRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
DELETE FROM auth.users
|
||||
WHERE id = $1
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.execute(&*self.pool)
|
||||
@@ -507,7 +509,11 @@ impl UserRepository for UserPgRepository {
|
||||
}
|
||||
|
||||
/// Finds a user by OIDC provider + subject pair
|
||||
async fn get_user_by_oidc_subject(&self, provider: &str, subject: &str) -> UserRepositoryResult<User> {
|
||||
async fn get_user_by_oidc_subject(
|
||||
&self,
|
||||
provider: &str,
|
||||
subject: &str,
|
||||
) -> UserRepositoryResult<User> {
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
@@ -517,7 +523,7 @@ impl UserRepository for UserPgRepository {
|
||||
oidc_provider, oidc_subject
|
||||
FROM auth.users
|
||||
WHERE oidc_provider = $1 AND oidc_subject = $2
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(provider)
|
||||
.bind(subject)
|
||||
@@ -549,7 +555,11 @@ impl UserRepository for UserPgRepository {
|
||||
}
|
||||
|
||||
/// Updates a user's storage quota
|
||||
async fn update_storage_quota(&self, user_id: &str, quota_bytes: i64) -> UserRepositoryResult<()> {
|
||||
async fn update_storage_quota(
|
||||
&self,
|
||||
user_id: &str,
|
||||
quota_bytes: i64,
|
||||
) -> UserRepositoryResult<()> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE auth.users
|
||||
@@ -557,7 +567,7 @@ impl UserRepository for UserPgRepository {
|
||||
storage_quota_bytes = $2,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(quota_bytes)
|
||||
@@ -570,12 +580,10 @@ impl UserRepository for UserPgRepository {
|
||||
|
||||
/// Counts the total number of users
|
||||
async fn count_users(&self) -> UserRepositoryResult<i64> {
|
||||
let row = sqlx::query(
|
||||
"SELECT COUNT(*) as count FROM auth.users"
|
||||
)
|
||||
.fetch_one(&*self.pool)
|
||||
.await
|
||||
.map_err(Self::map_sqlx_error)?;
|
||||
let row = sqlx::query("SELECT COUNT(*) as count FROM auth.users")
|
||||
.fetch_one(&*self.pool)
|
||||
.await
|
||||
.map_err(Self::map_sqlx_error)?;
|
||||
|
||||
let count: i64 = row.get("count");
|
||||
Ok(count)
|
||||
@@ -614,52 +622,74 @@ impl UserRepository for UserPgRepository {
|
||||
#[async_trait]
|
||||
impl UserStoragePort for UserPgRepository {
|
||||
async fn create_user(&self, user: User) -> Result<User, DomainError> {
|
||||
UserRepository::create_user(self, user).await.map_err(DomainError::from)
|
||||
UserRepository::create_user(self, user)
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
|
||||
async fn get_user_by_id(&self, id: &str) -> Result<User, DomainError> {
|
||||
UserRepository::get_user_by_id(self, id).await.map_err(DomainError::from)
|
||||
UserRepository::get_user_by_id(self, id)
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
|
||||
async fn get_user_by_username(&self, username: &str) -> Result<User, DomainError> {
|
||||
UserRepository::get_user_by_username(self, username).await.map_err(DomainError::from)
|
||||
UserRepository::get_user_by_username(self, username)
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
|
||||
async fn get_user_by_email(&self, email: &str) -> Result<User, DomainError> {
|
||||
UserRepository::get_user_by_email(self, email).await.map_err(DomainError::from)
|
||||
UserRepository::get_user_by_email(self, email)
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
|
||||
async fn update_user(&self, user: User) -> Result<User, DomainError> {
|
||||
UserRepository::update_user(self, user).await.map_err(DomainError::from)
|
||||
UserRepository::update_user(self, user)
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn update_storage_usage(&self, user_id: &str, usage_bytes: i64) -> Result<(), DomainError> {
|
||||
|
||||
async fn update_storage_usage(
|
||||
&self,
|
||||
user_id: &str,
|
||||
usage_bytes: i64,
|
||||
) -> Result<(), DomainError> {
|
||||
UserRepository::update_storage_usage(self, user_id, usage_bytes)
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
|
||||
async fn list_users(&self, limit: i64, offset: i64) -> Result<Vec<User>, DomainError> {
|
||||
UserRepository::list_users(self, limit, offset).await.map_err(DomainError::from)
|
||||
UserRepository::list_users(self, limit, offset)
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
|
||||
async fn list_users_by_role(&self, role: &str) -> Result<Vec<User>, DomainError> {
|
||||
UserRepository::list_users_by_role(self, role).await.map_err(DomainError::from)
|
||||
UserRepository::list_users_by_role(self, role)
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
|
||||
async fn delete_user(&self, user_id: &str) -> Result<(), DomainError> {
|
||||
UserRepository::delete_user(self, user_id)
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
|
||||
async fn change_password(&self, user_id: &str, password_hash: &str) -> Result<(), DomainError> {
|
||||
UserRepository::change_password(self, user_id, password_hash)
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn get_user_by_oidc_subject(&self, provider: &str, subject: &str) -> Result<User, DomainError> {
|
||||
async fn get_user_by_oidc_subject(
|
||||
&self,
|
||||
provider: &str,
|
||||
subject: &str,
|
||||
) -> Result<User, DomainError> {
|
||||
UserRepository::get_user_by_oidc_subject(self, provider, subject)
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
@@ -681,7 +711,11 @@ impl UserStoragePort for UserPgRepository {
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
|
||||
async fn update_storage_quota(&self, user_id: &str, quota_bytes: i64) -> Result<(), DomainError> {
|
||||
async fn update_storage_quota(
|
||||
&self,
|
||||
user_id: &str,
|
||||
quota_bytes: i64,
|
||||
) -> Result<(), DomainError> {
|
||||
UserRepository::update_storage_quota(self, user_id, quota_bytes)
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
@@ -692,4 +726,4 @@ impl UserStoragePort for UserPgRepository {
|
||||
.await
|
||||
.map_err(DomainError::from)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user