@cosmicdrift/kumiko-bundled-features 0.14.0 → 0.16.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 (269) hide show
  1. package/package.json +2 -2
  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/_internal/parse-override.ts +19 -0
  63. package/src/compliance-profiles/handlers/for-tenant.query.ts +10 -32
  64. package/src/compliance-profiles/handlers/needs-profile.query.ts +4 -7
  65. package/src/compliance-profiles/handlers/set-profile.write.ts +5 -7
  66. package/src/compliance-profiles/resolve-for-tenant.ts +11 -27
  67. package/src/compliance-profiles/schema/profile-selection.ts +2 -2
  68. package/src/compliance-profiles/seeding.ts +4 -7
  69. package/src/config/__tests__/app-overrides.test.ts +1 -1
  70. package/src/config/__tests__/cascade.integration.ts +1 -1
  71. package/src/config/__tests__/config.integration.ts +8 -27
  72. package/src/config/db/queries/resolver.ts +47 -0
  73. package/src/config/handlers/__tests__/prepare-config-write.test.ts +1 -1
  74. package/src/config/resolver.ts +14 -62
  75. package/src/config/table.ts +4 -4
  76. package/src/config/write-helpers.ts +7 -11
  77. package/src/custom-fields/__tests__/audit-integration.integration.ts +6 -6
  78. package/src/custom-fields/__tests__/custom-fields.integration.ts +7 -7
  79. package/src/custom-fields/__tests__/feature.test.ts +1 -1
  80. package/src/custom-fields/__tests__/field-access.integration.ts +6 -6
  81. package/src/custom-fields/__tests__/quota.integration.ts +6 -6
  82. package/src/custom-fields/__tests__/retention.integration.ts +12 -10
  83. package/src/custom-fields/__tests__/user-data-rights.integration.ts +27 -17
  84. package/src/custom-fields/__tests__/wire-for-entity.test.ts +5 -5
  85. package/src/custom-fields/db/queries/field-access.ts +16 -0
  86. package/src/custom-fields/db/queries/projection.ts +43 -0
  87. package/src/custom-fields/db/queries/quota.ts +14 -0
  88. package/src/custom-fields/db/queries/retention.ts +39 -0
  89. package/src/custom-fields/db/queries/user-data-rights.ts +54 -0
  90. package/src/custom-fields/lib/field-access.ts +2 -41
  91. package/src/custom-fields/lib/quota.ts +2 -25
  92. package/src/custom-fields/run-retention.ts +19 -21
  93. package/src/custom-fields/wire-for-entity.ts +30 -23
  94. package/src/custom-fields/wire-user-data-rights.ts +33 -85
  95. package/src/data-retention/__tests__/data-retention.integration.ts +1 -1
  96. package/src/data-retention/__tests__/keep-for.test.ts +1 -1
  97. package/src/data-retention/__tests__/override-schema.test.ts +1 -1
  98. package/src/data-retention/__tests__/policy-for.integration.ts +1 -1
  99. package/src/data-retention/__tests__/resolver.test.ts +1 -1
  100. package/src/data-retention/handlers/policy-for.query.ts +5 -8
  101. package/src/data-retention/resolve-for-tenant.ts +6 -8
  102. package/src/data-retention/schema/tenant-retention-override.ts +2 -2
  103. package/src/delivery/__tests__/delivery-events.integration.ts +8 -21
  104. package/src/delivery/__tests__/delivery.integration.ts +100 -190
  105. package/src/delivery/db/queries/preferences.ts +30 -0
  106. package/src/delivery/delivery-service.ts +8 -36
  107. package/src/delivery/feature.ts +10 -2
  108. package/src/delivery/handlers/log.query.ts +5 -7
  109. package/src/delivery/handlers/preferences.query.ts +2 -5
  110. package/src/delivery/tables.ts +26 -1
  111. package/src/delivery/upsert-preference.ts +8 -14
  112. package/src/feature-toggles/__tests__/feature-toggles.integration.ts +30 -30
  113. package/src/feature-toggles/__tests__/registered-system-tenant.test.ts +7 -6
  114. package/src/feature-toggles/db/queries/toggle-state.ts +25 -0
  115. package/src/feature-toggles/feature.ts +16 -2
  116. package/src/feature-toggles/global-feature-state-table.ts +1 -1
  117. package/src/feature-toggles/handlers/list.query.ts +9 -2
  118. package/src/feature-toggles/handlers/registered.query.ts +3 -7
  119. package/src/feature-toggles/handlers/set.write.ts +37 -25
  120. package/src/feature-toggles/toggle-runtime.ts +3 -6
  121. package/src/file-foundation/__tests__/feature.test.ts +1 -1
  122. package/src/file-foundation/__tests__/file-foundation.integration.ts +1 -1
  123. package/src/file-provider-inmemory/__tests__/feature.test.ts +1 -1
  124. package/src/file-provider-s3/__tests__/feature.test.ts +1 -1
  125. package/src/files/__tests__/files.integration.ts +18 -7
  126. package/src/files/schema/file-ref.ts +1 -1
  127. package/src/files-provider-s3/__tests__/env-helper.test.ts +1 -1
  128. package/src/files-provider-s3/__tests__/s3-provider.integration.ts +1 -1
  129. package/src/files-provider-s3/__tests__/s3-provider.test.ts +1 -1
  130. package/src/jobs/__tests__/job-system-user.integration.ts +1 -1
  131. package/src/jobs/__tests__/jobs-events.integration.ts +8 -21
  132. package/src/jobs/__tests__/jobs-feature.integration.ts +1 -1
  133. package/src/jobs/feature.ts +26 -15
  134. package/src/jobs/handlers/detail.query.ts +10 -8
  135. package/src/jobs/handlers/list.query.ts +9 -21
  136. package/src/jobs/handlers/retry.write.ts +2 -7
  137. package/src/jobs/job-run-logger.ts +3 -9
  138. package/src/jobs/job-run-table.ts +49 -17
  139. package/src/legal-pages/__tests__/legal-pages.integration.ts +1 -1
  140. package/src/mail-foundation/__tests__/feature.test.ts +1 -1
  141. package/src/mail-foundation/__tests__/mail-foundation.integration.ts +1 -1
  142. package/src/mail-transport-inmemory/__tests__/feature.test.ts +1 -1
  143. package/src/mail-transport-smtp/__tests__/feature.test.ts +1 -1
  144. package/src/rate-limiting/__tests__/rate-limiting.integration.ts +1 -1
  145. package/src/renderer-foundation/__tests__/api.test.ts +2 -2
  146. package/src/renderer-foundation/__tests__/collect-plugins.integration.ts +1 -1
  147. package/src/renderer-simple/__tests__/adapter.test.ts +2 -2
  148. package/src/renderer-simple/__tests__/simple-renderer.test.ts +1 -1
  149. package/src/secrets/__tests__/require-secrets-context.test.ts +6 -5
  150. package/src/secrets/__tests__/rotate.integration.ts +6 -9
  151. package/src/secrets/__tests__/secrets-events.integration.ts +6 -12
  152. package/src/secrets/__tests__/secrets.integration.ts +6 -11
  153. package/src/secrets/db/queries/read.ts +16 -0
  154. package/src/secrets/handlers/list.query.ts +16 -17
  155. package/src/secrets/handlers/rotate.job.ts +8 -12
  156. package/src/secrets/secrets-context.ts +9 -21
  157. package/src/secrets/table.ts +1 -1
  158. package/src/sessions/__tests__/cleanup.integration.ts +8 -6
  159. package/src/sessions/__tests__/password-auto-revoke.integration.ts +7 -6
  160. package/src/sessions/__tests__/sessions.integration.ts +23 -38
  161. package/src/sessions/__tests__/test-helpers.ts +1 -1
  162. package/src/sessions/db/queries/cleanup.ts +21 -0
  163. package/src/sessions/handlers/cleanup.job.ts +6 -29
  164. package/src/sessions/handlers/list.query.ts +24 -24
  165. package/src/sessions/handlers/mine.query.ts +24 -23
  166. package/src/sessions/handlers/revoke-all-for-user.write.ts +7 -11
  167. package/src/sessions/handlers/revoke-all-others.write.ts +7 -12
  168. package/src/sessions/handlers/revoke.write.ts +11 -18
  169. package/src/sessions/schema/user-session.ts +2 -2
  170. package/src/sessions/session-callbacks.ts +19 -21
  171. package/src/subscription-mollie/__tests__/feature.test.ts +1 -1
  172. package/src/subscription-mollie/__tests__/mollie-foundation.integration.ts +1 -1
  173. package/src/subscription-mollie/__tests__/verify-webhook.test.ts +8 -7
  174. package/src/subscription-stripe/__tests__/feature.test.ts +1 -1
  175. package/src/subscription-stripe/__tests__/plugin-methods.test.ts +14 -15
  176. package/src/subscription-stripe/__tests__/stripe-foundation.integration.ts +1 -1
  177. package/src/subscription-stripe/__tests__/verify-webhook.test.ts +14 -14
  178. package/src/subscription-stripe/verify-webhook.ts +1 -1
  179. package/src/template-resolver/__tests__/handlers.integration.ts +1 -1
  180. package/src/template-resolver/__tests__/template-resolver.integration.ts +3 -2
  181. package/src/template-resolver/api.ts +7 -13
  182. package/src/template-resolver/handlers/archive.write.ts +4 -7
  183. package/src/template-resolver/handlers/find-by-id.query.ts +4 -7
  184. package/src/template-resolver/handlers/list.query.ts +13 -21
  185. package/src/template-resolver/handlers/publish.write.ts +4 -7
  186. package/src/template-resolver/handlers/upsert-system.write.ts +7 -10
  187. package/src/template-resolver/handlers/upsert-tenant.write.ts +7 -10
  188. package/src/template-resolver/table.ts +2 -5
  189. package/src/tenant/__tests__/multi-tenant.integration.ts +1 -1
  190. package/src/tenant/__tests__/seed-testing.integration.ts +19 -45
  191. package/src/tenant/__tests__/tenant.integration.ts +1 -1
  192. package/src/tenant/handlers/active-tenant-ids.query.ts +3 -8
  193. package/src/tenant/handlers/add-member.write.ts +6 -8
  194. package/src/tenant/handlers/cancel-invitation.write.ts +5 -7
  195. package/src/tenant/handlers/invitations.query.ts +5 -10
  196. package/src/tenant/handlers/me.query.ts +2 -3
  197. package/src/tenant/handlers/members.query.ts +4 -5
  198. package/src/tenant/handlers/memberships.query.ts +2 -5
  199. package/src/tenant/handlers/remove-member.write.ts +6 -8
  200. package/src/tenant/handlers/resolve-user-ids.query.ts +6 -16
  201. package/src/tenant/handlers/update-member-roles.write.ts +6 -8
  202. package/src/tenant/invitation-table.ts +2 -5
  203. package/src/tenant/membership-table.ts +3 -6
  204. package/src/tenant/schema/tenant.ts +2 -2
  205. package/src/tenant/seeding.ts +12 -18
  206. package/src/text-content/README.md +1 -1
  207. package/src/text-content/__tests__/text-content.integration.ts +2 -2
  208. package/src/text-content/api.ts +2 -9
  209. package/src/text-content/handlers/by-slug.query.ts +6 -9
  210. package/src/text-content/handlers/by-tenant.query.ts +2 -2
  211. package/src/text-content/handlers/set.write.ts +7 -9
  212. package/src/text-content/seeding.ts +6 -9
  213. package/src/text-content/table.ts +2 -2
  214. package/src/text-content/web/__tests__/editor-read-only.test.tsx +31 -45
  215. package/src/text-content/web/__tests__/group-blocks.test.ts +1 -18
  216. package/src/text-content/web/client-plugin.tsx +11 -23
  217. package/src/tier-engine/__tests__/auto-default-tier.integration.ts +10 -16
  218. package/src/tier-engine/__tests__/compose-app.test.ts +1 -1
  219. package/src/tier-engine/__tests__/drift.test.ts +1 -1
  220. package/src/tier-engine/__tests__/resolver.integration.ts +6 -6
  221. package/src/tier-engine/__tests__/tier-engine.integration.ts +1 -1
  222. package/src/tier-engine/feature.ts +9 -16
  223. package/src/user/__tests__/seed-testing.integration.ts +10 -22
  224. package/src/user/__tests__/user-status.test.ts +1 -1
  225. package/src/user/__tests__/user.integration.ts +6 -5
  226. package/src/user/handlers/create.write.ts +5 -7
  227. package/src/user/handlers/find-for-auth.query.ts +5 -7
  228. package/src/user/schema/user.ts +2 -2
  229. package/src/user/seeding.ts +2 -3
  230. package/src/user-data-rights/__tests__/audit-log.integration.ts +24 -12
  231. package/src/user-data-rights/__tests__/cross-data-matrix.integration.ts +64 -37
  232. package/src/user-data-rights/__tests__/download.integration.ts +29 -46
  233. package/src/user-data-rights/__tests__/export-job-idempotency.integration.ts +35 -28
  234. package/src/user-data-rights/__tests__/export-job-schema.test.ts +2 -2
  235. package/src/user-data-rights/__tests__/policy-to-strategy.test.ts +1 -1
  236. package/src/user-data-rights/__tests__/request-cancel-deletion.integration.ts +11 -15
  237. package/src/user-data-rights/__tests__/request-deletion-callback.integration.ts +10 -12
  238. package/src/user-data-rights/__tests__/request-export.integration.ts +23 -16
  239. package/src/user-data-rights/__tests__/restriction-flow.integration.ts +24 -32
  240. package/src/user-data-rights/__tests__/run-export-jobs.integration.ts +142 -137
  241. package/src/user-data-rights/__tests__/run-forget-cleanup.integration.ts +46 -28
  242. package/src/user-data-rights/__tests__/run-user-export.integration.ts +20 -14
  243. package/src/user-data-rights/__tests__/token-helpers.test.ts +1 -1
  244. package/src/user-data-rights/__tests__/user-data-rights.integration.ts +1 -1
  245. package/src/user-data-rights/__tests__/zip-path.test.ts +1 -1
  246. package/src/user-data-rights/audit-download.ts +3 -3
  247. package/src/user-data-rights/db/queries/export-jobs.ts +23 -0
  248. package/src/user-data-rights/db/queries/forget-cleanup.ts +13 -0
  249. package/src/user-data-rights/handlers/cancel-deletion.write.ts +28 -22
  250. package/src/user-data-rights/handlers/download-by-job.query.ts +11 -21
  251. package/src/user-data-rights/handlers/download-by-token.query.ts +20 -35
  252. package/src/user-data-rights/handlers/export-status.query.ts +19 -33
  253. package/src/user-data-rights/handlers/lift-restriction.write.ts +7 -12
  254. package/src/user-data-rights/handlers/list-download-attempts.query.ts +14 -23
  255. package/src/user-data-rights/handlers/my-audit-log.query.ts +33 -23
  256. package/src/user-data-rights/handlers/request-deletion.write.ts +15 -15
  257. package/src/user-data-rights/handlers/request-export.write.ts +7 -11
  258. package/src/user-data-rights/handlers/restrict-account.write.ts +12 -12
  259. package/src/user-data-rights/run-export-jobs.ts +20 -60
  260. package/src/user-data-rights/run-forget-cleanup.ts +19 -33
  261. package/src/user-data-rights/run-user-export.ts +4 -6
  262. package/src/user-data-rights/schema/download-attempt.ts +2 -2
  263. package/src/user-data-rights/schema/download-token.ts +2 -2
  264. package/src/user-data-rights/schema/export-job.ts +2 -3
  265. package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.ts +37 -30
  266. package/src/user-data-rights-defaults/db/queries/user-hook.ts +17 -0
  267. package/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +12 -27
  268. package/src/user-data-rights-defaults/hooks/user.userdata-hook.ts +16 -18
  269. package/CHANGELOG.md +0 -689
