summaryrefslogtreecommitdiff
path: root/makima/src/db
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-05 00:48:38 +0000
committersoryu <soryu@soryu.co>2026-02-05 00:48:38 +0000
commit0302b4596e14210884df5d645df9a179d8f0c1c6 (patch)
tree46efe027dffa25a30e4eab87fd62de249c3075ad /makima/src/db
parente16d49b52a393aa9a762edf57f93434a4bd7844e (diff)
downloadsoryu-0302b4596e14210884df5d645df9a179d8f0c1c6.tar.gz
soryu-0302b4596e14210884df5d645df9a179d8f0c1c6.zip
Add multi-repository support for chains
Chains can now have multiple repositories attached, with one marked as primary. Repositories are used by contracts created from chain definitions. Backend changes: - Add chain_repositories table migration - Add ChainRepository model with CRUD operations - Add API endpoints for listing, adding, deleting repositories - Add endpoint to set a repository as primary - Update Chain and ChainEditorData models to use repositories - Update chain parser to support repositories in YAML format - Remove deprecated repository_url/local_path from Chain Frontend changes: - Add ChainRepository interface and API functions - Add repository section to ChainEditor showing attached repos - Add modal for adding new repositories (remote or local) - Support setting primary repository and removing repositories Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'makima/src/db')
-rw-r--r--makima/src/db/models.rs64
-rw-r--r--makima/src/db/repository.rs189
2 files changed, 224 insertions, 29 deletions
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs
index 4e569ec..30e1603 100644
--- a/makima/src/db/models.rs
+++ b/makima/src/db/models.rs
@@ -2652,16 +2652,40 @@ pub struct Chain {
pub loop_current_iteration: Option<i32>,
/// Progress check prompt/criteria for evaluating loop completion
pub loop_progress_check: Option<String>,
- /// Repository URL for contracts in this chain (optional)
- pub repository_url: Option<String>,
- /// Local path for contracts in this chain (optional)
- pub local_path: Option<String>,
/// Version for optimistic locking
pub version: i32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
+/// Chain repository record from the database
+#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct ChainRepository {
+ pub id: Uuid,
+ pub chain_id: Uuid,
+ pub name: String,
+ pub repository_url: Option<String>,
+ pub local_path: Option<String>,
+ pub source_type: String,
+ pub status: String,
+ pub is_primary: bool,
+ pub created_at: DateTime<Utc>,
+ pub updated_at: DateTime<Utc>,
+}
+
+impl ChainRepository {
+ /// Parse source_type string to RepositorySourceType enum
+ pub fn source_type_enum(&self) -> Result<RepositorySourceType, String> {
+ self.source_type.parse()
+ }
+
+ /// Parse status string to RepositoryStatus enum
+ pub fn status_enum(&self) -> Result<RepositoryStatus, String> {
+ self.status.parse()
+ }
+}
+
impl Chain {
/// Parse status string to ChainStatus enum
pub fn status_enum(&self) -> Result<ChainStatus, String> {
@@ -2724,6 +2748,7 @@ pub struct ChainWithContracts {
#[serde(flatten)]
pub chain: Chain,
pub contracts: Vec<ChainContractDetail>,
+ pub repositories: Vec<ChainRepository>,
}
/// Contract detail within a chain (includes contract info + chain link info)
@@ -2790,10 +2815,8 @@ pub struct CreateChainRequest {
pub name: String,
/// Optional description
pub description: Option<String>,
- /// Repository URL for contracts in this chain
- pub repository_url: Option<String>,
- /// Local path for contracts in this chain
- pub local_path: Option<String>,
+ /// Repositories for this chain
+ pub repositories: Option<Vec<AddChainRepositoryRequest>>,
/// Enable loop mode for iterative execution
#[serde(default)]
pub loop_enabled: Option<bool>,
@@ -2805,6 +2828,28 @@ pub struct CreateChainRequest {
pub contracts: Option<Vec<CreateChainContractRequest>>,
}
+/// Request to add a repository to a chain
+#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct AddChainRepositoryRequest {
+ /// Display name for the repository
+ pub name: String,
+ /// Remote repository URL (for remote repos)
+ pub repository_url: Option<String>,
+ /// Local filesystem path (for local repos)
+ pub local_path: Option<String>,
+ /// Source type: remote, local, or managed
+ #[serde(default = "default_source_type")]
+ pub source_type: String,
+ /// Whether this is the primary repository
+ #[serde(default)]
+ pub is_primary: bool,
+}
+
+fn default_source_type() -> String {
+ "remote".to_string()
+}
+
/// Request to create a contract within a chain
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
@@ -2934,8 +2979,7 @@ pub struct ChainEditorData {
pub id: Option<Uuid>,
pub name: String,
pub description: Option<String>,
- pub repository_url: Option<String>,
- pub local_path: Option<String>,
+ pub repositories: Vec<ChainRepository>,
pub loop_enabled: bool,
pub loop_max_iterations: Option<i32>,
pub loop_progress_check: Option<String>,
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs
index ec233ba..2b595b5 100644
--- a/makima/src/db/repository.rs
+++ b/makima/src/db/repository.rs
@@ -6,20 +6,20 @@ use sqlx::PgPool;
use uuid::Uuid;
use super::models::{
- AddContractDefinitionRequest, AddContractToChainRequest, Chain, ChainContract,
- ChainContractDefinition, ChainContractDetail, ChainDefinitionGraphNode,
+ AddChainRepositoryRequest, AddContractDefinitionRequest, AddContractToChainRequest, Chain,
+ ChainContract, ChainContractDefinition, ChainContractDetail, ChainDefinitionGraphNode,
ChainDefinitionGraphResponse, ChainEditorContract, ChainEditorData, ChainEditorDeliverable,
ChainEditorEdge, ChainEditorNode, ChainEditorTask, ChainEvent, ChainGraphEdge, ChainGraphNode,
- ChainGraphResponse, ChainSummary, ChainWithContracts, CheckpointPatch, CheckpointPatchInfo,
- Contract, ContractChatConversation, ContractChatMessageRecord, ContractEvent,
- ContractRepository, ContractSummary, ContractTypeTemplateRecord, ConversationMessage,
- ConversationSnapshot, CreateChainRequest, CreateContractRequest, CreateFileRequest,
- CreateTaskRequest, CreateTemplateRequest, Daemon, DaemonTaskAssignment, DaemonWithCapacity,
- DeliverableDefinition, File, FileSummary, FileVersion, HistoryEvent, HistoryQueryFilters,
- MeshChatConversation, MeshChatMessageRecord, PhaseChangeResult, PhaseConfig, PhaseDefinition,
- SupervisorHeartbeatRecord, SupervisorState, Task, TaskCheckpoint, TaskEvent, TaskSummary,
- UpdateChainRequest, UpdateContractDefinitionRequest, UpdateContractRequest, UpdateFileRequest,
- UpdateTaskRequest, UpdateTemplateRequest,
+ ChainGraphResponse, ChainRepository, ChainSummary, ChainWithContracts, CheckpointPatch,
+ CheckpointPatchInfo, Contract, ContractChatConversation, ContractChatMessageRecord,
+ ContractEvent, ContractRepository, ContractSummary, ContractTypeTemplateRecord,
+ ConversationMessage, ConversationSnapshot, CreateChainRequest, CreateContractRequest,
+ CreateFileRequest, CreateTaskRequest, CreateTemplateRequest, Daemon, DaemonTaskAssignment,
+ DaemonWithCapacity, DeliverableDefinition, File, FileSummary, FileVersion, HistoryEvent,
+ HistoryQueryFilters, MeshChatConversation, MeshChatMessageRecord, PhaseChangeResult,
+ PhaseConfig, PhaseDefinition, SupervisorHeartbeatRecord, SupervisorState, Task, TaskCheckpoint,
+ TaskEvent, TaskSummary, UpdateChainRequest, UpdateContractDefinitionRequest,
+ UpdateContractRequest, UpdateFileRequest, UpdateTaskRequest, UpdateTemplateRequest,
};
/// Repository error types.
@@ -4917,16 +4917,14 @@ pub async fn create_chain_for_owner(
sqlx::query_as::<_, Chain>(
r#"
- INSERT INTO chains (owner_id, name, description, repository_url, local_path, loop_enabled, loop_max_iterations, loop_progress_check)
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
+ INSERT INTO chains (owner_id, name, description, loop_enabled, loop_max_iterations, loop_progress_check)
+ VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *
"#,
)
.bind(owner_id)
.bind(&req.name)
.bind(&req.description)
- .bind(&req.repository_url)
- .bind(&req.local_path)
.bind(loop_enabled)
.bind(loop_max_iterations)
.bind(&req.loop_progress_check)
@@ -5181,12 +5179,165 @@ pub async fn get_chain_with_contracts(
match chain {
Some(chain) => {
let contracts = list_chain_contracts(pool, chain_id).await?;
- Ok(Some(ChainWithContracts { chain, contracts }))
+ let repositories = list_chain_repositories(pool, chain_id).await?;
+ Ok(Some(ChainWithContracts {
+ chain,
+ contracts,
+ repositories,
+ }))
}
None => Ok(None),
}
}
+// =============================================================================
+// Chain Repository Operations
+// =============================================================================
+
+/// List all repositories for a chain.
+pub async fn list_chain_repositories(
+ pool: &PgPool,
+ chain_id: Uuid,
+) -> Result<Vec<ChainRepository>, sqlx::Error> {
+ sqlx::query_as::<_, ChainRepository>(
+ r#"
+ SELECT *
+ FROM chain_repositories
+ WHERE chain_id = $1
+ ORDER BY is_primary DESC, created_at ASC
+ "#,
+ )
+ .bind(chain_id)
+ .fetch_all(pool)
+ .await
+}
+
+/// Get a chain repository by ID.
+pub async fn get_chain_repository(
+ pool: &PgPool,
+ chain_id: Uuid,
+ repository_id: Uuid,
+) -> Result<Option<ChainRepository>, sqlx::Error> {
+ sqlx::query_as::<_, ChainRepository>(
+ r#"
+ SELECT *
+ FROM chain_repositories
+ WHERE id = $1 AND chain_id = $2
+ "#,
+ )
+ .bind(repository_id)
+ .bind(chain_id)
+ .fetch_optional(pool)
+ .await
+}
+
+/// Add a repository to a chain.
+pub async fn add_chain_repository(
+ pool: &PgPool,
+ chain_id: Uuid,
+ req: &AddChainRepositoryRequest,
+) -> Result<ChainRepository, sqlx::Error> {
+ // If is_primary, clear other primaries first
+ if req.is_primary {
+ sqlx::query(
+ r#"
+ UPDATE chain_repositories
+ SET is_primary = false, updated_at = NOW()
+ WHERE chain_id = $1 AND is_primary = true
+ "#,
+ )
+ .bind(chain_id)
+ .execute(pool)
+ .await?;
+ }
+
+ sqlx::query_as::<_, ChainRepository>(
+ r#"
+ INSERT INTO chain_repositories (chain_id, name, repository_url, local_path, source_type, status, is_primary)
+ VALUES ($1, $2, $3, $4, $5, 'ready', $6)
+ RETURNING *
+ "#,
+ )
+ .bind(chain_id)
+ .bind(&req.name)
+ .bind(&req.repository_url)
+ .bind(&req.local_path)
+ .bind(&req.source_type)
+ .bind(req.is_primary)
+ .fetch_one(pool)
+ .await
+}
+
+/// Delete a repository from a chain.
+pub async fn delete_chain_repository(
+ pool: &PgPool,
+ chain_id: Uuid,
+ repository_id: Uuid,
+) -> Result<bool, sqlx::Error> {
+ let result = sqlx::query(
+ r#"
+ DELETE FROM chain_repositories
+ WHERE id = $1 AND chain_id = $2
+ "#,
+ )
+ .bind(repository_id)
+ .bind(chain_id)
+ .execute(pool)
+ .await?;
+
+ Ok(result.rows_affected() > 0)
+}
+
+/// Set a repository as primary for a chain.
+pub async fn set_chain_repository_primary(
+ pool: &PgPool,
+ chain_id: Uuid,
+ repository_id: Uuid,
+) -> Result<ChainRepository, sqlx::Error> {
+ // Clear existing primary
+ sqlx::query(
+ r#"
+ UPDATE chain_repositories
+ SET is_primary = false, updated_at = NOW()
+ WHERE chain_id = $1 AND is_primary = true
+ "#,
+ )
+ .bind(chain_id)
+ .execute(pool)
+ .await?;
+
+ // Set new primary
+ sqlx::query_as::<_, ChainRepository>(
+ r#"
+ UPDATE chain_repositories
+ SET is_primary = true, updated_at = NOW()
+ WHERE id = $1 AND chain_id = $2
+ RETURNING *
+ "#,
+ )
+ .bind(repository_id)
+ .bind(chain_id)
+ .fetch_one(pool)
+ .await
+}
+
+/// Get the primary repository for a chain.
+pub async fn get_chain_primary_repository(
+ pool: &PgPool,
+ chain_id: Uuid,
+) -> Result<Option<ChainRepository>, sqlx::Error> {
+ sqlx::query_as::<_, ChainRepository>(
+ r#"
+ SELECT *
+ FROM chain_repositories
+ WHERE chain_id = $1 AND is_primary = true
+ "#,
+ )
+ .bind(chain_id)
+ .fetch_optional(pool)
+ .await
+}
+
/// Get chain graph structure for visualization.
pub async fn get_chain_graph(
pool: &PgPool,
@@ -5381,6 +5532,7 @@ pub async fn get_chain_editor_data(
match chain {
Some(chain) => {
let contracts = list_chain_contracts(pool, chain_id).await?;
+ let repositories = list_chain_repositories(pool, chain_id).await?;
// Build nodes
let nodes: Vec<ChainEditorNode> = contracts
@@ -5415,8 +5567,7 @@ pub async fn get_chain_editor_data(
id: Some(chain.id),
name: chain.name,
description: chain.description,
- repository_url: chain.repository_url,
- local_path: chain.local_path,
+ repositories,
loop_enabled: chain.loop_enabled,
loop_max_iterations: chain.loop_max_iterations,
loop_progress_check: chain.loop_progress_check,