refactor: tech debt reduction - extract hooks, deduplicate helpers, update deps, add CI

- Extract useCustomStepFlow hook from TreeNavigationPage (1040 → 759 lines)
- Create core/filters.py with shared tree/step visibility filters
- Create services/export_service.py from session export logic
- Add GitHub Actions CI/CD pipeline (pytest + lint + build)
- Add GIN index migration for full-text search on trees
- Update FastAPI 0.128.5, Pydantic 2.12.5, SQLAlchemy 2.0.46, +5 more
- Fix regex → pattern deprecation in Query() params

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-08 08:14:22 -05:00
parent f4eb3fe186
commit b97596d286
12 changed files with 786 additions and 557 deletions

View File

@@ -3,12 +3,13 @@ from typing import Optional
from datetime import datetime, timezone
from decimal import Decimal
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import select, or_, and_, func, desc, Integer, case
from sqlalchemy import select, func, desc, Integer, case
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.api.deps import get_current_active_user, require_engineer_or_admin
from app.core.permissions import can_view_step, can_edit_step
from app.core.filters import build_step_visibility_filter
from app.models.user import User
from app.models.step_library import StepLibrary, StepRating
from app.models.step_category import StepCategory
@@ -53,29 +54,15 @@ async def get_step_or_404(
return step
def build_visibility_filter(user: User):
"""Build SQLAlchemy filter for step visibility based on user."""
if user.account_id:
return or_(
StepLibrary.visibility == 'public',
and_(StepLibrary.visibility == 'team', StepLibrary.account_id == user.account_id),
StepLibrary.created_by == user.id # Own private steps
)
else:
return or_(
StepLibrary.visibility == 'public',
StepLibrary.created_by == user.id
)
@router.get("", response_model=list[StepLibraryListResponse])
async def list_steps(
visibility: Optional[str] = Query(None, regex="^(private|team|public)$"),
visibility: Optional[str] = Query(None, pattern="^(private|team|public)$"),
category_id: Optional[UUID] = None,
tags: Optional[list[str]] = Query(None),
min_rating: Optional[float] = Query(None, ge=0, le=5),
step_type: Optional[str] = Query(None, regex="^(decision|action|solution)$"),
sort_by: str = Query("recent", regex="^(recent|popular|highest_rated|most_used)$"),
step_type: Optional[str] = Query(None, pattern="^(decision|action|solution)$"),
sort_by: str = Query("recent", pattern="^(recent|popular|highest_rated|most_used)$"),
limit: int = Query(20, ge=1, le=100),
offset: int = Query(0, ge=0),
db: AsyncSession = Depends(get_db),
@@ -84,7 +71,7 @@ async def list_steps(
"""List steps with filters and pagination."""
query = select(StepLibrary).where(
StepLibrary.is_active == True,
build_visibility_filter(current_user)
build_step_visibility_filter(current_user)
)
# Apply filters
@@ -165,7 +152,7 @@ async def search_steps(
query = select(StepLibrary).where(
StepLibrary.is_active == True,
build_visibility_filter(current_user),
build_step_visibility_filter(current_user),
func.to_tsvector('english', StepLibrary.title).match(search_query)
).order_by(desc(StepLibrary.rating_average)).limit(limit)
@@ -218,7 +205,7 @@ async def get_popular_tags(
func.count().label('count')
).where(
StepLibrary.is_active == True,
build_visibility_filter(current_user)
build_step_visibility_filter(current_user)
).group_by(
func.unnest(StepLibrary.tags)
).order_by(