importer feature
This commit is contained in:
21
crates/adapters/postgres/migrations/0002_import.sql
Normal file
21
crates/adapters/postgres/migrations/0002_import.sql
Normal file
@@ -0,0 +1,21 @@
|
||||
CREATE TABLE IF NOT EXISTS import_sessions (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
parsed_data TEXT NOT NULL,
|
||||
field_mappings TEXT,
|
||||
row_results TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
expires_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS import_profiles (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
field_mappings TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_import_sessions_user_id ON import_sessions (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_import_sessions_expires_at ON import_sessions (expires_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_import_profiles_user_id ON import_profiles (user_id);
|
||||
125
crates/adapters/postgres/src/import_profile.rs
Normal file
125
crates/adapters/postgres/src/import_profile.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use async_trait::async_trait;
|
||||
use chrono::NaiveDateTime;
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::ImportProfile,
|
||||
ports::ImportProfileRepository,
|
||||
value_objects::{ImportProfileId, UserId},
|
||||
};
|
||||
use sqlx::PgPool;
|
||||
|
||||
pub struct PostgresImportProfileRepository {
|
||||
pool: PgPool,
|
||||
}
|
||||
|
||||
impl PostgresImportProfileRepository {
|
||||
pub fn new(pool: PgPool) -> Self { Self { pool } }
|
||||
|
||||
fn map_err(e: sqlx::Error) -> DomainError {
|
||||
tracing::error!("DB error: {:?}", e);
|
||||
DomainError::InfrastructureError("Database operation failed".into())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ImportProfileRepository for PostgresImportProfileRepository {
|
||||
async fn save(&self, p: &ImportProfile) -> Result<(), DomainError> {
|
||||
let id = p.id.value().to_string();
|
||||
let user_id = p.user_id.value().to_string();
|
||||
sqlx::query(
|
||||
"INSERT INTO import_profiles (id, user_id, name, field_mappings, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, field_mappings = EXCLUDED.field_mappings",
|
||||
)
|
||||
.bind(&id)
|
||||
.bind(&user_id)
|
||||
.bind(&p.name)
|
||||
.bind(&p.field_mappings)
|
||||
.bind(p.created_at)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(Self::map_err)
|
||||
}
|
||||
|
||||
async fn list_for_user(&self, user_id: &UserId) -> Result<Vec<ImportProfile>, DomainError> {
|
||||
let uid = user_id.value().to_string();
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct Row {
|
||||
id: String,
|
||||
user_id: String,
|
||||
name: String,
|
||||
field_mappings: String,
|
||||
created_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
let rows = sqlx::query_as::<_, Row>(
|
||||
"SELECT id, user_id, name, field_mappings, created_at FROM import_profiles WHERE user_id = $1 ORDER BY created_at DESC",
|
||||
)
|
||||
.bind(&uid)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
|
||||
rows.into_iter().map(|r| -> Result<ImportProfile, DomainError> {
|
||||
Ok(ImportProfile {
|
||||
id: ImportProfileId::from_uuid(
|
||||
r.id.parse::<uuid::Uuid>().map_err(|e| DomainError::InfrastructureError(e.to_string()))?
|
||||
),
|
||||
user_id: UserId::from_uuid(
|
||||
r.user_id.parse::<uuid::Uuid>().map_err(|e| DomainError::InfrastructureError(e.to_string()))?
|
||||
),
|
||||
name: r.name,
|
||||
field_mappings: r.field_mappings,
|
||||
created_at: r.created_at,
|
||||
})
|
||||
}).collect()
|
||||
}
|
||||
|
||||
async fn get(&self, id: &ImportProfileId, user_id: &UserId) -> Result<Option<ImportProfile>, DomainError> {
|
||||
let id_str = id.value().to_string();
|
||||
let uid_str = user_id.value().to_string();
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct Row {
|
||||
id: String,
|
||||
user_id: String,
|
||||
name: String,
|
||||
field_mappings: String,
|
||||
created_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
let row = sqlx::query_as::<_, Row>(
|
||||
"SELECT id, user_id, name, field_mappings, created_at FROM import_profiles WHERE id = $1 AND user_id = $2",
|
||||
)
|
||||
.bind(&id_str)
|
||||
.bind(&uid_str)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
|
||||
Ok(row.map(|r| -> Result<ImportProfile, DomainError> {
|
||||
Ok(ImportProfile {
|
||||
id: ImportProfileId::from_uuid(
|
||||
r.id.parse::<uuid::Uuid>().map_err(|e| DomainError::InfrastructureError(e.to_string()))?
|
||||
),
|
||||
user_id: UserId::from_uuid(
|
||||
r.user_id.parse::<uuid::Uuid>().map_err(|e| DomainError::InfrastructureError(e.to_string()))?
|
||||
),
|
||||
name: r.name,
|
||||
field_mappings: r.field_mappings,
|
||||
created_at: r.created_at,
|
||||
})
|
||||
}).transpose()?)
|
||||
}
|
||||
|
||||
async fn delete(&self, id: &ImportProfileId) -> Result<(), DomainError> {
|
||||
let id_str = id.value().to_string();
|
||||
sqlx::query("DELETE FROM import_profiles WHERE id = $1")
|
||||
.bind(&id_str)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(Self::map_err)
|
||||
}
|
||||
}
|
||||
129
crates/adapters/postgres/src/import_session.rs
Normal file
129
crates/adapters/postgres/src/import_session.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
use async_trait::async_trait;
|
||||
use chrono::NaiveDateTime;
|
||||
use domain::{
|
||||
errors::DomainError,
|
||||
models::ImportSession,
|
||||
ports::ImportSessionRepository,
|
||||
value_objects::{ImportSessionId, UserId},
|
||||
};
|
||||
use sqlx::PgPool;
|
||||
|
||||
pub struct PostgresImportSessionRepository {
|
||||
pool: PgPool,
|
||||
}
|
||||
|
||||
impl PostgresImportSessionRepository {
|
||||
pub fn new(pool: PgPool) -> Self { Self { pool } }
|
||||
|
||||
fn map_err(e: sqlx::Error) -> DomainError {
|
||||
tracing::error!("DB error: {:?}", e);
|
||||
DomainError::InfrastructureError("Database operation failed".into())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ImportSessionRepository for PostgresImportSessionRepository {
|
||||
async fn create(&self, s: &ImportSession) -> Result<(), DomainError> {
|
||||
let id = s.id.value().to_string();
|
||||
let user_id = s.user_id.value().to_string();
|
||||
sqlx::query(
|
||||
"INSERT INTO import_sessions (id, user_id, parsed_data, field_mappings, row_results, created_at, expires_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
||||
)
|
||||
.bind(&id)
|
||||
.bind(&user_id)
|
||||
.bind(&s.parsed_data)
|
||||
.bind(&s.field_mappings)
|
||||
.bind(&s.row_results)
|
||||
.bind(s.created_at)
|
||||
.bind(s.expires_at)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(Self::map_err)
|
||||
}
|
||||
|
||||
async fn get(&self, id: &ImportSessionId, user_id: &UserId) -> Result<Option<ImportSession>, DomainError> {
|
||||
let id_str = id.value().to_string();
|
||||
let uid_str = user_id.value().to_string();
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct Row {
|
||||
id: String,
|
||||
user_id: String,
|
||||
parsed_data: String,
|
||||
field_mappings: Option<String>,
|
||||
row_results: Option<String>,
|
||||
created_at: NaiveDateTime,
|
||||
expires_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
let row = sqlx::query_as::<_, Row>(
|
||||
"SELECT id, user_id, parsed_data, field_mappings, row_results, created_at, expires_at
|
||||
FROM import_sessions WHERE id = $1 AND user_id = $2",
|
||||
)
|
||||
.bind(&id_str)
|
||||
.bind(&uid_str)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
|
||||
Ok(row.map(|r| -> Result<ImportSession, DomainError> {
|
||||
Ok(ImportSession {
|
||||
id: ImportSessionId::from_uuid(
|
||||
r.id.parse::<uuid::Uuid>().map_err(|e| DomainError::InfrastructureError(e.to_string()))?
|
||||
),
|
||||
user_id: UserId::from_uuid(
|
||||
r.user_id.parse::<uuid::Uuid>().map_err(|e| DomainError::InfrastructureError(e.to_string()))?
|
||||
),
|
||||
parsed_data: r.parsed_data,
|
||||
field_mappings: r.field_mappings,
|
||||
row_results: r.row_results,
|
||||
created_at: r.created_at,
|
||||
expires_at: r.expires_at,
|
||||
})
|
||||
}).transpose()?)
|
||||
}
|
||||
|
||||
async fn update(&self, s: &ImportSession) -> Result<(), DomainError> {
|
||||
let id = s.id.value().to_string();
|
||||
sqlx::query(
|
||||
"UPDATE import_sessions SET field_mappings = $1, row_results = $2 WHERE id = $3",
|
||||
)
|
||||
.bind(&s.field_mappings)
|
||||
.bind(&s.row_results)
|
||||
.bind(&id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(Self::map_err)
|
||||
}
|
||||
|
||||
async fn delete(&self, id: &ImportSessionId) -> Result<(), DomainError> {
|
||||
let id_str = id.value().to_string();
|
||||
sqlx::query("DELETE FROM import_sessions WHERE id = $1")
|
||||
.bind(&id_str)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(Self::map_err)
|
||||
}
|
||||
|
||||
async fn delete_expired(&self) -> Result<u64, DomainError> {
|
||||
let result = sqlx::query("DELETE FROM import_sessions WHERE expires_at < NOW()")
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(Self::map_err)?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
|
||||
async fn delete_expired_for_user(&self, user_id: &UserId) -> Result<(), DomainError> {
|
||||
let uid = user_id.value().to_string();
|
||||
sqlx::query("DELETE FROM import_sessions WHERE user_id = $1 AND expires_at < NOW()")
|
||||
.bind(&uid)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(Self::map_err)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ use domain::{
|
||||
};
|
||||
use sqlx::PgPool;
|
||||
|
||||
mod import_profile;
|
||||
mod import_session;
|
||||
mod models;
|
||||
mod users;
|
||||
|
||||
@@ -20,6 +22,8 @@ use models::{
|
||||
datetime_to_str,
|
||||
};
|
||||
|
||||
pub use import_profile::PostgresImportProfileRepository;
|
||||
pub use import_session::PostgresImportSessionRepository;
|
||||
pub use users::PostgresUserRepository;
|
||||
|
||||
fn format_year_month(ym: &str) -> String {
|
||||
@@ -775,6 +779,8 @@ pub async fn wire(database_url: &str) -> anyhow::Result<(
|
||||
std::sync::Arc<dyn domain::ports::DiaryRepository>,
|
||||
std::sync::Arc<dyn domain::ports::StatsRepository>,
|
||||
std::sync::Arc<dyn domain::ports::UserRepository>,
|
||||
std::sync::Arc<dyn domain::ports::ImportSessionRepository>,
|
||||
std::sync::Arc<dyn domain::ports::ImportProfileRepository>,
|
||||
)> {
|
||||
use anyhow::Context;
|
||||
|
||||
@@ -788,6 +794,9 @@ pub async fn wire(database_url: &str) -> anyhow::Result<(
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))
|
||||
.context("Database migration failed")?;
|
||||
|
||||
let import_session_repo = std::sync::Arc::new(PostgresImportSessionRepository::new(pool.clone()));
|
||||
let import_profile_repo = std::sync::Arc::new(PostgresImportProfileRepository::new(pool.clone()));
|
||||
|
||||
Ok((
|
||||
pool.clone(),
|
||||
std::sync::Arc::clone(&repo) as _,
|
||||
@@ -795,5 +804,7 @@ pub async fn wire(database_url: &str) -> anyhow::Result<(
|
||||
std::sync::Arc::clone(&repo) as _,
|
||||
std::sync::Arc::clone(&repo) as _,
|
||||
std::sync::Arc::new(PostgresUserRepository::new(pool)) as _,
|
||||
import_session_repo as _,
|
||||
import_profile_repo as _,
|
||||
))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user