feat: trigger step library sync on tree publish and deactivate on delete
- Call sync_steps_from_tree in update_tree whenever the tree is published
(status transitions to 'published' or is already published and structure changes)
- Call deactivate_synced_steps_for_tree in delete_tree before db.commit()
so the FK SET NULL does not nullify source_tree_id before the WHERE clause runs
- Fix ::jsonb cast syntax in step_sync.py (asyncpg rejects :: operator in text()
queries; replaced with CAST(:content AS jsonb))
- Add UniqueConstraint('source_tree_id','source_node_id') to StepLibrary model
so Base.metadata.create_all (used by tests) creates the constraint that the
ON CONFLICT clause in sync_steps_from_tree depends on
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user