feat: complete LOW priority code quality improvements
Backend: - LOW-002: Add Query validation with max page size limits (100) - LOW-003: Replace magic strings with TaskStatus.is_done flag - LOW-004: Add 'creation' trigger type validation - Add action_executor.py with UpdateFieldAction and AutoAssignAction Frontend: - LOW-005: Replace TypeScript 'any' with 'unknown' + type guards - LOW-006: Add ConfirmModal component with A11Y support - LOW-007: Add ToastContext for user feedback notifications - LOW-009: Add Skeleton components (17 loading states replaced) - LOW-010: Setup Vitest with 21 tests for ConfirmModal and Skeleton Components updated: - App.tsx, ProtectedRoute.tsx, Spaces.tsx, Projects.tsx, Tasks.tsx - ProjectSettings.tsx, AuditPage.tsx, WorkloadPage.tsx, ProjectHealthPage.tsx - Comments.tsx, AttachmentList.tsx, TriggerList.tsx, TaskDetailModal.tsx - NotificationBell.tsx, BlockerDialog.tsx, CalendarView.tsx, WorkloadUserDetail.tsx 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,17 @@
|
||||
import uuid
|
||||
import logging
|
||||
from typing import List, Dict, Any, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models import Trigger, TriggerLog, Task, User, Project
|
||||
from app.services.notification_service import NotificationService
|
||||
from app.services.action_executor import (
|
||||
ActionExecutor,
|
||||
ActionExecutionError,
|
||||
ActionValidationError,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TriggerService:
|
||||
@@ -74,23 +82,78 @@ class TriggerService:
|
||||
old_values: Dict[str, Any],
|
||||
new_values: Dict[str, Any],
|
||||
) -> TriggerLog:
|
||||
"""Execute trigger actions and log the result."""
|
||||
"""Execute trigger actions and log the result.
|
||||
|
||||
Uses a database savepoint to ensure atomicity - if any action fails,
|
||||
all previously executed actions within this trigger are rolled back.
|
||||
"""
|
||||
actions = trigger.actions if isinstance(trigger.actions, list) else [trigger.actions]
|
||||
executed_actions = []
|
||||
error_message = None
|
||||
|
||||
# Build execution context
|
||||
context = {
|
||||
"old_values": old_values,
|
||||
"new_values": new_values,
|
||||
"current_user": current_user,
|
||||
"trigger": trigger,
|
||||
}
|
||||
|
||||
# Use savepoint for transaction atomicity - if any action fails,
|
||||
# all changes made by previous actions will be rolled back
|
||||
savepoint = db.begin_nested()
|
||||
try:
|
||||
for action in actions:
|
||||
action_type = action.get("type")
|
||||
|
||||
# Handle built-in notify action
|
||||
if action_type == "notify":
|
||||
TriggerService._execute_notify_action(db, action, task, current_user, old_values, new_values)
|
||||
executed_actions.append({"type": action_type, "status": "success"})
|
||||
|
||||
# Handle update_field action (FEAT-014)
|
||||
elif action_type == "update_field":
|
||||
result = ActionExecutor.execute_action(db, task, action, context)
|
||||
if result:
|
||||
executed_actions.append(result)
|
||||
logger.info(
|
||||
f"Trigger '{trigger.name}' executed update_field: "
|
||||
f"field={result.get('field')}, new_value={result.get('new_value')}"
|
||||
)
|
||||
|
||||
# Handle auto_assign action (FEAT-015)
|
||||
elif action_type == "auto_assign":
|
||||
result = ActionExecutor.execute_action(db, task, action, context)
|
||||
if result:
|
||||
executed_actions.append(result)
|
||||
logger.info(
|
||||
f"Trigger '{trigger.name}' executed auto_assign: "
|
||||
f"strategy={result.get('strategy')}, assignee={result.get('new_assignee_id')}"
|
||||
)
|
||||
|
||||
# Try to execute via ActionExecutor for extensibility
|
||||
else:
|
||||
result = ActionExecutor.execute_action(db, task, action, context)
|
||||
if result:
|
||||
executed_actions.append(result)
|
||||
|
||||
# All actions succeeded, commit the savepoint
|
||||
savepoint.commit()
|
||||
status = "success"
|
||||
except Exception as e:
|
||||
except ActionExecutionError as e:
|
||||
# Rollback all changes made by previously executed actions
|
||||
savepoint.rollback()
|
||||
status = "failed"
|
||||
error_message = str(e)
|
||||
executed_actions.append({"type": "error", "message": str(e)})
|
||||
logger.error(f"Trigger '{trigger.name}' action execution failed, rolling back: {e}")
|
||||
except Exception as e:
|
||||
# Rollback all changes made by previously executed actions
|
||||
savepoint.rollback()
|
||||
status = "failed"
|
||||
error_message = str(e)
|
||||
executed_actions.append({"type": "error", "message": str(e)})
|
||||
logger.exception(f"Trigger '{trigger.name}' unexpected error, rolling back: {e}")
|
||||
|
||||
log = TriggerLog(
|
||||
id=str(uuid.uuid4()),
|
||||
@@ -198,3 +261,36 @@ class TriggerService:
|
||||
)
|
||||
db.add(log)
|
||||
return log
|
||||
|
||||
@staticmethod
|
||||
def validate_actions(actions: List[Dict[str, Any]], db: Session) -> None:
|
||||
"""Validate trigger actions configuration.
|
||||
|
||||
Args:
|
||||
actions: List of action configurations
|
||||
db: Database session
|
||||
|
||||
Raises:
|
||||
ActionValidationError: If any action is invalid
|
||||
"""
|
||||
valid_action_types = ["notify", "update_field", "auto_assign"]
|
||||
|
||||
for action in actions:
|
||||
action_type = action.get("type")
|
||||
if not action_type:
|
||||
raise ActionValidationError("Missing action 'type'")
|
||||
|
||||
if action_type not in valid_action_types:
|
||||
raise ActionValidationError(
|
||||
f"Invalid action type '{action_type}'. "
|
||||
f"Valid types: {valid_action_types}"
|
||||
)
|
||||
|
||||
# Validate via ActionExecutor for extensible actions
|
||||
if action_type in ["update_field", "auto_assign"]:
|
||||
ActionExecutor.validate_action(action, db)
|
||||
|
||||
@staticmethod
|
||||
def get_supported_action_types() -> List[str]:
|
||||
"""Get list of all supported action types."""
|
||||
return ["notify"] + ActionExecutor.get_supported_actions()
|
||||
|
||||
Reference in New Issue
Block a user