Some checks failed
CI / Clippy (push) Failing after 3m6s
CI / Rustfmt (push) Failing after 3m9s
CI / Cargo Audit & Deny (push) Successful in 5m2s
CI / Tests (push) Successful in 8m15s
CI / Security Blocking Checks (push) Successful in 10s
CI / Web Advisory Checks (push) Successful in 1m4s
CI / Web Blocking Checks (push) Failing after 4m52s
Publish Images And Chart / Resolve Publish Metadata (push) Successful in 2s
CI / Security Advisory Checks (push) Successful in 1m31s
Publish Images And Chart / Publish init-user (push) Failing after 30s
Publish Images And Chart / Publish init-packs (push) Failing after 1m41s
Publish Images And Chart / Publish migrations (push) Failing after 10s
Publish Images And Chart / Publish web (push) Failing after 11s
Publish Images And Chart / Publish sensor (push) Failing after 32s
Publish Images And Chart / Publish worker (push) Failing after 11s
Publish Images And Chart / Publish executor (push) Failing after 11s
Publish Images And Chart / Publish notifier (push) Failing after 9s
Publish Images And Chart / Publish api (push) Failing after 31s
Publish Images And Chart / Publish Helm Chart (push) Has been skipped
551 lines
16 KiB
Rust
551 lines
16 KiB
Rust
//! Pack repository for database operations on packs
|
|
//!
|
|
//! This module provides CRUD operations and queries for Pack entities.
|
|
|
|
use crate::models::{pack::Pack, JsonDict, JsonSchema};
|
|
use crate::{Error, Result};
|
|
use sqlx::{Executor, Postgres, QueryBuilder};
|
|
|
|
use super::{Create, Delete, FindById, FindByRef, List, Pagination, Patch, Repository, Update};
|
|
|
|
/// Repository for Pack operations
|
|
pub struct PackRepository;
|
|
|
|
impl Repository for PackRepository {
|
|
type Entity = Pack;
|
|
|
|
fn table_name() -> &'static str {
|
|
"pack"
|
|
}
|
|
}
|
|
|
|
/// Input for creating a new pack
|
|
#[derive(Debug, Clone)]
|
|
pub struct CreatePackInput {
|
|
pub r#ref: String,
|
|
pub label: String,
|
|
pub description: Option<String>,
|
|
pub version: String,
|
|
pub conf_schema: JsonSchema,
|
|
pub config: JsonDict,
|
|
pub meta: JsonDict,
|
|
pub tags: Vec<String>,
|
|
pub runtime_deps: Vec<String>,
|
|
pub dependencies: Vec<String>,
|
|
pub is_standard: bool,
|
|
pub installers: JsonDict,
|
|
}
|
|
|
|
/// Input for updating a pack
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct UpdatePackInput {
|
|
pub label: Option<String>,
|
|
pub description: Option<Patch<String>>,
|
|
pub version: Option<String>,
|
|
pub conf_schema: Option<JsonSchema>,
|
|
pub config: Option<JsonDict>,
|
|
pub meta: Option<JsonDict>,
|
|
pub tags: Option<Vec<String>>,
|
|
pub runtime_deps: Option<Vec<String>>,
|
|
pub dependencies: Option<Vec<String>>,
|
|
pub is_standard: Option<bool>,
|
|
pub installers: Option<JsonDict>,
|
|
}
|
|
|
|
const PACK_COLUMNS: &str = "id, ref, label, description, version, conf_schema, config, meta, tags, runtime_deps, dependencies, is_standard, installers, source_type, source_url, source_ref, checksum, checksum_verified, installed_at, installed_by, installation_method, storage_path, created, updated";
|
|
|
|
#[async_trait::async_trait]
|
|
impl FindById for PackRepository {
|
|
async fn find_by_id<'e, E>(executor: E, id: i64) -> Result<Option<Self::Entity>>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let query = format!("SELECT {} FROM pack WHERE id = $1", PACK_COLUMNS);
|
|
let pack = sqlx::query_as::<_, Pack>(&query)
|
|
.bind(id)
|
|
.fetch_optional(executor)
|
|
.await?;
|
|
|
|
Ok(pack)
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl FindByRef for PackRepository {
|
|
async fn find_by_ref<'e, E>(executor: E, ref_str: &str) -> Result<Option<Self::Entity>>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let query = format!("SELECT {} FROM pack WHERE ref = $1", PACK_COLUMNS);
|
|
let pack = sqlx::query_as::<_, Pack>(&query)
|
|
.bind(ref_str)
|
|
.fetch_optional(executor)
|
|
.await?;
|
|
|
|
Ok(pack)
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl List for PackRepository {
|
|
async fn list<'e, E>(executor: E) -> Result<Vec<Self::Entity>>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let query = format!("SELECT {} FROM pack ORDER BY ref ASC", PACK_COLUMNS);
|
|
let packs = sqlx::query_as::<_, Pack>(&query)
|
|
.fetch_all(executor)
|
|
.await?;
|
|
|
|
Ok(packs)
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl Create for PackRepository {
|
|
type CreateInput = CreatePackInput;
|
|
|
|
async fn create<'e, E>(executor: E, input: Self::CreateInput) -> Result<Self::Entity>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
// Validate ref format (alphanumeric, dots, underscores, hyphens)
|
|
if !input
|
|
.r#ref
|
|
.chars()
|
|
.all(|c| c.is_alphanumeric() || c == '.' || c == '_' || c == '-')
|
|
{
|
|
return Err(Error::validation(
|
|
"Pack ref must contain only alphanumeric characters, dots, underscores, and hyphens",
|
|
));
|
|
}
|
|
|
|
let query = format!(
|
|
r#"
|
|
INSERT INTO pack (ref, label, description, version, conf_schema, config, meta,
|
|
tags, runtime_deps, dependencies, is_standard, installers)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
|
RETURNING {}
|
|
"#,
|
|
PACK_COLUMNS
|
|
);
|
|
|
|
// Try to insert - database will enforce uniqueness constraint
|
|
let pack = sqlx::query_as::<_, Pack>(&query)
|
|
.bind(&input.r#ref)
|
|
.bind(&input.label)
|
|
.bind(&input.description)
|
|
.bind(&input.version)
|
|
.bind(&input.conf_schema)
|
|
.bind(&input.config)
|
|
.bind(&input.meta)
|
|
.bind(&input.tags)
|
|
.bind(&input.runtime_deps)
|
|
.bind(&input.dependencies)
|
|
.bind(input.is_standard)
|
|
.bind(&input.installers)
|
|
.fetch_one(executor)
|
|
.await
|
|
.map_err(|e| {
|
|
// Convert unique constraint violation to AlreadyExists error
|
|
if let sqlx::Error::Database(db_err) = &e {
|
|
if db_err.is_unique_violation() {
|
|
return Error::already_exists("Pack", "ref", &input.r#ref);
|
|
}
|
|
}
|
|
e.into()
|
|
})?;
|
|
|
|
Ok(pack)
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl Update for PackRepository {
|
|
type UpdateInput = UpdatePackInput;
|
|
|
|
async fn update<'e, E>(executor: E, id: i64, input: Self::UpdateInput) -> Result<Self::Entity>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
// Build dynamic UPDATE query
|
|
let mut query = QueryBuilder::new("UPDATE pack SET ");
|
|
let mut has_updates = false;
|
|
|
|
if let Some(label) = &input.label {
|
|
if has_updates {
|
|
query.push(", ");
|
|
}
|
|
query.push("label = ");
|
|
query.push_bind(label);
|
|
has_updates = true;
|
|
}
|
|
|
|
if let Some(description) = &input.description {
|
|
if has_updates {
|
|
query.push(", ");
|
|
}
|
|
query.push("description = ");
|
|
match description {
|
|
Patch::Set(value) => query.push_bind(value),
|
|
Patch::Clear => query.push_bind(Option::<String>::None),
|
|
};
|
|
has_updates = true;
|
|
}
|
|
|
|
if let Some(version) = &input.version {
|
|
if has_updates {
|
|
query.push(", ");
|
|
}
|
|
query.push("version = ");
|
|
query.push_bind(version);
|
|
has_updates = true;
|
|
}
|
|
|
|
if let Some(conf_schema) = &input.conf_schema {
|
|
if has_updates {
|
|
query.push(", ");
|
|
}
|
|
query.push("conf_schema = ");
|
|
query.push_bind(conf_schema);
|
|
has_updates = true;
|
|
}
|
|
|
|
if let Some(config) = &input.config {
|
|
if has_updates {
|
|
query.push(", ");
|
|
}
|
|
query.push("config = ");
|
|
query.push_bind(config);
|
|
has_updates = true;
|
|
}
|
|
|
|
if let Some(meta) = &input.meta {
|
|
if has_updates {
|
|
query.push(", ");
|
|
}
|
|
query.push("meta = ");
|
|
query.push_bind(meta);
|
|
has_updates = true;
|
|
}
|
|
|
|
if let Some(tags) = &input.tags {
|
|
if has_updates {
|
|
query.push(", ");
|
|
}
|
|
query.push("tags = ");
|
|
query.push_bind(tags);
|
|
has_updates = true;
|
|
}
|
|
|
|
if let Some(runtime_deps) = &input.runtime_deps {
|
|
if has_updates {
|
|
query.push(", ");
|
|
}
|
|
query.push("runtime_deps = ");
|
|
query.push_bind(runtime_deps);
|
|
has_updates = true;
|
|
}
|
|
|
|
if let Some(dependencies) = &input.dependencies {
|
|
if has_updates {
|
|
query.push(", ");
|
|
}
|
|
query.push("dependencies = ");
|
|
query.push_bind(dependencies);
|
|
has_updates = true;
|
|
}
|
|
|
|
if let Some(is_standard) = input.is_standard {
|
|
if has_updates {
|
|
query.push(", ");
|
|
}
|
|
query.push("is_standard = ");
|
|
query.push_bind(is_standard);
|
|
has_updates = true;
|
|
}
|
|
|
|
if let Some(installers) = &input.installers {
|
|
if has_updates {
|
|
query.push(", ");
|
|
}
|
|
query.push("installers = ");
|
|
query.push_bind(installers);
|
|
has_updates = true;
|
|
}
|
|
|
|
if !has_updates {
|
|
// No updates requested, fetch and return existing pack
|
|
return Self::find_by_id(executor, id)
|
|
.await?
|
|
.ok_or_else(|| Error::not_found("pack", "id", id.to_string()));
|
|
}
|
|
|
|
// Add updated timestamp
|
|
query.push(", updated = NOW() WHERE id = ");
|
|
query.push_bind(id);
|
|
query.push(" RETURNING ");
|
|
query.push(PACK_COLUMNS);
|
|
|
|
let pack = query
|
|
.build_query_as::<Pack>()
|
|
.fetch_one(executor)
|
|
.await
|
|
.map_err(|e| match e {
|
|
sqlx::Error::RowNotFound => Error::not_found("pack", "id", id.to_string()),
|
|
_ => e.into(),
|
|
})?;
|
|
|
|
Ok(pack)
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl Delete for PackRepository {
|
|
async fn delete<'e, E>(executor: E, id: i64) -> Result<bool>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let result = sqlx::query("DELETE FROM pack WHERE id = $1")
|
|
.bind(id)
|
|
.execute(executor)
|
|
.await?;
|
|
|
|
Ok(result.rows_affected() > 0)
|
|
}
|
|
}
|
|
|
|
impl PackRepository {
|
|
/// List packs with pagination
|
|
pub async fn list_paginated<'e, E>(executor: E, pagination: Pagination) -> Result<Vec<Pack>>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let query = format!(
|
|
"SELECT {} FROM pack ORDER BY ref ASC LIMIT $1 OFFSET $2",
|
|
PACK_COLUMNS
|
|
);
|
|
let packs = sqlx::query_as::<_, Pack>(&query)
|
|
.bind(pagination.limit())
|
|
.bind(pagination.offset())
|
|
.fetch_all(executor)
|
|
.await?;
|
|
|
|
Ok(packs)
|
|
}
|
|
|
|
/// Count total number of packs
|
|
pub async fn count<'e, E>(executor: E) -> Result<i64>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM pack")
|
|
.fetch_one(executor)
|
|
.await?;
|
|
|
|
Ok(count.0)
|
|
}
|
|
|
|
/// Find packs by tag
|
|
pub async fn find_by_tag<'e, E>(executor: E, tag: &str) -> Result<Vec<Pack>>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let query = format!(
|
|
"SELECT {} FROM pack WHERE $1 = ANY(tags) ORDER BY ref ASC",
|
|
PACK_COLUMNS
|
|
);
|
|
let packs = sqlx::query_as::<_, Pack>(&query)
|
|
.bind(tag)
|
|
.fetch_all(executor)
|
|
.await?;
|
|
|
|
Ok(packs)
|
|
}
|
|
|
|
/// Find standard packs
|
|
pub async fn find_standard<'e, E>(executor: E) -> Result<Vec<Pack>>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let query = format!(
|
|
"SELECT {} FROM pack WHERE is_standard = true ORDER BY ref ASC",
|
|
PACK_COLUMNS
|
|
);
|
|
let packs = sqlx::query_as::<_, Pack>(&query)
|
|
.fetch_all(executor)
|
|
.await?;
|
|
|
|
Ok(packs)
|
|
}
|
|
|
|
/// Search packs by name/label (case-insensitive)
|
|
pub async fn search<'e, E>(executor: E, query: &str) -> Result<Vec<Pack>>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let search_pattern = format!("%{}%", query.to_lowercase());
|
|
let sql = format!(
|
|
"SELECT {} FROM pack WHERE LOWER(ref) LIKE $1 OR LOWER(label) LIKE $1 OR LOWER(description) LIKE $1 ORDER BY ref ASC",
|
|
PACK_COLUMNS
|
|
);
|
|
let packs = sqlx::query_as::<_, Pack>(&sql)
|
|
.bind(&search_pattern)
|
|
.fetch_all(executor)
|
|
.await?;
|
|
|
|
Ok(packs)
|
|
}
|
|
|
|
/// Check if a pack with the given ref exists
|
|
pub async fn exists_by_ref<'e, E>(executor: E, ref_str: &str) -> Result<bool>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let exists: (bool,) = sqlx::query_as("SELECT EXISTS(SELECT 1 FROM pack WHERE ref = $1)")
|
|
.bind(ref_str)
|
|
.fetch_one(executor)
|
|
.await?;
|
|
|
|
Ok(exists.0)
|
|
}
|
|
|
|
/// Update installation metadata for a pack
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub async fn update_installation_metadata<'e, E>(
|
|
executor: E,
|
|
id: i64,
|
|
source_type: String,
|
|
source_url: Option<String>,
|
|
source_ref: Option<String>,
|
|
checksum: Option<String>,
|
|
checksum_verified: bool,
|
|
installed_by: Option<i64>,
|
|
installation_method: String,
|
|
storage_path: String,
|
|
) -> Result<Pack>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let query = format!(
|
|
r#"
|
|
UPDATE pack
|
|
SET source_type = $2,
|
|
source_url = $3,
|
|
source_ref = $4,
|
|
checksum = $5,
|
|
checksum_verified = $6,
|
|
installed_at = NOW(),
|
|
installed_by = $7,
|
|
installation_method = $8,
|
|
storage_path = $9,
|
|
updated = NOW()
|
|
WHERE id = $1
|
|
RETURNING {}
|
|
"#,
|
|
PACK_COLUMNS
|
|
);
|
|
let pack = sqlx::query_as::<_, Pack>(&query)
|
|
.bind(id)
|
|
.bind(source_type)
|
|
.bind(source_url)
|
|
.bind(source_ref)
|
|
.bind(checksum)
|
|
.bind(checksum_verified)
|
|
.bind(installed_by)
|
|
.bind(installation_method)
|
|
.bind(storage_path)
|
|
.fetch_one(executor)
|
|
.await
|
|
.map_err(|e| match e {
|
|
sqlx::Error::RowNotFound => Error::not_found("pack", "id", id.to_string()),
|
|
_ => e.into(),
|
|
})?;
|
|
|
|
Ok(pack)
|
|
}
|
|
|
|
/// Check if a pack has installation metadata
|
|
pub async fn is_installed<'e, E>(executor: E, pack_id: i64) -> Result<bool>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let exists: (bool,) = sqlx::query_as(
|
|
"SELECT EXISTS(SELECT 1 FROM pack WHERE id = $1 AND installed_at IS NOT NULL)",
|
|
)
|
|
.bind(pack_id)
|
|
.fetch_one(executor)
|
|
.await?;
|
|
|
|
Ok(exists.0)
|
|
}
|
|
|
|
/// List all installed packs
|
|
pub async fn list_installed<'e, E>(executor: E) -> Result<Vec<Pack>>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let query = format!(
|
|
"SELECT {} FROM pack WHERE installed_at IS NOT NULL ORDER BY installed_at DESC",
|
|
PACK_COLUMNS
|
|
);
|
|
let packs = sqlx::query_as::<_, Pack>(&query)
|
|
.fetch_all(executor)
|
|
.await?;
|
|
|
|
Ok(packs)
|
|
}
|
|
|
|
/// List packs by source type
|
|
pub async fn list_by_source_type<'e, E>(executor: E, source_type: &str) -> Result<Vec<Pack>>
|
|
where
|
|
E: Executor<'e, Database = Postgres> + 'e,
|
|
{
|
|
let query = format!(
|
|
"SELECT {} FROM pack WHERE source_type = $1 ORDER BY installed_at DESC",
|
|
PACK_COLUMNS
|
|
);
|
|
let packs = sqlx::query_as::<_, Pack>(&query)
|
|
.bind(source_type)
|
|
.fetch_all(executor)
|
|
.await?;
|
|
|
|
Ok(packs)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_create_pack_input() {
|
|
let input = CreatePackInput {
|
|
r#ref: "test.pack".to_string(),
|
|
label: "Test Pack".to_string(),
|
|
description: Some("A test pack".to_string()),
|
|
version: "1.0.0".to_string(),
|
|
conf_schema: serde_json::json!({}),
|
|
config: serde_json::json!({}),
|
|
meta: serde_json::json!({}),
|
|
tags: vec!["test".to_string()],
|
|
runtime_deps: vec![],
|
|
dependencies: vec![],
|
|
is_standard: false,
|
|
installers: serde_json::json!({}),
|
|
};
|
|
|
|
assert_eq!(input.r#ref, "test.pack");
|
|
assert_eq!(input.label, "Test Pack");
|
|
}
|
|
|
|
#[test]
|
|
fn test_update_pack_input_default() {
|
|
let input = UpdatePackInput::default();
|
|
assert!(input.label.is_none());
|
|
assert!(input.description.is_none());
|
|
assert!(input.version.is_none());
|
|
assert!(input.dependencies.is_none());
|
|
}
|
|
}
|