2025-03-06 04:22:01 +08:00
|
|
|
use anyhow::{anyhow, Result};
|
2025-04-02 02:09:11 +08:00
|
|
|
use diesel::{ExpressionMethods, JoinOnDsl, QueryDsl, Queryable};
|
2025-03-06 04:22:01 +08:00
|
|
|
use diesel_async::RunQueryDsl;
|
2025-04-05 04:01:40 +08:00
|
|
|
use futures::future::join;
|
2025-03-25 13:09:31 +08:00
|
|
|
use middleware::AuthenticatedUser;
|
2025-03-06 04:22:01 +08:00
|
|
|
use serde_yaml;
|
|
|
|
use uuid::Uuid;
|
|
|
|
|
2025-04-05 04:01:40 +08:00
|
|
|
use crate::metrics::types::{AssociatedCollection, AssociatedDashboard, BusterMetric, Dataset};
|
2025-04-02 02:09:11 +08:00
|
|
|
use database::enums::{AssetPermissionRole, AssetType, IdentityType};
|
2025-03-25 13:09:31 +08:00
|
|
|
use database::helpers::metric_files::fetch_metric_file_with_permissions;
|
2025-03-06 04:22:01 +08:00
|
|
|
use database::pool::get_pg_pool;
|
2025-04-02 03:25:00 +08:00
|
|
|
use database::schema::{
|
2025-04-04 02:24:06 +08:00
|
|
|
asset_permissions, collections, collections_to_assets, dashboard_files, datasets,
|
|
|
|
metric_files_to_dashboard_files, users,
|
2025-04-02 03:25:00 +08:00
|
|
|
};
|
2025-03-25 13:09:31 +08:00
|
|
|
use sharing::check_permission_access;
|
2025-03-06 04:22:01 +08:00
|
|
|
|
2025-03-12 06:06:07 +08:00
|
|
|
use super::Version;
|
|
|
|
|
2025-03-06 04:22:01 +08:00
|
|
|
#[derive(Queryable)]
|
|
|
|
struct DatasetInfo {
|
|
|
|
id: Uuid,
|
|
|
|
name: String,
|
2025-04-02 02:09:11 +08:00
|
|
|
data_source_id: Uuid,
|
2025-03-06 04:22:01 +08:00
|
|
|
}
|
|
|
|
|
2025-03-20 06:56:54 +08:00
|
|
|
#[derive(Queryable)]
|
|
|
|
struct AssetPermissionInfo {
|
|
|
|
role: AssetPermissionRole,
|
|
|
|
email: String,
|
|
|
|
name: Option<String>,
|
|
|
|
}
|
|
|
|
|
2025-04-02 03:25:00 +08:00
|
|
|
/// Fetch the dashboards associated with the given metric id
|
2025-04-04 02:24:06 +08:00
|
|
|
async fn fetch_associated_dashboards_for_metric(
|
|
|
|
metric_id: Uuid,
|
|
|
|
) -> Result<Vec<AssociatedDashboard>> {
|
2025-04-02 03:25:00 +08:00
|
|
|
let mut conn = get_pg_pool().get().await?;
|
|
|
|
let associated_dashboards = metric_files_to_dashboard_files::table
|
2025-04-04 02:24:06 +08:00
|
|
|
.inner_join(
|
|
|
|
dashboard_files::table
|
|
|
|
.on(dashboard_files::id.eq(metric_files_to_dashboard_files::dashboard_file_id)),
|
|
|
|
)
|
2025-04-02 03:25:00 +08:00
|
|
|
.filter(metric_files_to_dashboard_files::metric_file_id.eq(metric_id))
|
|
|
|
.filter(dashboard_files::deleted_at.is_null())
|
|
|
|
.filter(metric_files_to_dashboard_files::deleted_at.is_null())
|
|
|
|
.select((dashboard_files::id, dashboard_files::name))
|
|
|
|
.load::<(Uuid, String)>(&mut conn)
|
|
|
|
.await?
|
|
|
|
.into_iter()
|
2025-04-04 02:24:06 +08:00
|
|
|
.map(|(id, name)| AssociatedDashboard { id, name })
|
2025-04-02 03:25:00 +08:00
|
|
|
.collect();
|
|
|
|
Ok(associated_dashboards)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Fetch the collections associated with the given metric id
|
2025-04-04 02:24:06 +08:00
|
|
|
async fn fetch_associated_collections_for_metric(
|
|
|
|
metric_id: Uuid,
|
|
|
|
) -> Result<Vec<AssociatedCollection>> {
|
2025-04-02 03:25:00 +08:00
|
|
|
let mut conn = get_pg_pool().get().await?;
|
|
|
|
let associated_collections = collections_to_assets::table
|
|
|
|
.inner_join(collections::table.on(collections::id.eq(collections_to_assets::collection_id)))
|
|
|
|
.filter(collections_to_assets::asset_id.eq(metric_id))
|
|
|
|
.filter(collections_to_assets::asset_type.eq(AssetType::MetricFile))
|
|
|
|
.filter(collections::deleted_at.is_null())
|
|
|
|
.filter(collections_to_assets::deleted_at.is_null())
|
|
|
|
.select((collections::id, collections::name))
|
|
|
|
.load::<(Uuid, String)>(&mut conn)
|
|
|
|
.await?
|
|
|
|
.into_iter()
|
2025-04-04 02:24:06 +08:00
|
|
|
.map(|(id, name)| AssociatedCollection { id, name })
|
2025-04-02 03:25:00 +08:00
|
|
|
.collect();
|
|
|
|
Ok(associated_collections)
|
|
|
|
}
|
|
|
|
|
2025-03-20 00:49:56 +08:00
|
|
|
/// Handler to retrieve a metric by ID with optional version number
|
2025-03-20 06:56:54 +08:00
|
|
|
///
|
2025-03-20 00:49:56 +08:00
|
|
|
/// If version_number is provided, returns that specific version of the metric.
|
|
|
|
/// If version_number is None, returns the latest version of the metric.
|
2025-03-20 06:56:54 +08:00
|
|
|
pub async fn get_metric_handler(
|
|
|
|
metric_id: &Uuid,
|
2025-03-25 13:09:31 +08:00
|
|
|
user: &AuthenticatedUser,
|
2025-03-20 06:56:54 +08:00
|
|
|
version_number: Option<i32>,
|
|
|
|
) -> Result<BusterMetric> {
|
2025-03-25 13:09:31 +08:00
|
|
|
// 1. Fetch metric file with permission
|
|
|
|
let metric_file_with_permission = fetch_metric_file_with_permissions(metric_id, &user.id)
|
|
|
|
.await
|
|
|
|
.map_err(|e| anyhow!("Failed to fetch metric file with permissions: {}", e))?;
|
|
|
|
|
2025-04-08 06:47:00 +08:00
|
|
|
let metric_file_with_permission = if let Some(metric_file) = metric_file_with_permission {
|
2025-03-25 13:09:31 +08:00
|
|
|
metric_file
|
|
|
|
} else {
|
|
|
|
return Err(anyhow!("Metric file not found"));
|
2025-03-06 04:22:01 +08:00
|
|
|
};
|
|
|
|
|
2025-04-08 06:47:00 +08:00
|
|
|
// 2. Check if user has at least CanView permission
|
2025-03-25 13:09:31 +08:00
|
|
|
if !check_permission_access(
|
2025-04-08 06:47:00 +08:00
|
|
|
metric_file_with_permission.permission,
|
2025-04-04 02:24:06 +08:00
|
|
|
&[
|
|
|
|
AssetPermissionRole::FullAccess,
|
|
|
|
AssetPermissionRole::Owner,
|
2025-04-05 04:01:40 +08:00
|
|
|
AssetPermissionRole::CanEdit,
|
|
|
|
AssetPermissionRole::CanView,
|
2025-04-04 02:24:06 +08:00
|
|
|
],
|
2025-04-08 06:47:00 +08:00
|
|
|
metric_file_with_permission.metric_file.organization_id,
|
2025-03-25 13:09:31 +08:00
|
|
|
&user.organizations,
|
|
|
|
) {
|
|
|
|
return Err(anyhow!("You don't have permission to view this metric"));
|
|
|
|
}
|
|
|
|
|
2025-04-08 06:47:00 +08:00
|
|
|
// 3. Extract permission for consistent use in response
|
|
|
|
// If the asset is public and the user has no direct permission, default to CanView
|
|
|
|
let permission = metric_file_with_permission.permission
|
|
|
|
.unwrap_or(AssetPermissionRole::CanView);
|
2025-03-25 13:09:31 +08:00
|
|
|
|
2025-04-08 06:47:00 +08:00
|
|
|
let metric_file = metric_file_with_permission.metric_file;
|
2025-03-06 04:22:01 +08:00
|
|
|
|
|
|
|
// Map evaluation score to High/Moderate/Low
|
|
|
|
let evaluation_score = metric_file.evaluation_score.map(|score| {
|
|
|
|
if score >= 0.8 {
|
|
|
|
"High".to_string()
|
|
|
|
} else if score >= 0.5 {
|
|
|
|
"Moderate".to_string()
|
|
|
|
} else {
|
|
|
|
"Low".to_string()
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2025-03-20 00:49:56 +08:00
|
|
|
// Determine which version to use based on version_number parameter
|
|
|
|
let (metric_content, version_num) = if let Some(version) = version_number {
|
|
|
|
// Get the specific version if it exists
|
|
|
|
if let Some(v) = metric_file.version_history.get_version(version) {
|
|
|
|
match &v.content {
|
2025-03-20 06:56:54 +08:00
|
|
|
database::types::VersionContent::MetricYml(content) => {
|
|
|
|
(content.clone(), v.version_number)
|
|
|
|
}
|
|
|
|
_ => return Err(anyhow!("Invalid version content type")),
|
2025-03-20 00:49:56 +08:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return Err(anyhow!("Version {} not found", version));
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Get the latest version
|
|
|
|
if let Some(v) = metric_file.version_history.get_latest_version() {
|
|
|
|
match &v.content {
|
2025-03-20 06:56:54 +08:00
|
|
|
database::types::VersionContent::MetricYml(content) => {
|
|
|
|
(content.clone(), v.version_number)
|
|
|
|
}
|
|
|
|
_ => return Err(anyhow!("Invalid version content type")),
|
2025-03-20 00:49:56 +08:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Fall back to current content if no version history
|
2025-03-22 01:15:45 +08:00
|
|
|
(Box::new(metric_file.content.clone()), 1)
|
2025-03-20 00:49:56 +08:00
|
|
|
}
|
|
|
|
};
|
2025-03-12 11:25:27 +08:00
|
|
|
|
2025-03-06 04:22:01 +08:00
|
|
|
// Convert content to pretty YAML
|
2025-03-20 00:49:56 +08:00
|
|
|
let file = match serde_yaml::to_string(&metric_content) {
|
2025-03-06 04:22:01 +08:00
|
|
|
Ok(yaml) => yaml,
|
|
|
|
Err(e) => return Err(anyhow!("Failed to convert content to YAML: {}", e)),
|
|
|
|
};
|
|
|
|
|
2025-04-04 02:24:06 +08:00
|
|
|
// Data metadata is fetched directly from the metric_file database record
|
|
|
|
let data_metadata = metric_file.data_metadata;
|
2025-03-25 13:09:31 +08:00
|
|
|
|
2025-04-04 02:24:06 +08:00
|
|
|
let mut conn = get_pg_pool().get().await?;
|
2025-03-06 04:22:01 +08:00
|
|
|
|
|
|
|
// Get dataset information for all dataset IDs
|
|
|
|
let mut datasets = Vec::new();
|
2025-04-02 02:09:11 +08:00
|
|
|
let mut first_data_source_id = None;
|
2025-03-20 00:49:56 +08:00
|
|
|
for dataset_id in &metric_content.dataset_ids {
|
2025-03-06 04:22:01 +08:00
|
|
|
if let Ok(dataset_info) = datasets::table
|
|
|
|
.filter(datasets::id.eq(dataset_id))
|
|
|
|
.filter(datasets::deleted_at.is_null())
|
2025-04-02 02:09:11 +08:00
|
|
|
.select((datasets::id, datasets::name, datasets::data_source_id))
|
2025-03-06 04:22:01 +08:00
|
|
|
.first::<DatasetInfo>(&mut conn)
|
|
|
|
.await
|
|
|
|
{
|
|
|
|
datasets.push(Dataset {
|
|
|
|
id: dataset_info.id.to_string(),
|
|
|
|
name: dataset_info.name,
|
|
|
|
});
|
2025-04-02 02:09:11 +08:00
|
|
|
first_data_source_id = Some(dataset_info.data_source_id);
|
2025-03-06 04:22:01 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get user information
|
2025-03-10 23:41:32 +08:00
|
|
|
// let user_info = users::table
|
|
|
|
// .filter(users::id.eq(metric_file.created_by))
|
|
|
|
// .select((users::name, users::avatar_url))
|
|
|
|
// .first::<UserInfo>(&mut conn)
|
|
|
|
// .await
|
|
|
|
// .map_err(|e| anyhow!("Failed to get user information: {}", e))?;
|
2025-03-06 04:22:01 +08:00
|
|
|
|
2025-03-20 03:24:13 +08:00
|
|
|
let mut versions: Vec<Version> = metric_file
|
2025-03-12 06:06:07 +08:00
|
|
|
.version_history
|
|
|
|
.0
|
|
|
|
.values()
|
|
|
|
.map(|v| Version {
|
|
|
|
version_number: v.version_number,
|
|
|
|
updated_at: v.updated_at,
|
|
|
|
})
|
|
|
|
.collect();
|
2025-03-20 06:56:54 +08:00
|
|
|
|
2025-03-20 03:24:13 +08:00
|
|
|
// Sort versions by version_number in ascending order
|
|
|
|
versions.sort_by(|a, b| a.version_number.cmp(&b.version_number));
|
2025-03-20 06:56:54 +08:00
|
|
|
|
|
|
|
// Query individual permissions for this metric
|
|
|
|
let individual_permissions_query = asset_permissions::table
|
|
|
|
.inner_join(users::table.on(users::id.eq(asset_permissions::identity_id)))
|
|
|
|
.filter(asset_permissions::asset_id.eq(metric_id))
|
|
|
|
.filter(asset_permissions::asset_type.eq(AssetType::MetricFile))
|
|
|
|
.filter(asset_permissions::identity_type.eq(IdentityType::User))
|
|
|
|
.filter(asset_permissions::deleted_at.is_null())
|
2025-03-25 13:09:31 +08:00
|
|
|
.select((asset_permissions::role, users::email, users::name))
|
2025-03-20 06:56:54 +08:00
|
|
|
.load::<AssetPermissionInfo>(&mut conn)
|
|
|
|
.await;
|
|
|
|
|
|
|
|
// Get the user info for publicly_enabled_by if it exists
|
|
|
|
let public_enabled_by_user = if let Some(enabled_by_id) = metric_file.publicly_enabled_by {
|
|
|
|
users::table
|
|
|
|
.filter(users::id.eq(enabled_by_id))
|
|
|
|
.select(users::email)
|
|
|
|
.first::<String>(&mut conn)
|
|
|
|
.await
|
|
|
|
.ok()
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
};
|
|
|
|
|
2025-04-02 03:25:00 +08:00
|
|
|
// Concurrently fetch associated dashboards and collections
|
|
|
|
let metrics_id_clone = *metric_id;
|
|
|
|
let dashboards_future = fetch_associated_dashboards_for_metric(metrics_id_clone);
|
|
|
|
let collections_future = fetch_associated_collections_for_metric(metrics_id_clone);
|
2025-04-04 02:24:06 +08:00
|
|
|
|
2025-04-02 03:25:00 +08:00
|
|
|
// Await both futures concurrently
|
|
|
|
let (dashboards_result, collections_result) = join(dashboards_future, collections_future).await;
|
2025-04-04 02:24:06 +08:00
|
|
|
|
2025-04-02 03:25:00 +08:00
|
|
|
// Handle results, logging errors but returning empty Vecs for failed tasks
|
|
|
|
let dashboards = match dashboards_result {
|
|
|
|
Ok(dashboards) => dashboards,
|
|
|
|
Err(e) => {
|
2025-04-04 02:24:06 +08:00
|
|
|
tracing::error!(
|
|
|
|
"Failed to fetch associated dashboards for metric {}: {}",
|
|
|
|
metric_id,
|
|
|
|
e
|
|
|
|
);
|
2025-04-02 03:25:00 +08:00
|
|
|
vec![]
|
|
|
|
}
|
|
|
|
};
|
2025-04-04 02:24:06 +08:00
|
|
|
|
2025-04-02 03:25:00 +08:00
|
|
|
let collections = match collections_result {
|
|
|
|
Ok(collections) => collections,
|
|
|
|
Err(e) => {
|
2025-04-04 02:24:06 +08:00
|
|
|
tracing::error!(
|
|
|
|
"Failed to fetch associated collections for metric {}: {}",
|
|
|
|
metric_id,
|
|
|
|
e
|
|
|
|
);
|
2025-04-02 03:25:00 +08:00
|
|
|
vec![]
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2025-03-20 06:56:54 +08:00
|
|
|
// Convert AssetPermissionInfo to BusterShareIndividual
|
|
|
|
let individual_permissions = match individual_permissions_query {
|
|
|
|
Ok(permissions) => {
|
|
|
|
if permissions.is_empty() {
|
|
|
|
None
|
|
|
|
} else {
|
|
|
|
Some(
|
|
|
|
permissions
|
|
|
|
.into_iter()
|
|
|
|
.map(|p| crate::metrics::types::BusterShareIndividual {
|
|
|
|
email: p.email,
|
|
|
|
role: p.role,
|
|
|
|
name: p.name,
|
|
|
|
})
|
|
|
|
.collect::<Vec<crate::metrics::types::BusterShareIndividual>>(),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Err(_) => None,
|
|
|
|
};
|
|
|
|
|
2025-03-06 04:22:01 +08:00
|
|
|
// Construct BusterMetric
|
|
|
|
Ok(BusterMetric {
|
2025-03-12 09:30:46 +08:00
|
|
|
id: metric_file.id,
|
2025-03-06 04:22:01 +08:00
|
|
|
metric_type: "metric".to_string(),
|
2025-03-24 23:46:46 +08:00
|
|
|
name: metric_file.name,
|
2025-03-20 00:49:56 +08:00
|
|
|
version_number: version_num,
|
|
|
|
description: metric_content.description,
|
2025-03-06 04:22:01 +08:00
|
|
|
file_name: metric_file.file_name,
|
2025-03-20 00:49:56 +08:00
|
|
|
time_frame: metric_content.time_frame,
|
2025-03-06 04:22:01 +08:00
|
|
|
datasets,
|
2025-04-02 02:09:11 +08:00
|
|
|
data_source_id: first_data_source_id.map_or("".to_string(), |id| id.to_string()),
|
2025-03-06 04:22:01 +08:00
|
|
|
error: None,
|
2025-03-20 00:49:56 +08:00
|
|
|
chart_config: Some(metric_content.chart_config),
|
2025-03-06 04:22:01 +08:00
|
|
|
data_metadata,
|
|
|
|
status: metric_file.verification,
|
|
|
|
evaluation_score,
|
|
|
|
evaluation_summary: metric_file.evaluation_summary.unwrap_or_default(),
|
|
|
|
file,
|
2025-03-12 09:30:46 +08:00
|
|
|
created_at: metric_file.created_at,
|
|
|
|
updated_at: metric_file.updated_at,
|
|
|
|
sent_by_id: metric_file.created_by,
|
2025-03-10 23:41:32 +08:00
|
|
|
sent_by_name: "".to_string(),
|
|
|
|
sent_by_avatar_url: None,
|
2025-03-06 04:22:01 +08:00
|
|
|
code: None,
|
2025-04-02 03:25:00 +08:00
|
|
|
dashboards,
|
|
|
|
collections,
|
2025-03-12 06:06:07 +08:00
|
|
|
versions,
|
2025-03-25 13:09:31 +08:00
|
|
|
permission,
|
2025-03-20 03:23:00 +08:00
|
|
|
sql: metric_content.sql,
|
2025-03-20 06:56:54 +08:00
|
|
|
individual_permissions,
|
|
|
|
publicly_accessible: metric_file.publicly_accessible,
|
|
|
|
public_expiry_date: metric_file.public_expiry_date,
|
|
|
|
public_enabled_by: public_enabled_by_user,
|
|
|
|
public_password: None, // Currently not stored in the database
|
2025-03-06 04:22:01 +08:00
|
|
|
})
|
|
|
|
}
|