@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -75,10 +75,10 @@
75
75
  "@aws-sdk/client-s3": "^3.1045.0",
76
76
  "@aws-sdk/lib-storage": "^3.1045.0",
77
77
  "@aws-sdk/s3-request-presigner": "^3.1045.0",
78
- "@cosmicdrift/kumiko-dispatcher-live": "0.13.0",
79
- "@cosmicdrift/kumiko-framework": "0.13.0",
80
- "@cosmicdrift/kumiko-renderer": "0.13.0",
81
- "@cosmicdrift/kumiko-renderer-web": "0.13.0",
78
+ "@cosmicdrift/kumiko-dispatcher-live": "0.14.0",
79
+ "@cosmicdrift/kumiko-framework": "0.14.0",
80
+ "@cosmicdrift/kumiko-renderer": "0.14.0",
81
+ "@cosmicdrift/kumiko-renderer-web": "0.14.0",
82
82
  "@mollie/api-client": "^4.5.0",
83
83
  "@node-rs/argon2": "^2.0.2",
84
84
  "@types/nodemailer": "^8.0.0",
@@ -99,4 +99,4 @@
99
99
  "README.md",
100
100
  "LICENSE"
101
101
  ]
102
- }
102
+ }
@@ -1,10 +1,10 @@
1
+ import { describe, expect, it } from "bun:test";
1
2
  import { randomBytes } from "node:crypto";
2
3
  import {
3
4
  composeEnvSchema,
4
5
  type KumikoBootError,
5
6
  parseEnv,
6
7
  } from "@cosmicdrift/kumiko-framework/env";
7
- import { describe, expect, it } from "vitest";
8
8
  import { authEmailPasswordEnvSchema, createAuthEmailPasswordFeature } from "../auth-email-password";
9
9
  import { createSecretsFeature, secretsEnvSchema } from "../secrets";
