@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,8 +1,11 @@
1
1
  import {
2
2
  boolean,
3
3
  buildBaseColumns,
4
+ defineUnmanagedTable,
5
+ type EntityTableMeta,
4
6
  instant,
5
7
  table as pgTable,
8
+ sql,
6
9
  text,
7
10
  uniqueIndex,
8
11
  uuid,
@@ -12,7 +15,6 @@ import {
12
15
  createEntity,
13
16
  createTextField,
14
17
  } from "@cosmicdrift/kumiko-framework/engine";
15
- import { sql } from "drizzle-orm";
16
18
 
17
19
  // Delivery-log is an append-only stream of per-attempt records. The stream
18
20
  // of truth lives in the events-Tabelle (one aggregate per attempt, event
@@ -39,6 +41,29 @@ export const deliveryAttemptsTable = pgTable("read_delivery_attempts", {
39
41
  createdAt: instant("created_at").default(sql`now()`).notNull(),
40
42
  });
41
43
 
44
+ // **Unmanaged table** — bewusst KEIN createEntity. Begründung:
45
+ // - id kommt aus dem Aggregate-Stream (kein gen_random_uuid()-DEFAULT)
46
+ // - kein version/inserted_by/modified_by/modified_at — keine in-place-
47
+ // Edits, keine Audit-Spalten nötig (idempotent-on-replay via PK-Konflikt)
48
+ // - created_at statt inserted_at — historischer Naming-Drift, kein Bug
49
+ // App trägt Verantwortung für tenant-scoping in Queries + replay-idempotency.
50
+ // pgTable bleibt source-of-truth für Query-API; Phase 4 leitet das pgTable
51
+ // aus dieser Meta ab.
52
+ export const deliveryAttemptsTableMeta: EntityTableMeta = defineUnmanagedTable({
53
+ tableName: "read_delivery_attempts",
54
+ columns: [
55
+ { name: "id", pgType: "uuid", notNull: true, primaryKey: true },
56
+ { name: "tenant_id", pgType: "uuid", notNull: true },
57
+ { name: "notification_type", pgType: "text", notNull: true },
58
+ { name: "channel", pgType: "text", notNull: true },
59
+ { name: "recipient_id", pgType: "text", notNull: false },
60
+ { name: "recipient_address", pgType: "text", notNull: false },
61
+ { name: "status", pgType: "text", notNull: true },
62
+ { name: "error", pgType: "text", notNull: false },
63
+ { name: "created_at", pgType: "timestamptz", notNull: true, defaultSql: "now()" },
64
+ ],
65
+ });
66
+
42
67
  // User-scoped opt-in/opt-out for (notificationType, channel) pairs. Post-ES
43
68
  // refactor: each row is a notificationPreference aggregate with
44
69
  // `.created / .updated / .deleted` lifecycle events written via the
@@ -10,13 +10,9 @@
10
10
  // through to update. Worst case: one extra roundtrip for the loser of
11
11
  // the race. Happy path: same number of queries as the pre-ES upsert.
12
12
 
13
- import {
14
- createEventStoreExecutor,
15
- fetchOne,
16
- type TenantDb,
17
- } from "@cosmicdrift/kumiko-framework/db";
13
+ import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
14
+ import { createEventStoreExecutor, type TenantDb } from "@cosmicdrift/kumiko-framework/db";
18
15
  import type { SessionUser, TenantId, WriteResult } from "@cosmicdrift/kumiko-framework/engine";
19
- import { eq } from "drizzle-orm";
20
16
  import { notificationPreferenceEntity, notificationPreferencesTable } from "./tables";
21
17
 
22
18
  const executor = createEventStoreExecutor(
@@ -38,14 +34,12 @@ async function lookup(
38
34
  notificationType: string,
39
35
  channel: string,
40
36
  ): Promise<PreferenceLookupRow | undefined> {
41
- return fetchOne<PreferenceLookupRow>(
42
- db,
43
- notificationPreferencesTable,
44
- eq(notificationPreferencesTable.tenantId, tenantId),
45
- eq(notificationPreferencesTable.userId, userId),
46
- eq(notificationPreferencesTable.notificationType, notificationType),
47
- eq(notificationPreferencesTable.channel, channel),
48
- );
37
+ return fetchOne<PreferenceLookupRow>(db, notificationPreferencesTable, {
38
+ tenantId,
39
+ userId,
40
+ notificationType,
41
+ channel,
42
+ });
49
43
  }
50
44
 
51
45
  export type UpsertPreferenceInput = {
@@ -1,5 +1,7 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import { asRawClient, insertOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
1
3
  import {
2
- buildDrizzleTable,
4
+ buildEntityTable,
3
5
  createEventStoreExecutor,
4
6
  entityEventName,
5
7
  integer,
@@ -24,9 +26,7 @@ import {
24
26
  } from "@cosmicdrift/kumiko-framework/stack";
25
27
  import { createLateBoundHolder } from "@cosmicdrift/kumiko-framework/testing";
26
28
  import { generateId } from "@cosmicdrift/kumiko-framework/utils";
27
- import { sql } from "drizzle-orm";
28
29
  import { Temporal } from "temporal-polyfill";
29
- import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
30
30
  import { z } from "zod";
31
31
  import { FEATURE_TOGGLE_SET_EVENT_NAME } from "../constants";
32
32
  import { createFeatureTogglesFeature } from "../feature";
@@ -44,7 +44,7 @@ const widgetEntity = createEntity({
44
44
  active: createBooleanField({ default: true }),
45
45
  },
46
46
  });
47
- const widgetTable = buildDrizzleTable("widget", widgetEntity);
47
+ const widgetTable = buildEntityTable("widget", widgetEntity);
48
48
 
49
49
  const widgetCrud = createEventStoreExecutor(widgetTable, widgetEntity, {
50
50
  entityName: "widget",
@@ -79,7 +79,7 @@ const widgetAuditEntity = createEntity({
79
79
  widgetName: createTextField({ required: true, maxLength: 100 }),
80
80
  },
81
81
  });
82
- const widgetAuditTable = buildDrizzleTable("widget-audit", widgetAuditEntity);
82
+ const widgetAuditTable = buildEntityTable("widget-audit", widgetAuditEntity);
83
83
 
84
84
  function widgetAuditFeature(): FeatureDefinition {
85
85
  return defineFeature("widget-audit", (r) => {
@@ -92,7 +92,7 @@ function widgetAuditFeature(): FeatureDefinition {
92
92
  if (!ctx.db) return;
93
93
  const name = result.changes!["name"] as string | undefined;
94
94
  if (!name) return;
95
- await ctx.db.insert(widgetAuditTable).values({
95
+ await insertOne(ctx.db, widgetAuditTable, {
96
96
  id: generateId(),
97
97
  widgetName: name,
98
98
  version: 1,
@@ -128,13 +128,10 @@ function widgetTrackerFeature(): FeatureDefinition {
128
128
  table: widgetTrackerTable,
129
129
  apply: {
130
130
  [entityEventName("widget", "created")]: async (event, tx) => {
131
- await tx
132
- .insert(widgetTrackerTable)
133
- .values({ tenantId: event.tenantId, count: 1 })
134
- .onConflictDoUpdate({
135
- target: widgetTrackerTable.tenantId,
136
- set: { count: sql`${widgetTrackerTable.count} + 1` },
137
- });
131
+ await asRawClient(tx).unsafe(
132
+ `INSERT INTO "widget_tracker" (tenant_id, count) VALUES ($1::uuid, 1) ON CONFLICT (tenant_id) DO UPDATE SET count = widget_tracker.count + 1`,
133
+ [event.tenantId],
134
+ );
138
135
  },
139
136
  },
140
137
  });
@@ -183,15 +180,17 @@ afterAll(async () => {
183
180
  });
184
181
 
185
182
  beforeEach(async () => {
186
- await stack.db.delete(widgetAuditTable);
187
- await stack.db.delete(widgetTable);
188
- await stack.db.delete(widgetTrackerTable);
189
- await stack.db.delete(globalFeatureStateTable);
183
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${widgetAuditTable.tableName}"`);
184
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${widgetTable.tableName}"`);
185
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${widgetTrackerTable.tableName}"`);
186
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${globalFeatureStateTable.tableName}"`);
190
187
  // Wipe the event log + reset every consumer cursor so each test starts
191
188
  // from event-id 0. Tests that drain via eventDispatcher.runOnce() need
192
189
  // this or they drain a shared backlog and see false-positive counters.
193
- await stack.db.execute(sql`DELETE FROM kumiko_events`);
194
- await stack.db.execute(sql`UPDATE kumiko_event_consumers SET last_processed_event_id = 0`);
190
+ await asRawClient(stack.db).unsafe(`DELETE FROM kumiko_events`);
191
+ await asRawClient(stack.db).unsafe(
192
+ `UPDATE kumiko_event_consumers SET last_processed_event_id = 0`,
193
+ );
195
194
  await runtime.refresh();
196
195
  });
197
196
 
@@ -212,17 +211,17 @@ async function createWidget(name: string) {
212
211
  }
213
212
 
214
213
  async function countWidgets(): Promise<number> {
215
- const rows = await stack.db.select().from(widgetTable);
214
+ const rows = await selectMany(stack.db, widgetTable);
216
215
  return rows.length;
217
216
  }
218
217
 
219
218
  async function countAuditRows(): Promise<number> {
220
- const rows = await stack.db.select().from(widgetAuditTable);
219
+ const rows = await selectMany(stack.db, widgetAuditTable);
221
220
  return rows.length;
222
221
  }
223
222
 
224
223
  async function trackerCount(): Promise<number> {
225
- const rows = await stack.db.select().from(widgetTrackerTable);
224
+ const rows = await selectMany(stack.db, widgetTrackerTable);
226
225
  return rows[0]?.count ?? 0;
227
226
  }
228
227
 
@@ -231,15 +230,16 @@ async function trackerCount(): Promise<number> {
231
230
  // explicit shape — typed access everywhere else.
232
231
  type ConsumerCursorRow = { last_processed_event_id: number | string };
233
232
  async function trackerCursor(): Promise<number> {
234
- const rows = (await stack.db.execute(
235
- sql`SELECT last_processed_event_id FROM kumiko_event_consumers WHERE name LIKE '%tracker%' LIMIT 1`,
233
+ const rows = (await asRawClient(stack.db).unsafe(
234
+ `SELECT last_processed_event_id FROM kumiko_event_consumers WHERE name LIKE '%tracker%' LIMIT 1`,
236
235
  )) as unknown as readonly ConsumerCursorRow[];
237
236
  return Number(rows[0]?.last_processed_event_id ?? 0);
238
237
  }
239
238
 
240
239
  async function setTrackerCursor(value: number): Promise<void> {
241
- await stack.db.execute(
242
- sql`UPDATE kumiko_event_consumers SET last_processed_event_id = ${value} WHERE name LIKE '%tracker%'`,
240
+ await asRawClient(stack.db).unsafe(
241
+ `UPDATE kumiko_event_consumers SET last_processed_event_id = $1 WHERE name LIKE '%tracker%'`,
242
+ [value],
243
243
  );
244
244
  }
245
245
 
@@ -252,7 +252,7 @@ describe("feature-toggles runtime cache", () => {
252
252
  });
253
253
 
254
254
  test("refresh() re-reads the DB snapshot", async () => {
255
- await stack.db.insert(globalFeatureStateTable).values({
255
+ await insertOne(stack.db, globalFeatureStateTable, {
256
256
  featureName: "widget",
257
257
  enabled: false,
258
258
  version: 1,
@@ -330,7 +330,7 @@ describe("runtime on/off/on — the user's scenario", () => {
330
330
  expect(body.data?.previousEnabled).toBeNull();
331
331
 
332
332
  // Row persisted.
333
- const rows = await stack.db.select().from(globalFeatureStateTable);
333
+ const rows = await selectMany(stack.db, globalFeatureStateTable);
334
334
  expect(rows).toHaveLength(1);
335
335
 
336
336
  // Snapshot updated — widget:create now 403s.
@@ -517,8 +517,8 @@ describe("feature-toggles queries + audit automation", () => {
517
517
  admin,
518
518
  );
519
519
 
520
- const events = (await stack.db.execute(
521
- sql`SELECT type, payload FROM kumiko_events WHERE type = 'feature-toggles:event:toggle-set'`,
520
+ const events = (await asRawClient(stack.db).unsafe(
521
+ `SELECT type, payload FROM kumiko_events WHERE type = 'feature-toggles:event:toggle-set'`,
522
522
  )) as unknown as readonly {
523
523
  type: string;
524
524
  payload: Record<string, unknown>;
@@ -11,6 +11,7 @@
11
11
  // durchläuft. Die Convention-Pin ist die einzige Aussage des tests —
12
12
  // echtes integration-Verhalten deckt feature-toggles.integration.ts ab.
13
13
 
14
+ import { describe, expect, test } from "bun:test";
14
15
  import {
15
16
  createEntity,
16
17
  createRegistry,
@@ -21,7 +22,6 @@ import {
21
22
  } from "@cosmicdrift/kumiko-framework/engine";
22
23
  import { createDispatcher } from "@cosmicdrift/kumiko-framework/pipeline";
23
24
  import { createTestUser } from "@cosmicdrift/kumiko-framework/stack";
24
- import { describe, expect, test } from "vitest";
25
25
  import { createFeatureTogglesFeature } from "../feature";
26
26
  import type { GlobalFeatureToggleRuntime } from "../toggle-runtime";
27
27
 
@@ -43,12 +43,13 @@ describe("Sprint 8a: registered.query SYSTEM_TENANT_ID convention", () => {
43
43
 
44
44
  const registry = createRegistry([dummy, featureToggles]);
45
45
 
46
- // Mock ctx.db.select-chain damit der handler durch den DB-Pfad
47
- // kommt. Wir liefern leere overrides (.from() returnt []), das
48
- // genügt registered.query iteriert dann über registry.features
49
- // und ruft ctx.effectiveFeatures, was unser observable ist.
46
+ // Mock ctx.db via bun-db's asRawClient surface: handler ruft
47
+ // selectMany(ctx.db.raw, ...) asRawClient(raw).unsafe(...). Wir
48
+ // liefern leere overrides damit der handler durch den DB-Pfad kommt
49
+ // und dann ctx.effectiveFeatures aufruft (das observable hier).
50
50
  const mockDb = {
51
- select: () => ({ from: async () => [] as unknown[] }),
51
+ unsafe: async () => [] as unknown[],
52
+ begin: async () => undefined,
52
53
  } as unknown as Parameters<typeof createDispatcher>[1]["db"];
53
54
 
54
55
  const callerTenant = "00000000-0000-4000-8000-0000000000c1" as TenantId;
@@ -0,0 +1,25 @@
1
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
2
+ import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
3
+ import type { Temporal } from "temporal-polyfill";
4
+
5
+ export async function updateFeatureToggleOptimistic(
6
+ db: DbRunner,
7
+ params: {
8
+ readonly enabled: boolean;
9
+ readonly updatedBy: string;
10
+ readonly updatedAt: Temporal.Instant;
11
+ readonly featureName: string;
12
+ readonly expectedVersion: number;
13
+ },
14
+ ): Promise<readonly unknown[]> {
15
+ return asRawClient(db).unsafe(
16
+ 'UPDATE "read_global_feature_state" SET enabled = $1, version = version + 1, updated_by = $2, updated_at = $3 WHERE feature_name = $4 AND version = $5 RETURNING *',
17
+ [
18
+ params.enabled,
19
+ params.updatedBy,
20
+ params.updatedAt,
21
+ params.featureName,
22
+ params.expectedVersion,
23
+ ],
24
+ );
25
+ }
@@ -24,10 +24,18 @@ export type FeatureTogglesOptions = {
24
24
  //
25
25
  // Production setup: resolve the runtime after buildServer returns, then
26
26
  // pass `() => runtime`. For tests, use createLateBoundHolder + .get().
27
- readonly getRuntime: () => GlobalFeatureToggleRuntime;
27
+ //
28
+ // **Optional** — boot-mode (KUMIKO_DRY_RUN_ENV=boot) wires the feature
29
+ // up without ever calling the set-handler, so a runtime-stub is then
30
+ // pure cargo-cult. Omit `getRuntime` and the handler throws lazily on
31
+ // first call with an actionable message. App-authors who DO route to
32
+ // the set-handler at runtime MUST supply the accessor.
33
+ readonly getRuntime?: () => GlobalFeatureToggleRuntime;
28
34
  };
29
35
 
30
- export function createFeatureTogglesFeature(options: FeatureTogglesOptions): FeatureDefinition {
36
+ export function createFeatureTogglesFeature(
37
+ options: FeatureTogglesOptions = {},
38
+ ): FeatureDefinition {
31
39
  return defineFeature("feature-toggles", (r) => {
32
40
  r.systemScope();
33
41
 
@@ -79,6 +87,12 @@ export function createFeatureTogglesFeature(options: FeatureTogglesOptions): Fea
79
87
  // than re-parsing — the payload round-trips through JSON and is
80
88
  // fixed at the source.
81
89
  const payload = event.payload as { featureName: string; enabled: boolean }; // @cast-boundary engine-payload
90
+ if (!options.getRuntime) {
91
+ throw new Error(
92
+ "[feature-toggles] toggle-cache-sync MSP fired but createFeatureTogglesFeature " +
93
+ "was wired up without `getRuntime`. Wire the accessor in your app-config.",
94
+ );
95
+ }
82
96
  options.getRuntime().apply(payload.featureName, payload.enabled);
83
97
  },
84
98
  },
@@ -3,9 +3,9 @@ import {
3
3
  instant,
4
4
  integer,
5
5
  table as pgTable,
6
+ sql,
6
7
  text,
7
8
  } from "@cosmicdrift/kumiko-framework/db";
8
- import { sql } from "drizzle-orm";
9
9
 
10
10
  // Global feature-toggle override state. One row per feature that has ever
11
11
  // been explicitly flipped by an operator. Missing row = "no override,
@@ -1,3 +1,4 @@
1
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
1
2
  import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
2
3
  import { z } from "zod";
3
4
  import { globalFeatureStateTable } from "../global-feature-state-table";
@@ -11,8 +12,14 @@ export const listQuery = defineQueryHandler({
11
12
  schema: z.object({}),
12
13
  access: { roles: ["SystemAdmin", "Admin"] },
13
14
  handler: async (_event, ctx) => {
14
- type Row = typeof globalFeatureStateTable.$inferSelect;
15
- const rows = (await ctx.db.select().from(globalFeatureStateTable)) as Row[]; // @cast-boundary db-row
15
+ type Row = {
16
+ featureName: string;
17
+ enabled: boolean;
18
+ version: number;
19
+ updatedAt: Temporal.Instant;
20
+ updatedBy: string;
21
+ };
22
+ const rows = await selectMany<Row>(ctx.db.raw, globalFeatureStateTable);
16
23
  return {
17
24
  items: rows.map((r) => ({
18
25
  featureName: r.featureName,
@@ -1,3 +1,4 @@
1
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
1
2
  import { defineQueryHandler, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
2
3
  import { z } from "zod";
3
4
  import { globalFeatureStateTable } from "../global-feature-state-table";
@@ -15,13 +16,8 @@ export const registeredQuery = defineQueryHandler({
15
16
  schema: z.object({}),
16
17
  access: { roles: ["SystemAdmin", "Admin"] },
17
18
  handler: async (_event, ctx) => {
18
- type OverrideRow = Pick<typeof globalFeatureStateTable.$inferSelect, "featureName" | "enabled">;
19
- const overrideRows = (await ctx.db
20
- .select({
21
- featureName: globalFeatureStateTable.featureName,
22
- enabled: globalFeatureStateTable.enabled,
23
- })
24
- .from(globalFeatureStateTable)) as OverrideRow[]; // @cast-boundary db-row
19
+ type OverrideRow = { featureName: string; enabled: boolean };
20
+ const overrideRows = await selectMany<OverrideRow>(ctx.db.raw, globalFeatureStateTable);
25
21
  const overrides = new Map(overrideRows.map((r) => [r.featureName, r.enabled]));
26
22
 
27
23
  // SystemAdmin operator-tooling: das listing soll die PLATTFORM-truth
@@ -1,11 +1,10 @@
1
+ import { insertOne, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
1
2
  import { defineWriteHandler, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
2
-
3
3
  import {
4
4
  UnprocessableError,
5
5
  VersionConflictError,
6
6
  writeFailure,
7
7
  } from "@cosmicdrift/kumiko-framework/errors";
8
- import { and, eq, sql } from "drizzle-orm";
9
8
  import { Temporal } from "temporal-polyfill";
10
9
  import { z } from "zod";
11
10
  import {
@@ -13,6 +12,7 @@ import {
13
12
  FEATURE_TOGGLE_SET_EVENT_NAME,
14
13
  FeatureToggleErrors,
15
14
  } from "../constants";
15
+ import { updateFeatureToggleOptimistic } from "../db/queries/toggle-state";
16
16
  import { globalFeatureStateTable } from "../global-feature-state-table";
17
17
  import type { GlobalFeatureToggleRuntime } from "../toggle-runtime";
18
18
 
@@ -25,7 +25,13 @@ import type { GlobalFeatureToggleRuntime } from "../toggle-runtime";
25
25
  // flow: tests + setupTestStack construct the feature definition BEFORE the
26
26
  // runtime exists (the runtime needs the registry, which setupTestStack
27
27
  // builds from the features). The accessor is resolved lazily, at call time.
28
- export function createSetWriteHandler(getRuntime: () => GlobalFeatureToggleRuntime) {
28
+ //
29
+ // `undefined` accessor is legitimate at registration-time for boot-mode
30
+ // smoke-apps (`KUMIKO_DRY_RUN_ENV=boot`) that never dispatch a set-call.
31
+ // We throw lazily on first call with an actionable message — `as
32
+ // GlobalFeatureToggleRuntime`-casts at the registration site are no
33
+ // longer needed.
34
+ export function createSetWriteHandler(getRuntime: (() => GlobalFeatureToggleRuntime) | undefined) {
29
35
  return defineWriteHandler({
30
36
  name: "set",
31
37
  schema: z.object({
@@ -66,18 +72,25 @@ export function createSetWriteHandler(getRuntime: () => GlobalFeatureToggleRunti
66
72
  // Read current state for event payload + optimistic-lock version.
67
73
  // `$inferSelect` narrows the result shape to the real table schema —
68
74
  // no hand-rolled cast, no drift if a column is added later.
69
- type StateRow = typeof globalFeatureStateTable.$inferSelect;
70
- const [existing] = (await ctx.db
71
- .select()
72
- .from(globalFeatureStateTable)
73
- .where(eq(globalFeatureStateTable.featureName, featureName))
74
- .limit(1)) as StateRow[]; // @cast-boundary db-row
75
+ type StateRow = {
76
+ featureName: string;
77
+ enabled: boolean;
78
+ version: number;
79
+ updatedAt: Temporal.Instant;
80
+ updatedBy: string;
81
+ };
82
+ const [existing] = await selectMany<StateRow>(
83
+ ctx.db,
84
+ globalFeatureStateTable,
85
+ { featureName },
86
+ { limit: 1 },
87
+ );
75
88
 
76
89
  const previousEnabled = existing?.enabled ?? null;
77
90
 
78
91
  if (!existing) {
79
92
  // First-time override: insert.
80
- await ctx.db.insert(globalFeatureStateTable).values({
93
+ await insertOne(ctx.db, globalFeatureStateTable, {
81
94
  featureName,
82
95
  enabled,
83
96
  version: 1,
@@ -88,21 +101,13 @@ export function createSetWriteHandler(getRuntime: () => GlobalFeatureToggleRunti
88
101
  // Upsert with optimistic lock. Two operators flipping the same
89
102
  // toggle simultaneously is rare but possible — the version-WHERE
90
103
  // ensures only one wins; the loser sees VersionConflictError.
91
- const updated = await ctx.db
92
- .update(globalFeatureStateTable)
93
- .set({
94
- enabled,
95
- version: sql<number>`${globalFeatureStateTable.version} + 1`,
96
- updatedBy: event.user.id,
97
- updatedAt: Temporal.Now.instant(),
98
- })
99
- .where(
100
- and(
101
- eq(globalFeatureStateTable.featureName, featureName),
102
- eq(globalFeatureStateTable.version, existing.version),
103
- ),
104
- )
105
- .returning();
104
+ const updated = await updateFeatureToggleOptimistic(ctx.db, {
105
+ enabled,
106
+ updatedBy: event.user.id,
107
+ updatedAt: Temporal.Now.instant(),
108
+ featureName,
109
+ expectedVersion: existing.version,
110
+ });
106
111
 
107
112
  if (updated.length === 0) {
108
113
  return writeFailure(
@@ -147,6 +152,13 @@ export function createSetWriteHandler(getRuntime: () => GlobalFeatureToggleRunti
147
152
  // the `toggle-cache-sync` MSP (see feature-toggles-feature.ts). Both
148
153
  // paths are idempotent — Map.set is last-write-wins and the DB is
149
154
  // the source of truth after boot-time initialize().
155
+ if (!getRuntime) {
156
+ throw new Error(
157
+ "[feature-toggles] set-handler called but createFeatureTogglesFeature " +
158
+ "was wired up without `getRuntime`. Wire the accessor in your app-config " +
159
+ "(production: `() => runtime` after buildServer; tests: createLateBoundHolder.get).",
160
+ );
161
+ }
150
162
  getRuntime().apply(featureName, enabled);
151
163
 
152
164
  return {
@@ -1,3 +1,4 @@
1
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
1
2
  import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
2
3
  import {
3
4
  computeEffectiveFeatures,
@@ -25,12 +26,8 @@ export class GlobalFeatureToggleRuntime {
25
26
  ) {}
26
27
 
27
28
  async initialize(): Promise<void> {
28
- const rows = await this.db
29
- .select({
30
- featureName: globalFeatureStateTable.featureName,
31
- enabled: globalFeatureStateTable.enabled,
32
- })
33
- .from(globalFeatureStateTable);
29
+ type Row = { featureName: string; enabled: boolean };
30
+ const rows = await selectMany<Row>(this.db, globalFeatureStateTable);
34
31
  this.snapshot = new Map(rows.map((r) => [r.featureName, r.enabled]));
35
32
  }
36
33
 
@@ -2,7 +2,7 @@
2
2
  // Plugin-API-shaped file-foundation. Provider-specific configs/secrets
3
3
  // are tested in their own provider-feature (file-provider-s3/__tests__).
4
4
 
5
- import { describe, expect, test } from "vitest";
5
+ import { describe, expect, test } from "bun:test";
6
6
  import { fileFoundationFeature } from "../feature";
7
7
 
8
8
  describe("fileFoundationFeature — shape", () => {
@@ -2,6 +2,7 @@
2
2
  // provider-factory through the dispatcher so the real config-resolver
3
3
  // + secrets-context + tenant-scoped reads are exercised.
4
4
 
5
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
5
6
  import { randomBytes } from "node:crypto";
6
7
  import { createEncryptionProvider, type DbConnection } from "@cosmicdrift/kumiko-framework/db";
7
8
  import { defineFeature, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
@@ -19,7 +20,6 @@ import {
19
20
  createMutableMasterKeyProvider,
20
21
  type MutableMasterKeyProvider,
21
22
  } from "@cosmicdrift/kumiko-framework/testing";
22
- import { afterAll, beforeAll, describe, expect, test } from "vitest";
23
23
  import { z } from "zod";
24
24
  import { createConfigFeature } from "../../config";
25
25
  import { ConfigHandlers } from "../../config/constants";
@@ -1,6 +1,6 @@
1
1
  // feature.ts contract tests for file-provider-inmemory.
2
2
 
3
- import { describe, expect, test } from "vitest";
3
+ import { describe, expect, test } from "bun:test";
4
4
  import { clearStorage, fileProviderInMemoryFeature, listKeys } from "../feature";
5
5
 
6
6
  describe("fileProviderInMemoryFeature — shape", () => {
@@ -1,6 +1,6 @@
1
1
  // feature.ts contract tests for file-provider-s3.
2
2
 
3
- import { describe, expect, test } from "vitest";
3
+ import { describe, expect, test } from "bun:test";
4
4
  import { fileProviderS3Feature, S3_SECRET_ACCESS_KEY } from "../feature";
5
5
 
6
6
  describe("fileProviderS3Feature — shape", () => {
@@ -12,8 +12,14 @@
12
12
  import { defineFeature, EXT_USER_DATA } from "@cosmicdrift/kumiko-framework/engine";
13
13
  import { FILE_UPLOADED_EVENT_TYPE, fileRefsTable } from "@cosmicdrift/kumiko-framework/files";
14
14
  import { setupTestStack, type TestStack } from "@cosmicdrift/kumiko-framework/stack";
15
- import { getTableColumns } from "drizzle-orm";
16
- import { afterAll, beforeAll, describe, expect, test } from "vitest";
15
+
16
+ // Native dialect exposes column metadata on the `columns` array (EntityTableMeta)
17
+ // and on the Symbol.for("kumiko:schema:Columns") map (compat shape). Tests use the
18
+ // Symbol map because keys are JS field-names (camelCase), matching what
19
+ // feature-entity definitions declare.
20
+ const KUMIKO_COLUMNS_SYMBOL = Symbol.for("kumiko:schema:Columns");
21
+
22
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
17
23
  import { createFilesFeature, fileRefEntity } from "../feature";
18
24
 
19
25
  let stack: TestStack;
@@ -101,11 +107,16 @@ describe("files :: cross-feature behavior (F1, S1.7)", () => {
101
107
  });
102
108
 
103
109
  describe("files :: DDL-Konsistenz (M3, S1.7)", () => {
104
- // Drizzle's getTableColumns liefert die typed column-map ohne den
105
- // Symbol-Properties-Junk. Sauberer als Object.keys(table) das auch
106
- // interne Drizzle-Symbols mitnimmt.
110
+ // The native dialect's SchemaTable exposes its column map via the
111
+ // Symbol.for("kumiko:schema:Columns") metadata (kept for back-compat with
112
+ // anything that previously introspected pgTable that way). Keys are
113
+ // JS field-names — exactly the level feature-entity declarations live at.
107
114
  function pgColumnNames(): Set<string> {
108
- return new Set(Object.keys(getTableColumns(fileRefsTable)));
115
+ const cols = (fileRefsTable as unknown as Record<symbol, unknown>)[KUMIKO_COLUMNS_SYMBOL];
116
+ if (typeof cols !== "object" || cols === null) {
117
+ throw new Error("files.integration: fileRefsTable has no kumiko:schema:Columns symbol");
118
+ }
119
+ return new Set(Object.keys(cols as Record<string, unknown>));
109
120
  }
110
121
 
111
122
  test("Feature-Entity-Felder matchen die Framework-pgTable column-set", () => {
@@ -117,7 +128,7 @@ describe("files :: DDL-Konsistenz (M3, S1.7)", () => {
117
128
  // Vergleich: alle Feature-Felder muessen als Spalten in der pgTable
118
129
  // existieren (umgekehrt darf pgTable framework-managed Spalten haben
119
130
  // wie tenantId/createdAt/updatedAt/deletedAt — die deklariert das
120
- // Framework automatisch beim buildDrizzleTable-Mapping).
131
+ // Framework automatisch beim buildEntityTable-Mapping).
121
132
  const pgColumns = pgColumnNames();
122
133
  const featureFields = Object.keys(fileRefEntity.fields);
123
134
 
@@ -25,7 +25,7 @@ import {
25
25
  // in Sprint 5 — Tenant-Lifecycle löscht alle FileRefs.
26
26
  // 3. Boot-Validation für PII-Annotations greift (fileName, originalName).
27
27
  //
28
- // Kein buildDrizzleTable hier — die Mapping-Tabelle existiert schon im
28
+ // Kein buildEntityTable hier — die Mapping-Tabelle existiert schon im
29
29
  // Framework. Drizzle-Reads in den Sprint-2+-Hooks gehen direkt über
30
30
  // `fileRefsTable` aus `@cosmicdrift/kumiko-framework/files`.
31
31
  //
@@ -1,4 +1,4 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "vitest";
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
2
  import { parseS3EnvConfig } from "../env-helper";
3
3
 
4
4
  // Tests run against real process.env — we snapshot + restore per-test so
@@ -1,6 +1,6 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
1
2
  import type { FileStorageProvider } from "@cosmicdrift/kumiko-framework/files";
2
3
  import { generateId } from "@cosmicdrift/kumiko-framework/utils";
3
- import { afterAll, beforeAll, describe, expect, test } from "vitest";
4
4
  import { createS3ProviderFromEnv } from "../env-helper";
5
5
  import { createS3Provider } from "../s3-provider";
6
6