Skip to main content

flow_server/routes/
features.rs

1use crate::{
2    error::{AppError, AppResult},
3    state::AppState,
4};
5use axum::{
6    extract::{Path, State},
7    response::Json,
8    routing::{delete, get, post},
9    Router,
10};
11use flow_core::{CreateFeatureInput, DependencyRef, Feature, FeatureGraphNode, FeatureStats};
12use flow_db::FeatureStore;
13use serde::Deserialize;
14use std::sync::Arc;
15
16#[derive(Debug, Deserialize)]
17pub struct BulkCreateRequest {
18    features: Vec<CreateFeatureInput>,
19}
20
21/// Build the feature routes sub-router
22pub fn feature_routes() -> Router<Arc<AppState>> {
23    Router::new()
24        .route("/", get(list_features).post(create_feature))
25        .route("/bulk", post(bulk_create))
26        .route("/stats", get(get_stats))
27        .route("/ready", get(get_ready))
28        .route("/blocked", get(get_blocked))
29        .route("/graph", get(get_graph))
30        .route("/:id", get(get_feature_by_id))
31        .route("/:id/claim", post(claim_feature))
32        .route("/:id/pass", post(mark_passing))
33        .route("/:id/fail", post(mark_failing))
34        .route("/:id/skip", post(skip_feature))
35        .route("/:id/dependencies", post(add_dependency))
36        .route("/:id/dependencies/:dep_id", delete(remove_dependency))
37}
38
39/// GET /api/features — List all features
40async fn list_features(State(state): State<Arc<AppState>>) -> AppResult<Json<Vec<Feature>>> {
41    let db = state
42        .db
43        .as_ref()
44        .ok_or_else(|| AppError::Internal("Database not configured".into()))?;
45
46    let features = {
47        let conn = db.writer().lock().unwrap();
48        FeatureStore::get_all(&conn)?
49    };
50    Ok(Json(features))
51}
52
53/// GET /api/features/:id — Get feature by ID
54async fn get_feature_by_id(
55    State(state): State<Arc<AppState>>,
56    Path(id): Path<String>,
57) -> AppResult<Json<Feature>> {
58    let db = state
59        .db
60        .as_ref()
61        .ok_or_else(|| AppError::Internal("Database not configured".into()))?;
62
63    let id_num: i64 = id
64        .parse()
65        .map_err(|_| AppError::BadRequest("Invalid feature ID".into()))?;
66
67    let feature = {
68        let conn = db.writer().lock().unwrap();
69        FeatureStore::get_by_id(&conn, id_num)?
70            .ok_or_else(|| AppError::NotFound("Feature not found".into()))?
71    };
72    Ok(Json(feature))
73}
74
75/// GET /api/features/stats — Get feature statistics
76async fn get_stats(State(state): State<Arc<AppState>>) -> AppResult<Json<FeatureStats>> {
77    let db = state
78        .db
79        .as_ref()
80        .ok_or_else(|| AppError::Internal("Database not configured".into()))?;
81
82    let feature_stats = {
83        let conn = db.writer().lock().unwrap();
84        FeatureStore::get_stats(&conn)?
85    };
86    Ok(Json(feature_stats))
87}
88
89/// GET /api/features/ready — Get ready features
90async fn get_ready(State(state): State<Arc<AppState>>) -> AppResult<Json<Vec<Feature>>> {
91    let db = state
92        .db
93        .as_ref()
94        .ok_or_else(|| AppError::Internal("Database not configured".into()))?;
95
96    let features = {
97        let conn = db.writer().lock().unwrap();
98        FeatureStore::get_ready(&conn)?
99    };
100    Ok(Json(features))
101}
102
103/// GET /api/features/blocked — Get blocked features
104async fn get_blocked(State(state): State<Arc<AppState>>) -> AppResult<Json<Vec<Feature>>> {
105    let db = state
106        .db
107        .as_ref()
108        .ok_or_else(|| AppError::Internal("Database not configured".into()))?;
109
110    let features = {
111        let conn = db.writer().lock().unwrap();
112        FeatureStore::get_blocked(&conn)?
113    };
114    Ok(Json(features))
115}
116
117/// GET /api/features/graph — Get dependency graph
118async fn get_graph(State(state): State<Arc<AppState>>) -> AppResult<Json<Vec<FeatureGraphNode>>> {
119    let db = state
120        .db
121        .as_ref()
122        .ok_or_else(|| AppError::Internal("Database not configured".into()))?;
123
124    let features = {
125        let conn = db.writer().lock().unwrap();
126        FeatureStore::get_all(&conn)?
127    };
128
129    // Build reverse dependency map (dependents)
130    let mut dependents_map: std::collections::HashMap<i64, Vec<i64>> =
131        std::collections::HashMap::new();
132    for feature in &features {
133        for &dep_id in &feature.dependencies {
134            dependents_map.entry(dep_id).or_default().push(feature.id);
135        }
136    }
137
138    // Clone features for checking blocked status
139    let features_for_check = features.clone();
140
141    // Build graph from features
142    let graph: Vec<FeatureGraphNode> = features
143        .into_iter()
144        .map(|f| {
145            let is_blocked = !f.dependencies.is_empty()
146                && f.dependencies.iter().any(|dep_id| {
147                    features_for_check
148                        .iter()
149                        .any(|other| other.id == *dep_id && !other.passes)
150                });
151
152            FeatureGraphNode {
153                id: f.id,
154                name: f.name,
155                category: f.category,
156                priority: f.priority,
157                passes: f.passes,
158                in_progress: f.in_progress,
159                blocked: is_blocked,
160                dependencies: f.dependencies.clone(),
161                dependents: dependents_map.get(&f.id).cloned().unwrap_or_default(),
162            }
163        })
164        .collect();
165
166    Ok(Json(graph))
167}
168
169/// POST /api/features — Create a new feature
170async fn create_feature(
171    State(state): State<Arc<AppState>>,
172    Json(input): Json<CreateFeatureInput>,
173) -> AppResult<Json<Feature>> {
174    let db = state
175        .db
176        .as_ref()
177        .ok_or_else(|| AppError::Internal("Database not configured".into()))?;
178
179    let feature = {
180        let conn = db.writer().lock().unwrap();
181        FeatureStore::create(&conn, &input)?
182    };
183    Ok(Json(feature))
184}
185
186/// POST /api/features/bulk — Bulk create features
187async fn bulk_create(
188    State(state): State<Arc<AppState>>,
189    Json(request): Json<BulkCreateRequest>,
190) -> AppResult<Json<Vec<Feature>>> {
191    let db = state
192        .db
193        .as_ref()
194        .ok_or_else(|| AppError::Internal("Database not configured".into()))?;
195
196    let features = {
197        let conn = db.writer().lock().unwrap();
198        FeatureStore::create_bulk(&conn, &request.features)?
199    };
200    Ok(Json(features))
201}
202
203/// POST /api/features/:id/claim — Claim a feature
204async fn claim_feature(
205    State(state): State<Arc<AppState>>,
206    Path(id): Path<String>,
207) -> AppResult<Json<Feature>> {
208    let db = state
209        .db
210        .as_ref()
211        .ok_or_else(|| AppError::Internal("Database not configured".into()))?;
212
213    let id_num: i64 = id
214        .parse()
215        .map_err(|_| AppError::BadRequest("Invalid feature ID".into()))?;
216
217    let feature = {
218        let conn = db.writer().lock().unwrap();
219        FeatureStore::claim_and_get(&conn, id_num)?
220    };
221    Ok(Json(feature))
222}
223
224/// POST /api/features/:id/pass — Mark feature as passing
225async fn mark_passing(
226    State(state): State<Arc<AppState>>,
227    Path(id): Path<String>,
228) -> AppResult<Json<serde_json::Value>> {
229    let db = state
230        .db
231        .as_ref()
232        .ok_or_else(|| AppError::Internal("Database not configured".into()))?;
233
234    let id_num: i64 = id
235        .parse()
236        .map_err(|_| AppError::BadRequest("Invalid feature ID".into()))?;
237
238    let conn = db.writer().lock().unwrap();
239    FeatureStore::mark_passing(&conn, id_num)?;
240    Ok(Json(serde_json::json!({ "success": true })))
241}
242
243/// POST /api/features/:id/fail — Mark feature as failing
244async fn mark_failing(
245    State(state): State<Arc<AppState>>,
246    Path(id): Path<String>,
247) -> AppResult<Json<serde_json::Value>> {
248    let db = state
249        .db
250        .as_ref()
251        .ok_or_else(|| AppError::Internal("Database not configured".into()))?;
252
253    let id_num: i64 = id
254        .parse()
255        .map_err(|_| AppError::BadRequest("Invalid feature ID".into()))?;
256
257    let conn = db.writer().lock().unwrap();
258    FeatureStore::mark_failing(&conn, id_num)?;
259    Ok(Json(serde_json::json!({ "success": true })))
260}
261
262/// POST /api/features/:id/skip — Skip a feature
263async fn skip_feature(
264    State(state): State<Arc<AppState>>,
265    Path(id): Path<String>,
266) -> AppResult<Json<serde_json::Value>> {
267    let db = state
268        .db
269        .as_ref()
270        .ok_or_else(|| AppError::Internal("Database not configured".into()))?;
271
272    let id_num: i64 = id
273        .parse()
274        .map_err(|_| AppError::BadRequest("Invalid feature ID".into()))?;
275
276    let conn = db.writer().lock().unwrap();
277    FeatureStore::skip(&conn, id_num)?;
278    Ok(Json(serde_json::json!({ "success": true })))
279}
280
281/// POST /api/features/:id/dependencies — Add a dependency
282async fn add_dependency(
283    State(state): State<Arc<AppState>>,
284    Path(id): Path<String>,
285    Json(dep): Json<DependencyRef>,
286) -> AppResult<Json<serde_json::Value>> {
287    let db = state
288        .db
289        .as_ref()
290        .ok_or_else(|| AppError::Internal("Database not configured".into()))?;
291
292    let id_num: i64 = id
293        .parse()
294        .map_err(|_| AppError::BadRequest("Invalid feature ID".into()))?;
295
296    // Extract the dependency ID from `DependencyRef`
297    let DependencyRef::Id(dep_id) = dep else {
298        return Err(AppError::BadRequest("Invalid dependency reference".into()));
299    };
300
301    let conn = db.writer().lock().unwrap();
302    FeatureStore::add_dependency(&conn, id_num, dep_id)?;
303    Ok(Json(serde_json::json!({ "success": true })))
304}
305
306/// DELETE `/api/features/:id/dependencies/:dep_id` — Remove a dependency
307async fn remove_dependency(
308    State(state): State<Arc<AppState>>,
309    Path((id, dep_id)): Path<(String, String)>,
310) -> AppResult<Json<serde_json::Value>> {
311    let db = state
312        .db
313        .as_ref()
314        .ok_or_else(|| AppError::Internal("Database not configured".into()))?;
315
316    let id_num: i64 = id
317        .parse()
318        .map_err(|_| AppError::BadRequest("Invalid feature ID".into()))?;
319
320    let dep_id_num: i64 = dep_id
321        .parse()
322        .map_err(|_| AppError::BadRequest("Invalid dependency ID".into()))?;
323
324    let conn = db.writer().lock().unwrap();
325    FeatureStore::remove_dependency(&conn, id_num, dep_id_num)?;
326    Ok(Json(serde_json::json!({ "success": true })))
327}