10
10
  import {
@@ -17,9 +17,11 @@
17
17
  // updateMemberRoles auf — MUSS tenantIdOverride nutzen sonst
18
18
  // version_conflict.
19
19
 
20
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
20
21
  import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
21
22
  import { tmpdir } from "node:os";
22
23
  import { join } from "node:path";
24
+ import { asRawClient, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
23
25
  import { createRegistry, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
24
26
  import {
25
27
  createEsOperationsTable,
@@ -36,8 +38,6 @@ import {
36
38
  unsafeCreateEntityTable,
37
39
  unsafePushTables,
38
40
  } from "@cosmicdrift/kumiko-framework/stack";
39
- import { sql } from "drizzle-orm";
40
- import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
41
41
  import { createConfigFeature } from "../config/feature";
42
42
  import { createConfigResolver } from "../config/resolver";
43
43
  import { configValuesTable } from "../config/table";
@@ -79,7 +79,7 @@ afterAll(async () => {
79
79
  });
80
80
 
81
81
  beforeEach(async () => {
82
- await testDb.db.execute(sql`
82
+ await asRawClient(testDb.db).unsafe(`
83
83
  TRUNCATE kumiko_es_operations, kumiko_events, read_tenants, read_tenant_memberships
84
84
  `);
85
85
  });
@@ -144,21 +144,22 @@ describe("es-ops Phase 1.5 — E2E gegen real-Stack", () => {
144
144
  expect(result.appliedIds).toEqual(["2026-05-21-add-tenant-admin"]);
145
145
 
146
146
  // Verify (a) marker
147
- const markers = await testDb.db.select().from(esOperationsTable);
147
+ const markers = await selectMany(testDb.db, esOperationsTable);
148
148
  expect(markers).toHaveLength(1);
149
149
  expect(markers[0]?.id).toBe("2026-05-21-add-tenant-admin");
150
150
 
151
151
  // Verify (b) event in store mit tenant-membership.updated
152
- const events = (await testDb.db.execute(
153
- sql`SELECT type, tenant_id::text AS tenant_id FROM kumiko_events ORDER BY id`,
152
+ const events = (await asRawClient(testDb.db).unsafe(
153
+ `SELECT type, tenant_id::text AS tenant_id FROM kumiko_events ORDER BY id`,
154
154
  )) as unknown as readonly { type: string; tenant_id: string }[];
155
155
  const updateEvents = events.filter((e) => e.type === "tenant-membership.updated");
156
156
  expect(updateEvents).toHaveLength(1);
157
157
  expect(updateEvents[0]?.tenant_id).toBe(demoTenantId);
158
158
 
159
159
  // Verify (c) read-model aktualisiert
160
- const memberships = (await testDb.db.execute(
161
- sql`SELECT roles FROM read_tenant_memberships WHERE user_id = ${adminUserId}`,
160
+ const memberships = (await asRawClient(testDb.db).unsafe(
161
+ `SELECT roles FROM read_tenant_memberships WHERE user_id = $1`,
162
+ [adminUserId],
162
163
  )) as unknown as readonly { roles: string }[];
163
164
  expect(JSON.parse(memberships[0]?.roles ?? "[]")).toEqual(["Admin", "TenantAdmin"]);
164
165
  } finally {
@@ -202,7 +203,7 @@ describe("es-ops Phase 1.5 — E2E gegen real-Stack", () => {
202
203
  /dry-run found.*unknown handler-QN/,
203
204
  );
204
205
 
205
- const markers = await testDb.db.select().from(esOperationsTable);
206
+ const markers = await selectMany(testDb.db, esOperationsTable);
206
207
  expect(markers).toHaveLength(0);
207
208
  } finally {
208
209
  rmSync(dir, { recursive: true, force: true });
@@ -2,6 +2,8 @@
2
2
  // log IS the audit trail; this suite proves the query handler exposes the
3
3
  // right slices of it (tenant-isolated, filtered, paginated, content-intact).
4
4
 
5
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
6
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
5
7
  import {
6
8
  createEntity,
7
9
  createTextField,
@@ -18,8 +20,6 @@ import {
18
20
  testTenantId,
19
21
  unsafeCreateEntityTable,
20
22
  } from "@cosmicdrift/kumiko-framework/stack";
21
- import { sql } from "drizzle-orm";
22
- import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
23
23
  import { AuditQueries } from "../constants";
24
24
  import { createAuditFeature } from "../feature";
25
25
 
@@ -71,7 +71,7 @@ beforeEach(async () => {
71
71
  // Fresh event log per test — the audit query reads the events table
72
72
  // directly, so stale events from previous tests would leak into results.
73
73
  await resetEventStore(stack);
74
- await stack.db.execute(sql`TRUNCATE audit_widgets`);
74
+ await asRawClient(stack.db).unsafe(`TRUNCATE audit_widgets`);
75
75
  });
76
76
 
77
77
  async function createWidget(user: SessionUser, name: string, color?: string): Promise<string> {
@@ -11,37 +11,23 @@
11
11
  // append time (see event-store-executor → stripSensitive), so this query
12
12
  // can't surface PII that the entity definition marked as sensitive.
13
13
 
14
+ import { selectMany, type WhereObject } from "@cosmicdrift/kumiko-framework/bun-db";
14
15
  import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
15
16
  import { eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
16
- import { and, desc, eq, gte, lt, lte } from "drizzle-orm";
17
17
  import { z } from "zod";
18
18
 
19
- // Per-page cap. 100 keeps a single page payload bounded while being enough
20
- // for a humans-browse UI — clients that need exports iterate by `before`.
21
19
  const MAX_LIMIT = 100;
22
20
 
23
21
  export const listQuery = defineQueryHandler({
24
22
  name: "list",
25
23
  schema: z
26
24
  .object({
27
- // Cursor-style pagination: pass the `id` from the last row of the
28
- // previous page as `before`. bigserial ids are monotonic, so `< before`
29
- // reliably returns "the next older page". Beats OFFSET on large tables.
30
- // The regex pins the input to digits-only — otherwise an invalid value
31
- // would surface as a raw PG `invalid_text_representation` instead of a
32
- // clean 400 at the schema gate.
33
25
  before: z.string().regex(/^\d+$/, "cursor must be a positive integer").optional(),
34
26
  limit: z.number().int().min(1).max(MAX_LIMIT).default(50),
35
- // Filters — all optional. Combined via AND.
36
27
  aggregateType: z.string().optional(),
37
28
  aggregateId: z.uuid().optional(),
38
29
  eventType: z.string().optional(),
39
- // createdBy is stored as text on the events table (it accepts both UUIDs
40
- // and system actor strings like "SYSTEM"), so the filter is a plain
41
- // equality check on the raw value.
42
30
  userId: z.string().optional(),
43
- // Inclusive bounds. Clients pass ISO-8601; we parse to Temporal.Instant
44
- // and compare via the `instant()` column type.
45
31
  from: z.iso.datetime().optional(),
46
32
  to: z.iso.datetime().optional(),
47
33
  })
@@ -52,47 +38,49 @@ export const listQuery = defineQueryHandler({
52
38
  access: { roles: ["Admin", "SystemAdmin"] },
53
39
  handler: async (query, ctx) => {
54
40
  const p = query.payload;
55
- const tenantId = query.user.tenantId;
41
+ const where: WhereObject = { tenantId: query.user.tenantId };
42
+ if (p.aggregateType) where["aggregateType"] = p.aggregateType;
43
+ if (p.aggregateId) where["aggregateId"] = p.aggregateId;
44
+ if (p.eventType) where["type"] = p.eventType;
45
+ if (p.userId) where["createdBy"] = p.userId;
46
+ if (p.from || p.to) {
47
+ const range: { gte?: unknown; lte?: unknown } = {};
48
+ if (p.from) range.gte = Temporal.Instant.from(p.from);
49
+ if (p.to) range.lte = Temporal.Instant.from(p.to);
50
+ where["createdAt"] = range;
51
+ }
52
+ if (p.before) where["id"] = { lt: BigInt(p.before) };
56
53
 
57
- const conditions = [eq(eventsTable.tenantId, tenantId)];
58
- if (p.aggregateType) conditions.push(eq(eventsTable.aggregateType, p.aggregateType));
59
- if (p.aggregateId) conditions.push(eq(eventsTable.aggregateId, p.aggregateId));
60
- if (p.eventType) conditions.push(eq(eventsTable.type, p.eventType));
61
- if (p.userId) conditions.push(eq(eventsTable.createdBy, p.userId));
62
- if (p.from) conditions.push(gte(eventsTable.createdAt, Temporal.Instant.from(p.from)));
63
- if (p.to) conditions.push(lte(eventsTable.createdAt, Temporal.Instant.from(p.to)));
64
- // `before` = last seen id from the previous page. bigserial so `<` walks
65
- // backwards in chronological order. Schema-regex guarantees the string
66
- // is digits-only, so BigInt(...) can't throw.
67
- if (p.before) conditions.push(lt(eventsTable.id, BigInt(p.before)));
54
+ const rows = await selectMany<{
55
+ id: bigint;
56
+ aggregateId: string;
57
+ aggregateType: string;
58
+ version: number;
59
+ type: string;
60
+ payload: Record<string, unknown>;
61
+ metadata: Record<string, unknown>;
62
+ createdAt: unknown;
63
+ createdBy: string;
64
+ }>(ctx.db, eventsTable, where, {
65
+ orderBy: { col: "id", direction: "desc" },
66
+ limit: p.limit,
67
+ });
68
68
 
69
- const rows = await ctx.db
70
- .select({
71
- id: eventsTable.id,
72
- aggregateId: eventsTable.aggregateId,
73
- aggregateType: eventsTable.aggregateType,
74
- version: eventsTable.version,
75
- type: eventsTable.type,
76
- payload: eventsTable.payload,
77
- metadata: eventsTable.metadata,
78
- createdAt: eventsTable.createdAt,
79
- createdBy: eventsTable.createdBy,
80
- })
81
- .from(eventsTable)
82
- .where(and(...conditions))
83
- .orderBy(desc(eventsTable.id))
84
- .limit(p.limit);
85
-
86
- // bigint ids need serialisation — JSON can't carry a plain BigInt, and
87
- // clients pass the cursor back as a string via `before`. Stringified once
88
- // here so the response shape matches what the caller will re-submit.
89
- const serialised = rows.map((r) => ({ ...r, id: String(r["id"]) }));
69
+ const serialised = rows.map((r) => ({
70
+ id: String(r.id),
71
+ aggregateId: r.aggregateId,
72
+ aggregateType: r.aggregateType,
73
+ version: r.version,
74
+ type: r.type,
75
+ payload: r.payload,
76
+ metadata: r.metadata,
77
+ createdAt: r.createdAt,
78
+ createdBy: r.createdBy,
79
+ }));
90
80
  const last = serialised[serialised.length - 1];
91
81
  return {
92
82
  rows: serialised,
93
- // Cursor for the NEXT page. Null when this page is partial (we hit
94
- // the start of the log) so clients know to stop.
95
- nextBefore: serialised.length === p.limit && last ? last["id"] : null,
83
+ nextBefore: serialised.length === p.limit && last ? last.id : null,
96
84
  };
97
85
  },
98
86
  });
@@ -10,7 +10,9 @@
10
10
  // the stack must be built without `context.redis` — shared `beforeAll`
11
11
  // can't mix the two.
12
12
 
13
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
13
14
  import { randomBytes } from "node:crypto";
15
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
14
16
  import { createEncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
15
17
  import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
16
18
  import {
@@ -20,7 +22,6 @@ import {
20
22
  unsafeCreateEntityTable,
21
23
  unsafePushTables,
22
24
  } from "@cosmicdrift/kumiko-framework/stack";
23
- import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
24
25
  import { createConfigFeature } from "../../config";
25
26
  import { createConfigResolver } from "../../config/resolver";
26
27
  import { configValuesTable } from "../../config/table";
@@ -84,8 +85,8 @@ afterAll(async () => {
84
85
  });
85
86
 
86
87
  beforeEach(async () => {
87
- await stack.db.delete(userTable);
88
- await stack.db.delete(tenantMembershipsTable);
88
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${userTable.tableName}"`);
89
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${tenantMembershipsTable.tableName}"`);
89
90
  });
90
91
 
91
92
  async function seedLoginUser(
@@ -9,7 +9,9 @@
9
9
  // - Redis unset: handler still works, lockout is silently skipped (degrades
10
10
  // gracefully to the IP-rate-limiter at the edge)
11
11
 
12
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
12
13
  import { randomBytes } from "node:crypto";
14
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
13
15
  import { createEncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
14
16
  import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
15
17
  import {
@@ -19,7 +21,6 @@ import {
19
21
  unsafeCreateEntityTable,
20
22
  unsafePushTables,
21
23
  } from "@cosmicdrift/kumiko-framework/stack";
22
- import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
23
24
  import { createConfigFeature } from "../../config";
24
25
  import { createConfigResolver } from "../../config/resolver";
25
26
  import { configValuesTable } from "../../config/table";
@@ -84,8 +85,8 @@ afterAll(async () => {
84
85
  });
85
86
 
86
87
  beforeEach(async () => {
87
- await stack.db.delete(userTable);
88
- await stack.db.delete(tenantMembershipsTable);
88
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${userTable.tableName}"`);
89
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${tenantMembershipsTable.tableName}"`);
89
90
  // Clear lockout state between tests — the key prefix is feature-owned, so
90
91
  // a scan-and-del is the safe bet even if tests share a Redis namespace.
91
92
  await stack.redis.flushNamespace();
@@ -1,4 +1,6 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
1
2
  import { randomBytes } from "node:crypto";
3
+ import { asRawClient, insertOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
4
  import { createEncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
3
5
  import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
4
6
  import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
@@ -10,7 +12,6 @@ import {
10
12
  unsafeCreateEntityTable,
11
13
  unsafePushTables,
12
14
  } from "@cosmicdrift/kumiko-framework/stack";
13
- import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
14
15
  import { createConfigFeature } from "../../config";
15
16
  import { createConfigResolver } from "../../config/resolver";
16
17
  import { configValuesTable } from "../../config/table";
@@ -101,8 +102,8 @@ afterAll(async () => {
101
102
  });
102
103
 
103
104
  beforeEach(async () => {
104
- await stack.db.delete(userTable);
105
- await stack.db.delete(tenantMembershipsTable);
105
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${userTable.tableName}"`);
106
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${tenantMembershipsTable.tableName}"`);
106
107
  segmentsByUserAndTenant.clear();
107
108
  plansByTenant.clear();
108
109
  });
@@ -118,7 +119,7 @@ async function seedUser(email: string, password: string): Promise<string> {
118
119
  }
119
120
 
120
121
  async function addMembership(userId: string, tenantId: TenantId, roles: string[]): Promise<void> {
121
- await stack.db.insert(tenantMembershipsTable).values({
122
+ await insertOne(stack.db, tenantMembershipsTable, {
122
123
  userId,
123
124
  tenantId,
124
125
  roles: JSON.stringify(roles),
@@ -1,4 +1,6 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
1
2
  import { randomBytes } from "node:crypto";
3
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
2
4
  import { createEncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
3
5
  import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
4
6
  import {
@@ -15,7 +17,6 @@ import {
15
17
  getSetCookieRaw,
16
18
  getSetCookieValue,
17
19
  } from "@cosmicdrift/kumiko-framework/testing";
18
- import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
19
20
  import { createConfigFeature } from "../../config";
20
21
  import { createConfigResolver } from "../../config/resolver";
21
22
  import { configValuesTable } from "../../config/table";
@@ -67,8 +68,8 @@ afterAll(async () => {
67
68
  });
68
69
 
69
70
  beforeEach(async () => {
70
- await stack.db.delete(userTable);
71
- await stack.db.delete(tenantMembershipsTable);
71
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${userTable.tableName}"`);
72
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${tenantMembershipsTable.tableName}"`);
72
73
  });
73
74
 
74
75
  // Helper: seed a full login-ready user (user row + membership).
@@ -1,6 +1,6 @@
1
+ import { describe, expect, test } from "bun:test";
1
2
  import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
2
3
  import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
3
- import { describe, expect, test } from "vitest";
4
4
  import { runConfirmTokenFlow } from "../handlers/confirm-token-flow";
5
5
 
6
6
  // Pins the "fail-loud when ctx.redis is missing" branch. Without this
@@ -4,7 +4,7 @@
4
4
  // expiresAt, HTML escaped XSS-Versuche, beide Locales unterscheiden
5
5
  // sich erwartungsgemäß.
6
6
 
7
- import { describe, expect, test } from "vitest";
7
+ import { describe, expect, test } from "bun:test";
8
8
  import { renderResetPasswordEmail, renderVerifyEmail } from "../email-templates";
9
9
 
10
10
  describe("renderResetPasswordEmail", () => {
@@ -1,4 +1,6 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
1
2
  import { randomBytes } from "node:crypto";
3
+ import { asRawClient, selectMany, updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
4
  import { createEncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
3
5
  import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
4
6
  import {
@@ -8,9 +10,7 @@ import {
8
10
  unsafeCreateEntityTable,
9
11
  unsafePushTables,
10
12
  } from "@cosmicdrift/kumiko-framework/stack";
11
- import { eq } from "drizzle-orm";
12
13
  import { Temporal } from "temporal-polyfill";
13
- import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
14
14
  import { createConfigFeature } from "../../config";
15
15
  import { createConfigResolver } from "../../config/resolver";
16
16
  import { configValuesTable } from "../../config/table";
@@ -94,8 +94,8 @@ afterAll(async () => {
94
94
  });
95
95
 
96
96
  beforeEach(async () => {
97
- await stack.db.delete(userTable);
98
- await stack.db.delete(tenantMembershipsTable);
97
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${userTable.tableName}"`);
98
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${tenantMembershipsTable.tableName}"`);
99
99
  capturedEmails.length = 0;
100
100
  });
101
101
 
@@ -121,10 +121,7 @@ async function seedUser(opts: {
121
121
  // it directly via SQL after create. Row.version is left at 1; no
122
122
  // subsequent event-store writes happen on this row in these tests.
123
123
  if (opts.emailVerified === true) {
124
- await stack.db
125
- .update(userTable)
126
- .set({ emailVerified: true })
127
- .where(eq(userTable["id"], created.id));
124
+ await updateMany(stack.db, userTable, { emailVerified: true }, { id: created.id });
128
125
  }
129
126
  const tenantId = opts.tenantId ?? "00000000-0000-4000-8000-000000000001";
130
127
  await seedTenantMembership(stack.db, {
@@ -191,7 +188,7 @@ describe("POST /auth/verify-email", () => {
191
188
  const res = await post("/api/auth/verify-email", { token });
192
189
  expect(res.status).toBe(200);
193
190
 
194
- const row = (await stack.db.select().from(userTable)).find((r) => r["id"] === seed.id);
191
+ const row = (await selectMany(stack.db, userTable)).find((r) => r["id"] === seed.id);
195
192
  expect(row?.["emailVerified"]).toBe(true);
196
193
  });
197
194
 
@@ -214,7 +211,7 @@ describe("POST /auth/verify-email", () => {
214
211
  const seed = await seedUser({ email: "retry@example.com", password: "pw-retry-1234" });
215
212
  const { token } = signVerificationToken(seed.id, 60, verifySecret);
216
213
 
217
- await stack.db.delete(tenantMembershipsTable);
214
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${tenantMembershipsTable.tableName}"`);
218
215
  const firstAttempt = await post("/api/auth/verify-email", { token });
219
216
  expect(firstAttempt.status).toBe(422);
220
217
 
@@ -13,8 +13,8 @@
13
13
  // blobs, unsupported PRF — all the ways a verifier could leak a 500
14
14
  // instead of returning false.
15
15
 
16
+ import { describe, expect, test } from "bun:test";
16
17
  import { pbkdf2Sync } from "node:crypto";
17
- import { describe, expect, test } from "vitest";
18
18
  import { isIdentityV3Hash, verifyIdentityV3Hash } from "../identity-v3-hash";
19
19
  import { verifyPassword } from "../password-hashing";
20
20
 
@@ -5,7 +5,9 @@
5
5
  // Das ist der Kern-Use-Case der BMC-Migration — Legacy-Hashes 1:1
6
6
  // übernommen, Login funktioniert weiter.
7
7
 
8
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
8
9
  import { pbkdf2Sync, randomBytes } from "node:crypto";
10
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
9
11
  import { createEncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
10
12
  import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
11
13
  import {
@@ -15,7 +17,6 @@ import {
15
17
  unsafeCreateEntityTable,
16
18
  unsafePushTables,
17
19
  } from "@cosmicdrift/kumiko-framework/stack";
18
- import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
19
20
  import { createConfigFeature } from "../../config";
20
21
  import { createConfigResolver } from "../../config/resolver";
21
22
  import { configValuesTable } from "../../config/table";
@@ -79,8 +80,8 @@ afterAll(async () => {
79
80
  });
80
81
 
81
82
  beforeEach(async () => {
82
- await stack.db.delete(userTable);
83
- await stack.db.delete(tenantMembershipsTable);
83
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${userTable.tableName}"`);
84
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${tenantMembershipsTable.tableName}"`);
84
85
  });
85
86
 
86
87
  describe("Identity-V3 password-hash compatibility", () => {
@@ -14,6 +14,8 @@
14
14
  // 4. Branch-spezifischer Accept-Endpoint
15
15
  // 5. DB-State + Membership + Cookies/JWT verifizieren
16
16
 
17
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
18
+ import { asRawClient, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
17
19
  import {
18
20
  createSystemUser,
19
21
  type SessionUser,
@@ -26,8 +28,6 @@ import {
26
28
  unsafeCreateEntityTable,
27
29
  unsafePushTables,
28
30
  } from "@cosmicdrift/kumiko-framework/stack";
29
- import { eq } from "drizzle-orm";
30
- import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
31
31
  import { createConfigFeature } from "../../config";
32
32
  import { createConfigResolver } from "../../config/resolver";
33
33
  import { configValuesTable } from "../../config/table";
@@ -114,10 +114,10 @@ afterAll(async () => {
114
114
  });
115
115
 
116
116
  beforeEach(async () => {
117
- await stack.db.delete(userTable);
118
- await stack.db.delete(tenantMembershipsTable);
119
- await stack.db.delete(tenantInvitationsTable);
120
- await stack.db.delete(tenantTable);
117
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${userTable.tableName}"`);
118
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${tenantMembershipsTable.tableName}"`);
119
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${tenantInvitationsTable.tableName}"`);
120
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${tenantTable.tableName}"`);
121
121
  capturedInviteEmails.length = 0;
122
122
  const allKeys = await stack.redis.redis.keys("invite:*");
123
123
  if (allKeys.length > 0) await stack.redis.redis.del(...allKeys);
@@ -209,10 +209,7 @@ describe("invite-create", () => {
209
209
  expect(result.token).toBeTruthy();
210
210
  expect(result.token.length).toBeGreaterThanOrEqual(16);
211
211
 
212
- const rows = await stack.db
213
- .select()
214
- .from(tenantInvitationsTable)
215
- .where(eq(tenantInvitationsTable.email, BOB_EMAIL));
212
+ const rows = await selectMany(stack.db, tenantInvitationsTable, { email: BOB_EMAIL });
216
213
  expect(rows).toHaveLength(1);
217
214
  expect(rows[0]?.["status"]).toBe("pending");
218
215
  expect(rows[0]?.["role"]).toBe("Admin");
@@ -226,10 +223,7 @@ describe("invite-create", () => {
226
223
  expect(secondToken).toBe(firstToken);
227
224
 
228
225
  // Eine Row, role updated
229
- const rows = await stack.db
230
- .select()
231
- .from(tenantInvitationsTable)
232
- .where(eq(tenantInvitationsTable.email, BOB_EMAIL));
226
+ const rows = await selectMany(stack.db, tenantInvitationsTable, { email: BOB_EMAIL });
233
227
  expect(rows).toHaveLength(1);
234
228
  expect(rows[0]?.["role"]).toBe("Editor");
235
229
  });
@@ -250,19 +244,13 @@ describe("invite-accept (Branch 1: logged-in)", () => {
250
244
  expect(result.alreadyMember).toBe(false);
251
245
 
252
246
  // Bob hat jetzt 2 Memberships
253
- const memberships = await stack.db
254
- .select()
255
- .from(tenantMembershipsTable)
256
- .where(eq(tenantMembershipsTable.userId, bobId));
247
+ const memberships = await selectMany(stack.db, tenantMembershipsTable, { userId: bobId });
257
248
  expect(memberships).toHaveLength(2);
258
249
  const tenantIds = memberships.map((m) => m["tenantId"]).sort();
259
250
  expect(tenantIds).toEqual([TENANT_A_ID, TENANT_B_ID].sort());
260
251
 
261
252
  // Invitation status = accepted
262
- const inv = await stack.db
263
- .select()
264
- .from(tenantInvitationsTable)
265
- .where(eq(tenantInvitationsTable.email, BOB_EMAIL));
253
+ const inv = await selectMany(stack.db, tenantInvitationsTable, { email: BOB_EMAIL });
266
254
  expect(inv[0]?.["status"]).toBe("accepted");
267
255
  });
268
256
 
@@ -319,10 +307,7 @@ describe("invite-accept-with-login (Branch 2: anon + existing email)", () => {
319
307
  expect(setCookies).toContain("kumiko_auth=");
320
308
 
321
309
  // Membership added
322
- const memberships = await stack.db
323
- .select()
324
- .from(tenantMembershipsTable)
325
- .where(eq(tenantMembershipsTable.userId, bobId));
310
+ const memberships = await selectMany(stack.db, tenantMembershipsTable, { userId: bobId });
326
311
  expect(memberships).toHaveLength(2);
327
312
  });
328
313
 
@@ -359,10 +344,7 @@ describe("invite-signup-complete (Branch 3: anon + new email)", () => {
359
344
  expect(body.role).toBe("Admin");
360
345
 
361
346
  // Carol entstanden in users
362
- const carolRows = await stack.db
363
- .select()
364
- .from(userTable)
365
- .where(eq(userTable.email, CAROL_EMAIL));
347
+ const carolRows = await selectMany(stack.db, userTable, { email: CAROL_EMAIL });
366
348
  expect(carolRows).toHaveLength(1);
367
349
  expect(carolRows[0]?.["emailVerified"]).toBe(true);
368
350
  expect(carolRows[0]?.["id"]).toBe(body.user.id);
@@ -387,10 +369,7 @@ describe("invite-signup-complete (Branch 3: anon + new email)", () => {
387
369
  expect(body.error?.details?.reason).toBe(AuthErrors.invalidInviteToken);
388
370
 
389
371
  // Bob hat keine zweite Membership erworben
390
- const memberships = await stack.db
391
- .select()
392
- .from(tenantMembershipsTable)
393
- .where(eq(tenantMembershipsTable.userId, bobId));
372
+ const memberships = await selectMany(stack.db, tenantMembershipsTable, { userId: bobId });
394
373
  expect(memberships).toHaveLength(1);
395
374
  void GUEST;
396
375
  });
@@ -411,18 +390,12 @@ describe("cancel-invitation", () => {
411
390
  const token = await inviteEmail(BOB_EMAIL, "Admin");
412
391
 
413
392
  // Find invitationId
414
- const rows = await stack.db
415
- .select()
416
- .from(tenantInvitationsTable)
417
- .where(eq(tenantInvitationsTable.email, BOB_EMAIL));
393
+ const rows = await selectMany(stack.db, tenantInvitationsTable, { email: BOB_EMAIL });
418
394
  const invitationId = rows[0]?.["id"] as string;
419
395
 
420
396
  await stack.http.writeOk("tenant:write:cancel-invitation", { invitationId }, aliceSession());
421
397
 
422
- const updated = await stack.db
423
- .select()
424
- .from(tenantInvitationsTable)
425
- .where(eq(tenantInvitationsTable.id, invitationId));
398
+ const updated = await selectMany(stack.db, tenantInvitationsTable, { id: invitationId });
426
399
  expect(updated[0]?.["status"]).toBe("cancelled");
427
400
 
428
401
  // Accept mit dem gecancelten Token → invalid
@@ -437,7 +410,7 @@ describe("invitations-query (pending list)", () => {
437
410
  await inviteEmail(CAROL_EMAIL, "Editor");
438
411
 
439
412
  // Cancel das erste
440
- const allRows = await stack.db.select().from(tenantInvitationsTable);
413
+ const allRows = await selectMany(stack.db, tenantInvitationsTable);
441
414
  const bobInv = allRows.find((r) => r["email"] === BOB_EMAIL);
442
415
  if (!bobInv) throw new Error("bob invitation missing");
443
416
  await stack.http.writeOk(