mirror of https://github.com/buster-so/buster.git
Merge pull request #603 from buster-so/devin/BUS-1494-1753276227
Implement /api/v2/title endpoint for asset title retrieval (BUS-1494)
This commit is contained in:
commit
633a77e2ef
|
@ -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;
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
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';
|
||||
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');
|
||||
|
||||
const userOrg = await getUserOrganizationId(user.id);
|
||||
|
||||
let title: string | null = null;
|
||||
|
||||
switch (assetType) {
|
||||
case 'chat':
|
||||
title = await getChatTitle({ assetId, organizationId: userOrg?.organizationId });
|
||||
break;
|
||||
case 'metric':
|
||||
title = await getMetricTitle({ assetId, organizationId: userOrg?.organizationId });
|
||||
break;
|
||||
case 'collection':
|
||||
title = await getCollectionTitle({ assetId, organizationId: userOrg?.organizationId });
|
||||
break;
|
||||
case 'dashboard':
|
||||
title = await getDashboardTitle({ assetId, organizationId: userOrg?.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;
|
|
@ -0,0 +1,3 @@
|
|||
import app from './GET';
|
||||
|
||||
export default app;
|
|
@ -6,10 +6,4 @@ export {
|
|||
type GenerateAssetMessagesInput,
|
||||
} from './assets';
|
||||
|
||||
export {
|
||||
getChatDashboardFiles,
|
||||
type DashboardFileContext,
|
||||
type DashboardFile,
|
||||
} from './dashboards';
|
||||
|
||||
export type { DatabaseAssetType } from './assets';
|
||||
|
|
|
@ -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<typeof GetChatTitleInputSchema>;
|
||||
|
||||
export async function getChatTitle(input: GetChatTitleInput): Promise<string | null> {
|
||||
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;
|
||||
}
|
|
@ -12,3 +12,9 @@ export {
|
|||
type CreateMessageInput,
|
||||
type Chat,
|
||||
} from './chats';
|
||||
|
||||
export {
|
||||
getChatTitle,
|
||||
GetChatTitleInputSchema,
|
||||
type GetChatTitleInput,
|
||||
} from './get-chat-title';
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
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<typeof GetCollectionTitleInputSchema>;
|
||||
|
||||
// Updated return type to remove null since we now throw an error instead
|
||||
export async function getCollectionTitle(input: GetCollectionTitleInput): Promise<string> {
|
||||
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);
|
||||
|
||||
// Throw error instead of returning null
|
||||
if (!collection) {
|
||||
throw new Error(`Collection with ID ${validated.assetId} not found`);
|
||||
}
|
||||
|
||||
// Throw error for permission failure instead of returning null
|
||||
if (collection.organizationId !== validated.organizationId) {
|
||||
throw new Error(
|
||||
`Access denied: Collection with ID ${validated.assetId} does not belong to the specified organization`
|
||||
);
|
||||
}
|
||||
|
||||
return collection.name;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export {
|
||||
getCollectionTitle,
|
||||
GetCollectionTitleInputSchema,
|
||||
type GetCollectionTitleInput,
|
||||
} from './get-collection-title';
|
|
@ -0,0 +1,40 @@
|
|||
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<typeof GetDashboardTitleInputSchema>;
|
||||
|
||||
// Updated return type to remove null since we now throw an error instead
|
||||
export async function getDashboardTitle(input: GetDashboardTitleInput): Promise<string> {
|
||||
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);
|
||||
|
||||
// Throw error instead of returning null
|
||||
if (!dashboard) {
|
||||
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) {
|
||||
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;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
export {
|
||||
getChatDashboardFiles,
|
||||
type DashboardFileContext,
|
||||
type DashboardFile,
|
||||
} from './dashboards';
|
||||
|
||||
export {
|
||||
getDashboardTitle,
|
||||
GetDashboardTitleInputSchema,
|
||||
type GetDashboardTitleInput,
|
||||
} from './get-dashboard-title';
|
|
@ -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';
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
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<typeof GetMetricTitleInputSchema>;
|
||||
|
||||
// Updated return type to remove null since we now throw an error instead
|
||||
export async function getMetricTitle(input: GetMetricTitleInput): Promise<string> {
|
||||
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);
|
||||
|
||||
// Throw error instead of returning null
|
||||
if (!metric) {
|
||||
throw new Error(`Metric with ID ${validated.assetId} not found`);
|
||||
}
|
||||
|
||||
// Throw error for permission failure instead of returning null
|
||||
if (!metric.publiclyAccessible && metric.organizationId !== validated.organizationId) {
|
||||
throw new Error(
|
||||
`Access denied: Metric with ID ${validated.assetId} is not publicly accessible and does not belong to the specified organization`
|
||||
);
|
||||
}
|
||||
|
||||
return metric.name;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export {
|
||||
getMetricTitle,
|
||||
GetMetricTitleInputSchema,
|
||||
type GetMetricTitleInput,
|
||||
} from './get-metric-title';
|
|
@ -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": {
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
export * from './requests';
|
||||
export * from './responses';
|
|
@ -0,0 +1,9 @@
|
|||
import { z } from 'zod';
|
||||
import { AssetTypeSchema } from '../assets/asset-types.types';
|
||||
|
||||
export const GetTitleRequestSchema = z.object({
|
||||
assetId: z.string().uuid(),
|
||||
assetType: AssetTypeSchema,
|
||||
});
|
||||
|
||||
export type GetTitleRequest = z.infer<typeof GetTitleRequestSchema>;
|
|
@ -0,0 +1,7 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const GetTitleResponseSchema = z.object({
|
||||
title: z.string(),
|
||||
});
|
||||
|
||||
export type GetTitleResponse = z.infer<typeof GetTitleResponseSchema>;
|
Loading…
Reference in New Issue