@cosmicdrift/kumiko-bundled-features 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/package.json +2 -2
  2. package/src/__tests__/env-schemas.test.ts +1 -1
  3. package/src/__tests__/es-ops-e2e.integration.ts +10 -9
  4. package/src/audit/__tests__/audit.integration.ts +3 -3
  5. package/src/audit/handlers/list.query.ts +39 -51
  6. package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.ts +4 -3
  7. package/src/auth-email-password/__tests__/account-lockout.integration.ts +4 -3
  8. package/src/auth-email-password/__tests__/auth-claims.integration.ts +5 -4
  9. package/src/auth-email-password/__tests__/auth.integration.ts +4 -3
  10. package/src/auth-email-password/__tests__/confirm-token-flow.test.ts +1 -1
  11. package/src/auth-email-password/__tests__/email-templates.test.ts +1 -1
  12. package/src/auth-email-password/__tests__/email-verification.integration.ts +7 -10
  13. package/src/auth-email-password/__tests__/identity-v3-hash.test.ts +1 -1
  14. package/src/auth-email-password/__tests__/identity-v3-login.integration.ts +4 -3
  15. package/src/auth-email-password/__tests__/invite-flow.integration.ts +16 -43
  16. package/src/auth-email-password/__tests__/multi-roles.integration.ts +6 -9
  17. package/src/auth-email-password/__tests__/password-reset.integration.ts +8 -7
  18. package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.ts +4 -3
  19. package/src/auth-email-password/__tests__/seed-admin.integration.ts +19 -32
  20. package/src/auth-email-password/__tests__/session-callbacks.integration.ts +6 -5
  21. package/src/auth-email-password/__tests__/session-strict-mode.integration.ts +1 -1
  22. package/src/auth-email-password/__tests__/signed-token.test.ts +1 -1
  23. package/src/auth-email-password/__tests__/signup-flow.integration.ts +11 -15
  24. package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +26 -26
  25. package/src/auth-email-password/handlers/invite-accept.write.ts +24 -21
  26. package/src/auth-email-password/handlers/invite-create.write.ts +3 -8
  27. package/src/auth-email-password/handlers/invite-signup-complete.write.ts +20 -17
  28. package/src/auth-email-password/handlers/signup-confirm.write.ts +3 -7
  29. package/src/auth-email-password/seeding.ts +1 -1
  30. package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +1 -2
  31. package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +10 -19
  32. package/src/auth-email-password/web/__tests__/login-screen.test.tsx +12 -18
  33. package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +12 -17
  34. package/src/auth-email-password/web/__tests__/session-roles.test.ts +1 -1
  35. package/src/auth-email-password/web/__tests__/tenant-switcher.test.tsx +1 -8
  36. package/src/auth-email-password/web/__tests__/test-utils.tsx +4 -8
  37. package/src/auth-email-password/web/__tests__/user-menu.test.tsx +2 -8
  38. package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +10 -15
  39. package/src/billing-foundation/__tests__/billing-foundation.integration.ts +1 -1
  40. package/src/billing-foundation/__tests__/feature.test.ts +1 -1
  41. package/src/billing-foundation/__tests__/webhook-handler.test.ts +6 -5
  42. package/src/billing-foundation/db/queries/subscription-projection.ts +15 -0
  43. package/src/billing-foundation/get-subscription-for-tenant.ts +2 -6
  44. package/src/billing-foundation/handlers/create-portal-session.write.ts +2 -2
  45. package/src/billing-foundation/handlers/list-subscriptions.query.ts +4 -1
  46. package/src/billing-foundation/projection.ts +32 -13
  47. package/src/cap-counter/__tests__/cap-counter.integration.ts +1 -1
  48. package/src/cap-counter/__tests__/enforce-cap.test.ts +37 -32
  49. package/src/cap-counter/__tests__/with-cap-enforcement.integration.ts +1 -1
  50. package/src/cap-counter/enforce-cap.ts +14 -20
  51. package/src/cap-counter/handlers/get-counter.query.ts +7 -13
  52. package/src/cap-counter/handlers/increment.write.ts +2 -2
  53. package/src/cap-counter/handlers/mark-soft-warned.write.ts +2 -2
  54. package/src/channel-in-app/handlers/inbox.query.ts +7 -13
  55. package/src/channel-in-app/handlers/mark-all-read.write.ts +7 -9
  56. package/src/channel-in-app/handlers/mark-read.write.ts +8 -14
  57. package/src/channel-in-app/handlers/unread-count.query.ts +10 -9
  58. package/src/channel-in-app/in-app-channel.ts +10 -12
  59. package/src/channel-in-app/tables.ts +1 -1
  60. package/src/compliance-profiles/__tests__/compliance-profiles.integration.ts +1 -1
  61. package/src/compliance-profiles/__tests__/seeding.integration.ts +1 -1
  62. package/src/compliance-profiles/_internal/parse-override.ts +19 -0
  63. package/src/compliance-profiles/handlers/for-tenant.query.ts +10 -32
  64. package/src/compliance-profiles/handlers/needs-profile.query.ts +4 -7
  65. package/src/compliance-profiles/handlers/set-profile.write.ts +5 -7
  66. package/src/compliance-profiles/resolve-for-tenant.ts +11 -27
  67. package/src/compliance-profiles/schema/profile-selection.ts +2 -2
  68. package/src/compliance-profiles/seeding.ts +4 -7
  69. package/src/config/__tests__/app-overrides.test.ts +1 -1
  70. package/src/config/__tests__/cascade.integration.ts +1 -1
  71. package/src/config/__tests__/config.integration.ts +8 -27
  72. package/src/config/db/queries/resolver.ts +47 -0
  73. package/src/config/handlers/__tests__/prepare-config-write.test.ts +1 -1
  74. package/src/config/resolver.ts +14 -62
  75. package/src/config/table.ts +4 -4
  76. package/src/config/write-helpers.ts +7 -11
  77. package/src/custom-fields/__tests__/audit-integration.integration.ts +6 -6
  78. package/src/custom-fields/__tests__/custom-fields.integration.ts +7 -7
  79. package/src/custom-fields/__tests__/feature.test.ts +1 -1
  80. package/src/custom-fields/__tests__/field-access.integration.ts +6 -6
  81. package/src/custom-fields/__tests__/quota.integration.ts +6 -6
  82. package/src/custom-fields/__tests__/retention.integration.ts +12 -10
  83. package/src/custom-fields/__tests__/user-data-rights.integration.ts +27 -17
  84. package/src/custom-fields/__tests__/wire-for-entity.test.ts +5 -5
  85. package/src/custom-fields/db/queries/field-access.ts +16 -0
  86. package/src/custom-fields/db/queries/projection.ts +43 -0
  87. package/src/custom-fields/db/queries/quota.ts +14 -0
  88. package/src/custom-fields/db/queries/retention.ts +39 -0
  89. package/src/custom-fields/db/queries/user-data-rights.ts +54 -0
  90. package/src/custom-fields/lib/field-access.ts +2 -41
  91. package/src/custom-fields/lib/quota.ts +2 -25
  92. package/src/custom-fields/run-retention.ts +19 -21
  93. package/src/custom-fields/wire-for-entity.ts +30 -23
  94. package/src/custom-fields/wire-user-data-rights.ts +33 -85
  95. package/src/data-retention/__tests__/data-retention.integration.ts +1 -1
  96. package/src/data-retention/__tests__/keep-for.test.ts +1 -1
  97. package/src/data-retention/__tests__/override-schema.test.ts +1 -1
  98. package/src/data-retention/__tests__/policy-for.integration.ts +1 -1
  99. package/src/data-retention/__tests__/resolver.test.ts +1 -1
  100. package/src/data-retention/handlers/policy-for.query.ts +5 -8
  101. package/src/data-retention/resolve-for-tenant.ts +6 -8
  102. package/src/data-retention/schema/tenant-retention-override.ts +2 -2
  103. package/src/delivery/__tests__/delivery-events.integration.ts +8 -21
  104. package/src/delivery/__tests__/delivery.integration.ts +100 -190
  105. package/src/delivery/db/queries/preferences.ts +30 -0
  106. package/src/delivery/delivery-service.ts +8 -36
  107. package/src/delivery/feature.ts +10 -2
  108. package/src/delivery/handlers/log.query.ts +5 -7
  109. package/src/delivery/handlers/preferences.query.ts +2 -5
  110. package/src/delivery/tables.ts +26 -1
  111. package/src/delivery/upsert-preference.ts +8 -14
  112. package/src/feature-toggles/__tests__/feature-toggles.integration.ts +30 -30
  113. package/src/feature-toggles/__tests__/registered-system-tenant.test.ts +7 -6
  114. package/src/feature-toggles/db/queries/toggle-state.ts +25 -0
  115. package/src/feature-toggles/feature.ts +16 -2
  116. package/src/feature-toggles/global-feature-state-table.ts +1 -1
  117. package/src/feature-toggles/handlers/list.query.ts +9 -2
  118. package/src/feature-toggles/handlers/registered.query.ts +3 -7
  119. package/src/feature-toggles/handlers/set.write.ts +37 -25
  120. package/src/feature-toggles/toggle-runtime.ts +3 -6
  121. package/src/file-foundation/__tests__/feature.test.ts +1 -1
  122. package/src/file-foundation/__tests__/file-foundation.integration.ts +1 -1
  123. package/src/file-provider-inmemory/__tests__/feature.test.ts +1 -1
  124. package/src/file-provider-s3/__tests__/feature.test.ts +1 -1
  125. package/src/files/__tests__/files.integration.ts +18 -7
  126. package/src/files/schema/file-ref.ts +1 -1
  127. package/src/files-provider-s3/__tests__/env-helper.test.ts +1 -1
  128. package/src/files-provider-s3/__tests__/s3-provider.integration.ts +1 -1
  129. package/src/files-provider-s3/__tests__/s3-provider.test.ts +1 -1
  130. package/src/jobs/__tests__/job-system-user.integration.ts +1 -1
  131. package/src/jobs/__tests__/jobs-events.integration.ts +8 -21
  132. package/src/jobs/__tests__/jobs-feature.integration.ts +1 -1
  133. package/src/jobs/feature.ts +26 -15
  134. package/src/jobs/handlers/detail.query.ts +10 -8
  135. package/src/jobs/handlers/list.query.ts +9 -21
  136. package/src/jobs/handlers/retry.write.ts +2 -7
  137. package/src/jobs/job-run-logger.ts +3 -9
  138. package/src/jobs/job-run-table.ts +49 -17
  139. package/src/legal-pages/__tests__/legal-pages.integration.ts +1 -1
  140. package/src/mail-foundation/__tests__/feature.test.ts +1 -1
  141. package/src/mail-foundation/__tests__/mail-foundation.integration.ts +1 -1
  142. package/src/mail-transport-inmemory/__tests__/feature.test.ts +1 -1
  143. package/src/mail-transport-smtp/__tests__/feature.test.ts +1 -1
  144. package/src/rate-limiting/__tests__/rate-limiting.integration.ts +1 -1
  145. package/src/renderer-foundation/__tests__/api.test.ts +2 -2
  146. package/src/renderer-foundation/__tests__/collect-plugins.integration.ts +1 -1
  147. package/src/renderer-simple/__tests__/adapter.test.ts +2 -2
  148. package/src/renderer-simple/__tests__/simple-renderer.test.ts +1 -1
  149. package/src/secrets/__tests__/require-secrets-context.test.ts +6 -5
  150. package/src/secrets/__tests__/rotate.integration.ts +6 -9
  151. package/src/secrets/__tests__/secrets-events.integration.ts +6 -12
  152. package/src/secrets/__tests__/secrets.integration.ts +6 -11
  153. package/src/secrets/db/queries/read.ts +16 -0
  154. package/src/secrets/handlers/list.query.ts +16 -17
  155. package/src/secrets/handlers/rotate.job.ts +8 -12
  156. package/src/secrets/secrets-context.ts +9 -21
  157. package/src/secrets/table.ts +1 -1
  158. package/src/sessions/__tests__/cleanup.integration.ts +8 -6
  159. package/src/sessions/__tests__/password-auto-revoke.integration.ts +7 -6
  160. package/src/sessions/__tests__/sessions.integration.ts +23 -38
  161. package/src/sessions/__tests__/test-helpers.ts +1 -1
  162. package/src/sessions/db/queries/cleanup.ts +21 -0
  163. package/src/sessions/handlers/cleanup.job.ts +6 -29
  164. package/src/sessions/handlers/list.query.ts +24 -24
  165. package/src/sessions/handlers/mine.query.ts +24 -23
  166. package/src/sessions/handlers/revoke-all-for-user.write.ts +7 -11
  167. package/src/sessions/handlers/revoke-all-others.write.ts +7 -12
  168. package/src/sessions/handlers/revoke.write.ts +11 -18
  169. package/src/sessions/schema/user-session.ts +2 -2
  170. package/src/sessions/session-callbacks.ts +19 -21
  171. package/src/subscription-mollie/__tests__/feature.test.ts +1 -1
  172. package/src/subscription-mollie/__tests__/mollie-foundation.integration.ts +1 -1
  173. package/src/subscription-mollie/__tests__/verify-webhook.test.ts +8 -7
  174. package/src/subscription-stripe/__tests__/feature.test.ts +1 -1
  175. package/src/subscription-stripe/__tests__/plugin-methods.test.ts +14 -15
  176. package/src/subscription-stripe/__tests__/stripe-foundation.integration.ts +1 -1
  177. package/src/subscription-stripe/__tests__/verify-webhook.test.ts +14 -14
  178. package/src/subscription-stripe/verify-webhook.ts +1 -1
  179. package/src/template-resolver/__tests__/handlers.integration.ts +1 -1
  180. package/src/template-resolver/__tests__/template-resolver.integration.ts +3 -2
  181. package/src/template-resolver/api.ts +7 -13
  182. package/src/template-resolver/handlers/archive.write.ts +4 -7
  183. package/src/template-resolver/handlers/find-by-id.query.ts +4 -7
  184. package/src/template-resolver/handlers/list.query.ts +13 -21
  185. package/src/template-resolver/handlers/publish.write.ts +4 -7
  186. package/src/template-resolver/handlers/upsert-system.write.ts +7 -10
  187. package/src/template-resolver/handlers/upsert-tenant.write.ts +7 -10
  188. package/src/template-resolver/table.ts +2 -5
  189. package/src/tenant/__tests__/multi-tenant.integration.ts +1 -1
  190. package/src/tenant/__tests__/seed-testing.integration.ts +19 -45
  191. package/src/tenant/__tests__/tenant.integration.ts +1 -1
  192. package/src/tenant/handlers/active-tenant-ids.query.ts +3 -8
  193. package/src/tenant/handlers/add-member.write.ts +6 -8
  194. package/src/tenant/handlers/cancel-invitation.write.ts +5 -7
  195. package/src/tenant/handlers/invitations.query.ts +5 -10
  196. package/src/tenant/handlers/me.query.ts +2 -3
  197. package/src/tenant/handlers/members.query.ts +4 -5
  198. package/src/tenant/handlers/memberships.query.ts +2 -5
  199. package/src/tenant/handlers/remove-member.write.ts +6 -8
  200. package/src/tenant/handlers/resolve-user-ids.query.ts +6 -16
  201. package/src/tenant/handlers/update-member-roles.write.ts +6 -8
  202. package/src/tenant/invitation-table.ts +2 -5
  203. package/src/tenant/membership-table.ts +3 -6
  204. package/src/tenant/schema/tenant.ts +2 -2
  205. package/src/tenant/seeding.ts +12 -18
  206. package/src/text-content/README.md +1 -1
  207. package/src/text-content/__tests__/text-content.integration.ts +2 -2
  208. package/src/text-content/api.ts +2 -9
  209. package/src/text-content/handlers/by-slug.query.ts +6 -9
  210. package/src/text-content/handlers/by-tenant.query.ts +2 -2
  211. package/src/text-content/handlers/set.write.ts +7 -9
  212. package/src/text-content/seeding.ts +6 -9
  213. package/src/text-content/table.ts +2 -2
  214. package/src/text-content/web/__tests__/editor-read-only.test.tsx +31 -45
  215. package/src/text-content/web/__tests__/group-blocks.test.ts +1 -18
  216. package/src/text-content/web/client-plugin.tsx +11 -23
  217. package/src/tier-engine/__tests__/auto-default-tier.integration.ts +10 -16
  218. package/src/tier-engine/__tests__/compose-app.test.ts +1 -1
  219. package/src/tier-engine/__tests__/drift.test.ts +1 -1
  220. package/src/tier-engine/__tests__/resolver.integration.ts +6 -6
  221. package/src/tier-engine/__tests__/tier-engine.integration.ts +1 -1
  222. package/src/tier-engine/feature.ts +9 -16
  223. package/src/user/__tests__/seed-testing.integration.ts +10 -22
  224. package/src/user/__tests__/user-status.test.ts +1 -1
  225. package/src/user/__tests__/user.integration.ts +6 -5
  226. package/src/user/handlers/create.write.ts +5 -7
  227. package/src/user/handlers/find-for-auth.query.ts +5 -7
  228. package/src/user/schema/user.ts +2 -2
  229. package/src/user/seeding.ts +2 -3
  230. package/src/user-data-rights/__tests__/audit-log.integration.ts +24 -12
  231. package/src/user-data-rights/__tests__/cross-data-matrix.integration.ts +64 -37
  232. package/src/user-data-rights/__tests__/download.integration.ts +29 -46
  233. package/src/user-data-rights/__tests__/export-job-idempotency.integration.ts +35 -28
  234. package/src/user-data-rights/__tests__/export-job-schema.test.ts +2 -2
  235. package/src/user-data-rights/__tests__/policy-to-strategy.test.ts +1 -1
  236. package/src/user-data-rights/__tests__/request-cancel-deletion.integration.ts +11 -15
  237. package/src/user-data-rights/__tests__/request-deletion-callback.integration.ts +10 -12
  238. package/src/user-data-rights/__tests__/request-export.integration.ts +23 -16
  239. package/src/user-data-rights/__tests__/restriction-flow.integration.ts +24 -32
  240. package/src/user-data-rights/__tests__/run-export-jobs.integration.ts +142 -137
  241. package/src/user-data-rights/__tests__/run-forget-cleanup.integration.ts +46 -28
  242. package/src/user-data-rights/__tests__/run-user-export.integration.ts +20 -14
  243. package/src/user-data-rights/__tests__/token-helpers.test.ts +1 -1
  244. package/src/user-data-rights/__tests__/user-data-rights.integration.ts +1 -1
  245. package/src/user-data-rights/__tests__/zip-path.test.ts +1 -1
  246. package/src/user-data-rights/audit-download.ts +3 -3
  247. package/src/user-data-rights/db/queries/export-jobs.ts +23 -0
  248. package/src/user-data-rights/db/queries/forget-cleanup.ts +13 -0
  249. package/src/user-data-rights/handlers/cancel-deletion.write.ts +28 -22
  250. package/src/user-data-rights/handlers/download-by-job.query.ts +11 -21
  251. package/src/user-data-rights/handlers/download-by-token.query.ts +20 -35
  252. package/src/user-data-rights/handlers/export-status.query.ts +19 -33
  253. package/src/user-data-rights/handlers/lift-restriction.write.ts +7 -12
  254. package/src/user-data-rights/handlers/list-download-attempts.query.ts +14 -23
  255. package/src/user-data-rights/handlers/my-audit-log.query.ts +33 -23
  256. package/src/user-data-rights/handlers/request-deletion.write.ts +15 -15
  257. package/src/user-data-rights/handlers/request-export.write.ts +7 -11
  258. package/src/user-data-rights/handlers/restrict-account.write.ts +12 -12
  259. package/src/user-data-rights/run-export-jobs.ts +20 -60
  260. package/src/user-data-rights/run-forget-cleanup.ts +19 -33
  261. package/src/user-data-rights/run-user-export.ts +4 -6
  262. package/src/user-data-rights/schema/download-attempt.ts +2 -2
  263. package/src/user-data-rights/schema/download-token.ts +2 -2
  264. package/src/user-data-rights/schema/export-job.ts +2 -3
  265. package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.ts +37 -30
  266. package/src/user-data-rights-defaults/db/queries/user-hook.ts +17 -0
  267. package/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +12 -27
  268. package/src/user-data-rights-defaults/hooks/user.userdata-hook.ts +16 -18
  269. package/CHANGELOG.md +0 -689
