@cosmicdrift/kumiko-bundled-features 0.14.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 +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/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 -689
@@ -1,5 +1,7 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import { deleteMany, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
1
3
  import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
2
- import { buildDrizzleTable, createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
4
+ import { buildEntityTable, createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
3
5
  import {
4
6
  createEntity,
5
7
  createTextField,
@@ -15,8 +17,6 @@ import {
15
17
  TestUsers,
16
18
  unsafePushTables,
17
19
  } from "@cosmicdrift/kumiko-framework/stack";
18
- import { and, eq } from "drizzle-orm";
19
- import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
20
20
  import { z } from "zod";
21
21
  import { createChannelEmailFeature } from "../../channel-email/feature";
22
22
  import { createInMemoryTransport, type EmailMessage } from "../../channel-email/types";
@@ -176,7 +176,7 @@ const ticketEntity = createEntity({
176
176
  status: createTextField({ required: true }),
177
177
  },
178
178
  });
179
- const ticketTable = buildDrizzleTable("ticket", ticketEntity);
179
+ const ticketTable = buildEntityTable("ticket", ticketEntity);
180
180
 
181
181
  function ticketExecutor() {
182
182
  return createEventStoreExecutor(ticketTable, ticketEntity, { entityName: "ticket" });
@@ -346,10 +346,7 @@ describe("flow 1: handler sends notification via ctx.notify()", () => {
346
346
  expect(result).toEqual({ assigned: true });
347
347
 
348
348
  // InApp message in DB
349
- const messages = await db
350
- .select()
351
- .from(inAppMessagesTable)
352
- .where(eq(inAppMessagesTable.userId, user1.id));
349
+ const messages = await selectMany(db, inAppMessagesTable, { userId: user1.id });
353
350
  expect(messages).toHaveLength(1);
354
351
  expect(messages[0]?.["title"]).toBe("Neuer Auftrag");
355
352
  expect(messages[0]?.["body"]).toBe("Auftrag #42 wurde dir zugewiesen");
@@ -361,10 +358,9 @@ describe("flow 1: handler sends notification via ctx.notify()", () => {
361
358
  expect(sseEvents[0]?.data["userId"]).toBe(user1.id);
362
359
 
363
360
  // DeliveryLog entries for all 3 channels
364
- const logs = await db
365
- .select()
366
- .from(deliveryAttemptsTable)
367
- .where(eq(deliveryAttemptsTable.notificationType, "app:notify:order-assigned"));
361
+ const logs = await selectMany(db, deliveryAttemptsTable, {
362
+ notificationType: "app:notify:order-assigned",
363
+ });
368
364
  expect(logs).toHaveLength(3);
369
365
  const channels = logs.map((l) => l["channel"]);
370
366
  expect(channels).toContain("inApp");
@@ -452,15 +448,10 @@ describe("flow 3: broadcast to multiple users + markAllRead", () => {
452
448
 
453
449
  // Both users have messages in DB
454
450
  for (const user of [user1, user2]) {
455
- const messages = await db
456
- .select()
457
- .from(inAppMessagesTable)
458
- .where(
459
- and(
460
- eq(inAppMessagesTable.userId, user.id),
461
- eq(inAppMessagesTable.notificationType, "app:notify:announcement"),
462
- ),
463
- );
451
+ const messages = await selectMany(db, inAppMessagesTable, {
452
+ userId: user.id,
453
+ notificationType: "app:notify:announcement",
454
+ });
464
455
  expect(messages).toHaveLength(1);
465
456
  }
466
457
 
@@ -473,10 +464,9 @@ describe("flow 3: broadcast to multiple users + markAllRead", () => {
473
464
  });
474
465
 
475
466
  test("delivery log has entries for all recipients and channels", async () => {
476
- const logs = await db
477
- .select()
478
- .from(deliveryAttemptsTable)
479
- .where(eq(deliveryAttemptsTable.notificationType, "app:notify:announcement"));
467
+ const logs = await selectMany(db, deliveryAttemptsTable, {
468
+ notificationType: "app:notify:announcement",
469
+ });
480
470
 
481
471
  // 2 users × 3 channels (inApp + email + push) = 6 entries
482
472
  expect(logs).toHaveLength(6);
@@ -530,28 +520,18 @@ describe("flow 4: declarative notification via r.notification()", () => {
530
520
  );
531
521
 
532
522
  // user1 gets ticketAssigned, admin gets ticketCreatedAdmin
533
- const user1Messages = await db
534
- .select()
535
- .from(inAppMessagesTable)
536
- .where(
537
- and(
538
- eq(inAppMessagesTable.userId, user1.id),
539
- eq(inAppMessagesTable.notificationType, "tickets:notify:ticket-assigned"),
540
- ),
541
- );
523
+ const user1Messages = await selectMany(db, inAppMessagesTable, {
524
+ userId: user1.id,
525
+ notificationType: "tickets:notify:ticket-assigned",
526
+ });
542
527
  expect(user1Messages).toHaveLength(1);
543
528
  expect(user1Messages[0]?.["title"]).toBe("Neues Ticket");
544
529
  expect(user1Messages[0]?.["body"]).toContain("Server down");
545
530
 
546
- const adminMessages = await db
547
- .select()
548
- .from(inAppMessagesTable)
549
- .where(
550
- and(
551
- eq(inAppMessagesTable.userId, admin.id),
552
- eq(inAppMessagesTable.notificationType, "tickets:notify:ticket-created-admin"),
553
- ),
554
- );
531
+ const adminMessages = await selectMany(db, inAppMessagesTable, {
532
+ userId: admin.id,
533
+ notificationType: "tickets:notify:ticket-created-admin",
534
+ });
555
535
  expect(adminMessages).toHaveLength(1);
556
536
  expect(adminMessages[0]?.["title"]).toBe("Ticket erstellt");
557
537
 
@@ -564,16 +544,11 @@ describe("flow 4: declarative notification via r.notification()", () => {
564
544
  });
565
545
 
566
546
  test("delivery log entries for both notifications", async () => {
567
- const logs = await db
568
- .select()
569
- .from(deliveryAttemptsTable)
570
- .where(
571
- and(
572
- eq(deliveryAttemptsTable.channel, "inApp"),
573
- eq(deliveryAttemptsTable.recipientId, user1.id),
574
- eq(deliveryAttemptsTable.notificationType, "tickets:notify:ticket-assigned"),
575
- ),
576
- );
547
+ const logs = await selectMany(db, deliveryAttemptsTable, {
548
+ channel: "inApp",
549
+ recipientId: user1.id,
550
+ notificationType: "tickets:notify:ticket-assigned",
551
+ });
577
552
  expect(logs).toHaveLength(1);
578
553
  expect(logs[0]?.["status"]).toBe("sent");
579
554
  });
@@ -592,18 +567,16 @@ describe("flow 5: notification skipped when recipient is null", () => {
592
567
  );
593
568
 
594
569
  // No ticketAssigned notification (no assignee)
595
- const assigneeNotifs = await db
596
- .select()
597
- .from(inAppMessagesTable)
598
- .where(eq(inAppMessagesTable.notificationType, "tickets:notify:ticket-assigned"));
570
+ const assigneeNotifs = await selectMany(db, inAppMessagesTable, {
571
+ notificationType: "tickets:notify:ticket-assigned",
572
+ });
599
573
  // Only the one from flow 4 should exist
600
574
  expect(assigneeNotifs).toHaveLength(1);
601
575
 
602
576
  // But admin still gets ticketCreatedAdmin
603
- const adminMessages = await db
604
- .select()
605
- .from(inAppMessagesTable)
606
- .where(eq(inAppMessagesTable.notificationType, "tickets:notify:ticket-created-admin"));
577
+ const adminMessages = await selectMany(db, inAppMessagesTable, {
578
+ notificationType: "tickets:notify:ticket-created-admin",
579
+ });
607
580
  expect(adminMessages).toHaveLength(2); // flow 4 + flow 5
608
581
 
609
582
  // SSE: only 1 event (admin only, no assignee)
@@ -639,20 +612,14 @@ describe("flow 6: user preferences", () => {
639
612
  stack.events.reset();
640
613
 
641
614
  // Count messages before
642
- const before = await db
643
- .select()
644
- .from(inAppMessagesTable)
645
- .where(eq(inAppMessagesTable.userId, user1.id));
615
+ const before = await selectMany(db, inAppMessagesTable, { userId: user1.id });
646
616
  const beforeCount = before.length;
647
617
 
648
618
  // Send notification to user1 who has disabled inApp for orderAssigned
649
619
  await stack.http.writeOk("app:write:assign-order", { orderId: 99, driverId: user1.id }, admin);
650
620
 
651
621
  // No new InApp message for user1
652
- const after = await db
653
- .select()
654
- .from(inAppMessagesTable)
655
- .where(eq(inAppMessagesTable.userId, user1.id));
622
+ const after = await selectMany(db, inAppMessagesTable, { userId: user1.id });
656
623
  expect(after.length).toBe(beforeCount);
657
624
 
658
625
  // No SSE event
@@ -660,17 +627,12 @@ describe("flow 6: user preferences", () => {
660
627
  expect(notifs).toHaveLength(0);
661
628
 
662
629
  // DeliveryLog shows skipped with preference_disabled
663
- const logs = await db
664
- .select()
665
- .from(deliveryAttemptsTable)
666
- .where(
667
- and(
668
- eq(deliveryAttemptsTable.notificationType, "app:notify:order-assigned"),
669
- eq(deliveryAttemptsTable.recipientId, user1.id),
670
- eq(deliveryAttemptsTable.status, "skipped"),
671
- eq(deliveryAttemptsTable.error, "preference_disabled"),
672
- ),
673
- );
630
+ const logs = await selectMany(db, deliveryAttemptsTable, {
631
+ notificationType: "app:notify:order-assigned",
632
+ recipientId: user1.id,
633
+ status: "skipped",
634
+ error: "preference_disabled",
635
+ });
674
636
  expect(logs.length).toBeGreaterThanOrEqual(1);
675
637
  });
676
638
 
@@ -816,10 +778,9 @@ describe("flow 9: email channel with renderer", () => {
816
778
  });
817
779
 
818
780
  test("delivery log has entries for both channels", async () => {
819
- const logs = await db
820
- .select()
821
- .from(deliveryAttemptsTable)
822
- .where(eq(deliveryAttemptsTable.notificationType, "tickets:notify:ticket-assigned"));
781
+ const logs = await selectMany(db, deliveryAttemptsTable, {
782
+ notificationType: "tickets:notify:ticket-assigned",
783
+ });
823
784
 
824
785
  const channels = logs.map((l) => l["channel"]);
825
786
  expect(channels).toContain("inApp");
@@ -862,15 +823,10 @@ describe("flow 10: complete end-to-end", () => {
862
823
  // --- InApp Channel ---
863
824
 
864
825
  // InApp message in DB with template-transformed data
865
- const inAppMessages = await db
866
- .select()
867
- .from(inAppMessagesTable)
868
- .where(
869
- and(
870
- eq(inAppMessagesTable.userId, user2.id),
871
- eq(inAppMessagesTable.notificationType, "tickets:notify:ticket-assigned"),
872
- ),
873
- );
826
+ const inAppMessages = await selectMany(db, inAppMessagesTable, {
827
+ userId: user2.id,
828
+ notificationType: "tickets:notify:ticket-assigned",
829
+ });
874
830
  // Filter to this specific ticket by checking title
875
831
  const thisMessage = inAppMessages.find((m) =>
876
832
  (m["body"] as string)?.includes("Datenbank Backup"),
@@ -906,15 +862,10 @@ describe("flow 10: complete end-to-end", () => {
906
862
 
907
863
  // --- DeliveryLog ---
908
864
 
909
- const logs = await db
910
- .select()
911
- .from(deliveryAttemptsTable)
912
- .where(
913
- and(
914
- eq(deliveryAttemptsTable.notificationType, "tickets:notify:ticket-assigned"),
915
- eq(deliveryAttemptsTable.recipientId, user2.id),
916
- ),
917
- );
865
+ const logs = await selectMany(db, deliveryAttemptsTable, {
866
+ notificationType: "tickets:notify:ticket-assigned",
867
+ recipientId: user2.id,
868
+ });
918
869
  // Filter to logs from this test (there may be prior entries)
919
870
  const inAppLog = logs.find((l) => l["channel"] === "inApp");
920
871
  const emailLog = logs.find((l) => l["channel"] === "email");
@@ -937,10 +888,9 @@ describe("flow 8: tenant broadcast via to: { tenant }", () => {
937
888
  );
938
889
 
939
890
  // All 3 tenant users get a message
940
- const messages = await db
941
- .select()
942
- .from(inAppMessagesTable)
943
- .where(eq(inAppMessagesTable.notificationType, "app:notify:tenant-alert"));
891
+ const messages = await selectMany(db, inAppMessagesTable, {
892
+ notificationType: "app:notify:tenant-alert",
893
+ });
944
894
  const recipientIds = messages.map((m) => m["userId"]);
945
895
  expect(recipientIds).toContain(admin.id);
946
896
  expect(recipientIds).toContain(user1.id);
@@ -960,10 +910,9 @@ describe("flow 8: tenant broadcast via to: { tenant }", () => {
960
910
  });
961
911
 
962
912
  test("delivery log has entries for all recipients and channels", async () => {
963
- const logs = await db
964
- .select()
965
- .from(deliveryAttemptsTable)
966
- .where(eq(deliveryAttemptsTable.notificationType, "app:notify:tenant-alert"));
913
+ const logs = await selectMany(db, deliveryAttemptsTable, {
914
+ notificationType: "app:notify:tenant-alert",
915
+ });
967
916
 
968
917
  // 3 users × 3 channels (inApp + email + push) = 9
969
918
  expect(logs).toHaveLength(9);
@@ -1001,17 +950,12 @@ describe("flow 12: rate limiting", () => {
1001
950
  await stack.http.writeOk("app:write:assign-order", { orderId: 501, driverId: user1.id }, admin);
1002
951
 
1003
952
  // Email should be skipped (rate limited), but inApp + push should work
1004
- const emailLogs = await db
1005
- .select()
1006
- .from(deliveryAttemptsTable)
1007
- .where(
1008
- and(
1009
- eq(deliveryAttemptsTable.notificationType, "app:notify:order-assigned"),
1010
- eq(deliveryAttemptsTable.recipientId, user1.id),
1011
- eq(deliveryAttemptsTable.channel, "email"),
1012
- eq(deliveryAttemptsTable.error, "rate_limited"),
1013
- ),
1014
- );
953
+ const emailLogs = await selectMany(db, deliveryAttemptsTable, {
954
+ notificationType: "app:notify:order-assigned",
955
+ recipientId: user1.id,
956
+ channel: "email",
957
+ error: "rate_limited",
958
+ });
1015
959
  expect(emailLogs.length).toBeGreaterThanOrEqual(1);
1016
960
 
1017
961
  // InApp still works
@@ -1107,15 +1051,10 @@ describe("flow 12c: idempotency key dedup", () => {
1107
1051
  expect(emails.length).toBe(1);
1108
1052
 
1109
1053
  // Dup attempt is recorded in the log for audit
1110
- const dupLogs = await db
1111
- .select()
1112
- .from(deliveryAttemptsTable)
1113
- .where(
1114
- and(
1115
- eq(deliveryAttemptsTable.notificationType, "app:notify:idem-test"),
1116
- eq(deliveryAttemptsTable.error, "duplicate_idempotency_key"),
1117
- ),
1118
- );
1054
+ const dupLogs = await selectMany(db, deliveryAttemptsTable, {
1055
+ notificationType: "app:notify:idem-test",
1056
+ error: "duplicate_idempotency_key",
1057
+ });
1119
1058
  expect(dupLogs.length).toBe(1);
1120
1059
  });
1121
1060
 
@@ -1162,17 +1101,12 @@ describe("flow 12d: channel error paths", () => {
1162
1101
  expect(emails.length).toBe(0);
1163
1102
 
1164
1103
  // Log shows the failure with the original error string
1165
- const failedLogs = await db
1166
- .select()
1167
- .from(deliveryAttemptsTable)
1168
- .where(
1169
- and(
1170
- eq(deliveryAttemptsTable.notificationType, "app:notify:order-assigned"),
1171
- eq(deliveryAttemptsTable.recipientId, user1.id),
1172
- eq(deliveryAttemptsTable.channel, "email"),
1173
- eq(deliveryAttemptsTable.status, "failed"),
1174
- ),
1175
- );
1104
+ const failedLogs = await selectMany(db, deliveryAttemptsTable, {
1105
+ notificationType: "app:notify:order-assigned",
1106
+ recipientId: user1.id,
1107
+ channel: "email",
1108
+ status: "failed",
1109
+ });
1176
1110
  expect(failedLogs.length).toBeGreaterThanOrEqual(1);
1177
1111
  expect(failedLogs.at(-1)?.["error"]).toContain("smtp_timeout_simulated");
1178
1112
 
@@ -1200,17 +1134,12 @@ describe("flow 12d: channel error paths", () => {
1200
1134
  );
1201
1135
  expect(broadcastEmails.length).toBe(1);
1202
1136
 
1203
- const failedLogs = await db
1204
- .select()
1205
- .from(deliveryAttemptsTable)
1206
- .where(
1207
- and(
1208
- eq(deliveryAttemptsTable.notificationType, "app:notify:announcement"),
1209
- eq(deliveryAttemptsTable.channel, "email"),
1210
- eq(deliveryAttemptsTable.status, "failed"),
1211
- eq(deliveryAttemptsTable.error, "smtp_transient"),
1212
- ),
1213
- );
1137
+ const failedLogs = await selectMany(db, deliveryAttemptsTable, {
1138
+ notificationType: "app:notify:announcement",
1139
+ channel: "email",
1140
+ status: "failed",
1141
+ error: "smtp_transient",
1142
+ });
1214
1143
  expect(failedLogs.length).toBe(1);
1215
1144
  });
1216
1145
  });
@@ -1232,17 +1161,12 @@ describe("flow 13: tenant kill switch", () => {
1232
1161
  expect(pushes).toHaveLength(0);
1233
1162
 
1234
1163
  // DeliveryLog shows channel_disabled
1235
- const pushLogs = await db
1236
- .select()
1237
- .from(deliveryAttemptsTable)
1238
- .where(
1239
- and(
1240
- eq(deliveryAttemptsTable.notificationType, "app:notify:order-assigned"),
1241
- eq(deliveryAttemptsTable.recipientId, user1.id),
1242
- eq(deliveryAttemptsTable.channel, "push"),
1243
- eq(deliveryAttemptsTable.error, "channel_disabled"),
1244
- ),
1245
- );
1164
+ const pushLogs = await selectMany(db, deliveryAttemptsTable, {
1165
+ notificationType: "app:notify:order-assigned",
1166
+ recipientId: user1.id,
1167
+ channel: "push",
1168
+ error: "channel_disabled",
1169
+ });
1246
1170
  expect(pushLogs.length).toBeGreaterThanOrEqual(1);
1247
1171
 
1248
1172
  // InApp + Email still work
@@ -1259,9 +1183,7 @@ describe("flow 13: tenant kill switch", () => {
1259
1183
  describe("flow 14: wildcard-only preference conflicts resolve deterministically", () => {
1260
1184
  test("conflicting wildcards (type=*, false vs channel=*, true) → disabled wins", async () => {
1261
1185
  // Clean slate for user2 on this type/channel
1262
- await db
1263
- .delete(notificationPreferencesTable)
1264
- .where(eq(notificationPreferencesTable.userId, user2.id));
1186
+ await deleteMany(db, notificationPreferencesTable, { userId: user2.id });
1265
1187
 
1266
1188
  // Wildcard A: disable inApp globally
1267
1189
  await stack.http.writeOk(
@@ -1292,17 +1214,12 @@ describe("flow 14: wildcard-only preference conflicts resolve deterministically"
1292
1214
  const inAppEvents = stack.events.sse.filter((e) => e.type === "channel-in-app:event:delivered");
1293
1215
  expect(inAppEvents.filter((e) => e.data["userId"] === user2.id)).toHaveLength(0);
1294
1216
 
1295
- const skipped = await db
1296
- .select()
1297
- .from(deliveryAttemptsTable)
1298
- .where(
1299
- and(
1300
- eq(deliveryAttemptsTable.notificationType, "app:notify:wildcard-conflict"),
1301
- eq(deliveryAttemptsTable.recipientId, user2.id),
1302
- eq(deliveryAttemptsTable.channel, "inApp"),
1303
- eq(deliveryAttemptsTable.error, "preference_disabled"),
1304
- ),
1305
- );
1217
+ const skipped = await selectMany(db, deliveryAttemptsTable, {
1218
+ notificationType: "app:notify:wildcard-conflict",
1219
+ recipientId: user2.id,
1220
+ channel: "inApp",
1221
+ error: "preference_disabled",
1222
+ });
1306
1223
  expect(skipped.length).toBeGreaterThanOrEqual(1);
1307
1224
  });
1308
1225
 
@@ -1334,9 +1251,7 @@ describe("flow 14: wildcard-only preference conflicts resolve deterministically"
1334
1251
  expect(inAppEvents.filter((e) => e.data["userId"] === user2.id)).toHaveLength(1);
1335
1252
 
1336
1253
  // Clean up for later tests
1337
- await db
1338
- .delete(notificationPreferencesTable)
1339
- .where(eq(notificationPreferencesTable.userId, user2.id));
1254
+ await deleteMany(db, notificationPreferencesTable, { userId: user2.id });
1340
1255
  });
1341
1256
  });
1342
1257
 
@@ -1395,16 +1310,11 @@ describe("flow 16: repeated unsubscribe clicks are idempotent", () => {
1395
1310
  }
1396
1311
 
1397
1312
  // Exactly one row exists, marked disabled
1398
- const rows = await db
1399
- .select()
1400
- .from(notificationPreferencesTable)
1401
- .where(
1402
- and(
1403
- eq(notificationPreferencesTable.userId, user1.id),
1404
- eq(notificationPreferencesTable.notificationType, "app:notify:concurrent-unsub"),
1405
- eq(notificationPreferencesTable.channel, "email"),
1406
- ),
1407
- );
1313
+ const rows = await selectMany(db, notificationPreferencesTable, {
1314
+ userId: user1.id,
1315
+ notificationType: "app:notify:concurrent-unsub",
1316
+ channel: "email",
1317
+ });
1408
1318
  expect(rows).toHaveLength(1);
1409
1319
  expect(rows[0]?.["enabled"]).toBe(false);
1410
1320
  });
@@ -0,0 +1,30 @@
1
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
2
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
3
+ import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
4
+
5
+ export type NotificationPreferenceRow = {
6
+ readonly notificationType: string;
7
+ readonly channel: string;
8
+ readonly enabled: boolean;
9
+ };
10
+
11
+ export async function selectNotificationPreferences(
12
+ db: DbConnection,
13
+ tenantId: TenantId,
14
+ userId: string,
15
+ notificationType: string,
16
+ channelName: string,
17
+ ): Promise<readonly NotificationPreferenceRow[]> {
18
+ return asRawClient(db).unsafe<NotificationPreferenceRow>(
19
+ `SELECT notification_type AS "notificationType", channel, enabled
20
+ FROM read_notification_preferences
21
+ WHERE tenant_id = $1
22
+ AND user_id = $2
23
+ AND (
24
+ (notification_type = $3 AND channel = $4)
25
+ OR (notification_type = '*' AND channel = $4)
26
+ OR (notification_type = $3 AND channel = '*')
27
+ )`,
28
+ [tenantId, userId, notificationType, channelName],
29
+ );
30
+ }
@@ -7,11 +7,10 @@ import { append } from "@cosmicdrift/kumiko-framework/event-store";
7
7
  import { runProjectionsForEvent } from "@cosmicdrift/kumiko-framework/pipeline";
8
8
  import { bridgeStub } from "@cosmicdrift/kumiko-framework/testing/handler-context";
9
9
  import { generateId } from "@cosmicdrift/kumiko-framework/utils";
10
- import { and, eq, or } from "drizzle-orm";
11
10
  import type { Redis } from "ioredis";
12
11
  import { DELIVERY_ATTEMPT_EVENT } from "./constants";
12
+ import { selectNotificationPreferences } from "./db/queries/preferences";
13
13
  import { deliveryAttemptSchema } from "./events";
14
- import { notificationPreferencesTable } from "./tables";
15
14
  import type {
16
15
  ChannelContext,
17
16
  ChannelMessage,
@@ -227,40 +226,13 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
227
226
  notificationType: string,
228
227
  channelName: string,
229
228
  ): Promise<boolean> {
230
- type PrefRow = {
231
- readonly notificationType: string;
232
- readonly channel: string;
233
- readonly enabled: boolean;
234
- };
235
- // Drizzle's dynamic-table select() loses column types; assert once at
236
- // the boundary so the rest of this function works against a typed shape.
237
- const prefs = (await db
238
- .select({
239
- notificationType: notificationPreferencesTable.notificationType,
240
- channel: notificationPreferencesTable.channel,
241
- enabled: notificationPreferencesTable.enabled,
242
- })
243
- .from(notificationPreferencesTable)
244
- .where(
245
- and(
246
- eq(notificationPreferencesTable.tenantId, tenantId),
247
- eq(notificationPreferencesTable.userId, userId),
248
- or(
249
- and(
250
- eq(notificationPreferencesTable.notificationType, notificationType),
251
- eq(notificationPreferencesTable.channel, channelName),
252
- ),
253
- and(
254
- eq(notificationPreferencesTable.notificationType, "*"),
255
- eq(notificationPreferencesTable.channel, channelName),
256
- ),
257
- and(
258
- eq(notificationPreferencesTable.notificationType, notificationType),
259
- eq(notificationPreferencesTable.channel, "*"),
260
- ),
261
- ),
262
- ),
263
- )) as readonly PrefRow[]; // @cast-boundary db-row
229
+ const prefs = await selectNotificationPreferences(
230
+ db,
231
+ tenantId,
232
+ userId,
233
+ notificationType,
234
+ channelName,
235
+ );
264
236
 
265
237
  if (prefs.length === 0) return true;
266
238
 
@@ -1,3 +1,4 @@
1
+ import { insertOne } from "@cosmicdrift/kumiko-framework/bun-db";
1
2
  import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
2
3
  import type { z } from "zod";
3
4
  import { DELIVERY_ATTEMPT_EVENT } from "./constants";
@@ -34,7 +35,7 @@ export function createDeliveryFeature(): FeatureDefinition {
34
35
  const p = event.payload as z.infer<typeof deliveryAttemptSchema>; // @cast-boundary engine-payload
35
36
  // PK = aggregateId — replaying the same event twice conflicts on
36
37
  // the PK rather than silently duplicating the log row.
37
- await tx.insert(deliveryAttemptsTable).values({
38
+ await insertOne(tx, deliveryAttemptsTable, {
38
39
  id: event.aggregateId,
39
40
  tenantId: event.tenantId,
40
41
  notificationType: p.notificationType,
@@ -1,5 +1,5 @@
1
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
1
2
  import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
2
- import { desc } from "drizzle-orm";
3
3
  import { z } from "zod";
4
4
  import { deliveryAttemptsTable } from "../tables";
5
5
 
@@ -10,12 +10,10 @@ export const logQuery = defineQueryHandler({
10
10
  }),
11
11
  access: { roles: ["Admin", "SystemAdmin"] },
12
12
  handler: async (query, ctx) => {
13
- const rows = await ctx.db
14
- .select()
15
- .from(deliveryAttemptsTable)
16
- .orderBy(desc(deliveryAttemptsTable.createdAt))
17
- .limit(query.payload.limit);
18
-
13
+ const rows = await selectMany(ctx.db, deliveryAttemptsTable, undefined, {
14
+ orderBy: { col: "createdAt", direction: "desc" },
15
+ limit: query.payload.limit,
16
+ });
19
17
  return { rows };
20
18
  },
21
19
  });
@@ -1,5 +1,5 @@
1
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
1
2
  import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
2
- import { eq } from "drizzle-orm";
3
3
  import { z } from "zod";
4
4
  import { notificationPreferencesTable } from "../tables";
5
5
 
@@ -8,10 +8,7 @@ export const preferencesQuery = 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()
13
- .from(notificationPreferencesTable)
14
- .where(eq(notificationPreferencesTable.userId, query.user.id));
11
+ const rows = await selectMany(ctx.db, notificationPreferencesTable, { userId: query.user.id });
15
12
 
16
13
  return { rows };
17
14
  },