@cosmicdrift/kumiko-bundled-features 0.13.0 → 0.15.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 +6 -6
- package/src/__tests__/env-schemas.test.ts +1 -1
- package/src/__tests__/es-ops-e2e.integration.ts +10 -9
- package/src/audit/__tests__/audit.integration.ts +3 -3
- package/src/audit/handlers/list.query.ts +39 -51
- package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.ts +4 -3
- package/src/auth-email-password/__tests__/account-lockout.integration.ts +4 -3
- package/src/auth-email-password/__tests__/auth-claims.integration.ts +5 -4
- package/src/auth-email-password/__tests__/auth.integration.ts +4 -3
- package/src/auth-email-password/__tests__/confirm-token-flow.test.ts +1 -1
- package/src/auth-email-password/__tests__/email-templates.test.ts +1 -1
- package/src/auth-email-password/__tests__/email-verification.integration.ts +7 -10
- package/src/auth-email-password/__tests__/identity-v3-hash.test.ts +1 -1
- package/src/auth-email-password/__tests__/identity-v3-login.integration.ts +4 -3
- package/src/auth-email-password/__tests__/invite-flow.integration.ts +16 -43
- package/src/auth-email-password/__tests__/multi-roles.integration.ts +6 -9
- package/src/auth-email-password/__tests__/password-reset.integration.ts +8 -7
- package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.ts +4 -3
- package/src/auth-email-password/__tests__/seed-admin.integration.ts +19 -32
- package/src/auth-email-password/__tests__/session-callbacks.integration.ts +6 -5
- package/src/auth-email-password/__tests__/session-strict-mode.integration.ts +1 -1
- package/src/auth-email-password/__tests__/signed-token.test.ts +1 -1
- package/src/auth-email-password/__tests__/signup-flow.integration.ts +11 -15
- package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +26 -26
- package/src/auth-email-password/handlers/invite-accept.write.ts +24 -21
- package/src/auth-email-password/handlers/invite-create.write.ts +3 -8
- package/src/auth-email-password/handlers/invite-signup-complete.write.ts +20 -17
- package/src/auth-email-password/handlers/signup-confirm.write.ts +3 -7
- package/src/auth-email-password/seeding.ts +1 -1
- package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +1 -2
- package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +10 -19
- package/src/auth-email-password/web/__tests__/login-screen.test.tsx +12 -18
- package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +12 -17
- package/src/auth-email-password/web/__tests__/session-roles.test.ts +1 -1
- package/src/auth-email-password/web/__tests__/tenant-switcher.test.tsx +1 -8
- package/src/auth-email-password/web/__tests__/test-utils.tsx +4 -8
- package/src/auth-email-password/web/__tests__/user-menu.test.tsx +2 -8
- package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +10 -15
- package/src/billing-foundation/__tests__/billing-foundation.integration.ts +1 -1
- package/src/billing-foundation/__tests__/feature.test.ts +1 -1
- package/src/billing-foundation/__tests__/webhook-handler.test.ts +6 -5
- package/src/billing-foundation/db/queries/subscription-projection.ts +15 -0
- package/src/billing-foundation/get-subscription-for-tenant.ts +2 -6
- package/src/billing-foundation/handlers/create-portal-session.write.ts +2 -2
- package/src/billing-foundation/handlers/list-subscriptions.query.ts +4 -1
- package/src/billing-foundation/projection.ts +32 -13
- package/src/cap-counter/__tests__/cap-counter.integration.ts +1 -1
- package/src/cap-counter/__tests__/enforce-cap.test.ts +37 -32
- package/src/cap-counter/__tests__/with-cap-enforcement.integration.ts +1 -1
- package/src/cap-counter/enforce-cap.ts +14 -20
- package/src/cap-counter/handlers/get-counter.query.ts +7 -13
- package/src/cap-counter/handlers/increment.write.ts +2 -2
- package/src/cap-counter/handlers/mark-soft-warned.write.ts +2 -2
- package/src/channel-in-app/handlers/inbox.query.ts +7 -13
- package/src/channel-in-app/handlers/mark-all-read.write.ts +7 -9
- package/src/channel-in-app/handlers/mark-read.write.ts +8 -14
- package/src/channel-in-app/handlers/unread-count.query.ts +10 -9
- package/src/channel-in-app/in-app-channel.ts +10 -12
- package/src/channel-in-app/tables.ts +1 -1
- package/src/compliance-profiles/__tests__/compliance-profiles.integration.ts +1 -1
- package/src/compliance-profiles/__tests__/seeding.integration.ts +1 -1
- package/src/compliance-profiles/handlers/for-tenant.query.ts +4 -7
- package/src/compliance-profiles/handlers/needs-profile.query.ts +4 -7
- package/src/compliance-profiles/handlers/set-profile.write.ts +5 -7
- package/src/compliance-profiles/resolve-for-tenant.ts +5 -7
- package/src/compliance-profiles/schema/profile-selection.ts +2 -2
- package/src/compliance-profiles/seeding.ts +4 -7
- package/src/config/__tests__/app-overrides.test.ts +1 -1
- package/src/config/__tests__/cascade.integration.ts +1 -1
- package/src/config/__tests__/config.integration.ts +8 -27
- package/src/config/db/queries/resolver.ts +47 -0
- package/src/config/handlers/__tests__/prepare-config-write.test.ts +1 -1
- package/src/config/resolver.ts +14 -62
- package/src/config/table.ts +4 -4
- package/src/config/write-helpers.ts +7 -11
- package/src/custom-fields/__tests__/audit-integration.integration.ts +6 -6
- package/src/custom-fields/__tests__/custom-fields.integration.ts +7 -7
- package/src/custom-fields/__tests__/feature.test.ts +1 -1
- package/src/custom-fields/__tests__/field-access.integration.ts +6 -6
- package/src/custom-fields/__tests__/quota.integration.ts +6 -6
- package/src/custom-fields/__tests__/retention.integration.ts +12 -10
- package/src/custom-fields/__tests__/user-data-rights.integration.ts +27 -17
- package/src/custom-fields/__tests__/wire-for-entity.test.ts +5 -5
- package/src/custom-fields/db/queries/field-access.ts +16 -0
- package/src/custom-fields/db/queries/projection.ts +43 -0
- package/src/custom-fields/db/queries/quota.ts +14 -0
- package/src/custom-fields/db/queries/retention.ts +39 -0
- package/src/custom-fields/db/queries/user-data-rights.ts +54 -0
- package/src/custom-fields/lib/field-access.ts +2 -41
- package/src/custom-fields/lib/quota.ts +2 -25
- package/src/custom-fields/run-retention.ts +19 -21
- package/src/custom-fields/wire-for-entity.ts +30 -23
- package/src/custom-fields/wire-user-data-rights.ts +33 -85
- package/src/data-retention/__tests__/data-retention.integration.ts +1 -1
- package/src/data-retention/__tests__/keep-for.test.ts +1 -1
- package/src/data-retention/__tests__/override-schema.test.ts +1 -1
- package/src/data-retention/__tests__/policy-for.integration.ts +1 -1
- package/src/data-retention/__tests__/resolver.test.ts +1 -1
- package/src/data-retention/handlers/policy-for.query.ts +5 -8
- package/src/data-retention/resolve-for-tenant.ts +6 -8
- package/src/data-retention/schema/tenant-retention-override.ts +2 -2
- package/src/delivery/__tests__/delivery-events.integration.ts +8 -21
- package/src/delivery/__tests__/delivery.integration.ts +100 -190
- package/src/delivery/db/queries/preferences.ts +30 -0
- package/src/delivery/delivery-service.ts +8 -36
- package/src/delivery/feature.ts +2 -1
- package/src/delivery/handlers/log.query.ts +5 -7
- package/src/delivery/handlers/preferences.query.ts +2 -5
- package/src/delivery/tables.ts +26 -1
- package/src/delivery/upsert-preference.ts +8 -14
- package/src/feature-toggles/__tests__/feature-toggles.integration.ts +30 -30
- package/src/feature-toggles/__tests__/registered-system-tenant.test.ts +7 -6
- package/src/feature-toggles/db/queries/toggle-state.ts +25 -0
- package/src/feature-toggles/feature.ts +16 -2
- package/src/feature-toggles/global-feature-state-table.ts +1 -1
- package/src/feature-toggles/handlers/list.query.ts +9 -2
- package/src/feature-toggles/handlers/registered.query.ts +3 -7
- package/src/feature-toggles/handlers/set.write.ts +37 -25
- package/src/feature-toggles/toggle-runtime.ts +3 -6
- package/src/file-foundation/__tests__/feature.test.ts +1 -1
- package/src/file-foundation/__tests__/file-foundation.integration.ts +1 -1
- package/src/file-provider-inmemory/__tests__/feature.test.ts +1 -1
- package/src/file-provider-s3/__tests__/feature.test.ts +1 -1
- package/src/files/__tests__/files.integration.ts +18 -7
- package/src/files/schema/file-ref.ts +1 -1
- package/src/files-provider-s3/__tests__/env-helper.test.ts +1 -1
- package/src/files-provider-s3/__tests__/s3-provider.integration.ts +1 -1
- package/src/files-provider-s3/__tests__/s3-provider.test.ts +1 -1
- package/src/jobs/__tests__/job-system-user.integration.ts +1 -1
- package/src/jobs/__tests__/jobs-events.integration.ts +8 -21
- package/src/jobs/__tests__/jobs-feature.integration.ts +1 -1
- package/src/jobs/feature.ts +22 -14
- package/src/jobs/handlers/detail.query.ts +10 -8
- package/src/jobs/handlers/list.query.ts +9 -21
- package/src/jobs/handlers/retry.write.ts +2 -7
- package/src/jobs/job-run-logger.ts +3 -9
- package/src/jobs/job-run-table.ts +49 -17
- package/src/legal-pages/__tests__/legal-pages.integration.ts +1 -1
- package/src/mail-foundation/__tests__/feature.test.ts +1 -1
- package/src/mail-foundation/__tests__/mail-foundation.integration.ts +1 -1
- package/src/mail-transport-inmemory/__tests__/feature.test.ts +1 -1
- package/src/mail-transport-smtp/__tests__/feature.test.ts +1 -1
- package/src/rate-limiting/__tests__/rate-limiting.integration.ts +1 -1
- package/src/renderer-foundation/__tests__/api.test.ts +2 -2
- package/src/renderer-foundation/__tests__/collect-plugins.integration.ts +1 -1
- package/src/renderer-simple/__tests__/adapter.test.ts +2 -2
- package/src/renderer-simple/__tests__/simple-renderer.test.ts +1 -1
- package/src/secrets/__tests__/require-secrets-context.test.ts +6 -5
- package/src/secrets/__tests__/rotate.integration.ts +6 -9
- package/src/secrets/__tests__/secrets-events.integration.ts +6 -12
- package/src/secrets/__tests__/secrets.integration.ts +6 -11
- package/src/secrets/db/queries/read.ts +16 -0
- package/src/secrets/handlers/list.query.ts +16 -17
- package/src/secrets/handlers/rotate.job.ts +8 -12
- package/src/secrets/secrets-context.ts +9 -21
- package/src/secrets/table.ts +1 -1
- package/src/sessions/__tests__/cleanup.integration.ts +8 -6
- package/src/sessions/__tests__/password-auto-revoke.integration.ts +7 -6
- package/src/sessions/__tests__/sessions.integration.ts +23 -38
- package/src/sessions/__tests__/test-helpers.ts +1 -1
- package/src/sessions/db/queries/cleanup.ts +21 -0
- package/src/sessions/handlers/cleanup.job.ts +6 -29
- package/src/sessions/handlers/list.query.ts +24 -24
- package/src/sessions/handlers/mine.query.ts +24 -23
- package/src/sessions/handlers/revoke-all-for-user.write.ts +7 -11
- package/src/sessions/handlers/revoke-all-others.write.ts +7 -12
- package/src/sessions/handlers/revoke.write.ts +11 -18
- package/src/sessions/schema/user-session.ts +2 -2
- package/src/sessions/session-callbacks.ts +19 -21
- package/src/subscription-mollie/__tests__/feature.test.ts +1 -1
- package/src/subscription-mollie/__tests__/mollie-foundation.integration.ts +1 -1
- package/src/subscription-mollie/__tests__/verify-webhook.test.ts +8 -7
- package/src/subscription-stripe/__tests__/feature.test.ts +1 -1
- package/src/subscription-stripe/__tests__/plugin-methods.test.ts +14 -15
- package/src/subscription-stripe/__tests__/stripe-foundation.integration.ts +1 -1
- package/src/subscription-stripe/__tests__/verify-webhook.test.ts +14 -14
- package/src/subscription-stripe/verify-webhook.ts +1 -1
- package/src/template-resolver/__tests__/handlers.integration.ts +1 -1
- package/src/template-resolver/__tests__/template-resolver.integration.ts +3 -2
- package/src/template-resolver/api.ts +7 -13
- package/src/template-resolver/handlers/archive.write.ts +4 -7
- package/src/template-resolver/handlers/find-by-id.query.ts +4 -7
- package/src/template-resolver/handlers/list.query.ts +13 -21
- package/src/template-resolver/handlers/publish.write.ts +4 -7
- package/src/template-resolver/handlers/upsert-system.write.ts +7 -10
- package/src/template-resolver/handlers/upsert-tenant.write.ts +7 -10
- package/src/template-resolver/table.ts +2 -5
- package/src/tenant/__tests__/multi-tenant.integration.ts +1 -1
- package/src/tenant/__tests__/seed-testing.integration.ts +19 -45
- package/src/tenant/__tests__/tenant.integration.ts +1 -1
- package/src/tenant/handlers/active-tenant-ids.query.ts +3 -8
- package/src/tenant/handlers/add-member.write.ts +6 -8
- package/src/tenant/handlers/cancel-invitation.write.ts +5 -7
- package/src/tenant/handlers/invitations.query.ts +5 -10
- package/src/tenant/handlers/me.query.ts +2 -3
- package/src/tenant/handlers/members.query.ts +4 -5
- package/src/tenant/handlers/memberships.query.ts +2 -5
- package/src/tenant/handlers/remove-member.write.ts +6 -8
- package/src/tenant/handlers/resolve-user-ids.query.ts +6 -16
- package/src/tenant/handlers/update-member-roles.write.ts +6 -8
- package/src/tenant/invitation-table.ts +2 -5
- package/src/tenant/membership-table.ts +3 -6
- package/src/tenant/schema/tenant.ts +2 -2
- package/src/tenant/seeding.ts +12 -18
- package/src/text-content/README.md +1 -1
- package/src/text-content/__tests__/text-content.integration.ts +2 -2
- package/src/text-content/api.ts +2 -9
- package/src/text-content/handlers/by-slug.query.ts +6 -9
- package/src/text-content/handlers/by-tenant.query.ts +2 -2
- package/src/text-content/handlers/set.write.ts +7 -9
- package/src/text-content/seeding.ts +6 -9
- package/src/text-content/table.ts +2 -2
- package/src/text-content/web/__tests__/editor-read-only.test.tsx +31 -45
- package/src/text-content/web/__tests__/group-blocks.test.ts +1 -18
- package/src/text-content/web/client-plugin.tsx +11 -23
- package/src/tier-engine/__tests__/auto-default-tier.integration.ts +10 -16
- package/src/tier-engine/__tests__/compose-app.test.ts +1 -1
- package/src/tier-engine/__tests__/drift.test.ts +1 -1
- package/src/tier-engine/__tests__/resolver.integration.ts +6 -6
- package/src/tier-engine/__tests__/tier-engine.integration.ts +1 -1
- package/src/tier-engine/feature.ts +9 -16
- package/src/user/__tests__/seed-testing.integration.ts +10 -22
- package/src/user/__tests__/user-status.test.ts +1 -1
- package/src/user/__tests__/user.integration.ts +6 -5
- package/src/user/handlers/create.write.ts +5 -7
- package/src/user/handlers/find-for-auth.query.ts +5 -7
- package/src/user/schema/user.ts +2 -2
- package/src/user/seeding.ts +2 -3
- package/src/user-data-rights/__tests__/audit-log.integration.ts +24 -12
- package/src/user-data-rights/__tests__/cross-data-matrix.integration.ts +64 -37
- package/src/user-data-rights/__tests__/download.integration.ts +29 -46
- package/src/user-data-rights/__tests__/export-job-idempotency.integration.ts +35 -28
- package/src/user-data-rights/__tests__/export-job-schema.test.ts +2 -2
- package/src/user-data-rights/__tests__/policy-to-strategy.test.ts +1 -1
- package/src/user-data-rights/__tests__/request-cancel-deletion.integration.ts +11 -15
- package/src/user-data-rights/__tests__/request-deletion-callback.integration.ts +10 -12
- package/src/user-data-rights/__tests__/request-export.integration.ts +23 -16
- package/src/user-data-rights/__tests__/restriction-flow.integration.ts +24 -32
- package/src/user-data-rights/__tests__/run-export-jobs.integration.ts +142 -137
- package/src/user-data-rights/__tests__/run-forget-cleanup.integration.ts +46 -28
- package/src/user-data-rights/__tests__/run-user-export.integration.ts +20 -14
- package/src/user-data-rights/__tests__/token-helpers.test.ts +1 -1
- package/src/user-data-rights/__tests__/user-data-rights.integration.ts +1 -1
- package/src/user-data-rights/__tests__/zip-path.test.ts +1 -1
- package/src/user-data-rights/audit-download.ts +3 -3
- package/src/user-data-rights/db/queries/export-jobs.ts +23 -0
- package/src/user-data-rights/db/queries/forget-cleanup.ts +13 -0
- package/src/user-data-rights/handlers/cancel-deletion.write.ts +28 -22
- package/src/user-data-rights/handlers/download-by-job.query.ts +11 -21
- package/src/user-data-rights/handlers/download-by-token.query.ts +20 -35
- package/src/user-data-rights/handlers/export-status.query.ts +19 -33
- package/src/user-data-rights/handlers/lift-restriction.write.ts +7 -12
- package/src/user-data-rights/handlers/list-download-attempts.query.ts +14 -23
- package/src/user-data-rights/handlers/my-audit-log.query.ts +33 -23
- package/src/user-data-rights/handlers/request-deletion.write.ts +15 -15
- package/src/user-data-rights/handlers/request-export.write.ts +7 -11
- package/src/user-data-rights/handlers/restrict-account.write.ts +12 -12
- package/src/user-data-rights/run-export-jobs.ts +20 -60
- package/src/user-data-rights/run-forget-cleanup.ts +19 -33
- package/src/user-data-rights/run-user-export.ts +4 -6
- package/src/user-data-rights/schema/download-attempt.ts +2 -2
- package/src/user-data-rights/schema/download-token.ts +2 -2
- package/src/user-data-rights/schema/export-job.ts +2 -3
- package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.ts +37 -30
- package/src/user-data-rights-defaults/db/queries/user-hook.ts +17 -0
- package/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +12 -27
- package/src/user-data-rights-defaults/hooks/user.userdata-hook.ts +16 -18
- package/CHANGELOG.md +0 -680
|
@@ -11,28 +11,22 @@
|
|
|
11
11
|
// **Read-Only-Endpoint:** Pollt nur, kein State-Flip. Idempotent + cache-
|
|
12
12
|
// fest. UI poll-Intervall typisch 2-5s waehrend running.
|
|
13
13
|
|
|
14
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
14
15
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
15
16
|
import type { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
16
|
-
import { desc, eq } from "drizzle-orm";
|
|
17
17
|
import { z } from "zod";
|
|
18
18
|
import { exportJobsTable } from "../schema/export-job";
|
|
19
19
|
|
|
20
20
|
type Instant = InstanceType<ReturnType<typeof getTemporal>["Instant"]>;
|
|
21
21
|
|
|
22
|
-
// @cast-boundary db-row — drizzle's typed-select gibt korrekte Shapes
|
|
23
|
-
// fuer instant-Spalten zurueck (Temporal.Instant), aber TS-Inference
|
|
24
|
-
// ueber TenantDb-Wrapper kennt das nicht. Cast auf den narrow-Shape
|
|
25
|
-
// macht den Read-Pfad explizit. requestedAt ist `notNull` im Schema
|
|
26
|
-
// → niemals null. Lifecycle-Felder (completedAt/expiresAt) sind
|
|
27
|
-
// nullable bis Worker sie setzt.
|
|
28
22
|
type ExportJobRow = {
|
|
29
23
|
readonly id: string;
|
|
30
24
|
readonly status: string;
|
|
31
|
-
readonly
|
|
32
|
-
readonly
|
|
33
|
-
readonly
|
|
34
|
-
readonly
|
|
35
|
-
readonly
|
|
25
|
+
readonly requested_at: Instant;
|
|
26
|
+
readonly completed_at: Instant | null;
|
|
27
|
+
readonly expires_at: Instant | null;
|
|
28
|
+
readonly error_message: string | null;
|
|
29
|
+
readonly bytes_written: number | null;
|
|
36
30
|
};
|
|
37
31
|
|
|
38
32
|
export const exportStatusQuery = defineQueryHandler({
|
|
@@ -42,20 +36,12 @@ export const exportStatusQuery = defineQueryHandler({
|
|
|
42
36
|
handler: async (query, ctx) => {
|
|
43
37
|
// ctx.db.raw weil tenant-agnostisch — ein User der aus Tenant B
|
|
44
38
|
// pollt, sieht den aus Tenant A erstellten Job.
|
|
45
|
-
const rows =
|
|
46
|
-
.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
expiresAt: exportJobsTable["expiresAt"],
|
|
52
|
-
errorMessage: exportJobsTable["errorMessage"],
|
|
53
|
-
bytesWritten: exportJobsTable["bytesWritten"],
|
|
54
|
-
})
|
|
55
|
-
.from(exportJobsTable)
|
|
56
|
-
.where(eq(exportJobsTable["userId"], query.user.id))
|
|
57
|
-
.orderBy(desc(exportJobsTable["requestedAt"]))
|
|
58
|
-
.limit(1)) as ExportJobRow[]; // @cast-boundary db-row
|
|
39
|
+
const rows = await selectMany<ExportJobRow>(
|
|
40
|
+
ctx.db.raw,
|
|
41
|
+
exportJobsTable,
|
|
42
|
+
{ userId: query.user.id },
|
|
43
|
+
{ limit: 1, orderBy: { col: "requestedAt", direction: "desc" } },
|
|
44
|
+
);
|
|
59
45
|
|
|
60
46
|
const latest = rows[0];
|
|
61
47
|
if (!latest) return { hasJob: false as const };
|
|
@@ -63,13 +49,13 @@ export const exportStatusQuery = defineQueryHandler({
|
|
|
63
49
|
return {
|
|
64
50
|
hasJob: true as const,
|
|
65
51
|
job: {
|
|
66
|
-
id: latest
|
|
67
|
-
status: latest
|
|
68
|
-
requestedAt: latest.
|
|
69
|
-
completedAt: latest
|
|
70
|
-
expiresAt: latest
|
|
71
|
-
errorMessage: latest
|
|
72
|
-
bytesWritten: latest
|
|
52
|
+
id: latest["id"],
|
|
53
|
+
status: latest["status"],
|
|
54
|
+
requestedAt: latest["requested_at"].toString(),
|
|
55
|
+
completedAt: latest["completed_at"]?.toString() ?? null,
|
|
56
|
+
expiresAt: latest["expires_at"]?.toString() ?? null,
|
|
57
|
+
errorMessage: latest["error_message"],
|
|
58
|
+
bytesWritten: latest["bytes_written"],
|
|
73
59
|
},
|
|
74
60
|
};
|
|
75
61
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { fetchOne, updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
3
|
import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
|
-
import { eq } from "drizzle-orm";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { USER_STATUS, userTable } from "../../user";
|
|
6
6
|
|
|
@@ -29,13 +29,11 @@ export const liftRestrictionWrite = defineWriteHandler({
|
|
|
29
29
|
schema: z.object({}),
|
|
30
30
|
access: { openToAll: true },
|
|
31
31
|
handler: async (event, ctx) => {
|
|
32
|
-
const userRow = await ctx.db.raw
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
.where(eq(userTable["id"], event.user.id))
|
|
36
|
-
.limit(1);
|
|
32
|
+
const userRow = await fetchOne<{ status: string }>(ctx.db.raw, userTable, {
|
|
33
|
+
id: event.user.id,
|
|
34
|
+
});
|
|
37
35
|
|
|
38
|
-
if (userRow
|
|
36
|
+
if (!userRow) {
|
|
39
37
|
return writeFailure(
|
|
40
38
|
new UnprocessableError("user_not_found", {
|
|
41
39
|
details: { reason: "user_not_found", userId: event.user.id },
|
|
@@ -43,7 +41,7 @@ export const liftRestrictionWrite = defineWriteHandler({
|
|
|
43
41
|
);
|
|
44
42
|
}
|
|
45
43
|
|
|
46
|
-
const currentStatus = userRow[
|
|
44
|
+
const currentStatus = userRow["status"];
|
|
47
45
|
if (currentStatus !== USER_STATUS.Restricted) {
|
|
48
46
|
return writeFailure(
|
|
49
47
|
new UnprocessableError("not_restricted", {
|
|
@@ -52,10 +50,7 @@ export const liftRestrictionWrite = defineWriteHandler({
|
|
|
52
50
|
);
|
|
53
51
|
}
|
|
54
52
|
|
|
55
|
-
await ctx.db.raw
|
|
56
|
-
.update(userTable)
|
|
57
|
-
.set({ status: USER_STATUS.Active })
|
|
58
|
-
.where(eq(userTable["id"], event.user.id));
|
|
53
|
+
await updateMany(ctx.db.raw, userTable, { status: USER_STATUS.Active }, { id: event.user.id });
|
|
59
54
|
|
|
60
55
|
return {
|
|
61
56
|
isSuccess: true as const,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { selectMany, type WhereObject } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import { and, desc, eq, gte, lte } from "drizzle-orm";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { downloadAttemptsTable } from "../schema/download-attempt";
|
|
5
5
|
|
|
@@ -24,29 +24,20 @@ export const listDownloadAttemptsQuery = defineQueryHandler({
|
|
|
24
24
|
access: { roles: ["Admin", "SystemAdmin"] },
|
|
25
25
|
handler: async (query, ctx) => {
|
|
26
26
|
const p = query.payload;
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
if (p.
|
|
30
|
-
if (p.
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
const where: WhereObject = { tenantId: query.user.tenantId };
|
|
28
|
+
if (p.result) where["result"] = p.result;
|
|
29
|
+
if (p.ip) where["ip"] = p.ip;
|
|
30
|
+
if (p.from || p.to) {
|
|
31
|
+
const range: { gte?: unknown; lte?: unknown } = {};
|
|
32
|
+
if (p.from) range.gte = Temporal.Instant.from(p.from);
|
|
33
|
+
if (p.to) range.lte = Temporal.Instant.from(p.to);
|
|
34
|
+
where["attemptedAt"] = range;
|
|
35
|
+
}
|
|
33
36
|
|
|
34
|
-
const rows = await ctx.db
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
via: t["via"],
|
|
39
|
-
tokenHash: t["tokenHash"],
|
|
40
|
-
jobId: t["jobId"],
|
|
41
|
-
attemptedByUserId: t["attemptedByUserId"],
|
|
42
|
-
ip: t["ip"],
|
|
43
|
-
userAgent: t["userAgent"],
|
|
44
|
-
attemptedAt: t["attemptedAt"],
|
|
45
|
-
})
|
|
46
|
-
.from(t)
|
|
47
|
-
.where(and(...conditions))
|
|
48
|
-
.orderBy(desc(t["attemptedAt"]))
|
|
49
|
-
.limit(p.limit);
|
|
37
|
+
const rows = await selectMany(ctx.db, downloadAttemptsTable, where, {
|
|
38
|
+
orderBy: { col: "attemptedAt", direction: "desc" },
|
|
39
|
+
limit: p.limit,
|
|
40
|
+
});
|
|
50
41
|
|
|
51
42
|
return { rows };
|
|
52
43
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { selectMany, type WhereObject } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
3
|
import { eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
3
|
-
import { and, desc, eq, gte, lt, lte } from "drizzle-orm";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
|
|
6
6
|
// DSGVO Art. 15 Selbstauskunft — User reads HIS OWN audit-log.
|
|
@@ -28,32 +28,42 @@ export const myAuditLogQuery = defineQueryHandler({
|
|
|
28
28
|
handler: async (query, ctx) => {
|
|
29
29
|
const p = query.payload;
|
|
30
30
|
|
|
31
|
-
const conditions = [eq(eventsTable.createdBy, query.user.id)];
|
|
32
|
-
if (p.aggregateType) conditions.push(eq(eventsTable.aggregateType, p.aggregateType));
|
|
33
|
-
if (p.eventType) conditions.push(eq(eventsTable.type, p.eventType));
|
|
34
|
-
if (p.from) conditions.push(gte(eventsTable.createdAt, Temporal.Instant.from(p.from)));
|
|
35
|
-
if (p.to) conditions.push(lte(eventsTable.createdAt, Temporal.Instant.from(p.to)));
|
|
36
|
-
if (p.before) conditions.push(lt(eventsTable.id, BigInt(p.before)));
|
|
37
|
-
|
|
38
31
|
// ctx.db.raw weil events-table tenantId-Spalte hat und TenantDb
|
|
39
32
|
// sonst auto-filtert auf currentTenant. Account-weite Sicht ist
|
|
40
33
|
// hier explizit gewollt; Sicherung erfolgt via createdBy-Filter.
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
34
|
+
const where: WhereObject = { createdBy: query.user.id };
|
|
35
|
+
if (p.aggregateType) where["aggregateType"] = p.aggregateType;
|
|
36
|
+
if (p.eventType) where["type"] = p.eventType;
|
|
37
|
+
if (p.from || p.to) {
|
|
38
|
+
const range: { gte?: unknown; lte?: unknown } = {};
|
|
39
|
+
if (p.from) range.gte = Temporal.Instant.from(p.from);
|
|
40
|
+
if (p.to) range.lte = Temporal.Instant.from(p.to);
|
|
41
|
+
where["createdAt"] = range;
|
|
42
|
+
}
|
|
43
|
+
if (p.before) where["id"] = { lt: BigInt(p.before) };
|
|
44
|
+
|
|
45
|
+
const rows = await selectMany<{
|
|
46
|
+
id: bigint;
|
|
47
|
+
aggregate_id: string;
|
|
48
|
+
aggregate_type: string;
|
|
49
|
+
version: number;
|
|
50
|
+
type: string;
|
|
51
|
+
payload: Record<string, unknown>;
|
|
52
|
+
created_at: unknown;
|
|
53
|
+
}>(ctx.db.raw, eventsTable, where, {
|
|
54
|
+
orderBy: { col: "id", direction: "desc" },
|
|
55
|
+
limit: p.limit,
|
|
56
|
+
});
|
|
55
57
|
|
|
56
|
-
const serialised = rows.map((r) => ({
|
|
58
|
+
const serialised = rows.map((r) => ({
|
|
59
|
+
id: String(r["id"]),
|
|
60
|
+
aggregateId: r["aggregate_id"],
|
|
61
|
+
aggregateType: r["aggregate_type"],
|
|
62
|
+
version: r["version"],
|
|
63
|
+
type: r["type"],
|
|
64
|
+
payload: r["payload"],
|
|
65
|
+
createdAt: r["created_at"],
|
|
66
|
+
}));
|
|
57
67
|
const last = serialised[serialised.length - 1];
|
|
58
68
|
return {
|
|
59
69
|
rows: serialised,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { fetchOne, updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { addDurationSpec, type DurationSpec } from "@cosmicdrift/kumiko-framework/compliance";
|
|
2
3
|
import { createSystemUser, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
4
|
import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
5
|
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
5
|
-
import { eq } from "drizzle-orm";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { USER_STATUS, userTable } from "../../user";
|
|
8
8
|
|
|
@@ -36,13 +36,11 @@ export function createRequestDeletionHandler(opts: RequestDeletionOptions = {})
|
|
|
36
36
|
handler: async (event, ctx) => {
|
|
37
37
|
// ctx.db.raw (kein TenantDb-Wrapper) weil User-Entity tenant-agnostisch
|
|
38
38
|
// ist — siehe Plan-Doc Cross-Tenant-Section.
|
|
39
|
-
const userRow = await ctx.db.raw
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
.where(eq(userTable["id"], event.user.id))
|
|
43
|
-
.limit(1);
|
|
39
|
+
const userRow = await fetchOne<{ status: string; email: string }>(ctx.db.raw, userTable, {
|
|
40
|
+
id: event.user.id,
|
|
41
|
+
});
|
|
44
42
|
|
|
45
|
-
if (userRow
|
|
43
|
+
if (!userRow) {
|
|
46
44
|
return writeFailure(
|
|
47
45
|
new UnprocessableError("user_not_found", {
|
|
48
46
|
details: { reason: "user_not_found", userId: event.user.id },
|
|
@@ -50,12 +48,12 @@ export function createRequestDeletionHandler(opts: RequestDeletionOptions = {})
|
|
|
50
48
|
);
|
|
51
49
|
}
|
|
52
50
|
|
|
53
|
-
if (userRow[
|
|
51
|
+
if (userRow["status"] !== USER_STATUS.Active) {
|
|
54
52
|
return writeFailure(
|
|
55
53
|
new UnprocessableError("user_not_in_active_state", {
|
|
56
54
|
details: {
|
|
57
55
|
reason: "user_not_in_active_state",
|
|
58
|
-
currentStatus: userRow[
|
|
56
|
+
currentStatus: userRow["status"],
|
|
59
57
|
},
|
|
60
58
|
}),
|
|
61
59
|
);
|
|
@@ -78,19 +76,21 @@ export function createRequestDeletionHandler(opts: RequestDeletionOptions = {})
|
|
|
78
76
|
const T = getTemporal();
|
|
79
77
|
const gracePeriodEnd = addDurationSpec(T.Now.instant(), gracePeriod);
|
|
80
78
|
|
|
81
|
-
await
|
|
82
|
-
.
|
|
83
|
-
|
|
79
|
+
await updateMany(
|
|
80
|
+
ctx.db.raw,
|
|
81
|
+
userTable,
|
|
82
|
+
{
|
|
84
83
|
status: USER_STATUS.DeletionRequested,
|
|
85
84
|
gracePeriodEnd,
|
|
86
|
-
}
|
|
87
|
-
|
|
85
|
+
},
|
|
86
|
+
{ id: event.user.id },
|
|
87
|
+
);
|
|
88
88
|
|
|
89
89
|
// Best-effort Email-Notification. Send-Failure darf das Write nicht
|
|
90
90
|
// killen — siehe Type-Doc oben. console.warn ist die Operator-
|
|
91
91
|
// Sichtbarkeit; defineWriteHandler-Context fuehrt aktuell keinen
|
|
92
92
|
// structured-logger durch, Refactor-Kandidat wenn ctx.log threadet.
|
|
93
|
-
const userEmail = userRow[
|
|
93
|
+
const userEmail = userRow["email"];
|
|
94
94
|
if (opts.sendDeletionRequestedEmail && userEmail && userEmail.length > 0) {
|
|
95
95
|
try {
|
|
96
96
|
await opts.sendDeletionRequestedEmail({
|
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
// 1. Klick — Worker liest sein Compliance-Profile aus DIESEM Tenant
|
|
23
23
|
// fuer Job-TTL/Stale/Cleanup.
|
|
24
24
|
|
|
25
|
-
import {
|
|
25
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
26
|
+
import { createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
26
27
|
import { defineWriteHandler, type SaveContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
27
28
|
import type { WriteFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
28
29
|
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
29
|
-
import { eq, inArray } from "drizzle-orm";
|
|
30
30
|
import { z } from "zod";
|
|
31
31
|
import {
|
|
32
32
|
ACTIVE_JOB_CONSTRAINT,
|
|
@@ -143,13 +143,9 @@ async function findActiveJob(
|
|
|
143
143
|
db: import("@cosmicdrift/kumiko-framework/db").DbRunner,
|
|
144
144
|
userId: string,
|
|
145
145
|
): Promise<{ id: string; status: string } | null> {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
eq(exportJobsTable["userId"], userId),
|
|
152
|
-
inArray(exportJobsTable["status"], [EXPORT_JOB_STATUS.Pending, EXPORT_JOB_STATUS.Running]),
|
|
153
|
-
)) as { id: string; status: string } | null;
|
|
154
|
-
return row;
|
|
146
|
+
const row = await fetchOne<{ id: string; status: string }>(db, exportJobsTable, {
|
|
147
|
+
userId,
|
|
148
|
+
status: [EXPORT_JOB_STATUS.Pending, EXPORT_JOB_STATUS.Running],
|
|
149
|
+
});
|
|
150
|
+
return row ?? null;
|
|
155
151
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { fetchOne, updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { createSystemUser, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
3
|
import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
|
-
import { eq } from "drizzle-orm";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { USER_STATUS, userTable } from "../../user";
|
|
6
6
|
|
|
@@ -26,13 +26,11 @@ export const restrictAccountWrite = defineWriteHandler({
|
|
|
26
26
|
handler: async (event, ctx) => {
|
|
27
27
|
// ctx.db.raw weil User-Entity tenant-agnostisch ist (analog
|
|
28
28
|
// request-deletion.write.ts Cross-Tenant-Section).
|
|
29
|
-
const userRow = await ctx.db.raw
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
.where(eq(userTable["id"], event.user.id))
|
|
33
|
-
.limit(1);
|
|
29
|
+
const userRow = await fetchOne<{ status: string }>(ctx.db.raw, userTable, {
|
|
30
|
+
id: event.user.id,
|
|
31
|
+
});
|
|
34
32
|
|
|
35
|
-
if (userRow
|
|
33
|
+
if (!userRow) {
|
|
36
34
|
return writeFailure(
|
|
37
35
|
new UnprocessableError("user_not_found", {
|
|
38
36
|
details: { reason: "user_not_found", userId: event.user.id },
|
|
@@ -40,7 +38,7 @@ export const restrictAccountWrite = defineWriteHandler({
|
|
|
40
38
|
);
|
|
41
39
|
}
|
|
42
40
|
|
|
43
|
-
const currentStatus = userRow
|
|
41
|
+
const currentStatus = userRow.status;
|
|
44
42
|
if (currentStatus === USER_STATUS.Restricted) {
|
|
45
43
|
return writeFailure(
|
|
46
44
|
new UnprocessableError("already_restricted", {
|
|
@@ -56,10 +54,12 @@ export const restrictAccountWrite = defineWriteHandler({
|
|
|
56
54
|
);
|
|
57
55
|
}
|
|
58
56
|
|
|
59
|
-
await
|
|
60
|
-
.
|
|
61
|
-
|
|
62
|
-
.
|
|
57
|
+
await updateMany(
|
|
58
|
+
ctx.db.raw,
|
|
59
|
+
userTable,
|
|
60
|
+
{ status: USER_STATUS.Restricted },
|
|
61
|
+
{ id: event.user.id },
|
|
62
|
+
);
|
|
63
63
|
|
|
64
64
|
// Cross-Feature: alle live sessions revoken — sonst koennte der User
|
|
65
65
|
// mit existierendem JWT bis zur Token-Expiry weiter schreiben.
|
|
@@ -44,13 +44,10 @@
|
|
|
44
44
|
// (`expiresAt + exportStorageCleanupGraceHours < now`) — abgelaufene ZIPs
|
|
45
45
|
// auf S3 sollen nicht ewig liegen.
|
|
46
46
|
|
|
47
|
+
import { fetchOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
47
48
|
import { addDurationSpec } from "@cosmicdrift/kumiko-framework/compliance";
|
|
48
49
|
import type { DbConnection, DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
49
|
-
import {
|
|
50
|
-
createEventStoreExecutor,
|
|
51
|
-
createTenantDb,
|
|
52
|
-
fetchOne,
|
|
53
|
-
} from "@cosmicdrift/kumiko-framework/db";
|
|
50
|
+
import { createEventStoreExecutor, createTenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
54
51
|
import type { Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
55
52
|
import { createSystemUser } from "@cosmicdrift/kumiko-framework/engine";
|
|
56
53
|
import {
|
|
@@ -59,9 +56,9 @@ import {
|
|
|
59
56
|
type ZipEntry,
|
|
60
57
|
} from "@cosmicdrift/kumiko-framework/files";
|
|
61
58
|
import type { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
62
|
-
import { and, asc, eq, isNotNull, or } from "drizzle-orm";
|
|
63
59
|
import { resolveProfileForTenant } from "../compliance-profiles";
|
|
64
60
|
import { userTable } from "../user";
|
|
61
|
+
import { selectExportJobsForStorageCleanup } from "./db/queries/export-jobs";
|
|
65
62
|
import { runUserExport, type UserExportBundle } from "./run-user-export";
|
|
66
63
|
import { exportDownloadTokenEntity, exportDownloadTokensTable } from "./schema/download-token";
|
|
67
64
|
import { EXPORT_JOB_STATUS, exportJobEntity, exportJobsTable } from "./schema/export-job";
|
|
@@ -306,17 +303,14 @@ interface JobRow {
|
|
|
306
303
|
}
|
|
307
304
|
|
|
308
305
|
async function fetchPendingJobs(db: DbRunner): Promise<readonly JobRow[]> {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
.from(exportJobsTable)
|
|
318
|
-
.where(eq(exportJobsTable["status"], EXPORT_JOB_STATUS.Pending))
|
|
319
|
-
.orderBy(asc(exportJobsTable["requestedAt"]))) as readonly JobRow[];
|
|
306
|
+
return selectMany<JobRow>(
|
|
307
|
+
db,
|
|
308
|
+
exportJobsTable,
|
|
309
|
+
{ status: EXPORT_JOB_STATUS.Pending },
|
|
310
|
+
{
|
|
311
|
+
orderBy: { col: "requestedAt", direction: "asc" },
|
|
312
|
+
},
|
|
313
|
+
);
|
|
320
314
|
}
|
|
321
315
|
|
|
322
316
|
type ProcessOutcome =
|
|
@@ -530,23 +524,13 @@ async function staleDetectionPass(args: {
|
|
|
530
524
|
// wuerde 30-60min-alte stale-Jobs UNERKANNT lassen. Cron laeuft alle
|
|
531
525
|
// 60s, zu jedem Zeitpunkt sind nur wenige Jobs in `running` —
|
|
532
526
|
// alle fetchen + profile-resolve im Loop ist bezahlbar + korrekt.
|
|
533
|
-
|
|
534
|
-
const candidates = (await db
|
|
535
|
-
.select({
|
|
536
|
-
id: exportJobsTable["id"],
|
|
537
|
-
version: exportJobsTable["version"],
|
|
538
|
-
userId: exportJobsTable["userId"],
|
|
539
|
-
requestedFromTenantId: exportJobsTable["requestedFromTenantId"],
|
|
540
|
-
startedAt: exportJobsTable["startedAt"],
|
|
541
|
-
})
|
|
542
|
-
.from(exportJobsTable)
|
|
543
|
-
.where(eq(exportJobsTable["status"], EXPORT_JOB_STATUS.Running))) as readonly {
|
|
527
|
+
const candidates = await selectMany<{
|
|
544
528
|
id: string;
|
|
545
529
|
version: number;
|
|
546
530
|
userId: string;
|
|
547
531
|
requestedFromTenantId: TenantId;
|
|
548
532
|
startedAt: Instant | null;
|
|
549
|
-
}
|
|
533
|
+
}>(db, exportJobsTable, { status: EXPORT_JOB_STATUS.Running });
|
|
550
534
|
|
|
551
535
|
const failed: string[] = [];
|
|
552
536
|
for (const c of candidates) {
|
|
@@ -615,36 +599,12 @@ async function storageCleanupPass(args: {
|
|
|
615
599
|
// bereits in der DB statt im Loop. Bei skalierender DB-Historie (10k+
|
|
616
600
|
// done-jobs nach 30 Tagen) reduziert das den Worker-Roundtrip drastisch.
|
|
617
601
|
//
|
|
618
|
-
//
|
|
619
|
-
const candidates =
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
requestedFromTenantId: exportJobsTable["requestedFromTenantId"],
|
|
625
|
-
downloadStorageKey: exportJobsTable["downloadStorageKey"],
|
|
626
|
-
expiresAt: exportJobsTable["expiresAt"],
|
|
627
|
-
})
|
|
628
|
-
.from(exportJobsTable)
|
|
629
|
-
.where(
|
|
630
|
-
and(
|
|
631
|
-
// Beide Pfade: status in (done, failed) + downloadStorageKey gesetzt.
|
|
632
|
-
// Filter im Loop verfeinert (done braucht expiresAt+grace, failed
|
|
633
|
-
// sofort).
|
|
634
|
-
or(
|
|
635
|
-
eq(exportJobsTable["status"], EXPORT_JOB_STATUS.Done),
|
|
636
|
-
eq(exportJobsTable["status"], EXPORT_JOB_STATUS.Failed),
|
|
637
|
-
),
|
|
638
|
-
isNotNull(exportJobsTable["downloadStorageKey"]),
|
|
639
|
-
),
|
|
640
|
-
)) as readonly {
|
|
641
|
-
id: string;
|
|
642
|
-
version: number;
|
|
643
|
-
status: string;
|
|
644
|
-
requestedFromTenantId: TenantId;
|
|
645
|
-
downloadStorageKey: string | null;
|
|
646
|
-
expiresAt: Instant | null;
|
|
647
|
-
}[];
|
|
602
|
+
// or() + isNotNull(): no bun-db helper covers this combination — raw SQL.
|
|
603
|
+
const candidates = await selectExportJobsForStorageCleanup(
|
|
604
|
+
db,
|
|
605
|
+
EXPORT_JOB_STATUS.Done,
|
|
606
|
+
EXPORT_JOB_STATUS.Failed,
|
|
607
|
+
);
|
|
648
608
|
|
|
649
609
|
const cleaned: string[] = [];
|
|
650
610
|
for (const c of candidates) {
|
|
@@ -796,7 +756,7 @@ function countingStream(source: AsyncIterable<Uint8Array>): {
|
|
|
796
756
|
// aus jedem Worker-Tenant-Context.
|
|
797
757
|
async function lookupUserEmail(db: DbConnection, userId: string): Promise<string | null> {
|
|
798
758
|
// @cast-boundary db-row.
|
|
799
|
-
const row = (await fetchOne(db, userTable,
|
|
759
|
+
const row = (await fetchOne(db, userTable, { id: userId })) as {
|
|
800
760
|
email: string | null;
|
|
801
761
|
} | null;
|
|
802
762
|
return row?.email ?? null;
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// Outer-Tx aktiv, BEGIN sonst). Folge: ein failing Hook bei User A
|
|
19
19
|
// rollt nur dessen Sub-Tx zurueck, User B + bisherige User-Status-Flips
|
|
20
20
|
// bleiben commit-able. Ohne diese Sub-Tx wuerde der Outer-Dispatcher-Tx
|
|
21
|
-
// (alle writeHandler laufen in `db.
|
|
21
|
+
// (alle writeHandler laufen in `db.begin(...)`) den ganzen
|
|
22
22
|
// Cleanup-Run beim ersten Hook-Throw zurueckrollen.
|
|
23
23
|
//
|
|
24
24
|
// **Idempotenz:** Hooks sind idempotent designed (siehe
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
// gefailten Hooks bleibt im DeletionRequested-Status (next Lauf
|
|
33
33
|
// retried automatisch).
|
|
34
34
|
|
|
35
|
+
import { fetchOne, selectMany, updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
35
36
|
import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
36
37
|
import {
|
|
37
38
|
EXT_USER_DATA,
|
|
@@ -41,10 +42,10 @@ import {
|
|
|
41
42
|
type UserDataDeleteStrategy,
|
|
42
43
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
43
44
|
import type { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
44
|
-
import { and, eq, lte } from "drizzle-orm";
|
|
45
45
|
import { resolveRetentionPolicyForTenant } from "../data-retention";
|
|
46
46
|
import { tenantMembershipsTable } from "../tenant";
|
|
47
47
|
import { USER_STATUS, userTable } from "../user";
|
|
48
|
+
import { selectUsersDueForForgetCleanup } from "./db/queries/forget-cleanup";
|
|
48
49
|
|
|
49
50
|
type Instant = InstanceType<ReturnType<typeof getTemporal>["Instant"]>;
|
|
50
51
|
|
|
@@ -111,16 +112,12 @@ export async function runForgetCleanup(
|
|
|
111
112
|
const { db, registry, now, sendDeletionExecutedEmail } = args;
|
|
112
113
|
|
|
113
114
|
// Step 1: Find users with expired grace period.
|
|
114
|
-
//
|
|
115
|
-
const dueUsers =
|
|
116
|
-
|
|
117
|
-
.
|
|
118
|
-
.
|
|
119
|
-
|
|
120
|
-
eq(userTable["status"], USER_STATUS.DeletionRequested),
|
|
121
|
-
lte(userTable["gracePeriodEnd"], now),
|
|
122
|
-
),
|
|
123
|
-
)) as Array<{ id: string }>;
|
|
115
|
+
// lte with Instant: no bun-db operator covers this — raw SQL.
|
|
116
|
+
const dueUsers = await selectUsersDueForForgetCleanup(
|
|
117
|
+
db,
|
|
118
|
+
USER_STATUS.DeletionRequested,
|
|
119
|
+
now.toString(),
|
|
120
|
+
);
|
|
124
121
|
|
|
125
122
|
if (dueUsers.length === 0) {
|
|
126
123
|
return { processedUserIds: [], hookCallsAttempted: 0, errors: [] };
|
|
@@ -212,23 +209,14 @@ async function processUser(args: {
|
|
|
212
209
|
// Nach der Tx ist email = "deleted-{id}@{tenant}.example" oder NULL.
|
|
213
210
|
// Memory-cache laesst Atom-5b-Callback nach success-flip den
|
|
214
211
|
// ORIGINAL-email an App-Author-Callback geben.
|
|
215
|
-
|
|
216
|
-
const userPreTx = (await db
|
|
217
|
-
.select({ email: userTable["email"] })
|
|
218
|
-
.from(userTable)
|
|
219
|
-
.where(eq(userTable["id"], userId))
|
|
220
|
-
.limit(1)) as Array<{ email: string | null }>;
|
|
212
|
+
const userPreTx = await fetchOne<{ email: string | null }>(db, userTable, { id: userId });
|
|
221
213
|
const userEmailBeforeDelete =
|
|
222
|
-
userPreTx
|
|
214
|
+
userPreTx?.email && userPreTx.email.length > 0 ? userPreTx.email : null;
|
|
223
215
|
|
|
224
216
|
// Memberships fuer diesen User holen — alle Tenants in denen er Mitglied ist.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
.from(tenantMembershipsTable)
|
|
229
|
-
.where(eq(tenantMembershipsTable["userId"], userId))) as Array<{
|
|
230
|
-
tenantId: TenantId;
|
|
231
|
-
}>;
|
|
217
|
+
const memberships = await selectMany<{ tenantId: TenantId }>(db, tenantMembershipsTable, {
|
|
218
|
+
userId,
|
|
219
|
+
});
|
|
232
220
|
// tenant-Liste fuer Atom 5b Email — Memberships VOR Tx, weil hooks
|
|
233
221
|
// memberships in der Tx loeschen. Orphan-User (0 memberships) liefert
|
|
234
222
|
// [] in Email-args; App-Author-Template kann das case-handlen.
|
|
@@ -259,8 +247,8 @@ async function processUser(args: {
|
|
|
259
247
|
let currentTenantId: TenantId | null = null;
|
|
260
248
|
let currentEntityName: string | null = null;
|
|
261
249
|
try {
|
|
262
|
-
await (db as {
|
|
263
|
-
|
|
250
|
+
await (db as { begin: (fn: (tx: DbRunner) => Promise<void>) => Promise<void> }).begin(
|
|
251
|
+
async (tx) => {
|
|
264
252
|
for (const tenantId of tenantList) {
|
|
265
253
|
currentTenantId = tenantId;
|
|
266
254
|
for (const entry of hookEntries) {
|
|
@@ -282,12 +270,10 @@ async function processUser(args: {
|
|
|
282
270
|
// geworfen hat, kommen wir hier nicht an — die Tx rollback'd
|
|
283
271
|
// alles, der User bleibt im DeletionRequested-Status, naechster
|
|
284
272
|
// Run retried.
|
|
285
|
-
await tx
|
|
286
|
-
.update(userTable)
|
|
287
|
-
.set({ status: USER_STATUS.Deleted })
|
|
288
|
-
.where(eq(userTable["id"], userId));
|
|
273
|
+
await updateMany(tx, userTable, { status: USER_STATUS.Deleted }, { id: userId });
|
|
289
274
|
txSucceeded = true;
|
|
290
|
-
}
|
|
275
|
+
},
|
|
276
|
+
);
|
|
291
277
|
} catch (e) {
|
|
292
278
|
// currentTenantId/currentEntityName tracken den Failing-Hook —
|
|
293
279
|
// Operator sieht "Hook fileRef in Tenant A failed for user X" statt
|