Create multiple features at once. Features are assigned sequential priorities starting from: - starting_priority if specified (must be >= 1) - max(existing priorities) + 1 if not specified This is useful for: - Expanding a project with new features via AI - Importing f
(project_name: str, bulk: FeatureBulkCreate)
| 219 | |
| 220 | @router.post("/bulk", response_model=FeatureBulkCreateResponse) |
| 221 | async def create_features_bulk(project_name: str, bulk: FeatureBulkCreate): |
| 222 | """ |
| 223 | Create multiple features at once. |
| 224 | |
| 225 | Features are assigned sequential priorities starting from: |
| 226 | - starting_priority if specified (must be >= 1) |
| 227 | - max(existing priorities) + 1 if not specified |
| 228 | |
| 229 | This is useful for: |
| 230 | - Expanding a project with new features via AI |
| 231 | - Importing features from external sources |
| 232 | - Batch operations |
| 233 | |
| 234 | Returns: |
| 235 | {"created": N, "features": [...]} |
| 236 | """ |
| 237 | project_name = validate_project_name(project_name) |
| 238 | project_dir = _get_project_path(project_name) |
| 239 | |
| 240 | if not project_dir: |
| 241 | raise HTTPException(status_code=404, detail=f"Project '{project_name}' not found in registry") |
| 242 | |
| 243 | if not project_dir.exists(): |
| 244 | raise HTTPException(status_code=404, detail="Project directory not found") |
| 245 | |
| 246 | if not bulk.features: |
| 247 | return FeatureBulkCreateResponse(created=0, features=[]) |
| 248 | |
| 249 | # Validate starting_priority if provided |
| 250 | if bulk.starting_priority is not None and bulk.starting_priority < 1: |
| 251 | raise HTTPException(status_code=400, detail="starting_priority must be >= 1") |
| 252 | |
| 253 | _, Feature = _get_db_classes() |
| 254 | |
| 255 | try: |
| 256 | with get_db_session(project_dir) as session: |
| 257 | # Determine starting priority |
| 258 | # Note: SQLite uses file-level locking, not row-level locking, so we rely on |
| 259 | # SQLite's transaction isolation. Concurrent bulk creates may get overlapping |
| 260 | # priorities, but this is acceptable since priorities can be reordered. |
| 261 | if bulk.starting_priority is not None: |
| 262 | current_priority = bulk.starting_priority |
| 263 | else: |
| 264 | max_priority_feature = ( |
| 265 | session.query(Feature) |
| 266 | .order_by(Feature.priority.desc()) |
| 267 | .first() |
| 268 | ) |
| 269 | current_priority = (max_priority_feature.priority + 1) if max_priority_feature else 1 |
| 270 | |
| 271 | created_ids = [] |
| 272 | |
| 273 | for feature_data in bulk.features: |
| 274 | db_feature = Feature( |
| 275 | priority=current_priority, |
| 276 | category=feature_data.category, |
| 277 | name=feature_data.name, |
| 278 | description=feature_data.description, |
nothing calls this directly
no test coverage detected