@@ -14,15 +14,16 @@
14
14
  // `drizzle/generate.ts` ergänzen (= via subscriptionsProjectionTable-
15
15
  // import). setupTestStack pusht sie automatisch via r.projection.table.
16
16
 
17
- import { buildDrizzleTable } from "@cosmicdrift/kumiko-framework/db";
17
+ import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
18
18
  import { defineApply } from "@cosmicdrift/kumiko-framework/engine";
19
+ import { upsertSubscriptionProjectionRow } from "./db/queries/subscription-projection";
19
20
  import { subscriptionEntity } from "./entities";
20
21
  import type { SubscriptionEventPayload } from "./events";
21
22
 
22
23
  // Drizzle-table-instance aus dem entity-shape. Wird sowohl von der
23
24
  // projection-apply als auch von list-query / get-helper genutzt damit
24
25
  // alle drei Stellen denselben column-namespace teilen.
25
- export const subscriptionsProjectionTable = buildDrizzleTable("subscription", subscriptionEntity);
26
+ export const subscriptionsProjectionTable = buildEntityTable("subscription", subscriptionEntity);
26
27
 
27
28
  // =============================================================================
28
29
  // Shared helpers
@@ -62,17 +63,35 @@ async function upsert(
62
63
  // jemand nur teil-felder updated (z.B. invoice-payment-failed nur
63
64
  // status+tier), nutzen wir trotzdem den vollen payload für den
64
65
  // INSERT-Pfad und nur den teil-`set` für ON CONFLICT.
65
- await tx
66
- .insert(subscriptionsProjectionTable)
67
- .values({
68
- id: event.aggregateId,
69
- tenantId: event.tenantId,
70
- ...fullSetFromPayload(fullPayload),
71
- })
72
- .onConflictDoUpdate({
73
- target: subscriptionsProjectionTable["id"],
74
- set,
75
- });
66
+ const insertCols = {
67
+ id: event.aggregateId,
68
+ tenant_id: event.tenantId,
69
+ provider_name: fullPayload.providerName,
70
+ provider_customer_id: fullPayload.providerCustomerId,
71
+ provider_subscription_id: fullPayload.providerSubscriptionId,
72
+ status: fullPayload.status,
73
+ tier: fullPayload.tier,
74
+ current_period_end: fullPayload.currentPeriodEndIso,
75
+ };
76
+ // Map camelCase set-keys to snake_case DB columns.
77
+ const setMap: Record<keyof typeof set, string> = {
78
+ providerName: "provider_name",
79
+ providerCustomerId: "provider_customer_id",
80
+ providerSubscriptionId: "provider_subscription_id",
81
+ status: "status",
82
+ tier: "tier",
83
+ currentPeriodEnd: "current_period_end",
84
+ };
85
+ const insertParams = Object.values(insertCols);
86
+ const setEntries = Object.entries(set).filter(([, v]) => v !== undefined);
87
+ const setClauses: string[] = [];
88
+ const allParams: unknown[] = [...insertParams];
89
+ for (const [k, v] of setEntries) {
90
+ allParams.push(v);
91
+ setClauses.push(`"${setMap[k as keyof typeof set]}" = $${allParams.length}`);
92
+ }
93
+ const tableName = (subscriptionsProjectionTable as { tableName: string }).tableName;
94
+ await upsertSubscriptionProjectionRow(tx, tableName, insertCols, setClauses, allParams);
76
95
  }
