@cosmicdrift/kumiko-bundled-features 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +91 -0
- package/package.json +22 -13
- package/src/auth-email-password/auth-user-row.ts +6 -0
- package/src/auth-email-password/constants.ts +11 -0
- package/src/auth-email-password/handlers/change-password.write.ts +1 -1
- package/src/auth-email-password/handlers/confirm-token-flow.ts +1 -1
- package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +7 -7
- package/src/auth-email-password/handlers/invite-accept.write.ts +7 -6
- package/src/auth-email-password/handlers/invite-create.write.ts +3 -3
- package/src/auth-email-password/handlers/invite-signup-complete.write.ts +4 -4
- package/src/auth-email-password/handlers/login.write.ts +32 -2
- package/src/auth-email-password/handlers/logout.write.ts +2 -2
- package/src/auth-email-password/handlers/signup-confirm.write.ts +1 -1
- package/src/auth-email-password/i18n.ts +4 -0
- package/src/auth-email-password/web/auth-client.ts +1 -1
- package/src/billing-foundation/events.ts +1 -1
- package/src/billing-foundation/feature.ts +44 -47
- package/src/billing-foundation/handlers/create-portal-session.write.ts +3 -3
- package/src/billing-foundation/handlers/process-event.write.ts +3 -3
- package/src/billing-foundation/projection.ts +1 -1
- package/src/billing-foundation/webhook-handler.ts +1 -1
- package/src/cap-counter/constants.ts +1 -1
- package/src/cap-counter/enforce-cap.ts +1 -1
- package/src/cap-counter/feature.ts +3 -7
- package/src/cap-counter/handlers/get-counter.query.ts +1 -1
- package/src/cap-counter/handlers/increment-rolling.write.ts +2 -2
- package/src/cap-counter/handlers/increment.write.ts +3 -3
- package/src/cap-counter/handlers/mark-soft-warned.write.ts +2 -2
- package/src/channel-email/email-channel.ts +1 -1
- package/src/channel-email/types.ts +1 -1
- package/src/compliance-profiles/README.md +88 -0
- package/src/compliance-profiles/__tests__/compliance-profiles.integration.ts +308 -0
- package/src/compliance-profiles/__tests__/seeding.integration.ts +93 -0
- package/src/compliance-profiles/feature.ts +51 -0
- package/src/compliance-profiles/handlers/for-tenant.query.ts +64 -0
- package/src/compliance-profiles/handlers/list-profiles.query.ts +44 -0
- package/src/compliance-profiles/handlers/needs-profile.query.ts +56 -0
- package/src/compliance-profiles/handlers/set-profile.write.ts +144 -0
- package/src/compliance-profiles/handlers/sub-processors.query.ts +43 -0
- package/src/compliance-profiles/index.ts +6 -0
- package/src/compliance-profiles/resolve-for-tenant.ts +63 -0
- package/src/compliance-profiles/schema/profile-selection.ts +52 -0
- package/src/compliance-profiles/seeding.ts +96 -0
- package/src/config/resolver.ts +1 -1
- package/src/data-retention/__tests__/data-retention.integration.ts +49 -0
- package/src/data-retention/__tests__/keep-for.test.ts +77 -0
- package/src/data-retention/__tests__/override-schema.test.ts +96 -0
- package/src/data-retention/__tests__/policy-for.integration.ts +172 -0
- package/src/data-retention/__tests__/resolver.test.ts +201 -0
- package/src/data-retention/_internal/parse-override.ts +34 -0
- package/src/data-retention/feature.ts +57 -0
- package/src/data-retention/handlers/policy-for.query.ts +57 -0
- package/src/data-retention/index.ts +18 -0
- package/src/data-retention/keep-for.ts +75 -0
- package/src/data-retention/override-schema.ts +37 -0
- package/src/data-retention/presets.ts +72 -0
- package/src/data-retention/resolve-for-tenant.ts +50 -0
- package/src/data-retention/resolver.ts +107 -0
- package/src/data-retention/schema/tenant-retention-override.ts +47 -0
- package/src/delivery/feature.ts +1 -1
- package/src/delivery/testing.ts +1 -2
- package/src/delivery/upsert-preference.ts +1 -1
- package/src/feature-toggles/feature.ts +1 -1
- package/src/feature-toggles/handlers/list.query.ts +1 -1
- package/src/feature-toggles/handlers/registered.query.ts +9 -2
- package/src/feature-toggles/handlers/set.write.ts +3 -3
- package/src/file-foundation/feature.ts +44 -4
- package/src/file-foundation/index.ts +1 -0
- package/src/file-provider-inmemory/feature.ts +6 -3
- package/src/file-provider-s3/feature.ts +10 -12
- package/src/files/README.md +50 -0
- package/src/files/__tests__/files.integration.ts +157 -0
- package/src/files/feature.ts +34 -0
- package/src/files/index.ts +1 -0
- package/src/files/schema/file-ref.ts +58 -0
- package/src/files-provider-s3/s3-provider.ts +90 -1
- package/src/jobs/handlers/list.query.ts +3 -3
- package/src/jobs/handlers/trigger.write.ts +1 -1
- package/src/legal-pages/constants.ts +1 -0
- package/src/legal-pages/web/client-plugin.ts +42 -0
- package/src/legal-pages/web/index.ts +4 -0
- package/src/mail-foundation/feature.ts +1 -1
- package/src/mail-transport-smtp/feature.ts +2 -2
- package/src/renderer-simple/simple-renderer.ts +1 -1
- package/src/secrets/__tests__/require-secrets-context.test.ts +81 -0
- package/src/secrets/feature.ts +10 -6
- package/src/secrets/handlers/rotate.job.ts +2 -2
- package/src/sessions/constants.ts +4 -0
- package/src/sessions/feature.ts +3 -0
- package/src/sessions/handlers/cleanup.job.ts +2 -2
- package/src/sessions/handlers/revoke-all-for-user.write.ts +42 -0
- package/src/step-dispatcher/feature.ts +62 -0
- package/src/step-dispatcher/index.ts +16 -0
- package/src/step-dispatcher/mail-runner.ts +32 -0
- package/src/step-dispatcher/webhook-runner.ts +67 -0
- package/src/subscription-mollie/plugin-methods.ts +1 -1
- package/src/subscription-mollie/verify-webhook.ts +9 -5
- package/src/subscription-stripe/verify-webhook.ts +3 -3
- package/src/tenant/handlers/active-tenant-ids.query.ts +1 -1
- package/src/tenant/handlers/cancel-invitation.write.ts +1 -1
- package/src/tenant/handlers/remove-member.write.ts +1 -1
- package/src/tenant/handlers/resolve-user-ids.query.ts +1 -1
- package/src/tenant/handlers/update-member-roles.write.ts +3 -3
- package/src/text-content/constants.ts +2 -0
- package/src/text-content/feature.ts +20 -4
- package/src/text-content/handlers/by-tenant.query.ts +56 -0
- package/src/text-content/handlers/set.write.ts +1 -1
- package/src/text-content/web/client-plugin.ts +113 -0
- package/src/text-content/web/index.ts +8 -0
- package/src/tier-engine/__tests__/auto-default-tier.integration.ts +118 -0
- package/src/tier-engine/feature.ts +23 -13
- package/src/user/__tests__/user-status.test.ts +39 -0
- package/src/user/handlers/find-for-auth.query.ts +1 -1
- package/src/user/index.ts +11 -1
- package/src/user/schema/user.ts +76 -0
- package/src/user/seeding.ts +2 -2
- package/src/user-data-rights/COMPLIANCE.md +182 -0
- package/src/user-data-rights/README.md +109 -0
- package/src/user-data-rights/__tests__/audit-log.integration.ts +199 -0
- package/src/user-data-rights/__tests__/cross-data-matrix.integration.ts +349 -0
- package/src/user-data-rights/__tests__/download.integration.ts +565 -0
- package/src/user-data-rights/__tests__/export-job-idempotency.integration.ts +244 -0
- package/src/user-data-rights/__tests__/export-job-schema.test.ts +163 -0
- package/src/user-data-rights/__tests__/policy-to-strategy.test.ts +30 -0
- package/src/user-data-rights/__tests__/request-cancel-deletion.integration.ts +370 -0
- package/src/user-data-rights/__tests__/request-deletion-callback.integration.ts +179 -0
- package/src/user-data-rights/__tests__/request-export.integration.ts +269 -0
- package/src/user-data-rights/__tests__/restriction-flow.integration.ts +309 -0
- package/src/user-data-rights/__tests__/run-export-jobs.integration.ts +1124 -0
- package/src/user-data-rights/__tests__/run-forget-cleanup.integration.ts +703 -0
- package/src/user-data-rights/__tests__/run-user-export.integration.ts +291 -0
- package/src/user-data-rights/__tests__/token-helpers.test.ts +63 -0
- package/src/user-data-rights/__tests__/user-data-rights.integration.ts +57 -0
- package/src/user-data-rights/__tests__/zip-path.test.ts +119 -0
- package/src/user-data-rights/audit-download.ts +125 -0
- package/src/user-data-rights/feature.ts +310 -0
- package/src/user-data-rights/handlers/cancel-deletion.write.ts +84 -0
- package/src/user-data-rights/handlers/download-by-job.query.ts +206 -0
- package/src/user-data-rights/handlers/download-by-token.query.ts +255 -0
- package/src/user-data-rights/handlers/export-status.query.ts +76 -0
- package/src/user-data-rights/handlers/lift-restriction.write.ts +68 -0
- package/src/user-data-rights/handlers/list-download-attempts.query.ts +53 -0
- package/src/user-data-rights/handlers/my-audit-log.query.ts +63 -0
- package/src/user-data-rights/handlers/request-deletion.write.ts +123 -0
- package/src/user-data-rights/handlers/request-export.write.ts +155 -0
- package/src/user-data-rights/handlers/restrict-account.write.ts +81 -0
- package/src/user-data-rights/handlers/run-forget-cleanup.write.ts +61 -0
- package/src/user-data-rights/i18n.ts +37 -0
- package/src/user-data-rights/index.ts +19 -0
- package/src/user-data-rights/run-export-jobs.ts +878 -0
- package/src/user-data-rights/run-forget-cleanup.ts +333 -0
- package/src/user-data-rights/run-user-export.ts +211 -0
- package/src/user-data-rights/schema/download-attempt.ts +37 -0
- package/src/user-data-rights/schema/download-token.ts +111 -0
- package/src/user-data-rights/schema/export-job.ts +166 -0
- package/src/user-data-rights/token-helpers.ts +67 -0
- package/src/user-data-rights/zip-path.ts +94 -0
- package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.ts +337 -0
- package/src/user-data-rights-defaults/feature.ts +40 -0
- package/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +109 -0
- package/src/user-data-rights-defaults/hooks/user.userdata-hook.ts +91 -0
- package/src/user-data-rights-defaults/index.ts +6 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ComplianceProfileKey,
|
|
3
|
+
type ComplianceProfileOverride,
|
|
4
|
+
type EffectiveComplianceProfile,
|
|
5
|
+
resolveComplianceProfile,
|
|
6
|
+
} from "@cosmicdrift/kumiko-framework/compliance";
|
|
7
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/db";
|
|
8
|
+
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
9
|
+
import { eq } from "drizzle-orm";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { tenantComplianceProfileTable } from "../schema/profile-selection";
|
|
12
|
+
|
|
13
|
+
// Liefert das effektive Compliance-Profile fuer den aktuellen Tenant.
|
|
14
|
+
// Macht den exposesApi-Marker aus feature.ts mit echtem Inhalt.
|
|
15
|
+
//
|
|
16
|
+
// Default-Verhalten (Edge-Case-Decision aus S1.1): kein Profile-
|
|
17
|
+
// Eintrag → minimal-no-region + warning="no-profile-selected".
|
|
18
|
+
// Caller (z.B. user-data-rights in Sprint 2) sieht das warning und
|
|
19
|
+
// kann Onboarding-Banner triggern.
|
|
20
|
+
export const forTenantQuery = defineQueryHandler({
|
|
21
|
+
name: "for-tenant",
|
|
22
|
+
schema: z.object({}),
|
|
23
|
+
access: { openToAll: true },
|
|
24
|
+
handler: async (query, ctx): Promise<EffectiveComplianceProfile> => {
|
|
25
|
+
const row = (await fetchOne(
|
|
26
|
+
ctx.db,
|
|
27
|
+
tenantComplianceProfileTable,
|
|
28
|
+
eq(tenantComplianceProfileTable["tenantId"], query.user.tenantId),
|
|
29
|
+
)) as { profileKey: string; override: string | null } | null; // @cast-boundary db-runner
|
|
30
|
+
|
|
31
|
+
if (!row) {
|
|
32
|
+
return resolveComplianceProfile({});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const override = parseOverride(row.override, query.user.tenantId);
|
|
36
|
+
return resolveComplianceProfile({
|
|
37
|
+
selection: row.profileKey as ComplianceProfileKey, // @cast-boundary engine-payload
|
|
38
|
+
override,
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
function parseOverride(
|
|
44
|
+
raw: string | null,
|
|
45
|
+
tenantId: string,
|
|
46
|
+
): ComplianceProfileOverride | undefined {
|
|
47
|
+
if (!raw || raw.trim() === "") return undefined;
|
|
48
|
+
try {
|
|
49
|
+
const parsed: unknown = JSON.parse(raw);
|
|
50
|
+
return parsed as ComplianceProfileOverride; // @cast-boundary engine-payload
|
|
51
|
+
} catch (e: unknown) {
|
|
52
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
53
|
+
// Defensiv: ungültiges JSON wird als "kein Override" behandelt. Der
|
|
54
|
+
// set-profile-Handler validiert Zod das Override schon — invalides
|
|
55
|
+
// JSON in der DB ist also nur möglich bei manueller DB-Manipulation
|
|
56
|
+
// oder Migration-Bug. Resolver-Caller darf trotzdem nicht crashen.
|
|
57
|
+
// Operator-Sichtbarkeit via console.warn — Telemetry-Hook spaeter.
|
|
58
|
+
// biome-ignore lint/suspicious/noConsole: operator visibility for DB-corruption edge-case
|
|
59
|
+
console.warn(
|
|
60
|
+
`[compliance-profiles:for-tenant] tenant ${tenantId}: stored override is not valid JSON, falling back to base profile. Reason: ${reason}`,
|
|
61
|
+
);
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
COMPLIANCE_PROFILES,
|
|
3
|
+
type ComplianceProfile,
|
|
4
|
+
type ComplianceProfileKey,
|
|
5
|
+
SELECTABLE_PROFILE_KEYS,
|
|
6
|
+
} from "@cosmicdrift/kumiko-framework/compliance";
|
|
7
|
+
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
// Liefert alle waehlbaren Compliance-Profile fuer das Tenant-Onboarding.
|
|
11
|
+
// Pure In-Memory-Read der Constants — keine DB-Abfrage. Kein Caching
|
|
12
|
+
// noetig (modulo Pre-Boot bereits aufgeloest).
|
|
13
|
+
//
|
|
14
|
+
// Filtert minimal-no-region raus — das ist Default-Fallback, nicht
|
|
15
|
+
// auswählbar (Production soll explizite Wahl treffen).
|
|
16
|
+
export const listProfilesQuery = defineQueryHandler({
|
|
17
|
+
name: "list-profiles",
|
|
18
|
+
schema: z.object({}),
|
|
19
|
+
access: { openToAll: true },
|
|
20
|
+
handler: async (): Promise<{ profiles: readonly ComplianceProfileSummary[] }> => {
|
|
21
|
+
return {
|
|
22
|
+
profiles: SELECTABLE_PROFILE_KEYS.map(toSummary),
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
interface ComplianceProfileSummary {
|
|
28
|
+
readonly key: ComplianceProfileKey;
|
|
29
|
+
readonly region: string;
|
|
30
|
+
readonly label: string;
|
|
31
|
+
readonly authorityContact: string;
|
|
32
|
+
readonly languages: readonly string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function toSummary(key: ComplianceProfileKey): ComplianceProfileSummary {
|
|
36
|
+
const p: ComplianceProfile = COMPLIANCE_PROFILES[key];
|
|
37
|
+
return {
|
|
38
|
+
key: p.key,
|
|
39
|
+
region: p.region,
|
|
40
|
+
label: p.label,
|
|
41
|
+
authorityContact: p.breach.authorityContact,
|
|
42
|
+
languages: p.notifications.languages,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
|
|
2
|
+
import type { ComplianceProfileKey } from "@cosmicdrift/kumiko-framework/compliance";
|
|
3
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/db";
|
|
4
|
+
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
5
|
+
import { eq } from "drizzle-orm";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { tenantComplianceProfileTable } from "../schema/profile-selection";
|
|
8
|
+
|
|
9
|
+
// Onboarding-Banner-Trigger fuer Tenant-Admin.
|
|
10
|
+
//
|
|
11
|
+
// Sprint 1.5 — minimaler API-Endpoint, UI-Banner kommt in einem
|
|
12
|
+
// spaeteren UI-Sprint. Reine Read-Query: gibt es einen Eintrag in
|
|
13
|
+
// tenantComplianceProfile fuer den aktuellen Tenant?
|
|
14
|
+
//
|
|
15
|
+
// Wenn nein → Tenant-Admin muss Profile waehlen (Pflicht beim
|
|
16
|
+
// Onboarding). Bis zur Wahl laeuft minimal-no-region mit warning,
|
|
17
|
+
// das in der Tenant-Dashboard-Banner sichtbar gemacht werden soll.
|
|
18
|
+
//
|
|
19
|
+
// Access: TenantAdmin only — der Banner ist nur fuer Tenant-Admins
|
|
20
|
+
// relevant, nicht fuer normale Member.
|
|
21
|
+
export const needsProfileQuery = defineQueryHandler({
|
|
22
|
+
name: "needs-profile",
|
|
23
|
+
schema: z.object({}),
|
|
24
|
+
access: { roles: [ROLES.TenantAdmin] },
|
|
25
|
+
handler: async (query, ctx): Promise<NeedsProfileResponse> => {
|
|
26
|
+
const row = (await fetchOne(
|
|
27
|
+
ctx.db,
|
|
28
|
+
tenantComplianceProfileTable,
|
|
29
|
+
eq(tenantComplianceProfileTable["tenantId"], query.user.tenantId),
|
|
30
|
+
)) as { profileKey: ComplianceProfileKey } | null; // @cast-boundary db-runner
|
|
31
|
+
|
|
32
|
+
if (!row) {
|
|
33
|
+
return {
|
|
34
|
+
needsSelection: true,
|
|
35
|
+
currentProfile: null,
|
|
36
|
+
reason: "no_profile_selected",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// S1.7 X1: minimal-no-region ist via set-profile (Zod) nicht mehr
|
|
41
|
+
// setzbar. Wenn Sprint 2 einen seedComplianceProfile-Helper liefert
|
|
42
|
+
// der den Migration-Edge-Case einführt, kommt hier wieder ein
|
|
43
|
+
// defensiver Pfad rein — bis dahin: jeder existierende Eintrag ist
|
|
44
|
+
// ein bewusst gewähltes Production-Profile.
|
|
45
|
+
return {
|
|
46
|
+
needsSelection: false,
|
|
47
|
+
currentProfile: row.profileKey,
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
interface NeedsProfileResponse {
|
|
53
|
+
readonly needsSelection: boolean;
|
|
54
|
+
readonly currentProfile: ComplianceProfileKey | null;
|
|
55
|
+
readonly reason?: "no_profile_selected";
|
|
56
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
|
|
2
|
+
import {
|
|
3
|
+
complianceProfileOverrideSchema,
|
|
4
|
+
SELECTABLE_PROFILE_KEYS,
|
|
5
|
+
} from "@cosmicdrift/kumiko-framework/compliance";
|
|
6
|
+
import { createEventStoreExecutor, fetchOne } from "@cosmicdrift/kumiko-framework/db";
|
|
7
|
+
import { defineWriteHandler, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
8
|
+
import {
|
|
9
|
+
AccessDeniedError,
|
|
10
|
+
UnprocessableError,
|
|
11
|
+
validationErrorFromZod,
|
|
12
|
+
writeFailure,
|
|
13
|
+
} from "@cosmicdrift/kumiko-framework/errors";
|
|
14
|
+
import { eq } from "drizzle-orm";
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
import {
|
|
17
|
+
tenantComplianceProfileEntity,
|
|
18
|
+
tenantComplianceProfileTable,
|
|
19
|
+
} from "../schema/profile-selection";
|
|
20
|
+
|
|
21
|
+
const crud = createEventStoreExecutor(tenantComplianceProfileTable, tenantComplianceProfileEntity, {
|
|
22
|
+
entityName: "tenant-compliance-profile",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Schema engt sich auf die 3 oeffentlich waehlbaren Profile (Sprint 1.7
|
|
26
|
+
// X1) — minimal-no-region ist Default-Fallback fuer "noch keine Wahl",
|
|
27
|
+
// nicht eine waehlbare Production-Option. Symmetrisch zu
|
|
28
|
+
// SELECTABLE_PROFILE_KEYS aus der framework/compliance-Liste.
|
|
29
|
+
const profileKeySchema = z.enum(SELECTABLE_PROFILE_KEYS);
|
|
30
|
+
|
|
31
|
+
// Tenant-Admin setzt Profile-Key + optional Override-JSON.
|
|
32
|
+
//
|
|
33
|
+
// Upsert-Verhalten: erste Wahl insert, weitere update. Idempotent —
|
|
34
|
+
// wer mit gleichen Werten zweimal aufruft, kriegt das gleiche Ergebnis
|
|
35
|
+
// (modulo aktualisierte Audit-Events im Event-Store).
|
|
36
|
+
//
|
|
37
|
+
// Cross-Tenant-Pfad: SystemAdmin kann via `tenantIdOverride` fuer einen
|
|
38
|
+
// anderen Tenant schreiben (Plattform-Operator-Setup, Customer-
|
|
39
|
+
// Onboarding-Migrationen). TenantAdmin's Override-Versuch → 403.
|
|
40
|
+
// executorUser.tenantId muss = ziel-tenant sein damit der event-store-
|
|
41
|
+
// Stream-Lookup nicht miss → version_conflict gibt (Memory:
|
|
42
|
+
// feedback_event_store_tenant_consistency).
|
|
43
|
+
//
|
|
44
|
+
// Validation:
|
|
45
|
+
// - profileKey muss in SELECTABLE_PROFILE_KEYS sein (Zod-checked)
|
|
46
|
+
// - override (optional) muss valides JSON-Object sein
|
|
47
|
+
// - override Top-Level-Keys muessen in ALLOWED_OVERRIDE_KEYS sein
|
|
48
|
+
// — verhindert Tippfehler die deepMerge stillschweigend ignoriert
|
|
49
|
+
export const setProfileWrite = defineWriteHandler({
|
|
50
|
+
name: "set-profile",
|
|
51
|
+
schema: z.object({
|
|
52
|
+
profileKey: profileKeySchema,
|
|
53
|
+
override: z.string().nullable().optional(),
|
|
54
|
+
tenantIdOverride: z.string().min(1).optional(),
|
|
55
|
+
}),
|
|
56
|
+
// SystemAdmin kann Profile fuer Customer-Setup setzen (Plattform-
|
|
57
|
+
// Operator-Pfad). TenantAdmin nur fuer eigenen Tenant.
|
|
58
|
+
access: { roles: [ROLES.TenantAdmin, ROLES.SystemAdmin] },
|
|
59
|
+
handler: async (event, ctx) => {
|
|
60
|
+
const tenantOverride = event.payload.tenantIdOverride;
|
|
61
|
+
if (tenantOverride !== undefined && !event.user.roles.includes(ROLES.SystemAdmin)) {
|
|
62
|
+
return writeFailure(
|
|
63
|
+
new AccessDeniedError({
|
|
64
|
+
i18nKey: "complianceProfiles.errors.tenantOverrideRequiresSystemAdmin",
|
|
65
|
+
details: { reason: "tenant_override_requires_system_admin" },
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const tenantId = (tenantOverride ?? event.user.tenantId) as TenantId; // @cast-boundary engine-payload
|
|
70
|
+
const executorUser = tenantOverride !== undefined ? { ...event.user, tenantId } : event.user;
|
|
71
|
+
|
|
72
|
+
// Override-Validation: muss parseables JSON-Object sein UND dem
|
|
73
|
+
// ComplianceProfileOverride-Schema entsprechen (S1.9 Z3 — strict-Zod
|
|
74
|
+
// mit Top-Level + Sub-Level-Whitelist via .strict()). Tippfehler
|
|
75
|
+
// wie `{ userRights: { weeks: 3 } }` werden hier rejected statt vom
|
|
76
|
+
// deepMerge silent ins Profile gespliced.
|
|
77
|
+
//
|
|
78
|
+
// Errors via writeFailure + Kumiko-Error-Klassen (S1.10 M3) statt
|
|
79
|
+
// throw — landen so mit Path-Detail im response-body statt als
|
|
80
|
+
// generic internal_error.
|
|
81
|
+
if (event.payload.override) {
|
|
82
|
+
let parsed: unknown;
|
|
83
|
+
try {
|
|
84
|
+
parsed = JSON.parse(event.payload.override);
|
|
85
|
+
} catch (e: unknown) {
|
|
86
|
+
const parseError = e instanceof Error ? e.message : String(e);
|
|
87
|
+
return writeFailure(
|
|
88
|
+
new UnprocessableError("compliance_override_invalid_json", {
|
|
89
|
+
details: {
|
|
90
|
+
reason: "compliance_override_invalid_json",
|
|
91
|
+
parseError,
|
|
92
|
+
},
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const validation = complianceProfileOverrideSchema.safeParse(parsed);
|
|
97
|
+
if (!validation.success) {
|
|
98
|
+
return writeFailure(validationErrorFromZod(validation.error));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Upsert: existierenden Eintrag suchen
|
|
103
|
+
const existing = (await fetchOne(
|
|
104
|
+
ctx.db,
|
|
105
|
+
tenantComplianceProfileTable,
|
|
106
|
+
eq(tenantComplianceProfileTable["tenantId"], tenantId),
|
|
107
|
+
)) as { id: string; version: number } | null; // @cast-boundary db-runner
|
|
108
|
+
|
|
109
|
+
if (existing) {
|
|
110
|
+
const result = await crud.update(
|
|
111
|
+
{
|
|
112
|
+
id: existing.id,
|
|
113
|
+
version: existing.version,
|
|
114
|
+
changes: {
|
|
115
|
+
profileKey: event.payload.profileKey,
|
|
116
|
+
override: event.payload.override ?? null,
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
executorUser,
|
|
120
|
+
ctx.db,
|
|
121
|
+
);
|
|
122
|
+
if (!result.isSuccess) return result;
|
|
123
|
+
return {
|
|
124
|
+
isSuccess: true as const,
|
|
125
|
+
data: { profileKey: event.payload.profileKey, isNew: false },
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const result = await crud.create(
|
|
130
|
+
{
|
|
131
|
+
profileKey: event.payload.profileKey,
|
|
132
|
+
override: event.payload.override ?? null,
|
|
133
|
+
tenantId,
|
|
134
|
+
},
|
|
135
|
+
executorUser,
|
|
136
|
+
ctx.db,
|
|
137
|
+
);
|
|
138
|
+
if (!result.isSuccess) return result;
|
|
139
|
+
return {
|
|
140
|
+
isSuccess: true as const,
|
|
141
|
+
data: { profileKey: event.payload.profileKey, isNew: true },
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getActiveSubProcessors,
|
|
3
|
+
getPlannedSubProcessors,
|
|
4
|
+
KUMIKO_SUB_PROCESSORS,
|
|
5
|
+
type SubProcessor,
|
|
6
|
+
} from "@cosmicdrift/kumiko-framework/compliance";
|
|
7
|
+
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
8
|
+
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
|
|
11
|
+
// Public Sub-Processor-Liste — anonymous accessible (Memory:
|
|
12
|
+
// project_anonymous_access). Matched die DSGVO Art. 28(2) Pflicht
|
|
13
|
+
// dass die Liste der Auftragsverarbeiter oeffentlich einsehbar ist
|
|
14
|
+
// (typisch verlinkt aus Datenschutzerklaerung + AVV).
|
|
15
|
+
//
|
|
16
|
+
// Format: JSON mit getrennten active/planned-Sektionen. Tenant-Admins
|
|
17
|
+
// kriegen Notification ueber Aenderungen via Cron-Job (Sprint 1.5+
|
|
18
|
+
// oder S9 compliance-as-product). RSS-Feed kommt in S9.
|
|
19
|
+
//
|
|
20
|
+
// Zwei Auflistungen statt einer flachen Liste:
|
|
21
|
+
// - `active`: aktuell eingesetzte Sub-Processors
|
|
22
|
+
// - `planned`: bekannte zukünftige Sub-Processors (Tenant-Admin-
|
|
23
|
+
// Lead-Time bevor sie aktiv werden — typisch bei AI/Stripe)
|
|
24
|
+
export const subProcessorsQuery = defineQueryHandler({
|
|
25
|
+
name: "sub-processors",
|
|
26
|
+
schema: z.object({}),
|
|
27
|
+
access: { roles: ["anonymous", "Member", "User", "TenantAdmin", "SystemAdmin"] },
|
|
28
|
+
handler: async (): Promise<SubProcessorListResponse> => {
|
|
29
|
+
return {
|
|
30
|
+
active: [...getActiveSubProcessors()],
|
|
31
|
+
planned: [...getPlannedSubProcessors()],
|
|
32
|
+
generatedAt: getTemporal().Now.instant().toString(),
|
|
33
|
+
total: KUMIKO_SUB_PROCESSORS.length,
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
interface SubProcessorListResponse {
|
|
39
|
+
readonly active: readonly SubProcessor[];
|
|
40
|
+
readonly planned: readonly SubProcessor[];
|
|
41
|
+
readonly generatedAt: string;
|
|
42
|
+
readonly total: number;
|
|
43
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Direkter Resolver-Helper fuer Bulk-Iteration ohne dispatcher-Roundtrip.
|
|
2
|
+
//
|
|
3
|
+
// `for-tenant`-Query (handlers/for-tenant.query.ts) ist die Cross-Feature-
|
|
4
|
+
// API fuer Handler-Pfade. Worker (S2.U3 Atom 3b) lebt im JobContext
|
|
5
|
+
// ohne `queryAs` — braucht direkten DB-Lookup + resolveComplianceProfile.
|
|
6
|
+
//
|
|
7
|
+
// Pattern matched data-retention's `resolveRetentionPolicyForTenant`.
|
|
8
|
+
// Beide Pfade nutzen `resolveComplianceProfile` aus framework/compliance,
|
|
9
|
+
// also kein Drift zwischen Query-API und Worker-Pfad.
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
type ComplianceProfileKey,
|
|
13
|
+
type ComplianceProfileOverride,
|
|
14
|
+
type EffectiveComplianceProfile,
|
|
15
|
+
resolveComplianceProfile,
|
|
16
|
+
} from "@cosmicdrift/kumiko-framework/compliance";
|
|
17
|
+
import { type DbRunner, fetchOne } from "@cosmicdrift/kumiko-framework/db";
|
|
18
|
+
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
19
|
+
import { eq } from "drizzle-orm";
|
|
20
|
+
import { tenantComplianceProfileTable } from "./schema/profile-selection";
|
|
21
|
+
|
|
22
|
+
export interface ResolveProfileForTenantArgs {
|
|
23
|
+
readonly db: DbRunner;
|
|
24
|
+
readonly tenantId: TenantId;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function resolveProfileForTenant(
|
|
28
|
+
args: ResolveProfileForTenantArgs,
|
|
29
|
+
): Promise<EffectiveComplianceProfile> {
|
|
30
|
+
const row = (await fetchOne(
|
|
31
|
+
args.db,
|
|
32
|
+
tenantComplianceProfileTable,
|
|
33
|
+
eq(tenantComplianceProfileTable["tenantId"], args.tenantId),
|
|
34
|
+
)) as { profileKey: string; override: string | null } | null; // @cast-boundary db-runner
|
|
35
|
+
|
|
36
|
+
if (!row) {
|
|
37
|
+
return resolveComplianceProfile({});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const override = parseOverride(row.override, args.tenantId);
|
|
41
|
+
return resolveComplianceProfile({
|
|
42
|
+
selection: row.profileKey as ComplianceProfileKey, // @cast-boundary engine-payload
|
|
43
|
+
override,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseOverride(
|
|
48
|
+
raw: string | null,
|
|
49
|
+
tenantId: string,
|
|
50
|
+
): ComplianceProfileOverride | undefined {
|
|
51
|
+
if (!raw || raw.trim() === "") return undefined;
|
|
52
|
+
try {
|
|
53
|
+
const parsed: unknown = JSON.parse(raw);
|
|
54
|
+
return parsed as ComplianceProfileOverride; // @cast-boundary engine-payload
|
|
55
|
+
} catch (e: unknown) {
|
|
56
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
57
|
+
// biome-ignore lint/suspicious/noConsole: operator visibility for DB-corruption edge-case
|
|
58
|
+
console.warn(
|
|
59
|
+
`[compliance-profiles:resolve-for-tenant] tenant ${tenantId}: stored override is not valid JSON, ignoring. Reason: ${reason}`,
|
|
60
|
+
);
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { buildDrizzleTable } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
+
import {
|
|
3
|
+
createEntity,
|
|
4
|
+
createLongTextField,
|
|
5
|
+
createSelectField,
|
|
6
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
|
+
|
|
8
|
+
// Tenant-1-zu-1: Pro Tenant genau eine Profile-Wahl.
|
|
9
|
+
//
|
|
10
|
+
// Architektur-Entscheidung (2026-05-06): Profile-Selection lebt als
|
|
11
|
+
// separate Entity im compliance-profiles-Feature, NICHT als config-key
|
|
12
|
+
// im tenant-Feature. Begruendung:
|
|
13
|
+
// (a) override ist strukturiertes JSON, config-key-Pattern (timezone/
|
|
14
|
+
// locale) ist key-value-flach
|
|
15
|
+
// (b) Profile-Wechsel ist audit-relevant — Event-Store gibt das
|
|
16
|
+
// automatisch fuer Entity-Writes
|
|
17
|
+
// (c) Plan-Files in docs/plans/datenschutz/compliance-profiles.md
|
|
18
|
+
// nennen sie explizit als tenantComplianceProfile-Entity
|
|
19
|
+
//
|
|
20
|
+
// Wer in 6 Monaten zweifelt warum nicht config-key: siehe oben.
|
|
21
|
+
//
|
|
22
|
+
// override als JSON-String in longText: kein dedizierter jsonField-Typ
|
|
23
|
+
// im Framework; embedded hat festes Schema, das hier dynamisch ist.
|
|
24
|
+
// Zod-Validation beim set-profile-Handler stellt Schema-Konformitaet
|
|
25
|
+
// sicher.
|
|
26
|
+
export const tenantComplianceProfileEntity = createEntity({
|
|
27
|
+
table: "read_tenant_compliance_profiles",
|
|
28
|
+
fields: {
|
|
29
|
+
profileKey: createSelectField({
|
|
30
|
+
required: true,
|
|
31
|
+
options: ["eu-dsgvo", "swiss-dsg", "de-hr-dsgvo-hgb", "minimal-no-region"] as const,
|
|
32
|
+
}),
|
|
33
|
+
// override: JSON-String mit Partial-ComplianceProfile. NULL/leer
|
|
34
|
+
// bedeutet "Default-Profile, keine Override". Validiert beim
|
|
35
|
+
// set-profile-Handler via Zod.
|
|
36
|
+
override: createLongTextField({
|
|
37
|
+
allowPlaintext: "is-business-data",
|
|
38
|
+
}),
|
|
39
|
+
},
|
|
40
|
+
indexes: [
|
|
41
|
+
// Pro Tenant nur EIN Profile-Datensatz. Boot-Validator-Comment in
|
|
42
|
+
// EntityIndexDef warnt vor single-column-tenantId-Index als redundant
|
|
43
|
+
// — UNIQUE-Constraint ist hier aber semantisch noetig (1:1-Relation)
|
|
44
|
+
// und nicht nur Performance-Hint, daher explizit deklariert.
|
|
45
|
+
{ unique: true, columns: ["tenantId"] },
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const tenantComplianceProfileTable = buildDrizzleTable(
|
|
50
|
+
"tenantComplianceProfile",
|
|
51
|
+
tenantComplianceProfileEntity,
|
|
52
|
+
);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Test-Helper für compliance-profiles. Legt einen Profile-Eintrag direkt
|
|
2
|
+
// über den Event-Store-Executor an — gleicher Pfad wie der echte
|
|
3
|
+
// set-profile-Handler, aber ohne Zod-Schema-Engung (akzeptiert
|
|
4
|
+
// minimal-no-region für Migration-Edge-Case-Tests) und ohne Access-
|
|
5
|
+
// Check. Idempotent: zweiter Call mit gleichem tenantId updated.
|
|
6
|
+
//
|
|
7
|
+
// Sprint 2 user-data-rights nutzt das fuer Test-Setup ("user kann
|
|
8
|
+
// Daten exportieren mit profile X" — pro Test ein frischer Tenant +
|
|
9
|
+
// Profile-Wahl in einem Helper-Call).
|
|
10
|
+
//
|
|
11
|
+
// Pattern matched seedTextBlock aus text-content.
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
ComplianceProfileKey,
|
|
15
|
+
ComplianceProfileOverride,
|
|
16
|
+
} from "@cosmicdrift/kumiko-framework/compliance";
|
|
17
|
+
import {
|
|
18
|
+
createEventStoreExecutor,
|
|
19
|
+
createTenantDb,
|
|
20
|
+
type DbConnection,
|
|
21
|
+
fetchOne,
|
|
22
|
+
} from "@cosmicdrift/kumiko-framework/db";
|
|
23
|
+
import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
24
|
+
import { TestUsers } from "@cosmicdrift/kumiko-framework/stack";
|
|
25
|
+
import { eq } from "drizzle-orm";
|
|
26
|
+
import {
|
|
27
|
+
tenantComplianceProfileEntity,
|
|
28
|
+
tenantComplianceProfileTable,
|
|
29
|
+
} from "./schema/profile-selection";
|
|
30
|
+
|
|
31
|
+
const executor = createEventStoreExecutor(
|
|
32
|
+
tenantComplianceProfileTable,
|
|
33
|
+
tenantComplianceProfileEntity,
|
|
34
|
+
{ entityName: "tenant-compliance-profile" },
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
export type SeedComplianceProfileOptions = {
|
|
38
|
+
readonly tenantId: TenantId;
|
|
39
|
+
readonly profileKey: ComplianceProfileKey;
|
|
40
|
+
readonly override?: ComplianceProfileOverride;
|
|
41
|
+
readonly by?: SessionUser;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export async function seedComplianceProfile(
|
|
45
|
+
db: DbConnection,
|
|
46
|
+
opts: SeedComplianceProfileOptions,
|
|
47
|
+
): Promise<{ id: string | number }> {
|
|
48
|
+
// user.tenantId muss === opts.tenantId sein damit Event-Store-Stream
|
|
49
|
+
// + Projection im selben Tenant-Bucket landen (Memory:
|
|
50
|
+
// feedback_event_store_tenant_consistency).
|
|
51
|
+
const by = opts.by ?? { ...TestUsers.systemAdmin, tenantId: opts.tenantId };
|
|
52
|
+
const tdb = createTenantDb(db, opts.tenantId, "system");
|
|
53
|
+
const overrideJson = opts.override !== undefined ? JSON.stringify(opts.override) : null;
|
|
54
|
+
|
|
55
|
+
const existing = (await fetchOne(
|
|
56
|
+
db,
|
|
57
|
+
tenantComplianceProfileTable,
|
|
58
|
+
eq(tenantComplianceProfileTable["tenantId"], opts.tenantId),
|
|
59
|
+
)) as { id: string; version: number } | null; // @cast-boundary db-runner
|
|
60
|
+
|
|
61
|
+
if (existing) {
|
|
62
|
+
const result = await executor.update(
|
|
63
|
+
{
|
|
64
|
+
id: existing.id,
|
|
65
|
+
version: existing.version,
|
|
66
|
+
changes: { profileKey: opts.profileKey, override: overrideJson },
|
|
67
|
+
},
|
|
68
|
+
by,
|
|
69
|
+
tdb,
|
|
70
|
+
);
|
|
71
|
+
if (!result.isSuccess) {
|
|
72
|
+
throw new Error(`seedComplianceProfile update failed: ${JSON.stringify(result)}`);
|
|
73
|
+
}
|
|
74
|
+
return { id: existing.id };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const result = await executor.create(
|
|
78
|
+
{
|
|
79
|
+
profileKey: opts.profileKey,
|
|
80
|
+
override: overrideJson,
|
|
81
|
+
tenantId: opts.tenantId,
|
|
82
|
+
},
|
|
83
|
+
by,
|
|
84
|
+
tdb,
|
|
85
|
+
);
|
|
86
|
+
if (!result.isSuccess) {
|
|
87
|
+
throw new Error(`seedComplianceProfile create failed: ${JSON.stringify(result)}`);
|
|
88
|
+
}
|
|
89
|
+
// @cast-boundary db-row: executor.create-result enthält die inserted
|
|
90
|
+
// Row als Record<string, unknown>; id ist nach INSERT garantiert.
|
|
91
|
+
const data = result.data as { id?: string };
|
|
92
|
+
if (data.id === undefined) {
|
|
93
|
+
throw new Error("seedComplianceProfile: executor.create did not return an id");
|
|
94
|
+
}
|
|
95
|
+
return { id: data.id };
|
|
96
|
+
}
|
package/src/config/resolver.ts
CHANGED
|
@@ -179,7 +179,7 @@ export function createConfigResolver(options: ConfigResolverOptions = {}): Confi
|
|
|
179
179
|
|
|
180
180
|
const result = new Map<string, ConfigRow>();
|
|
181
181
|
for (const row of rows) {
|
|
182
|
-
const r = row as ConfigRow;
|
|
182
|
+
const r = row as ConfigRow; // @cast-boundary db-row
|
|
183
183
|
// Higher specificity wins: user > tenant > system. Under the ES
|
|
184
184
|
// schema system rows carry SYSTEM_TENANT_ID instead of NULL, so the
|
|
185
185
|
// "tenant set" check compares against the sentinel rather than null.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Boot-Smoke-Test (S2.D2.5 M4) — verifiziert dass das data-retention-
|
|
2
|
+
// Feature im setupTestStack hochfaehrt + Entity-Schema valide ist.
|
|
3
|
+
//
|
|
4
|
+
// Tiefere Integration (Cleanup-Job mit DB-Operations + Strategy-Dispatch
|
|
5
|
+
// + Cron-Trigger) kommt in S2.D2b. Dieser Smoke-Test fanngt frueh ab
|
|
6
|
+
// ob Boot-Validation oder Entity-Definition gebrochen ist — pre-S2.D2b
|
|
7
|
+
// Sicherheitsnetz.
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
setupTestStack,
|
|
11
|
+
type TestStack,
|
|
12
|
+
unsafeCreateEntityTable,
|
|
13
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
14
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
15
|
+
import { createDataRetentionFeature, tenantRetentionOverrideEntity } from "../feature";
|
|
16
|
+
|
|
17
|
+
let stack: TestStack;
|
|
18
|
+
|
|
19
|
+
const feature = createDataRetentionFeature();
|
|
20
|
+
|
|
21
|
+
beforeAll(async () => {
|
|
22
|
+
stack = await setupTestStack({ features: [feature] });
|
|
23
|
+
await unsafeCreateEntityTable(stack.db, tenantRetentionOverrideEntity);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterAll(async () => {
|
|
27
|
+
await stack.cleanup();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("data-retention :: feature-definition smoke", () => {
|
|
31
|
+
test("Feature laedt clean (Boot-Validation passed)", () => {
|
|
32
|
+
// setupTestStack hat das Feature im beforeAll geladen — wenn Boot-
|
|
33
|
+
// Validation fehlschlaegt, waere dieser Block nie gelaufen.
|
|
34
|
+
expect(stack).toBeDefined();
|
|
35
|
+
expect(feature.name).toBe("data-retention");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("tenantRetentionOverride-Entity ist registriert", () => {
|
|
39
|
+
expect(feature.entities["tenant-retention-override"]).toBeDefined();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("Entity-Definition hat UNIQUE(tenantId, entityName) als 1:1-Constraint", () => {
|
|
43
|
+
const entity = feature.entities["tenant-retention-override"];
|
|
44
|
+
const indexes = entity?.indexes ?? [];
|
|
45
|
+
const uniqueIndex = indexes.find((i) => i.unique === true);
|
|
46
|
+
expect(uniqueIndex).toBeDefined();
|
|
47
|
+
expect(uniqueIndex?.columns).toEqual(["tenantId", "entityName"]);
|
|
48
|
+
});
|
|
49
|
+
});
|