mirror of https://github.com/buster-so/buster.git
Merge pull request #1009 from buster-so/wells-bus-1841-migrate-get-dashboard-endpoint-to-v2
cleanup duplicate code to shared-helpers for metric_files
This commit is contained in:
commit
0aaa53c685
|
@ -1,14 +1,10 @@
|
|||
import { checkPermission } from '@buster/access-controls';
|
||||
import {
|
||||
type User,
|
||||
getAssetsAssociatedWithMetric,
|
||||
getCollectionsAssociatedWithDashboard,
|
||||
getDashboardById,
|
||||
getMetricFileById,
|
||||
getOrganizationMemberCount,
|
||||
getUser,
|
||||
getUsersWithDashboardPermissions,
|
||||
getUsersWithMetricPermissions,
|
||||
} from '@buster/database/queries';
|
||||
import {
|
||||
GetDashboardParamsSchema,
|
||||
|
@ -16,18 +12,17 @@ import {
|
|||
type GetDashboardResponse,
|
||||
} from '@buster/server-shared/dashboards';
|
||||
import type { DashboardYml } from '@buster/server-shared/dashboards';
|
||||
import {
|
||||
DEFAULT_CHART_THEME,
|
||||
type DataMetadata,
|
||||
type GetMetricResponse,
|
||||
type Metric,
|
||||
type MetricYml,
|
||||
} from '@buster/server-shared/metrics';
|
||||
import type { Metric } from '@buster/server-shared/metrics';
|
||||
import type { VerificationStatus } from '@buster/server-shared/share';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { Hono } from 'hono';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
import yaml from 'js-yaml';
|
||||
import {
|
||||
buildMetricResponse,
|
||||
fetchAndProcessMetricData,
|
||||
getPubliclyEnabledByUser,
|
||||
} from '../../../../shared-helpers/metric-helpers';
|
||||
|
||||
interface GetDashboardHandlerParams {
|
||||
dashboardId: string;
|
||||
|
@ -201,7 +196,7 @@ async function getDashboardHandler(
|
|||
// Extract metric IDs from dashboard config
|
||||
const metricIds = extractMetricIds(resolvedContent);
|
||||
|
||||
const metrics = await getMetricsFromDashboardMetricIds(metricIds, user.id);
|
||||
const metrics = await getMetricsFromDashboardMetricIds(metricIds, user);
|
||||
|
||||
// Get the extra dashboard info concurrently
|
||||
const [individualPermissions, workspaceMemberCount, collections, publicEnabledBy] =
|
||||
|
@ -266,140 +261,31 @@ function extractMetricIds(content: DashboardYml): string[] {
|
|||
}
|
||||
}
|
||||
|
||||
async function getPubliclyEnabledByUser(enabledById: string | null): Promise<string | null> {
|
||||
if (enabledById) {
|
||||
const publicEnabledByUser = await getUser({ id: enabledById });
|
||||
return publicEnabledByUser.email;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function getMetricsFromDashboardMetricIds(
|
||||
metricIds: string[],
|
||||
userId: string
|
||||
user: User
|
||||
): Promise<Record<string, Metric>> {
|
||||
const metricsObj: Record<string, Metric> = {};
|
||||
const promises: Promise<Metric>[] = metricIds.map((metricId) =>
|
||||
getMetricFileForDashboard(metricId, userId)
|
||||
);
|
||||
const metrics = await Promise.all(promises);
|
||||
for (const metric of metrics) {
|
||||
metricsObj[metric.id] = metric;
|
||||
|
||||
// Process all metrics concurrently
|
||||
const promises = metricIds.map(async (metricId) => {
|
||||
const processedData = await fetchAndProcessMetricData(metricId, user, {
|
||||
publicAccessPreviouslyVerified: true, // Access is inherited from dashboard access at a minimum
|
||||
});
|
||||
|
||||
// Build the metric response
|
||||
const metric = await buildMetricResponse(processedData, user.id);
|
||||
return { metricId, metric };
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
// Filter out failed metrics and build the response object
|
||||
for (const result of results) {
|
||||
if (result) {
|
||||
metricsObj[result.metricId] = result.metric;
|
||||
}
|
||||
}
|
||||
|
||||
return metricsObj;
|
||||
}
|
||||
|
||||
async function getMetricFileForDashboard(metricId: string, userId: string): Promise<Metric> {
|
||||
// Fetch metric details with permissions
|
||||
const metricFile = await getMetricFileById(metricId);
|
||||
|
||||
if (!metricFile) {
|
||||
console.warn(`Metric file not found: ${metricId}`);
|
||||
throw new HTTPException(404, {
|
||||
message: 'Metric file not found',
|
||||
});
|
||||
}
|
||||
|
||||
let { effectiveRole } = await checkPermission({
|
||||
userId: userId,
|
||||
assetId: metricId,
|
||||
assetType: 'metric_file',
|
||||
requiredRole: 'can_view',
|
||||
organizationId: metricFile.organizationId,
|
||||
workspaceSharing: metricFile.workspaceSharing || 'none',
|
||||
});
|
||||
|
||||
// If user has no access, grant can_view because we have already checked dashboard public access
|
||||
effectiveRole = effectiveRole || 'can_view';
|
||||
|
||||
// Parse version history
|
||||
const versionHistory = metricFile.versionHistory;
|
||||
const versions: Array<{ version_number: number; updated_at: string }> = [];
|
||||
|
||||
Object.values(versionHistory).forEach((version) => {
|
||||
versions.push({
|
||||
version_number: version.version_number,
|
||||
updated_at: version.updated_at,
|
||||
});
|
||||
});
|
||||
versions.sort((a, b) => a.version_number - b.version_number);
|
||||
|
||||
// Use current/latest version
|
||||
const content = metricFile.content as MetricYml;
|
||||
const description = content.description;
|
||||
const timeFrame = content.timeFrame;
|
||||
const chartConfig = content.chartConfig;
|
||||
const sql = content.sql;
|
||||
const updatedAt = metricFile.updatedAt;
|
||||
const versionNum = Math.max(...versions.map((v) => v.version_number), 1);
|
||||
|
||||
// Color fallback was apart of v1 api logic
|
||||
if (!chartConfig.colors) {
|
||||
chartConfig.colors = DEFAULT_CHART_THEME;
|
||||
}
|
||||
|
||||
const fileYaml = yaml.dump(content);
|
||||
|
||||
// Get the extra metric info concurrently
|
||||
const [individualPermissions, workspaceMemberCount, associatedAssets, publicEnabledBy] =
|
||||
await Promise.all([
|
||||
getUsersWithMetricPermissions({ metricId }),
|
||||
getOrganizationMemberCount(metricFile.organizationId),
|
||||
getAssetsAssociatedWithMetric(metricFile.id, userId),
|
||||
getPubliclyEnabledByUser(metricFile.publiclyEnabledBy),
|
||||
]);
|
||||
|
||||
const { dashboards, collections } = associatedAssets;
|
||||
|
||||
// Not used but still exists in frontend code so including it here
|
||||
const evaluationScore = (() => {
|
||||
if (!metricFile.evaluationScore) {
|
||||
return 'Low';
|
||||
}
|
||||
if (metricFile.evaluationScore > 0.8) {
|
||||
return 'High';
|
||||
}
|
||||
if (metricFile.evaluationScore > 0.5) {
|
||||
return 'Moderate';
|
||||
}
|
||||
return 'Low';
|
||||
})();
|
||||
|
||||
// Build the response
|
||||
const response: GetMetricResponse = {
|
||||
id: metricFile.id,
|
||||
type: 'metric_file',
|
||||
name: metricFile.name,
|
||||
version_number: versionNum,
|
||||
error: null,
|
||||
description,
|
||||
file_name: metricFile.fileName,
|
||||
time_frame: timeFrame,
|
||||
data_source_id: metricFile.dataSourceId,
|
||||
chart_config: chartConfig,
|
||||
data_metadata: metricFile.dataMetadata as DataMetadata,
|
||||
status: metricFile.verification as VerificationStatus,
|
||||
file: fileYaml,
|
||||
created_at: metricFile.createdAt,
|
||||
updated_at: updatedAt,
|
||||
sent_by_id: metricFile.createdBy,
|
||||
sent_by_name: '',
|
||||
sent_by_avatar_url: null,
|
||||
sql,
|
||||
dashboards,
|
||||
collections,
|
||||
versions,
|
||||
evaluation_score: evaluationScore,
|
||||
evaluation_summary: metricFile.evaluationSummary || '',
|
||||
permission: effectiveRole,
|
||||
individual_permissions: individualPermissions,
|
||||
publicly_accessible: metricFile.publiclyAccessible,
|
||||
public_expiry_date: metricFile.publicExpiryDate,
|
||||
public_enabled_by: publicEnabledBy,
|
||||
public_password: metricFile.publicPassword,
|
||||
workspace_sharing: metricFile.workspaceSharing,
|
||||
workspace_member_count: workspaceMemberCount,
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
|
|
@ -1,24 +1,15 @@
|
|||
import { checkPermission } from '@buster/access-controls';
|
||||
import type { User } from '@buster/database/queries';
|
||||
import {
|
||||
type User,
|
||||
getAssetsAssociatedWithMetric,
|
||||
getMetricFileById,
|
||||
getOrganizationMemberCount,
|
||||
getUser,
|
||||
getUsersWithMetricPermissions,
|
||||
} from '@buster/database/queries';
|
||||
import {
|
||||
DEFAULT_CHART_THEME,
|
||||
GetMetricParamsSchema,
|
||||
GetMetricQuerySchema,
|
||||
type GetMetricResponse,
|
||||
} from '@buster/server-shared/metrics';
|
||||
import type { ChartConfigProps, DataMetadata, MetricYml } from '@buster/server-shared/metrics';
|
||||
import type { VerificationStatus } from '@buster/server-shared/share';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { Hono } from 'hono';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
import yaml from 'js-yaml';
|
||||
import {
|
||||
buildMetricResponse,
|
||||
fetchAndProcessMetricData,
|
||||
} from '../../../../shared-helpers/metric-helpers';
|
||||
import { standardErrorHandler } from '../../../../utils/response';
|
||||
|
||||
interface GetMetricHandlerParams {
|
||||
|
@ -67,212 +58,13 @@ async function getMetricHandler(
|
|||
): Promise<GetMetricResponse> {
|
||||
const { metricId, versionNumber, password } = params;
|
||||
|
||||
// Fetch metric details with permissions
|
||||
const metricFile = await getMetricFileById(metricId);
|
||||
|
||||
if (!metricFile) {
|
||||
console.warn(`Metric file not found: ${metricId}`);
|
||||
throw new HTTPException(404, {
|
||||
message: 'Metric file not found',
|
||||
});
|
||||
}
|
||||
|
||||
let { hasAccess, effectiveRole } = await checkPermission({
|
||||
userId: user.id,
|
||||
assetId: metricId,
|
||||
assetType: 'metric_file',
|
||||
requiredRole: 'can_view',
|
||||
organizationId: metricFile.organizationId,
|
||||
workspaceSharing: metricFile.workspaceSharing || 'none',
|
||||
// Use shared helper to fetch and process metric data
|
||||
const processedData = await fetchAndProcessMetricData(metricId, user, {
|
||||
publicAccessPreviouslyVerified: false,
|
||||
password,
|
||||
versionNumber,
|
||||
});
|
||||
|
||||
// Check public access
|
||||
if (!hasAccess) {
|
||||
if (!metricFile.publiclyAccessible) {
|
||||
console.warn(`Permission denied for user ${user.id} to metric ${metricId}`);
|
||||
throw new HTTPException(403, {
|
||||
message: "You don't have permission to view this metric",
|
||||
});
|
||||
}
|
||||
|
||||
// Check if public access has expired
|
||||
const today = new Date();
|
||||
if (metricFile.publicExpiryDate && new Date(metricFile.publicExpiryDate) < today) {
|
||||
console.warn(`Public access expired for metric ${metricId}`);
|
||||
throw new HTTPException(403, {
|
||||
message: 'Public access to this metric has expired',
|
||||
});
|
||||
}
|
||||
|
||||
// Check password if required
|
||||
if (metricFile.publicPassword) {
|
||||
if (!password) {
|
||||
console.warn(`Public password required for metric ${metricId}`);
|
||||
throw new HTTPException(418, {
|
||||
message: 'Password required for public access',
|
||||
});
|
||||
}
|
||||
|
||||
if (password !== metricFile.publicPassword) {
|
||||
console.warn(`Incorrect public password for metric ${metricId}`);
|
||||
throw new HTTPException(403, {
|
||||
message: 'Incorrect password for public access',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
hasAccess = true;
|
||||
effectiveRole = 'can_view';
|
||||
}
|
||||
|
||||
if (!hasAccess || !effectiveRole) {
|
||||
// This should never be hit because we have already checked for public access
|
||||
console.warn(`Permission denied for user ${user.id} to metric ${metricId}`);
|
||||
throw new HTTPException(403, {
|
||||
message: "You don't have permission to view this metric",
|
||||
});
|
||||
}
|
||||
|
||||
// Resolve version-specific data
|
||||
let resolvedContent: MetricYml;
|
||||
let resolvedName: string;
|
||||
let resolvedDescription: string | null;
|
||||
let resolvedTimeFrame: string;
|
||||
let resolvedChartConfig: ChartConfigProps;
|
||||
let resolvedSql: string;
|
||||
let resolvedUpdatedAt: string;
|
||||
let resolvedVersionNum: number;
|
||||
|
||||
// Parse version history
|
||||
const versionHistory = metricFile.versionHistory;
|
||||
const versions: Array<{ version_number: number; updated_at: string }> = [];
|
||||
|
||||
Object.values(versionHistory).forEach((version) => {
|
||||
versions.push({
|
||||
version_number: version.version_number,
|
||||
updated_at: version.updated_at,
|
||||
});
|
||||
});
|
||||
versions.sort((a, b) => a.version_number - b.version_number);
|
||||
|
||||
// Get requested version if included in the request
|
||||
if (versionNumber) {
|
||||
const requestedVersionNumToString = versionNumber.toString();
|
||||
|
||||
if (versionHistory[requestedVersionNumToString]) {
|
||||
const version = versionHistory[requestedVersionNumToString];
|
||||
|
||||
if (!version || !version.content) {
|
||||
throw new HTTPException(404, {
|
||||
message: `Version ${versionNumber} not found`,
|
||||
});
|
||||
}
|
||||
|
||||
const versionContent = version.content as MetricYml;
|
||||
resolvedContent = versionContent;
|
||||
resolvedName = versionContent.name;
|
||||
resolvedDescription = versionContent.description;
|
||||
resolvedTimeFrame = versionContent.timeFrame;
|
||||
resolvedChartConfig = versionContent.chartConfig;
|
||||
resolvedSql = versionContent.sql;
|
||||
resolvedUpdatedAt = version.updated_at;
|
||||
resolvedVersionNum = version.version_number;
|
||||
} else {
|
||||
throw new HTTPException(404, {
|
||||
message: `Version ${versionNumber} not found`,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Use current/latest version
|
||||
const currentContent = metricFile.content as MetricYml;
|
||||
resolvedContent = currentContent;
|
||||
resolvedName = metricFile.name;
|
||||
resolvedDescription = currentContent.description;
|
||||
resolvedTimeFrame = currentContent.timeFrame;
|
||||
resolvedChartConfig = currentContent.chartConfig;
|
||||
resolvedSql = currentContent.sql;
|
||||
resolvedUpdatedAt = metricFile.updatedAt;
|
||||
|
||||
// Determine latest version number
|
||||
const maxVersion = Math.max(...versions.map((v) => v.version_number), 1);
|
||||
resolvedVersionNum = maxVersion;
|
||||
}
|
||||
|
||||
// Color fallback was apart of v1 api logic
|
||||
if (!resolvedChartConfig.colors) {
|
||||
resolvedChartConfig.colors = DEFAULT_CHART_THEME;
|
||||
}
|
||||
|
||||
const fileYaml = yaml.dump(resolvedContent);
|
||||
|
||||
// Get the extra metric info concurrently
|
||||
const [individualPermissions, workspaceMemberCount, associatedAssets, publicEnabledBy] =
|
||||
await Promise.all([
|
||||
getUsersWithMetricPermissions({ metricId }),
|
||||
getOrganizationMemberCount(metricFile.organizationId),
|
||||
getAssetsAssociatedWithMetric(metricFile.id, user.id),
|
||||
getPubliclyEnabledByUser(metricFile.publiclyEnabledBy),
|
||||
]);
|
||||
|
||||
const { dashboards, collections } = associatedAssets;
|
||||
|
||||
// Not used but still exists in frontend code so including it here
|
||||
const evaluationScore = (() => {
|
||||
if (!metricFile.evaluationScore) {
|
||||
return 'Low';
|
||||
}
|
||||
if (metricFile.evaluationScore > 0.8) {
|
||||
return 'High';
|
||||
}
|
||||
if (metricFile.evaluationScore > 0.5) {
|
||||
return 'Moderate';
|
||||
}
|
||||
return 'Low';
|
||||
})();
|
||||
|
||||
// Build the response
|
||||
const response: GetMetricResponse = {
|
||||
id: metricFile.id,
|
||||
type: 'metric_file',
|
||||
name: resolvedName,
|
||||
version_number: resolvedVersionNum,
|
||||
error: null,
|
||||
description: resolvedDescription,
|
||||
file_name: metricFile.fileName,
|
||||
time_frame: resolvedTimeFrame,
|
||||
data_source_id: metricFile.dataSourceId,
|
||||
chart_config: resolvedChartConfig,
|
||||
data_metadata: metricFile.dataMetadata as DataMetadata,
|
||||
status: metricFile.verification as VerificationStatus,
|
||||
file: fileYaml,
|
||||
created_at: metricFile.createdAt,
|
||||
updated_at: resolvedUpdatedAt,
|
||||
sent_by_id: metricFile.createdBy,
|
||||
sent_by_name: '',
|
||||
sent_by_avatar_url: null,
|
||||
sql: resolvedSql,
|
||||
dashboards,
|
||||
collections,
|
||||
versions,
|
||||
evaluation_score: evaluationScore,
|
||||
evaluation_summary: metricFile.evaluationSummary || '',
|
||||
permission: effectiveRole,
|
||||
individual_permissions: individualPermissions,
|
||||
publicly_accessible: metricFile.publiclyAccessible,
|
||||
public_expiry_date: metricFile.publicExpiryDate,
|
||||
public_enabled_by: publicEnabledBy,
|
||||
public_password: metricFile.publicPassword,
|
||||
workspace_sharing: metricFile.workspaceSharing,
|
||||
workspace_member_count: workspaceMemberCount,
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async function getPubliclyEnabledByUser(enabledById: string | null): Promise<string | null> {
|
||||
if (enabledById) {
|
||||
const publicEnabledByUser = await getUser({ id: enabledById });
|
||||
return publicEnabledByUser.email;
|
||||
}
|
||||
return null;
|
||||
// Build and return the complete metric response
|
||||
return await buildMetricResponse(processedData, user.id);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,310 @@
|
|||
import { checkPermission } from '@buster/access-controls';
|
||||
import {
|
||||
type MetricFile,
|
||||
type User,
|
||||
getAssetsAssociatedWithMetric,
|
||||
getMetricFileById,
|
||||
getOrganizationMemberCount,
|
||||
getUser,
|
||||
getUsersWithMetricPermissions,
|
||||
} from '@buster/database/queries';
|
||||
import {
|
||||
type ChartConfigProps,
|
||||
DEFAULT_CHART_THEME,
|
||||
type DataMetadata,
|
||||
type GetMetricResponse,
|
||||
type MetricYml,
|
||||
} from '@buster/server-shared/metrics';
|
||||
import type { AssetPermissionRole, VerificationStatus } from '@buster/server-shared/share';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
import yaml from 'js-yaml';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const MetricAccessOptionsSchema = z.object({
|
||||
/** If public access has been verified by a parent resource set to true */
|
||||
publicAccessPreviouslyVerified: z.boolean().default(false),
|
||||
/** Password for public access validation */
|
||||
password: z.string().optional(),
|
||||
/** Version number to fetch */
|
||||
versionNumber: z.number().int().optional(),
|
||||
});
|
||||
|
||||
export type MetricAccessOptions = z.infer<typeof MetricAccessOptionsSchema>;
|
||||
|
||||
export interface ProcessedMetricData {
|
||||
metricFile: MetricFile;
|
||||
resolvedContent: MetricYml;
|
||||
resolvedName: string;
|
||||
resolvedDescription: string | null;
|
||||
resolvedTimeFrame: string;
|
||||
resolvedChartConfig: ChartConfigProps;
|
||||
resolvedSql: string;
|
||||
resolvedUpdatedAt: string;
|
||||
resolvedVersionNum: number;
|
||||
effectiveRole: AssetPermissionRole;
|
||||
versions: Array<{ version_number: number; updated_at: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared helper function to fetch and process metric data with flexible access control
|
||||
*/
|
||||
export async function fetchAndProcessMetricData(
|
||||
metricId: string,
|
||||
user: User,
|
||||
options: MetricAccessOptions
|
||||
): Promise<ProcessedMetricData> {
|
||||
const { publicAccessPreviouslyVerified = false, password, versionNumber } = options;
|
||||
|
||||
// Fetch metric details
|
||||
const metricFile = await getMetricFileById(metricId);
|
||||
|
||||
if (!metricFile) {
|
||||
console.warn(`Metric file not found: ${metricId}`);
|
||||
throw new HTTPException(404, {
|
||||
message: 'Metric file not found',
|
||||
});
|
||||
}
|
||||
|
||||
let effectiveRole: AssetPermissionRole | undefined = publicAccessPreviouslyVerified
|
||||
? 'can_view'
|
||||
: undefined;
|
||||
|
||||
const permissionResult = await checkPermission({
|
||||
userId: user.id,
|
||||
assetId: metricId,
|
||||
assetType: 'metric_file',
|
||||
requiredRole: 'can_view',
|
||||
organizationId: metricFile.organizationId,
|
||||
workspaceSharing: metricFile.workspaceSharing || 'none',
|
||||
});
|
||||
|
||||
effectiveRole = permissionResult.effectiveRole ? permissionResult.effectiveRole : effectiveRole;
|
||||
|
||||
// Check public access if needed
|
||||
if (!effectiveRole) {
|
||||
if (!metricFile.publiclyAccessible) {
|
||||
console.warn(`Permission denied for user ${user.id} to metric ${metricId}`);
|
||||
throw new HTTPException(403, {
|
||||
message: "You don't have permission to view this metric",
|
||||
});
|
||||
}
|
||||
|
||||
// Check if public access has expired
|
||||
const today = new Date();
|
||||
if (metricFile.publicExpiryDate && new Date(metricFile.publicExpiryDate) < today) {
|
||||
console.warn(`Public access expired for metric ${metricId}`);
|
||||
throw new HTTPException(403, {
|
||||
message: 'Public access to this metric has expired',
|
||||
});
|
||||
}
|
||||
|
||||
// Check password if required
|
||||
if (metricFile.publicPassword) {
|
||||
if (!password) {
|
||||
console.warn(`Public password required for metric ${metricId}`);
|
||||
throw new HTTPException(418, {
|
||||
message: 'Password required for public access',
|
||||
});
|
||||
}
|
||||
|
||||
if (password !== metricFile.publicPassword) {
|
||||
console.warn(`Incorrect public password for metric ${metricId}`);
|
||||
throw new HTTPException(403, {
|
||||
message: 'Incorrect password for public access',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
effectiveRole = 'can_view';
|
||||
}
|
||||
|
||||
// Shouldn't ever be hit because we throw errors above if public access is not verified
|
||||
if (!effectiveRole) {
|
||||
console.warn(`Permission denied for user ${user.id} to metric ${metricId}`);
|
||||
throw new HTTPException(403, {
|
||||
message: "You don't have permission to view this metric",
|
||||
});
|
||||
}
|
||||
|
||||
// Parse version history
|
||||
const versionHistory = metricFile.versionHistory;
|
||||
const versions: Array<{ version_number: number; updated_at: string }> = [];
|
||||
|
||||
Object.values(versionHistory).forEach((version) => {
|
||||
versions.push({
|
||||
version_number: version.version_number,
|
||||
updated_at: version.updated_at,
|
||||
});
|
||||
});
|
||||
versions.sort((a, b) => a.version_number - b.version_number);
|
||||
|
||||
// Resolve version-specific data
|
||||
let resolvedContent: MetricYml;
|
||||
let resolvedName: string;
|
||||
let resolvedDescription: string | null;
|
||||
let resolvedTimeFrame: string;
|
||||
let resolvedChartConfig: ChartConfigProps;
|
||||
let resolvedSql: string;
|
||||
let resolvedUpdatedAt: string;
|
||||
let resolvedVersionNum: number;
|
||||
|
||||
// Get requested version if included in the request
|
||||
if (versionNumber) {
|
||||
const requestedVersionNumToString = versionNumber.toString();
|
||||
|
||||
if (versionHistory[requestedVersionNumToString]) {
|
||||
const version = versionHistory[requestedVersionNumToString];
|
||||
|
||||
if (!version || !version.content) {
|
||||
throw new HTTPException(404, {
|
||||
message: `Version ${versionNumber} not found`,
|
||||
});
|
||||
}
|
||||
|
||||
const versionContent = version.content as MetricYml;
|
||||
resolvedContent = versionContent;
|
||||
resolvedName = versionContent.name;
|
||||
resolvedDescription = versionContent.description;
|
||||
resolvedTimeFrame = versionContent.timeFrame;
|
||||
resolvedChartConfig = versionContent.chartConfig;
|
||||
resolvedSql = versionContent.sql;
|
||||
resolvedUpdatedAt = version.updated_at;
|
||||
resolvedVersionNum = version.version_number;
|
||||
} else {
|
||||
throw new HTTPException(404, {
|
||||
message: `Version ${versionNumber} not found`,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Use current/latest version
|
||||
const currentContent = metricFile.content as MetricYml;
|
||||
resolvedContent = currentContent;
|
||||
resolvedName = metricFile.name;
|
||||
resolvedDescription = currentContent.description;
|
||||
resolvedTimeFrame = currentContent.timeFrame;
|
||||
resolvedChartConfig = currentContent.chartConfig;
|
||||
resolvedSql = currentContent.sql;
|
||||
resolvedUpdatedAt = metricFile.updatedAt;
|
||||
|
||||
// Determine latest version number
|
||||
const maxVersion = Math.max(...versions.map((v) => v.version_number), 1);
|
||||
resolvedVersionNum = maxVersion;
|
||||
}
|
||||
|
||||
// Color fallback from v1 api logic
|
||||
if (!resolvedChartConfig.colors) {
|
||||
resolvedChartConfig.colors = DEFAULT_CHART_THEME;
|
||||
}
|
||||
|
||||
return {
|
||||
metricFile,
|
||||
resolvedContent,
|
||||
resolvedName,
|
||||
resolvedDescription,
|
||||
resolvedTimeFrame,
|
||||
resolvedChartConfig,
|
||||
resolvedSql,
|
||||
resolvedUpdatedAt,
|
||||
resolvedVersionNum,
|
||||
effectiveRole,
|
||||
versions,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a complete metric response from processed metric data
|
||||
*/
|
||||
export async function buildMetricResponse(
|
||||
processedData: ProcessedMetricData,
|
||||
userId: string
|
||||
): Promise<GetMetricResponse> {
|
||||
const {
|
||||
metricFile,
|
||||
resolvedContent,
|
||||
resolvedName,
|
||||
resolvedDescription,
|
||||
resolvedTimeFrame,
|
||||
resolvedChartConfig,
|
||||
resolvedSql,
|
||||
resolvedUpdatedAt,
|
||||
resolvedVersionNum,
|
||||
effectiveRole,
|
||||
versions,
|
||||
} = processedData;
|
||||
|
||||
const fileYaml = yaml.dump(resolvedContent);
|
||||
|
||||
// Get the extra metric info concurrently
|
||||
const [individualPermissions, workspaceMemberCount, associatedAssets, publicEnabledBy] =
|
||||
await Promise.all([
|
||||
getUsersWithMetricPermissions({ metricId: metricFile.id }),
|
||||
getOrganizationMemberCount(metricFile.organizationId),
|
||||
getAssetsAssociatedWithMetric(metricFile.id, userId),
|
||||
getPubliclyEnabledByUser(metricFile.publiclyEnabledBy),
|
||||
]);
|
||||
|
||||
const { dashboards, collections } = associatedAssets;
|
||||
|
||||
// Not used but still exists in frontend code so including it here
|
||||
const evaluationScore = (() => {
|
||||
if (!metricFile.evaluationScore) {
|
||||
return 'Low';
|
||||
}
|
||||
if (metricFile.evaluationScore > 0.8) {
|
||||
return 'High';
|
||||
}
|
||||
if (metricFile.evaluationScore > 0.5) {
|
||||
return 'Moderate';
|
||||
}
|
||||
return 'Low';
|
||||
})() as 'High' | 'Moderate' | 'Low';
|
||||
|
||||
// Build the response
|
||||
const response: GetMetricResponse = {
|
||||
id: metricFile.id,
|
||||
type: 'metric_file',
|
||||
name: resolvedName,
|
||||
version_number: resolvedVersionNum,
|
||||
error: null,
|
||||
description: resolvedDescription,
|
||||
file_name: metricFile.fileName,
|
||||
time_frame: resolvedTimeFrame,
|
||||
data_source_id: metricFile.dataSourceId,
|
||||
chart_config: resolvedChartConfig,
|
||||
data_metadata: metricFile.dataMetadata as DataMetadata,
|
||||
status: metricFile.verification as VerificationStatus,
|
||||
file: fileYaml,
|
||||
created_at: metricFile.createdAt,
|
||||
updated_at: resolvedUpdatedAt,
|
||||
sent_by_id: metricFile.createdBy,
|
||||
sent_by_name: '',
|
||||
sent_by_avatar_url: null,
|
||||
sql: resolvedSql,
|
||||
dashboards,
|
||||
collections,
|
||||
versions,
|
||||
evaluation_score: evaluationScore,
|
||||
evaluation_summary: metricFile.evaluationSummary || '',
|
||||
permission: effectiveRole,
|
||||
individual_permissions: individualPermissions,
|
||||
publicly_accessible: metricFile.publiclyAccessible,
|
||||
public_expiry_date: metricFile.publicExpiryDate,
|
||||
public_enabled_by: publicEnabledBy,
|
||||
public_password: metricFile.publicPassword,
|
||||
workspace_sharing: metricFile.workspaceSharing,
|
||||
workspace_member_count: workspaceMemberCount,
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get publicly enabled by user email
|
||||
*/
|
||||
export async function getPubliclyEnabledByUser(enabledById: string | null): Promise<string | null> {
|
||||
if (enabledById) {
|
||||
const publicEnabledByUser = await getUser({ id: enabledById });
|
||||
return publicEnabledByUser.email;
|
||||
}
|
||||
return null;
|
||||
}
|
Loading…
Reference in New Issue