From 30b148694c843c662fb35fae3fdeb1fdc495d4b3 Mon Sep 17 00:00:00 2001 From: Corentin JOGUET Date: Fri, 8 May 2026 13:27:41 +0200 Subject: [PATCH] =?UTF-8?q?feat(bridge):=20allow=20slug=20->=20table=5Fid?= =?UTF-8?q?=20resolution=20via=20BASEROW=5FTABLE=5FIDS=20=E2=80=94=20Patch?= =?UTF-8?q?=20029?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Callers can now hit /api/v1/views/table/personne instead of /table/609 and the bridge resolves the slug to the numeric Baserow table ID using the BASEROW_TABLE_IDS env var (already present, just unused before). Resolution accepts either a digits-only string (preserved behavior) or a slug. Falls back to a 400 validation error when neither matches. Note: this fixes the *routing* layer only. The downstream Baserow API still requires a user JWT for some operations (notably GET /views/table) and returns 401 PERMISSION_DENIED with the current DB-token-based bridge config. That's a separate architectural concern — tracked for a later patch (bridge user-JWT exchange). Patch 029. --- bridge/src/lib/config.ts | 22 ++++++++++++++++++++++ bridge/src/routes/views.ts | 26 +++++++++++++++++++++++--- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/bridge/src/lib/config.ts b/bridge/src/lib/config.ts index bf7b5f3..0d25fb6 100644 --- a/bridge/src/lib/config.ts +++ b/bridge/src/lib/config.ts @@ -39,6 +39,27 @@ const ConfigSchema = z.object({ // global (XADD MAXLEN ~). Défaut 10 000 events. Les events plus anciens sont // purgés automatiquement par Redis (mode ~ = approximatif, plus performant). streamMaxLen: z.coerce.number().int().positive().default(10_000), + // Optional slug -> table_id map so callers can use human-friendly slugs + // (e.g. /api/v1/views/table/personne) instead of the numeric Baserow ID. + // Format: JSON object string like '{"personne":609,"formation":610}'. + baserowTableIds: z + .string() + .optional() + .transform((raw) => { + if (!raw) return {} as Record; + try { + const parsed = JSON.parse(raw) as Record; + const out: Record = {}; + for (const [k, v] of Object.entries(parsed)) { + if (typeof v === 'number' && Number.isInteger(v) && v > 0) { + out[k.toLowerCase()] = v; + } + } + return out; + } catch { + return {} as Record; + } + }), }); export type Config = z.infer; @@ -68,6 +89,7 @@ export function loadConfig(): Config { rateLimitMutationMax: process.env.RATE_LIMIT_MUTATION_MAX, rateLimitMutationWindow: process.env.RATE_LIMIT_MUTATION_WINDOW, streamMaxLen: process.env.STREAM_MAXLEN, + baserowTableIds: process.env.BASEROW_TABLE_IDS, }); if (!parsed.success) { diff --git a/bridge/src/routes/views.ts b/bridge/src/routes/views.ts index 5174ab1..4657318 100644 --- a/bridge/src/routes/views.ts +++ b/bridge/src/routes/views.ts @@ -82,6 +82,25 @@ function parseIntParam(raw: string, label: string): number { return n; } +/** + * Resolve a table param to a numeric Baserow ID. Accepts either a digits-only + * string (e.g. "609") or a slug (e.g. "personne") that maps via the + * BASEROW_TABLE_IDS env (parsed in config). Throws a 400 validation error if + * neither resolves. + */ +function resolveTableId(raw: string, container: ReturnType): number { + if (/^\d+$/.test(raw)) { + const n = Number.parseInt(raw, 10); + if (n > 0) return n; + } + const map = container.config.baserowTableIds ?? {}; + const id = map[raw.toLowerCase()]; + if (typeof id === 'number' && id > 0) return id; + throw errors.validation([ + { message: `tableId must be a positive integer or a known slug (got: ${raw})` }, + ]); +} + function parseIntQuery(url: URL, name: string, defaultVal: number, max?: number): number { const raw = url.searchParams.get(name); if (!raw) return defaultVal; @@ -97,8 +116,9 @@ function parseIntQuery(url: URL, name: string, defaultVal: number, max?: number) // --------------------------------------------------------------------------- viewsRoutes.get('/table/:tableId', requireScope('read:tables'), async (c) => { - const tableId = parseIntParam(c.req.param('tableId'), 'tableId'); - const { repos, redis } = getContainer(); + const container = getContainer(); + const tableId = resolveTableId(c.req.param('tableId'), container); + const { repos, redis } = container; const views = await repos.views.listByTable(tableId, redis); @@ -126,7 +146,7 @@ viewsRoutes.get('/:viewId/data', requireScope('read:tables'), async (c) => { if (!tableIdRaw) { throw errors.validation([{ message: 'tableId query param required' }]); } - const tableId = parseIntParam(tableIdRaw, 'tableId'); + const tableId = resolveTableId(tableIdRaw, getContainer()); const result = await repos.views.getViewData(viewId, tableId, { page,