feat(bridge): allow slug -> table_id resolution via BASEROW_TABLE_IDS — Patch 029
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.
This commit is contained in:
parent
3ea5f822f1
commit
30b148694c
2 changed files with 45 additions and 3 deletions
|
|
@ -39,6 +39,27 @@ const ConfigSchema = z.object({
|
||||||
// global (XADD MAXLEN ~). Défaut 10 000 events. Les events plus anciens sont
|
// global (XADD MAXLEN ~). Défaut 10 000 events. Les events plus anciens sont
|
||||||
// purgés automatiquement par Redis (mode ~ = approximatif, plus performant).
|
// purgés automatiquement par Redis (mode ~ = approximatif, plus performant).
|
||||||
streamMaxLen: z.coerce.number().int().positive().default(10_000),
|
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<string, number>;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
||||||
|
const out: Record<string, number> = {};
|
||||||
|
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<string, number>;
|
||||||
|
}
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Config = z.infer<typeof ConfigSchema>;
|
export type Config = z.infer<typeof ConfigSchema>;
|
||||||
|
|
@ -68,6 +89,7 @@ export function loadConfig(): Config {
|
||||||
rateLimitMutationMax: process.env.RATE_LIMIT_MUTATION_MAX,
|
rateLimitMutationMax: process.env.RATE_LIMIT_MUTATION_MAX,
|
||||||
rateLimitMutationWindow: process.env.RATE_LIMIT_MUTATION_WINDOW,
|
rateLimitMutationWindow: process.env.RATE_LIMIT_MUTATION_WINDOW,
|
||||||
streamMaxLen: process.env.STREAM_MAXLEN,
|
streamMaxLen: process.env.STREAM_MAXLEN,
|
||||||
|
baserowTableIds: process.env.BASEROW_TABLE_IDS,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,25 @@ function parseIntParam(raw: string, label: string): number {
|
||||||
return n;
|
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<typeof getContainer>): 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 {
|
function parseIntQuery(url: URL, name: string, defaultVal: number, max?: number): number {
|
||||||
const raw = url.searchParams.get(name);
|
const raw = url.searchParams.get(name);
|
||||||
if (!raw) return defaultVal;
|
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) => {
|
viewsRoutes.get('/table/:tableId', requireScope('read:tables'), async (c) => {
|
||||||
const tableId = parseIntParam(c.req.param('tableId'), 'tableId');
|
const container = getContainer();
|
||||||
const { repos, redis } = getContainer();
|
const tableId = resolveTableId(c.req.param('tableId'), container);
|
||||||
|
const { repos, redis } = container;
|
||||||
|
|
||||||
const views = await repos.views.listByTable(tableId, redis);
|
const views = await repos.views.listByTable(tableId, redis);
|
||||||
|
|
||||||
|
|
@ -126,7 +146,7 @@ viewsRoutes.get('/:viewId/data', requireScope('read:tables'), async (c) => {
|
||||||
if (!tableIdRaw) {
|
if (!tableIdRaw) {
|
||||||
throw errors.validation([{ message: 'tableId query param required' }]);
|
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, {
|
const result = await repos.views.getViewData(viewId, tableId, {
|
||||||
page,
|
page,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue