/** * Routes /api/views — R3.1.a database-view. * * Deux endpoints : * GET /api/views/table/:tableId — liste les vues d'une table avec cache Redis * GET /api/views/:viewId/data — donnees paginées d'une vue (filters/sort/group * appliques par Baserow via view_id param) * * Separation de /api/v1/tables/* voulue : les routes `tables` sont des metadata * generiques (CRUD rows/fields/views sans cache), les routes `views` ici sont * des endpoints specialises R3.1.a avec cache et pagination orientee "database * view" style Notion. * * Permissions mappees sur les scopes generiques du bridge : * - `database.tables.read` dans `acadenice_permissions[]` est traite comme * `read:tables` par le RBAC DocAdenice avant emission du JWT. On utilise * donc `requireScope('read:tables')` pour rester coherent avec le systeme * existant. De meme `database.rows.read` -> `read:tables` (meme scope * car les rows sont lues en contexte de table). */ import { Hono } from 'hono'; import type { Row } from '../domain/row.js'; import type { View } from '../domain/view.js'; import { getContainer } from '../lib/container.js'; import { errors } from '../lib/errors.js'; import { type AuthVariables, requireScope } from '../middleware/auth.js'; export const viewsRoutes = new Hono<{ Variables: AuthVariables }>(); // --------------------------------------------------------------------------- // Serialisation // --------------------------------------------------------------------------- function serializeView(v: View) { return { id: v.id, tableId: v.tableId, name: v.name, type: v.type, order: v.order, filters: v.filters, sortings: v.sortings, groupBys: v.groupBys, }; } function serializeRow(r: Row) { return { id: r.id, tableId: r.tableId, fields: r.fields, order: r.order, createdOn: r.createdOn?.toISOString() ?? null, updatedOn: r.updatedOn?.toISOString() ?? null, }; } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function parseIntParam(raw: string, label: string): number { const n = Number.parseInt(raw, 10); if (Number.isNaN(n) || n <= 0) { throw errors.validation([{ message: `${label} must be a positive integer` }]); } return n; } function parseIntQuery(url: URL, name: string, defaultVal: number, max?: number): number { const raw = url.searchParams.get(name); if (!raw) return defaultVal; const n = Number.parseInt(raw, 10); if (Number.isNaN(n) || n <= 0) { throw errors.validation([{ message: `${name} must be a positive integer` }]); } return max !== undefined ? Math.min(n, max) : n; } // --------------------------------------------------------------------------- // GET /api/views/table/:tableId — liste les vues d'une table (cached) // --------------------------------------------------------------------------- viewsRoutes.get('/table/:tableId', requireScope('read:tables'), async (c) => { const tableId = parseIntParam(c.req.param('tableId'), 'tableId'); const { repos, redis } = getContainer(); const views = await repos.views.listByTable(tableId, redis); return c.json({ data: views.map(serializeView), total: views.length }); }); // --------------------------------------------------------------------------- // GET /api/views/:viewId/data — donnees d'une vue (rows + pagination cached) // --------------------------------------------------------------------------- viewsRoutes.get('/:viewId/data', requireScope('read:tables'), async (c) => { const viewId = parseIntParam(c.req.param('viewId'), 'viewId'); const { repos, redis } = getContainer(); const url = new URL(c.req.url); const page = parseIntQuery(url, 'page', 1); const size = parseIntQuery(url, 'size', 100, 200); const search = url.searchParams.get('search') ?? undefined; // tableId est requis pour construire les Row instances avec le bon identifiant // de table. Baserow ne retourne pas le tableId dans la reponse listRows, donc // le caller doit le fournir. Cela evite un aller-retour supplementaire pour // aller chercher la vue puis deduire sa table. const tableIdRaw = url.searchParams.get('tableId'); if (!tableIdRaw) { throw errors.validation([{ message: 'tableId query param required' }]); } const tableId = parseIntParam(tableIdRaw, 'tableId'); const result = await repos.views.getViewData(viewId, tableId, { page, size, search, redis, }); return c.json({ data: result.items.map(serializeRow), total: result.meta.total, page: result.meta.page, size: result.meta.per_page, viewType: result.viewType, }); });