@@ -17,10 +17,22 @@
17
17
 
18
18
  import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
19
19
  import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
20
- import { getTableName, sql } from "drizzle-orm";
21
- import type { PgTable } from "drizzle-orm/pg-core";
20
+ import {
21
+ selectFieldDefinitionsWithSerialized,
22
+ selectHostRowsWithCustomFields,
23
+ updateHostRowCustomFields,
24
+ } from "./db/queries/retention";
22
25
  import { parseSerializedField } from "./lib/parse-serialized-field";
23
26
 
27
+ const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
28
+ function getTableName(table: unknown): string {
29
+ if (typeof table === "object" && table !== null) {
30
+ const sym = (table as Record<symbol, unknown>)[KUMIKO_NAME_SYMBOL];
31
+ if (typeof sym === "string") return sym;
32
+ }
33
+ throw new Error("custom-fields/run-retention: table missing kumiko:schema:Name symbol");
34
+ }
35
+
24
36
  type Instant = InstanceType<ReturnType<typeof getTemporal>["Instant"]>;
25
37
 
26
38
  // Lifted from data-retention/keep-for.ts because the helper isn't re-exported
@@ -48,7 +60,7 @@ export interface RunCustomFieldsRetentionOptions {
48
60
  readonly db: DbRunner;
49
61
  readonly tenantId: string;
50
62
  readonly entityName: string;
51
- readonly entityTable: PgTable;
63
+ readonly entityTable: unknown;
52
64
  /** Current time, injected for time-travel-tests. */
53
65
  readonly now: Instant;
54
66
  }
