@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.
Files changed (268) hide show
  1. package/package.json +6 -6
  2. package/src/__tests__/env-schemas.test.ts +1 -1
  3. package/src/__tests__/es-ops-e2e.integration.ts +10 -9
  4. package/src/audit/__tests__/audit.integration.ts +3 -3
  5. package/src/audit/handlers/list.query.ts +39 -51
  6. package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.ts +4 -3
  7. package/src/auth-email-password/__tests__/account-lockout.integration.ts +4 -3
  8. package/src/auth-email-password/__tests__/auth-claims.integration.ts +5 -4
  9. package/src/auth-email-password/__tests__/auth.integration.ts +4 -3
  10. package/src/auth-email-password/__tests__/confirm-token-flow.test.ts +1 -1
  11. package/src/auth-email-password/__tests__/email-templates.test.ts +1 -1
  12. package/src/auth-email-password/__tests__/email-verification.integration.ts +7 -10
  13. package/src/auth-email-password/__tests__/identity-v3-hash.test.ts +1 -1
  14. package/src/auth-email-password/__tests__/identity-v3-login.integration.ts +4 -3
  15. package/src/auth-email-password/__tests__/invite-flow.integration.ts +16 -43
  16. package/src/auth-email-password/__tests__/multi-roles.integration.ts +6 -9
  17. package/src/auth-email-password/__tests__/password-reset.integration.ts +8 -7
  18. package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.ts +4 -3
  19. package/src/auth-email-password/__tests__/seed-admin.integration.ts +19 -32
  20. package/src/auth-email-password/__tests__/session-callbacks.integration.ts +6 -5
  21. package/src/auth-email-password/__tests__/session-strict-mode.integration.ts +1 -1
  22. package/src/auth-email-password/__tests__/signed-token.test.ts +1 -1
  23. package/src/auth-email-password/__tests__/signup-flow.integration.ts +11 -15
  24. package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +26 -26
  25. package/src/auth-email-password/handlers/invite-accept.write.ts +24 -21
  26. package/src/auth-email-password/handlers/invite-create.write.ts +3 -8
  27. package/src/auth-email-password/handlers/invite-signup-complete.write.ts +20 -17
  28. package/src/auth-email-password/handlers/signup-confirm.write.ts +3 -7
  29. package/src/auth-email-password/seeding.ts +1 -1
  30. package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +1 -2
  31. package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +10 -19
  32. package/src/auth-email-password/web/__tests__/login-screen.test.tsx +12 -18
  33. package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +12 -17
  34. package/src/auth-email-password/web/__tests__/session-roles.test.ts +1 -1
  35. package/src/auth-email-password/web/__tests__/tenant-switcher.test.tsx +1 -8
  36. package/src/auth-email-password/web/__tests__/test-utils.tsx +4 -8
  37. package/src/auth-email-password/web/__tests__/user-menu.test.tsx +2 -8
  38. package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +10 -15
  39. package/src/billing-foundation/__tests__/billing-foundation.integration.ts +1 -1
  40. package/src/billing-foundation/__tests__/feature.test.ts +1 -1
  41. package/src/billing-foundation/__tests__/webhook-handler.test.ts +6 -5
  42. package/src/billing-foundation/db/queries/subscription-projection.ts +15 -0
  43. package/src/billing-foundation/get-subscription-for-tenant.ts +2 -6
  44. package/src/billing-foundation/handlers/create-portal-session.write.ts +2 -2
  45. package/src/billing-foundation/handlers/list-subscriptions.query.ts +4 -1
  46. package/src/billing-foundation/projection.ts +32 -13
  47. package/src/cap-counter/__tests__/cap-counter.integration.ts +1 -1
  48. package/src/cap-counter/__tests__/enforce-cap.test.ts +37 -32
  49. package/src/cap-counter/__tests__/with-cap-enforcement.integration.ts +1 -1
  50. package/src/cap-counter/enforce-cap.ts +14 -20
  51. package/src/cap-counter/handlers/get-counter.query.ts +7 -13
  52. package/src/cap-counter/handlers/increment.write.ts +2 -2
  53. package/src/cap-counter/handlers/mark-soft-warned.write.ts +2 -2
  54. package/src/channel-in-app/handlers/inbox.query.ts +7 -13
  55. package/src/channel-in-app/handlers/mark-all-read.write.ts +7 -9
  56. package/src/channel-in-app/handlers/mark-read.write.ts +8 -14
  57. package/src/channel-in-app/handlers/unread-count.query.ts +10 -9
  58. package/src/channel-in-app/in-app-channel.ts +10 -12
  59. package/src/channel-in-app/tables.ts +1 -1
  60. package/src/compliance-profiles/__tests__/compliance-profiles.integration.ts +1 -1
  61. package/src/compliance-profiles/__tests__/seeding.integration.ts +1 -1
  62. package/src/compliance-profiles/handlers/for-tenant.query.ts +4 -7
  63. package/src/compliance-profiles/handlers/needs-profile.query.ts +4 -7
  64. package/src/compliance-profiles/handlers/set-profile.write.ts +5 -7
  65. package/src/compliance-profiles/resolve-for-tenant.ts +5 -7
  66. package/src/compliance-profiles/schema/profile-selection.ts +2 -2
  67. package/src/compliance-profiles/seeding.ts +4 -7
  68. package/src/config/__tests__/app-overrides.test.ts +1 -1
  69. package/src/config/__tests__/cascade.integration.ts +1 -1
  70. package/src/config/__tests__/config.integration.ts +8 -27
  71. package/src/config/db/queries/resolver.ts +47 -0
  72. package/src/config/handlers/__tests__/prepare-config-write.test.ts +1 -1
  73. package/src/config/resolver.ts +14 -62
  74. package/src/config/table.ts +4 -4
  75. package/src/config/write-helpers.ts +7 -11
  76. package/src/custom-fields/__tests__/audit-integration.integration.ts +6 -6
  77. package/src/custom-fields/__tests__/custom-fields.integration.ts +7 -7
  78. package/src/custom-fields/__tests__/feature.test.ts +1 -1
  79. package/src/custom-fields/__tests__/field-access.integration.ts +6 -6
  80. package/src/custom-fields/__tests__/quota.integration.ts +6 -6
  81. package/src/custom-fields/__tests__/retention.integration.ts +12 -10
  82. package/src/custom-fields/__tests__/user-data-rights.integration.ts +27 -17
  83. package/src/custom-fields/__tests__/wire-for-entity.test.ts +5 -5
  84. package/src/custom-fields/db/queries/field-access.ts +16 -0
  85. package/src/custom-fields/db/queries/projection.ts +43 -0
  86. package/src/custom-fields/db/queries/quota.ts +14 -0
  87. package/src/custom-fields/db/queries/retention.ts +39 -0
  88. package/src/custom-fields/db/queries/user-data-rights.ts +54 -0
  89. package/src/custom-fields/lib/field-access.ts +2 -41
  90. package/src/custom-fields/lib/quota.ts +2 -25
  91. package/src/custom-fields/run-retention.ts +19 -21
  92. package/src/custom-fields/wire-for-entity.ts +30 -23
  93. package/src/custom-fields/wire-user-data-rights.ts +33 -85
  94. package/src/data-retention/__tests__/data-retention.integration.ts +1 -1
  95. package/src/data-retention/__tests__/keep-for.test.ts +1 -1
  96. package/src/data-retention/__tests__/override-schema.test.ts +1 -1
  97. package/src/data-retention/__tests__/policy-for.integration.ts +1 -1
  98. package/src/data-retention/__tests__/resolver.test.ts +1 -1
  99. package/src/data-retention/handlers/policy-for.query.ts +5 -8
  100. package/src/data-retention/resolve-for-tenant.ts +6 -8
  101. package/src/data-retention/schema/tenant-retention-override.ts +2 -2
  102. package/src/delivery/__tests__/delivery-events.integration.ts +8 -21
  103. package/src/delivery/__tests__/delivery.integration.ts +100 -190
  104. package/src/delivery/db/queries/preferences.ts +30 -0
  105. package/src/delivery/delivery-service.ts +8 -36
  106. package/src/delivery/feature.ts +2 -1
  107. package/src/delivery/handlers/log.query.ts +5 -7
  108. package/src/delivery/handlers/preferences.query.ts +2 -5
  109. package/src/delivery/tables.ts +26 -1
  110. package/src/delivery/upsert-preference.ts +8 -14
  111. package/src/feature-toggles/__tests__/feature-toggles.integration.ts +30 -30
  112. package/src/feature-toggles/__tests__/registered-system-tenant.test.ts +7 -6
  113. package/src/feature-toggles/db/queries/toggle-state.ts +25 -0
  114. package/src/feature-toggles/feature.ts +16 -2
  115. package/src/feature-toggles/global-feature-state-table.ts +1 -1
  116. package/src/feature-toggles/handlers/list.query.ts +9 -2
  117. package/src/feature-toggles/handlers/registered.query.ts +3 -7
  118. package/src/feature-toggles/handlers/set.write.ts +37 -25
  119. package/src/feature-toggles/toggle-runtime.ts +3 -6
  120. package/src/file-foundation/__tests__/feature.test.ts +1 -1
  121. package/src/file-foundation/__tests__/file-foundation.integration.ts +1 -1
  122. package/src/file-provider-inmemory/__tests__/feature.test.ts +1 -1
  123. package/src/file-provider-s3/__tests__/feature.test.ts +1 -1
  124. package/src/files/__tests__/files.integration.ts +18 -7
  125. package/src/files/schema/file-ref.ts +1 -1
  126. package/src/files-provider-s3/__tests__/env-helper.test.ts +1 -1
  127. package/src/files-provider-s3/__tests__/s3-provider.integration.ts +1 -1
  128. package/src/files-provider-s3/__tests__/s3-provider.test.ts +1 -1
  129. package/src/jobs/__tests__/job-system-user.integration.ts +1 -1
  130. package/src/jobs/__tests__/jobs-events.integration.ts +8 -21
  131. package/src/jobs/__tests__/jobs-feature.integration.ts +1 -1
  132. package/src/jobs/feature.ts +22 -14
  133. package/src/jobs/handlers/detail.query.ts +10 -8
  134. package/src/jobs/handlers/list.query.ts +9 -21
  135. package/src/jobs/handlers/retry.write.ts +2 -7
  136. package/src/jobs/job-run-logger.ts +3 -9
  137. package/src/jobs/job-run-table.ts +49 -17
  138. package/src/legal-pages/__tests__/legal-pages.integration.ts +1 -1
  139. package/src/mail-foundation/__tests__/feature.test.ts +1 -1
  140. package/src/mail-foundation/__tests__/mail-foundation.integration.ts +1 -1
  141. package/src/mail-transport-inmemory/__tests__/feature.test.ts +1 -1
  142. package/src/mail-transport-smtp/__tests__/feature.test.ts +1 -1
  143. package/src/rate-limiting/__tests__/rate-limiting.integration.ts +1 -1
  144. package/src/renderer-foundation/__tests__/api.test.ts +2 -2
  145. package/src/renderer-foundation/__tests__/collect-plugins.integration.ts +1 -1
  146. package/src/renderer-simple/__tests__/adapter.test.ts +2 -2
  147. package/src/renderer-simple/__tests__/simple-renderer.test.ts +1 -1
  148. package/src/secrets/__tests__/require-secrets-context.test.ts +6 -5
  149. package/src/secrets/__tests__/rotate.integration.ts +6 -9
  150. package/src/secrets/__tests__/secrets-events.integration.ts +6 -12
  151. package/src/secrets/__tests__/secrets.integration.ts +6 -11
  152. package/src/secrets/db/queries/read.ts +16 -0
  153. package/src/secrets/handlers/list.query.ts +16 -17
  154. package/src/secrets/handlers/rotate.job.ts +8 -12
  155. package/src/secrets/secrets-context.ts +9 -21
  156. package/src/secrets/table.ts +1 -1
  157. package/src/sessions/__tests__/cleanup.integration.ts +8 -6
  158. package/src/sessions/__tests__/password-auto-revoke.integration.ts +7 -6
  159. package/src/sessions/__tests__/sessions.integration.ts +23 -38
  160. package/src/sessions/__tests__/test-helpers.ts +1 -1
  161. package/src/sessions/db/queries/cleanup.ts +21 -0
  162. package/src/sessions/handlers/cleanup.job.ts +6 -29
  163. package/src/sessions/handlers/list.query.ts +24 -24
  164. package/src/sessions/handlers/mine.query.ts +24 -23
  165. package/src/sessions/handlers/revoke-all-for-user.write.ts +7 -11
  166. package/src/sessions/handlers/revoke-all-others.write.ts +7 -12
  167. package/src/sessions/handlers/revoke.write.ts +11 -18
  168. package/src/sessions/schema/user-session.ts +2 -2
  169. package/src/sessions/session-callbacks.ts +19 -21
  170. package/src/subscription-mollie/__tests__/feature.test.ts +1 -1
  171. package/src/subscription-mollie/__tests__/mollie-foundation.integration.ts +1 -1
  172. package/src/subscription-mollie/__tests__/verify-webhook.test.ts +8 -7
  173. package/src/subscription-stripe/__tests__/feature.test.ts +1 -1
  174. package/src/subscription-stripe/__tests__/plugin-methods.test.ts +14 -15
  175. package/src/subscription-stripe/__tests__/stripe-foundation.integration.ts +1 -1
  176. package/src/subscription-stripe/__tests__/verify-webhook.test.ts +14 -14
  177. package/src/subscription-stripe/verify-webhook.ts +1 -1
  178. package/src/template-resolver/__tests__/handlers.integration.ts +1 -1
  179. package/src/template-resolver/__tests__/template-resolver.integration.ts +3 -2
  180. package/src/template-resolver/api.ts +7 -13
  181. package/src/template-resolver/handlers/archive.write.ts +4 -7
  182. package/src/template-resolver/handlers/find-by-id.query.ts +4 -7
  183. package/src/template-resolver/handlers/list.query.ts +13 -21
  184. package/src/template-resolver/handlers/publish.write.ts +4 -7
  185. package/src/template-resolver/handlers/upsert-system.write.ts +7 -10
  186. package/src/template-resolver/handlers/upsert-tenant.write.ts +7 -10
  187. package/src/template-resolver/table.ts +2 -5
  188. package/src/tenant/__tests__/multi-tenant.integration.ts +1 -1
  189. package/src/tenant/__tests__/seed-testing.integration.ts +19 -45
  190. package/src/tenant/__tests__/tenant.integration.ts +1 -1
  191. package/src/tenant/handlers/active-tenant-ids.query.ts +3 -8
  192. package/src/tenant/handlers/add-member.write.ts +6 -8
  193. package/src/tenant/handlers/cancel-invitation.write.ts +5 -7
  194. package/src/tenant/handlers/invitations.query.ts +5 -10
  195. package/src/tenant/handlers/me.query.ts +2 -3
  196. package/src/tenant/handlers/members.query.ts +4 -5
  197. package/src/tenant/handlers/memberships.query.ts +2 -5
  198. package/src/tenant/handlers/remove-member.write.ts +6 -8
  199. package/src/tenant/handlers/resolve-user-ids.query.ts +6 -16
  200. package/src/tenant/handlers/update-member-roles.write.ts +6 -8
  201. package/src/tenant/invitation-table.ts +2 -5
  202. package/src/tenant/membership-table.ts +3 -6
  203. package/src/tenant/schema/tenant.ts +2 -2
  204. package/src/tenant/seeding.ts +12 -18
  205. package/src/text-content/README.md +1 -1
  206. package/src/text-content/__tests__/text-content.integration.ts +2 -2
  207. package/src/text-content/api.ts +2 -9
  208. package/src/text-content/handlers/by-slug.query.ts +6 -9
  209. package/src/text-content/handlers/by-tenant.query.ts +2 -2
  210. package/src/text-content/handlers/set.write.ts +7 -9
  211. package/src/text-content/seeding.ts +6 -9
  212. package/src/text-content/table.ts +2 -2
  213. package/src/text-content/web/__tests__/editor-read-only.test.tsx +31 -45
  214. package/src/text-content/web/__tests__/group-blocks.test.ts +1 -18
  215. package/src/text-content/web/client-plugin.tsx +11 -23
  216. package/src/tier-engine/__tests__/auto-default-tier.integration.ts +10 -16
  217. package/src/tier-engine/__tests__/compose-app.test.ts +1 -1
  218. package/src/tier-engine/__tests__/drift.test.ts +1 -1
  219. package/src/tier-engine/__tests__/resolver.integration.ts +6 -6
  220. package/src/tier-engine/__tests__/tier-engine.integration.ts +1 -1
  221. package/src/tier-engine/feature.ts +9 -16
  222. package/src/user/__tests__/seed-testing.integration.ts +10 -22
  223. package/src/user/__tests__/user-status.test.ts +1 -1
  224. package/src/user/__tests__/user.integration.ts +6 -5
  225. package/src/user/handlers/create.write.ts +5 -7
  226. package/src/user/handlers/find-for-auth.query.ts +5 -7
  227. package/src/user/schema/user.ts +2 -2
  228. package/src/user/seeding.ts +2 -3
  229. package/src/user-data-rights/__tests__/audit-log.integration.ts +24 -12
  230. package/src/user-data-rights/__tests__/cross-data-matrix.integration.ts +64 -37
  231. package/src/user-data-rights/__tests__/download.integration.ts +29 -46
  232. package/src/user-data-rights/__tests__/export-job-idempotency.integration.ts +35 -28
  233. package/src/user-data-rights/__tests__/export-job-schema.test.ts +2 -2
  234. package/src/user-data-rights/__tests__/policy-to-strategy.test.ts +1 -1
  235. package/src/user-data-rights/__tests__/request-cancel-deletion.integration.ts +11 -15
  236. package/src/user-data-rights/__tests__/request-deletion-callback.integration.ts +10 -12
  237. package/src/user-data-rights/__tests__/request-export.integration.ts +23 -16
  238. package/src/user-data-rights/__tests__/restriction-flow.integration.ts +24 -32
  239. package/src/user-data-rights/__tests__/run-export-jobs.integration.ts +142 -137
  240. package/src/user-data-rights/__tests__/run-forget-cleanup.integration.ts +46 -28
  241. package/src/user-data-rights/__tests__/run-user-export.integration.ts +20 -14
  242. package/src/user-data-rights/__tests__/token-helpers.test.ts +1 -1
  243. package/src/user-data-rights/__tests__/user-data-rights.integration.ts +1 -1
  244. package/src/user-data-rights/__tests__/zip-path.test.ts +1 -1
  245. package/src/user-data-rights/audit-download.ts +3 -3
  246. package/src/user-data-rights/db/queries/export-jobs.ts +23 -0
  247. package/src/user-data-rights/db/queries/forget-cleanup.ts +13 -0
  248. package/src/user-data-rights/handlers/cancel-deletion.write.ts +28 -22
  249. package/src/user-data-rights/handlers/download-by-job.query.ts +11 -21
  250. package/src/user-data-rights/handlers/download-by-token.query.ts +20 -35
  251. package/src/user-data-rights/handlers/export-status.query.ts +19 -33
  252. package/src/user-data-rights/handlers/lift-restriction.write.ts +7 -12
  253. package/src/user-data-rights/handlers/list-download-attempts.query.ts +14 -23
  254. package/src/user-data-rights/handlers/my-audit-log.query.ts +33 -23
  255. package/src/user-data-rights/handlers/request-deletion.write.ts +15 -15
  256. package/src/user-data-rights/handlers/request-export.write.ts +7 -11
  257. package/src/user-data-rights/handlers/restrict-account.write.ts +12 -12
  258. package/src/user-data-rights/run-export-jobs.ts +20 -60
  259. package/src/user-data-rights/run-forget-cleanup.ts +19 -33
  260. package/src/user-data-rights/run-user-export.ts +4 -6
  261. package/src/user-data-rights/schema/download-attempt.ts +2 -2
  262. package/src/user-data-rights/schema/download-token.ts +2 -2
  263. package/src/user-data-rights/schema/export-job.ts +2 -3
  264. package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.ts +37 -30
  265. package/src/user-data-rights-defaults/db/queries/user-hook.ts +17 -0
  266. package/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +12 -27
  267. package/src/user-data-rights-defaults/hooks/user.userdata-hook.ts +16 -18
  268. 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 requestedAt: Instant;
