summaryrefslogtreecommitdiff
path: root/makima/src/db
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/db')
-rw-r--r--makima/src/db/models.rs39
-rw-r--r--makima/src/db/repository.rs166
2 files changed, 205 insertions, 0 deletions
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs
index ca12eb2..8ab3a10 100644
--- a/makima/src/db/models.rs
+++ b/makima/src/db/models.rs
@@ -1507,3 +1507,42 @@ pub struct DaemonWithCapacity {
pub last_heartbeat_at: DateTime<Utc>,
pub connected_at: DateTime<Utc>,
}
+
+// ============================================================================
+// Repository History (for storing and suggesting previously used repositories)
+// ============================================================================
+
+/// Repository history entry - tracks previously used repositories for suggestions
+#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct RepositoryHistoryEntry {
+ pub id: Uuid,
+ pub owner_id: Uuid,
+ pub name: String,
+ pub repository_url: Option<String>,
+ pub local_path: Option<String>,
+ pub source_type: String,
+ pub use_count: i32,
+ pub last_used_at: DateTime<Utc>,
+ pub created_at: DateTime<Utc>,
+}
+
+/// Response for repository history list endpoint
+#[derive(Debug, Serialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct RepositoryHistoryListResponse {
+ pub entries: Vec<RepositoryHistoryEntry>,
+ pub total: i64,
+}
+
+/// Request for getting repository suggestions
+#[derive(Debug, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct RepositorySuggestionsQuery {
+ /// Filter by source type: 'remote' or 'local'
+ pub source_type: Option<String>,
+ /// Optional search query to filter by name or URL/path
+ pub query: Option<String>,
+ /// Limit results (default: 10)
+ pub limit: Option<i32>,
+}
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs
index 7933f1e..2f28c1a 100644
--- a/makima/src/db/repository.rs
+++ b/makima/src/db/repository.rs
@@ -3005,3 +3005,169 @@ pub async fn get_daemon_task_assignment(
.fetch_optional(pool)
.await
}
+
+// ============================================================================
+// Repository History Functions
+// ============================================================================
+
+use super::models::RepositoryHistoryEntry;
+
+/// List all repository history entries for an owner, ordered by use_count DESC, last_used_at DESC.
+pub async fn list_repository_history_for_owner(
+ pool: &PgPool,
+ owner_id: Uuid,
+) -> Result<Vec<RepositoryHistoryEntry>, sqlx::Error> {
+ sqlx::query_as::<_, RepositoryHistoryEntry>(
+ r#"
+ SELECT id, owner_id, name, repository_url, local_path, source_type, use_count, last_used_at, created_at
+ FROM repository_history
+ WHERE owner_id = $1
+ ORDER BY use_count DESC, last_used_at DESC
+ "#,
+ )
+ .bind(owner_id)
+ .fetch_all(pool)
+ .await
+}
+
+/// Get repository suggestions for an owner, optionally filtered by source type and query.
+pub async fn get_repository_suggestions(
+ pool: &PgPool,
+ owner_id: Uuid,
+ source_type: Option<&str>,
+ query: Option<&str>,
+ limit: i32,
+) -> Result<Vec<RepositoryHistoryEntry>, sqlx::Error> {
+ // Build query dynamically based on filters
+ let mut sql = String::from(
+ r#"
+ SELECT id, owner_id, name, repository_url, local_path, source_type, use_count, last_used_at, created_at
+ FROM repository_history
+ WHERE owner_id = $1
+ "#,
+ );
+
+ let mut param_idx = 2;
+
+ if source_type.is_some() {
+ sql.push_str(&format!(" AND source_type = ${}", param_idx));
+ param_idx += 1;
+ }
+
+ if query.is_some() {
+ sql.push_str(&format!(
+ " AND (LOWER(name) LIKE ${} OR LOWER(COALESCE(repository_url, '')) LIKE ${} OR LOWER(COALESCE(local_path, '')) LIKE ${})",
+ param_idx, param_idx, param_idx
+ ));
+ param_idx += 1;
+ }
+
+ sql.push_str(&format!(
+ " ORDER BY use_count DESC, last_used_at DESC LIMIT ${}",
+ param_idx
+ ));
+
+ // Build and execute query with the appropriate bindings
+ let mut query_builder = sqlx::query_as::<_, RepositoryHistoryEntry>(&sql).bind(owner_id);
+
+ if let Some(st) = source_type {
+ query_builder = query_builder.bind(st);
+ }
+
+ if let Some(q) = query {
+ let search_pattern = format!("%{}%", q.to_lowercase());
+ query_builder = query_builder.bind(search_pattern);
+ }
+
+ query_builder = query_builder.bind(limit);
+
+ query_builder.fetch_all(pool).await
+}
+
+/// Add or update a repository history entry.
+/// If an entry with the same URL (for remote) or path (for local) already exists,
+/// increment use_count and update last_used_at and name.
+/// Otherwise, create a new entry.
+pub async fn add_or_update_repository_history(
+ pool: &PgPool,
+ owner_id: Uuid,
+ name: &str,
+ repository_url: Option<&str>,
+ local_path: Option<&str>,
+ source_type: &str,
+) -> Result<RepositoryHistoryEntry, sqlx::Error> {
+ // Use UPSERT (INSERT ... ON CONFLICT)
+ if source_type == "remote" {
+ let url = repository_url.ok_or_else(|| {
+ sqlx::Error::Protocol("repository_url required for remote type".to_string())
+ })?;
+
+ sqlx::query_as::<_, RepositoryHistoryEntry>(
+ r#"
+ INSERT INTO repository_history (owner_id, name, repository_url, local_path, source_type, use_count, last_used_at)
+ VALUES ($1, $2, $3, NULL, $4, 1, NOW())
+ ON CONFLICT (owner_id, repository_url) WHERE source_type = 'remote' AND repository_url IS NOT NULL
+ DO UPDATE SET
+ name = EXCLUDED.name,
+ use_count = repository_history.use_count + 1,
+ last_used_at = NOW()
+ RETURNING id, owner_id, name, repository_url, local_path, source_type, use_count, last_used_at, created_at
+ "#,
+ )
+ .bind(owner_id)
+ .bind(name)
+ .bind(url)
+ .bind(source_type)
+ .fetch_one(pool)
+ .await
+ } else if source_type == "local" {
+ let path = local_path.ok_or_else(|| {
+ sqlx::Error::Protocol("local_path required for local type".to_string())
+ })?;
+
+ sqlx::query_as::<_, RepositoryHistoryEntry>(
+ r#"
+ INSERT INTO repository_history (owner_id, name, repository_url, local_path, source_type, use_count, last_used_at)
+ VALUES ($1, $2, NULL, $3, $4, 1, NOW())
+ ON CONFLICT (owner_id, local_path) WHERE source_type = 'local' AND local_path IS NOT NULL
+ DO UPDATE SET
+ name = EXCLUDED.name,
+ use_count = repository_history.use_count + 1,
+ last_used_at = NOW()
+ RETURNING id, owner_id, name, repository_url, local_path, source_type, use_count, last_used_at, created_at
+ "#,
+ )
+ .bind(owner_id)
+ .bind(name)
+ .bind(path)
+ .bind(source_type)
+ .fetch_one(pool)
+ .await
+ } else {
+ Err(sqlx::Error::Protocol(format!(
+ "Invalid source_type: {}",
+ source_type
+ )))
+ }
+}
+
+/// Delete a repository history entry.
+/// Returns true if an entry was deleted, false if not found.
+pub async fn delete_repository_history(
+ pool: &PgPool,
+ id: Uuid,
+ owner_id: Uuid,
+) -> Result<bool, sqlx::Error> {
+ let result = sqlx::query(
+ r#"
+ DELETE FROM repository_history
+ WHERE id = $1 AND owner_id = $2
+ "#,
+ )
+ .bind(id)
+ .bind(owner_id)
+ .execute(pool)
+ .await?;
+
+ Ok(result.rows_affected() > 0)
+}