@@ -75,18 +87,13 @@ export async function runCustomFieldsRetention(
75
87
  return { rowsScanned: 0, rowsUpdated: 0, removalsByFieldKey: {} };
76
88
  }
77
89
 
78
- const tableName = sql.identifier(getTableName(opts.entityTable));
79
- const rowsResult = await opts.db.execute(sql`
80
- SELECT id, modified_at, custom_fields
81
- FROM ${tableName}
82
- WHERE tenant_id = ${opts.tenantId} AND custom_fields IS NOT NULL
83
- `);
90
+ const tableName = getTableName(opts.entityTable);
91
+ const rows = await selectHostRowsWithCustomFields(opts.db, tableName, opts.tenantId);
84
92
 
85
93
  const removalsByFieldKey: Record<string, number> = {};
86
94
  let rowsUpdated = 0;
87
95
  let rowsScanned = 0;
88
96
 
89
- const rows: ReadonlyArray<unknown> = Array.isArray(rowsResult) ? rowsResult : [];
90
97
  for (const raw of rows) {
91
98
  rowsScanned++;
92
99
  const row = asHostRow(raw);
@@ -125,11 +132,7 @@ export async function runCustomFieldsRetention(
125
132
  removalsByFieldKey[key] = (removalsByFieldKey[key] ?? 0) + 1;
126
133
  }
127
134
 
128
- await opts.db.execute(sql`
129
- UPDATE ${tableName}
130
- SET custom_fields = ${JSON.stringify(mutated)}::jsonb
131
- WHERE id = ${row.id}
132
- `);
135
+ await updateHostRowCustomFields(opts.db, tableName, JSON.stringify(mutated), row.id);
133
136
  rowsUpdated++;
134
137
  }
