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:
wellsbunk5 2025-09-19 12:37:06 -06:00 committed by GitHub
commit 0aaa53c685
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 350 additions and 362 deletions

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;
}