@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,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
  });