Basis: Certvia dev@a48c5fb als Fundament für Craftvia
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>
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
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];
|
||||
}
|
||||
Reference in New Issue
Block a user