32
- readonly completedAt: Instant | null;
33
- readonly expiresAt: Instant | null;
34
- readonly errorMessage: string | null;
35
- readonly bytesWritten: number | null;
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 = (await ctx.db.raw
46
- .select({
47
- id: exportJobsTable["id"],
48
- status: exportJobsTable["status"],
49
- requestedAt: exportJobsTable["requestedAt"],
50
- completedAt: exportJobsTable["completedAt"],
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.id,
67
- status: latest.status,
68
- requestedAt: latest.requestedAt.toString(),
69
- completedAt: latest.completedAt?.toString() ?? null,
70
- expiresAt: latest.expiresAt?.toString() ?? null,
71
- errorMessage: latest.errorMessage,
72
- bytesWritten: latest.bytesWritten,
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
- .select({ status: userTable["status"] })
34
- .from(userTable)
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.length === 0) {
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[0]?.status;
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 t = downloadAttemptsTable;
28
- const conditions = [eq(t["tenantId"], query.user.tenantId)];
29
- if (p.result) conditions.push(eq(t["result"], p.result));
30
- if (p.ip) conditions.push(eq(t["ip"], p.ip));
31
- if (p.from) conditions.push(gte(t["attemptedAt"], Temporal.Instant.from(p.from)));
32
- if (p.to) conditions.push(lte(t["attemptedAt"], Temporal.Instant.from(p.to)));
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
- .select({
36
- id: t["id"],
37
- result: t["result"],
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 rows = await ctx.db.raw
42
- .select({
43
- id: eventsTable.id,
44
- aggregateId: eventsTable.aggregateId,
45
- aggregateType: eventsTable.aggregateType,
46
- version: eventsTable.version,
47
- type: eventsTable.type,
48
- payload: eventsTable.payload,
49
- createdAt: eventsTable.createdAt,
50
- })
51
- .from(eventsTable)
52
- .where(and(...conditions))
53
- .orderBy(desc(eventsTable.id))
54
- .limit(p.limit);
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) => ({ ...r, id: String(r["id"]) }));
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
- .select({ status: userTable["status"], email: userTable["email"] })
41
- .from(userTable)
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.length === 0) {
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[0]?.status !== USER_STATUS.Active) {
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[0]?.status,
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 ctx.db.raw
82
- .update(userTable)
83
- .set({
79
+ await updateMany(
80
+ ctx.db.raw,
81
+ userTable,
82
+ {
84
83
  status: USER_STATUS.DeletionRequested,
85
84
  gracePeriodEnd,
86
- })
87
- .where(eq(userTable["id"], event.user.id));
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[0]?.email;
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 { createEventStoreExecutor, fetchOne } from "@cosmicdrift/kumiko-framework/db";
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
- // @cast-boundary db-row fetchOne liefert generic DbRow.
147
- // Variadic-conditions werden intern mit AND verknuepft.
148
- const row = (await fetchOne(
149
- db,
150
- exportJobsTable,
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
- .select({ status: userTable["status"] })
31
- .from(userTable)
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.length === 0) {
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[0]?.status;
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 ctx.db.raw
60
- .update(userTable)
61
- .set({ status: USER_STATUS.Restricted })
62
- .where(eq(userTable["id"], event.user.id));
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
- // @cast-boundary db-row.
310
- return (await db
311
- .select({
312
- id: exportJobsTable["id"],
313
- version: exportJobsTable["version"],
314
- userId: exportJobsTable["userId"],
315
- requestedFromTenantId: exportJobsTable["requestedFromTenantId"],
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
- // @cast-boundary db-row.
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
- // @cast-boundary db-row.
619
- const candidates = (await db
620
- .select({
621
- id: exportJobsTable["id"],
622
- version: exportJobsTable["version"],
623
- status: exportJobsTable["status"],
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, eq(userTable["id"], userId))) as {
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.transaction(...)`) den ganzen
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
- // @cast-boundary db-row drizzle-select gibt Record-Shape zurueck.
115
- const dueUsers = (await db
116
- .select({ id: userTable["id"] })
117
- .from(userTable)
118
- .where(
119
- and(
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
- // @cast-boundary db-row.
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[0]?.email && userPreTx[0].email.length > 0 ? userPreTx[0].email : null;
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
- // @cast-boundary db-row.
226
- const memberships = (await db
227
- .select({ tenantId: tenantMembershipsTable["tenantId"] })
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 { transaction: (fn: (tx: DbRunner) => Promise<void>) => Promise<void> }) // @cast-boundary db-runner
263
- .transaction(async (tx) => {
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