use crate::storage::DbPool;
use async_trait::async_trait;
use sea_query::{Asterisk, Expr, ExprTrait, IntoIden, Order, Query};
use serde::Deserialize;
use super::helpers::{Page, PaginatedResult, Projection};
use crate::errors::OrionError;
use crate::storage::models::Connector;
use crate::storage::{build_sqlx, schema::Connectors};
#[derive(Debug, Deserialize, utoipa::ToSchema)]
pub struct CreateConnectorRequest {
pub id: Option<String>,
pub name: String,
pub connector_type: crate::connector::ConnectorType,
#[serde(default = "default_config")]
pub config: serde_json::Value,
pub enabled: Option<bool>,
#[serde(default)]
pub tags: Vec<String>,
}
fn default_config() -> serde_json::Value {
serde_json::json!({})
}
#[derive(Debug, Deserialize, utoipa::ToSchema)]
pub struct UpdateConnectorRequest {
pub name: Option<String>,
pub connector_type: Option<crate::connector::ConnectorType>,
pub config: Option<serde_json::Value>,
pub enabled: Option<bool>,
pub tags: Option<Vec<String>>,
}
#[derive(Debug, Default, Deserialize, serde::Serialize, utoipa::IntoParams)]
#[into_params(parameter_in = Query)]
pub struct ConnectorFilter {
pub tag: Option<String>,
pub limit: Option<i64>,
pub offset: Option<i64>,
pub sort_by: Option<String>,
pub sort_order: Option<String>,
}
fn build_condition(filter: &ConnectorFilter) -> sea_query::Condition {
let mut cond = sea_query::Condition::all();
if let Some(ref tag) = filter.tag {
cond = cond.add(
Expr::col(Connectors::TagsJson).like(super::helpers::tag_like_pattern(tag.as_str())),
);
}
cond
}
#[async_trait]
pub trait ConnectorRepository: Send + Sync {
async fn create(&self, req: &CreateConnectorRequest) -> Result<Connector, OrionError>;
async fn get_by_id(&self, id: &str) -> Result<Connector, OrionError>;
async fn list_paginated(
&self,
filter: &ConnectorFilter,
) -> Result<PaginatedResult<Connector>, OrionError>;
async fn update(&self, id: &str, req: &UpdateConnectorRequest)
-> Result<Connector, OrionError>;
async fn delete(&self, id: &str) -> Result<(), OrionError>;
async fn list_enabled(&self) -> Result<Vec<Connector>, OrionError>;
async fn exists_by_name(&self, name: &str) -> Result<bool, OrionError>;
async fn get_by_name(&self, name: &str) -> Result<Connector, OrionError>;
async fn snapshot(&self, filter: &ConnectorFilter) -> Result<Vec<Connector>, OrionError>;
}
fn connector_select(id: &str) -> sea_query::SelectStatement {
Query::select()
.column(Asterisk)
.from(Connectors::Table)
.and_where(Expr::col(Connectors::Id).eq(id))
.to_owned()
}
fn connector_not_found(id: &str) -> OrionError {
OrionError::NotFound(format!("Connector '{id}' not found"))
}
pub struct SqlConnectorRepository {
pool: DbPool,
cipher: Option<std::sync::Arc<crate::storage::config_encryption::ConfigCipher>>,
}
impl SqlConnectorRepository {
pub fn new(pool: DbPool) -> Self {
Self { pool, cipher: None }
}
pub fn with_cipher(
pool: DbPool,
cipher: Option<std::sync::Arc<crate::storage::config_encryption::ConfigCipher>>,
) -> Self {
Self { pool, cipher }
}
fn store_form(&self, config_json: &str) -> Result<String, OrionError> {
match &self.cipher {
Some(cipher) => cipher.encrypt(config_json),
None => Ok(config_json.to_string()),
}
}
fn open_row(&self, mut row: Connector) -> Result<Connector, OrionError> {
use crate::storage::config_encryption::ConfigCipher;
row.config_json = match &self.cipher {
Some(cipher) => cipher.decrypt(&row.config_json)?,
None if ConfigCipher::is_encrypted(&row.config_json) => {
return Err(OrionError::internal(format!(
"connector '{}' is encrypted at rest but \
storage.connector_encryption_key is not set",
row.id
)));
}
None => row.config_json,
};
Ok(row)
}
}
#[async_trait]
impl ConnectorRepository for SqlConnectorRepository {
async fn create(&self, req: &CreateConnectorRequest) -> Result<Connector, OrionError> {
crate::metrics::timed_db_op("connectors.create", async {
let id = req
.id
.clone()
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
let config_json = self.store_form(&serde_json::to_string(&req.config)?)?;
let tags_json = serde_json::to_string(&req.tags)?;
let mut insert = Query::insert();
insert
.into_table(Connectors::Table)
.columns([
Connectors::Id,
Connectors::Name,
Connectors::ConnectorType,
Connectors::ConfigJson,
Connectors::Enabled,
Connectors::TagsJson,
])
.values_panic([
id.as_str().into(),
req.name.as_str().into(),
req.connector_type.as_str().into(),
config_json.as_str().into(),
req.enabled.unwrap_or(true).into(),
tags_json.as_str().into(),
]);
let row = super::helpers::write_returning_row(
&self.pool,
super::helpers::WriteStatement::Insert(&mut insert),
&mut connector_select(&id),
|e| {
super::helpers::map_duplicate(e, || {
format!("Connector with name '{}' already exists", req.name)
})
},
|| connector_not_found(&id),
)
.await?;
self.open_row(row)
})
.await
}
async fn get_by_id(&self, id: &str) -> Result<Connector, OrionError> {
crate::metrics::timed_db_op("connectors.get_by_id", async {
let (sql, values) = build_sqlx(&mut connector_select(id));
self.pool
.fetch_optional_as::<Connector>(&sql, values)
.await?
.ok_or_else(|| connector_not_found(id))
.and_then(|row| self.open_row(row))
})
.await
}
async fn list_paginated(
&self,
filter: &ConnectorFilter,
) -> Result<PaginatedResult<Connector>, OrionError> {
crate::metrics::timed_db_op("connectors.list_paginated", async {
let (limit, offset) = super::helpers::clamp_pagination(filter.limit, filter.offset);
let sort_iden = match filter.sort_by.as_deref() {
Some("connector_type") => Connectors::ConnectorType,
Some("created_at") => Connectors::CreatedAt,
Some("updated_at") => Connectors::UpdatedAt,
_ => Connectors::Name,
};
let order = match filter.sort_order.as_deref() {
Some("desc") => Order::Desc,
_ => Order::Asc,
};
let cond = build_condition(filter);
let page: PaginatedResult<Connector> = super::helpers::paginate(
&self.pool,
Page {
from: Connectors::Table.into_iden(),
projection: Projection::All,
cond,
sort: sort_iden.into_iden(),
order,
limit,
offset,
},
)
.await?;
Ok(PaginatedResult {
data: page
.data
.into_iter()
.map(|row| self.open_row(row))
.collect::<Result<_, _>>()?,
total: page.total,
limit: page.limit,
offset: page.offset,
})
})
.await
}
async fn update(
&self,
id: &str,
req: &UpdateConnectorRequest,
) -> Result<Connector, OrionError> {
crate::metrics::timed_db_op("connectors.update", async {
let existing = self.get_by_id(id).await?;
let name = req.name.as_deref().unwrap_or(&existing.name);
let connector_type: &str = req
.connector_type
.as_ref()
.map(|c| c.as_str())
.unwrap_or(existing.connector_type.as_str());
let config_json = self.store_form(&match &req.config {
Some(c) => serde_json::to_string(c)?,
None => existing.config_json.clone(),
})?;
let enabled = req.enabled.unwrap_or(existing.enabled);
let tags_json = match &req.tags {
Some(t) => serde_json::to_string(t)?,
None => existing.tags_json.clone(),
};
let mut update = Query::update()
.table(Connectors::Table)
.value(Connectors::Name, name)
.value(Connectors::ConnectorType, connector_type)
.value(Connectors::ConfigJson, &config_json)
.value(Connectors::Enabled, enabled)
.value(Connectors::TagsJson, tags_json.as_str())
.and_where(Expr::col(Connectors::Id).eq(id))
.to_owned();
let row = super::helpers::write_returning_row(
&self.pool,
super::helpers::WriteStatement::Update(&mut update),
&mut connector_select(id),
OrionError::Storage,
|| connector_not_found(id),
)
.await?;
self.open_row(row)
})
.await
}
async fn delete(&self, id: &str) -> Result<(), OrionError> {
crate::metrics::timed_db_op("connectors.delete", async {
let (sql, values) = build_sqlx(
Query::delete()
.from_table(Connectors::Table)
.and_where(Expr::col(Connectors::Id).eq(id)),
);
let rows_affected = self.pool.execute_query(&sql, values).await?;
if rows_affected == 0 {
return Err(OrionError::NotFound(format!("Connector '{id}' not found")));
}
Ok(())
})
.await
}
async fn list_enabled(&self) -> Result<Vec<Connector>, OrionError> {
crate::metrics::timed_db_op("connectors.list_enabled", async {
let (sql, values) = build_sqlx(
Query::select()
.column(Asterisk)
.from(Connectors::Table)
.and_where(Expr::col(Connectors::Enabled).eq(true))
.order_by(Connectors::Name, Order::Asc),
);
self.pool
.fetch_all_as::<Connector>(&sql, values)
.await?
.into_iter()
.map(|row| self.open_row(row))
.collect()
})
.await
}
async fn exists_by_name(&self, name: &str) -> Result<bool, OrionError> {
crate::metrics::timed_db_op("connectors.exists_by_name", async {
Ok(super::helpers::count_where(
&self.pool,
Connectors::Table,
sea_query::Condition::all().add(Expr::col(Connectors::Name).eq(name)),
)
.await?
> 0)
})
.await
}
async fn snapshot(&self, filter: &ConnectorFilter) -> Result<Vec<Connector>, OrionError> {
crate::metrics::timed_db_op("connectors.snapshot", async {
let rows: Vec<Connector> = super::helpers::snapshot_pages(
&self.pool,
super::helpers::EXPORT_PAGE_SIZE,
|limit, offset| {
Query::select()
.column(Asterisk)
.from(Connectors::Table)
.cond_where(build_condition(filter))
.order_by(Connectors::Name, Order::Asc)
.limit(limit as u64)
.offset(offset as u64)
.to_owned()
},
)
.await?;
rows.into_iter().map(|row| self.open_row(row)).collect()
})
.await
}
async fn get_by_name(&self, name: &str) -> Result<Connector, OrionError> {
crate::metrics::timed_db_op("connectors.get_by_name", async {
let (sql, values) = build_sqlx(
Query::select()
.column(Asterisk)
.from(Connectors::Table)
.and_where(Expr::col(Connectors::Name).eq(name)),
);
super::helpers::fetch_required::<Connector>(&self.pool, &sql, values, || {
OrionError::NotFound(format!("Connector '{name}' not found"))
})
.await
.and_then(|row| self.open_row(row))
})
.await
}
}