feat: add saved views — database table, CRUD API, migration

- New views table (id, name, filters jsonb, sort_key, is_public, creator_id)
- GET/POST/PATCH/DELETE /views endpoints
- Register views router in server

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Gjermund Høsøien Wiggen
2026-06-09 11:10:25 +02:00
parent 000e97e1bd
commit aa90b88991
10 changed files with 1615 additions and 154 deletions

View File

@@ -112,3 +112,14 @@ export const customFieldValues = pgTable('custom_field_values', {
ticketIdIdx: index('custom_field_values_ticket_id_idx').on(table.ticket_id),
cfIdIdx: index('custom_field_values_custom_field_id_idx').on(table.custom_field_id),
}));
export const views = pgTable('views', {
id: uuid('id').primaryKey().defaultRandom(),
name: text('name').notNull(),
filters: jsonb('filters').notNull().default('[]'),
sort_key: text('sort_key').default('updated'),
columns: jsonb('columns').default('[]'),
is_public: boolean('is_public').default(false),
creator_id: uuid('creator_id').references(() => users.id),
created_at: timestamp('created_at', { withTimezone: true }).defaultNow(),
});

View File

@@ -12,6 +12,7 @@ import { createCustomFieldsRouter } from './routes/custom-fields.ts';
import { createLifecyclesRouter } from './routes/lifecycles.ts';
import { createUsersRouter } from './routes/users.ts';
import { createTemplatesRouter } from './routes/templates.ts';
import { createViewsRouter } from './routes/views.ts';
let db: Db | null = null;
@@ -35,6 +36,7 @@ app.route('/custom-fields', createCustomFieldsRouter(getDb()));
app.route('/lifecycles', createLifecyclesRouter(getDb()));
app.route('/users', createUsersRouter(getDb()));
app.route('/templates', createTemplatesRouter(getDb()));
app.route('/views', createViewsRouter(getDb()));
export default app;
export { app };

84
src/routes/views.ts Normal file
View File

@@ -0,0 +1,84 @@
import { Hono } from 'hono';
import { HTTPException } from 'hono/http-exception';
import { asc, eq } from 'drizzle-orm';
import type { Db } from '../db/index.ts';
import { views } from '../db/schema.ts';
export function createViewsRouter(db: Db): Hono {
const router = new Hono();
router.get('/', async (c) => {
const result = await db.query.views.findMany({
orderBy: asc(views.name),
});
return c.json(result);
});
router.post('/', async (c) => {
const body = await c.req.json();
const name = String(body.name ?? '').trim();
if (!name) {
throw new HTTPException(400, { message: 'name is required' });
}
const [view] = await db.insert(views).values({
name,
filters: body.filters ?? [],
sort_key: body.sort_key ?? 'updated',
columns: body.columns ?? [],
is_public: body.is_public ?? false,
creator_id: body.creator_id || null,
}).returning();
if (!view) {
throw new HTTPException(500, { message: 'Failed to create view' });
}
return c.json(view, 201);
});
router.patch('/:id', async (c) => {
const id = c.req.param('id');
const body = await c.req.json();
const existing = await db.query.views.findFirst({
where: eq(views.id, id),
});
if (!existing) {
throw new HTTPException(404, { message: 'View not found' });
}
const updateData: Partial<typeof views.$inferInsert> = {};
if (body.name !== undefined) updateData.name = String(body.name).trim();
if (body.filters !== undefined) updateData.filters = body.filters;
if (body.sort_key !== undefined) updateData.sort_key = body.sort_key;
if (body.columns !== undefined) updateData.columns = body.columns;
if (body.is_public !== undefined) updateData.is_public = body.is_public;
const [updated] = await db.update(views)
.set(updateData)
.where(eq(views.id, id))
.returning();
return c.json(updated);
});
router.delete('/:id', async (c) => {
const id = c.req.param('id');
const existing = await db.query.views.findFirst({
where: eq(views.id, id),
});
if (!existing) {
throw new HTTPException(404, { message: 'View not found' });
}
await db.delete(views).where(eq(views.id, id));
return c.json({ ok: true });
});
return router;
}