diff options
| author | soryu <soryu@soryu.co> | 2025-12-24 05:45:22 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2025-12-24 05:45:22 +0000 |
| commit | 2faba0388f93d8e4fb86219eba7883b331d501ff (patch) | |
| tree | 92b83b8d558a652d3777627b2ac95ded250faa48 /makima/src/db/repository.rs | |
| parent | 8f016a0e9d14badc39dffd67ed6fb862f9d08496 (diff) | |
| download | soryu-2faba0388f93d8e4fb86219eba7883b331d501ff.tar.gz soryu-2faba0388f93d8e4fb86219eba7883b331d501ff.zip | |
Add versioning to files
Diffstat (limited to 'makima/src/db/repository.rs')
| -rw-r--r-- | makima/src/db/repository.rs | 144 |
1 files changed, 143 insertions, 1 deletions
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index 5b962ee..4137ba6 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -4,7 +4,7 @@ use chrono::Utc; use sqlx::PgPool; use uuid::Uuid; -use super::models::{CreateFileRequest, File, UpdateFileRequest}; +use super::models::{CreateFileRequest, File, FileVersion, UpdateFileRequest}; /// Default owner ID for anonymous users. pub const ANONYMOUS_OWNER_ID: Uuid = Uuid::from_u128(0x00000000_0000_0000_0000_000000000002); @@ -221,3 +221,145 @@ pub async fn count_files(pool: &PgPool) -> Result<i64, sqlx::Error> { Ok(result.0) } + +// ============================================================================= +// Version History Functions +// ============================================================================= + +/// Set the version source for the current transaction. +/// This is used by the trigger to record who made the change. +pub async fn set_version_source(pool: &PgPool, source: &str) -> Result<(), sqlx::Error> { + sqlx::query(&format!("SET LOCAL app.version_source = '{}'", source)) + .execute(pool) + .await?; + Ok(()) +} + +/// Set the change description for the current transaction. +pub async fn set_change_description(pool: &PgPool, description: &str) -> Result<(), sqlx::Error> { + // Escape single quotes for SQL + let escaped = description.replace('\'', "''"); + sqlx::query(&format!("SET LOCAL app.change_description = '{}'", escaped)) + .execute(pool) + .await?; + Ok(()) +} + +/// List all versions of a file, ordered by version DESC. +pub async fn list_file_versions(pool: &PgPool, file_id: Uuid) -> Result<Vec<FileVersion>, sqlx::Error> { + // First get the current version from the files table + let current = get_file(pool, file_id).await?; + + let mut versions = sqlx::query_as::<_, FileVersion>( + r#" + SELECT id, file_id, version, name, description, summary, body, source, change_description, created_at + FROM file_versions + WHERE file_id = $1 + ORDER BY version DESC + "#, + ) + .bind(file_id) + .fetch_all(pool) + .await?; + + // Add the current version as the first entry if it exists + if let Some(file) = current { + let current_version = FileVersion { + id: file.id, + file_id: file.id, + version: file.version, + name: file.name, + description: file.description, + summary: file.summary, + body: file.body, + source: "user".to_string(), // Current version source + change_description: None, + created_at: file.updated_at, + }; + versions.insert(0, current_version); + } + + Ok(versions) +} + +/// Get a specific version of a file. +pub async fn get_file_version( + pool: &PgPool, + file_id: Uuid, + version: i32, +) -> Result<Option<FileVersion>, sqlx::Error> { + // First check if this is the current version + if let Some(file) = get_file(pool, file_id).await? { + if file.version == version { + return Ok(Some(FileVersion { + id: file.id, + file_id: file.id, + version: file.version, + name: file.name, + description: file.description, + summary: file.summary, + body: file.body, + source: "user".to_string(), + change_description: None, + created_at: file.updated_at, + })); + } + } + + // Otherwise, look in the versions table + sqlx::query_as::<_, FileVersion>( + r#" + SELECT id, file_id, version, name, description, summary, body, source, change_description, created_at + FROM file_versions + WHERE file_id = $1 AND version = $2 + "#, + ) + .bind(file_id) + .bind(version) + .fetch_optional(pool) + .await +} + +/// Restore a file to a previous version. +/// This creates a new version with the content from the target version. +pub async fn restore_file_version( + pool: &PgPool, + file_id: Uuid, + target_version: i32, + current_version: i32, +) -> Result<Option<File>, RepositoryError> { + // Get the target version content + let target = get_file_version(pool, file_id, target_version).await?; + let Some(target) = target else { + return Ok(None); + }; + + // Set version source and description for the trigger + set_version_source(pool, "system").await?; + set_change_description(pool, &format!("Restored from version {}", target_version)).await?; + + // Update the file with the target version's content + // This will trigger the save_file_version trigger to save the current state first + let update_req = UpdateFileRequest { + name: Some(target.name), + description: target.description, + transcript: None, + summary: target.summary, + body: Some(target.body), + version: Some(current_version), + }; + + update_file(pool, file_id, update_req).await +} + +/// Count versions for a file. +pub async fn count_file_versions(pool: &PgPool, file_id: Uuid) -> Result<i64, sqlx::Error> { + let result: (i64,) = sqlx::query_as( + "SELECT COUNT(*) + 1 FROM file_versions WHERE file_id = $1", // +1 for current version + ) + .bind(file_id) + .fetch_one(pool) + .await?; + + Ok(result.0) +} |
