Some checks are pending
CI / Lint bridge (Biome) (push) Waiting to run
CI / Type-check bridge (push) Blocked by required conditions
CI / Tests unit bridge (push) Blocked by required conditions
CI / Tests integration bridge (push) Blocked by required conditions
CI / Security scan (push) Waiting to run
CI / Docker build + healthcheck (push) Blocked by required conditions
Two new endpoints under /api/v1/views:
GET /api/v1/views/table/:tableId — list views for a table with Redis
cache TTL 60s. Returns full view metadata (filters, sortings, groupBys,
order). Cache invalidated by view.created|updated|deleted webhook events.
GET /api/v1/views/:viewId/data — paginated rows of a view applying
Baserow view filters/sorts via ?view_id= query param. Redis cache TTL 30s
keyed by (viewId, page, size, search). Requires tableId query param.
Domain: View entity extended with order, filters, sortings, groupBys.
Adapter: BaserowListOptions gains viewId param (forwards to Baserow ?view_id=).
Webhook: baserow-handler extended for view.* events — invalidates views:table
and views:data cache keys. rows.* events now also invalidate views:data:*.
Tests: +44 tests (336 total, was 292). Routes 20, repo 20, webhook 4.
Coverage: view.ts 100%, routes/views.ts 100% lines, baserow-handler 100%.
Co-Authored-By: Amelia (bmad-bmm-dev BYAN) <noreply@anthropic.com>
131 lines
4.7 KiB
TypeScript
131 lines
4.7 KiB
TypeScript
/**
|
|
* 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,
|
|
});
|
|
});
|