Skip to main content

flow_server/routes/
features.rs

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