From 102e1e161687c562df9bd5be4e63a7f172e1ae32 Mon Sep 17 00:00:00 2001 From: Simon Axelsson Date: Mon, 7 Sep 2026 18:06:36 +0200 Subject: [PATCH] feat: expose tenant-safe dataset registry HTTP surface --- app/api/0pt/datasets/route.ts | 91 +++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 app/api/0pt/datasets/route.ts diff --git a/app/api/0pt/datasets/route.ts b/app/api/0pt/datasets/route.ts new file mode 100644 index 0000000..2ad412b --- /dev/null +++ b/app/api/0pt/datasets/route.ts @@ -0,0 +1,91 @@ +import { NextRequest, NextResponse } from "next/server"; + +import { assertTenantMatchesVerified } from "@/src/http/tenant-guard"; +import { siaxErrorResponse } from "@/src/http/siax-error-response"; +import { SiaxError } from "@/src/platform/errors"; +import { datasetService } from "@/src/services/datasets"; +import type { DatasetStatus, DatasetType } from "@/src/types/dataset"; + +const DATASET_TYPES = new Set(["raw", "structured", "enriched", "curated", "feature", "derived"]); +const DATASET_STATUSES = new Set(["active", "archived", "deprecated"]); + +function actorId(request: NextRequest): string { + return request.headers.get("x-0pt-verified-sub") ?? "local-dev"; +} + +function stringField(body: Record, field: string): string { + const value = body[field]; + if (typeof value !== "string" || !value.trim()) { + throw new SiaxError("INVALID_REQUEST", "0pt.dataset", `${field} is required`); + } + return value.trim(); +} + +export async function POST(request: NextRequest) { + try { + const raw = await request.json(); + if (!raw || typeof raw !== "object" || Array.isArray(raw)) { + throw new SiaxError("INVALID_REQUEST", "0pt.dataset", "JSON object body is required"); + } + const body = raw as Record; + const tenantId = stringField(body, "tenant_id"); + const guard = assertTenantMatchesVerified(request, tenantId); + if (guard) return guard; + + const datasetType = stringField(body, "dataset_type") as DatasetType; + if (!DATASET_TYPES.has(datasetType)) { + throw new SiaxError("INVALID_REQUEST", "0pt.dataset", "unsupported dataset_type"); + } + const status = body.status === undefined ? undefined : String(body.status) as DatasetStatus; + if (status !== undefined && !DATASET_STATUSES.has(status)) { + throw new SiaxError("INVALID_REQUEST", "0pt.dataset", "unsupported status"); + } + + const dataset = await datasetService.registerDataset({ + dataset_id: stringField(body, "dataset_id"), + tenant_id: tenantId, + project_id: stringField(body, "project_id"), + name: stringField(body, "name"), + dataset_type: datasetType, + ...(status ? { status } : {}), + ...(typeof body.description === "string" && body.description.trim() + ? { description: body.description.trim() } + : {}), + ...(typeof body.correlation_id === "string" && body.correlation_id.trim() + ? { correlation_id: body.correlation_id.trim() } + : {}), + created_by: actorId(request), + }); + return NextResponse.json(dataset, { status: 201 }); + } catch (error: unknown) { + return siaxErrorResponse(error); + } +} + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const tenantId = searchParams.get("tenant_id")?.trim(); + if (!tenantId) throw new SiaxError("INVALID_REQUEST", "0pt.dataset", "tenant_id is required"); + const guard = assertTenantMatchesVerified(request, tenantId); + if (guard) return guard; + + const typeRaw = searchParams.get("dataset_type") ?? undefined; + const statusRaw = searchParams.get("status") ?? undefined; + if (typeRaw && !DATASET_TYPES.has(typeRaw as DatasetType)) { + throw new SiaxError("INVALID_REQUEST", "0pt.dataset", "unsupported dataset_type"); + } + if (statusRaw && !DATASET_STATUSES.has(statusRaw as DatasetStatus)) { + throw new SiaxError("INVALID_REQUEST", "0pt.dataset", "unsupported status"); + } + + return NextResponse.json(await datasetService.listDatasets({ + tenant_id: tenantId, + project_id: searchParams.get("project_id") ?? undefined, + dataset_type: typeRaw as DatasetType | undefined, + status: statusRaw as DatasetStatus | undefined, + })); + } catch (error: unknown) { + return siaxErrorResponse(error); + } +}