mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-04-28 11:40:25 +00:00
731 lines
No EOL
26 KiB
Python
731 lines
No EOL
26 KiB
Python
# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Response, Query
|
|
from fastapi_pagination import Page
|
|
from fastapi_pagination.ext.sqlmodel import paginate
|
|
from sqlmodel import Session, select, desc, and_, delete
|
|
from typing import Optional
|
|
from uuid import uuid4
|
|
import logging
|
|
from pydantic import ValidationError
|
|
|
|
from app.model.trigger.trigger import Trigger, TriggerIn, TriggerOut, TriggerUpdate, TriggerConfigSchemaOut
|
|
from app.model.trigger.trigger_execution import TriggerExecution, TriggerExecutionOut
|
|
from app.model.trigger.app_configs import (
|
|
get_config_schema,
|
|
validate_config,
|
|
has_config,
|
|
validate_activation,
|
|
ActivationError,
|
|
)
|
|
from app.model.trigger.app_configs.config_registry import requires_authentication
|
|
from app.model.chat.chat_history import ChatHistory
|
|
from app.type.trigger_types import TriggerType, TriggerStatus
|
|
from app.component.auth import Auth, auth_must
|
|
from app.component.database import session
|
|
from app.component.redis_utils import get_redis_manager
|
|
from app.service.trigger.trigger_schedule_service import TriggerScheduleService
|
|
from fastapi_babel import _
|
|
from sqlalchemy import func
|
|
|
|
logger = logging.getLogger("server_trigger_controller")
|
|
|
|
|
|
ACTIVE_STATUSES = (TriggerStatus.active, TriggerStatus.pending_verification)
|
|
MAX_ACTIVE_PER_USER = 25
|
|
MAX_ACTIVE_PER_PROJECT = 5
|
|
|
|
|
|
def get_active_trigger_counts(session: Session, user_id: str, project_id: str | None = None) -> tuple[int, int]:
|
|
"""Return (user_active_count, project_active_count) for active/pending triggers."""
|
|
user_count = session.exec(
|
|
select(func.count(Trigger.id)).where(
|
|
and_(
|
|
Trigger.user_id == user_id,
|
|
Trigger.status.in_(ACTIVE_STATUSES), # type: ignore[attr-defined]
|
|
)
|
|
)
|
|
).one()
|
|
|
|
project_count = 0
|
|
if project_id:
|
|
project_count = session.exec(
|
|
select(func.count(Trigger.id)).where(
|
|
and_(
|
|
Trigger.user_id == user_id,
|
|
Trigger.project_id == project_id,
|
|
Trigger.status.in_(ACTIVE_STATUSES), # type: ignore[attr-defined]
|
|
)
|
|
)
|
|
).one()
|
|
|
|
return user_count, project_count
|
|
|
|
|
|
def get_execution_counts(session: Session, trigger_ids: list[int]) -> dict[int, int]:
|
|
"""Get execution counts for multiple triggers in a single query."""
|
|
if not trigger_ids:
|
|
return {}
|
|
|
|
result = session.exec(
|
|
select(TriggerExecution.trigger_id, func.count(TriggerExecution.id))
|
|
.where(TriggerExecution.trigger_id.in_(trigger_ids))
|
|
.group_by(TriggerExecution.trigger_id)
|
|
).all()
|
|
|
|
return {trigger_id: count for trigger_id, count in result}
|
|
|
|
|
|
def trigger_to_out(trigger: Trigger, execution_count: int = 0) -> TriggerOut:
|
|
"""Convert Trigger model to TriggerOut with execution count."""
|
|
return TriggerOut(
|
|
id=trigger.id,
|
|
user_id=trigger.user_id,
|
|
project_id=trigger.project_id,
|
|
name=trigger.name,
|
|
description=trigger.description,
|
|
trigger_type=trigger.trigger_type,
|
|
status=trigger.status,
|
|
execution_count=execution_count,
|
|
webhook_url=trigger.webhook_url,
|
|
webhook_method=trigger.webhook_method,
|
|
custom_cron_expression=trigger.custom_cron_expression,
|
|
listener_type=trigger.listener_type,
|
|
agent_model=trigger.agent_model,
|
|
task_prompt=trigger.task_prompt,
|
|
config=trigger.config,
|
|
max_executions_per_hour=trigger.max_executions_per_hour,
|
|
max_executions_per_day=trigger.max_executions_per_day,
|
|
is_single_execution=trigger.is_single_execution,
|
|
last_executed_at=trigger.last_executed_at,
|
|
next_run_at=trigger.next_run_at,
|
|
last_execution_status=trigger.last_execution_status,
|
|
created_at=trigger.created_at,
|
|
updated_at=trigger.updated_at,
|
|
)
|
|
|
|
|
|
router = APIRouter(prefix="/trigger", tags=["Triggers"])
|
|
|
|
@router.post("/", name="create trigger", response_model=TriggerOut)
|
|
def create_trigger(
|
|
data: TriggerIn,
|
|
session: Session = Depends(session),
|
|
auth: Auth = Depends(auth_must)
|
|
):
|
|
"""Create a new trigger."""
|
|
user_id = auth.user.id
|
|
|
|
try:
|
|
# Check if project_id exists in chat_history, if not create one
|
|
if data.project_id:
|
|
existing_chat = session.exec(
|
|
select(ChatHistory).where(ChatHistory.project_id == data.project_id)
|
|
).first()
|
|
|
|
if not existing_chat:
|
|
# Create a new chat_history for this project
|
|
chat_history = ChatHistory(
|
|
user_id=user_id,
|
|
task_id=data.project_id, # Using project_id as task_id
|
|
project_id=data.project_id,
|
|
question=f"Project created via trigger: {data.name}",
|
|
language="en",
|
|
model_platform=data.agent_model or "none",
|
|
model_type=data.agent_model or "none",
|
|
installed_mcp="none", #Expects String
|
|
api_key="",
|
|
api_url="",
|
|
max_retries=3,
|
|
project_name=data.name,
|
|
summary=data.description or "",
|
|
tokens=0,
|
|
spend=0,
|
|
status=2 # completed status
|
|
)
|
|
session.add(chat_history)
|
|
session.commit()
|
|
session.refresh(chat_history)
|
|
|
|
logger.info("Chat history created for new project", extra={
|
|
"user_id": user_id,
|
|
"project_id": data.project_id,
|
|
"chat_history_id": chat_history.id
|
|
})
|
|
|
|
# Send WebSocket notification about new project
|
|
try:
|
|
redis_manager = get_redis_manager()
|
|
redis_manager.publish_execution_event({
|
|
"type": "project_created",
|
|
"project_id": data.project_id,
|
|
"project_name": data.name,
|
|
"chat_history_id": chat_history.id,
|
|
"trigger_name": data.name,
|
|
"user_id": str(user_id),
|
|
"created_at": chat_history.created_at.isoformat() if chat_history.created_at else None
|
|
})
|
|
logger.debug("WebSocket notification sent for new project", extra={
|
|
"user_id": user_id,
|
|
"project_id": data.project_id
|
|
})
|
|
except Exception as e:
|
|
logger.warning("Failed to send WebSocket notification for new project", extra={
|
|
"user_id": user_id,
|
|
"project_id": data.project_id,
|
|
"error": str(e)
|
|
})
|
|
|
|
# Generate webhook URL for webhook-based triggers
|
|
webhook_url = None
|
|
if data.trigger_type in (TriggerType.webhook, TriggerType.slack_trigger):
|
|
webhook_url = f"/webhook/trigger/{uuid4()}"
|
|
|
|
# Validate trigger-type specific config
|
|
if data.config and has_config(data.trigger_type):
|
|
try:
|
|
validate_config(data.trigger_type, data.config)
|
|
except ValidationError as e:
|
|
logger.warning("Invalid trigger config", extra={
|
|
"user_id": user_id,
|
|
"trigger_type": data.trigger_type.value,
|
|
"errors": e.errors()
|
|
})
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Invalid config for {data.trigger_type.value}: {e.errors()}"
|
|
)
|
|
|
|
# Create trigger instance
|
|
trigger_data = data.model_dump()
|
|
trigger_data["user_id"] = str(user_id)
|
|
trigger_data["webhook_url"] = webhook_url
|
|
|
|
# Determine desired initial status based on auth requirements
|
|
if has_config(data.trigger_type) and data.config and requires_authentication(data.trigger_type, data.config):
|
|
desired_status = TriggerStatus.pending_verification
|
|
else:
|
|
desired_status = TriggerStatus.active
|
|
|
|
# Check concurrent active-trigger limits before auto-activating
|
|
user_active, project_active = get_active_trigger_counts(
|
|
session, str(user_id), data.project_id
|
|
)
|
|
if user_active >= MAX_ACTIVE_PER_USER or (
|
|
data.project_id and project_active >= MAX_ACTIVE_PER_PROJECT
|
|
):
|
|
logger.info(
|
|
"Active trigger limit reached — new trigger created as inactive",
|
|
extra={
|
|
"user_id": user_id,
|
|
"project_id": data.project_id,
|
|
"user_active": user_active,
|
|
"project_active": project_active,
|
|
},
|
|
)
|
|
trigger_data["status"] = TriggerStatus.inactive
|
|
else:
|
|
trigger_data["status"] = desired_status
|
|
|
|
trigger = Trigger(**trigger_data)
|
|
session.add(trigger)
|
|
session.commit()
|
|
session.refresh(trigger)
|
|
|
|
# Calculate next_run_at for scheduled triggers
|
|
if trigger.trigger_type == TriggerType.schedule and trigger.custom_cron_expression:
|
|
schedule_service = TriggerScheduleService(session)
|
|
trigger.next_run_at = schedule_service.calculate_next_run_at(trigger)
|
|
session.add(trigger)
|
|
session.commit()
|
|
session.refresh(trigger)
|
|
|
|
logger.info("Trigger created", extra={
|
|
"user_id": user_id,
|
|
"trigger_id": trigger.id,
|
|
"trigger_type": data.trigger_type.value,
|
|
"next_run_at": trigger.next_run_at.isoformat() if trigger.next_run_at else None
|
|
})
|
|
|
|
return trigger_to_out(trigger, 0) # New trigger has 0 executions
|
|
|
|
except Exception as e:
|
|
session.rollback()
|
|
logger.error("Trigger creation failed", extra={
|
|
"user_id": user_id,
|
|
"error": str(e)
|
|
}, exc_info=True)
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
|
|
@router.get("/", name="list triggers")
|
|
def list_triggers(
|
|
trigger_type: Optional[TriggerType] = Query(None, description="Filter by trigger type"),
|
|
status: Optional[TriggerStatus] = Query(None, description="Filter by status"),
|
|
project_id: Optional[str] = Query(None, description="Filter by project ID"),
|
|
session: Session = Depends(session),
|
|
auth: Auth = Depends(auth_must)
|
|
) -> Page[TriggerOut]:
|
|
"""List triggers for current user."""
|
|
user_id = auth.user.id
|
|
|
|
# Build query with filters
|
|
conditions = [Trigger.user_id == str(user_id)]
|
|
|
|
if trigger_type:
|
|
conditions.append(Trigger.trigger_type == trigger_type)
|
|
|
|
if status is not None:
|
|
conditions.append(Trigger.status == status)
|
|
|
|
if project_id:
|
|
conditions.append(Trigger.project_id == project_id)
|
|
|
|
stmt = (
|
|
select(Trigger)
|
|
.where(and_(*conditions))
|
|
.order_by(desc(Trigger.created_at))
|
|
)
|
|
|
|
result = paginate(session, stmt)
|
|
total = result.total if hasattr(result, 'total') else 0
|
|
|
|
# Get execution counts for all triggers in the result
|
|
trigger_ids = [t.id for t in result.items]
|
|
counts = get_execution_counts(session, trigger_ids)
|
|
|
|
# Convert triggers to TriggerOut with execution counts
|
|
result.items = [trigger_to_out(t, counts.get(t.id, 0)) for t in result.items]
|
|
|
|
logger.debug("Triggers listed", extra={
|
|
"user_id": user_id,
|
|
"total": total,
|
|
"filters": {
|
|
"trigger_type": trigger_type.value if trigger_type else None,
|
|
"status": status.value if status is not None else None,
|
|
"project_id": project_id
|
|
}
|
|
})
|
|
|
|
return result
|
|
|
|
@router.get("/{trigger_id}", name="get trigger", response_model=TriggerOut)
|
|
def get_trigger(
|
|
trigger_id: int,
|
|
session: Session = Depends(session),
|
|
auth: Auth = Depends(auth_must)
|
|
):
|
|
"""Get a specific trigger by ID."""
|
|
user_id = auth.user.id
|
|
|
|
trigger = session.exec(
|
|
select(Trigger).where(
|
|
and_(Trigger.id == trigger_id, Trigger.user_id == str(user_id))
|
|
)
|
|
).first()
|
|
|
|
if not trigger:
|
|
logger.warning("Trigger not found", extra={
|
|
"user_id": user_id,
|
|
"trigger_id": trigger_id
|
|
})
|
|
raise HTTPException(status_code=404, detail="Trigger not found")
|
|
|
|
# Get execution count
|
|
counts = get_execution_counts(session, [trigger_id])
|
|
execution_count = counts.get(trigger_id, 0)
|
|
|
|
logger.debug("Trigger retrieved", extra={
|
|
"user_id": user_id,
|
|
"trigger_id": trigger_id
|
|
})
|
|
|
|
return trigger_to_out(trigger, execution_count)
|
|
|
|
|
|
@router.put("/{trigger_id}", name="update trigger", response_model=TriggerOut)
|
|
def update_trigger(
|
|
trigger_id: int,
|
|
data: TriggerUpdate,
|
|
session: Session = Depends(session),
|
|
auth: Auth = Depends(auth_must)
|
|
):
|
|
"""Update a trigger."""
|
|
user_id = auth.user.id
|
|
|
|
trigger = session.exec(
|
|
select(Trigger).where(
|
|
and_(Trigger.id == trigger_id, Trigger.user_id == str(user_id))
|
|
)
|
|
).first()
|
|
|
|
if not trigger:
|
|
logger.warning("Trigger not found for update", extra={
|
|
"user_id": user_id,
|
|
"trigger_id": trigger_id
|
|
})
|
|
raise HTTPException(status_code=404, detail="Trigger not found")
|
|
|
|
try:
|
|
update_data = data.model_dump(exclude_unset=True)
|
|
|
|
# Validate config if being updated
|
|
if "config" in update_data and update_data["config"] is not None:
|
|
if has_config(trigger.trigger_type):
|
|
try:
|
|
validate_config(trigger.trigger_type, update_data["config"])
|
|
except ValidationError as e:
|
|
logger.warning("Invalid trigger config on update", extra={
|
|
"user_id": user_id,
|
|
"trigger_id": trigger_id,
|
|
"trigger_type": trigger.trigger_type.value,
|
|
"errors": e.errors()
|
|
})
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Invalid config for {trigger.trigger_type.value}: {e.errors()}"
|
|
)
|
|
|
|
for key, value in update_data.items():
|
|
setattr(trigger, key, value)
|
|
|
|
# Recalculate next_run_at if cron expression or status changed for scheduled triggers
|
|
if trigger.trigger_type == TriggerType.schedule:
|
|
if "custom_cron_expression" in update_data or "status" in update_data:
|
|
if trigger.status == TriggerStatus.active and trigger.custom_cron_expression:
|
|
schedule_service = TriggerScheduleService(session)
|
|
trigger.next_run_at = schedule_service.calculate_next_run_at(trigger)
|
|
|
|
session.add(trigger)
|
|
session.commit()
|
|
session.refresh(trigger)
|
|
|
|
# Get execution count
|
|
counts = get_execution_counts(session, [trigger_id])
|
|
execution_count = counts.get(trigger_id, 0)
|
|
|
|
logger.info("Trigger updated", extra={
|
|
"user_id": user_id,
|
|
"trigger_id": trigger_id,
|
|
"fields_updated": list(update_data.keys()),
|
|
"next_run_at": trigger.next_run_at.isoformat() if trigger.next_run_at else None
|
|
})
|
|
|
|
return trigger_to_out(trigger, execution_count)
|
|
|
|
except Exception as e:
|
|
session.rollback()
|
|
logger.error("Trigger update failed", extra={
|
|
"user_id": user_id,
|
|
"trigger_id": trigger_id,
|
|
"error": str(e)
|
|
}, exc_info=True)
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
|
|
@router.delete("/{trigger_id}", name="delete trigger")
|
|
def delete_trigger(
|
|
trigger_id: int,
|
|
session: Session = Depends(session),
|
|
auth: Auth = Depends(auth_must)
|
|
):
|
|
"""Delete a trigger."""
|
|
user_id = auth.user.id
|
|
|
|
trigger = session.exec(
|
|
select(Trigger).where(
|
|
and_(Trigger.id == trigger_id, Trigger.user_id == str(user_id))
|
|
)
|
|
).first()
|
|
|
|
if not trigger:
|
|
logger.warning("Trigger not found for deletion", extra={
|
|
"user_id": user_id,
|
|
"trigger_id": trigger_id
|
|
})
|
|
raise HTTPException(status_code=404, detail="Trigger not found")
|
|
|
|
try:
|
|
# Delete execution logs first (bulk delete)
|
|
session.exec(
|
|
delete(TriggerExecution).where(
|
|
TriggerExecution.trigger_id == trigger_id
|
|
)
|
|
)
|
|
|
|
# Then delete the trigger
|
|
session.delete(trigger)
|
|
|
|
session.commit()
|
|
|
|
logger.info("Trigger deleted", extra={
|
|
"user_id": user_id,
|
|
"trigger_id": trigger_id
|
|
})
|
|
|
|
return Response(status_code=204)
|
|
|
|
except Exception as e:
|
|
session.rollback()
|
|
logger.error("Trigger deletion failed", extra={
|
|
"user_id": user_id,
|
|
"trigger_id": trigger_id,
|
|
"error": str(e)
|
|
}, exc_info=True)
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
|
|
@router.post("/{trigger_id}/activate", name="activate trigger", response_model=TriggerOut)
|
|
def activate_trigger(
|
|
trigger_id: int,
|
|
session: Session = Depends(session),
|
|
auth: Auth = Depends(auth_must)
|
|
):
|
|
"""Activate a trigger."""
|
|
user_id = auth.user.id
|
|
|
|
trigger = session.exec(
|
|
select(Trigger).where(
|
|
and_(Trigger.id == trigger_id, Trigger.user_id == str(user_id))
|
|
)
|
|
).first()
|
|
|
|
if not trigger:
|
|
logger.warning("Trigger not found for activation", extra={
|
|
"user_id": user_id,
|
|
"trigger_id": trigger_id
|
|
})
|
|
raise HTTPException(status_code=404, detail="Trigger not found")
|
|
|
|
try:
|
|
# --- Concurrent active-trigger limits ---
|
|
user_active, project_active = get_active_trigger_counts(
|
|
session, str(user_id), trigger.project_id
|
|
)
|
|
if user_active >= MAX_ACTIVE_PER_USER:
|
|
logger.warning("User active trigger limit reached", extra={
|
|
"user_id": user_id,
|
|
"trigger_id": trigger_id,
|
|
"current_active": user_active,
|
|
"limit": MAX_ACTIVE_PER_USER,
|
|
})
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Maximum number of concurrent active triggers ({MAX_ACTIVE_PER_USER}) reached for this user"
|
|
)
|
|
|
|
if trigger.project_id and project_active >= MAX_ACTIVE_PER_PROJECT:
|
|
logger.warning("Project active trigger limit reached", extra={
|
|
"user_id": user_id,
|
|
"trigger_id": trigger_id,
|
|
"project_id": trigger.project_id,
|
|
"current_active": project_active,
|
|
"limit": MAX_ACTIVE_PER_PROJECT,
|
|
})
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Maximum number of concurrent active triggers ({MAX_ACTIVE_PER_PROJECT}) reached for this project"
|
|
)
|
|
|
|
# Check if authentication is required first — auth-required triggers
|
|
# go straight to pending_verification (credentials are provided via
|
|
# the auth flow, so missing-credential errors are expected).
|
|
if has_config(trigger.trigger_type) and requires_authentication(trigger.trigger_type, trigger.config):
|
|
trigger.status = TriggerStatus.pending_verification
|
|
logger.info("Trigger set to pending verification (authentication required)", extra={
|
|
"user_id": user_id,
|
|
"trigger_id": trigger_id,
|
|
"trigger_type": trigger.trigger_type.value
|
|
})
|
|
# Save the status change before raising the exception
|
|
session.add(trigger)
|
|
session.commit()
|
|
session.refresh(trigger)
|
|
raise HTTPException(
|
|
status_code=401,
|
|
detail={
|
|
"message": "Authentication required for this trigger type",
|
|
"missing_requirements": ["authentication"],
|
|
"trigger_type": trigger.trigger_type.value
|
|
}
|
|
)
|
|
|
|
# For non-auth triggers, validate activation requirements
|
|
if has_config(trigger.trigger_type):
|
|
try:
|
|
validate_activation(
|
|
trigger_type=trigger.trigger_type,
|
|
config_data=trigger.config,
|
|
user_id=int(user_id),
|
|
session=session
|
|
)
|
|
except ActivationError as e:
|
|
logger.warning("Trigger activation requirements not met", extra={
|
|
"user_id": user_id,
|
|
"trigger_id": trigger_id,
|
|
"trigger_type": trigger.trigger_type.value,
|
|
"missing_requirements": e.missing_requirements
|
|
})
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={
|
|
"message": e.message,
|
|
"missing_requirements": e.missing_requirements,
|
|
"trigger_type": trigger.trigger_type.value
|
|
}
|
|
)
|
|
|
|
trigger.status = TriggerStatus.active
|
|
session.add(trigger)
|
|
session.commit()
|
|
session.refresh(trigger)
|
|
|
|
# Get execution count
|
|
counts = get_execution_counts(session, [trigger_id])
|
|
execution_count = counts.get(trigger_id, 0)
|
|
|
|
logger.info("Trigger status updated", extra={
|
|
"user_id": user_id,
|
|
"trigger_id": trigger_id,
|
|
"status": trigger.status.value
|
|
})
|
|
|
|
return trigger_to_out(trigger, execution_count)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
session.rollback()
|
|
logger.error("Trigger activation failed", extra={
|
|
"user_id": user_id,
|
|
"trigger_id": trigger_id,
|
|
"error": str(e)
|
|
}, exc_info=True)
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
|
|
@router.post("/{trigger_id}/deactivate", name="deactivate trigger", response_model=TriggerOut)
|
|
def deactivate_trigger(
|
|
trigger_id: int,
|
|
session: Session = Depends(session),
|
|
auth: Auth = Depends(auth_must)
|
|
):
|
|
"""Deactivate a trigger."""
|
|
user_id = auth.user.id
|
|
|
|
trigger = session.exec(
|
|
select(Trigger).where(
|
|
and_(Trigger.id == trigger_id, Trigger.user_id == str(user_id))
|
|
)
|
|
).first()
|
|
|
|
if not trigger:
|
|
logger.warning("Trigger not found for deactivation", extra={
|
|
"user_id": user_id,
|
|
"trigger_id": trigger_id
|
|
})
|
|
raise HTTPException(status_code=404, detail="Trigger not found")
|
|
|
|
try:
|
|
trigger.status = TriggerStatus.inactive
|
|
session.add(trigger)
|
|
session.commit()
|
|
session.refresh(trigger)
|
|
|
|
# Get execution count
|
|
counts = get_execution_counts(session, [trigger_id])
|
|
execution_count = counts.get(trigger_id, 0)
|
|
|
|
logger.info("Trigger deactivated", extra={
|
|
"user_id": user_id,
|
|
"trigger_id": trigger_id
|
|
})
|
|
|
|
return trigger_to_out(trigger, execution_count)
|
|
|
|
except Exception as e:
|
|
session.rollback()
|
|
logger.error("Trigger deactivation failed", extra={
|
|
"user_id": user_id,
|
|
"trigger_id": trigger_id,
|
|
"error": str(e)
|
|
}, exc_info=True)
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
|
|
@router.get("/{trigger_id}/executions", name="list trigger executions")
|
|
def list_trigger_executions(
|
|
trigger_id: int,
|
|
session: Session = Depends(session),
|
|
auth: Auth = Depends(auth_must)
|
|
) -> Page[TriggerExecutionOut]:
|
|
"""List executions for a specific trigger."""
|
|
user_id = auth.user.id
|
|
|
|
# First verify the trigger belongs to the user
|
|
trigger = session.exec(
|
|
select(Trigger).where(
|
|
and_(Trigger.id == trigger_id, Trigger.user_id == str(user_id))
|
|
)
|
|
).first()
|
|
|
|
if not trigger:
|
|
logger.warning("Trigger not found for executions list", extra={
|
|
"user_id": user_id,
|
|
"trigger_id": trigger_id
|
|
})
|
|
raise HTTPException(status_code=404, detail="Trigger not found")
|
|
|
|
# Get executions for this trigger
|
|
stmt = (
|
|
select(TriggerExecution)
|
|
.where(TriggerExecution.trigger_id == trigger_id)
|
|
.order_by(desc(TriggerExecution.created_at))
|
|
)
|
|
|
|
result = paginate(session, stmt)
|
|
total = result.total if hasattr(result, 'total') else 0
|
|
|
|
logger.debug("Trigger executions listed", extra={
|
|
"user_id": user_id,
|
|
"trigger_id": trigger_id,
|
|
"total": total
|
|
})
|
|
|
|
return result
|
|
|
|
|
|
# ============================================================================
|
|
# Trigger Config Endpoints
|
|
# ============================================================================
|
|
|
|
@router.get("/{trigger_type}/config", name="get trigger type config schema")
|
|
def get_trigger_type_config(
|
|
trigger_type: TriggerType,
|
|
auth: Auth = Depends(auth_must)
|
|
) -> TriggerConfigSchemaOut:
|
|
"""
|
|
Get the configuration schema for a specific trigger type.
|
|
|
|
This endpoint returns the JSON schema for the trigger type's config field,
|
|
which can be used by the frontend to dynamically render configuration forms.
|
|
"""
|
|
schema = get_config_schema(trigger_type)
|
|
|
|
return TriggerConfigSchemaOut(
|
|
trigger_type=trigger_type.value,
|
|
has_config=has_config(trigger_type),
|
|
schema_=schema
|
|
) |