diff --git a/apps/server/src/api/v2/metric_files/get-metric-data.ts b/apps/server/src/api/v2/metric_files/get-metric-data.ts new file mode 100644 index 000000000..b2a24cb32 --- /dev/null +++ b/apps/server/src/api/v2/metric_files/get-metric-data.ts @@ -0,0 +1,78 @@ +import { type AssetPermissionCheck, checkPermission } from '@buster/access-controls'; +import type { User } from '@buster/database'; +import { getUserOrganizationId } from '@buster/database'; +import type { MetricDataResponse } from '@buster/server-shared/metrics'; +import { HTTPException } from 'hono/http-exception'; + +/** + * Handler for retrieving metric data + * + * This handler: + * 1. Validates user has access to the organization + * 2. Checks user has permission to view the metric file + * 3. Retrieves the metric definition + * 4. Parses the metric content to extract SQL + * 5. Executes the query against the data source + * 6. Returns the data with metadata and pagination info + * + * @param metricId - The ID of the metric to retrieve data for + * @param limit - Maximum number of rows to return (default 5000, max 5000) + * @param user - The authenticated user + * @returns The metric data with metadata + */ +export async function getMetricDataHandler( + metricId: string, + limit: number = 5000, + user: User +): Promise { + // Get user's organization + const userOrg = await getUserOrganizationId(user.id); + + if (!userOrg) { + throw new HTTPException(403, { + message: 'You must be part of an organization to access metric data', + }); + } + + const { organizationId } = userOrg; + + // Check if user has permission to view this metric file + const permissionCheck: AssetPermissionCheck = { + userId: user.id, + assetId: metricId, + assetType: 'metric_file', + requiredRole: 'can_view', + organizationId, + }; + + const permissionResult = await checkPermission(permissionCheck); + + if (!permissionResult.hasAccess) { + throw new HTTPException(403, { + message: 'You do not have permission to view this metric', + }); + } + + // Ensure limit is within bounds + const queryLimit = Math.min(Math.max(limit, 1), 5000); + + // TODO: Implement the following steps in subsequent tickets: + // 1. Retrieve metric definition from database + // 2. Parse metric content (YAML/JSON) to extract SQL query + // 3. Get data source connection details + // 4. Execute query using appropriate data source adapter + // 5. Process results and build metadata + // 6. Check if there are more records beyond the limit + + // Placeholder response for now + return { + data: [], + data_metadata: { + column_count: 0, + column_metadata: [], + row_count: 0, + }, + metricId, + has_more_records: false, + }; +} \ No newline at end of file diff --git a/apps/server/src/api/v2/metric_files/index.ts b/apps/server/src/api/v2/metric_files/index.ts index ee350b8ad..a3a628820 100644 --- a/apps/server/src/api/v2/metric_files/index.ts +++ b/apps/server/src/api/v2/metric_files/index.ts @@ -1,15 +1,36 @@ -import { MetricDownloadParamsSchema } from '@buster/server-shared/metrics'; +import { + MetricDataParamsSchema, + MetricDataQuerySchema, + MetricDownloadParamsSchema, +} from '@buster/server-shared/metrics'; import { zValidator } from '@hono/zod-validator'; import { Hono } from 'hono'; import { HTTPException } from 'hono/http-exception'; import { requireAuth } from '../../../middleware/auth'; import '../../../types/hono.types'; import { downloadMetricFileHandler } from './download-metric-file'; +import { getMetricDataHandler } from './get-metric-data'; const app = new Hono() // Apply authentication middleware to all routes .use('*', requireAuth) + // GET /metric_files/:id/data - Get metric data with pagination + .get( + '/:id/data', + zValidator('param', MetricDataParamsSchema), + zValidator('query', MetricDataQuerySchema), + async (c) => { + const { id } = c.req.valid('param'); + const { limit } = c.req.valid('query'); + const user = c.get('busterUser'); + + const response = await getMetricDataHandler(id, limit, user); + + return c.json(response); + } + ) + // GET /metric_files/:id/download - Download metric file data as CSV .get('/:id/download', zValidator('param', MetricDownloadParamsSchema), async (c) => { const { id } = c.req.valid('param'); diff --git a/packages/server-shared/src/metrics/requests.types.ts b/packages/server-shared/src/metrics/requests.types.ts index e5197fbb3..f382e43b0 100644 --- a/packages/server-shared/src/metrics/requests.types.ts +++ b/packages/server-shared/src/metrics/requests.types.ts @@ -8,7 +8,9 @@ export const GetMetricRequestSchema = z.object({ version_number: z.number().optional(), //api will default to latest if not provided }); -export const GetMetricDataRequestSchema = GetMetricRequestSchema; +export const GetMetricDataRequestSchema = GetMetricRequestSchema.extend({ + limit: z.number().min(1).max(5000).default(5000).optional(), +}); export const GetMetricListRequestSchema = z.object({ /** The token representing the current page number for pagination */ diff --git a/packages/server-shared/src/metrics/responses.types.ts b/packages/server-shared/src/metrics/responses.types.ts index c6c36e274..f8bfa3d86 100644 --- a/packages/server-shared/src/metrics/responses.types.ts +++ b/packages/server-shared/src/metrics/responses.types.ts @@ -39,3 +39,21 @@ export type ShareMetricResponse = z.infer; export type ShareDeleteResponse = z.infer; export type ShareUpdateResponse = z.infer; export type MetricDataResponse = z.infer; + +/** + * Path parameters for metric data endpoint + */ +export const MetricDataParamsSchema = z.object({ + id: z.string().uuid('Metric ID must be a valid UUID'), +}); + +export type MetricDataParams = z.infer; + +/** + * Query parameters for metric data endpoint + */ +export const MetricDataQuerySchema = z.object({ + limit: z.coerce.number().min(1).max(5000).default(5000).optional(), +}); + +export type MetricDataQuery = z.infer;