flow_server/routes/
features.rs1use 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
19pub 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
37async 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
52async 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
73async 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
88async 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
103async 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
118async 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 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 let features_for_check = features.clone();
141
142 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
168async 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
184async 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
200async 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
220async 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
238async 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
256async 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
274async 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 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
298async 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}