refactor: convert to_domain methods to From/TryFrom trait impls
Replace ad-hoc to_domain/into_domain conversion methods on SQL repository structs with idiomatic From and TryFrom trait implementations on the record types, following standard Rust conventions.
This commit is contained in:
parent
590868e51f
commit
7330e5b59e
13 changed files with 345 additions and 370 deletions
|
|
@ -17,20 +17,6 @@ impl SqlAiUsageRepository {
|
||||||
pub fn new(pool: DatabasePool) -> Self {
|
pub fn new(pool: DatabasePool) -> Self {
|
||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_domain(record: AiUsageRecord) -> AiUsage {
|
|
||||||
AiUsage {
|
|
||||||
id: AiUsageId::from(record.id),
|
|
||||||
user_id: UserId::from(record.user_id),
|
|
||||||
model: record.model,
|
|
||||||
endpoint: record.endpoint,
|
|
||||||
prompt_tokens: record.prompt_tokens,
|
|
||||||
completion_tokens: record.completion_tokens,
|
|
||||||
total_tokens: record.total_tokens,
|
|
||||||
cost: record.cost,
|
|
||||||
created_at: record.created_at,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|
@ -54,7 +40,7 @@ impl AiUsageRepository for SqlAiUsageRepository {
|
||||||
.await
|
.await
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
||||||
|
|
||||||
Ok(Self::to_domain(record))
|
Ok(record.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn summary_for_user(&self, user_id: UserId) -> Result<AiUsageSummary, RepositoryError> {
|
async fn summary_for_user(&self, user_id: UserId) -> Result<AiUsageSummary, RepositoryError> {
|
||||||
|
|
@ -98,6 +84,22 @@ struct AiUsageRecord {
|
||||||
created_at: DateTime<Utc>,
|
created_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<AiUsageRecord> for AiUsage {
|
||||||
|
fn from(record: AiUsageRecord) -> Self {
|
||||||
|
AiUsage {
|
||||||
|
id: AiUsageId::from(record.id),
|
||||||
|
user_id: UserId::from(record.user_id),
|
||||||
|
model: record.model,
|
||||||
|
endpoint: record.endpoint,
|
||||||
|
prompt_tokens: record.prompt_tokens,
|
||||||
|
completion_tokens: record.completion_tokens,
|
||||||
|
total_tokens: record.total_tokens,
|
||||||
|
cost: record.cost,
|
||||||
|
created_at: record.created_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(sqlx::FromRow)]
|
#[derive(sqlx::FromRow)]
|
||||||
#[allow(clippy::struct_field_names)]
|
#[allow(clippy::struct_field_names)]
|
||||||
struct AiUsageSummaryRecord {
|
struct AiUsageSummaryRecord {
|
||||||
|
|
|
||||||
|
|
@ -17,26 +17,6 @@ impl SqlPasskeyCredentialRepository {
|
||||||
pub fn new(pool: DatabasePool) -> Self {
|
pub fn new(pool: DatabasePool) -> Self {
|
||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_domain(record: PasskeyCredentialRecord) -> PasskeyCredential {
|
|
||||||
let PasskeyCredentialRecord {
|
|
||||||
id,
|
|
||||||
user_id,
|
|
||||||
credential_json,
|
|
||||||
name,
|
|
||||||
created_at,
|
|
||||||
last_used_at,
|
|
||||||
} = record;
|
|
||||||
|
|
||||||
PasskeyCredential {
|
|
||||||
id: PasskeyCredentialId::from(id),
|
|
||||||
user_id: UserId::from(user_id),
|
|
||||||
credential_json,
|
|
||||||
name,
|
|
||||||
created_at,
|
|
||||||
last_used_at,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|
@ -61,7 +41,7 @@ impl PasskeyCredentialRepository for SqlPasskeyCredentialRepository {
|
||||||
RepositoryError::unexpected(format!("failed to insert passkey credential: {err}"))
|
RepositoryError::unexpected(format!("failed to insert passkey credential: {err}"))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(Self::to_domain(record))
|
Ok(record.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get(&self, id: PasskeyCredentialId) -> Result<PasskeyCredential, RepositoryError> {
|
async fn get(&self, id: PasskeyCredentialId) -> Result<PasskeyCredential, RepositoryError> {
|
||||||
|
|
@ -82,7 +62,7 @@ impl PasskeyCredentialRepository for SqlPasskeyCredentialRepository {
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(Self::to_domain(record))
|
Ok(record.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_by_user(
|
async fn list_by_user(
|
||||||
|
|
@ -104,7 +84,7 @@ impl PasskeyCredentialRepository for SqlPasskeyCredentialRepository {
|
||||||
RepositoryError::unexpected(format!("failed to list passkey credentials: {err}"))
|
RepositoryError::unexpected(format!("failed to list passkey credentials: {err}"))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(records.into_iter().map(Self::to_domain).collect())
|
Ok(records.into_iter().map(Into::into).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_all(&self) -> Result<Vec<PasskeyCredential>, RepositoryError> {
|
async fn list_all(&self) -> Result<Vec<PasskeyCredential>, RepositoryError> {
|
||||||
|
|
@ -123,7 +103,7 @@ impl PasskeyCredentialRepository for SqlPasskeyCredentialRepository {
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(records.into_iter().map(Self::to_domain).collect())
|
Ok(records.into_iter().map(Into::into).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_credential_json(
|
async fn update_credential_json(
|
||||||
|
|
@ -183,3 +163,16 @@ struct PasskeyCredentialRecord {
|
||||||
created_at: DateTime<Utc>,
|
created_at: DateTime<Utc>,
|
||||||
last_used_at: Option<DateTime<Utc>>,
|
last_used_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<PasskeyCredentialRecord> for PasskeyCredential {
|
||||||
|
fn from(record: PasskeyCredentialRecord) -> Self {
|
||||||
|
PasskeyCredential {
|
||||||
|
id: PasskeyCredentialId::from(record.id),
|
||||||
|
user_id: UserId::from(record.user_id),
|
||||||
|
credential_json: record.credential_json,
|
||||||
|
name: record.name,
|
||||||
|
created_at: record.created_at,
|
||||||
|
last_used_at: record.last_used_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,26 +17,6 @@ impl SqlRegistrationTokenRepository {
|
||||||
pub fn new(pool: DatabasePool) -> Self {
|
pub fn new(pool: DatabasePool) -> Self {
|
||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_domain(record: RegistrationTokenRecord) -> RegistrationToken {
|
|
||||||
let RegistrationTokenRecord {
|
|
||||||
id,
|
|
||||||
token_hash,
|
|
||||||
created_at,
|
|
||||||
expires_at,
|
|
||||||
used_at,
|
|
||||||
used_by_user_id,
|
|
||||||
} = record;
|
|
||||||
|
|
||||||
RegistrationToken {
|
|
||||||
id: RegistrationTokenId::from(id),
|
|
||||||
token_hash,
|
|
||||||
created_at,
|
|
||||||
expires_at,
|
|
||||||
used_at,
|
|
||||||
used_by_user_id: used_by_user_id.map(UserId::from),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|
@ -61,7 +41,7 @@ impl RegistrationTokenRepository for SqlRegistrationTokenRepository {
|
||||||
RepositoryError::unexpected(format!("failed to insert registration token: {err}"))
|
RepositoryError::unexpected(format!("failed to insert registration token: {err}"))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(Self::to_domain(record))
|
Ok(record.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_by_token_hash(
|
async fn get_by_token_hash(
|
||||||
|
|
@ -83,7 +63,7 @@ impl RegistrationTokenRepository for SqlRegistrationTokenRepository {
|
||||||
})?
|
})?
|
||||||
.ok_or(RepositoryError::NotFound)?;
|
.ok_or(RepositoryError::NotFound)?;
|
||||||
|
|
||||||
Ok(Self::to_domain(record))
|
Ok(record.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn mark_used(
|
async fn mark_used(
|
||||||
|
|
@ -119,3 +99,16 @@ struct RegistrationTokenRecord {
|
||||||
used_at: Option<DateTime<Utc>>,
|
used_at: Option<DateTime<Utc>>,
|
||||||
used_by_user_id: Option<i64>,
|
used_by_user_id: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<RegistrationTokenRecord> for RegistrationToken {
|
||||||
|
fn from(record: RegistrationTokenRecord) -> Self {
|
||||||
|
RegistrationToken {
|
||||||
|
id: RegistrationTokenId::from(record.id),
|
||||||
|
token_hash: record.token_hash,
|
||||||
|
created_at: record.created_at,
|
||||||
|
expires_at: record.expires_at,
|
||||||
|
used_at: record.used_at,
|
||||||
|
used_by_user_id: record.used_by_user_id.map(UserId::from),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,24 +16,6 @@ impl SqlSessionRepository {
|
||||||
pub fn new(pool: DatabasePool) -> Self {
|
pub fn new(pool: DatabasePool) -> Self {
|
||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_domain(record: SessionRecord) -> Session {
|
|
||||||
let SessionRecord {
|
|
||||||
id,
|
|
||||||
user_id,
|
|
||||||
session_token_hash,
|
|
||||||
created_at,
|
|
||||||
expires_at,
|
|
||||||
} = record;
|
|
||||||
|
|
||||||
Session::new(
|
|
||||||
SessionId::from(id),
|
|
||||||
UserId::from(user_id),
|
|
||||||
session_token_hash,
|
|
||||||
created_at,
|
|
||||||
expires_at,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|
@ -59,7 +41,7 @@ impl SessionRepository for SqlSessionRepository {
|
||||||
RepositoryError::unexpected(format!("failed to insert session: {err}"))
|
RepositoryError::unexpected(format!("failed to insert session: {err}"))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(Self::to_domain(record))
|
Ok(record.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get(&self, id: SessionId) -> Result<Session, RepositoryError> {
|
async fn get(&self, id: SessionId) -> Result<Session, RepositoryError> {
|
||||||
|
|
@ -72,7 +54,7 @@ impl SessionRepository for SqlSessionRepository {
|
||||||
.map_err(|err| RepositoryError::unexpected(format!("failed to get session: {err}")))?
|
.map_err(|err| RepositoryError::unexpected(format!("failed to get session: {err}")))?
|
||||||
.ok_or(RepositoryError::NotFound)?;
|
.ok_or(RepositoryError::NotFound)?;
|
||||||
|
|
||||||
Ok(Self::to_domain(record))
|
Ok(record.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_by_token_hash(&self, token_hash: &str) -> Result<Session, RepositoryError> {
|
async fn get_by_token_hash(&self, token_hash: &str) -> Result<Session, RepositoryError> {
|
||||||
|
|
@ -87,7 +69,7 @@ impl SessionRepository for SqlSessionRepository {
|
||||||
})?
|
})?
|
||||||
.ok_or(RepositoryError::NotFound)?;
|
.ok_or(RepositoryError::NotFound)?;
|
||||||
|
|
||||||
Ok(Self::to_domain(record))
|
Ok(record.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete(&self, id: SessionId) -> Result<(), RepositoryError> {
|
async fn delete(&self, id: SessionId) -> Result<(), RepositoryError> {
|
||||||
|
|
@ -124,3 +106,15 @@ struct SessionRecord {
|
||||||
created_at: chrono::DateTime<Utc>,
|
created_at: chrono::DateTime<Utc>,
|
||||||
expires_at: chrono::DateTime<Utc>,
|
expires_at: chrono::DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<SessionRecord> for Session {
|
||||||
|
fn from(record: SessionRecord) -> Self {
|
||||||
|
Session::new(
|
||||||
|
SessionId::from(record.id),
|
||||||
|
UserId::from(record.user_id),
|
||||||
|
record.session_token_hash,
|
||||||
|
record.created_at,
|
||||||
|
record.expires_at,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,28 +17,6 @@ impl SqlTokenRepository {
|
||||||
pub fn new(pool: DatabasePool) -> Self {
|
pub fn new(pool: DatabasePool) -> Self {
|
||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_domain(record: TokenRecord) -> Token {
|
|
||||||
let TokenRecord {
|
|
||||||
id,
|
|
||||||
user_id,
|
|
||||||
token_hash,
|
|
||||||
name,
|
|
||||||
created_at,
|
|
||||||
last_used_at,
|
|
||||||
revoked_at,
|
|
||||||
} = record;
|
|
||||||
|
|
||||||
Token::new(
|
|
||||||
TokenId::from(id),
|
|
||||||
UserId::from(user_id),
|
|
||||||
token_hash,
|
|
||||||
name,
|
|
||||||
created_at,
|
|
||||||
last_used_at,
|
|
||||||
revoked_at,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|
@ -67,7 +45,7 @@ impl TokenRepository for SqlTokenRepository {
|
||||||
RepositoryError::unexpected(err.to_string())
|
RepositoryError::unexpected(err.to_string())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(Self::to_domain(record))
|
Ok(record.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get(&self, id: TokenId) -> Result<Token, RepositoryError> {
|
async fn get(&self, id: TokenId) -> Result<Token, RepositoryError> {
|
||||||
|
|
@ -80,7 +58,7 @@ impl TokenRepository for SqlTokenRepository {
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
||||||
.ok_or(RepositoryError::NotFound)?;
|
.ok_or(RepositoryError::NotFound)?;
|
||||||
|
|
||||||
Ok(Self::to_domain(record))
|
Ok(record.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_by_token_hash(&self, token_hash: &str) -> Result<Token, RepositoryError> {
|
async fn get_by_token_hash(&self, token_hash: &str) -> Result<Token, RepositoryError> {
|
||||||
|
|
@ -93,7 +71,7 @@ impl TokenRepository for SqlTokenRepository {
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
||||||
.ok_or(RepositoryError::NotFound)?;
|
.ok_or(RepositoryError::NotFound)?;
|
||||||
|
|
||||||
Ok(Self::to_domain(record))
|
Ok(record.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_by_user(&self, user_id: UserId) -> Result<Vec<Token>, RepositoryError> {
|
async fn list_by_user(&self, user_id: UserId) -> Result<Vec<Token>, RepositoryError> {
|
||||||
|
|
@ -105,7 +83,7 @@ impl TokenRepository for SqlTokenRepository {
|
||||||
.await
|
.await
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
||||||
|
|
||||||
Ok(records.into_iter().map(Self::to_domain).collect())
|
Ok(records.into_iter().map(Into::into).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn revoke(&self, id: TokenId) -> Result<Token, RepositoryError> {
|
async fn revoke(&self, id: TokenId) -> Result<Token, RepositoryError> {
|
||||||
|
|
@ -120,7 +98,7 @@ impl TokenRepository for SqlTokenRepository {
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
||||||
|
|
||||||
match record {
|
match record {
|
||||||
Some(record) => Ok(Self::to_domain(record)),
|
Some(record) => Ok(record.into()),
|
||||||
None => Err(RepositoryError::NotFound),
|
None => Err(RepositoryError::NotFound),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -149,3 +127,17 @@ struct TokenRecord {
|
||||||
last_used_at: Option<DateTime<Utc>>,
|
last_used_at: Option<DateTime<Utc>>,
|
||||||
revoked_at: Option<DateTime<Utc>>,
|
revoked_at: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<TokenRecord> for Token {
|
||||||
|
fn from(record: TokenRecord) -> Self {
|
||||||
|
Token::new(
|
||||||
|
TokenId::from(record.id),
|
||||||
|
UserId::from(record.user_id),
|
||||||
|
record.token_hash,
|
||||||
|
record.name,
|
||||||
|
record.created_at,
|
||||||
|
record.last_used_at,
|
||||||
|
record.revoked_at,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,17 +17,6 @@ impl SqlUserRepository {
|
||||||
pub fn new(pool: DatabasePool) -> Self {
|
pub fn new(pool: DatabasePool) -> Self {
|
||||||
Self { pool }
|
Self { pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_domain(record: UserRecord) -> User {
|
|
||||||
let UserRecord {
|
|
||||||
id,
|
|
||||||
username,
|
|
||||||
uuid,
|
|
||||||
created_at,
|
|
||||||
} = record;
|
|
||||||
|
|
||||||
User::new(UserId::from(id), username, uuid, created_at)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|
@ -49,7 +38,7 @@ impl UserRepository for SqlUserRepository {
|
||||||
RepositoryError::unexpected(err.to_string())
|
RepositoryError::unexpected(err.to_string())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(Self::to_domain(record))
|
Ok(record.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get(&self, id: UserId) -> Result<User, RepositoryError> {
|
async fn get(&self, id: UserId) -> Result<User, RepositoryError> {
|
||||||
|
|
@ -62,7 +51,7 @@ impl UserRepository for SqlUserRepository {
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
||||||
.ok_or(RepositoryError::NotFound)?;
|
.ok_or(RepositoryError::NotFound)?;
|
||||||
|
|
||||||
Ok(Self::to_domain(record))
|
Ok(record.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_by_username(&self, username: &str) -> Result<User, RepositoryError> {
|
async fn get_by_username(&self, username: &str) -> Result<User, RepositoryError> {
|
||||||
|
|
@ -75,7 +64,7 @@ impl UserRepository for SqlUserRepository {
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
||||||
.ok_or(RepositoryError::NotFound)?;
|
.ok_or(RepositoryError::NotFound)?;
|
||||||
|
|
||||||
Ok(Self::to_domain(record))
|
Ok(record.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_by_uuid(&self, uuid: &str) -> Result<User, RepositoryError> {
|
async fn get_by_uuid(&self, uuid: &str) -> Result<User, RepositoryError> {
|
||||||
|
|
@ -88,7 +77,7 @@ impl UserRepository for SqlUserRepository {
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
||||||
.ok_or(RepositoryError::NotFound)?;
|
.ok_or(RepositoryError::NotFound)?;
|
||||||
|
|
||||||
Ok(Self::to_domain(record))
|
Ok(record.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn exists(&self) -> Result<bool, RepositoryError> {
|
async fn exists(&self) -> Result<bool, RepositoryError> {
|
||||||
|
|
@ -110,7 +99,7 @@ impl UserRepository for SqlUserRepository {
|
||||||
.await
|
.await
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
||||||
|
|
||||||
Ok(records.into_iter().map(Self::to_domain).collect())
|
Ok(records.into_iter().map(Into::into).collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,3 +110,14 @@ struct UserRecord {
|
||||||
uuid: String,
|
uuid: String,
|
||||||
created_at: DateTime<Utc>,
|
created_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<UserRecord> for User {
|
||||||
|
fn from(record: UserRecord) -> Self {
|
||||||
|
User::new(
|
||||||
|
UserId::from(record.id),
|
||||||
|
record.username,
|
||||||
|
record.uuid,
|
||||||
|
record.created_at,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use crate::infrastructure::database::DatabasePool;
|
||||||
use crate::infrastructure::repositories::macros::push_update_field;
|
use crate::infrastructure::repositories::macros::push_update_field;
|
||||||
|
|
||||||
const BASE_SELECT: &str = r"
|
const BASE_SELECT: &str = r"
|
||||||
SELECT
|
SELECT
|
||||||
b.id, b.roast_id, b.roast_date, b.amount, b.remaining, b.closed, b.finished_at, b.created_at, b.updated_at,
|
b.id, b.roast_id, b.roast_date, b.amount, b.remaining, b.closed, b.finished_at, b.created_at, b.updated_at,
|
||||||
r.name as roast_name, r.slug as roast_slug,
|
r.name as roast_name, r.slug as roast_slug,
|
||||||
rr.name as roaster_name, rr.slug as roaster_slug
|
rr.name as roaster_name, rr.slug as roaster_slug
|
||||||
|
|
@ -49,40 +49,6 @@ impl SqlBagRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_domain(record: BagRecord) -> Bag {
|
|
||||||
Bag {
|
|
||||||
id: BagId::new(record.id),
|
|
||||||
roast_id: RoastId::new(record.roast_id),
|
|
||||||
roast_date: record.roast_date,
|
|
||||||
amount: record.amount,
|
|
||||||
remaining: record.remaining,
|
|
||||||
closed: record.closed,
|
|
||||||
finished_at: record.finished_at,
|
|
||||||
created_at: record.created_at,
|
|
||||||
updated_at: record.updated_at,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_domain_with_roast(record: BagWithRoastRecord) -> BagWithRoast {
|
|
||||||
BagWithRoast {
|
|
||||||
bag: Bag {
|
|
||||||
id: BagId::new(record.id),
|
|
||||||
roast_id: RoastId::new(record.roast_id),
|
|
||||||
roast_date: record.roast_date,
|
|
||||||
amount: record.amount,
|
|
||||||
remaining: record.remaining,
|
|
||||||
closed: record.closed,
|
|
||||||
finished_at: record.finished_at,
|
|
||||||
created_at: record.created_at,
|
|
||||||
updated_at: record.updated_at,
|
|
||||||
},
|
|
||||||
roast_name: record.roast_name,
|
|
||||||
roaster_name: record.roaster_name,
|
|
||||||
roast_slug: record.roast_slug,
|
|
||||||
roaster_slug: record.roaster_slug,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_where_clause(filter: &BagFilter) -> Option<String> {
|
fn build_where_clause(filter: &BagFilter) -> Option<String> {
|
||||||
let mut conditions = Vec::new();
|
let mut conditions = Vec::new();
|
||||||
|
|
||||||
|
|
@ -130,7 +96,7 @@ impl BagRepository for SqlBagRepository {
|
||||||
.await
|
.await
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
||||||
|
|
||||||
Ok(Self::to_domain(record))
|
Ok(record.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get(&self, id: BagId) -> Result<Bag, RepositoryError> {
|
async fn get(&self, id: BagId) -> Result<Bag, RepositoryError> {
|
||||||
|
|
@ -147,7 +113,7 @@ impl BagRepository for SqlBagRepository {
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
||||||
.ok_or(RepositoryError::NotFound)?;
|
.ok_or(RepositoryError::NotFound)?;
|
||||||
|
|
||||||
Ok(Self::to_domain(record))
|
Ok(record.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_with_roast(&self, id: BagId) -> Result<BagWithRoast, RepositoryError> {
|
async fn get_with_roast(&self, id: BagId) -> Result<BagWithRoast, RepositoryError> {
|
||||||
|
|
@ -160,7 +126,7 @@ impl BagRepository for SqlBagRepository {
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
||||||
.ok_or(RepositoryError::NotFound)?;
|
.ok_or(RepositoryError::NotFound)?;
|
||||||
|
|
||||||
Ok(Self::to_domain_with_roast(record))
|
Ok(record.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list(
|
async fn list(
|
||||||
|
|
@ -197,7 +163,7 @@ impl BagRepository for SqlBagRepository {
|
||||||
&count_query,
|
&count_query,
|
||||||
&order_clause,
|
&order_clause,
|
||||||
sf.as_ref(),
|
sf.as_ref(),
|
||||||
|record| Ok(Self::to_domain_with_roast(record)),
|
|record: BagWithRoastRecord| Ok(record.into()),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
@ -233,7 +199,7 @@ impl BagRepository for SqlBagRepository {
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
||||||
.ok_or(RepositoryError::NotFound)?;
|
.ok_or(RepositoryError::NotFound)?;
|
||||||
|
|
||||||
Ok(Self::to_domain(record))
|
Ok(record.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete(&self, id: BagId) -> Result<(), RepositoryError> {
|
async fn delete(&self, id: BagId) -> Result<(), RepositoryError> {
|
||||||
|
|
@ -266,6 +232,22 @@ struct BagRecord {
|
||||||
updated_at: DateTime<Utc>,
|
updated_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<BagRecord> for Bag {
|
||||||
|
fn from(record: BagRecord) -> Self {
|
||||||
|
Bag {
|
||||||
|
id: BagId::new(record.id),
|
||||||
|
roast_id: RoastId::new(record.roast_id),
|
||||||
|
roast_date: record.roast_date,
|
||||||
|
amount: record.amount,
|
||||||
|
remaining: record.remaining,
|
||||||
|
closed: record.closed,
|
||||||
|
finished_at: record.finished_at,
|
||||||
|
created_at: record.created_at,
|
||||||
|
updated_at: record.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(sqlx::FromRow)]
|
#[derive(sqlx::FromRow)]
|
||||||
struct BagWithRoastRecord {
|
struct BagWithRoastRecord {
|
||||||
id: i64,
|
id: i64,
|
||||||
|
|
@ -282,3 +264,25 @@ struct BagWithRoastRecord {
|
||||||
roaster_name: String,
|
roaster_name: String,
|
||||||
roaster_slug: String,
|
roaster_slug: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<BagWithRoastRecord> for BagWithRoast {
|
||||||
|
fn from(record: BagWithRoastRecord) -> Self {
|
||||||
|
BagWithRoast {
|
||||||
|
bag: Bag {
|
||||||
|
id: BagId::new(record.id),
|
||||||
|
roast_id: RoastId::new(record.roast_id),
|
||||||
|
roast_date: record.roast_date,
|
||||||
|
amount: record.amount,
|
||||||
|
remaining: record.remaining,
|
||||||
|
closed: record.closed,
|
||||||
|
finished_at: record.finished_at,
|
||||||
|
created_at: record.created_at,
|
||||||
|
updated_at: record.updated_at,
|
||||||
|
},
|
||||||
|
roast_name: record.roast_name,
|
||||||
|
roaster_name: record.roaster_name,
|
||||||
|
roast_slug: record.roast_slug,
|
||||||
|
roaster_slug: record.roaster_slug,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,17 @@ const BASE_SELECT: &str = r"
|
||||||
LEFT JOIN gear g_fp ON br.filter_paper_id = g_fp.id
|
LEFT JOIN gear g_fp ON br.filter_paper_id = g_fp.id
|
||||||
";
|
";
|
||||||
|
|
||||||
|
fn decode_quick_notes(raw: Option<String>) -> Vec<QuickNote> {
|
||||||
|
match raw {
|
||||||
|
Some(s) if !s.is_empty() => serde_json::from_str::<Vec<String>>(&s)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|v| QuickNote::from_str_value(v))
|
||||||
|
.collect(),
|
||||||
|
_ => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SqlBrewRepository {
|
pub struct SqlBrewRepository {
|
||||||
pool: DatabasePool,
|
pool: DatabasePool,
|
||||||
|
|
@ -56,17 +67,6 @@ impl SqlBrewRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decode_quick_notes(raw: Option<String>) -> Vec<QuickNote> {
|
|
||||||
match raw {
|
|
||||||
Some(s) if !s.is_empty() => serde_json::from_str::<Vec<String>>(&s)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.iter()
|
|
||||||
.filter_map(|v| QuickNote::from_str_value(v))
|
|
||||||
.collect(),
|
|
||||||
_ => Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn encode_quick_notes(notes: &[QuickNote]) -> Option<String> {
|
fn encode_quick_notes(notes: &[QuickNote]) -> Option<String> {
|
||||||
if notes.is_empty() {
|
if notes.is_empty() {
|
||||||
None
|
None
|
||||||
|
|
@ -82,52 +82,6 @@ impl SqlBrewRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_domain(record: BrewRecord) -> Brew {
|
|
||||||
Brew {
|
|
||||||
id: BrewId::new(record.id),
|
|
||||||
bag_id: BagId::new(record.bag_id),
|
|
||||||
coffee_weight: record.coffee_weight,
|
|
||||||
grinder_id: GearId::new(record.grinder_id),
|
|
||||||
grind_setting: record.grind_setting,
|
|
||||||
brewer_id: GearId::new(record.brewer_id),
|
|
||||||
filter_paper_id: record.filter_paper_id.map(GearId::new),
|
|
||||||
water_volume: record.water_volume,
|
|
||||||
water_temp: record.water_temp,
|
|
||||||
quick_notes: Self::decode_quick_notes(record.quick_notes),
|
|
||||||
brew_time: record.brew_time,
|
|
||||||
created_at: record.created_at,
|
|
||||||
updated_at: record.updated_at,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_domain_with_details(record: BrewWithDetailsRecord) -> BrewWithDetails {
|
|
||||||
BrewWithDetails {
|
|
||||||
brew: Brew {
|
|
||||||
id: BrewId::new(record.id),
|
|
||||||
bag_id: BagId::new(record.bag_id),
|
|
||||||
coffee_weight: record.coffee_weight,
|
|
||||||
grinder_id: GearId::new(record.grinder_id),
|
|
||||||
grind_setting: record.grind_setting,
|
|
||||||
brewer_id: GearId::new(record.brewer_id),
|
|
||||||
filter_paper_id: record.filter_paper_id.map(GearId::new),
|
|
||||||
water_volume: record.water_volume,
|
|
||||||
water_temp: record.water_temp,
|
|
||||||
quick_notes: Self::decode_quick_notes(record.quick_notes),
|
|
||||||
brew_time: record.brew_time,
|
|
||||||
created_at: record.created_at,
|
|
||||||
updated_at: record.updated_at,
|
|
||||||
},
|
|
||||||
roast_name: record.roast_name,
|
|
||||||
roaster_name: record.roaster_name,
|
|
||||||
roast_slug: record.roast_slug,
|
|
||||||
roaster_slug: record.roaster_slug,
|
|
||||||
grinder_name: record.grinder_name,
|
|
||||||
grinder_model: record.grinder_model,
|
|
||||||
brewer_name: record.brewer_name,
|
|
||||||
filter_paper_name: record.filter_paper_name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_where_clause(filter: &BrewFilter) -> Option<String> {
|
fn build_where_clause(filter: &BrewFilter) -> Option<String> {
|
||||||
// SAFETY: Direct interpolation is safe here because `bag_id` is an i64 from a typed wrapper.
|
// SAFETY: Direct interpolation is safe here because `bag_id` is an i64 from a typed wrapper.
|
||||||
filter
|
filter
|
||||||
|
|
@ -201,7 +155,7 @@ impl BrewRepository for SqlBrewRepository {
|
||||||
.await
|
.await
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
||||||
|
|
||||||
Ok(Self::to_domain(record))
|
Ok(record.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get(&self, id: BrewId) -> Result<Brew, RepositoryError> {
|
async fn get(&self, id: BrewId) -> Result<Brew, RepositoryError> {
|
||||||
|
|
@ -218,7 +172,7 @@ impl BrewRepository for SqlBrewRepository {
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
||||||
.ok_or(RepositoryError::NotFound)?;
|
.ok_or(RepositoryError::NotFound)?;
|
||||||
|
|
||||||
Ok(Self::to_domain(record))
|
Ok(record.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_with_details(&self, id: BrewId) -> Result<BrewWithDetails, RepositoryError> {
|
async fn get_with_details(&self, id: BrewId) -> Result<BrewWithDetails, RepositoryError> {
|
||||||
|
|
@ -231,7 +185,7 @@ impl BrewRepository for SqlBrewRepository {
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
||||||
.ok_or(RepositoryError::NotFound)?;
|
.ok_or(RepositoryError::NotFound)?;
|
||||||
|
|
||||||
Ok(Self::to_domain_with_details(record))
|
Ok(record.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list(
|
async fn list(
|
||||||
|
|
@ -271,7 +225,7 @@ impl BrewRepository for SqlBrewRepository {
|
||||||
&count_query,
|
&count_query,
|
||||||
&order_clause,
|
&order_clause,
|
||||||
sf.as_ref(),
|
sf.as_ref(),
|
||||||
|record| Ok(Self::to_domain_with_details(record)),
|
|record: BrewWithDetailsRecord| Ok(record.into()),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
@ -339,7 +293,7 @@ impl BrewRepository for SqlBrewRepository {
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
||||||
.ok_or(RepositoryError::NotFound)?;
|
.ok_or(RepositoryError::NotFound)?;
|
||||||
|
|
||||||
Ok(Self::to_domain(record))
|
Ok(record.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete(&self, id: BrewId) -> Result<(), RepositoryError> {
|
async fn delete(&self, id: BrewId) -> Result<(), RepositoryError> {
|
||||||
|
|
@ -376,6 +330,26 @@ struct BrewRecord {
|
||||||
updated_at: DateTime<Utc>,
|
updated_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<BrewRecord> for Brew {
|
||||||
|
fn from(record: BrewRecord) -> Self {
|
||||||
|
Brew {
|
||||||
|
id: BrewId::new(record.id),
|
||||||
|
bag_id: BagId::new(record.bag_id),
|
||||||
|
coffee_weight: record.coffee_weight,
|
||||||
|
grinder_id: GearId::new(record.grinder_id),
|
||||||
|
grind_setting: record.grind_setting,
|
||||||
|
brewer_id: GearId::new(record.brewer_id),
|
||||||
|
filter_paper_id: record.filter_paper_id.map(GearId::new),
|
||||||
|
water_volume: record.water_volume,
|
||||||
|
water_temp: record.water_temp,
|
||||||
|
quick_notes: decode_quick_notes(record.quick_notes),
|
||||||
|
brew_time: record.brew_time,
|
||||||
|
created_at: record.created_at,
|
||||||
|
updated_at: record.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(sqlx::FromRow)]
|
#[derive(sqlx::FromRow)]
|
||||||
struct BrewWithDetailsRecord {
|
struct BrewWithDetailsRecord {
|
||||||
id: i64,
|
id: i64,
|
||||||
|
|
@ -400,3 +374,33 @@ struct BrewWithDetailsRecord {
|
||||||
brewer_name: String,
|
brewer_name: String,
|
||||||
filter_paper_name: Option<String>,
|
filter_paper_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<BrewWithDetailsRecord> for BrewWithDetails {
|
||||||
|
fn from(record: BrewWithDetailsRecord) -> Self {
|
||||||
|
BrewWithDetails {
|
||||||
|
brew: Brew {
|
||||||
|
id: BrewId::new(record.id),
|
||||||
|
bag_id: BagId::new(record.bag_id),
|
||||||
|
coffee_weight: record.coffee_weight,
|
||||||
|
grinder_id: GearId::new(record.grinder_id),
|
||||||
|
grind_setting: record.grind_setting,
|
||||||
|
brewer_id: GearId::new(record.brewer_id),
|
||||||
|
filter_paper_id: record.filter_paper_id.map(GearId::new),
|
||||||
|
water_volume: record.water_volume,
|
||||||
|
water_temp: record.water_temp,
|
||||||
|
quick_notes: decode_quick_notes(record.quick_notes),
|
||||||
|
brew_time: record.brew_time,
|
||||||
|
created_at: record.created_at,
|
||||||
|
updated_at: record.updated_at,
|
||||||
|
},
|
||||||
|
roast_name: record.roast_name,
|
||||||
|
roaster_name: record.roaster_name,
|
||||||
|
roast_slug: record.roast_slug,
|
||||||
|
roaster_slug: record.roaster_slug,
|
||||||
|
grinder_name: record.grinder_name,
|
||||||
|
grinder_model: record.grinder_model,
|
||||||
|
brewer_name: record.brewer_name,
|
||||||
|
filter_paper_name: record.filter_paper_name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,34 +33,6 @@ impl SqlCafeRepository {
|
||||||
CafeSortKey::Country => format!("LOWER(country) {dir_sql}, LOWER(name) ASC"),
|
CafeSortKey::Country => format!("LOWER(country) {dir_sql}, LOWER(name) ASC"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn into_domain(record: CafeRecord) -> Cafe {
|
|
||||||
let CafeRecord {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
slug,
|
|
||||||
city,
|
|
||||||
country,
|
|
||||||
latitude,
|
|
||||||
longitude,
|
|
||||||
website,
|
|
||||||
created_at,
|
|
||||||
updated_at,
|
|
||||||
} = record;
|
|
||||||
|
|
||||||
Cafe {
|
|
||||||
id: CafeId::from(id),
|
|
||||||
name,
|
|
||||||
slug,
|
|
||||||
city,
|
|
||||||
country,
|
|
||||||
latitude,
|
|
||||||
longitude,
|
|
||||||
website,
|
|
||||||
created_at,
|
|
||||||
updated_at,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|
@ -96,7 +68,7 @@ impl CafeRepository for SqlCafeRepository {
|
||||||
RepositoryError::unexpected(err.to_string())
|
RepositoryError::unexpected(err.to_string())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(Self::into_domain(record))
|
Ok(record.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get(&self, id: CafeId) -> Result<Cafe, RepositoryError> {
|
async fn get(&self, id: CafeId) -> Result<Cafe, RepositoryError> {
|
||||||
|
|
@ -109,7 +81,7 @@ impl CafeRepository for SqlCafeRepository {
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
||||||
|
|
||||||
match record {
|
match record {
|
||||||
Some(record) => Ok(Self::into_domain(record)),
|
Some(record) => Ok(record.into()),
|
||||||
None => Err(RepositoryError::NotFound),
|
None => Err(RepositoryError::NotFound),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -124,7 +96,7 @@ impl CafeRepository for SqlCafeRepository {
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
||||||
|
|
||||||
match record {
|
match record {
|
||||||
Some(record) => Ok(Self::into_domain(record)),
|
Some(record) => Ok(record.into()),
|
||||||
None => Err(RepositoryError::NotFound),
|
None => Err(RepositoryError::NotFound),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -148,7 +120,7 @@ impl CafeRepository for SqlCafeRepository {
|
||||||
count_query,
|
count_query,
|
||||||
&order_clause,
|
&order_clause,
|
||||||
sf.as_ref(),
|
sf.as_ref(),
|
||||||
|record| Ok(Self::into_domain(record)),
|
|record: CafeRecord| Ok(record.into()),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
@ -210,3 +182,20 @@ struct CafeRecord {
|
||||||
created_at: DateTime<Utc>,
|
created_at: DateTime<Utc>,
|
||||||
updated_at: DateTime<Utc>,
|
updated_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<CafeRecord> for Cafe {
|
||||||
|
fn from(record: CafeRecord) -> Self {
|
||||||
|
Cafe {
|
||||||
|
id: CafeId::from(record.id),
|
||||||
|
name: record.name,
|
||||||
|
slug: record.slug,
|
||||||
|
city: record.city,
|
||||||
|
country: record.country,
|
||||||
|
latitude: record.latitude,
|
||||||
|
longitude: record.longitude,
|
||||||
|
website: record.website,
|
||||||
|
created_at: record.created_at,
|
||||||
|
updated_at: record.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,35 +57,6 @@ impl SqlCupRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_domain(record: CupRecord) -> Cup {
|
|
||||||
Cup {
|
|
||||||
id: CupId::new(record.id),
|
|
||||||
roast_id: RoastId::new(record.roast_id),
|
|
||||||
cafe_id: CafeId::new(record.cafe_id),
|
|
||||||
created_at: record.created_at,
|
|
||||||
updated_at: record.updated_at,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_domain_with_details(record: CupWithDetailsRecord) -> CupWithDetails {
|
|
||||||
CupWithDetails {
|
|
||||||
cup: Cup {
|
|
||||||
id: CupId::new(record.id),
|
|
||||||
roast_id: RoastId::new(record.roast_id),
|
|
||||||
cafe_id: CafeId::new(record.cafe_id),
|
|
||||||
created_at: record.created_at,
|
|
||||||
updated_at: record.updated_at,
|
|
||||||
},
|
|
||||||
roast_name: record.roast_name,
|
|
||||||
roaster_name: record.roaster_name,
|
|
||||||
roast_slug: record.roast_slug,
|
|
||||||
roaster_slug: record.roaster_slug,
|
|
||||||
cafe_name: record.cafe_name,
|
|
||||||
cafe_slug: record.cafe_slug,
|
|
||||||
cafe_city: record.cafe_city,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_where_clause(filter: &CupFilter) -> Option<String> {
|
fn build_where_clause(filter: &CupFilter) -> Option<String> {
|
||||||
let mut conditions = Vec::new();
|
let mut conditions = Vec::new();
|
||||||
|
|
||||||
|
|
@ -121,7 +92,7 @@ impl CupRepository for SqlCupRepository {
|
||||||
.await
|
.await
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
||||||
|
|
||||||
Ok(Self::to_domain(record))
|
Ok(record.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get(&self, id: CupId) -> Result<Cup, RepositoryError> {
|
async fn get(&self, id: CupId) -> Result<Cup, RepositoryError> {
|
||||||
|
|
@ -134,7 +105,7 @@ impl CupRepository for SqlCupRepository {
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
||||||
|
|
||||||
match record {
|
match record {
|
||||||
Some(record) => Ok(Self::to_domain(record)),
|
Some(record) => Ok(record.into()),
|
||||||
None => Err(RepositoryError::NotFound),
|
None => Err(RepositoryError::NotFound),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -149,7 +120,7 @@ impl CupRepository for SqlCupRepository {
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
||||||
.ok_or(RepositoryError::NotFound)?;
|
.ok_or(RepositoryError::NotFound)?;
|
||||||
|
|
||||||
Ok(Self::to_domain_with_details(record))
|
Ok(record.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list(
|
async fn list(
|
||||||
|
|
@ -189,7 +160,7 @@ impl CupRepository for SqlCupRepository {
|
||||||
&count_query,
|
&count_query,
|
||||||
&order_clause,
|
&order_clause,
|
||||||
sf.as_ref(),
|
sf.as_ref(),
|
||||||
|record| Ok(Self::to_domain_with_details(record)),
|
|record: CupWithDetailsRecord| Ok(record.into()),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
@ -226,7 +197,7 @@ impl CupRepository for SqlCupRepository {
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
||||||
.ok_or(RepositoryError::NotFound)?;
|
.ok_or(RepositoryError::NotFound)?;
|
||||||
|
|
||||||
Ok(Self::to_domain(record))
|
Ok(record.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete(&self, id: CupId) -> Result<(), RepositoryError> {
|
async fn delete(&self, id: CupId) -> Result<(), RepositoryError> {
|
||||||
|
|
@ -253,6 +224,18 @@ struct CupRecord {
|
||||||
updated_at: DateTime<Utc>,
|
updated_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<CupRecord> for Cup {
|
||||||
|
fn from(record: CupRecord) -> Self {
|
||||||
|
Cup {
|
||||||
|
id: CupId::new(record.id),
|
||||||
|
roast_id: RoastId::new(record.roast_id),
|
||||||
|
cafe_id: CafeId::new(record.cafe_id),
|
||||||
|
created_at: record.created_at,
|
||||||
|
updated_at: record.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(sqlx::FromRow)]
|
#[derive(sqlx::FromRow)]
|
||||||
struct CupWithDetailsRecord {
|
struct CupWithDetailsRecord {
|
||||||
id: i64,
|
id: i64,
|
||||||
|
|
@ -268,3 +251,24 @@ struct CupWithDetailsRecord {
|
||||||
cafe_slug: String,
|
cafe_slug: String,
|
||||||
cafe_city: String,
|
cafe_city: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<CupWithDetailsRecord> for CupWithDetails {
|
||||||
|
fn from(record: CupWithDetailsRecord) -> Self {
|
||||||
|
CupWithDetails {
|
||||||
|
cup: Cup {
|
||||||
|
id: CupId::new(record.id),
|
||||||
|
roast_id: RoastId::new(record.roast_id),
|
||||||
|
cafe_id: CafeId::new(record.cafe_id),
|
||||||
|
created_at: record.created_at,
|
||||||
|
updated_at: record.updated_at,
|
||||||
|
},
|
||||||
|
roast_name: record.roast_name,
|
||||||
|
roaster_name: record.roaster_name,
|
||||||
|
roast_slug: record.roast_slug,
|
||||||
|
roaster_slug: record.roaster_slug,
|
||||||
|
cafe_name: record.cafe_name,
|
||||||
|
cafe_slug: record.cafe_slug,
|
||||||
|
cafe_city: record.cafe_city,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,21 +36,6 @@ impl SqlGearRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_domain(record: GearRecord) -> Result<Gear, RepositoryError> {
|
|
||||||
let category = GearCategory::from_str(&record.category).map_err(|()| {
|
|
||||||
RepositoryError::unexpected(format!("invalid category: {}", record.category))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(Gear {
|
|
||||||
id: GearId::new(record.id),
|
|
||||||
category,
|
|
||||||
make: record.make,
|
|
||||||
model: record.model,
|
|
||||||
created_at: record.created_at,
|
|
||||||
updated_at: record.updated_at,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_where_clause(filter: &GearFilter) -> Option<&'static str> {
|
fn build_where_clause(filter: &GearFilter) -> Option<&'static str> {
|
||||||
filter.category.as_ref().map(|category| match category {
|
filter.category.as_ref().map(|category| match category {
|
||||||
GearCategory::Grinder => "category = 'grinder'",
|
GearCategory::Grinder => "category = 'grinder'",
|
||||||
|
|
@ -80,7 +65,7 @@ impl GearRepository for SqlGearRepository {
|
||||||
.await
|
.await
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
||||||
|
|
||||||
Self::to_domain(record)
|
record.try_into()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get(&self, id: GearId) -> Result<Gear, RepositoryError> {
|
async fn get(&self, id: GearId) -> Result<Gear, RepositoryError> {
|
||||||
|
|
@ -97,7 +82,7 @@ impl GearRepository for SqlGearRepository {
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
||||||
.ok_or(RepositoryError::NotFound)?;
|
.ok_or(RepositoryError::NotFound)?;
|
||||||
|
|
||||||
Self::to_domain(record)
|
record.try_into()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list(
|
async fn list(
|
||||||
|
|
@ -134,7 +119,7 @@ impl GearRepository for SqlGearRepository {
|
||||||
&count_query,
|
&count_query,
|
||||||
&order_clause,
|
&order_clause,
|
||||||
sf.as_ref(),
|
sf.as_ref(),
|
||||||
Self::to_domain,
|
|record: GearRecord| record.try_into(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
@ -159,7 +144,7 @@ impl GearRepository for SqlGearRepository {
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
||||||
.ok_or(RepositoryError::NotFound)?;
|
.ok_or(RepositoryError::NotFound)?;
|
||||||
|
|
||||||
Self::to_domain(record)
|
record.try_into()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete(&self, id: GearId) -> Result<(), RepositoryError> {
|
async fn delete(&self, id: GearId) -> Result<(), RepositoryError> {
|
||||||
|
|
@ -188,3 +173,22 @@ struct GearRecord {
|
||||||
created_at: DateTime<Utc>,
|
created_at: DateTime<Utc>,
|
||||||
updated_at: DateTime<Utc>,
|
updated_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TryFrom<GearRecord> for Gear {
|
||||||
|
type Error = RepositoryError;
|
||||||
|
|
||||||
|
fn try_from(record: GearRecord) -> Result<Self, Self::Error> {
|
||||||
|
let category = GearCategory::from_str(&record.category).map_err(|()| {
|
||||||
|
RepositoryError::unexpected(format!("invalid category: {}", record.category))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Gear {
|
||||||
|
id: GearId::new(record.id),
|
||||||
|
category,
|
||||||
|
make: record.make,
|
||||||
|
model: record.model,
|
||||||
|
created_at: record.created_at,
|
||||||
|
updated_at: record.updated_at,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,28 +33,6 @@ impl SqlRoasterRepository {
|
||||||
RoasterSortKey::City => format!("LOWER(COALESCE(city, '')) {dir_sql}, LOWER(name) ASC"),
|
RoasterSortKey::City => format!("LOWER(COALESCE(city, '')) {dir_sql}, LOWER(name) ASC"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn into_domain(record: RoasterRecord) -> Roaster {
|
|
||||||
let RoasterRecord {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
slug,
|
|
||||||
country,
|
|
||||||
city,
|
|
||||||
homepage,
|
|
||||||
created_at,
|
|
||||||
} = record;
|
|
||||||
|
|
||||||
Roaster {
|
|
||||||
id: RoasterId::from(id),
|
|
||||||
name,
|
|
||||||
slug,
|
|
||||||
country,
|
|
||||||
city,
|
|
||||||
homepage,
|
|
||||||
created_at,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|
@ -87,7 +65,7 @@ impl RoasterRepository for SqlRoasterRepository {
|
||||||
RepositoryError::unexpected(err.to_string())
|
RepositoryError::unexpected(err.to_string())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(Self::into_domain(record))
|
Ok(record.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get(&self, id: RoasterId) -> Result<Roaster, RepositoryError> {
|
async fn get(&self, id: RoasterId) -> Result<Roaster, RepositoryError> {
|
||||||
|
|
@ -100,7 +78,7 @@ impl RoasterRepository for SqlRoasterRepository {
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
||||||
|
|
||||||
match record {
|
match record {
|
||||||
Some(record) => Ok(Self::into_domain(record)),
|
Some(record) => Ok(record.into()),
|
||||||
None => Err(RepositoryError::NotFound),
|
None => Err(RepositoryError::NotFound),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -115,7 +93,7 @@ impl RoasterRepository for SqlRoasterRepository {
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?;
|
||||||
|
|
||||||
match record {
|
match record {
|
||||||
Some(record) => Ok(Self::into_domain(record)),
|
Some(record) => Ok(record.into()),
|
||||||
None => Err(RepositoryError::NotFound),
|
None => Err(RepositoryError::NotFound),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -140,7 +118,7 @@ impl RoasterRepository for SqlRoasterRepository {
|
||||||
count_query,
|
count_query,
|
||||||
&order_clause,
|
&order_clause,
|
||||||
sf.as_ref(),
|
sf.as_ref(),
|
||||||
|record| Ok(Self::into_domain(record)),
|
|record: RoasterRecord| Ok(record.into()),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
@ -206,3 +184,17 @@ struct RoasterRecord {
|
||||||
homepage: Option<String>,
|
homepage: Option<String>,
|
||||||
created_at: DateTime<Utc>,
|
created_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<RoasterRecord> for Roaster {
|
||||||
|
fn from(record: RoasterRecord) -> Self {
|
||||||
|
Roaster {
|
||||||
|
id: RoasterId::from(record.id),
|
||||||
|
name: record.name,
|
||||||
|
slug: record.slug,
|
||||||
|
country: record.country,
|
||||||
|
city: record.city,
|
||||||
|
homepage: record.homepage,
|
||||||
|
created_at: record.created_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ impl RoastRepository for SqlRoastRepository {
|
||||||
map_insert_error(err, "unknown roaster reference")
|
map_insert_error(err, "unknown roaster reference")
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
record.into_roast()
|
record.try_into()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get(&self, id: RoastId) -> Result<Roast, RepositoryError> {
|
async fn get(&self, id: RoastId) -> Result<Roast, RepositoryError> {
|
||||||
|
|
@ -115,7 +115,7 @@ impl RoastRepository for SqlRoastRepository {
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
||||||
.map(RoastRecord::into_roast)
|
.map(Roast::try_from)
|
||||||
.transpose()?
|
.transpose()?
|
||||||
.ok_or(RepositoryError::NotFound)
|
.ok_or(RepositoryError::NotFound)
|
||||||
}
|
}
|
||||||
|
|
@ -131,7 +131,7 @@ impl RoastRepository for SqlRoastRepository {
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
||||||
.map(RoastWithRoasterRecord::into_with_roaster)
|
.map(RoastWithRoaster::try_from)
|
||||||
.transpose()?
|
.transpose()?
|
||||||
.ok_or(RepositoryError::NotFound)
|
.ok_or(RepositoryError::NotFound)
|
||||||
}
|
}
|
||||||
|
|
@ -149,7 +149,7 @@ impl RoastRepository for SqlRoastRepository {
|
||||||
.fetch_optional(&self.pool)
|
.fetch_optional(&self.pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
.map_err(|err| RepositoryError::unexpected(err.to_string()))?
|
||||||
.map(RoastRecord::into_roast)
|
.map(Roast::try_from)
|
||||||
.transpose()?
|
.transpose()?
|
||||||
.ok_or(RepositoryError::NotFound)
|
.ok_or(RepositoryError::NotFound)
|
||||||
}
|
}
|
||||||
|
|
@ -184,7 +184,7 @@ impl RoastRepository for SqlRoastRepository {
|
||||||
count_query,
|
count_query,
|
||||||
&order_clause,
|
&order_clause,
|
||||||
sf.as_ref(),
|
sf.as_ref(),
|
||||||
|record: RoastWithRoasterRecord| record.into_with_roaster(),
|
|record: RoastWithRoasterRecord| record.try_into(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
@ -203,7 +203,7 @@ impl RoastRepository for SqlRoastRepository {
|
||||||
|
|
||||||
records
|
records
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(RoastWithRoasterRecord::into_with_roaster)
|
.map(RoastWithRoaster::try_from)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -307,9 +307,11 @@ struct RoastRecord {
|
||||||
created_at: DateTime<Utc>,
|
created_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RoastRecord {
|
impl TryFrom<RoastRecord> for Roast {
|
||||||
fn into_roast(self) -> Result<Roast, RepositoryError> {
|
type Error = RepositoryError;
|
||||||
let tasting_notes = match self.tasting_notes {
|
|
||||||
|
fn try_from(record: RoastRecord) -> Result<Self, Self::Error> {
|
||||||
|
let tasting_notes = match record.tasting_notes {
|
||||||
Some(raw) => from_str::<Vec<String>>(&raw).map_err(|err| {
|
Some(raw) => from_str::<Vec<String>>(&raw).map_err(|err| {
|
||||||
RepositoryError::unexpected(format!("failed to decode tasting notes: {err}"))
|
RepositoryError::unexpected(format!("failed to decode tasting notes: {err}"))
|
||||||
})?,
|
})?,
|
||||||
|
|
@ -317,16 +319,16 @@ impl RoastRecord {
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Roast {
|
Ok(Roast {
|
||||||
id: RoastId::from(self.id),
|
id: RoastId::from(record.id),
|
||||||
roaster_id: RoasterId::from(self.roaster_id),
|
roaster_id: RoasterId::from(record.roaster_id),
|
||||||
name: self.name,
|
name: record.name,
|
||||||
slug: self.slug,
|
slug: record.slug,
|
||||||
origin: self.origin,
|
origin: record.origin,
|
||||||
region: self.region,
|
region: record.region,
|
||||||
producer: self.producer,
|
producer: record.producer,
|
||||||
process: self.process,
|
process: record.process,
|
||||||
tasting_notes,
|
tasting_notes,
|
||||||
created_at: self.created_at,
|
created_at: record.created_at,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -347,9 +349,11 @@ struct RoastWithRoasterRecord {
|
||||||
roaster_slug: String,
|
roaster_slug: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RoastWithRoasterRecord {
|
impl TryFrom<RoastWithRoasterRecord> for RoastWithRoaster {
|
||||||
fn into_with_roaster(self) -> Result<RoastWithRoaster, RepositoryError> {
|
type Error = RepositoryError;
|
||||||
let tasting_notes = match self.tasting_notes {
|
|
||||||
|
fn try_from(record: RoastWithRoasterRecord) -> Result<Self, Self::Error> {
|
||||||
|
let tasting_notes = match record.tasting_notes {
|
||||||
Some(raw) => from_str::<Vec<String>>(&raw).map_err(|err| {
|
Some(raw) => from_str::<Vec<String>>(&raw).map_err(|err| {
|
||||||
RepositoryError::unexpected(format!("failed to decode tasting notes: {err}"))
|
RepositoryError::unexpected(format!("failed to decode tasting notes: {err}"))
|
||||||
})?,
|
})?,
|
||||||
|
|
@ -358,19 +362,19 @@ impl RoastWithRoasterRecord {
|
||||||
|
|
||||||
Ok(RoastWithRoaster {
|
Ok(RoastWithRoaster {
|
||||||
roast: Roast {
|
roast: Roast {
|
||||||
id: RoastId::from(self.id),
|
id: RoastId::from(record.id),
|
||||||
roaster_id: RoasterId::from(self.roaster_id),
|
roaster_id: RoasterId::from(record.roaster_id),
|
||||||
name: self.name,
|
name: record.name,
|
||||||
slug: self.slug,
|
slug: record.slug,
|
||||||
origin: self.origin,
|
origin: record.origin,
|
||||||
region: self.region,
|
region: record.region,
|
||||||
producer: self.producer,
|
producer: record.producer,
|
||||||
process: self.process,
|
process: record.process,
|
||||||
tasting_notes,
|
tasting_notes,
|
||||||
created_at: self.created_at,
|
created_at: record.created_at,
|
||||||
},
|
},
|
||||||
roaster_name: self.roaster_name,
|
roaster_name: record.roaster_name,
|
||||||
roaster_slug: self.roaster_slug,
|
roaster_slug: record.roaster_slug,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue