Lines
98.62 %
Functions
17.29 %
Branches
100 %
pub mod user {
use crate::db::DBError;
use crate::error::ServerError;
use crate::user::User;
use finance::tag::Tag;
use sqlx::Connection;
use sqlx::types::Uuid;
const KIND_AUTOMATION: &str = "automation";
const KIND_TEMPLATE: &str = "template";
pub struct ScriptInfo {
pub id: Uuid,
pub name: Option<String>,
pub size: i32,
pub enabled: Option<String>,
}
pub struct ScriptDetail {
pub bytecode: Vec<u8>,
pub struct TemplateInfo {
pub struct TemplateDetail {
pub source: String,
fn db_err(err: sqlx::Error) -> ServerError {
log::error!("{}", t!("Database error: %{err}", err = err : {:?}));
ServerError::DB(DBError::Sqlx(err))
impl User {
/// Upsert a canonical `(name, value)` tag and link it to `artifact_id`
/// using the id the upsert RETURNS — never a locally minted uuid, which
/// would dangle when the canonical row already exists.
async fn link_artifact_tag(
conn: &mut sqlx::PgConnection,
artifact_id: Uuid,
name: &str,
value: &str,
) -> Result<(), ServerError> {
let canonical_id = Tag {
id: Uuid::new_v4(),
tag_name: name.to_string(),
tag_value: value.to_string(),
description: None,
.commit(&mut *conn)
.await
.map_err(|err| {
ServerError::Finance(err)
})?;
sqlx::query_file!(
"sql/insert/artifact_tags/artifact_tag.sql",
&artifact_id,
&canonical_id
)
.execute(&mut *conn)
.map_err(db_err)?;
Ok(())
pub async fn list_scripts(&self) -> Result<Vec<ScriptInfo>, ServerError> {
let mut conn = self.get_connection().await?;
let rows = sqlx::query_file!("sql/select/artifacts/by_kind.sql", KIND_AUTOMATION)
.fetch_all(&mut *conn)
Ok(rows
.into_iter()
.map(|r| ScriptInfo {
id: r.id,
name: r.artifact_name,
size: r.size.unwrap_or(0),
enabled: r.enabled,
})
.collect())
pub async fn get_script(&self, id: Uuid) -> Result<ScriptDetail, ServerError> {
let row = sqlx::query_file!("sql/select/artifacts/by_id.sql", &id, KIND_AUTOMATION)
.fetch_one(&mut *conn)
Ok(ScriptDetail {
id: row.id,
name: row.artifact_name,
bytecode: row.bytecode.unwrap_or_default(),
enabled: row.enabled,
pub async fn create_script(
&self,
bytecode: Vec<u8>,
name: Option<String>,
) -> Result<Uuid, ServerError> {
let mut tx = conn.begin().await.map_err(db_err)?;
let id = Uuid::new_v4();
"sql/insert/artifacts/artifact.sql",
&id,
Some(&bytecode[..]),
None::<&str>
.execute(&mut *tx)
User::link_artifact_tag(&mut tx, id, "kind", KIND_AUTOMATION).await?;
if let Some(name) = name {
User::link_artifact_tag(&mut tx, id, "name", &name).await?;
tx.commit().await.map_err(db_err)?;
Ok(id)
pub async fn update_script_bytecode(
id: Uuid,
sqlx::query_file!("sql/update/artifacts/bytecode.sql", &id, &bytecode)
pub async fn delete_script(&self, id: Uuid) -> Result<(), ServerError> {
self.delete_artifact(id).await
pub async fn set_script_enabled(&self, id: Uuid, enabled: bool) -> Result<(), ServerError> {
self.set_artifact_enabled(id, enabled).await
pub async fn update_script_name(
self.update_artifact_name(id, name).await
pub async fn list_templates(&self) -> Result<Vec<TemplateInfo>, ServerError> {
let rows = sqlx::query_file!("sql/select/artifacts/by_kind.sql", KIND_TEMPLATE)
.map(|r| TemplateInfo {
pub async fn get_template(&self, id: Uuid) -> Result<TemplateDetail, ServerError> {
let row = sqlx::query_file!("sql/select/artifacts/by_id.sql", &id, KIND_TEMPLATE)
Ok(TemplateDetail {
source: row.source.unwrap_or_default(),
pub async fn create_template(
source: String,
None::<&[u8]>,
Some(source.as_str())
User::link_artifact_tag(&mut tx, id, "kind", KIND_TEMPLATE).await?;
pub async fn update_template_source(
sqlx::query_file!("sql/update/artifacts/source.sql", &id, &source)
pub async fn delete_template(&self, id: Uuid) -> Result<(), ServerError> {
pub async fn update_template_name(
/// Lifecycle delete: detach links and drop the row in ONE transaction so
/// the deferred kind-tag DELETE guard sees the parent gone at commit.
async fn delete_artifact(&self, id: Uuid) -> Result<(), ServerError> {
sqlx::query_file!("sql/delete/artifact_tags/by_artifact.sql", &id)
sqlx::query_file!("sql/delete/artifacts/by_id.sql", &id)
async fn set_artifact_enabled(&self, id: Uuid, enabled: bool) -> Result<(), ServerError> {
"sql/delete/artifact_tags/by_artifact_and_name.sql",
"enabled"
if !enabled {
User::link_artifact_tag(&mut tx, id, "enabled", "false").await?;
async fn update_artifact_name(
"name"
#[cfg(test)]
mod artifact_mgmt_tests;