Unveränderter Stand von certvia/dev (a48c5fb) plus Craftvia-Spezifikation und Brandbook unter docs/craftvia/. ISMS-Module werden im Folgecommit entfernt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
206 lines
9.4 KiB
TypeScript
206 lines
9.4 KiB
TypeScript
import type { PrismaClient, Framework } from "@prisma/client";
|
|
import type { ParsedPackage, ParsedDoc } from "./import-policies";
|
|
import { parsePackageFiles, readPackageVersion, mappingFileFor } from "./import-policies";
|
|
|
|
/**
|
|
* Globale DB-Vorlagenablage (Plattform-Ebene). Master-Quelle für den Mandanten-Import:
|
|
* eine `PolicyTemplateVersion` je Paketversion mit Inhalten je Sprache (`locale`).
|
|
*
|
|
* - `syncTemplatesFromParsed`: schreibt ein `ParsedPackage` (aus den Dateien geparst)
|
|
* für eine Sprache in eine Version. Für eine bestehende Version werden die Inhalte
|
|
* dieser Sprache vollständig ersetzt (idempotent). Sicher, weil die Vorlagen-Tabellen
|
|
* keine Mandanten-/Nutzerdaten enthalten.
|
|
* - `loadPublishedPackage`: liest die aktuell veröffentlichte Version in der gewünschten
|
|
* Sprache als `ParsedPackage` — dieselbe Struktur wie der Datei-Parser, sodass der
|
|
* non-destruktive Mandanten-Reconcile identisch damit arbeitet. Liefert null, wenn es
|
|
* keine veröffentlichte Version bzw. keine Inhalte in der Sprache gibt (→ Datei-Fallback).
|
|
*/
|
|
|
|
type DocType = ParsedDoc["type"];
|
|
|
|
/** Ein Datei-Paket (eine Sprache) in eine Vorlagen-Version schreiben; Inhalte der Sprache ersetzen. */
|
|
export async function syncTemplatesFromParsed(
|
|
prisma: PrismaClient,
|
|
pkg: ParsedPackage,
|
|
locale: string,
|
|
framework: Framework = "TISAX",
|
|
options: { publish?: boolean; publishedBy?: string | null } = {},
|
|
): Promise<{ versionId: string; version: string }> {
|
|
const publish = options.publish ?? true;
|
|
// Eindeutigkeit je (Framework, Version): ISO 2.1 und TISAX 2.1 sind verschiedene Zeilen.
|
|
const existing = await prisma.policyTemplateVersion.findUnique({
|
|
where: { framework_version: { framework, version: pkg.version } },
|
|
});
|
|
const version =
|
|
existing ??
|
|
(await prisma.policyTemplateVersion.create({
|
|
data: {
|
|
framework,
|
|
version: pkg.version,
|
|
status: publish ? "PUBLISHED" : "DRAFT",
|
|
publishedBy: publish ? (options.publishedBy ?? null) : null,
|
|
publishedAt: publish ? new Date() : null,
|
|
},
|
|
}));
|
|
if (existing && publish && existing.status !== "PUBLISHED") {
|
|
await prisma.policyTemplateVersion.update({
|
|
where: { id: existing.id },
|
|
data: { status: "PUBLISHED", publishedAt: new Date(), publishedBy: options.publishedBy ?? null },
|
|
});
|
|
}
|
|
|
|
const versionId = version.id;
|
|
// Inhalte dieser Sprache vollständig ersetzen (idempotenter Sync).
|
|
await prisma.$transaction([
|
|
prisma.policyTemplateDoc.deleteMany({ where: { versionId, locale } }),
|
|
prisma.policyTemplateRequirement.deleteMany({ where: { versionId, locale } }),
|
|
prisma.policyTemplateVariable.deleteMany({ where: { versionId, locale } }),
|
|
prisma.policyTemplateBaselineParam.deleteMany({ where: { versionId, locale } }),
|
|
prisma.policyTemplateEvidence.deleteMany({ where: { versionId, locale } }),
|
|
prisma.policyTemplateDoc.createMany({
|
|
data: pkg.documents.map((d) => ({
|
|
versionId, locale, code: d.code, type: d.type, title: d.title,
|
|
policyCode: d.policyCode, fulfills: d.fulfills, rawMarkdown: d.rawMarkdown, orderIdx: d.orderIdx,
|
|
})),
|
|
}),
|
|
prisma.policyTemplateRequirement.createMany({
|
|
data: pkg.requirements.map((r) => ({
|
|
versionId, locale, reqId: r.reqId, policyCode: r.policyCode, control: r.control,
|
|
obligation: r.obligation, condition: r.condition, requirement: r.requirement,
|
|
implementation: r.implementation, vaCodes: r.vaCodes, nachweisLink: r.nachweisLink,
|
|
})),
|
|
}),
|
|
prisma.policyTemplateVariable.createMany({
|
|
data: pkg.variables.map((v) => ({
|
|
versionId, locale, key: v.key, title: v.title, kind: v.kind,
|
|
groupName: v.groupName, value: v.value, required: v.required, orderIdx: v.orderIdx,
|
|
})),
|
|
}),
|
|
prisma.policyTemplateBaselineParam.createMany({
|
|
data: pkg.baseline.map((b) => ({
|
|
versionId, locale, blId: b.blId, section: b.section, name: b.name, vorgabe: b.vorgabe, orderIdx: b.orderIdx,
|
|
})),
|
|
}),
|
|
prisma.policyTemplateEvidence.createMany({
|
|
data: pkg.evidence.map((e) => ({
|
|
versionId, locale, nr: e.nr, policyCode: e.policyCode, nachweis: e.nachweis,
|
|
quelle: e.quelle, verantwortlich: e.verantwortlich, turnus: e.turnus,
|
|
})),
|
|
}),
|
|
]);
|
|
|
|
return { versionId, version: version.version };
|
|
}
|
|
|
|
/**
|
|
* Aktuell veröffentlichte Vorlagen-Version in der gewünschten Sprache als `ParsedPackage`.
|
|
* Liefert null, wenn keine veröffentlichte Version existiert oder die Sprache keine
|
|
* Dokumente enthält (→ Aufrufer nutzt den Datei-Fallback).
|
|
*/
|
|
export async function loadPublishedPackage(
|
|
prisma: PrismaClient,
|
|
locale: string,
|
|
framework: Framework = "TISAX",
|
|
): Promise<ParsedPackage | null> {
|
|
const version = await prisma.policyTemplateVersion.findFirst({
|
|
where: { status: "PUBLISHED", framework },
|
|
orderBy: [{ publishedAt: "desc" }, { createdAt: "desc" }],
|
|
});
|
|
if (!version) return null;
|
|
|
|
const [docs, reqs, vars, baseline, evidence] = await Promise.all([
|
|
prisma.policyTemplateDoc.findMany({ where: { versionId: version.id, locale }, orderBy: { orderIdx: "asc" } }),
|
|
prisma.policyTemplateRequirement.findMany({ where: { versionId: version.id, locale }, orderBy: { orderIdx: "asc" } }),
|
|
prisma.policyTemplateVariable.findMany({ where: { versionId: version.id, locale }, orderBy: { orderIdx: "asc" } }),
|
|
prisma.policyTemplateBaselineParam.findMany({ where: { versionId: version.id, locale }, orderBy: { orderIdx: "asc" } }),
|
|
prisma.policyTemplateEvidence.findMany({ where: { versionId: version.id, locale }, orderBy: { nr: "asc" } }),
|
|
]);
|
|
if (docs.length === 0) return null; // Sprache (noch) nicht befüllt → Fallback
|
|
|
|
return {
|
|
version: version.version,
|
|
documents: docs.map((d) => ({
|
|
code: d.code, type: d.type as DocType, title: d.title, policyCode: d.policyCode,
|
|
fulfills: d.fulfills, rawMarkdown: d.rawMarkdown, orderIdx: d.orderIdx,
|
|
})),
|
|
requirements: reqs.map((r) => ({
|
|
reqId: r.reqId, policyCode: r.policyCode, control: r.control, obligation: r.obligation,
|
|
condition: r.condition, requirement: r.requirement, implementation: r.implementation,
|
|
vaCodes: r.vaCodes, nachweisLink: r.nachweisLink,
|
|
})),
|
|
variables: vars.map((v) => ({
|
|
key: v.key, title: v.title, kind: v.kind, groupName: v.groupName,
|
|
value: v.value, required: v.required, orderIdx: v.orderIdx,
|
|
})),
|
|
baseline: baseline.map((b) => ({
|
|
blId: b.blId, section: b.section, name: b.name, vorgabe: b.vorgabe, orderIdx: b.orderIdx,
|
|
})),
|
|
evidence: evidence.map((e) => ({
|
|
nr: e.nr, policyCode: e.policyCode, nachweis: e.nachweis, quelle: e.quelle,
|
|
verantwortlich: e.verantwortlich, turnus: e.turnus,
|
|
})),
|
|
};
|
|
}
|
|
|
|
/** Sprachpräferenz eines Mandanten (Default "de"). Owner-Client (RLS-Bypass) — für Plattform- UND Mandanten-Aufrufer nutzbar. */
|
|
export async function getTenantLocale(prisma: PrismaClient, tenantId: string): Promise<string> {
|
|
const s = await prisma.tenantSettings.findUnique({ where: { tenantId }, select: { locale: true } });
|
|
return s?.locale ?? "de";
|
|
}
|
|
|
|
/**
|
|
* Auflösen des Vorlagenpakets für einen Mandanten: veröffentlichte DB-Version in der
|
|
* Sprache des Mandanten → DE-Fallback → Datei-Fallback. So funktioniert der Import
|
|
* identisch, egal ob die DB-Ablage schon befüllt ist.
|
|
*/
|
|
export async function resolvePackageForTenant(
|
|
prisma: PrismaClient,
|
|
tenantId: string,
|
|
seedDir: string,
|
|
framework: Framework = "TISAX",
|
|
): Promise<{ pkg: ParsedPackage; source: "db" | "file"; locale: string }> {
|
|
const locale = await getTenantLocale(prisma, tenantId);
|
|
const inLocale = await loadPublishedPackage(prisma, locale, framework);
|
|
if (inLocale) return { pkg: inLocale, source: "db", locale };
|
|
if (locale !== "de") {
|
|
const de = await loadPublishedPackage(prisma, "de", framework);
|
|
if (de) return { pkg: de, source: "db", locale: "de" };
|
|
}
|
|
// Datei-Fallback: das framework-passende Mapping (mapping.json / mapping-iso.json).
|
|
return { pkg: parsePackageFiles(seedDir, mappingFileFor(framework)), source: "file", locale };
|
|
}
|
|
|
|
/** Verfügbare Paketversion für den Update-Vergleich: veröffentlichte DB-Version, sonst Datei-Version. */
|
|
export async function getAvailableVersion(
|
|
prisma: PrismaClient,
|
|
seedDir: string,
|
|
framework: Framework = "TISAX",
|
|
): Promise<string> {
|
|
const v = await prisma.policyTemplateVersion.findFirst({
|
|
where: { status: "PUBLISHED", framework },
|
|
orderBy: [{ publishedAt: "desc" }, { createdAt: "desc" }],
|
|
select: { version: true },
|
|
});
|
|
return v?.version ?? readPackageVersion(seedDir, mappingFileFor(framework));
|
|
}
|
|
|
|
/**
|
|
* Frameworks eines Mandanten (AP1), Primär-Framework zuerst. Fällt auf `["TISAX"]`
|
|
* zurück, falls (noch) keine `TenantFramework`-Zeile existiert — so bleibt jeder
|
|
* Bestands-/Alt-Mandant ohne Zeile als TISAX lauffähig (Fail-safe zu Invariante 3).
|
|
* Owner-Client (RLS-Bypass) — für Plattform- UND Mandanten-Aufrufer nutzbar.
|
|
*/
|
|
export async function getTenantFrameworks(prisma: PrismaClient, tenantId: string): Promise<Framework[]> {
|
|
const rows = await prisma.tenantFramework.findMany({
|
|
where: { tenantId },
|
|
orderBy: [{ isPrimary: "desc" }, { createdAt: "asc" }],
|
|
select: { framework: true },
|
|
});
|
|
return rows.length ? rows.map((r) => r.framework) : ["TISAX"];
|
|
}
|
|
|
|
/** Primär-Framework eines Mandanten (erstes aus {@link getTenantFrameworks}). */
|
|
export async function getTenantPrimaryFramework(prisma: PrismaClient, tenantId: string): Promise<Framework> {
|
|
return (await getTenantFrameworks(prisma, tenantId))[0];
|
|
}
|