From 5b4020527590a68c72a1d775d3b21c79ba3a9d15 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 13:14:58 +0000 Subject: [PATCH 1/4] Implement /api/v2/title endpoint for BUS-1494 - Add server-shared types for GetTitleRequest and GetTitleResponse with Zod validation - Create database query functions for each asset type (chat, metric, collection, dashboard) - Implement GET endpoint with zValidator middleware and exhaustive switch statement - Add proper permission checks (publiclyAccessible OR organizationId match) - Export new functions from database query index files - Add title route to v2 API index - Follow existing Hono API patterns with standardErrorHandler Co-Authored-By: nate@buster.so --- apps/server/src/api/v2/index.ts | 4 +- apps/server/src/api/v2/title/GET.ts | 49 +++++++++++++++++++ apps/server/src/api/v2/title/index.ts | 3 ++ .../queries/assets/get-collection-title.ts | 34 +++++++++++++ .../src/queries/assets/get-dashboard-title.ts | 35 +++++++++++++ .../src/queries/assets/get-metric-title.ts | 35 +++++++++++++ packages/database/src/queries/assets/index.ts | 18 +++++++ .../src/queries/chats/get-chat-title.ts | 35 +++++++++++++ packages/database/src/queries/chats/index.ts | 6 +++ packages/server-shared/package.json | 4 ++ packages/server-shared/src/title/index.ts | 2 + packages/server-shared/src/title/requests.ts | 8 +++ packages/server-shared/src/title/responses.ts | 7 +++ 13 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 apps/server/src/api/v2/title/GET.ts create mode 100644 apps/server/src/api/v2/title/index.ts create mode 100644 packages/database/src/queries/assets/get-collection-title.ts create mode 100644 packages/database/src/queries/assets/get-dashboard-title.ts create mode 100644 packages/database/src/queries/assets/get-metric-title.ts create mode 100644 packages/database/src/queries/chats/get-chat-title.ts create mode 100644 packages/server-shared/src/title/index.ts create mode 100644 packages/server-shared/src/title/requests.ts create mode 100644 packages/server-shared/src/title/responses.ts diff --git a/apps/server/src/api/v2/index.ts b/apps/server/src/api/v2/index.ts index c73a3a3e7..bd3315aa5 100644 --- a/apps/server/src/api/v2/index.ts +++ b/apps/server/src/api/v2/index.ts @@ -8,6 +8,7 @@ import organizationRoutes from './organization'; import securityRoutes from './security'; import slackRoutes from './slack'; import supportRoutes from './support'; +import titleRoutes from './title'; import userRoutes from './users'; const app = new Hono() @@ -19,6 +20,7 @@ const app = new Hono() .route('/support', supportRoutes) .route('/security', securityRoutes) .route('/organizations', organizationRoutes) - .route('/dictionaries', dictionariesRoutes); + .route('/dictionaries', dictionariesRoutes) + .route('/title', titleRoutes); export default app; diff --git a/apps/server/src/api/v2/title/GET.ts b/apps/server/src/api/v2/title/GET.ts new file mode 100644 index 000000000..c2adf97fa --- /dev/null +++ b/apps/server/src/api/v2/title/GET.ts @@ -0,0 +1,49 @@ +import { getChatTitle } from '@buster/database/queries/chats'; +import { getCollectionTitle, getDashboardTitle, getMetricTitle } from '@buster/database/queries/assets'; +import { GetTitleRequestSchema, type GetTitleResponse } from '@buster/server-shared/title'; +import { zValidator } from '@hono/zod-validator'; +import { Hono } from 'hono'; +import { HTTPException } from 'hono/http-exception'; +import { requireAuth } from '../../../middleware/auth'; +import { standardErrorHandler } from '../../../utils/response'; + +const app = new Hono() + .use('*', requireAuth) + .get('/', zValidator('query', GetTitleRequestSchema), async (c) => { + try { + const { assetId, assetType } = c.req.valid('query'); + const user = c.get('busterUser'); + + let title: string | null = null; + + switch (assetType) { + case 'chat': + title = await getChatTitle({ assetId, organizationId: user.organizationId }); + break; + case 'metric': + title = await getMetricTitle({ assetId, organizationId: user.organizationId }); + break; + case 'collection': + title = await getCollectionTitle({ assetId, organizationId: user.organizationId }); + break; + case 'dashboard': + title = await getDashboardTitle({ assetId, organizationId: user.organizationId }); + break; + default: + const _exhaustive: never = assetType; + throw new HTTPException(400, { message: `Unsupported asset type: ${assetType}` }); + } + + if (title === null) { + throw new HTTPException(404, { message: 'Asset not found or access denied' }); + } + + const response: GetTitleResponse = { title }; + return c.json(response); + } catch (error) { + return standardErrorHandler(error, c); + } + }) + .onError(standardErrorHandler); + +export default app; diff --git a/apps/server/src/api/v2/title/index.ts b/apps/server/src/api/v2/title/index.ts new file mode 100644 index 000000000..31d377c39 --- /dev/null +++ b/apps/server/src/api/v2/title/index.ts @@ -0,0 +1,3 @@ +import app from './GET'; + +export default app; diff --git a/packages/database/src/queries/assets/get-collection-title.ts b/packages/database/src/queries/assets/get-collection-title.ts new file mode 100644 index 000000000..1d98c61cc --- /dev/null +++ b/packages/database/src/queries/assets/get-collection-title.ts @@ -0,0 +1,34 @@ +import { and, eq, isNull } from 'drizzle-orm'; +import { z } from 'zod'; +import { db } from '../../connection'; +import { collections } from '../../schema'; + +export const GetCollectionTitleInputSchema = z.object({ + assetId: z.string().uuid(), + organizationId: z.string().uuid().optional(), +}); + +export type GetCollectionTitleInput = z.infer; + +export async function getCollectionTitle(input: GetCollectionTitleInput): Promise { + const validated = GetCollectionTitleInputSchema.parse(input); + + const [collection] = await db + .select({ + name: collections.name, + organizationId: collections.organizationId, + }) + .from(collections) + .where(and(eq(collections.id, validated.assetId), isNull(collections.deletedAt))) + .limit(1); + + if (!collection) { + return null; + } + + if (collection.organizationId !== validated.organizationId) { + return null; + } + + return collection.name; +} diff --git a/packages/database/src/queries/assets/get-dashboard-title.ts b/packages/database/src/queries/assets/get-dashboard-title.ts new file mode 100644 index 000000000..63dd9417a --- /dev/null +++ b/packages/database/src/queries/assets/get-dashboard-title.ts @@ -0,0 +1,35 @@ +import { and, eq, isNull } from 'drizzle-orm'; +import { z } from 'zod'; +import { db } from '../../connection'; +import { dashboardFiles } from '../../schema'; + +export const GetDashboardTitleInputSchema = z.object({ + assetId: z.string().uuid(), + organizationId: z.string().uuid().optional(), +}); + +export type GetDashboardTitleInput = z.infer; + +export async function getDashboardTitle(input: GetDashboardTitleInput): Promise { + const validated = GetDashboardTitleInputSchema.parse(input); + + const [dashboard] = await db + .select({ + name: dashboardFiles.name, + publiclyAccessible: dashboardFiles.publiclyAccessible, + organizationId: dashboardFiles.organizationId, + }) + .from(dashboardFiles) + .where(and(eq(dashboardFiles.id, validated.assetId), isNull(dashboardFiles.deletedAt))) + .limit(1); + + if (!dashboard) { + return null; + } + + if (!dashboard.publiclyAccessible && dashboard.organizationId !== validated.organizationId) { + return null; + } + + return dashboard.name; +} diff --git a/packages/database/src/queries/assets/get-metric-title.ts b/packages/database/src/queries/assets/get-metric-title.ts new file mode 100644 index 000000000..6da66797b --- /dev/null +++ b/packages/database/src/queries/assets/get-metric-title.ts @@ -0,0 +1,35 @@ +import { and, eq, isNull } from 'drizzle-orm'; +import { z } from 'zod'; +import { db } from '../../connection'; +import { metricFiles } from '../../schema'; + +export const GetMetricTitleInputSchema = z.object({ + assetId: z.string().uuid(), + organizationId: z.string().uuid().optional(), +}); + +export type GetMetricTitleInput = z.infer; + +export async function getMetricTitle(input: GetMetricTitleInput): Promise { + const validated = GetMetricTitleInputSchema.parse(input); + + const [metric] = await db + .select({ + name: metricFiles.name, + publiclyAccessible: metricFiles.publiclyAccessible, + organizationId: metricFiles.organizationId, + }) + .from(metricFiles) + .where(and(eq(metricFiles.id, validated.assetId), isNull(metricFiles.deletedAt))) + .limit(1); + + if (!metric) { + return null; + } + + if (!metric.publiclyAccessible && metric.organizationId !== validated.organizationId) { + return null; + } + + return metric.name; +} diff --git a/packages/database/src/queries/assets/index.ts b/packages/database/src/queries/assets/index.ts index 27d906d6a..2b38ec906 100644 --- a/packages/database/src/queries/assets/index.ts +++ b/packages/database/src/queries/assets/index.ts @@ -11,3 +11,21 @@ export { type DashboardFileContext, type DashboardFile, } from './dashboards'; + +export { + getMetricTitle, + GetMetricTitleInputSchema, + type GetMetricTitleInput, +} from './get-metric-title'; + +export { + getCollectionTitle, + GetCollectionTitleInputSchema, + type GetCollectionTitleInput, +} from './get-collection-title'; + +export { + getDashboardTitle, + GetDashboardTitleInputSchema, + type GetDashboardTitleInput, +} from './get-dashboard-title'; diff --git a/packages/database/src/queries/chats/get-chat-title.ts b/packages/database/src/queries/chats/get-chat-title.ts new file mode 100644 index 000000000..e22cfe5be --- /dev/null +++ b/packages/database/src/queries/chats/get-chat-title.ts @@ -0,0 +1,35 @@ +import { and, eq, isNull } from 'drizzle-orm'; +import { z } from 'zod'; +import { db } from '../../connection'; +import { chats } from '../../schema'; + +export const GetChatTitleInputSchema = z.object({ + assetId: z.string().uuid(), + organizationId: z.string().uuid().optional(), +}); + +export type GetChatTitleInput = z.infer; + +export async function getChatTitle(input: GetChatTitleInput): Promise { + const validated = GetChatTitleInputSchema.parse(input); + + const [chat] = await db + .select({ + title: chats.title, + publiclyAccessible: chats.publiclyAccessible, + organizationId: chats.organizationId, + }) + .from(chats) + .where(and(eq(chats.id, validated.assetId), isNull(chats.deletedAt))) + .limit(1); + + if (!chat) { + return null; + } + + if (!chat.publiclyAccessible && chat.organizationId !== validated.organizationId) { + return null; + } + + return chat.title; +} diff --git a/packages/database/src/queries/chats/index.ts b/packages/database/src/queries/chats/index.ts index 7e6a38b66..adb814cd9 100644 --- a/packages/database/src/queries/chats/index.ts +++ b/packages/database/src/queries/chats/index.ts @@ -12,3 +12,9 @@ export { type CreateMessageInput, type Chat, } from './chats'; + +export { + getChatTitle, + GetChatTitleInputSchema, + type GetChatTitleInput, +} from './get-chat-title'; diff --git a/packages/server-shared/package.json b/packages/server-shared/package.json index d7ae6a8d8..4d9d8f8c4 100644 --- a/packages/server-shared/package.json +++ b/packages/server-shared/package.json @@ -63,6 +63,10 @@ "./dictionary": { "types": "./dist/dictionary/index.d.ts", "default": "./dist/dictionary/index.js" + }, + "./title": { + "types": "./dist/title/index.d.ts", + "default": "./dist/title/index.js" } }, "dependencies": { diff --git a/packages/server-shared/src/title/index.ts b/packages/server-shared/src/title/index.ts new file mode 100644 index 000000000..0b4b018e3 --- /dev/null +++ b/packages/server-shared/src/title/index.ts @@ -0,0 +1,2 @@ +export * from './requests'; +export * from './responses'; diff --git a/packages/server-shared/src/title/requests.ts b/packages/server-shared/src/title/requests.ts new file mode 100644 index 000000000..72f0a52b1 --- /dev/null +++ b/packages/server-shared/src/title/requests.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; + +export const GetTitleRequestSchema = z.object({ + assetId: z.string().uuid(), + assetType: z.enum(['chat', 'metric', 'collection', 'dashboard']), +}); + +export type GetTitleRequest = z.infer; diff --git a/packages/server-shared/src/title/responses.ts b/packages/server-shared/src/title/responses.ts new file mode 100644 index 000000000..decfedcba --- /dev/null +++ b/packages/server-shared/src/title/responses.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; + +export const GetTitleResponseSchema = z.object({ + title: z.string(), +}); + +export type GetTitleResponse = z.infer; From 783b95b190c34ec3c2d791f353d67df05df1c17f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 13:26:58 +0000 Subject: [PATCH 2/4] Fix CI failures: correct import paths and user organization access - Import database queries from main @buster/database package - Use getUserOrganizationId() instead of user.organizationId - Add proper error handling for users without organizations - Follow existing patterns from other API handlers Co-Authored-By: nate@buster.so --- apps/server/src/api/v2/title/GET.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/server/src/api/v2/title/GET.ts b/apps/server/src/api/v2/title/GET.ts index c2adf97fa..395eac76e 100644 --- a/apps/server/src/api/v2/title/GET.ts +++ b/apps/server/src/api/v2/title/GET.ts @@ -1,5 +1,4 @@ -import { getChatTitle } from '@buster/database/queries/chats'; -import { getCollectionTitle, getDashboardTitle, getMetricTitle } from '@buster/database/queries/assets'; +import { getChatTitle, getCollectionTitle, getDashboardTitle, getMetricTitle, getUserOrganizationId } from '@buster/database'; import { GetTitleRequestSchema, type GetTitleResponse } from '@buster/server-shared/title'; import { zValidator } from '@hono/zod-validator'; import { Hono } from 'hono'; @@ -14,20 +13,25 @@ const app = new Hono() const { assetId, assetType } = c.req.valid('query'); const user = c.get('busterUser'); + const userOrg = await getUserOrganizationId(user.id); + if (!userOrg) { + throw new HTTPException(403, { message: 'User is not associated with an organization' }); + } + let title: string | null = null; switch (assetType) { case 'chat': - title = await getChatTitle({ assetId, organizationId: user.organizationId }); + title = await getChatTitle({ assetId, organizationId: userOrg.organizationId }); break; case 'metric': - title = await getMetricTitle({ assetId, organizationId: user.organizationId }); + title = await getMetricTitle({ assetId, organizationId: userOrg.organizationId }); break; case 'collection': - title = await getCollectionTitle({ assetId, organizationId: user.organizationId }); + title = await getCollectionTitle({ assetId, organizationId: userOrg.organizationId }); break; case 'dashboard': - title = await getDashboardTitle({ assetId, organizationId: user.organizationId }); + title = await getDashboardTitle({ assetId, organizationId: userOrg.organizationId }); break; default: const _exhaustive: never = assetType; From 8bea0e8401cea5d423ddc61d0d86616ced950be0 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 13:38:45 +0000 Subject: [PATCH 3/4] Fix Biome linting error: wrap switch default case in block scope - Add curly braces around default case content to satisfy noSwitchDeclarations rule - Prevents variable declarations from being accessible to other switch cases - Apply Biome formatting fixes for import statement and spacing Co-Authored-By: nate@buster.so --- apps/server/src/api/v2/title/GET.ts | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/apps/server/src/api/v2/title/GET.ts b/apps/server/src/api/v2/title/GET.ts index 395eac76e..c5fb7579a 100644 --- a/apps/server/src/api/v2/title/GET.ts +++ b/apps/server/src/api/v2/title/GET.ts @@ -1,4 +1,10 @@ -import { getChatTitle, getCollectionTitle, getDashboardTitle, getMetricTitle, getUserOrganizationId } from '@buster/database'; +import { + getChatTitle, + getCollectionTitle, + getDashboardTitle, + getMetricTitle, + getUserOrganizationId, +} from '@buster/database'; import { GetTitleRequestSchema, type GetTitleResponse } from '@buster/server-shared/title'; import { zValidator } from '@hono/zod-validator'; import { Hono } from 'hono'; @@ -12,14 +18,14 @@ const app = new Hono() try { const { assetId, assetType } = c.req.valid('query'); const user = c.get('busterUser'); - + const userOrg = await getUserOrganizationId(user.id); if (!userOrg) { throw new HTTPException(403, { message: 'User is not associated with an organization' }); } - + let title: string | null = null; - + switch (assetType) { case 'chat': title = await getChatTitle({ assetId, organizationId: userOrg.organizationId }); @@ -33,15 +39,16 @@ const app = new Hono() case 'dashboard': title = await getDashboardTitle({ assetId, organizationId: userOrg.organizationId }); break; - default: + default: { const _exhaustive: never = assetType; throw new HTTPException(400, { message: `Unsupported asset type: ${assetType}` }); + } } - + if (title === null) { throw new HTTPException(404, { message: 'Asset not found or access denied' }); } - + const response: GetTitleResponse = { title }; return c.json(response); } catch (error) { From 5e6eb76f5bc591f07119c60374b7d9b90e5c03dc Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Thu, 24 Jul 2025 17:33:37 -0600 Subject: [PATCH 4/4] move get asset types around --- apps/server/src/api/v2/title/GET.ts | 11 ++++----- packages/database/src/queries/assets/index.ts | 23 ------------------- .../get-collection-title.ts | 11 ++++++--- .../database/src/queries/collections/index.ts | 5 ++++ .../{assets => dashboards}/dashboards.ts | 0 .../get-dashboard-title.ts | 11 ++++++--- .../database/src/queries/dashboards/index.ts | 11 +++++++++ packages/database/src/queries/index.ts | 3 +++ .../{assets => metrics}/get-metric-title.ts | 11 ++++++--- .../database/src/queries/metrics/index.ts | 5 ++++ packages/server-shared/src/title/requests.ts | 3 ++- 11 files changed, 54 insertions(+), 40 deletions(-) rename packages/database/src/queries/{assets => collections}/get-collection-title.ts (67%) create mode 100644 packages/database/src/queries/collections/index.ts rename packages/database/src/queries/{assets => dashboards}/dashboards.ts (100%) rename packages/database/src/queries/{assets => dashboards}/get-dashboard-title.ts (68%) create mode 100644 packages/database/src/queries/dashboards/index.ts rename packages/database/src/queries/{assets => metrics}/get-metric-title.ts (68%) create mode 100644 packages/database/src/queries/metrics/index.ts diff --git a/apps/server/src/api/v2/title/GET.ts b/apps/server/src/api/v2/title/GET.ts index c5fb7579a..5a18c3127 100644 --- a/apps/server/src/api/v2/title/GET.ts +++ b/apps/server/src/api/v2/title/GET.ts @@ -20,24 +20,21 @@ const app = new Hono() const user = c.get('busterUser'); const userOrg = await getUserOrganizationId(user.id); - if (!userOrg) { - throw new HTTPException(403, { message: 'User is not associated with an organization' }); - } let title: string | null = null; switch (assetType) { case 'chat': - title = await getChatTitle({ assetId, organizationId: userOrg.organizationId }); + title = await getChatTitle({ assetId, organizationId: userOrg?.organizationId }); break; case 'metric': - title = await getMetricTitle({ assetId, organizationId: userOrg.organizationId }); + title = await getMetricTitle({ assetId, organizationId: userOrg?.organizationId }); break; case 'collection': - title = await getCollectionTitle({ assetId, organizationId: userOrg.organizationId }); + title = await getCollectionTitle({ assetId, organizationId: userOrg?.organizationId }); break; case 'dashboard': - title = await getDashboardTitle({ assetId, organizationId: userOrg.organizationId }); + title = await getDashboardTitle({ assetId, organizationId: userOrg?.organizationId }); break; default: { const _exhaustive: never = assetType; diff --git a/packages/database/src/queries/assets/index.ts b/packages/database/src/queries/assets/index.ts index cbc76ec58..b14d13084 100644 --- a/packages/database/src/queries/assets/index.ts +++ b/packages/database/src/queries/assets/index.ts @@ -6,27 +6,4 @@ export { type GenerateAssetMessagesInput, } from './assets'; -export { - getChatDashboardFiles, - type DashboardFileContext, - type DashboardFile, -} from './dashboards'; - -export { - getMetricTitle, - GetMetricTitleInputSchema, - type GetMetricTitleInput, -} from './get-metric-title'; - -export { - getCollectionTitle, - GetCollectionTitleInputSchema, - type GetCollectionTitleInput, -} from './get-collection-title'; - -export { - getDashboardTitle, - GetDashboardTitleInputSchema, - type GetDashboardTitleInput, -} from './get-dashboard-title'; export type { DatabaseAssetType } from './assets'; diff --git a/packages/database/src/queries/assets/get-collection-title.ts b/packages/database/src/queries/collections/get-collection-title.ts similarity index 67% rename from packages/database/src/queries/assets/get-collection-title.ts rename to packages/database/src/queries/collections/get-collection-title.ts index 1d98c61cc..a4722d300 100644 --- a/packages/database/src/queries/assets/get-collection-title.ts +++ b/packages/database/src/queries/collections/get-collection-title.ts @@ -10,7 +10,8 @@ export const GetCollectionTitleInputSchema = z.object({ export type GetCollectionTitleInput = z.infer; -export async function getCollectionTitle(input: GetCollectionTitleInput): Promise { +// Updated return type to remove null since we now throw an error instead +export async function getCollectionTitle(input: GetCollectionTitleInput): Promise { const validated = GetCollectionTitleInputSchema.parse(input); const [collection] = await db @@ -22,12 +23,16 @@ export async function getCollectionTitle(input: GetCollectionTitleInput): Promis .where(and(eq(collections.id, validated.assetId), isNull(collections.deletedAt))) .limit(1); + // Throw error instead of returning null if (!collection) { - return null; + throw new Error(`Collection with ID ${validated.assetId} not found`); } + // Throw error for permission failure instead of returning null if (collection.organizationId !== validated.organizationId) { - return null; + throw new Error( + `Access denied: Collection with ID ${validated.assetId} does not belong to the specified organization` + ); } return collection.name; diff --git a/packages/database/src/queries/collections/index.ts b/packages/database/src/queries/collections/index.ts new file mode 100644 index 000000000..1bc049c9e --- /dev/null +++ b/packages/database/src/queries/collections/index.ts @@ -0,0 +1,5 @@ +export { + getCollectionTitle, + GetCollectionTitleInputSchema, + type GetCollectionTitleInput, +} from './get-collection-title'; diff --git a/packages/database/src/queries/assets/dashboards.ts b/packages/database/src/queries/dashboards/dashboards.ts similarity index 100% rename from packages/database/src/queries/assets/dashboards.ts rename to packages/database/src/queries/dashboards/dashboards.ts diff --git a/packages/database/src/queries/assets/get-dashboard-title.ts b/packages/database/src/queries/dashboards/get-dashboard-title.ts similarity index 68% rename from packages/database/src/queries/assets/get-dashboard-title.ts rename to packages/database/src/queries/dashboards/get-dashboard-title.ts index 63dd9417a..7425c919b 100644 --- a/packages/database/src/queries/assets/get-dashboard-title.ts +++ b/packages/database/src/queries/dashboards/get-dashboard-title.ts @@ -10,7 +10,8 @@ export const GetDashboardTitleInputSchema = z.object({ export type GetDashboardTitleInput = z.infer; -export async function getDashboardTitle(input: GetDashboardTitleInput): Promise { +// Updated return type to remove null since we now throw an error instead +export async function getDashboardTitle(input: GetDashboardTitleInput): Promise { const validated = GetDashboardTitleInputSchema.parse(input); const [dashboard] = await db @@ -23,12 +24,16 @@ export async function getDashboardTitle(input: GetDashboardTitleInput): Promise< .where(and(eq(dashboardFiles.id, validated.assetId), isNull(dashboardFiles.deletedAt))) .limit(1); + // Throw error instead of returning null if (!dashboard) { - return null; + throw new Error(`Dashboard with ID ${validated.assetId} not found`); } + // Throw error for permission failure instead of returning null if (!dashboard.publiclyAccessible && dashboard.organizationId !== validated.organizationId) { - return null; + throw new Error( + `Access denied: Dashboard with ID ${validated.assetId} is not publicly accessible and does not belong to the specified organization` + ); } return dashboard.name; diff --git a/packages/database/src/queries/dashboards/index.ts b/packages/database/src/queries/dashboards/index.ts new file mode 100644 index 000000000..b284f5a8f --- /dev/null +++ b/packages/database/src/queries/dashboards/index.ts @@ -0,0 +1,11 @@ +export { + getChatDashboardFiles, + type DashboardFileContext, + type DashboardFile, +} from './dashboards'; + +export { + getDashboardTitle, + GetDashboardTitleInputSchema, + type GetDashboardTitleInput, +} from './get-dashboard-title'; diff --git a/packages/database/src/queries/index.ts b/packages/database/src/queries/index.ts index 278c06028..bc72287e2 100644 --- a/packages/database/src/queries/index.ts +++ b/packages/database/src/queries/index.ts @@ -5,3 +5,6 @@ export * from './assets'; export * from './metadata'; export * from './chats'; export * from './organizations'; +export * from './dashboards'; +export * from './metrics'; +export * from './collections'; diff --git a/packages/database/src/queries/assets/get-metric-title.ts b/packages/database/src/queries/metrics/get-metric-title.ts similarity index 68% rename from packages/database/src/queries/assets/get-metric-title.ts rename to packages/database/src/queries/metrics/get-metric-title.ts index 6da66797b..5fe548ac4 100644 --- a/packages/database/src/queries/assets/get-metric-title.ts +++ b/packages/database/src/queries/metrics/get-metric-title.ts @@ -10,7 +10,8 @@ export const GetMetricTitleInputSchema = z.object({ export type GetMetricTitleInput = z.infer; -export async function getMetricTitle(input: GetMetricTitleInput): Promise { +// Updated return type to remove null since we now throw an error instead +export async function getMetricTitle(input: GetMetricTitleInput): Promise { const validated = GetMetricTitleInputSchema.parse(input); const [metric] = await db @@ -23,12 +24,16 @@ export async function getMetricTitle(input: GetMetricTitleInput): Promise;