refactor:Decouple Domain Models from Direct Database Access (#27316)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -28,7 +28,7 @@ Example:
|
||||
runs = repo.get_paginated_workflow_runs(
|
||||
tenant_id="tenant-123",
|
||||
app_id="app-456",
|
||||
triggered_from="debugging",
|
||||
triggered_from=WorkflowRunTriggeredFrom.DEBUGGING,
|
||||
limit=20
|
||||
)
|
||||
```
|
||||
@@ -40,7 +40,14 @@ from typing import Protocol
|
||||
|
||||
from core.workflow.repositories.workflow_execution_repository import WorkflowExecutionRepository
|
||||
from libs.infinite_scroll_pagination import InfiniteScrollPagination
|
||||
from models.enums import WorkflowRunTriggeredFrom
|
||||
from models.workflow import WorkflowRun
|
||||
from repositories.types import (
|
||||
AverageInteractionStats,
|
||||
DailyRunsStats,
|
||||
DailyTerminalsStats,
|
||||
DailyTokenCostStats,
|
||||
)
|
||||
|
||||
|
||||
class APIWorkflowRunRepository(WorkflowExecutionRepository, Protocol):
|
||||
@@ -56,7 +63,7 @@ class APIWorkflowRunRepository(WorkflowExecutionRepository, Protocol):
|
||||
self,
|
||||
tenant_id: str,
|
||||
app_id: str,
|
||||
triggered_from: str,
|
||||
triggered_from: WorkflowRunTriggeredFrom | Sequence[WorkflowRunTriggeredFrom],
|
||||
limit: int = 20,
|
||||
last_id: str | None = None,
|
||||
status: str | None = None,
|
||||
@@ -71,7 +78,7 @@ class APIWorkflowRunRepository(WorkflowExecutionRepository, Protocol):
|
||||
Args:
|
||||
tenant_id: Tenant identifier for multi-tenant isolation
|
||||
app_id: Application identifier
|
||||
triggered_from: Filter by trigger source (e.g., "debugging", "app-run")
|
||||
triggered_from: Filter by trigger source(s) (e.g., "debugging", "app-run", or list of values)
|
||||
limit: Maximum number of records to return (default: 20)
|
||||
last_id: Cursor for pagination - ID of the last record from previous page
|
||||
status: Optional filter by status (e.g., "running", "succeeded", "failed")
|
||||
@@ -109,6 +116,31 @@ class APIWorkflowRunRepository(WorkflowExecutionRepository, Protocol):
|
||||
"""
|
||||
...
|
||||
|
||||
def get_workflow_run_by_id_without_tenant(
|
||||
self,
|
||||
run_id: str,
|
||||
) -> WorkflowRun | None:
|
||||
"""
|
||||
Get a specific workflow run by ID without tenant/app context.
|
||||
|
||||
Retrieves a single workflow run using only the run ID, without
|
||||
requiring tenant_id or app_id. This method is intended for internal
|
||||
system operations like tracing and monitoring where the tenant context
|
||||
is not available upfront.
|
||||
|
||||
Args:
|
||||
run_id: Workflow run identifier
|
||||
|
||||
Returns:
|
||||
WorkflowRun object if found, None otherwise
|
||||
|
||||
Note:
|
||||
This method bypasses tenant isolation checks and should only be used
|
||||
in trusted system contexts like ops trace collection. For user-facing
|
||||
operations, use get_workflow_run_by_id() with proper tenant isolation.
|
||||
"""
|
||||
...
|
||||
|
||||
def get_workflow_runs_count(
|
||||
self,
|
||||
tenant_id: str,
|
||||
@@ -218,3 +250,119 @@ class APIWorkflowRunRepository(WorkflowExecutionRepository, Protocol):
|
||||
and ensure proper data retention policies are followed.
|
||||
"""
|
||||
...
|
||||
|
||||
def get_daily_runs_statistics(
|
||||
self,
|
||||
tenant_id: str,
|
||||
app_id: str,
|
||||
triggered_from: str,
|
||||
start_date: datetime | None = None,
|
||||
end_date: datetime | None = None,
|
||||
timezone: str = "UTC",
|
||||
) -> list[DailyRunsStats]:
|
||||
"""
|
||||
Get daily runs statistics.
|
||||
|
||||
Retrieves daily workflow runs count grouped by date for a specific app
|
||||
and trigger source. Used for workflow statistics dashboard.
|
||||
|
||||
Args:
|
||||
tenant_id: Tenant identifier for multi-tenant isolation
|
||||
app_id: Application identifier
|
||||
triggered_from: Filter by trigger source (e.g., "app-run")
|
||||
start_date: Optional start date filter
|
||||
end_date: Optional end date filter
|
||||
timezone: Timezone for date grouping (default: "UTC")
|
||||
|
||||
Returns:
|
||||
List of dictionaries containing date and runs count:
|
||||
[{"date": "2024-01-01", "runs": 10}, ...]
|
||||
"""
|
||||
...
|
||||
|
||||
def get_daily_terminals_statistics(
|
||||
self,
|
||||
tenant_id: str,
|
||||
app_id: str,
|
||||
triggered_from: str,
|
||||
start_date: datetime | None = None,
|
||||
end_date: datetime | None = None,
|
||||
timezone: str = "UTC",
|
||||
) -> list[DailyTerminalsStats]:
|
||||
"""
|
||||
Get daily terminals statistics.
|
||||
|
||||
Retrieves daily unique terminal count grouped by date for a specific app
|
||||
and trigger source. Used for workflow statistics dashboard.
|
||||
|
||||
Args:
|
||||
tenant_id: Tenant identifier for multi-tenant isolation
|
||||
app_id: Application identifier
|
||||
triggered_from: Filter by trigger source (e.g., "app-run")
|
||||
start_date: Optional start date filter
|
||||
end_date: Optional end date filter
|
||||
timezone: Timezone for date grouping (default: "UTC")
|
||||
|
||||
Returns:
|
||||
List of dictionaries containing date and terminal count:
|
||||
[{"date": "2024-01-01", "terminal_count": 5}, ...]
|
||||
"""
|
||||
...
|
||||
|
||||
def get_daily_token_cost_statistics(
|
||||
self,
|
||||
tenant_id: str,
|
||||
app_id: str,
|
||||
triggered_from: str,
|
||||
start_date: datetime | None = None,
|
||||
end_date: datetime | None = None,
|
||||
timezone: str = "UTC",
|
||||
) -> list[DailyTokenCostStats]:
|
||||
"""
|
||||
Get daily token cost statistics.
|
||||
|
||||
Retrieves daily total token count grouped by date for a specific app
|
||||
and trigger source. Used for workflow statistics dashboard.
|
||||
|
||||
Args:
|
||||
tenant_id: Tenant identifier for multi-tenant isolation
|
||||
app_id: Application identifier
|
||||
triggered_from: Filter by trigger source (e.g., "app-run")
|
||||
start_date: Optional start date filter
|
||||
end_date: Optional end date filter
|
||||
timezone: Timezone for date grouping (default: "UTC")
|
||||
|
||||
Returns:
|
||||
List of dictionaries containing date and token count:
|
||||
[{"date": "2024-01-01", "token_count": 1000}, ...]
|
||||
"""
|
||||
...
|
||||
|
||||
def get_average_app_interaction_statistics(
|
||||
self,
|
||||
tenant_id: str,
|
||||
app_id: str,
|
||||
triggered_from: str,
|
||||
start_date: datetime | None = None,
|
||||
end_date: datetime | None = None,
|
||||
timezone: str = "UTC",
|
||||
) -> list[AverageInteractionStats]:
|
||||
"""
|
||||
Get average app interaction statistics.
|
||||
|
||||
Retrieves daily average interactions per user grouped by date for a specific app
|
||||
and trigger source. Used for workflow statistics dashboard.
|
||||
|
||||
Args:
|
||||
tenant_id: Tenant identifier for multi-tenant isolation
|
||||
app_id: Application identifier
|
||||
triggered_from: Filter by trigger source (e.g., "app-run")
|
||||
start_date: Optional start date filter
|
||||
end_date: Optional end date filter
|
||||
timezone: Timezone for date grouping (default: "UTC")
|
||||
|
||||
Returns:
|
||||
List of dictionaries containing date and average interactions:
|
||||
[{"date": "2024-01-01", "interactions": 2.5}, ...]
|
||||
"""
|
||||
...
|
||||
|
||||
@@ -22,16 +22,25 @@ Implementation Notes:
|
||||
import logging
|
||||
from collections.abc import Sequence
|
||||
from datetime import datetime
|
||||
from typing import cast
|
||||
from decimal import Decimal
|
||||
from typing import Any, cast
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import delete, func, select
|
||||
from sqlalchemy.engine import CursorResult
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from libs.infinite_scroll_pagination import InfiniteScrollPagination
|
||||
from libs.time_parser import get_time_threshold
|
||||
from models.enums import WorkflowRunTriggeredFrom
|
||||
from models.workflow import WorkflowRun
|
||||
from repositories.api_workflow_run_repository import APIWorkflowRunRepository
|
||||
from repositories.types import (
|
||||
AverageInteractionStats,
|
||||
DailyRunsStats,
|
||||
DailyTerminalsStats,
|
||||
DailyTokenCostStats,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -61,7 +70,7 @@ class DifyAPISQLAlchemyWorkflowRunRepository(APIWorkflowRunRepository):
|
||||
self,
|
||||
tenant_id: str,
|
||||
app_id: str,
|
||||
triggered_from: str,
|
||||
triggered_from: WorkflowRunTriggeredFrom | Sequence[WorkflowRunTriggeredFrom],
|
||||
limit: int = 20,
|
||||
last_id: str | None = None,
|
||||
status: str | None = None,
|
||||
@@ -78,9 +87,14 @@ class DifyAPISQLAlchemyWorkflowRunRepository(APIWorkflowRunRepository):
|
||||
base_stmt = select(WorkflowRun).where(
|
||||
WorkflowRun.tenant_id == tenant_id,
|
||||
WorkflowRun.app_id == app_id,
|
||||
WorkflowRun.triggered_from == triggered_from,
|
||||
)
|
||||
|
||||
# Handle triggered_from values
|
||||
if isinstance(triggered_from, WorkflowRunTriggeredFrom):
|
||||
triggered_from = [triggered_from]
|
||||
if triggered_from:
|
||||
base_stmt = base_stmt.where(WorkflowRun.triggered_from.in_(triggered_from))
|
||||
|
||||
# Add optional status filter
|
||||
if status:
|
||||
base_stmt = base_stmt.where(WorkflowRun.status == status)
|
||||
@@ -126,6 +140,17 @@ class DifyAPISQLAlchemyWorkflowRunRepository(APIWorkflowRunRepository):
|
||||
)
|
||||
return session.scalar(stmt)
|
||||
|
||||
def get_workflow_run_by_id_without_tenant(
|
||||
self,
|
||||
run_id: str,
|
||||
) -> WorkflowRun | None:
|
||||
"""
|
||||
Get a specific workflow run by ID without tenant/app context.
|
||||
"""
|
||||
with self._session_maker() as session:
|
||||
stmt = select(WorkflowRun).where(WorkflowRun.id == run_id)
|
||||
return session.scalar(stmt)
|
||||
|
||||
def get_workflow_runs_count(
|
||||
self,
|
||||
tenant_id: str,
|
||||
@@ -275,3 +300,213 @@ class DifyAPISQLAlchemyWorkflowRunRepository(APIWorkflowRunRepository):
|
||||
|
||||
logger.info("Total deleted %s workflow runs for app %s", total_deleted, app_id)
|
||||
return total_deleted
|
||||
|
||||
def get_daily_runs_statistics(
|
||||
self,
|
||||
tenant_id: str,
|
||||
app_id: str,
|
||||
triggered_from: str,
|
||||
start_date: datetime | None = None,
|
||||
end_date: datetime | None = None,
|
||||
timezone: str = "UTC",
|
||||
) -> list[DailyRunsStats]:
|
||||
"""
|
||||
Get daily runs statistics using raw SQL for optimal performance.
|
||||
"""
|
||||
sql_query = """SELECT
|
||||
DATE(DATE_TRUNC('day', created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz )) AS date,
|
||||
COUNT(id) AS runs
|
||||
FROM
|
||||
workflow_runs
|
||||
WHERE
|
||||
tenant_id = :tenant_id
|
||||
AND app_id = :app_id
|
||||
AND triggered_from = :triggered_from"""
|
||||
|
||||
arg_dict: dict[str, Any] = {
|
||||
"tz": timezone,
|
||||
"tenant_id": tenant_id,
|
||||
"app_id": app_id,
|
||||
"triggered_from": triggered_from,
|
||||
}
|
||||
|
||||
if start_date:
|
||||
sql_query += " AND created_at >= :start_date"
|
||||
arg_dict["start_date"] = start_date
|
||||
|
||||
if end_date:
|
||||
sql_query += " AND created_at < :end_date"
|
||||
arg_dict["end_date"] = end_date
|
||||
|
||||
sql_query += " GROUP BY date ORDER BY date"
|
||||
|
||||
response_data = []
|
||||
with self._session_maker() as session:
|
||||
rs = session.execute(sa.text(sql_query), arg_dict)
|
||||
for row in rs:
|
||||
response_data.append({"date": str(row.date), "runs": row.runs})
|
||||
|
||||
return cast(list[DailyRunsStats], response_data)
|
||||
|
||||
def get_daily_terminals_statistics(
|
||||
self,
|
||||
tenant_id: str,
|
||||
app_id: str,
|
||||
triggered_from: str,
|
||||
start_date: datetime | None = None,
|
||||
end_date: datetime | None = None,
|
||||
timezone: str = "UTC",
|
||||
) -> list[DailyTerminalsStats]:
|
||||
"""
|
||||
Get daily terminals statistics using raw SQL for optimal performance.
|
||||
"""
|
||||
sql_query = """SELECT
|
||||
DATE(DATE_TRUNC('day', created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz )) AS date,
|
||||
COUNT(DISTINCT created_by) AS terminal_count
|
||||
FROM
|
||||
workflow_runs
|
||||
WHERE
|
||||
tenant_id = :tenant_id
|
||||
AND app_id = :app_id
|
||||
AND triggered_from = :triggered_from"""
|
||||
|
||||
arg_dict: dict[str, Any] = {
|
||||
"tz": timezone,
|
||||
"tenant_id": tenant_id,
|
||||
"app_id": app_id,
|
||||
"triggered_from": triggered_from,
|
||||
}
|
||||
|
||||
if start_date:
|
||||
sql_query += " AND created_at >= :start_date"
|
||||
arg_dict["start_date"] = start_date
|
||||
|
||||
if end_date:
|
||||
sql_query += " AND created_at < :end_date"
|
||||
arg_dict["end_date"] = end_date
|
||||
|
||||
sql_query += " GROUP BY date ORDER BY date"
|
||||
|
||||
response_data = []
|
||||
with self._session_maker() as session:
|
||||
rs = session.execute(sa.text(sql_query), arg_dict)
|
||||
for row in rs:
|
||||
response_data.append({"date": str(row.date), "terminal_count": row.terminal_count})
|
||||
|
||||
return cast(list[DailyTerminalsStats], response_data)
|
||||
|
||||
def get_daily_token_cost_statistics(
|
||||
self,
|
||||
tenant_id: str,
|
||||
app_id: str,
|
||||
triggered_from: str,
|
||||
start_date: datetime | None = None,
|
||||
end_date: datetime | None = None,
|
||||
timezone: str = "UTC",
|
||||
) -> list[DailyTokenCostStats]:
|
||||
"""
|
||||
Get daily token cost statistics using raw SQL for optimal performance.
|
||||
"""
|
||||
sql_query = """SELECT
|
||||
DATE(DATE_TRUNC('day', created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz )) AS date,
|
||||
SUM(total_tokens) AS token_count
|
||||
FROM
|
||||
workflow_runs
|
||||
WHERE
|
||||
tenant_id = :tenant_id
|
||||
AND app_id = :app_id
|
||||
AND triggered_from = :triggered_from"""
|
||||
|
||||
arg_dict: dict[str, Any] = {
|
||||
"tz": timezone,
|
||||
"tenant_id": tenant_id,
|
||||
"app_id": app_id,
|
||||
"triggered_from": triggered_from,
|
||||
}
|
||||
|
||||
if start_date:
|
||||
sql_query += " AND created_at >= :start_date"
|
||||
arg_dict["start_date"] = start_date
|
||||
|
||||
if end_date:
|
||||
sql_query += " AND created_at < :end_date"
|
||||
arg_dict["end_date"] = end_date
|
||||
|
||||
sql_query += " GROUP BY date ORDER BY date"
|
||||
|
||||
response_data = []
|
||||
with self._session_maker() as session:
|
||||
rs = session.execute(sa.text(sql_query), arg_dict)
|
||||
for row in rs:
|
||||
response_data.append(
|
||||
{
|
||||
"date": str(row.date),
|
||||
"token_count": row.token_count,
|
||||
}
|
||||
)
|
||||
|
||||
return cast(list[DailyTokenCostStats], response_data)
|
||||
|
||||
def get_average_app_interaction_statistics(
|
||||
self,
|
||||
tenant_id: str,
|
||||
app_id: str,
|
||||
triggered_from: str,
|
||||
start_date: datetime | None = None,
|
||||
end_date: datetime | None = None,
|
||||
timezone: str = "UTC",
|
||||
) -> list[AverageInteractionStats]:
|
||||
"""
|
||||
Get average app interaction statistics using raw SQL for optimal performance.
|
||||
"""
|
||||
sql_query = """SELECT
|
||||
AVG(sub.interactions) AS interactions,
|
||||
sub.date
|
||||
FROM
|
||||
(
|
||||
SELECT
|
||||
DATE(DATE_TRUNC('day', c.created_at AT TIME ZONE 'UTC' AT TIME ZONE :tz )) AS date,
|
||||
c.created_by,
|
||||
COUNT(c.id) AS interactions
|
||||
FROM
|
||||
workflow_runs c
|
||||
WHERE
|
||||
c.tenant_id = :tenant_id
|
||||
AND c.app_id = :app_id
|
||||
AND c.triggered_from = :triggered_from
|
||||
{{start}}
|
||||
{{end}}
|
||||
GROUP BY
|
||||
date, c.created_by
|
||||
) sub
|
||||
GROUP BY
|
||||
sub.date"""
|
||||
|
||||
arg_dict: dict[str, Any] = {
|
||||
"tz": timezone,
|
||||
"tenant_id": tenant_id,
|
||||
"app_id": app_id,
|
||||
"triggered_from": triggered_from,
|
||||
}
|
||||
|
||||
if start_date:
|
||||
sql_query = sql_query.replace("{{start}}", " AND c.created_at >= :start_date")
|
||||
arg_dict["start_date"] = start_date
|
||||
else:
|
||||
sql_query = sql_query.replace("{{start}}", "")
|
||||
|
||||
if end_date:
|
||||
sql_query = sql_query.replace("{{end}}", " AND c.created_at < :end_date")
|
||||
arg_dict["end_date"] = end_date
|
||||
else:
|
||||
sql_query = sql_query.replace("{{end}}", "")
|
||||
|
||||
response_data = []
|
||||
with self._session_maker() as session:
|
||||
rs = session.execute(sa.text(sql_query), arg_dict)
|
||||
for row in rs:
|
||||
response_data.append(
|
||||
{"date": str(row.date), "interactions": float(row.interactions.quantize(Decimal("0.01")))}
|
||||
)
|
||||
|
||||
return cast(list[AverageInteractionStats], response_data)
|
||||
|
||||
21
api/repositories/types.py
Normal file
21
api/repositories/types.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from typing import TypedDict
|
||||
|
||||
|
||||
class DailyRunsStats(TypedDict):
|
||||
date: str
|
||||
runs: int
|
||||
|
||||
|
||||
class DailyTerminalsStats(TypedDict):
|
||||
date: str
|
||||
terminal_count: int
|
||||
|
||||
|
||||
class DailyTokenCostStats(TypedDict):
|
||||
date: str
|
||||
token_count: int
|
||||
|
||||
|
||||
class AverageInteractionStats(TypedDict):
|
||||
date: str
|
||||
interactions: float
|
||||
Reference in New Issue
Block a user