mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
feat(capabilities): advertise the FHIR REST face and share-link surface
Add a single canonical FHIR_REST_RESOURCE_TYPES catalogue (plus the $everything operation and honoured search params) to fhir/rest.ts; the metadata CapabilityStatement and the share-link resource-type enum now derive from it so the advertised set can never drift from what is routed. Extend GET /api/meta/capabilities with the FHIR REST descriptor (restBaseUrl, readScope, resourceTypes, operations, searchParams) and a clinician-share descriptor (supported, maxDays, resourceTypes, scopeable sections) — every list sourced from its canonical registry, guarded by the existing drift assertions. Grow the OpenAPI CapabilitiesResponse to match. Fill the clinician test-matrix gap: assert the clinician view never requests a KVNR / identifier opt-in from the aggregator (default-OFF by construction) and resolves a rolling window without reaching data older than the frozen start.
This commit is contained in:
1 parent
93694cacd0
commit
d4686ea2a6
7 files changed
+240
-24
No files matched your search
@@ -9,14 +9,12 @@
|
||||
*/
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { FHIR_READ_SCOPE, fhirJsonResponse } from "@/lib/fhir/rest";
|
||||
|
||||
const READ_RESOURCES = [
|
||||
"Patient",
|
||||
"Observation",
|
||||
"MedicationStatement",
|
||||
"MedicationAdministration",
|
||||
] as const;
|
||||
import {
|
||||
FHIR_READ_SCOPE,
|
||||
FHIR_REST_RESOURCE_TYPES,
|
||||
FHIR_SEARCH_PARAMS,
|
||||
fhirJsonResponse,
|
||||
} from "@/lib/fhir/rest";
|
||||
|
||||
export const GET = apiHandler(async () => {
|
||||
await requireAuth(FHIR_READ_SCOPE);
|
||||
@@ -35,13 +33,13 @@ export const GET = apiHandler(async () => {
|
||||
documentation:
|
||||
"Read-only access to the authenticated user's own health record.",
|
||||
resource: [
|
||||
...READ_RESOURCES.map((type) => ({
|
||||
...FHIR_REST_RESOURCE_TYPES.map((type) => ({
|
||||
type,
|
||||
interaction: [{ code: "read" }, { code: "search-type" }],
|
||||
searchParam: [
|
||||
{ name: "_count", type: "number" },
|
||||
{ name: "_offset", type: "number" },
|
||||
],
|
||||
searchParam: FHIR_SEARCH_PARAMS.map((name) => ({
|
||||
name,
|
||||
type: "number",
|
||||
})),
|
||||
})),
|
||||
{
|
||||
type: "Patient",
|
||||
|
||||
@@ -52,6 +52,17 @@ import {
|
||||
SNOMED_SYSTEM,
|
||||
GERMAN_ATC_DEFAULT_LOCALES,
|
||||
} from "@/lib/fhir/build-bundle";
|
||||
import {
|
||||
FHIR_READ_SCOPE,
|
||||
FHIR_REST_RESOURCE_TYPES,
|
||||
FHIR_EVERYTHING_OPERATION,
|
||||
FHIR_SEARCH_PARAMS,
|
||||
} from "@/lib/fhir/rest";
|
||||
import {
|
||||
SHARE_LINK_MAX_DAYS,
|
||||
SHARE_LINK_RESOURCE_TYPES,
|
||||
} from "@/lib/validations/clinician-share-link";
|
||||
import { exportSectionsSchema } from "@/lib/validations/health-record-export";
|
||||
|
||||
const SESSION_OK = {
|
||||
session: { id: "sess-1", expiresAt: new Date(Date.now() + 3_600_000) },
|
||||
@@ -75,6 +86,17 @@ type CapabilitiesBody = {
|
||||
atcSystem: string;
|
||||
snomedRoute: string;
|
||||
germanAtcDefaultLocales: string[];
|
||||
restBaseUrl: string;
|
||||
readScope: string;
|
||||
resourceTypes: string[];
|
||||
operations: string[];
|
||||
searchParams: string[];
|
||||
};
|
||||
share: {
|
||||
supported: boolean;
|
||||
maxDays: number;
|
||||
resourceTypes: string[];
|
||||
sections: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -139,6 +161,33 @@ describe("GET /api/meta/capabilities — drift guards", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("fhir REST descriptor mirrors the canonical rest.ts constants", async () => {
|
||||
const res = await call();
|
||||
const body = (await res.json()) as CapabilitiesBody;
|
||||
expect(body.data.fhir.restBaseUrl).toBe("/api/fhir");
|
||||
expect(body.data.fhir.readScope).toBe(FHIR_READ_SCOPE);
|
||||
expect(body.data.fhir.resourceTypes).toEqual([
|
||||
...FHIR_REST_RESOURCE_TYPES,
|
||||
]);
|
||||
expect(body.data.fhir.operations).toEqual([FHIR_EVERYTHING_OPERATION]);
|
||||
expect(body.data.fhir.searchParams).toEqual([...FHIR_SEARCH_PARAMS]);
|
||||
});
|
||||
|
||||
it("share descriptor mirrors the canonical share-link + section sources", async () => {
|
||||
const res = await call();
|
||||
const body = (await res.json()) as CapabilitiesBody;
|
||||
expect(body.data.share.supported).toBe(true);
|
||||
expect(body.data.share.maxDays).toBe(SHARE_LINK_MAX_DAYS);
|
||||
// The shareable resource types are exactly the REST catalogue, by
|
||||
// construction — the share can never scope to an unrouted type.
|
||||
expect(body.data.share.resourceTypes).toEqual([
|
||||
...SHARE_LINK_RESOURCE_TYPES,
|
||||
]);
|
||||
expect([...body.data.share.sections].sort()).toEqual(
|
||||
Object.keys(exportSectionsSchema.shape).sort(),
|
||||
);
|
||||
});
|
||||
|
||||
it("quantityTypes are sourced from the HealthKit ingest mapping", async () => {
|
||||
const res = await call();
|
||||
const body = (await res.json()) as CapabilitiesBody;
|
||||
|
||||
@@ -40,6 +40,17 @@ import {
|
||||
SNOMED_SYSTEM,
|
||||
GERMAN_ATC_DEFAULT_LOCALES,
|
||||
} from "@/lib/fhir/build-bundle";
|
||||
import {
|
||||
FHIR_READ_SCOPE,
|
||||
FHIR_REST_RESOURCE_TYPES,
|
||||
FHIR_EVERYTHING_OPERATION,
|
||||
FHIR_SEARCH_PARAMS,
|
||||
} from "@/lib/fhir/rest";
|
||||
import {
|
||||
SHARE_LINK_MAX_DAYS,
|
||||
SHARE_LINK_RESOURCE_TYPES,
|
||||
} from "@/lib/validations/clinician-share-link";
|
||||
import { exportSectionsSchema } from "@/lib/validations/health-record-export";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -96,6 +107,23 @@ export const GET = apiHandler(async () => {
|
||||
atcSystem: ATC_SYSTEM,
|
||||
snomedRoute: SNOMED_SYSTEM,
|
||||
germanAtcDefaultLocales: GERMAN_ATC_DEFAULT_LOCALES,
|
||||
// Read-only REST face (v1.11): the resource types served, the
|
||||
// whole-record operation, the honoured search params and the narrow
|
||||
// Bearer scope. All sourced from the canonical `rest.ts` constants.
|
||||
restBaseUrl: "/api/fhir",
|
||||
readScope: FHIR_READ_SCOPE,
|
||||
resourceTypes: FHIR_REST_RESOURCE_TYPES,
|
||||
operations: [FHIR_EVERYTHING_OPERATION],
|
||||
searchParams: FHIR_SEARCH_PARAMS,
|
||||
},
|
||||
// Clinician share-link surface (v1.11): a scoped, time-boxed, revocable
|
||||
// read-only link to the owner's record. Descriptor sourced from the
|
||||
// canonical share validation + export-section constants.
|
||||
share: {
|
||||
supported: true,
|
||||
maxDays: SHARE_LINK_MAX_DAYS,
|
||||
resourceTypes: SHARE_LINK_RESOURCE_TYPES,
|
||||
sections: Object.keys(exportSectionsSchema.shape),
|
||||
},
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user