use sqlx::PgPool;
use uuid::Uuid;
use super::models::{NewSpec, Spec, SpecRevision, SpecRevisionSummary};
#[derive(Clone)]
pub struct SpecsRepository {
pool: PgPool,
}
impl SpecsRepository {
pub fn new(pool: PgPool) -> Self {
Self { pool }
}
pub async fn create(&self, spec: NewSpec) -> Result<Spec, sqlx::Error> {
let mut tx = self.pool.begin().await?;
let row = sqlx::query_as::<_, Spec>(
r#"
INSERT INTO specs (project_id, name, current_sha256, spec_type, spec_version)
VALUES ($1, $2, $3, $4, $5)
RETURNING *
"#,
)
.bind(spec.project_id)
.bind(&spec.name)
.bind(&spec.sha256)
.bind(&spec.spec_type)
.bind(&spec.spec_version)
.fetch_one(&mut *tx)
.await?;
sqlx::query(
r#"
INSERT INTO spec_revisions (spec_id, revision, sha256, content, filename)
VALUES ($1, 1, $2, $3, $4)
"#,
)
.bind(row.id)
.bind(&spec.sha256)
.bind(&spec.content)
.bind(&spec.filename)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(row)
}
pub async fn list(
&self,
spec_type: Option<&str>,
name: Option<&str>,
) -> Result<Vec<Spec>, sqlx::Error> {
let mut query = String::from("SELECT * FROM specs WHERE 1=1");
let mut params: Vec<String> = vec![];
if let Some(t) = spec_type {
params.push(t.to_string());
query.push_str(&format!(" AND spec_type = ${}", params.len()));
}
if let Some(n) = name {
params.push(n.to_string());
query.push_str(&format!(" AND name ILIKE ${}", params.len()));
}
query.push_str(" ORDER BY name");
let mut q = sqlx::query_as::<_, Spec>(&query);
for param in ¶ms {
q = q.bind(param);
}
q.fetch_all(&self.pool).await
}
pub async fn list_for_project(&self, project_id: Uuid) -> Result<Vec<Spec>, sqlx::Error> {
sqlx::query_as::<_, Spec>("SELECT * FROM specs WHERE project_id = $1 ORDER BY name")
.bind(project_id)
.fetch_all(&self.pool)
.await
}
pub async fn get_by_id(&self, id: Uuid) -> Result<Option<Spec>, sqlx::Error> {
sqlx::query_as::<_, Spec>("SELECT * FROM specs WHERE id = $1")
.bind(id)
.fetch_optional(&self.pool)
.await
}
pub async fn get_by_project_and_name(
&self,
project_id: Uuid,
name: &str,
) -> Result<Option<Spec>, sqlx::Error> {
sqlx::query_as::<_, Spec>("SELECT * FROM specs WHERE project_id = $1 AND name = $2")
.bind(project_id)
.bind(name)
.fetch_optional(&self.pool)
.await
}
pub async fn get_latest_revision(
&self,
spec_id: Uuid,
) -> Result<Option<SpecRevision>, sqlx::Error> {
sqlx::query_as::<_, SpecRevision>(
r#"
SELECT * FROM spec_revisions
WHERE spec_id = $1
ORDER BY revision DESC
LIMIT 1
"#,
)
.bind(spec_id)
.fetch_optional(&self.pool)
.await
}
#[allow(clippy::too_many_arguments)]
pub async fn update(
&self,
project_id: Uuid,
name: &str,
spec_type: &str,
spec_version: &str,
sha256: &str,
content: Vec<u8>,
filename: &str,
) -> Result<(Spec, i32), sqlx::Error> {
let mut tx = self.pool.begin().await?;
let spec: Spec = sqlx::query_as("SELECT * FROM specs WHERE project_id = $1 AND name = $2")
.bind(project_id)
.bind(name)
.fetch_one(&mut *tx)
.await?;
let next_revision: i32 = sqlx::query_scalar(
"SELECT COALESCE(MAX(revision), 0) + 1 FROM spec_revisions WHERE spec_id = $1",
)
.bind(spec.id)
.fetch_one(&mut *tx)
.await?;
sqlx::query(
r#"
INSERT INTO spec_revisions (spec_id, revision, sha256, content, filename)
VALUES ($1, $2, $3, $4, $5)
"#,
)
.bind(spec.id)
.bind(next_revision)
.bind(sha256)
.bind(&content)
.bind(filename)
.execute(&mut *tx)
.await?;
let updated_spec = sqlx::query_as::<_, Spec>(
r#"
UPDATE specs
SET current_sha256 = $2, spec_type = $3, spec_version = $4, updated_at = NOW()
WHERE id = $1
RETURNING *
"#,
)
.bind(spec.id)
.bind(sha256)
.bind(spec_type)
.bind(spec_version)
.fetch_one(&mut *tx)
.await?;
tx.commit().await?;
Ok((updated_spec, next_revision))
}
pub async fn delete(&self, id: Uuid) -> Result<bool, sqlx::Error> {
let result = sqlx::query("DELETE FROM specs WHERE id = $1")
.bind(id)
.execute(&self.pool)
.await?;
Ok(result.rows_affected() > 0)
}
pub async fn get_history(
&self,
spec_id: Uuid,
) -> Result<Vec<SpecRevisionSummary>, sqlx::Error> {
let revisions = sqlx::query_as::<_, SpecRevision>(
r#"
SELECT * FROM spec_revisions
WHERE spec_id = $1
ORDER BY revision DESC
"#,
)
.bind(spec_id)
.fetch_all(&self.pool)
.await?;
Ok(revisions
.into_iter()
.map(|r| SpecRevisionSummary {
revision: r.revision,
sha256: r.sha256,
filename: r.filename,
created_at: r.created_at,
})
.collect())
}
pub async fn get_revision(
&self,
spec_id: Uuid,
revision: i32,
) -> Result<Option<SpecRevision>, sqlx::Error> {
sqlx::query_as::<_, SpecRevision>(
r#"
SELECT * FROM spec_revisions
WHERE spec_id = $1 AND revision = $2
"#,
)
.bind(spec_id)
.bind(revision)
.fetch_optional(&self.pool)
.await
}
}