All files / src/app/api/compliance route.ts

96.66% Statements 29/30
88.88% Branches 24/27
100% Functions 4/4
100% Lines 29/29

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167          2x           2x                 9x       24x 24x   24x 24x 24x     24x 18x                                                                                                                 18x 18x 11x 11x     9x 10x   11x 9x 9x 9x 9x                       18x 18x 4x           4x 2x       18x               6x                                                       6x    
import { NextResponse } from "next/server";
import { runQuery } from "@/lib/neo4j";
import { edcClient } from "@/lib/edc";
import { requireAuth, isAuthError } from "@/lib/auth-guard";
 
export const dynamic = "force-dynamic";
 
/**
 * Approved fictional participants — DID slug → display info.
 * Used as fallback when no Participant nodes exist in Neo4j.
 */
const SLUG_DISPLAY: Record<string, { name: string; type: string }> = {
  "alpha-klinik": { name: "AlphaKlinik Berlin", type: "DATA_HOLDER" },
  pharmaco: { name: "PharmaCo Research AG", type: "DATA_USER" },
  medreg: { name: "MedReg DE", type: "HDAB" },
  lmc: { name: "Limburg Medical Centre", type: "DATA_HOLDER" },
  irs: { name: "Institut de Recherche Santé", type: "HDAB" },
};
 
function didSlug(did: string): string {
  return decodeURIComponent(did).split(":").pop()?.toLowerCase() ?? "";
}
 
export async function GET(req: Request) {
  const auth = await requireAuth();
  Iif (isAuthError(auth)) return auth;
 
  const { searchParams } = new URL(req.url);
  const consumerId = searchParams.get("consumerId");
  const datasetId = searchParams.get("datasetId");
 
  // List mode: return participants, datasets, and the full compliance matrix
  if (!consumerId || !datasetId) {
    const [consumers, datasets, matrixRows] = await Promise.all([
      // Show ALL participants (not just those with access applications)
      runQuery<{ id: string; name: string; type: string }>(
        `MATCH (p:Participant)
         WHERE p.name IS NOT NULL AND p.name <> ''
         RETURN DISTINCT
                coalesce(p.participantId, p.id) AS id,
                p.name                          AS name,
                p.participantType               AS type
         ORDER BY p.name`,
      ),
      // Show ALL datasets (not just those with HDAB approvals)
      runQuery<{ id: string; title: string }>(
        `MATCH (ds:HealthDataset)
         WHERE ds.title IS NOT NULL OR ds.name IS NOT NULL
         RETURN DISTINCT
                coalesce(ds.id, ds.datasetId)   AS id,
                coalesce(ds.title, ds.name)     AS title
         ORDER BY title`,
      ),
      // Compliance matrix: for every participant, check what chain elements exist
      runQuery<{
        consumerId: string;
        consumerName: string;
        consumerType: string;
        hasApplication: boolean;
        applicationStatus: string | null;
        hasApproval: boolean;
        approvalStatus: string | null;
        datasetId: string | null;
        datasetTitle: string | null;
        hasContract: boolean;
        ehdsArticle: string | null;
      }>(
        `MATCH (p:Participant)
         WHERE p.name IS NOT NULL AND p.name <> ''
         WITH DISTINCT p
         OPTIONAL MATCH (p)-[:SUBMITTED]->(app:AccessApplication)
         OPTIONAL MATCH (approval:HDABApproval)-[:APPROVES]->(app)
         OPTIONAL MATCH (approval)-[:GRANTS_ACCESS_TO]->(ds:HealthDataset)
         OPTIONAL MATCH (contract:Contract)-[:GOVERNS]->(dp:DataProduct)-[:DESCRIBED_BY]->(ds)
         RETURN coalesce(p.participantId, p.id) AS consumerId,
                p.name                          AS consumerName,
                p.participantType               AS consumerType,
                app IS NOT NULL                 AS hasApplication,
                app.status                      AS applicationStatus,
                approval IS NOT NULL            AS hasApproval,
                approval.status                 AS approvalStatus,
                coalesce(ds.id, ds.datasetId)   AS datasetId,
                coalesce(ds.title, ds.name)     AS datasetTitle,
                contract IS NOT NULL            AS hasContract,
                approval.ehdsArticle            AS ehdsArticle
         ORDER BY p.name`,
      ),
    ]);
 
    // If Neo4j has no consumers, fall back to EDC-V activated participants
    let finalConsumers = consumers;
    if (consumers.length === 0) {
      try {
        const participants = await edcClient.management<
          { "@id": string; identity?: string; state?: string }[]
        >("/v5alpha/participants");
        const active = (Array.isArray(participants) ? participants : []).filter(
          (p) => (p.state ?? "ACTIVATED") === "ACTIVATED",
        );
        finalConsumers = active.map((p) => {
          const did = p.identity ?? p["@id"];
          const slug = didSlug(did);
          const info = SLUG_DISPLAY[slug];
          return {
            id: p["@id"],
            name: info?.name ?? slug ?? p["@id"].slice(0, 12),
            type: info?.type ?? "PARTICIPANT",
          };
        });
      } catch {
        // EDC-V also unavailable — keep empty
      }
    }
 
    // If Neo4j has no datasets, provide discoverable HealthDatasets
    let finalDatasets = datasets;
    if (datasets.length === 0) {
      const graphDatasets = await runQuery<{ id: string; title: string }>(
        `MATCH (ds:HealthDataset)
         RETURN coalesce(ds.id, ds.datasetId) AS id,
                coalesce(ds.title, ds.name)   AS title
         ORDER BY ds.title`,
      );
      if (graphDatasets && graphDatasets.length > 0) {
        finalDatasets = graphDatasets;
      }
    }
 
    return NextResponse.json({
      consumers: finalConsumers,
      datasets: finalDatasets,
      matrix: matrixRows,
    });
  }
 
  // Check mode: walk the EHDS approval chain for the given consumer + dataset
  const rows = await runQuery<{
    consumer: string;
    applicationId: string;
    applicationStatus: string;
    approvalId: string;
    approvalStatus: string;
    ehdsArticle: string;
    dataset: string;
    contract: string;
  }>(
    `MATCH (consumer:Participant)
     WHERE coalesce(consumer.participantId, consumer.id) = $consumerId
     MATCH (consumer)-[:SUBMITTED]->(app:AccessApplication)
     MATCH (approval:HDABApproval)-[:APPROVES]->(app)
     MATCH (approval)-[:GRANTS_ACCESS_TO]->(dataset:HealthDataset)
     WHERE coalesce(dataset.id, dataset.datasetId) = $datasetId
     OPTIONAL MATCH (contract:Contract)-[:GOVERNS]->(dp:DataProduct)-[:DESCRIBED_BY]->(dataset)
     RETURN coalesce(consumer.participantId, consumer.id)  AS consumer,
            app.applicationId                              AS applicationId,
            app.status                                     AS applicationStatus,
            approval.approvalId                            AS approvalId,
            approval.status                                AS approvalStatus,
            approval.ehdsArticle                           AS ehdsArticle,
            coalesce(dataset.id, dataset.datasetId)        AS dataset,
            contract.contractId                            AS contract`,
    { consumerId, datasetId },
  );
 
  return NextResponse.json({ compliant: rows.length > 0, chain: rows });
}