@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
@@ -10,7 +10,9 @@
10
10
  // Pattern follows cap-counter.integration.ts: probe-feature mit own entity,
11
11
  // wired via wireCustomFieldsFor.
12
12
 
13
- import { buildDrizzleTable } from "@cosmicdrift/kumiko-framework/db";
13
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
14
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
15
+ import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
14
16
  import {
15
17
  createEntity,
16
18
  createEntityExecutor,
@@ -25,8 +27,6 @@ import {
25
27
  type TestStack,
26
28
  unsafeCreateEntityTable,
27
29
  } from "@cosmicdrift/kumiko-framework/stack";
28
- import { sql } from "drizzle-orm";
29
- import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
30
30
  import { z } from "zod";
31
31
  import { fieldDefinitionEntity } from "../entity";
32
32
  import { createCustomFieldsFeature } from "../feature";
@@ -41,7 +41,7 @@ const propertyEntity = createEntity({
41
41
  customFields: customFieldsField(),
42
42
  },
43
43
  });
44
- const propertyTable = buildDrizzleTable("property", propertyEntity);
44
+ const propertyTable = buildEntityTable("property", propertyEntity);
45
45
 
46
46
  const propertyFeature = defineFeature("property-test", (r) => {
47
47
  r.entity("property", propertyEntity);
@@ -91,9 +91,9 @@ afterAll(async () => {
91
91
 
92
92
  beforeEach(async () => {
93
93
  // Clean slate per test — event-log + entity-rows.
94
- await stack.db.execute(sql`DELETE FROM kumiko_events`);
95
- await stack.db.execute(sql`DELETE FROM read_t1_properties`);
96
- await stack.db.execute(sql`DELETE FROM read_custom_field_definitions`);
94
+ await asRawClient(stack.db).unsafe(`DELETE FROM kumiko_events`);
95
+ await asRawClient(stack.db).unsafe(`DELETE FROM read_t1_properties`);
96
+ await asRawClient(stack.db).unsafe(`DELETE FROM read_custom_field_definitions`);
97
97
  });
98
98
 
99
99
  // --- Helpers ---
@@ -1,4 +1,4 @@
1
- import { describe, expect, test } from "vitest";
1
+ import { describe, expect, test } from "bun:test";
2
2
  import { fieldDefinitionAggregateId } from "../aggregate-id";
3
3
  import { SUPPORTED_FIELD_TYPES } from "../constants";
4
4
  import { createCustomFieldsFeature } from "../feature";
@@ -9,7 +9,9 @@
9
9
  // exactly as in B2 (no extra gate) — the existing roundtrip-test suite
10
10
  // stays green, and we add an explicit covers-the-no-op-path test here too.
11
11
 
12
- import { buildDrizzleTable } from "@cosmicdrift/kumiko-framework/db";
12
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
13
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
14
+ import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
13
15
  import {
14
16
  createEntity,
15
17
  createEntityExecutor,
@@ -24,8 +26,6 @@ import {
24
26
  type TestStack,
25
27
  unsafeCreateEntityTable,
26
28
  } from "@cosmicdrift/kumiko-framework/stack";
27
- import { sql } from "drizzle-orm";
28
- import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
29
29
  import { z } from "zod";
30
30
  import { fieldDefinitionEntity } from "../entity";
31
31
  import { createCustomFieldsFeature } from "../feature";
@@ -38,7 +38,7 @@ const propertyEntity = createEntity({
38
38
  customFields: customFieldsField(),
39
39
  },
40
40
  });
41
- const propertyTable = buildDrizzleTable("property", propertyEntity);
41
+ const propertyTable = buildEntityTable("property", propertyEntity);
42
42
 
43
43
  const propertyFeature = defineFeature("property-t15b", (r) => {
44
44
  r.entity("property", propertyEntity);
@@ -84,8 +84,8 @@ afterAll(async () => {
84
84
 
85
85
  beforeEach(async () => {
86
86
  await resetEventStore(stack);
87
- await stack.db.execute(sql`DELETE FROM read_t15b_properties`);
88
- await stack.db.execute(sql`DELETE FROM read_custom_field_definitions`);
87
+ await asRawClient(stack.db).unsafe(`DELETE FROM read_t15b_properties`);
88
+ await asRawClient(stack.db).unsafe(`DELETE FROM read_custom_field_definitions`);
89
89
  });
90
90
 
91
91
  async function defineField(fieldKey: string, serializedField: Record<string, unknown>) {
@@ -5,7 +5,9 @@
5
5
  // rejects with `unprocessable` + reason `cap_exceeded` when the
6
6
  // tenant already has >= N definitions.
7
7
 
8
- import { buildDrizzleTable } from "@cosmicdrift/kumiko-framework/db";
8
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
9
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
10
+ import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
9
11
  import {
10
12
  createEntity,
11
13
  createEntityExecutor,
@@ -20,8 +22,6 @@ import {
20
22
  type TestStack,
21
23
  unsafeCreateEntityTable,
22
24
  } from "@cosmicdrift/kumiko-framework/stack";
23
- import { sql } from "drizzle-orm";
24
- import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
25
25
  import { z } from "zod";
26
26
  import { fieldDefinitionEntity } from "../entity";
27
27
  import { createCustomFieldsFeature } from "../feature";
@@ -34,7 +34,7 @@ const propertyEntity = createEntity({
34
34
  customFields: customFieldsField(),
35
35
  },
36
36
  });
37
- const propertyTable = buildDrizzleTable("property", propertyEntity);
37
+ const propertyTable = buildEntityTable("property", propertyEntity);
38
38
 
39
39
  const propertyFeature = defineFeature("property-t15e", (r) => {
40
40
  r.entity("property", propertyEntity);
@@ -74,8 +74,8 @@ afterAll(async () => {
74
74
 
75
75
  beforeEach(async () => {
76
76
  await resetEventStore(stack);
77
- await stack.db.execute(sql`DELETE FROM read_t15e_properties`);
78
- await stack.db.execute(sql`DELETE FROM read_custom_field_definitions`);
77
+ await asRawClient(stack.db).unsafe(`DELETE FROM read_t15e_properties`);
78
+ await asRawClient(stack.db).unsafe(`DELETE FROM read_custom_field_definitions`);
79
79
  });
80
80
 
81
81
  async function defineField(fieldKey: string) {
@@ -9,7 +9,9 @@
9
9
  // The reference timestamp is the host row's `modified_at`, not a per-key
10
10
  // timestamp — see run-retention.ts header for the rationale.
11
11
 
12
- import { buildDrizzleTable } from "@cosmicdrift/kumiko-framework/db";
12
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
13
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
14
+ import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
13
15
  import {
14
16
  createEntity,
15
17
  createEntityExecutor,
@@ -25,8 +27,6 @@ import {
25
27
  unsafeCreateEntityTable,
26
28
  } from "@cosmicdrift/kumiko-framework/stack";
27
29
  import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
28
- import { sql } from "drizzle-orm";
29
- import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
30
30
  import { z } from "zod";
31
31
  import { fieldDefinitionEntity } from "../entity";
32
32
  import { createCustomFieldsFeature } from "../feature";
@@ -40,7 +40,7 @@ const propertyEntity = createEntity({
40
40
  customFields: customFieldsField(),
41
41
  },
42
42
  });
43
- const propertyTable = buildDrizzleTable("property", propertyEntity);
43
+ const propertyTable = buildEntityTable("property", propertyEntity);
44
44
 
45
45
  const propertyFeature = defineFeature("property-t15d", (r) => {
46
46
  r.entity("property", propertyEntity);
@@ -81,8 +81,8 @@ afterAll(async () => {
81
81
 
82
82
  beforeEach(async () => {
83
83
  await resetEventStore(stack);
84
- await stack.db.execute(sql`DELETE FROM read_t15d_properties`);
85
- await stack.db.execute(sql`DELETE FROM read_custom_field_definitions`);
84
+ await asRawClient(stack.db).unsafe(`DELETE FROM read_t15d_properties`);
85
+ await asRawClient(stack.db).unsafe(`DELETE FROM read_custom_field_definitions`);
86
86
  });
87
87
 
88
88
  async function defineField(fieldKey: string, serializedField: Record<string, unknown>) {
@@ -116,14 +116,16 @@ async function setField(entityId: string, fieldKey: string, value: unknown) {
116
116
  // older than the retention cutoff. Faster than waiting `keepFor` real
117
117
  // time and the cleanest way to drive the cron under test.
118
118
  async function backdateRow(id: string, isoOlderThan: string) {
119
- await stack.db.execute(
120
- sql`UPDATE read_t15d_properties SET modified_at = ${isoOlderThan}::timestamptz WHERE id = ${id}`,
119
+ await asRawClient(stack.db).unsafe(
120
+ `UPDATE read_t15d_properties SET modified_at = $1::timestamptz WHERE id = $2`,
121
+ [isoOlderThan, id],
121
122
  );
122
123
  }
123
124
 
124
125
  async function readRow(id: string): Promise<Record<string, unknown> | undefined> {
125
- const rows = await stack.db.execute(
126
- sql`SELECT id, custom_fields FROM read_t15d_properties WHERE id = ${id}`,
126
+ const rows = await asRawClient(stack.db).unsafe(
127
+ `SELECT id, custom_fields FROM read_t15d_properties WHERE id = $1`,
128
+ [id],
127
129
  );
128
130
  return (rows as ReadonlyArray<Record<string, unknown>>)[0];
129
131
  }
@@ -14,7 +14,9 @@
14
14
  // * Forget strategy=delete: no-op — the host entity's own user-data-
15
15
  // rights hook handles the row delete, jsonb travels with the row.
16
16
 
17
- import { buildDrizzleTable } from "@cosmicdrift/kumiko-framework/db";
17
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
18
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
19
+ import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
18
20
  import {
19
21
  createEntity,
20
22
  createEntityExecutor,
@@ -32,8 +34,6 @@ import {
32
34
  type TestStack,
33
35
  unsafeCreateEntityTable,
34
36
  } from "@cosmicdrift/kumiko-framework/stack";
35
- import { sql } from "drizzle-orm";
36
- import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
37
37
  import { z } from "zod";
38
38
  import { createComplianceProfilesFeature } from "../../compliance-profiles";
39
39
  import { createDataRetentionFeature } from "../../data-retention";
@@ -52,17 +52,20 @@ const propertyEntity = createEntity({
52
52
  customFields: customFieldsField(),
53
53
  },
54
54
  });
55
- const propertyTable = buildDrizzleTable("property", propertyEntity);
55
+ const propertyTable = buildEntityTable("property", propertyEntity);
56
56
 
57
57
  // Host entity gets its own EXT_USER_DATA-registration too — that's the
58
58
  // canonical setup (host bundle handles row-anonymize/delete, custom-fields
59
59
  // adds its strip-sensitive-jsonb layer on top). Both hooks fire in the
60
60
  // same cleanup-run.
61
61
  const hostExportHook: UserDataExportHook = async (ctx) => {
62
- const rows = await ctx.db.execute(sql`
62
+ const rows = await asRawClient(ctx.db).unsafe(
63
+ `
63
64
  SELECT id, name FROM read_t15c_properties
64
- WHERE inserted_by_id = ${ctx.userId} AND tenant_id = ${ctx.tenantId}
65
- `);
65
+ WHERE inserted_by_id = $1 AND tenant_id = $2
66
+ `,
67
+ [ctx.userId, ctx.tenantId],
68
+ );
66
69
  const list = rows as ReadonlyArray<Record<string, unknown>>;
67
70
  if (list.length === 0) return null;
68
71
  return {
@@ -73,16 +76,22 @@ const hostExportHook: UserDataExportHook = async (ctx) => {
73
76
 
74
77
  const hostDeleteHook: UserDataDeleteHook = async (ctx, strategy) => {
75
78
  if (strategy === "delete") {
76
- await ctx.db.execute(sql`
79
+ await asRawClient(ctx.db).unsafe(
80
+ `
77
81
  DELETE FROM read_t15c_properties
78
- WHERE inserted_by_id = ${ctx.userId} AND tenant_id = ${ctx.tenantId}
79
- `);
82
+ WHERE inserted_by_id = $1 AND tenant_id = $2
83
+ `,
84
+ [ctx.userId, ctx.tenantId],
85
+ );
80
86
  } else {
81
87
  // anonymize: clear owner, keep row + non-sensitive customFields
82
- await ctx.db.execute(sql`
88
+ await asRawClient(ctx.db).unsafe(
89
+ `
83
90
  UPDATE read_t15c_properties SET inserted_by_id = NULL
84
- WHERE inserted_by_id = ${ctx.userId} AND tenant_id = ${ctx.tenantId}
85
- `);
91
+ WHERE inserted_by_id = $1 AND tenant_id = $2
92
+ `,
93
+ [ctx.userId, ctx.tenantId],
94
+ );
86
95
  }
87
96
  };
88
97
 
@@ -143,8 +152,8 @@ afterAll(async () => {
143
152
 
144
153
  beforeEach(async () => {
145
154
  await resetEventStore(stack);
146
- await stack.db.execute(sql`DELETE FROM read_t15c_properties`);
147
- await stack.db.execute(sql`DELETE FROM read_custom_field_definitions`);
155
+ await asRawClient(stack.db).unsafe(`DELETE FROM read_t15c_properties`);
156
+ await asRawClient(stack.db).unsafe(`DELETE FROM read_custom_field_definitions`);
148
157
  });
149
158
 
150
159
  async function defineField(fieldKey: string, serializedField: Record<string, unknown>) {
@@ -175,8 +184,9 @@ async function setField(entityId: string, fieldKey: string, value: unknown) {
175
184
  }
176
185
 
177
186
  async function readRow(id: string): Promise<Record<string, unknown> | undefined> {
178
- const rows = await stack.db.execute(
179
- sql`SELECT id, custom_fields FROM read_t15c_properties WHERE id = ${id}`,
187
+ const rows = await asRawClient(stack.db).unsafe(
188
+ `SELECT id, custom_fields FROM read_t15c_properties WHERE id = $1`,
189
+ [id],
180
190
  );
181
191
  const list = rows as ReadonlyArray<Record<string, unknown>>;
182
192
  return list[0];
@@ -1,6 +1,6 @@
1
- import { buildDrizzleTable } from "@cosmicdrift/kumiko-framework/db";
1
+ import { describe, expect, test } from "bun:test";
2
+ import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
2
3
  import { createEntity, createTextField, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
3
- import { describe, expect, test } from "vitest";
4
4
  import { customFieldsField, wireCustomFieldsFor } from "../wire-for-entity";
5
5
 
6
6
  // B2 wireCustomFieldsFor: einziger Aufruf registriert MSP + postQuery-hook +
@@ -15,7 +15,7 @@ const propertyEntity = createEntity({
15
15
  },
16
16
  });
17
17
 
18
- const propertyTable = buildDrizzleTable("property", propertyEntity);
18
+ const propertyTable = buildEntityTable("property", propertyEntity);
19
19
 
20
20
  describe("wireCustomFieldsFor", () => {
21
21
  test("registers useExtension + MSP + postQuery-entity-hook + search-payload-extension", () => {
@@ -43,7 +43,7 @@ describe("wireCustomFieldsFor", () => {
43
43
  expect(feature.entityHooks.postQuery["property"]).toHaveLength(1);
44
44
 
45
45
  // 4. search-payload-extension on "property"
46
- expect(feature.searchPayloadExtensions["property"]).toHaveLength(1);
46
+ expect(feature.searchPayloadExtensions!["property"]).toHaveLength(1);
47
47
  });
48
48
 
49
49
  test("postQuery-hook flattens row.customFields onto root", async () => {
@@ -107,7 +107,7 @@ describe("wireCustomFieldsFor", () => {
107
107
  wireCustomFieldsFor(r, "property", propertyTable);
108
108
  });
109
109
 
110
- const contributor = feature.searchPayloadExtensions["property"]?.[0]?.fn;
110
+ const contributor = feature.searchPayloadExtensions!["property"]?.[0]?.fn;
111
111
  expect(contributor).toBeDefined();
112
112
  const result = await contributor?.({
113
113
  entityName: "property",
@@ -0,0 +1,16 @@
1
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
2
+ import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
3
+
4
+ export async function selectSerializedFieldDefinition(
5
+ db: TenantDb,
6
+ tenantId: string,
7
+ entityName: string,
8
+ fieldKey: string,
9
+ ): Promise<unknown | null> {
10
+ const rows = await asRawClient(db.raw).unsafe(
11
+ "SELECT serialized_field FROM read_custom_field_definitions WHERE entity_name = $1 AND field_key = $2 AND tenant_id = $3 LIMIT 1",
12
+ [entityName, fieldKey, tenantId],
13
+ );
14
+ const first = (rows as ReadonlyArray<Record<string, unknown>>)[0];
15
+ return first ? (first["serialized_field"] ?? null) : null;
16
+ }
@@ -0,0 +1,43 @@
1
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
2
+ import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
3
+
4
+ function quoteTable(tableName: string): string {
5
+ return `"${tableName.replace(/"/g, '""')}"`;
6
+ }
7
+
8
+ export async function setCustomFieldValue(
9
+ db: DbRunner,
10
+ tableName: string,
11
+ fieldKey: string,
12
+ valueJson: string,
13
+ aggregateId: string,
14
+ ): Promise<void> {
15
+ const tbl = quoteTable(tableName);
16
+ const escapedKey = fieldKey.replace(/'/g, "''");
17
+ await asRawClient(db).unsafe(
18
+ `UPDATE ${tbl} SET custom_fields = jsonb_set(custom_fields, '{${escapedKey}}', $1::jsonb, true) WHERE id = $2`,
19
+ [valueJson, aggregateId],
20
+ );
21
+ }
22
+
23
+ export async function clearCustomFieldKey(
24
+ db: DbRunner,
25
+ tableName: string,
26
+ fieldKey: string,
27
+ aggregateId: string,
28
+ ): Promise<void> {
29
+ const tbl = quoteTable(tableName);
30
+ await asRawClient(db).unsafe(
31
+ `UPDATE ${tbl} SET custom_fields = custom_fields - $1 WHERE id = $2`,
32
+ [fieldKey, aggregateId],
33
+ );
34
+ }
35
+
36
+ export async function removeCustomFieldKeyFromAllRows(
37
+ db: DbRunner,
38
+ tableName: string,
39
+ fieldKey: string,
40
+ ): Promise<void> {
41
+ const tbl = quoteTable(tableName);
42
+ await asRawClient(db).unsafe(`UPDATE ${tbl} SET custom_fields = custom_fields - $1`, [fieldKey]);
43
+ }
@@ -0,0 +1,14 @@
1
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
2
+ import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
3
+
4
+ export async function countTenantFieldDefinitions(db: TenantDb, tenantId: string): Promise<number> {
5
+ const rowsResult = await asRawClient(db.raw).unsafe(
6
+ "SELECT COUNT(*)::int AS n FROM read_custom_field_definitions WHERE tenant_id = $1",
7
+ [tenantId],
8
+ );
9
+ const rows = rowsResult as ReadonlyArray<Record<string, unknown>>;
10
+ const first = rows[0];
11
+ if (!first) return 0;
12
+ const n = first["n"];
13
+ return typeof n === "number" ? n : Number.parseInt(String(n ?? 0), 10);
14
+ }
@@ -0,0 +1,39 @@
1
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
2
+ import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
3
+
4
+ export async function selectFieldDefinitionsWithSerialized(
5
+ db: DbRunner,
6
+ entityName: string,
7
+ tenantId: string,
8
+ ): Promise<readonly { field_key: string; serialized_field: unknown }[]> {
9
+ return asRawClient(db).unsafe(
10
+ "SELECT field_key, serialized_field FROM read_custom_field_definitions WHERE entity_name = $1 AND tenant_id = $2",
11
+ [entityName, tenantId],
12
+ ) as Promise<readonly { field_key: string; serialized_field: unknown }[]>;
13
+ }
14
+
15
+ export async function selectHostRowsWithCustomFields(
16
+ db: DbRunner,
17
+ tableName: string,
18
+ tenantId: string,
19
+ ): Promise<readonly unknown[]> {
20
+ const quoted = `"${tableName.replace(/"/g, '""')}"`;
21
+ const rowsResult = await asRawClient(db).unsafe(
22
+ `SELECT id, modified_at, custom_fields FROM ${quoted} WHERE tenant_id = $1 AND custom_fields IS NOT NULL`,
23
+ [tenantId],
24
+ );
25
+ return Array.isArray(rowsResult) ? rowsResult : [];
26
+ }
27
+
28
+ export async function updateHostRowCustomFields(
29
+ db: DbRunner,
30
+ tableName: string,
31
+ customFieldsJson: string,
32
+ rowId: string,
33
+ ): Promise<void> {
34
+ const quoted = `"${tableName.replace(/"/g, '""')}"`;
35
+ await asRawClient(db).unsafe(`UPDATE ${quoted} SET custom_fields = $1::jsonb WHERE id = $2`, [
36
+ customFieldsJson,
37
+ rowId,
38
+ ]);
39
+ }
@@ -0,0 +1,54 @@
1
+ import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
2
+ import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
3
+
4
+ function quoteTable(tableName: string): string {
5
+ return `"${tableName.replace(/"/g, '""')}"`;
6
+ }
7
+
8
+ function quoteColumn(columnName: string): string {
9
+ return `"${columnName.replace(/"/g, '""')}"`;
10
+ }
11
+
12
+ export async function selectCustomFieldsHostRows(
13
+ db: DbRunner,
14
+ tableName: string,
15
+ userIdColumn: string,
16
+ userId: string,
17
+ tenantId: string,
18
+ ): Promise<readonly unknown[]> {
19
+ const tbl = quoteTable(tableName);
20
+ const userCol = quoteColumn(userIdColumn);
21
+ const rowsResult = await asRawClient(db).unsafe(
22
+ `SELECT id, custom_fields FROM ${tbl} WHERE ${userCol} = $1 AND tenant_id = $2`,
23
+ [userId, tenantId],
24
+ );
25
+ return Array.isArray(rowsResult) ? rowsResult : [];
26
+ }
27
+
28
+ export async function stripSensitiveCustomFieldKeys(
29
+ db: DbRunner,
30
+ tableName: string,
31
+ userIdColumn: string,
32
+ sensitiveKeys: readonly string[],
33
+ userId: string,
34
+ tenantId: string,
35
+ ): Promise<void> {
36
+ const tbl = quoteTable(tableName);
37
+ const userCol = quoteColumn(userIdColumn);
38
+ const placeholders = sensitiveKeys.map((_, i) => `$${i + 1}`).join(" - ");
39
+ await asRawClient(db).unsafe(
40
+ `UPDATE ${tbl} SET custom_fields = custom_fields - ${placeholders} WHERE ${userCol} = $${sensitiveKeys.length + 1} AND tenant_id = $${sensitiveKeys.length + 2}`,
41
+ [...sensitiveKeys, userId, tenantId],
42
+ );
43
+ }
44
+
45
+ export async function selectFieldDefinitionsForEntity(
46
+ db: DbRunner,
47
+ entityName: string,
48
+ tenantId: string,
49
+ ): Promise<readonly { field_key: string; serialized_field: unknown }[]> {
50
+ return asRawClient(db).unsafe(
51
+ "SELECT field_key, serialized_field FROM read_custom_field_definitions WHERE entity_name = $1 AND tenant_id = $2",
52
+ [entityName, tenantId],
53
+ ) as Promise<readonly { field_key: string; serialized_field: unknown }[]>;
54
+ }
@@ -1,12 +1,7 @@
1
1
  // T1.5b — per-field write access-check for the set/clear handlers.
2
- //
3
- // Loads a fieldDefinition by (tenantId, entityName, fieldKey), reads its
4
- // `serializedField.fieldAccess.write` array, and verifies the calling user
5
- // holds at least one of the listed roles. When `fieldAccess.write` is
6
- // absent or empty the handler-level RBAC is the only gate.
7
2
 
8
3
  import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
9
- import { sql } from "drizzle-orm";
4
+ import { selectSerializedFieldDefinition } from "../db/queries/field-access";
10
5
  import { parseSerializedField } from "./parse-serialized-field";
11
6
 
12
7
  export type FieldAccessCheckResult =
@@ -17,36 +12,6 @@ export type FieldAccessCheckResult =
17
12
  requiredRoles?: ReadonlyArray<string>;
18
13
  };
19
14
 
20
- // Resolution mirrors the Plan-Doc v2 system+tenant UNION: the active
21
- // definition for a fieldKey on an entity is either system-scope or
22
- // tenant-scope, never both (B1 conflict-rule). The tenant-scoped row sits
23
- // in the caller's tenantId; system-scoped rows would sit under
24
- // SYSTEM_TENANT_ID. B1 only ships the tenant-scoped pipeline, so we only
25
- // query the caller's tenant — system-scope lookup will land in B2.
26
- async function loadSerializedField(
27
- db: TenantDb,
28
- tenantId: string,
29
- entityName: string,
30
- fieldKey: string,
31
- ): Promise<unknown | null> {
32
- // TenantDb's tenant-filtered API doesn't expose raw SQL — for this
33
- // single-row lookup we drop down to the underlying DbRunner. tenantId
34
- // is still pinned in the WHERE clause so we don't lose isolation.
35
- const rows = await db.raw.execute(sql`
36
- SELECT serialized_field
37
- FROM read_custom_field_definitions
38
- WHERE entity_name = ${entityName}
39
- AND field_key = ${fieldKey}
40
- AND tenant_id = ${tenantId}
41
- LIMIT 1
42
- `);
43
- const first = (rows as ReadonlyArray<Record<string, unknown>>)[0]; // @cast-boundary db-row
44
- return first ? (first["serialized_field"] ?? null) : null;
45
- }
46
-
47
- // Per Plan-Doc T1.5b: an empty / undefined `write` array means the field
48
- // inherits the handler-level RBAC unchanged. Only an explicit non-empty
49
- // list constrains. Intersection is role-name equality (case-sensitive).
50
15
  export async function checkFieldAccessForWrite(
51
16
  db: TenantDb,
52
17
  tenantId: string,
@@ -54,16 +19,12 @@ export async function checkFieldAccessForWrite(
54
19
  fieldKey: string,
55
20
  userRoles: ReadonlyArray<string>,
56
21
  ): Promise<FieldAccessCheckResult> {
57
- const serialized = await loadSerializedField(db, tenantId, entityName, fieldKey);
22
+ const serialized = await selectSerializedFieldDefinition(db, tenantId, entityName, fieldKey);
58
23
  if (serialized === null) {
59
24
  return { ok: false, reason: "field_definition_not_found" };
60
25
  }
61
26
 
62
27
  const parsed = parseSerializedField(serialized);
63
- // skip: corrupt serialized_field on disk → treat as no-access-restriction
64
- // rather than 500. Loader already returned null on missing row, so a
65
- // null here means parse-failure on a present row; behave like an open
66
- // field (next gate is the handler-level RBAC).
67
28
  if (!parsed) return { ok: true };
68
29
 
69
30
  const required = parsed.fieldAccess?.write;
@@ -1,28 +1,5 @@
1
1
  // T1.5e — per-tenant fieldDefinition quota.
2
2
  //
3
- // `countTenantFieldDefinitions(db, tenantId)` runs a single COUNT(*) against
4
- // `read_custom_field_definitions` scoped to the caller's tenant. The
5
- // `define-tenant-field` handler consults this before insert and rejects
6
- // with `cap_exceeded` once a configurable per-tenant ceiling is reached.
7
- //
8
- // This is a simple projection-count rather than a `cap-counter`-bundle
9
- // counter, because the read-projection is the authoritative source
10
- // (soft-deleted rows already drop out) and we don't need rolling-window
11
- // semantics. A future iteration can swap to `cap-counter` if pricing
12
- // wants e.g. monthly-roll definition allowances.
13
-
14
- import type { TenantDb } from "@cosmicdrift/kumiko-framework/db";
15
- import { sql } from "drizzle-orm";
3
+ // Re-exports from db/queries/quota.ts for backward-compatible import paths.
16
4
 
17
- export async function countTenantFieldDefinitions(db: TenantDb, tenantId: string): Promise<number> {
18
- const rowsResult = await db.raw.execute(sql`
19
- SELECT COUNT(*)::int AS n
20
- FROM read_custom_field_definitions
21
- WHERE tenant_id = ${tenantId}
22
- `);
23
- const rows = rowsResult as ReadonlyArray<Record<string, unknown>>; // @cast-boundary db-row
24
- const first = rows[0];
25
- if (!first) return 0;
26
- const n = first["n"];
27
- return typeof n === "number" ? n : Number.parseInt(String(n ?? 0), 10);
28
- }
5
+ export { countTenantFieldDefinitions } from "../db/queries/quota";