diff --git a/backend/app/api/endpoints/trees.py b/backend/app/api/endpoints/trees.py index fb63f7e3..dbed1105 100644 --- a/backend/app/api/endpoints/trees.py +++ b/backend/app/api/endpoints/trees.py @@ -28,6 +28,7 @@ from app.core.subscriptions import check_tree_limit from app.core.audit import log_audit from app.core.config import settings from app.core.tree_validation import can_publish_tree +from app.core.step_sync import sync_steps_from_tree, deactivate_synced_steps_for_tree router = APIRouter(prefix="/trees", tags=["trees"]) @@ -640,6 +641,21 @@ async def update_tree( if "tree_structure" in update_data: tree.version += 1 + # Sync steps to step library when publishing + if update_data.get("status") == 'published' or tree.status == 'published': + _structure = update_data.get("tree_structure", tree.tree_structure) + _type = update_data.get("tree_type", tree.tree_type) + _is_public = update_data.get("is_public", tree.is_public) + await sync_steps_from_tree( + db=db, + tree_id=tree.id, + tree_type=_type, + tree_structure=_structure, + author_id=tree.author_id, + account_id=tree.account_id, + is_public=_is_public, + ) + # Handle tags replacement if tags_data is not None: from app.models.tag import tree_tag_assignments @@ -753,6 +769,10 @@ async def delete_tree( tree_tag_assignments.delete().where(tree_tag_assignments.c.tree_id == tree.id) ) + # Deactivate any synced step library entries before deletion + # (must happen before db.delete/commit — FK SET NULL would lose the reference) + await deactivate_synced_steps_for_tree(db, tree.id) + await log_audit(db, current_user.id, "tree.delete", "tree", tree.id, {"tree_name": tree.name}) await db.commit() diff --git a/backend/app/core/step_sync.py b/backend/app/core/step_sync.py index cd90325f..4a42066a 100644 --- a/backend/app/core/step_sync.py +++ b/backend/app/core/step_sync.py @@ -136,7 +136,7 @@ async def sync_steps_from_tree( helpful_yes, helpful_no, is_featured, is_verified, created_at, updated_at ) VALUES ( - gen_random_uuid(), :title, :step_type, :content::jsonb, + gen_random_uuid(), :title, :step_type, CAST(:content AS jsonb), :created_by, :account_id, :visibility, true, :source_tree_id, :source_node_id, :last_synced_at, '{}', true, diff --git a/backend/app/models/step_library.py b/backend/app/models/step_library.py index bd2b223e..e93c1f75 100644 --- a/backend/app/models/step_library.py +++ b/backend/app/models/step_library.py @@ -2,7 +2,7 @@ import uuid from datetime import datetime, timezone from decimal import Decimal from typing import TYPE_CHECKING, Optional -from sqlalchemy import String, DateTime, Integer, Boolean, Text, Numeric, ForeignKey, CheckConstraint +from sqlalchemy import String, DateTime, Integer, Boolean, Text, Numeric, ForeignKey, CheckConstraint, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.dialects.postgresql import UUID, JSONB, ARRAY from app.core.database import Base @@ -23,6 +23,7 @@ class StepLibrary(Base): "step_type IN ('decision', 'action', 'solution')", name='ck_step_library_step_type' ), + UniqueConstraint('source_tree_id', 'source_node_id', name='uq_step_library_source_node'), ) id: Mapped[uuid.UUID] = mapped_column(