Add a dependency relationship between features. The dep_id feature must be completed before feature_id can be started. Validates: self-reference, existence, circular dependencies, max limit.
(project_name: str, feature_id: int, dep_id: int)
| 581 | |
| 582 | @router.post("/{feature_id}/dependencies/{dep_id}") |
| 583 | async def add_dependency(project_name: str, feature_id: int, dep_id: int): |
| 584 | """Add a dependency relationship between features. |
| 585 | |
| 586 | The dep_id feature must be completed before feature_id can be started. |
| 587 | Validates: self-reference, existence, circular dependencies, max limit. |
| 588 | """ |
| 589 | project_name = validate_project_name(project_name) |
| 590 | |
| 591 | # Security: Self-reference check |
| 592 | if feature_id == dep_id: |
| 593 | raise HTTPException(status_code=400, detail="A feature cannot depend on itself") |
| 594 | |
| 595 | project_dir = _get_project_path(project_name) |
| 596 | |
| 597 | if not project_dir: |
| 598 | raise HTTPException(status_code=404, detail=f"Project '{project_name}' not found in registry") |
| 599 | |
| 600 | if not project_dir.exists(): |
| 601 | raise HTTPException(status_code=404, detail="Project directory not found") |
| 602 | |
| 603 | would_create_circular_dependency, MAX_DEPENDENCIES_PER_FEATURE = _get_dependency_resolver() |
| 604 | _, Feature = _get_db_classes() |
| 605 | |
| 606 | try: |
| 607 | with get_db_session(project_dir) as session: |
| 608 | feature = session.query(Feature).filter(Feature.id == feature_id).first() |
| 609 | dependency = session.query(Feature).filter(Feature.id == dep_id).first() |
| 610 | |
| 611 | if not feature: |
| 612 | raise HTTPException(status_code=404, detail=f"Feature {feature_id} not found") |
| 613 | if not dependency: |
| 614 | raise HTTPException(status_code=404, detail=f"Dependency {dep_id} not found") |
| 615 | |
| 616 | current_deps = feature.dependencies or [] |
| 617 | |
| 618 | # Security: Limit check |
| 619 | if len(current_deps) >= MAX_DEPENDENCIES_PER_FEATURE: |
| 620 | raise HTTPException(status_code=400, detail=f"Maximum {MAX_DEPENDENCIES_PER_FEATURE} dependencies allowed") |
| 621 | |
| 622 | if dep_id in current_deps: |
| 623 | raise HTTPException(status_code=400, detail="Dependency already exists") |
| 624 | |
| 625 | # Security: Circular dependency check |
| 626 | # source_id = feature_id (gaining dep), target_id = dep_id (being depended upon) |
| 627 | all_features = [f.to_dict() for f in session.query(Feature).all()] |
| 628 | if would_create_circular_dependency(all_features, feature_id, dep_id): |
| 629 | raise HTTPException(status_code=400, detail="Would create circular dependency") |
| 630 | |
| 631 | current_deps.append(dep_id) |
| 632 | feature.dependencies = sorted(current_deps) |
| 633 | session.commit() |
| 634 | |
| 635 | return {"success": True, "feature_id": feature_id, "dependencies": feature.dependencies} |
| 636 | except HTTPException: |
| 637 | raise |
| 638 | except Exception: |
| 639 | logger.exception("Failed to add dependency") |
| 640 | raise HTTPException(status_code=500, detail="Failed to add dependency") |
nothing calls this directly
no test coverage detected