feat: implement reports API endpoints for BUS-1605

- Add Report schema extending ReportElements with metadata fields
- Create GET /api/v2/reports endpoint with pagination
- Create PUT /api/v2/reports/:id endpoint for updates
- Use stubbed data responses following established patterns
- Follow modular route structure and Zod-first validation

Co-Authored-By: nate@buster.so <nate@buster.so>
This commit is contained in:
Devin AI 2025-08-03 00:38:50 +00:00
parent ef8b105d64
commit abc801f10b
8 changed files with 222 additions and 1 deletions

View File

@ -5,6 +5,7 @@ import chatsRoutes from './chats';
import dictionariesRoutes from './dictionaries';
import electricShapeRoutes from './electric-shape';
import organizationRoutes from './organization';
import reportsRoutes from './reports';
import securityRoutes from './security';
import slackRoutes from './slack';
import supportRoutes from './support';
@ -23,6 +24,7 @@ const app = new Hono()
.route('/organizations', organizationRoutes)
.route('/dictionaries', dictionariesRoutes)
.route('/title', titleRoutes)
.route('/temp', tempRoutes);
.route('/temp', tempRoutes)
.route('/reports', reportsRoutes);
export default app;

View File

@ -0,0 +1,101 @@
import type { User } from '@buster/database';
import type {
GetReportsListRequest,
GetReportsListResponse
} from '@buster/server-shared/reports';
import { GetReportsListRequestSchema } from '@buster/server-shared/reports';
import { zValidator } from '@hono/zod-validator';
import { Hono } from 'hono';
async function getReportsListHandler(
request: GetReportsListRequest,
user: User
): Promise<GetReportsListResponse> {
const stubbedReports = [
{
id: 'report-1',
name: 'Sales Analysis Q4',
file_name: 'sales_analysis_q4.md',
description: 'Quarterly sales performance analysis',
created_by: user.id,
created_at: '2024-01-15T10:00:00Z',
updated_at: '2024-01-20T14:30:00Z',
deleted_at: null,
publicly_accessible: false,
content: [
{
type: 'h1' as const,
children: [{ text: 'Sales Analysis Q4' }]
},
{
type: 'p' as const,
children: [{ text: 'This report analyzes our Q4 sales performance.' }]
}
]
},
{
id: 'report-2',
name: 'Customer Metrics Dashboard',
file_name: 'customer_metrics.md',
description: 'Key customer engagement metrics',
created_by: user.id,
created_at: '2024-01-10T09:00:00Z',
updated_at: '2024-01-18T16:45:00Z',
deleted_at: null,
publicly_accessible: true,
content: [
{
type: 'h1' as const,
children: [{ text: 'Customer Metrics' }]
}
]
},
{
id: 'report-3',
name: 'Marketing Campaign Results',
file_name: 'marketing_campaign_results.md',
description: 'Analysis of recent marketing campaigns',
created_by: user.id,
created_at: '2024-01-05T08:30:00Z',
updated_at: '2024-01-12T11:15:00Z',
deleted_at: null,
publicly_accessible: false,
content: [
{
type: 'h1' as const,
children: [{ text: 'Marketing Campaign Results' }]
},
{
type: 'p' as const,
children: [{ text: 'Overview of our recent marketing initiatives and their performance.' }]
}
]
}
];
const { page_token, page_size } = request;
const startIndex = page_token * page_size;
const endIndex = startIndex + page_size;
const paginatedReports = stubbedReports.slice(startIndex, endIndex);
return {
data: paginatedReports,
pagination: {
page: page_token,
page_size: page_size,
total: stubbedReports.length,
total_pages: Math.ceil(stubbedReports.length / page_size)
}
};
}
const app = new Hono()
.get('/', zValidator('query', GetReportsListRequestSchema), async (c) => {
const request = c.req.valid('query');
const user = c.get('busterUser');
const response = await getReportsListHandler(request, user);
return c.json(response);
});
export default app;

View File

@ -0,0 +1,61 @@
import type { User } from '@buster/database';
import type {
UpdateReportRequest,
UpdateReportResponse
} from '@buster/server-shared/reports';
import { UpdateReportRequestSchema } from '@buster/server-shared/reports';
import { zValidator } from '@hono/zod-validator';
import { Hono } from 'hono';
import { HTTPException } from 'hono/http-exception';
async function updateReportHandler(
reportId: string,
request: UpdateReportRequest,
user: User
): Promise<UpdateReportResponse> {
const existingReport = {
id: reportId,
name: 'Sales Analysis Q4',
file_name: 'sales_analysis_q4.md',
description: 'Quarterly sales performance analysis',
created_by: user.id,
created_at: '2024-01-15T10:00:00Z',
updated_at: '2024-01-20T14:30:00Z',
deleted_at: null,
publicly_accessible: false,
content: [
{
type: 'h1' as const,
children: [{ text: 'Sales Analysis Q4' }]
},
{
type: 'p' as const,
children: [{ text: 'This report analyzes our Q4 sales performance.' }]
}
]
};
if (!reportId || reportId === 'invalid') {
throw new HTTPException(404, { message: 'Report not found' });
}
const updatedReport = {
...existingReport,
...request,
updated_at: new Date().toISOString()
};
return updatedReport;
}
const app = new Hono()
.put('/', zValidator('json', UpdateReportRequestSchema), async (c) => {
const reportId = c.req.param('id');
const request = c.req.valid('json');
const user = c.get('busterUser');
const response = await updateReportHandler(reportId, request, user);
return c.json(response);
});
export default app;

View File

@ -0,0 +1,11 @@
import { Hono } from 'hono';
import { requireAuth } from '../../../middleware/auth';
import GET from './GET';
import PUT_ID from './[id]';
const app = new Hono()
.use('*', requireAuth)
.route('/', GET)
.route('/:id', PUT_ID);
export default app;

View File

@ -1 +1,4 @@
export * from './report-elements';
export * from './reports.types';
export * from './requests';
export * from './responses';

View File

@ -0,0 +1,17 @@
import { z } from 'zod';
import { ReportElementsSchema } from './report-elements';
export const ReportSchema = z.object({
id: z.string(),
name: z.string(),
file_name: z.string(),
description: z.string(),
created_by: z.string(),
created_at: z.string(),
updated_at: z.string(),
deleted_at: z.string().nullable(),
publicly_accessible: z.boolean(),
content: ReportElementsSchema,
});
export type Report = z.infer<typeof ReportSchema>;

View File

@ -0,0 +1,17 @@
import { z } from 'zod';
import { ReportElementsSchema } from './report-elements';
export const GetReportsListRequestSchema = z.object({
page_token: z.number().optional().default(0),
page_size: z.number().optional().default(50),
});
export const UpdateReportRequestSchema = z.object({
name: z.string().optional(),
description: z.string().optional(),
publicly_accessible: z.boolean().optional(),
content: ReportElementsSchema.optional(),
});
export type GetReportsListRequest = z.infer<typeof GetReportsListRequestSchema>;
export type UpdateReportRequest = z.infer<typeof UpdateReportRequestSchema>;

View File

@ -0,0 +1,9 @@
import { z } from 'zod';
import { PaginatedResponseSchema } from '../type-utilities/pagination';
import { ReportSchema } from './reports.types';
export const GetReportsListResponseSchema = PaginatedResponseSchema(ReportSchema);
export const UpdateReportResponseSchema = ReportSchema;
export type GetReportsListResponse = z.infer<typeof GetReportsListResponseSchema>;
export type UpdateReportResponse = z.infer<typeof UpdateReportResponseSchema>;