135
138
 
@@ -171,12 +174,7 @@ async function loadRetentionPolicies(
171
174
  tenantId: string,
172
175
  entityName: string,
173
176
  ): Promise<Map<string, RetentionPolicy>> {
174
- const rowsResult = await db.execute(sql`
175
- SELECT field_key, serialized_field
176
- FROM read_custom_field_definitions
177
- WHERE entity_name = ${entityName} AND tenant_id = ${tenantId}
178
- `);
179
- const rows: ReadonlyArray<unknown> = Array.isArray(rowsResult) ? rowsResult : [];
177
+ const rows = await selectFieldDefinitionsWithSerialized(db, entityName, tenantId);
180
178
  const out = new Map<string, RetentionPolicy>();
181
179
  for (const raw of rows) {
182
180
  // skip: see asHostRow rationale.
@@ -3,10 +3,22 @@ import {
3
3
  type FeatureRegistrar,
4
4
  type JsonbFieldDef,
5
5
  } from "@cosmicdrift/kumiko-framework/engine";
6
- import type { AnyColumn } from "drizzle-orm";
7
- import { eq, sql } from "drizzle-orm";
8
- import type { PgTable } from "drizzle-orm/pg-core";
9
6
  import { CUSTOM_FIELDS_EXTENSION } from "./constants";
7
+ import {
8
+ clearCustomFieldKey,
9
+ removeCustomFieldKeyFromAllRows,
10
+ setCustomFieldValue,
11
+ } from "./db/queries/projection";
12
+
13
+ const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
14
+ function getTableName(table: unknown): string {
15
+ if (typeof table === "object" && table !== null) {
16
+ const sym = (table as Record<symbol, unknown>)[KUMIKO_NAME_SYMBOL];
17
+ if (typeof sym === "string") return sym;
18
+ }
19
+ throw new Error("wire-for-entity: table missing kumiko:schema:Name symbol");
20
+ }
21
+
10
22
  import type { CustomFieldClearedPayload, CustomFieldSetPayload } from "./events";
11
23
  import { customFieldsFeature } from "./feature";
12
24
 
@@ -40,7 +52,7 @@ export function customFieldsField(): JsonbFieldDef {
40
52
  // });
41
53
  //
42
54
  // Der `entityTable`-Parameter ist die Drizzle-Table-Instance (typically
43
- // `buildDrizzleTable(name, entity)`-Output). Die Closure über `entityTable`
55
+ // `buildEntityTable(name, entity)`-Output). Die Closure über `entityTable`
44
56
  // erspart der MSP-apply-fn einen runtime-table-lookup über die Registry.
45
57
  //
46
58
  // **Was registriert wird**:
@@ -67,7 +79,7 @@ export function customFieldsField(): JsonbFieldDef {
67
79
  export function wireCustomFieldsFor<TReg extends FeatureRegistrar<string>>(
68
80
  r: TReg,
69
81
  entityName: string,
70
- entityTable: PgTable,
82
+ entityTable: unknown,
71
83
  ): void {
72
84
  // biome-ignore lint/correctness/useHookAtTopLevel: r.useExtension is a registrar-API method, not a React hook — false positive on the "use"-prefix heuristic.
73
85
  r.useExtension(CUSTOM_FIELDS_EXTENSION, entityName);
@@ -91,14 +103,15 @@ export function wireCustomFieldsFor<TReg extends FeatureRegistrar<string>>(
91
103
 
92
104
  // jsonb_set: setze key auf value. Wenn key noch nicht existiert →
93
105
  // wird angelegt (create_missing=true ist default). value muss als
94
- // jsonb-literal kommen — Drizzle sql-template stringifiziert für uns.
95
- const idCol = (entityTable as unknown as Record<string, AnyColumn>)["id"] as AnyColumn; // @cast-boundary db-row
96
- await tx
97
- .update(entityTable)
98
- .set({
99
- customFields: sql`jsonb_set(${sql.identifier("custom_fields")}, ${sql.raw(`'{${payload.fieldKey.replace(/'/g, "''")}}'`)}, ${JSON.stringify(payload.value)}::jsonb, true)`,
100
- })
101
- .where(eq(idCol, event.aggregateId));
106
+ // jsonb-literal kommen.
107
+ const tableName = getTableName(entityTable);
108
+ await setCustomFieldValue(
109
+ tx,
110
+ tableName,
111
+ payload.fieldKey,
112
+ JSON.stringify(payload.value),
113
+ event.aggregateId,
114
+ );
102
115
  },
103
116
  [clearedEventType]: async (event, tx) => {
104
117
  // skip: MSP feuert für alle aggregate-types — nur unsere host-entity
@@ -107,13 +120,8 @@ export function wireCustomFieldsFor<TReg extends FeatureRegistrar<string>>(
107
120
  const payload = event.payload as CustomFieldClearedPayload; // @cast-boundary engine-payload
108
121
 
109
122
  // jsonb minus operator (`-`) entfernt key aus jsonb-object.
110
- const idCol = (entityTable as unknown as Record<string, AnyColumn>)["id"] as AnyColumn; // @cast-boundary db-row
111
- await tx
112
- .update(entityTable)
113
- .set({
114
- customFields: sql`${sql.identifier("custom_fields")} - ${payload.fieldKey}`,
115
- })
116
- .where(eq(idCol, event.aggregateId));
123
+ const tableName = getTableName(entityTable);
124
+ await clearCustomFieldKey(tx, tableName, payload.fieldKey, event.aggregateId);
117
125
  },
118
126
  [fieldDefDeletedType]: async (event, tx) => {
119
127
  // fieldDefinition.deleted fires nur einmal pro fieldDef-delete
@@ -125,9 +133,8 @@ export function wireCustomFieldsFor<TReg extends FeatureRegistrar<string>>(
125
133
  // ihre Rows.
126
134
  if (payload.entityName !== entityName) return;
127
135
 
128
- await tx.update(entityTable).set({
129
- customFields: sql`${sql.identifier("custom_fields")} - ${payload.fieldKey}`,
130
- });
136
+ const tableName = getTableName(entityTable);
137
+ await removeCustomFieldKeyFromAllRows(tx, tableName, payload.fieldKey);
131
138
  },
132
139
  },
133
140
  });
@@ -1,53 +1,26 @@
1
1
  // T1.5c — user-data-rights wiring for custom-fields.
2
- //
3
- // A consumer that wires `customFields` onto a user-owned host entity
4
- // (e.g. `comment`, `note`, anything with an inserted_by_id column) calls
5
- // this in addition to `wireCustomFieldsFor`:
6
- //
7
- // wireCustomFieldsFor(r, "comment", commentTable);
8
- // wireCustomFieldsUserDataRightsFor(r, {
9
- // entityName: "comment",
10
- // entityTable: commentTable,
11
- // userIdColumn: "inserted_by_id",
12
- // });
13
- //
14
- // Result: a second `r.useExtension(EXT_USER_DATA, "comment", { export, delete })`
15
- // registration whose hooks read/write the customFields jsonb column.
16
- //
17
- // **Export** — every row owned by the user is included; the full customFields
18
- // jsonb travels into the user's export bundle so they can see *all* their
19
- // custom-field data, sensitive or not (DSGVO Art. 15+20 — completeness wins).
20
- //
21
- // **Forget (strategy=anonymize)** — only `sensitive=true` customField keys are
22
- // stripped from the jsonb (`customFields - 'sensitiveKey1' - 'sensitiveKey2'`).
23
- // Non-sensitive customFields stay so the row remains useful to other tenants
24
- // / co-authors. Matches the host-entity anonymize-then-keep contract.
25
- //
26
- // **Forget (strategy=delete)** — no-op. The host entity's own user-data-rights
27
- // hook will delete the row entirely; jsonb goes with it.
28
- //
29
- // Side-step: this wiring requires `user-data-rights` to be installed in the
30
- // composed feature set; if it's not, the boot-validator will reject the
31
- // extension as unknown. That is the consumer's call — it's explicitly opt-in
32
- // (call this function or don't), exactly because some consumers wire custom-
33
- // fields onto tenant-owned entities (e.g. `property`) where DSGVO forget
34
- // doesn't apply per-user.
35
2
 
36
3
  import type { UserDataDeleteHook, UserDataExportHook } from "@cosmicdrift/kumiko-framework/engine";
37
4
  import { EXT_USER_DATA, type FeatureRegistrar } from "@cosmicdrift/kumiko-framework/engine";
38
- import { getTableName, sql } from "drizzle-orm";
39
- import type { PgTable } from "drizzle-orm/pg-core";
5
+ import {
6
+ selectCustomFieldsHostRows,
7
+ selectFieldDefinitionsForEntity,
8
+ stripSensitiveCustomFieldKeys,
9
+ } from "./db/queries/user-data-rights";
40
10
  import { parseSerializedField } from "./lib/parse-serialized-field";
41
11
 
12
+ const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
13
+ function getTableName(table: unknown): string {
14
+ if (typeof table === "object" && table !== null) {
15
+ const sym = (table as Record<symbol, unknown>)[KUMIKO_NAME_SYMBOL];
16
+ if (typeof sym === "string") return sym;
17
+ }
18
+ throw new Error("wire-user-data-rights: table missing kumiko:schema:Name symbol");
19
+ }
20
+
42
21
  export interface WireCustomFieldsUserDataRightsOptions {
43
- /** Host entity name as registered with wireCustomFieldsFor. */
44
22
  readonly entityName: string;
45
- /** Drizzle table for the host entity. Must have a `customFields` jsonb column. */
46
- readonly entityTable: PgTable;
47
- /**
48
- * Snake-case DB column that holds the owning user's id (e.g. `inserted_by_id`,
49
- * `author_id`, `assignee_id`). The hooks filter rows on this + tenant_id.
50
- */
23
+ readonly entityTable: unknown;
51
24
  readonly userIdColumn: string;
52
25
  }
53
26
 
@@ -56,11 +29,6 @@ interface CustomFieldsHostRow {
56
29
  readonly customFields: Record<string, unknown> | null;
57
30
  }
58
31
 
59
- // Drizzle's raw `execute(sql\`SELECT id, custom_fields\`)` returns rows
60
- // keyed in db-column casing (snake_case), not the field-mapping casing.
61
- // The typeguard normalises into the camel-cased internal shape so the
62
- // rest of the hook can stay JS-idiomatic. `in` + `instanceof Object` keep
63
- // the narrowing cast-free.
64
32
  function asCustomFieldsHostRow(value: unknown): CustomFieldsHostRow | null {
65
33
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
66
34
  if (!("id" in value) || typeof value.id !== "string") return null;
@@ -68,8 +36,6 @@ function asCustomFieldsHostRow(value: unknown): CustomFieldsHostRow | null {
68
36
  const cf = value.custom_fields;
69
37
  if (cf === null) return { id: value.id, customFields: null };
70
38
  if (!cf || typeof cf !== "object" || Array.isArray(cf)) return null;
71
- // Object.entries on a narrowed `object` returns `[string, unknown][]` —
72
- // fromEntries widens that back into a typed Record without a cast.
73
39
  return { id: value.id, customFields: Object.fromEntries(Object.entries(cf)) };
74
40
  }
75
41
 
@@ -77,22 +43,19 @@ export function wireCustomFieldsUserDataRightsFor<TReg extends FeatureRegistrar<
77
43
  r: TReg,
78
44
  opts: WireCustomFieldsUserDataRightsOptions,
79
45
  ): void {
80
- const tableName = sql.identifier(getTableName(opts.entityTable));
81
- const userCol = sql.identifier(opts.userIdColumn);
46
+ const tableName = getTableName(opts.entityTable);
82
47
 
83
48
  const exportHook: UserDataExportHook = async (ctx) => {
84
- const rowsResult = await ctx.db.execute(sql`
85
- SELECT id, custom_fields
86
- FROM ${tableName}
87
- WHERE ${userCol} = ${ctx.userId} AND tenant_id = ${ctx.tenantId}
88
- `);
89
- const rows: ReadonlyArray<unknown> = Array.isArray(rowsResult) ? rowsResult : [];
49
+ const rows = await selectCustomFieldsHostRows(
50
+ ctx.db,
51
+ tableName,
52
+ opts.userIdColumn,
53
+ ctx.userId,
54
+ ctx.tenantId,
55
+ );
90
56
  const snippetRows: Array<{ id: string; customFields: Record<string, unknown> }> = [];
91
57
  for (const raw of rows) {
92
58
  const row = asCustomFieldsHostRow(raw);
93
- // skip: drizzle-execute can hand back loosely-typed rows from raw
94
- // queries; if a row's shape doesn't fit, skip rather than guess.
95
- // Real schemas always match — this is defense in depth.
96
59
  if (!row) continue;
97
60
  const customFields = row.customFields;
98
61
  if (customFields && Object.keys(customFields).length > 0) {
@@ -104,30 +67,22 @@ export function wireCustomFieldsUserDataRightsFor<TReg extends FeatureRegistrar<
104
67
  };
105
68
 
106
69
  const deleteHook: UserDataDeleteHook = async (ctx, strategy) => {
107
- // skip: strategy=delete is handled by the host entity's own user-
108
- // data-rights hook (it removes the row; customFields jsonb travels
109
- // with it). Nothing left for this layer to do.
70
+ // skip: delete strategy removes rows wholesale custom-field redaction N/A.
110
71
  if (strategy === "delete") return;
111
72
  const sensitiveKeys = await loadSensitiveFieldKeys(ctx.db, ctx.tenantId, opts.entityName);
112
- // skip: no sensitive keys declared for this entity → anonymize is a
113
- // no-op. Avoids a useless UPDATE statement.
73
+ // skip: no sensitive custom fields configured for this entity.
114
74
  if (sensitiveKeys.length === 0) return;
115
75
 
116
- // Build the chain of jsonb minus operators: customFields - 'k1' - 'k2' - ...
117
- const minusChain = sensitiveKeys.reduce<ReturnType<typeof sql>>(
118
- (acc, key) => sql`${acc} - ${key}`,
119
- sql`custom_fields`,
76
+ await stripSensitiveCustomFieldKeys(
77
+ ctx.db,
78
+ tableName,
79
+ opts.userIdColumn,
80
+ sensitiveKeys,
81
+ ctx.userId,
82
+ ctx.tenantId,
120
83
  );
121
- await ctx.db.execute(sql`
122
- UPDATE ${tableName}
123
- SET custom_fields = ${minusChain}
124
- WHERE ${userCol} = ${ctx.userId} AND tenant_id = ${ctx.tenantId}
125
- `);
126
84
  };
127
85
 
128
- // r.useExtension's options-bag accepts a structural object — pass the
129
- // hooks inline so TS sees the literal-typed shape and Drizzle's strict
130
- // mode doesn't reject the nominal UserDataExtensionHooks branding.
131
86
  // biome-ignore lint/correctness/useHookAtTopLevel: r.useExtension is a registrar API, not a React hook.
132
87
  r.useExtension(EXT_USER_DATA, opts.entityName, {
133
88
  export: exportHook,
@@ -151,16 +106,9 @@ async function loadSensitiveFieldKeys(
151
106
  tenantId: string,
152
107
  entityName: string,
153
108
  ): Promise<string[]> {
154
- const rowsResult = await db.execute(sql`
155
- SELECT field_key, serialized_field
156
- FROM read_custom_field_definitions
157
- WHERE entity_name = ${entityName} AND tenant_id = ${tenantId}
158
- `);
159
- const rows: ReadonlyArray<unknown> = Array.isArray(rowsResult) ? rowsResult : [];
109
+ const rows = await selectFieldDefinitionsForEntity(db, entityName, tenantId);
160
110
  const keys: string[] = [];
161
111
  for (const raw of rows) {
162
- // skip: see isCustomFieldsHostRow rationale — defense in depth against
163
- // driver shape drift.
164
112
  if (!isFieldDefinitionRow(raw)) continue;
165
113
  const parsed = parseSerializedField(raw.serialized_field);
166
114
  if (parsed?.sensitive === true) keys.push(raw.field_key);
@@ -6,12 +6,12 @@
6
6
  // ob Boot-Validation oder Entity-Definition gebrochen ist — pre-S2.D2b
7
7
  // Sicherheitsnetz.
8
8
 
9
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
9
10
  import {
10
11
  setupTestStack,
11
12
  type TestStack,
12
13
  unsafeCreateEntityTable,
13
14
  } from "@cosmicdrift/kumiko-framework/stack";
14
- import { afterAll, beforeAll, describe, expect, test } from "vitest";
15
15
  import { createDataRetentionFeature, tenantRetentionOverrideEntity } from "../feature";
16
16
 
17
17
  let stack: TestStack;
@@ -1,5 +1,5 @@
1
+ import { beforeAll, describe, expect, test } from "bun:test";
1
2
  import { ensureTemporalPolyfill, getTemporal } from "@cosmicdrift/kumiko-framework/time";
2
- import { beforeAll, describe, expect, test } from "vitest";
3
3
  import { computeCutoff, InvalidKeepForError, isPastCutoff } from "../keep-for";
4
4
 
5
5
  beforeAll(async () => {
@@ -1,7 +1,7 @@
1
1
  // Tests fuer retentionOverrideSchema (S2.D2.5 M2+M3) — strict-Zod
2
2
  // faengt Sub-Level-Tippfehler + Strategy-Enum-Drift + keepFor-Format-Drift.
3
3
 
4
- import { describe, expect, test } from "vitest";
4
+ import { describe, expect, test } from "bun:test";
5
5
  import { retentionOverrideSchema } from "../override-schema";
6
6
 
7
7
  describe("retentionOverrideSchema — accept-Faelle", () => {
@@ -2,6 +2,7 @@
2
2
  // API für Forget-Flow + Cleanup-Job. Round-trip: Override in DB seeden,
3
3
  // Query rufen, verify dass resolver Override greift.
4
4
 
5
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
5
6
  import {
6
7
  createEventStoreExecutor,
7
8
  createTenantDb,
@@ -15,7 +16,6 @@ import {
15
16
  testTenantId,
16
17
  unsafeCreateEntityTable,
17
18
  } from "@cosmicdrift/kumiko-framework/stack";
18
- import { afterAll, beforeAll, describe, expect, test } from "vitest";
19
19
  import { createDataRetentionFeature, tenantRetentionOverrideEntity } from "../feature";
20
20
  import { tenantRetentionOverrideTable } from "../schema/tenant-retention-override";
21
21
 
@@ -1,8 +1,8 @@
1
1
  // Unit-Tests für resolveRetentionPolicy — pure function, keine
2
2
  // Test-Stack-Abhängigkeit.
3
3
 
4
+ import { describe, expect, test } from "bun:test";
4
5
  import { createEntity, createTextField } from "@cosmicdrift/kumiko-framework/engine";
5
- import { describe, expect, test } from "vitest";
6
6
  import { resolveRetentionPolicy } from "../resolver";
7
7
 
8
8
  describe("resolveRetentionPolicy — Layer-Resolution", () => {
@@ -1,6 +1,5 @@
1
- import { fetchOne } from "@cosmicdrift/kumiko-framework/db";
1
+ import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
2
  import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
3
- import { eq } from "drizzle-orm";
4
3
  import { z } from "zod";
5
4
  import { parseRetentionOverrideOrNull } from "../_internal/parse-override";
6
5
  import { type EffectiveRetentionPolicy, resolveRetentionPolicy } from "../resolver";
@@ -28,12 +27,10 @@ export const policyForQuery = defineQueryHandler({
28
27
  const entityName = query.payload.entityName;
29
28
 
30
29
  // Layer 3: Tenant-Override aus DB laden (UNIQUE(tenantId, entityName))
31
- const overrideRow = (await fetchOne(
32
- ctx.db,
33
- tenantRetentionOverrideTable,
34
- eq(tenantRetentionOverrideTable["tenantId"], query.user.tenantId),
35
- eq(tenantRetentionOverrideTable["entityName"], entityName),
36
- )) as { config: string | null } | null; // @cast-boundary db-runner
30
+ const overrideRow = (await fetchOne(ctx.db, tenantRetentionOverrideTable, {
31
+ tenantId: query.user.tenantId,
32
+ entityName,
33
+ })) as { config: string | null } | null; // @cast-boundary db-runner
37
34
 
38
35
  const tenantOverride = parseRetentionOverrideOrNull(
39
36
  overrideRow?.config ?? null,
@@ -6,9 +6,9 @@
6
6
  // HandlerContext. Beide Pfade nutzen denselben `parseRetentionOverrideOrNull`
7
7
  // + `resolveRetentionPolicy`, also kein Drift-Risiko.
8
8
 
9
- import { type DbRunner, fetchOne } from "@cosmicdrift/kumiko-framework/db";
9
+ import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
10
+ import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
10
11
  import type { Registry, TenantId } from "@cosmicdrift/kumiko-framework/engine";
11
- import { eq } from "drizzle-orm";
12
12
  import { parseRetentionOverrideOrNull } from "./_internal/parse-override";
13
13
  import { type EffectiveRetentionPolicy, resolveRetentionPolicy } from "./resolver";
14
14
  import { tenantRetentionOverrideTable } from "./schema/tenant-retention-override";
@@ -23,12 +23,10 @@ export interface ResolveForTenantArgs {
23
23
  export async function resolveRetentionPolicyForTenant(
24
24
  args: ResolveForTenantArgs,
25
25
  ): Promise<EffectiveRetentionPolicy> {
26
- const overrideRow = (await fetchOne(
27
- args.db,
28
- tenantRetentionOverrideTable,
29
- eq(tenantRetentionOverrideTable["tenantId"], args.tenantId),
30
- eq(tenantRetentionOverrideTable["entityName"], args.entityName),
31
- )) as { config: string | null } | null; // @cast-boundary db-runner
26
+ const overrideRow = (await fetchOne(args.db, tenantRetentionOverrideTable, {
27
+ tenantId: args.tenantId,
28
+ entityName: args.entityName,
29
+ })) as { config: string | null } | null; // @cast-boundary db-runner
32
30
 
33
31
  const tenantOverride = parseRetentionOverrideOrNull(
34
32
  overrideRow?.config ?? null,
@@ -1,4 +1,4 @@
1
- import { buildDrizzleTable } from "@cosmicdrift/kumiko-framework/db";
1
+ import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
2
2
  import {
3
3
  createEntity,
4
4
  createLongTextField,
@@ -41,7 +41,7 @@ export const tenantRetentionOverrideEntity = createEntity({
41
41
  indexes: [{ unique: true, columns: ["tenantId", "entityName"] }],
42
42
  });
43
43
 
44
- export const tenantRetentionOverrideTable = buildDrizzleTable(
44
+ export const tenantRetentionOverrideTable = buildEntityTable(
45
45
  "tenantRetentionOverride",
46
46
  tenantRetentionOverrideEntity,
47
47
  );
@@ -6,6 +6,8 @@
6
6
  // aggregateType) fails loudly instead of breaking downstream consumers
7
7
  // (MSPs, audit-feature, event-replays) who subscribe by name.
8
8
 
9
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
10
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
9
11
  import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
10
12
  import { eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
11
13
  import {
@@ -16,8 +18,7 @@ import {
16
18
  unsafeCreateEntityTable,
17
19
  unsafePushTables,
18
20
  } from "@cosmicdrift/kumiko-framework/stack";
19
- import { eq } from "drizzle-orm";
20
- import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
21
+ import { resetTestTables } from "@cosmicdrift/kumiko-framework/testing";
21
22
  import { createChannelInAppFeature } from "../../channel-in-app/feature";
22
23
  import { inAppMessagesTable } from "../../channel-in-app/tables";
23
24
  import { createConfigFeature, createConfigResolver } from "../../config";
@@ -75,9 +76,7 @@ afterAll(async () => {
75
76
  });
76
77
 
77
78
  beforeEach(async () => {
78
- // Fresh state per test so event-count assertions are deterministic.
79
- await db.delete(eventsTable);
80
- await db.delete(deliveryAttemptsTable);
79
+ await resetTestTables(db, [eventsTable, deliveryAttemptsTable]);
81
80
  });
82
81
 
83
82
  describe("delivery event shape", () => {
@@ -89,10 +88,7 @@ describe("delivery event shape", () => {
89
88
  admin.tenantId,
90
89
  );
91
90
 
92
- const events = await db
93
- .select()
94
- .from(eventsTable)
95
- .where(eq(eventsTable.aggregateType, "deliveryAttempt"));
91
+ const events = await selectMany(db, eventsTable, { aggregateType: "deliveryAttempt" });
96
92
 
97
93
  // One channel registered (in-app) → one delivery attempt → one event.
98
94
  expect(events).toHaveLength(1);
@@ -121,10 +117,7 @@ describe("delivery event shape", () => {
121
117
  admin.tenantId,
122
118
  );
123
119
 
124
- const [event] = await db
125
- .select()
126
- .from(eventsTable)
127
- .where(eq(eventsTable.aggregateType, "deliveryAttempt"));
120
+ const [event] = await selectMany(db, eventsTable, { aggregateType: "deliveryAttempt" });
128
121
  if (!event) throw new Error("expected one event");
129
122
 
130
123
  // The service schema-parses before append (see logDelivery), but we
@@ -145,10 +138,7 @@ describe("delivery event shape", () => {
145
138
  admin.tenantId,
146
139
  );
147
140
 
148
- const [event] = await db
149
- .select()
150
- .from(eventsTable)
151
- .where(eq(eventsTable.aggregateType, "deliveryAttempt"));
141
+ const [event] = await selectMany(db, eventsTable, { aggregateType: "deliveryAttempt" });
152
142
  if (!event) throw new Error("expected one event");
153
143
 
154
144
  // PK is unique — a matching row on `id === aggregateId` is already the
@@ -156,10 +146,7 @@ describe("delivery event shape", () => {
156
146
  // + tenantSecretsTable: projection-row PK IS the event aggregateId, so
157
147
  // a replay of the same event conflicts on the PK rather than
158
148
  // duplicating the log row.
159
- const [row] = await db
160
- .select()
161
- .from(deliveryAttemptsTable)
162
- .where(eq(deliveryAttemptsTable.id, event.aggregateId));
149
+ const [row] = await selectMany(db, deliveryAttemptsTable, { id: event.aggregateId });
163
150
  expect(row).toBeDefined();
164
151
  expect(row?.notificationType).toBe("example:notify:pk-link");
165
152
  });