summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--makima/src/db/models.rs94
-rw-r--r--makima/src/db/repository.rs281
2 files changed, 375 insertions, 0 deletions
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs
index 169f468..e4fc2b1 100644
--- a/makima/src/db/models.rs
+++ b/makima/src/db/models.rs
@@ -2887,3 +2887,97 @@ pub struct DirectiveMemoryListResponse {
pub memories: Vec<DirectiveMemory>,
pub total: i64,
}
+
+// =============================================================================
+// Order Types
+// =============================================================================
+
+/// An order — a unit of work (task/feature/bug/chore) that can be tracked independently
+/// or linked to a directive/contract.
+#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct Order {
+ pub id: Uuid,
+ pub owner_id: Uuid,
+ pub title: String,
+ pub description: Option<String>,
+ /// Type: feature, bug, chore, spike, epic
+ pub order_type: String,
+ /// Status: backlog, ready, in_progress, review, done, cancelled
+ pub status: String,
+ /// Priority: urgent, high, medium, low
+ pub priority: String,
+ pub directive_id: Option<Uuid>,
+ pub contract_id: Option<Uuid>,
+ pub repository_url: Option<String>,
+ pub version: i32,
+ pub created_at: DateTime<Utc>,
+ pub updated_at: DateTime<Utc>,
+}
+
+/// Summary for order list views.
+#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct OrderSummary {
+ pub id: Uuid,
+ pub owner_id: Uuid,
+ pub title: String,
+ pub description: Option<String>,
+ pub order_type: String,
+ pub status: String,
+ pub priority: String,
+ pub directive_id: Option<Uuid>,
+ pub contract_id: Option<Uuid>,
+ pub repository_url: Option<String>,
+ pub version: i32,
+ pub created_at: DateTime<Utc>,
+ pub updated_at: DateTime<Utc>,
+ pub label_count: i64,
+}
+
+/// Request to create a new order.
+#[derive(Debug, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct CreateOrderRequest {
+ pub title: String,
+ pub description: Option<String>,
+ pub order_type: Option<String>,
+ pub priority: Option<String>,
+ pub directive_id: Option<Uuid>,
+ pub contract_id: Option<Uuid>,
+ pub repository_url: Option<String>,
+}
+
+/// Request to update an order.
+#[derive(Debug, Default, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct UpdateOrderRequest {
+ pub title: Option<String>,
+ pub description: Option<String>,
+ pub order_type: Option<String>,
+ pub status: Option<String>,
+ pub priority: Option<String>,
+ pub directive_id: Option<Option<Uuid>>,
+ pub contract_id: Option<Option<Uuid>>,
+ pub repository_url: Option<String>,
+ pub version: Option<i32>,
+}
+
+/// A label that can be applied to orders.
+#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct OrderLabel {
+ pub id: Uuid,
+ pub owner_id: Uuid,
+ pub name: String,
+ pub color: String,
+ pub created_at: DateTime<Utc>,
+}
+
+/// Request to create a new order label.
+#[derive(Debug, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct CreateOrderLabelRequest {
+ pub name: String,
+ pub color: Option<String>,
+}
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs
index 95460f7..31a85bc 100644
--- a/makima/src/db/repository.rs
+++ b/makima/src/db/repository.rs
@@ -20,6 +20,8 @@ use super::models::{
PhaseDefinition, SupervisorHeartbeatRecord, SupervisorState,
Task, TaskCheckpoint, TaskEvent, TaskSummary, UpdateContractRequest,
UpdateFileRequest, UpdateTaskRequest, UpdateTemplateRequest,
+ Order, OrderSummary, CreateOrderRequest, UpdateOrderRequest,
+ OrderLabel, CreateOrderLabelRequest,
};
/// Repository error types.
@@ -5872,3 +5874,282 @@ pub async fn clear_directive_memories(
.await?;
Ok(result.rows_affected())
}
+
+// =============================================================================
+// Order Repository Functions
+// =============================================================================
+
+/// Create a new order for an owner.
+pub async fn create_order_for_owner(
+ pool: &PgPool,
+ owner_id: Uuid,
+ req: CreateOrderRequest,
+) -> Result<Order, sqlx::Error> {
+ sqlx::query_as::<_, Order>(
+ r#"
+ INSERT INTO orders (owner_id, title, description, order_type, priority, directive_id, contract_id, repository_url)
+ VALUES ($1, $2, $3, COALESCE($4, 'feature'), COALESCE($5, 'medium'), $6, $7, $8)
+ RETURNING *
+ "#,
+ )
+ .bind(owner_id)
+ .bind(&req.title)
+ .bind(&req.description)
+ .bind(&req.order_type)
+ .bind(&req.priority)
+ .bind(req.directive_id)
+ .bind(req.contract_id)
+ .bind(&req.repository_url)
+ .fetch_one(pool)
+ .await
+}
+
+/// Get a single order for an owner.
+pub async fn get_order_for_owner(
+ pool: &PgPool,
+ owner_id: Uuid,
+ id: Uuid,
+) -> Result<Option<Order>, sqlx::Error> {
+ sqlx::query_as::<_, Order>(
+ r#"SELECT * FROM orders WHERE id = $1 AND owner_id = $2"#,
+ )
+ .bind(id)
+ .bind(owner_id)
+ .fetch_optional(pool)
+ .await
+}
+
+/// List orders for an owner with optional filtering.
+pub async fn list_orders_for_owner(
+ pool: &PgPool,
+ owner_id: Uuid,
+ status: Option<&str>,
+ order_type: Option<&str>,
+ priority: Option<&str>,
+ directive_id: Option<Uuid>,
+ contract_id: Option<Uuid>,
+) -> Result<Vec<OrderSummary>, sqlx::Error> {
+ sqlx::query_as::<_, OrderSummary>(
+ r#"
+ SELECT
+ o.id, o.owner_id, o.title, o.description, o.order_type, o.status, o.priority,
+ o.directive_id, o.contract_id, o.repository_url, o.version,
+ o.created_at, o.updated_at,
+ COALESCE((SELECT COUNT(*) FROM order_label_assignments WHERE order_id = o.id), 0) as label_count
+ FROM orders o
+ WHERE o.owner_id = $1
+ AND ($2::text IS NULL OR o.status = $2)
+ AND ($3::text IS NULL OR o.order_type = $3)
+ AND ($4::text IS NULL OR o.priority = $4)
+ AND ($5::uuid IS NULL OR o.directive_id = $5)
+ AND ($6::uuid IS NULL OR o.contract_id = $6)
+ ORDER BY
+ CASE o.priority
+ WHEN 'urgent' THEN 0
+ WHEN 'high' THEN 1
+ WHEN 'medium' THEN 2
+ WHEN 'low' THEN 3
+ END,
+ o.created_at DESC
+ "#,
+ )
+ .bind(owner_id)
+ .bind(status)
+ .bind(order_type)
+ .bind(priority)
+ .bind(directive_id)
+ .bind(contract_id)
+ .fetch_all(pool)
+ .await
+}
+
+/// Update an order for an owner with optimistic locking.
+pub async fn update_order_for_owner(
+ pool: &PgPool,
+ owner_id: Uuid,
+ id: Uuid,
+ req: UpdateOrderRequest,
+) -> Result<Option<Order>, RepositoryError> {
+ let current = sqlx::query_as::<_, Order>(
+ r#"SELECT * FROM orders WHERE id = $1 AND owner_id = $2"#,
+ )
+ .bind(id)
+ .bind(owner_id)
+ .fetch_optional(pool)
+ .await
+ .map_err(RepositoryError::Database)?;
+
+ let current = match current {
+ Some(c) => c,
+ None => return Ok(None),
+ };
+
+ if let Some(expected_version) = req.version {
+ if expected_version != current.version {
+ return Err(RepositoryError::VersionConflict {
+ expected: expected_version,
+ actual: current.version,
+ });
+ }
+ }
+
+ let title = req.title.as_deref().unwrap_or(&current.title);
+ let description = req.description.as_deref().or(current.description.as_deref());
+ let order_type = req.order_type.as_deref().unwrap_or(&current.order_type);
+ let status = req.status.as_deref().unwrap_or(&current.status);
+ let priority = req.priority.as_deref().unwrap_or(&current.priority);
+ let directive_id = match &req.directive_id {
+ Some(v) => *v,
+ None => current.directive_id,
+ };
+ let contract_id = match &req.contract_id {
+ Some(v) => *v,
+ None => current.contract_id,
+ };
+ let repository_url = req.repository_url.as_deref().or(current.repository_url.as_deref());
+
+ sqlx::query_as::<_, Order>(
+ r#"
+ UPDATE orders
+ SET title = $3, description = $4, order_type = $5, status = $6, priority = $7,
+ directive_id = $8, contract_id = $9, repository_url = $10,
+ version = version + 1, updated_at = NOW()
+ WHERE id = $1 AND owner_id = $2
+ RETURNING *
+ "#,
+ )
+ .bind(id)
+ .bind(owner_id)
+ .bind(title)
+ .bind(description)
+ .bind(order_type)
+ .bind(status)
+ .bind(priority)
+ .bind(directive_id)
+ .bind(contract_id)
+ .bind(repository_url)
+ .fetch_optional(pool)
+ .await
+ .map_err(RepositoryError::Database)
+}
+
+/// Delete an order for an owner.
+pub async fn delete_order_for_owner(
+ pool: &PgPool,
+ owner_id: Uuid,
+ id: Uuid,
+) -> Result<bool, sqlx::Error> {
+ let result = sqlx::query(
+ r#"DELETE FROM orders WHERE id = $1 AND owner_id = $2"#,
+ )
+ .bind(id)
+ .bind(owner_id)
+ .execute(pool)
+ .await?;
+
+ Ok(result.rows_affected() > 0)
+}
+
+// =============================================================================
+// Order Label Functions
+// =============================================================================
+
+/// Create a new order label for an owner.
+pub async fn create_order_label_for_owner(
+ pool: &PgPool,
+ owner_id: Uuid,
+ req: CreateOrderLabelRequest,
+) -> Result<OrderLabel, sqlx::Error> {
+ sqlx::query_as::<_, OrderLabel>(
+ r#"
+ INSERT INTO order_labels (owner_id, name, color)
+ VALUES ($1, $2, COALESCE($3, '#75aafc'))
+ RETURNING *
+ "#,
+ )
+ .bind(owner_id)
+ .bind(&req.name)
+ .bind(&req.color)
+ .fetch_one(pool)
+ .await
+}
+
+/// List all order labels for an owner.
+pub async fn list_order_labels_for_owner(
+ pool: &PgPool,
+ owner_id: Uuid,
+) -> Result<Vec<OrderLabel>, sqlx::Error> {
+ sqlx::query_as::<_, OrderLabel>(
+ r#"SELECT * FROM order_labels WHERE owner_id = $1 ORDER BY name"#,
+ )
+ .bind(owner_id)
+ .fetch_all(pool)
+ .await
+}
+
+/// Delete an order label for an owner.
+pub async fn delete_order_label_for_owner(
+ pool: &PgPool,
+ owner_id: Uuid,
+ id: Uuid,
+) -> Result<bool, sqlx::Error> {
+ let result = sqlx::query(
+ r#"DELETE FROM order_labels WHERE id = $1 AND owner_id = $2"#,
+ )
+ .bind(id)
+ .bind(owner_id)
+ .execute(pool)
+ .await?;
+
+ Ok(result.rows_affected() > 0)
+}
+
+/// Add a label to an order.
+pub async fn add_label_to_order(
+ pool: &PgPool,
+ order_id: Uuid,
+ label_id: Uuid,
+) -> Result<(), sqlx::Error> {
+ sqlx::query(
+ r#"INSERT INTO order_label_assignments (order_id, label_id) VALUES ($1, $2) ON CONFLICT DO NOTHING"#,
+ )
+ .bind(order_id)
+ .bind(label_id)
+ .execute(pool)
+ .await?;
+ Ok(())
+}
+
+/// Remove a label from an order.
+pub async fn remove_label_from_order(
+ pool: &PgPool,
+ order_id: Uuid,
+ label_id: Uuid,
+) -> Result<bool, sqlx::Error> {
+ let result = sqlx::query(
+ r#"DELETE FROM order_label_assignments WHERE order_id = $1 AND label_id = $2"#,
+ )
+ .bind(order_id)
+ .bind(label_id)
+ .execute(pool)
+ .await?;
+ Ok(result.rows_affected() > 0)
+}
+
+/// Get all labels for an order.
+pub async fn get_labels_for_order(
+ pool: &PgPool,
+ order_id: Uuid,
+) -> Result<Vec<OrderLabel>, sqlx::Error> {
+ sqlx::query_as::<_, OrderLabel>(
+ r#"
+ SELECT ol.* FROM order_labels ol
+ INNER JOIN order_label_assignments ola ON ol.id = ola.label_id
+ WHERE ola.order_id = $1
+ ORDER BY ol.name
+ "#,
+ )
+ .bind(order_id)
+ .fetch_all(pool)
+ .await
+}