diff options
| -rw-r--r-- | makima/src/db/models.rs | 94 | ||||
| -rw-r--r-- | makima/src/db/repository.rs | 281 |
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(¤t.title); + let description = req.description.as_deref().or(current.description.as_deref()); + let order_type = req.order_type.as_deref().unwrap_or(¤t.order_type); + let status = req.status.as_deref().unwrap_or(¤t.status); + let priority = req.priority.as_deref().unwrap_or(¤t.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 +} |
