mirror of https://github.com/buster-so/buster.git
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 <nate@buster.so>
This commit is contained in:
parent
446bfc6a8e
commit
5b40205275
|
@ -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,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;
|
|
@ -0,0 +1,3 @@
|
||||||
|
import app from './GET';
|
||||||
|
|
||||||
|
export default app;
|
|
@ -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<typeof GetCollectionTitleInputSchema>;
|
||||||
|
|
||||||
|
export async function getCollectionTitle(input: GetCollectionTitleInput): Promise<string | null> {
|
||||||
|
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;
|
||||||
|
}
|
|
@ -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<typeof GetDashboardTitleInputSchema>;
|
||||||
|
|
||||||
|
export async function getDashboardTitle(input: GetDashboardTitleInput): Promise<string | null> {
|
||||||
|
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;
|
||||||
|
}
|
|
@ -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<typeof GetMetricTitleInputSchema>;
|
||||||
|
|
||||||
|
export async function getMetricTitle(input: GetMetricTitleInput): Promise<string | null> {
|
||||||
|
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;
|
||||||
|
}
|
|
@ -11,3 +11,21 @@ export {
|
||||||
type DashboardFileContext,
|
type DashboardFileContext,
|
||||||
type DashboardFile,
|
type DashboardFile,
|
||||||
} from './dashboards';
|
} 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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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,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<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