@cosmicdrift/kumiko-bundled-features 0.314.0 → 0.315.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/package.json +9 -9
- package/src/compliance-profiles/resolve-for-tenant.ts +15 -5
- package/src/data-retention/__tests__/retention-cleanup-files.integration.test.ts +6 -6
- package/src/data-retention/__tests__/retention-cleanup-kms.integration.test.ts +1 -1
- package/src/data-retention/__tests__/retention-cleanup.integration.test.ts +10 -10
- package/src/data-retention/changes.json +6 -0
- package/src/data-retention/feature.ts +1 -1
- package/src/data-retention/handlers/policy-for.query.ts +14 -31
- package/src/data-retention/index.ts +2 -0
- package/src/data-retention/resolve-for-tenant.ts +25 -17
- package/src/data-retention/resolve-tenant-preset.ts +5 -2
- package/src/data-retention/run-retention-cleanup.ts +5 -4
- package/src/delivery/__tests__/delivery.integration.test.ts +189 -19
- package/src/delivery/address-opt-out.ts +49 -21
- package/src/delivery/changes.json +12 -0
- package/src/delivery/constants.ts +1 -0
- package/src/delivery/delivery-service.ts +48 -27
- package/src/delivery/feature.ts +5 -1
- package/src/delivery/handlers/unsubscribe-address.write.ts +40 -0
- package/src/delivery/handlers/unsubscribe-user.write.ts +43 -0
- package/src/delivery/index.ts +1 -0
- package/src/delivery/public-names.ts +6 -0
- package/src/delivery/unsubscribe.ts +144 -67
- package/src/delivery/upsert-preference.ts +74 -60
- package/src/document-ingest-foundation/__tests__/feature.integration.test.ts +1 -0
- package/src/file-derivatives/__tests__/public-variant-cross-tenant.integration.test.ts +2 -1
- package/src/file-derivatives/__tests__/public-variant-predicate-args.integration.test.ts +2 -1
- package/src/file-derivatives/__tests__/public-variant-route.integration.test.ts +4 -1
- package/src/shared/index.ts +1 -0
- package/src/shared/is-tenant-db.ts +9 -0
- package/src/user-data-rights/__tests__/file-storage-unification.integration.test.ts +5 -1
- package/src/user-data-rights/__tests__/retention-preset-forget.integration.test.ts +272 -0
- package/src/user-data-rights/run-forget-cleanup.ts +6 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.315.0",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -133,12 +133,12 @@
|
|
|
133
133
|
"./workflow-runner": "./src/workflow-runner/index.ts"
|
|
134
134
|
},
|
|
135
135
|
"dependencies": {
|
|
136
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
137
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
138
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
139
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
140
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
141
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
136
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.315.0",
|
|
137
|
+
"@cosmicdrift/kumiko-framework": "0.315.0",
|
|
138
|
+
"@cosmicdrift/kumiko-headless": "0.315.0",
|
|
139
|
+
"@cosmicdrift/kumiko-renderer": "0.315.0",
|
|
140
|
+
"@cosmicdrift/kumiko-renderer-web": "0.315.0",
|
|
141
|
+
"@cosmicdrift/kumiko-types": "0.315.0",
|
|
142
142
|
"@mollie/api-client": "^4.5.0",
|
|
143
143
|
"@node-rs/argon2": "^2.0.2",
|
|
144
144
|
"@types/mailparser": "^3.4.6",
|
|
@@ -167,8 +167,8 @@
|
|
|
167
167
|
],
|
|
168
168
|
"devDependencies": {
|
|
169
169
|
"@testing-library/user-event": "^14.6.1",
|
|
170
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
171
|
-
"@cosmicdrift/kumiko-locale-es": "0.
|
|
170
|
+
"@cosmicdrift/kumiko-locale-de": "0.315.0",
|
|
171
|
+
"@cosmicdrift/kumiko-locale-es": "0.315.0",
|
|
172
172
|
"jsqr": "^1.4.0"
|
|
173
173
|
}
|
|
174
174
|
}
|
|
@@ -14,22 +14,32 @@ import {
|
|
|
14
14
|
type EffectiveComplianceProfile,
|
|
15
15
|
resolveComplianceProfile,
|
|
16
16
|
} from "@cosmicdrift/kumiko-framework/compliance";
|
|
17
|
-
import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
17
|
+
import type { DbRunner, TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
18
18
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
19
|
+
import { isTenantDb } from "../shared";
|
|
19
20
|
import { parseComplianceProfileOverride } from "./_internal/parse-override";
|
|
20
21
|
import { tenantComplianceProfileTable } from "./schema/profile-selection";
|
|
21
22
|
|
|
22
23
|
export interface ResolveProfileForTenantArgs {
|
|
23
|
-
|
|
24
|
+
// data-retention's resolveTenantRetentionPreset forwards this via
|
|
25
|
+
// resolveRetentionPolicyForTenant, whose own `db` is DbRunner | TenantDb
|
|
26
|
+
// (bulk cron vs. per-hook TenantDb). This lookup is a plain tenant-scoped
|
|
27
|
+
// read either way.
|
|
28
|
+
readonly db: DbRunner | TenantDb;
|
|
24
29
|
readonly tenantId: TenantId;
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
export async function resolveProfileForTenant(
|
|
28
33
|
args: ResolveProfileForTenantArgs,
|
|
29
34
|
): Promise<EffectiveComplianceProfile> {
|
|
30
|
-
const row = (
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
const row = isTenantDb(args.db)
|
|
36
|
+
? ((await args.db.fetchOne<{ profileKey: string; override: string | null }>(
|
|
37
|
+
tenantComplianceProfileTable,
|
|
38
|
+
{ tenantId: args.tenantId },
|
|
39
|
+
)) ?? null)
|
|
40
|
+
: ((await fetchOne(args.db, tenantComplianceProfileTable, {
|
|
41
|
+
tenantId: args.tenantId,
|
|
42
|
+
})) as { profileKey: string; override: string | null } | null); // @cast-boundary db-runner
|
|
33
43
|
|
|
34
44
|
if (!row) {
|
|
35
45
|
return resolveComplianceProfile({});
|
|
@@ -249,7 +249,7 @@ describe("runRetentionCleanup :: hardDelete purges file bytes + derivatives + fi
|
|
|
249
249
|
db: stack.db,
|
|
250
250
|
registry: stack.registry,
|
|
251
251
|
tenantId: T1,
|
|
252
|
-
|
|
252
|
+
preloadedTenantPreset: null,
|
|
253
253
|
now,
|
|
254
254
|
files: filesCtx(),
|
|
255
255
|
});
|
|
@@ -289,7 +289,7 @@ describe("runRetentionCleanup :: hardDelete purges file bytes + derivatives + fi
|
|
|
289
289
|
db: stack.db,
|
|
290
290
|
registry: stack.registry,
|
|
291
291
|
tenantId: T1,
|
|
292
|
-
|
|
292
|
+
preloadedTenantPreset: null,
|
|
293
293
|
now,
|
|
294
294
|
files: filesCtx(),
|
|
295
295
|
});
|
|
@@ -324,7 +324,7 @@ describe("runRetentionCleanup :: hardDelete purges file bytes + derivatives + fi
|
|
|
324
324
|
db: stack.db,
|
|
325
325
|
registry: stack.registry,
|
|
326
326
|
tenantId: T1,
|
|
327
|
-
|
|
327
|
+
preloadedTenantPreset: null,
|
|
328
328
|
now,
|
|
329
329
|
files: filesCtx(failingProvider),
|
|
330
330
|
});
|
|
@@ -339,7 +339,7 @@ describe("runRetentionCleanup :: hardDelete purges file bytes + derivatives + fi
|
|
|
339
339
|
db: stack.db,
|
|
340
340
|
registry: stack.registry,
|
|
341
341
|
tenantId: T1,
|
|
342
|
-
|
|
342
|
+
preloadedTenantPreset: null,
|
|
343
343
|
now,
|
|
344
344
|
files: filesCtx(),
|
|
345
345
|
});
|
|
@@ -365,7 +365,7 @@ describe("runRetentionCleanup :: hardDelete purges file bytes + derivatives + fi
|
|
|
365
365
|
db: stack.db,
|
|
366
366
|
registry: stack.registry,
|
|
367
367
|
tenantId: T1,
|
|
368
|
-
|
|
368
|
+
preloadedTenantPreset: null,
|
|
369
369
|
now,
|
|
370
370
|
});
|
|
371
371
|
|
|
@@ -405,7 +405,7 @@ describe("runRetentionCleanup :: hardDelete purges file bytes + derivatives + fi
|
|
|
405
405
|
db: stack.db,
|
|
406
406
|
registry: stack.registry,
|
|
407
407
|
tenantId: T1,
|
|
408
|
-
|
|
408
|
+
preloadedTenantPreset: null,
|
|
409
409
|
now,
|
|
410
410
|
files: filesCtx(),
|
|
411
411
|
});
|
|
@@ -206,7 +206,7 @@ describe("runRetentionCleanup :: real postgres", () => {
|
|
|
206
206
|
db: stack.db,
|
|
207
207
|
registry: stack.registry,
|
|
208
208
|
tenantId: T1,
|
|
209
|
-
|
|
209
|
+
preloadedTenantPreset: null,
|
|
210
210
|
now,
|
|
211
211
|
});
|
|
212
212
|
|
|
@@ -223,7 +223,7 @@ describe("runRetentionCleanup :: real postgres", () => {
|
|
|
223
223
|
db: stack.db,
|
|
224
224
|
registry: stack.registry,
|
|
225
225
|
tenantId: T1,
|
|
226
|
-
|
|
226
|
+
preloadedTenantPreset: null,
|
|
227
227
|
now,
|
|
228
228
|
});
|
|
229
229
|
|
|
@@ -240,7 +240,7 @@ describe("runRetentionCleanup :: real postgres", () => {
|
|
|
240
240
|
db: stack.db,
|
|
241
241
|
registry: stack.registry,
|
|
242
242
|
tenantId: T1,
|
|
243
|
-
|
|
243
|
+
preloadedTenantPreset: null,
|
|
244
244
|
now,
|
|
245
245
|
});
|
|
246
246
|
|
|
@@ -254,7 +254,7 @@ describe("runRetentionCleanup :: real postgres", () => {
|
|
|
254
254
|
db: stack.db,
|
|
255
255
|
registry: stack.registry,
|
|
256
256
|
tenantId: T1,
|
|
257
|
-
|
|
257
|
+
preloadedTenantPreset: null,
|
|
258
258
|
now,
|
|
259
259
|
});
|
|
260
260
|
|
|
@@ -301,7 +301,7 @@ describe("runRetentionCleanup :: real postgres", () => {
|
|
|
301
301
|
db: stack.db,
|
|
302
302
|
registry: stack.registry,
|
|
303
303
|
tenantId: T1,
|
|
304
|
-
|
|
304
|
+
preloadedTenantPreset: null,
|
|
305
305
|
now,
|
|
306
306
|
});
|
|
307
307
|
|
|
@@ -326,7 +326,7 @@ describe("runRetentionCleanup :: real postgres", () => {
|
|
|
326
326
|
db: stack.db,
|
|
327
327
|
registry: stack.registry,
|
|
328
328
|
tenantId: T1,
|
|
329
|
-
|
|
329
|
+
preloadedTenantPreset: null,
|
|
330
330
|
now,
|
|
331
331
|
});
|
|
332
332
|
|
|
@@ -351,7 +351,7 @@ describe("runRetentionCleanup :: real postgres", () => {
|
|
|
351
351
|
db: stack.db,
|
|
352
352
|
registry: stack.registry,
|
|
353
353
|
tenantId: T1,
|
|
354
|
-
|
|
354
|
+
preloadedTenantPreset: null,
|
|
355
355
|
now,
|
|
356
356
|
});
|
|
357
357
|
expect(first.anonymized).toBe(1);
|
|
@@ -364,7 +364,7 @@ describe("runRetentionCleanup :: real postgres", () => {
|
|
|
364
364
|
db: stack.db,
|
|
365
365
|
registry: stack.registry,
|
|
366
366
|
tenantId: T1,
|
|
367
|
-
|
|
367
|
+
preloadedTenantPreset: null,
|
|
368
368
|
now,
|
|
369
369
|
});
|
|
370
370
|
expect(second.anonymized).toBe(0);
|
|
@@ -388,7 +388,7 @@ describe("runRetentionCleanup :: real postgres", () => {
|
|
|
388
388
|
db: stack.db,
|
|
389
389
|
registry: stack.registry,
|
|
390
390
|
tenantId: T1,
|
|
391
|
-
|
|
391
|
+
preloadedTenantPreset: null,
|
|
392
392
|
now,
|
|
393
393
|
batchLimit: 1,
|
|
394
394
|
});
|
|
@@ -412,7 +412,7 @@ describe("runRetentionCleanup :: real postgres", () => {
|
|
|
412
412
|
db: stack.db,
|
|
413
413
|
registry: stack.registry,
|
|
414
414
|
tenantId: T1,
|
|
415
|
-
|
|
415
|
+
preloadedTenantPreset: null,
|
|
416
416
|
now,
|
|
417
417
|
});
|
|
418
418
|
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.315.0",
|
|
4
|
+
"type": "breaking",
|
|
5
|
+
"title": "forget/policy-for now honor the tenant retention preset",
|
|
6
|
+
"migration": "ResolveForTenantArgs.tenantPreset was renamed to preloadedTenantPreset (mirrors preloadedOverride): omitting it now makes the resolver load the tenant's retention preset itself instead of skipping it. Behavior change: forget (Art. 17) and the policy-for query now honor the tenant's compliance-profile-derived retention preset, not just entity defaults and per-tenant overrides. For tenants with a mapped compliance profile (e.g. de-hr-dsgvo-hgb), forget now keeps invoice/booking/contract rows via blockDelete and anonymizes order rows instead of hard-deleting them; notes-history mentions on such hosts are no longer shredded when the mentioned note's host entity is preset-protected. policy-for (data-retention:query:policy-for) now returns source: \"preset\" where it previously returned \"none\" for these entities."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.313.0",
|
|
4
10
|
"type": "fix",
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
1
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
2
|
import * as z from "zod";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { tenantRetentionOverrideTable } from "../schema/tenant-retention-override";
|
|
3
|
+
import { resolveRetentionPolicyForTenant } from "../resolve-for-tenant";
|
|
4
|
+
import type { EffectiveRetentionPolicy } from "../resolver";
|
|
7
5
|
|
|
8
6
|
// retention:query:policy-for — Cross-Feature-API fuer den Forget-Flow.
|
|
9
7
|
//
|
|
@@ -11,9 +9,9 @@ import { tenantRetentionOverrideTable } from "../schema/tenant-retention-overrid
|
|
|
11
9
|
// Forget mit "delete" oder "anonymize" oder "blockDelete-bis-Frist"
|
|
12
10
|
// laufen soll. Plus Cleanup-Job-Sprint-2.D2b fuer das gleiche.
|
|
13
11
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
12
|
+
// Delegates to resolveRetentionPolicyForTenant so forget, the cleanup cron and
|
|
13
|
+
// this query can never disagree about the tenant preset. Letting the resolver
|
|
14
|
+
// load the preset itself costs one read per call, fine for a single lookup.
|
|
17
15
|
//
|
|
18
16
|
// access: openToAll — andere Features im selben Tenant duerfen das
|
|
19
17
|
// abrufen. Keine PII im Result, nur Policy-Metadata.
|
|
@@ -33,31 +31,16 @@ export const policyForQuery = defineQueryHandler({
|
|
|
33
31
|
description:
|
|
34
32
|
"Resolves the effective retention policy for one entity name in the caller's tenant (keep-for duration plus delete or anonymize strategy) by layering the entity default, tenant preset and tenant override, so a forget flow or cleanup job knows how that data may be removed.",
|
|
35
33
|
handler: async (query, ctx): Promise<EffectiveRetentionPolicy> => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
if (!ctx.registry) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"data-retention:policy-for: ctx.registry required (HandlerContext incomplete)",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return resolveRetentionPolicyForTenant({
|
|
40
|
+
db: ctx.db,
|
|
41
|
+
registry: ctx.registry,
|
|
40
42
|
tenantId: query.user.tenantId,
|
|
41
|
-
entityName,
|
|
42
|
-
})) as { config: string | null } | null; // @cast-boundary db-runner
|
|
43
|
-
|
|
44
|
-
const tenantOverride = parseRetentionOverrideOrNull(
|
|
45
|
-
overrideRow?.config ?? null,
|
|
46
|
-
query.user.tenantId,
|
|
47
|
-
"data-retention:policy-for",
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
// Layer 1: Entity-Default aus Registry
|
|
51
|
-
const entityDef = ctx.registry?.getEntity(entityName) ?? null;
|
|
52
|
-
|
|
53
|
-
// Layer 2: Tenant-Preset — Storage kommt mit S2.D2b. Bis dahin null.
|
|
54
|
-
const tenantPreset = null;
|
|
55
|
-
|
|
56
|
-
return resolveRetentionPolicy({
|
|
57
|
-
entityName,
|
|
58
|
-
entityDef,
|
|
59
|
-
tenantPreset,
|
|
60
|
-
tenantOverride,
|
|
43
|
+
entityName: query.payload.entityName,
|
|
61
44
|
});
|
|
62
45
|
},
|
|
63
46
|
});
|
|
@@ -2,6 +2,7 @@ export type {
|
|
|
2
2
|
EffectiveRetentionPolicy,
|
|
3
3
|
ResolveForTenantArgs,
|
|
4
4
|
ResolveRetentionPolicyArgs,
|
|
5
|
+
ResolveTenantPresetArgs,
|
|
5
6
|
RetentionOverride,
|
|
6
7
|
RetentionPreset,
|
|
7
8
|
RetentionPresetKey,
|
|
@@ -11,6 +12,7 @@ export {
|
|
|
11
12
|
RETENTION_PRESETS,
|
|
12
13
|
resolveRetentionPolicy,
|
|
13
14
|
resolveRetentionPolicyForTenant,
|
|
15
|
+
resolveTenantRetentionPreset,
|
|
14
16
|
retentionOverrideSchema,
|
|
15
17
|
SELECTABLE_RETENTION_PRESETS,
|
|
16
18
|
tenantRetentionOverrideEntity,
|
|
@@ -1,23 +1,17 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// API fuer einzelne Lookups. Der Cleanup-Runner iteriert N Entities × M
|
|
5
|
-
// Tenants — Handler-Roundtrip pro Lookup waere zu teuer + braucht einen
|
|
6
|
-
// HandlerContext. Beide Pfade nutzen denselben `parseRetentionOverrideOrNull`
|
|
7
|
-
// + `resolveRetentionPolicy`, also kein Drift-Risiko.
|
|
1
|
+
// Single implementation of the three-layer policy resolution: the cleanup cron,
|
|
2
|
+
// the forget runner and the `policy-for` query all go through here, so a caller
|
|
3
|
+
// cannot skip a layer (e.g. the tenant preset) by accident.
|
|
8
4
|
|
|
9
5
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
10
6
|
import type { DbRunner, TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
11
7
|
import type { Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
8
|
+
import { isTenantDb } from "../shared";
|
|
12
9
|
import { parseRetentionOverrideOrNull } from "./_internal/parse-override";
|
|
13
10
|
import type { RetentionPresetKey } from "./presets";
|
|
11
|
+
import { resolveTenantRetentionPreset } from "./resolve-tenant-preset";
|
|
14
12
|
import { type EffectiveRetentionPolicy, resolveRetentionPolicy } from "./resolver";
|
|
15
13
|
import { tenantRetentionOverrideTable } from "./schema/tenant-retention-override";
|
|
16
14
|
|
|
17
|
-
function isTenantDb(db: DbRunner | TenantDb): db is TenantDb {
|
|
18
|
-
return typeof (db as Partial<TenantDb>).unsafeRaw === "function";
|
|
19
|
-
}
|
|
20
|
-
|
|
21
15
|
export interface ResolveForTenantArgs {
|
|
22
16
|
// fw#2914 — a EXT_USER_DATA hook's ctx.db is a TenantDb (method-form);
|
|
23
17
|
// the cleanup cron's own db is still a raw DbRunner. Both are valid here —
|
|
@@ -27,12 +21,17 @@ export interface ResolveForTenantArgs {
|
|
|
27
21
|
readonly tenantId: TenantId;
|
|
28
22
|
readonly entityName: string;
|
|
29
23
|
/**
|
|
30
|
-
* Layer 2 — Tenant-Preset
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
24
|
+
* Layer 2 — Tenant-Preset. `undefined` (the default) means this resolver
|
|
25
|
+
* derives it itself via resolveTenantRetentionPreset — one compliance-
|
|
26
|
+
* profile read per call, fine for single-lookup callers (`policy-for`, a
|
|
27
|
+
* per-hook TenantDb caller like notes-history-user-data). A bulk caller
|
|
28
|
+
* iterating N entities × M tenants (the forget-cleanup runner, the
|
|
29
|
+
* retention-cleanup cron) MUST resolve it ONCE per tenant and pass it in
|
|
30
|
+
* here instead, or it re-derives per entity. `null` means "resolved
|
|
31
|
+
* already, no preset applies" — the resolver falls back to Entity-Default
|
|
32
|
+
* (Layer 1) + Tenant-Override (Layer 3).
|
|
34
33
|
*/
|
|
35
|
-
readonly
|
|
34
|
+
readonly preloadedTenantPreset?: RetentionPresetKey | null;
|
|
36
35
|
/**
|
|
37
36
|
* Pre-fetched override row for this (tenantId, entityName) pair — lets a
|
|
38
37
|
* bulk caller (the cleanup cron, N entities × M tenants) read every
|
|
@@ -68,10 +67,19 @@ export async function resolveRetentionPolicyForTenant(
|
|
|
68
67
|
|
|
69
68
|
const entityDef = args.registry.getEntity(args.entityName) ?? null;
|
|
70
69
|
|
|
70
|
+
const tenantPreset =
|
|
71
|
+
args.preloadedTenantPreset !== undefined
|
|
72
|
+
? args.preloadedTenantPreset
|
|
73
|
+
: await resolveTenantRetentionPreset({
|
|
74
|
+
db: args.db,
|
|
75
|
+
registry: args.registry,
|
|
76
|
+
tenantId: args.tenantId,
|
|
77
|
+
});
|
|
78
|
+
|
|
71
79
|
return resolveRetentionPolicy({
|
|
72
80
|
entityName: args.entityName,
|
|
73
81
|
entityDef,
|
|
74
|
-
tenantPreset
|
|
82
|
+
tenantPreset,
|
|
75
83
|
tenantOverride,
|
|
76
84
|
});
|
|
77
85
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// sogar namensgleich. minimal-no-region → "default" (No-Op-Preset).
|
|
13
13
|
|
|
14
14
|
import type { ComplianceProfileKey } from "@cosmicdrift/kumiko-framework/compliance";
|
|
15
|
-
import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
15
|
+
import type { DbRunner, TenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
16
16
|
import type { Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
17
17
|
import { resolveProfileForTenant } from "../compliance-profiles";
|
|
18
18
|
import type { RetentionPresetKey } from "./presets";
|
|
@@ -29,7 +29,10 @@ const PROFILE_TO_PRESET: Readonly<Record<ComplianceProfileKey, RetentionPresetKe
|
|
|
29
29
|
} satisfies Readonly<Record<ComplianceProfileKey, RetentionPresetKey>>;
|
|
30
30
|
|
|
31
31
|
export interface ResolveTenantPresetArgs {
|
|
32
|
-
|
|
32
|
+
// resolveRetentionPolicyForTenant calls this itself when its own
|
|
33
|
+
// caller omits preloadedTenantPreset. Its `db` is DbRunner | TenantDb (bulk
|
|
34
|
+
// cron vs. per-hook TenantDb), so this must accept both too.
|
|
35
|
+
readonly db: DbRunner | TenantDb;
|
|
33
36
|
readonly registry: Registry;
|
|
34
37
|
readonly tenantId: TenantId;
|
|
35
38
|
}
|
|
@@ -95,8 +95,9 @@ export interface RunRetentionCleanupArgs {
|
|
|
95
95
|
readonly db: DbRunner;
|
|
96
96
|
readonly registry: Registry;
|
|
97
97
|
readonly tenantId: TenantId;
|
|
98
|
-
/** Layer-2
|
|
99
|
-
|
|
98
|
+
/** Layer-2 preset from resolveTenantRetentionPreset, resolved once per tenant
|
|
99
|
+
* by the caller rather than per entity here. null = layers 1 and 3 only. */
|
|
100
|
+
readonly preloadedTenantPreset: RetentionPresetKey | null;
|
|
100
101
|
/** Now-Injection — Tests pinnen den Wert ohne Date-Mock (Pattern keep-for.ts). */
|
|
101
102
|
readonly now: Instant;
|
|
102
103
|
readonly batchLimit?: number;
|
|
@@ -587,7 +588,7 @@ async function anonymizeMatchingRows(
|
|
|
587
588
|
export async function runRetentionCleanup(
|
|
588
589
|
args: RunRetentionCleanupArgs,
|
|
589
590
|
): Promise<RunRetentionCleanupResult> {
|
|
590
|
-
const { db, registry, tenantId,
|
|
591
|
+
const { db, registry, tenantId, preloadedTenantPreset, now } = args;
|
|
591
592
|
const batchLimit = args.batchLimit ?? DEFAULT_BATCH_LIMIT;
|
|
592
593
|
const kms = args.kms ?? configuredPiiSubjectKms();
|
|
593
594
|
|
|
@@ -628,7 +629,7 @@ export async function runRetentionCleanup(
|
|
|
628
629
|
registry,
|
|
629
630
|
tenantId,
|
|
630
631
|
entityName,
|
|
631
|
-
|
|
632
|
+
preloadedTenantPreset,
|
|
632
633
|
preloadedOverride: overrideByEntity.get(entityName) ?? null,
|
|
633
634
|
});
|
|
634
635
|
const policy = resolved.policy;
|