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 securityRoutes from './security';
|
||||||
import slackRoutes from './slack';
|
import slackRoutes from './slack';
|
||||||
import supportRoutes from './support';
|
import supportRoutes from './support';
|
||||||
|
import titleRoutes from './title';
|
||||||
import userRoutes from './users';
|
import userRoutes from './users';
|
||||||
|
|
||||||
const app = new Hono()
|
const app = new Hono()
|
||||||
|
@ -19,6 +20,7 @@ const app = new Hono()
|
||||||
.route('/support', supportRoutes)
|
.route('/support', supportRoutes)
|
||||||
.route('/security', securityRoutes)
|
.route('/security', securityRoutes)
|
||||||
.route('/organizations', organizationRoutes)
|
.route('/organizations', organizationRoutes)
|
||||||
.route('/dictionaries', dictionariesRoutes);
|
.route('/dictionaries', dictionariesRoutes)
|
||||||
|
.route('/title', titleRoutes);
|
||||||
|
|
||||||
export default app;
|
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,
|
type GenerateAssetMessagesInput,
|
||||||
} from './assets';
|
} from './assets';
|
||||||
|
|
||||||
export {
|
|
||||||
getChatDashboardFiles,
|
|
||||||
type DashboardFileContext,
|
|
||||||
type DashboardFile,
|
|
||||||
} from './dashboards';
|
|
||||||
|
|
||||||
export type { DatabaseAssetType } from './assets';
|
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 CreateMessageInput,
|
||||||
type Chat,
|
type Chat,
|
||||||
} from './chats';
|
} 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 './metadata';
|
||||||
export * from './chats';
|
export * from './chats';
|
||||||
export * from './organizations';
|
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": {
|
"./dictionary": {
|
||||||
"types": "./dist/dictionary/index.d.ts",
|
"types": "./dist/dictionary/index.d.ts",
|
||||||
"default": "./dist/dictionary/index.js"
|
"default": "./dist/dictionary/index.js"
|
||||||
|
},
|
||||||
|
"./title": {
|
||||||
|
"types": "./dist/title/index.d.ts",
|
||||||
|
"default": "./dist/title/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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