77
96
 
78
97
  // =============================================================================
@@ -6,6 +6,7 @@
6
6
  // assert. Mirrors the mail-foundation / file-foundation integration
7
7
  // test pattern.
8
8
 
9
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
9
10
  import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
10
11
  import { defineFeature, type WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
11
12
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
@@ -17,7 +18,6 @@ import {
17
18
  testTenantId,
18
19
  unsafeCreateEntityTable,
19
20
  } from "@cosmicdrift/kumiko-framework/stack";
20
- import { afterAll, beforeAll, describe, expect, test } from "vitest";
21
21
  import { z } from "zod";
22
22
  import { CapCounterHandlers, CapCounterQueries } from "../constants";
23
23
  import {
@@ -3,8 +3,7 @@
3
3
  // without spinning up the test-stack — the real event-store +
4
4
  // dispatcher integration is exercised in cap-counter.integration.ts.
5
5
 
6
- import { describe, expect, test, vi } from "vitest";
7
-
6
+ import { describe, expect, mock, test } from "bun:test";
8
7
  // Temporal: rely on the global ambient declaration from temporal-spec.
9
8
  // The framework polyfill is loaded by setupTestStack, but pure unit
10
9
  // tests (no stack) need a manual polyfill — vitest.setup.ts does that.
@@ -18,35 +17,38 @@ import {
18
17
  enforceRollingCapAndMaybeNotify,
19
18
  } from "../enforce-cap";
20
19
 
21
- // --- Calendar-Period stub: db.select().from(...).where(...).limit(1) ---
20
+ // Test-mock: ctx.db unterstützt sowohl bun-db's .unsafe() (selectMany ruft das)
21
+ // als auch drizzle's .select().from().where() chain (rolling-path nutzt das
22
+ // direkt). Beide pfade returnen denselben rows-set unabhängig von filtern.
23
+
24
+ function makeMockDb(rows: unknown[]) {
25
+ return {
26
+ unsafe: async () => rows,
27
+ begin: async <T>(fn: (tx: unknown) => Promise<T>) =>
28
+ fn({ unsafe: async () => rows, begin: async () => undefined }),
29
+ select: () => ({
30
+ from: () => ({
31
+ where: Object.assign(async () => rows, {
32
+ // calendar path also chains .limit(1) after .where()
33
+ limit: async () => rows,
34
+ }),
35
+ }),
36
+ }),
37
+ };
38
+ }
22
39
 
23
40
  function stubCalendarCtx(rows: { value: number; lastSoftWarnedAt: unknown }[]) {
24
41
  const ctx = {
25
- db: {
26
- select: () => ({
27
- from: () => ({
28
- where: () => ({
29
- limit: async () => rows,
30
- }),
31
- }),
32
- }),
33
- },
42
+ db: makeMockDb(rows),
34
43
  user: { tenantId: "tenant-test" },
35
44
  };
36
45
  return ctx as unknown as Parameters<typeof enforceCap>[0];
37
46
  }
38
47
 
39
- // --- Rolling-Window stub: db.select(...).from(...).where(...) returns rows ---
40
-
41
48
  function stubRollingCtx(eventPayloads: { amount: number }[]) {
49
+ const rows = eventPayloads.map((p) => ({ payload: p }));
42
50
  const ctx = {
43
- db: {
44
- select: () => ({
45
- from: () => ({
46
- where: async () => eventPayloads.map((p) => ({ payload: p })),
47
- }),
48
- }),
49
- },
51
+ db: makeMockDb(rows),
50
52
  user: { tenantId: "tenant-test" },
51
53
  };
52
54
  return ctx as unknown as Parameters<typeof enforceRollingCap>[0];
@@ -284,7 +286,7 @@ describe("enforceCapAndMaybeNotify — calendar", () => {
284
286
 
285
287
  test("ok → notifier NICHT aufgerufen", async () => {
286
288
  const ctx = stubCalendarCtx([{ value: 100, lastSoftWarnedAt: null }]);
287
- const notify = vi.fn();
289
+ const notify = mock();
288
290
  const result = await enforceCapAndMaybeNotify(ctx, { ...baseOpts, notify });
289
291
  expect(result.state).toBe("ok");
290
292
  expect(notify).not.toHaveBeenCalled();
@@ -292,19 +294,21 @@ describe("enforceCapAndMaybeNotify — calendar", () => {
292
294
 
293
295
  test("soft-hit, crossed=true → notifier mit info-payload + ctx.write markSoftWarned", async () => {
294
296
  const ctx = stubCalendarCtx([{ value: 1100, lastSoftWarnedAt: null }]);
295
- const write = vi.fn(async () => ({ isSuccess: true, data: {} }));
297
+ const write = mock(async () => ({ isSuccess: true, data: {} }));
296
298
  (ctx as unknown as { write: typeof write }).write = write;
297
- const notify = vi.fn();
299
+ const notify = mock();
298
300
 
299
301
  const result = await enforceCapAndMaybeNotify(ctx, { ...baseOpts, notify });
300
302
  expect(result.state).toBe("soft-hit");
301
- expect(notify).toHaveBeenCalledExactlyOnceWith({
303
+ expect(notify).toHaveBeenCalledTimes(1);
304
+ expect(notify).toHaveBeenCalledWith({
302
305
  capName: "mails-per-month",
303
306
  value: 1100,
304
307
  limit: 1000,
305
308
  tenantId: "tenant-test",
306
309
  });
307
- expect(write).toHaveBeenCalledExactlyOnceWith("cap-counter:write:mark-soft-warned", {
310
+ expect(write).toHaveBeenCalledTimes(1);
311
+ expect(write).toHaveBeenCalledWith("cap-counter:write:mark-soft-warned", {
308
312
  capName: "mails-per-month",
309
313
  periodStartIso: PERIOD,
310
314
  });
@@ -312,7 +316,7 @@ describe("enforceCapAndMaybeNotify — calendar", () => {
312
316
 
313
317
  test("soft-hit, crossed=false (already warned) → notifier NICHT erneut aufgerufen", async () => {
314
318
  const ctx = stubCalendarCtx([{ value: 1150, lastSoftWarnedAt: "2026-05-15T12:00:00Z" }]);
315
- const notify = vi.fn();
319
+ const notify = mock();
316
320
  const result = await enforceCapAndMaybeNotify(ctx, { ...baseOpts, notify });
317
321
  expect(result.state).toBe("soft-hit");
318
322
  expect(notify).not.toHaveBeenCalled();
@@ -320,7 +324,7 @@ describe("enforceCapAndMaybeNotify — calendar", () => {
320
324
 
321
325
  test("hard-hit → throws CapExceededError BEVOR notifier feuert", async () => {
322
326
  const ctx = stubCalendarCtx([{ value: 1500, lastSoftWarnedAt: null }]);
323
- const notify = vi.fn();
327
+ const notify = mock();
324
328
  await expect(enforceCapAndMaybeNotify(ctx, { ...baseOpts, notify })).rejects.toThrow(
325
329
  CapExceededError,
326
330
  );
@@ -342,7 +346,7 @@ describe("enforceRollingCapAndMaybeNotify — rolling", () => {
342
346
 
343
347
  test("ok → notifier NICHT aufgerufen", async () => {
344
348
  const ctx = stubRollingCtx([{ amount: 100 }]);
345
- const notify = vi.fn();
349
+ const notify = mock();
346
350
  const result = await enforceRollingCapAndMaybeNotify(ctx, { ...baseOpts, notify });
347
351
  expect(result.state).toBe("ok");
348
352
  expect(notify).not.toHaveBeenCalled();
@@ -350,10 +354,11 @@ describe("enforceRollingCapAndMaybeNotify — rolling", () => {
350
354
 
351
355
  test("soft-hit → notifier feuert (ohne dedup, Caller-Verantwortung)", async () => {
352
356
  const ctx = stubRollingCtx([{ amount: 6000 }, { amount: 5000 }]);
353
- const notify = vi.fn();
357
+ const notify = mock();
354
358
  const result = await enforceRollingCapAndMaybeNotify(ctx, { ...baseOpts, notify });
355
359
  expect(result.state).toBe("soft-hit");
356
- expect(notify).toHaveBeenCalledExactlyOnceWith({
360
+ expect(notify).toHaveBeenCalledTimes(1);
361
+ expect(notify).toHaveBeenCalledWith({
357
362
  capName: "ai-tokens-7d",
358
363
  value: 11000,
359
364
  limit: 10000,
@@ -367,7 +372,7 @@ describe("enforceRollingCapAndMaybeNotify — rolling", () => {
367
372
  // ein Refactor heimlich Dedup einbaut ohne projection-row, fällt
368
373
  // das hier auf.
369
374
  const ctx = stubRollingCtx([{ amount: 11000 }]);
370
- const notify = vi.fn();
375
+ const notify = mock();
371
376
  await enforceRollingCapAndMaybeNotify(ctx, { ...baseOpts, notify });
372
377
  await enforceRollingCapAndMaybeNotify(ctx, { ...baseOpts, notify });
373
378
  expect(notify).toHaveBeenCalledTimes(2);
@@ -8,6 +8,7 @@
8
8
  // 5. Failed handler: counter NICHT inkrementiert (cap-quota nicht
9
9
  // verbrannt für gescheiterte writes)
10
10
 
11
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
11
12
  import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
12
13
  import { defineFeature, type WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
13
14
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
@@ -18,7 +19,6 @@ import {
18
19
  testTenantId,
19
20
  unsafeCreateEntityTable,
20
21
  } from "@cosmicdrift/kumiko-framework/stack";
21
- import { afterAll, beforeAll, describe, expect, test } from "vitest";
22
22
  import { z } from "zod";
23
23
  import { CapCounterQueries } from "../constants";
24
24
  import type { SoftHitNotifier } from "../enforce-cap";
@@ -1,6 +1,6 @@
1
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
1
2
  import { createEntityExecutor, type HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
2
3
  import { eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
3
- import { and, eq, gte } from "drizzle-orm";
4
4
  import { rollingCapAggregateId } from "./aggregate-id";
5
5
  import {
6
6
  CAP_COUNTER_ROLLING_AGGREGATE_TYPE,
@@ -109,13 +109,12 @@ export async function enforceCap(
109
109
  const softThreshold = options.limit * tolerance.soft;
110
110
  const hardThreshold = options.limit * tolerance.hard;
111
111
 
112
- const rows = await ctx.db
113
- .select()
114
- .from(table)
115
- .where(
116
- and(eq(table["capName"], options.capName), eq(table["periodStart"], options.periodStartIso)),
117
- )
118
- .limit(1);
112
+ const rows = await selectMany(
113
+ ctx.db,
114
+ table,
115
+ { capName: options.capName, periodStart: options.periodStartIso },
116
+ { limit: 1 },
117
+ );
119
118
 
120
119
  const row = rows[0];
121
120
  const value = row ? (row["value"] as number) : 0; // @cast-boundary db-row
@@ -189,18 +188,13 @@ export async function enforceRollingCap(
189
188
  // covers the prefix; the additional aggregate_id eq narrows to the
190
189
  // single rolling-stream. Postgres can use the index even with the
191
190
  // aggregate_id filter applied as a residual.
192
- const rows = await ctx.db
193
- .select({ payload: eventsTable.payload })
194
- .from(eventsTable)
195
- .where(
196
- and(
197
- eq(eventsTable.tenantId, ctx.user.tenantId),
198
- eq(eventsTable.aggregateType, CAP_COUNTER_ROLLING_AGGREGATE_TYPE),
199
- eq(eventsTable.aggregateId, aggregateId),
200
- eq(eventsTable.type, ROLLING_INCREMENTED_EVENT_QN),
201
- gte(eventsTable.createdAt, cutoff),
202
- ),
203
- );
191
+ const rows = await selectMany<{ payload: { amount?: number } }>(ctx.db, eventsTable, {
192
+ tenantId: ctx.user.tenantId,
193
+ aggregateType: CAP_COUNTER_ROLLING_AGGREGATE_TYPE,
194
+ aggregateId,
195
+ type: ROLLING_INCREMENTED_EVENT_QN,
196
+ createdAt: { gte: cutoff },
197
+ });
204
198
 
205
199
  let value = 0;
206
200
  for (const row of rows) {
@@ -1,5 +1,5 @@
1
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
1
2
  import { createEntityExecutor, type QueryHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
2
- import { and, eq } from "drizzle-orm";
3
3
  import { z } from "zod";
4
4
  import { capCounterEntity } from "../entity";
5
5
 
@@ -25,18 +25,12 @@ export const getCounterQuery: QueryHandlerDef = {
25
25
  const { capName, periodStartIso } = query.payload as z.infer<typeof getCounterSchema>; // @cast-boundary engine-payload
26
26
 
27
27
  // ctx.db is tenant-scoped; filter by capName + periodStart explicitly.
28
- const rows = await ctx.db
29
- .select()
30
- .from(table)
31
- .where(
32
- and(
33
- eq(table["capName"], capName),
34
- // periodStart is stored as Temporal.Instant; compare against
35
- // the iso string directly (timestamptz-column round-trips).
36
- eq(table["periodStart"], periodStartIso),
37
- ),
38
- )
39
- .limit(1);
28
+ const rows = await selectMany(
29
+ ctx.db,
30
+ table,
31
+ { capName, periodStart: periodStartIso },
32
+ { limit: 1 },
33
+ );
40
34
 
41
35
  return rows[0] ?? null;
42
36
  },
@@ -1,5 +1,5 @@
1
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
1
2
  import { createEntityExecutor, type WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
2
- import { eq } from "drizzle-orm";
3
3
  import { Temporal } from "temporal-polyfill";
4
4
  import { z } from "zod";
5
5
  import { capCounterAggregateId } from "../aggregate-id";
@@ -55,7 +55,7 @@ export const incrementCapHandler: WriteHandlerDef = {
55
55
 
56
56
  // Read existing aggregate's projection-row to decide create vs update.
57
57
  // ctx.db is auto-tenant-scoped — id-lookup is unique per tenant.
58
- const existing = await ctx.db.select().from(table).where(eq(table["id"], aggregateId)).limit(1);
58
+ const existing = await selectMany(ctx.db, table, { id: aggregateId }, { limit: 1 });
59
59
 
60
60
  if (existing.length === 0) {
61
61
  return executor.create(
@@ -1,5 +1,5 @@
1
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
1
2
  import { createEntityExecutor, type WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
2
- import { eq } from "drizzle-orm";
3
3
  import { Temporal } from "temporal-polyfill";
4
4
  import { z } from "zod";
5
5
  import { capCounterAggregateId } from "../aggregate-id";
@@ -32,7 +32,7 @@ export const markSoftWarnedHandler: WriteHandlerDef = {
32
32
  payload.periodStartIso,
33
33
  );
34
34
 
35
- const existing = await ctx.db.select().from(table).where(eq(table["id"], aggregateId)).limit(1);
35
+ const existing = await selectMany(ctx.db, table, { id: aggregateId }, { limit: 1 });
36
36
  if (existing.length === 0) {
37
37
  throw new Error(
38
38
  `cap-counter: cannot mark-soft-warned, no counter found for tenant=${event.user.tenantId} cap=${payload.capName} period=${payload.periodStartIso}`,
@@ -1,5 +1,5 @@
1
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
1
2
  import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
2
- import { and, desc, eq } from "drizzle-orm";
3
3
  import { z } from "zod";
4
4
  import { inAppMessagesTable } from "../tables";
5
5
 
@@ -11,18 +11,12 @@ export const inboxQuery = defineQueryHandler({
11
11
  }),
12
12
  access: { openToAll: true },
13
13
  handler: async (query, ctx) => {
14
- const conditions = [eq(inAppMessagesTable.userId, query.user.id)];
15
- if (query.payload.unreadOnly) {
16
- conditions.push(eq(inAppMessagesTable.isRead, false));
17
- }
18
-
19
- const rows = await ctx.db
20
- .select()
21
- .from(inAppMessagesTable)
22
- .where(and(...conditions))
23
- .orderBy(desc(inAppMessagesTable.createdAt))
24
- .limit(query.payload.limit);
25
-
14
+ const where: Record<string, unknown> = { userId: query.user.id };
15
+ if (query.payload.unreadOnly) where["isRead"] = false;
16
+ const rows = await selectMany(ctx.db, inAppMessagesTable, where, {
17
+ limit: query.payload.limit,
18
+ orderBy: { col: "createdAt", direction: "desc" },
19
+ });
26
20
  return { rows };
27
21
  },
28
22
  });
@@ -1,5 +1,5 @@
1
+ import { updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
1
2
  import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
2
- import { and, eq } from "drizzle-orm";
3
3
  import { z } from "zod";
4
4
  import { inAppMessagesTable } from "../tables";
5
5
 
@@ -8,14 +8,12 @@ export const markAllReadWrite = defineWriteHandler({
8
8
  schema: z.object({}),
9
9
  access: { openToAll: true },
10
10
  handler: async (event, ctx) => {
11
- const rows = await ctx.db
12
- .update(inAppMessagesTable)
13
- .set({ isRead: true, readAt: Temporal.Now.instant() })
14
- .where(
15
- and(eq(inAppMessagesTable.userId, event.user.id), eq(inAppMessagesTable.isRead, false)),
16
- )
17
- .returning();
18
-
11
+ const rows = await updateMany(
12
+ ctx.db,
13
+ inAppMessagesTable,
14
+ { isRead: true, readAt: Temporal.Now.instant() },
15
+ { userId: event.user.id, isRead: false },
16
+ );
19
17
  return { isSuccess: true, data: { marked: rows.length } };
20
18
  },
21
19
  });
@@ -1,6 +1,6 @@
1
+ import { updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
1
2
  import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
2
3
  import { NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
3
- import { and, eq } from "drizzle-orm";
4
4
  import { z } from "zod";
5
5
  import { inAppMessagesTable } from "../tables";
6
6
 
@@ -12,21 +12,15 @@ export const markReadWrite = defineWriteHandler({
12
12
  }),
13
13
  access: { openToAll: true },
14
14
  handler: async (event, ctx) => {
15
- const rows = await ctx.db
16
- .update(inAppMessagesTable)
17
- .set({ isRead: true, readAt: Temporal.Now.instant() })
18
- .where(
19
- and(
20
- eq(inAppMessagesTable.id, event.payload.id),
21
- eq(inAppMessagesTable.userId, event.user.id),
22
- ),
23
- )
24
- .returning();
25
-
15
+ const rows = await updateMany(
16
+ ctx.db,
17
+ inAppMessagesTable,
18
+ { isRead: true, readAt: Temporal.Now.instant() },
19
+ { id: event.payload.id, userId: event.user.id },
20
+ );
26
21
  if (rows.length === 0) {
27
22
  return writeFailure(new NotFoundError("inAppMessage", event.payload.id));
28
23
  }
29
-
30
- return { isSuccess: true, data: { id: rows[0]?.["id"] } };
24
+ return { isSuccess: true, data: { id: (rows[0] as { id: number } | undefined)?.id } };
31
25
  },
32
26
  });
@@ -1,5 +1,5 @@
1
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
1
2
  import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
2
- import { and, count, eq } from "drizzle-orm";
3
3
  import { z } from "zod";
4
4
  import { inAppMessagesTable } from "../tables";
5
5
 
@@ -8,13 +8,14 @@ export const unreadCountQuery = defineQueryHandler({
8
8
  schema: z.object({}),
9
9
  access: { openToAll: true },
10
10
  handler: async (query, ctx) => {
11
- const rows = await ctx.db
12
- .select({ value: count() })
13
- .from(inAppMessagesTable)
14
- .where(
15
- and(eq(inAppMessagesTable.userId, query.user.id), eq(inAppMessagesTable.isRead, false)),
16
- );
17
-
18
- return { count: rows[0]?.["value"] ?? 0 };
11
+ // bun-db hat keinen aggregate-helper — selectMany alle matching rows,
12
+ // count() in JS. Pragma: unread-counts sind low-cardinality (user-
13
+ // scoped, max ~hunderte rows). Wenn das wachsen sollte: raw .unsafe()
14
+ // mit COUNT(*).
15
+ const rows = await selectMany(ctx.db, inAppMessagesTable, {
16
+ userId: query.user.id,
17
+ isRead: false,
18
+ });
19
+ return { count: rows.length };
19
20
  },
20
21
  });
@@ -1,3 +1,4 @@
1
+ import { insertOne } from "@cosmicdrift/kumiko-framework/bun-db";
1
2
  import { tenantChannel } from "@cosmicdrift/kumiko-framework/engine";
2
3
  import type { DeliveryChannel } from "../delivery";
3
4
  import { inAppMessagesTable } from "./tables";
@@ -14,23 +15,20 @@ export const inAppChannel: DeliveryChannel = {
14
15
  // address is the user-id string after the ES migration — keep it as-is.
15
16
  const userId = address;
16
17
 
17
- const rows = await ctx.db
18
- .insert(inAppMessagesTable)
19
- .values({
20
- tenantId: ctx.tenantId,
21
- userId,
22
- notificationType: message.notificationType,
23
- title: message.title,
24
- body: message.body ?? null,
25
- data: message.data ? JSON.stringify(message.data) : null,
26
- })
27
- .returning();
18
+ const row = await insertOne<{ id: string }>(ctx.db, inAppMessagesTable, {
19
+ tenantId: ctx.tenantId,
20
+ userId,
21
+ notificationType: message.notificationType,
22
+ title: message.title,
23
+ body: message.body ?? null,
24
+ data: message.data ? JSON.stringify(message.data) : null,
25
+ });
28
26
 
29
27
  if (ctx.sseBroker) {
30
28
  ctx.sseBroker.pushToChannel(tenantChannel(ctx.tenantId), {
31
29
  type: "channel-in-app:event:delivered",
32
30
  data: {
33
- id: rows[0]?.["id"],
31
+ id: row?.id,
34
32
  userId,
35
33
  notificationType: message.notificationType,
36
34
  title: message.title,
@@ -3,10 +3,10 @@ import {
3
3
  instant,
4
4
  table as pgTable,
5
5
  serial,
6
+ sql,
6
7
  text,
7
8
  uuid,
8
9
  } from "@cosmicdrift/kumiko-framework/db";
9
- import { sql } from "drizzle-orm";
10
10
 
11
11
  export const inAppMessagesTable = pgTable("in_app_messages", {
12
12
  id: serial("id").primaryKey(),
@@ -1,3 +1,4 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
1
2
  import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
2
3
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
3
4
  import {
@@ -7,7 +8,6 @@ import {
7
8
  testTenantId,
8
9
  unsafeCreateEntityTable,
9
10
  } from "@cosmicdrift/kumiko-framework/stack";
10
- import { afterAll, beforeAll, describe, expect, test } from "vitest";
11
11
  import { createComplianceProfilesFeature, tenantComplianceProfileEntity } from "../feature";
12
12
 
13
13
  const SET_PROFILE = "compliance-profiles:write:set-profile";
@@ -8,6 +8,7 @@
8
8
  // 3. Override wird als JSON-String persistiert + via for-tenant
9
9
  // korrekt zurueckgelesen
10
10
 
11
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
11
12
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
12
13
  import {
13
14
  createTestUser,
@@ -16,7 +17,6 @@ import {
16
17
  testTenantId,
17
18
  unsafeCreateEntityTable,
18
19
  } from "@cosmicdrift/kumiko-framework/stack";
19
- import { afterAll, beforeAll, describe, expect, test } from "vitest";
20
20
  import { createComplianceProfilesFeature, tenantComplianceProfileEntity } from "../feature";
21
21
  import { seedComplianceProfile } from "../seeding";
22
22
 
@@ -0,0 +1,19 @@
1
+ import type { ComplianceProfileOverride } from "@cosmicdrift/kumiko-framework/compliance";
2
+ import { parseJsonSafe } from "@cosmicdrift/kumiko-framework/utils";
3
+
4
+ export function parseComplianceProfileOverride(
5
+ raw: string | null,
6
+ tenantId: string,
7
+ callerLabel: string,
8
+ ): ComplianceProfileOverride | undefined {
9
+ if (!raw || raw.trim() === "") return undefined;
10
+ const parsed = parseJsonSafe<ComplianceProfileOverride | null>(raw, null);
11
+ if (parsed === null) {
12
+ // biome-ignore lint/suspicious/noConsole: operator visibility for DB-corruption edge-case
13
+ console.warn(
14
+ `[${callerLabel}] tenant ${tenantId}: stored override is not valid JSON, ignoring.`,
15
+ );
16
+ return undefined;
17
+ }
18
+ return parsed;
19
+ }