@cosmicdrift/kumiko-samples 0.387.0 → 0.389.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 (274) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +8 -1
  3. package/packages/bundled-features/src/agent-tools/changes.json +12 -0
  4. package/packages/bundled-features/src/agent-tools/index.ts +1 -1
  5. package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +48 -32
  6. package/packages/bundled-features/src/audit/changes.json +12 -0
  7. package/packages/bundled-features/src/audit/feature.ts +1 -0
  8. package/packages/bundled-features/src/audit/handlers/audit-events.ts +1 -1
  9. package/packages/bundled-features/src/auth-email-password/changes.json +12 -0
  10. package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +1 -0
  11. package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +1 -0
  12. package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +1 -0
  13. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +4 -0
  14. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +8 -0
  15. package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +16 -3
  16. package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -0
  17. package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +8 -2
  18. package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +23 -21
  19. package/packages/bundled-features/src/auth-email-password/web/default-auth-pages-gate.tsx +6 -3
  20. package/packages/bundled-features/src/auth-email-password/web/forgot-password-screen.tsx +11 -1
  21. package/packages/bundled-features/src/auth-email-password/web/index.ts +5 -0
  22. package/packages/bundled-features/src/auth-email-password/web/login-screen.tsx +16 -5
  23. package/packages/bundled-features/src/auth-email-password/web/reset-password-screen.tsx +7 -1
  24. package/packages/bundled-features/src/auth-email-password/web/return-path.ts +35 -0
  25. package/packages/bundled-features/src/auth-email-password/web/session.tsx +6 -1
  26. package/packages/bundled-features/src/auth-email-password/web/signup-complete-screen.tsx +11 -3
  27. package/packages/bundled-features/src/auth-email-password/web/signup-screen.tsx +12 -2
  28. package/packages/bundled-features/src/auth-mfa/changes.json +12 -0
  29. package/packages/bundled-features/src/auth-mfa/config.ts +3 -7
  30. package/packages/bundled-features/src/auth-mfa/feature-exports.ts +2 -2
  31. package/packages/bundled-features/src/auth-mfa/feature.ts +6 -0
  32. package/packages/bundled-features/src/auth-mfa/mfa-status-checker.ts +8 -5
  33. package/packages/bundled-features/src/auth-mfa/web/i18n.ts +6 -1
  34. package/packages/bundled-features/src/auth-mfa/web/mfa-code-or-recovery-field.tsx +68 -0
  35. package/packages/bundled-features/src/auth-mfa/web/mfa-disable-dialog.tsx +23 -13
  36. package/packages/bundled-features/src/auth-mfa/web/mfa-regenerate-recovery-dialog.tsx +23 -13
  37. package/packages/bundled-features/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +15 -6
  38. package/packages/bundled-features/src/auth-mfa/web/mfa-verify-screen.tsx +65 -18
  39. package/packages/bundled-features/src/billing-foundation/changes.json +55 -0
  40. package/packages/bundled-features/src/billing-foundation/checkout-core.ts +32 -42
  41. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +8 -2
  42. package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +2 -0
  43. package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +14 -3
  44. package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +3 -4
  45. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +12 -6
  46. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +35 -19
  47. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +12 -8
  48. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-revoke.ts +14 -8
  49. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +5 -0
  50. package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +8 -10
  51. package/packages/bundled-features/src/billing-foundation/events.ts +1 -0
  52. package/packages/bundled-features/src/billing-foundation/feature.ts +3 -1
  53. package/packages/bundled-features/src/billing-foundation/handlers/billing-plans.query.ts +71 -6
  54. package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +1 -0
  55. package/packages/bundled-features/src/billing-foundation/handlers/plan-switch-preview.query.ts +65 -0
  56. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +9 -2
  57. package/packages/bundled-features/src/billing-foundation/handlers/start-plan-checkout.write.ts +44 -8
  58. package/packages/bundled-features/src/billing-foundation/handlers/switch-plan.write.ts +72 -82
  59. package/packages/bundled-features/src/billing-foundation/handlers/sync-subscription.write.ts +14 -1
  60. package/packages/bundled-features/src/billing-foundation/i18n.ts +22 -0
  61. package/packages/bundled-features/src/billing-foundation/index.ts +2 -0
  62. package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +62 -27
  63. package/packages/bundled-features/src/billing-foundation/plan-switch.ts +84 -0
  64. package/packages/bundled-features/src/billing-foundation/types.ts +80 -1
  65. package/packages/bundled-features/src/billing-foundation/validate-options.ts +19 -0
  66. package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +119 -47
  67. package/packages/bundled-features/src/billing-foundation/web/checkout-consent-dialog.tsx +89 -42
  68. package/packages/bundled-features/src/billing-foundation/web/constants.ts +4 -0
  69. package/packages/bundled-features/src/billing-foundation/web/plan-price.ts +55 -0
  70. package/packages/bundled-features/src/billing-foundation/web/plan-switch-confirm-dialog.tsx +224 -0
  71. package/packages/bundled-features/src/cap-counter/cap-reservation.ts +25 -8
  72. package/packages/bundled-features/src/cap-counter/changes.json +30 -0
  73. package/packages/bundled-features/src/cap-counter/enforce-cap.ts +2 -1
  74. package/packages/bundled-features/src/cap-counter/feature.ts +1 -0
  75. package/packages/bundled-features/src/cap-counter/stock-cap-guard.ts +8 -1
  76. package/packages/bundled-features/src/cap-overview/feature.ts +1 -0
  77. package/packages/bundled-features/src/channel-email/changes.json +6 -0
  78. package/packages/bundled-features/src/channel-email/feature.ts +1 -1
  79. package/packages/bundled-features/src/config/changes.json +18 -0
  80. package/packages/bundled-features/src/config/handlers/cascade.query.ts +4 -2
  81. package/packages/bundled-features/src/config/handlers/set.write.ts +19 -2
  82. package/packages/bundled-features/src/config/handlers/values.query.ts +4 -2
  83. package/packages/bundled-features/src/config/write-helpers.ts +27 -8
  84. package/packages/bundled-features/src/crypto-shredding/changes.json +18 -0
  85. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +11 -10
  86. package/packages/bundled-features/src/custom-fields/lib/parse-serialized-field.ts +1 -1
  87. package/packages/bundled-features/src/delivery/changes.json +42 -0
  88. package/packages/bundled-features/src/delivery/channel-context.ts +9 -3
  89. package/packages/bundled-features/src/delivery/db/queries/preferences.ts +2 -2
  90. package/packages/bundled-features/src/delivery/delivery-service.ts +35 -5
  91. package/packages/bundled-features/src/delivery/feature.ts +1 -0
  92. package/packages/bundled-features/src/delivery/handlers/preferences.query.ts +20 -1
  93. package/packages/bundled-features/src/delivery/handlers/unsubscribe-page-chrome.query.ts +1 -1
  94. package/packages/bundled-features/src/delivery/index.ts +5 -0
  95. package/packages/bundled-features/src/delivery/system-channel-context.ts +33 -0
  96. package/packages/bundled-features/src/delivery/unsubscribe-link.ts +3 -1
  97. package/packages/bundled-features/src/delivery/unsubscribe-page-strings.ts +1 -0
  98. package/packages/bundled-features/src/delivery/unsubscribe-page.ts +6 -0
  99. package/packages/bundled-features/src/delivery/unsubscribe.ts +36 -15
  100. package/packages/bundled-features/src/document-ingest-foundation/append-missing-extract-event.ts +84 -0
  101. package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
  102. package/packages/bundled-features/src/document-ingest-foundation/events.ts +15 -0
  103. package/packages/bundled-features/src/document-ingest-foundation/index.ts +11 -0
  104. package/packages/bundled-features/src/feature-toggles/feature.ts +1 -0
  105. package/packages/bundled-features/src/file-foundation/index.ts +1 -0
  106. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +7 -1
  107. package/packages/bundled-features/src/file-provider-s3/env-named-feature.ts +0 -2
  108. package/packages/bundled-features/src/files/README.md +1 -1
  109. package/packages/bundled-features/src/form-draft/changes.json +6 -0
  110. package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +2 -2
  111. package/packages/bundled-features/src/form-draft/decrypt-draft.ts +4 -1
  112. package/packages/bundled-features/src/host-routing/README.md +1 -1
  113. package/packages/bundled-features/src/host-routing/changes.json +12 -0
  114. package/packages/bundled-features/src/host-routing/dispatch.ts +16 -17
  115. package/packages/bundled-features/src/host-routing/options.ts +2 -1
  116. package/packages/bundled-features/src/host-routing/validate-options.ts +5 -0
  117. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +18 -0
  118. package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +1 -0
  119. package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +17 -5
  120. package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +0 -1
  121. package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +0 -3
  122. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -3
  123. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +32 -16
  124. package/packages/bundled-features/src/jobs/changes.json +13 -0
  125. package/packages/bundled-features/src/jobs/feature.ts +15 -4
  126. package/packages/bundled-features/src/jobs/handlers/tenant-failures.query.ts +45 -40
  127. package/packages/bundled-features/src/jobs/handlers/tenant-runs.query.ts +63 -59
  128. package/packages/bundled-features/src/jobs/job-run-logger.ts +5 -4
  129. package/packages/bundled-features/src/ledger/web/index.ts +1 -1
  130. package/packages/bundled-features/src/legal-pages/README.md +1 -1
  131. package/packages/bundled-features/src/legal-pages/seed.ts +5 -2
  132. package/packages/bundled-features/src/mail-foundation/changes.json +6 -0
  133. package/packages/bundled-features/src/mail-foundation/feature.ts +5 -3
  134. package/packages/bundled-features/src/mail-transport-smtp/changes.json +6 -0
  135. package/packages/bundled-features/src/mail-transport-smtp/config-keys.ts +1 -1
  136. package/packages/bundled-features/src/mail-transport-smtp/feature.ts +9 -11
  137. package/packages/bundled-features/src/managed-pages/changes.json +6 -0
  138. package/packages/bundled-features/src/managed-pages/constants.ts +1 -0
  139. package/packages/bundled-features/src/managed-pages/feature.ts +3 -1
  140. package/packages/bundled-features/src/managed-pages/index.ts +1 -0
  141. package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +1 -1
  142. package/packages/bundled-features/src/personal-access-tokens/feature.ts +5 -1
  143. package/packages/bundled-features/src/renderer-pdf/changes.json +6 -0
  144. package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +27 -10
  145. package/packages/bundled-features/src/secrets/changes.json +6 -0
  146. package/packages/bundled-features/src/secrets/write-gate.ts +9 -2
  147. package/packages/bundled-features/src/sessions/changes.json +7 -0
  148. package/packages/bundled-features/src/sessions/feature.ts +8 -2
  149. package/packages/bundled-features/src/sessions/index.ts +1 -1
  150. package/packages/bundled-features/src/sessions/session-callbacks.ts +1 -9
  151. package/packages/bundled-features/src/shared/decrypt-stored-pii.ts +10 -0
  152. package/packages/bundled-features/src/shared/index.ts +1 -1
  153. package/packages/bundled-features/src/shared/mail-template-body.ts +10 -3
  154. package/packages/bundled-features/src/shared/purge-files-reaching-subject.ts +24 -2
  155. package/packages/bundled-features/src/shared/run-in-sub-transaction.ts +5 -5
  156. package/packages/bundled-features/src/step-dispatcher/changes.json +12 -0
  157. package/packages/bundled-features/src/step-dispatcher/deliver-dispatch-request.ts +17 -3
  158. package/packages/bundled-features/src/step-dispatcher/feature.ts +1 -0
  159. package/packages/bundled-features/src/step-dispatcher/mail-runner.ts +2 -1
  160. package/packages/bundled-features/src/subscription-dodo/changes.json +14 -0
  161. package/packages/bundled-features/src/subscription-dodo/client.ts +45 -0
  162. package/packages/bundled-features/src/subscription-dodo/constants.ts +24 -0
  163. package/packages/bundled-features/src/subscription-dodo/feature.ts +207 -0
  164. package/packages/bundled-features/src/subscription-dodo/index.ts +16 -0
  165. package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +345 -0
  166. package/packages/bundled-features/src/subscription-dodo/runtime.ts +126 -0
  167. package/packages/bundled-features/src/subscription-dodo/verify-webhook.ts +262 -0
  168. package/packages/bundled-features/src/subscription-stripe/changes.json +12 -0
  169. package/packages/bundled-features/src/subscription-stripe/feature.ts +1 -0
  170. package/packages/bundled-features/src/subscription-stripe/runtime.ts +7 -6
  171. package/packages/bundled-features/src/tags/feature.ts +1 -0
  172. package/packages/bundled-features/src/template-resolver/api.ts +31 -4
  173. package/packages/bundled-features/src/template-resolver/changes.json +84 -0
  174. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +1 -0
  175. package/packages/bundled-features/src/template-resolver/document-ops.ts +20 -1
  176. package/packages/bundled-features/src/template-resolver/feature.ts +4 -4
  177. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +3 -2
  178. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +27 -5
  179. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +3 -2
  180. package/packages/bundled-features/src/template-resolver/handlers/document-base.ts +3 -2
  181. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +3 -2
  182. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +36 -11
  183. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +18 -19
  184. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +45 -10
  185. package/packages/bundled-features/src/template-resolver/handlers/patch-draft.write.ts +1 -1
  186. package/packages/bundled-features/src/template-resolver/handlers/send-test-status.query.ts +22 -19
  187. package/packages/bundled-features/src/template-resolver/handlers/tenant-override-counts.query.ts +9 -5
  188. package/packages/bundled-features/src/template-resolver/handlers/tenant-override-selection.ts +8 -6
  189. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +44 -5
  190. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +37 -26
  191. package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +7 -2
  192. package/packages/bundled-features/src/template-resolver/seeding.ts +42 -17
  193. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +74 -12
  194. package/packages/bundled-features/src/template-resolver/template-delivery-policy.ts +0 -11
  195. package/packages/bundled-features/src/template-resolver/template-meta.ts +12 -4
  196. package/packages/bundled-features/src/template-resolver/template-preview.ts +11 -3
  197. package/packages/bundled-features/src/template-resolver/web/constants.ts +7 -5
  198. package/packages/bundled-features/src/template-resolver/web/i18n.ts +6 -0
  199. package/packages/bundled-features/src/template-resolver/web/index.ts +7 -0
  200. package/packages/bundled-features/src/template-resolver/web/mail-layout-constants.ts +8 -0
  201. package/packages/bundled-features/src/template-resolver/web/qualified-names.ts +1 -0
  202. package/packages/bundled-features/src/tenant/changes.json +6 -0
  203. package/packages/bundled-features/src/tenant/feature.ts +1 -0
  204. package/packages/bundled-features/src/tenant/handlers/member-directory.query.ts +9 -3
  205. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -3
  206. package/packages/bundled-features/src/tenant/handlers/tenant-directory.query.ts +7 -4
  207. package/packages/bundled-features/src/tenant-lifecycle/changes.json +12 -0
  208. package/packages/bundled-features/src/tenant-lifecycle/constants.ts +5 -0
  209. package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +5 -1
  210. package/packages/bundled-features/src/tenant-lifecycle/stages.ts +35 -1
  211. package/packages/bundled-features/src/tier-engine/changes.json +6 -0
  212. package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +4 -1
  213. package/packages/bundled-features/src/tier-engine/web/constants.ts +3 -3
  214. package/packages/bundled-features/src/user/feature.ts +1 -0
  215. package/packages/bundled-features/src/user-data-rights/changes.json +12 -0
  216. package/packages/bundled-features/src/user-data-rights/feature.ts +1 -0
  217. package/packages/bundled-features/src/user-data-rights/index.ts +6 -1
  218. package/packages/bundled-features/src/user-data-rights/run-export-jobs.ts +3 -4
  219. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +2 -1
  220. package/packages/bundled-features/src/user-data-rights/web/constants.ts +6 -0
  221. package/packages/bundled-features/src/user-data-rights/web/index.ts +1 -0
  222. package/packages/bundled-features/src/waitlist/changes.json +36 -0
  223. package/packages/bundled-features/src/waitlist/feature.ts +1 -0
  224. package/packages/bundled-features/src/waitlist/handlers/export-purge.write.ts +2 -1
  225. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +28 -9
  226. package/packages/bundled-features/src/waitlist/import.ts +28 -3
  227. package/packages/bundled-features/src/waitlist/legacy-migrate.ts +24 -4
  228. package/packages/bundled-features/src/waitlist/link-on-signup.ts +20 -10
  229. package/packages/bundled-features/src/waitlist/options.ts +5 -1
  230. package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +1 -1
  231. package/samples/apps/styleguide/src/app/client.tsx +2 -0
  232. package/samples/apps/styleguide/src/app/seed.ts +6 -4
  233. package/samples/apps/styleguide/src/app/server.ts +4 -1
  234. package/samples/apps/styleguide/src/features/widgets/feature.ts +15 -4
  235. package/samples/apps/styleguide/src/features/widgets/i18n.ts +11 -0
  236. package/samples/apps/styleguide/src/features/widgets/web/Widgets.tsx +24 -0
  237. package/samples/apps/styleguide/src/features/workboard/feature.ts +185 -0
  238. package/samples/apps/styleguide/src/features/workboard/i18n.ts +27 -0
  239. package/samples/apps/styleguide/src/features/workboard/seed.ts +121 -0
  240. package/samples/apps/styleguide/src/features/workboard/web.ts +13 -0
  241. package/samples/apps/ui-walkthrough/src/features/rental/i18n.ts +2 -0
  242. package/samples/apps/ui-walkthrough/src/features/rental/screens.ts +9 -0
  243. package/samples/apps/use-all-bundled/bin/main.ts +2 -0
  244. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +5 -0
  245. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +90 -15
  246. package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +16 -6
  247. package/samples/apps/use-all-bundled/feature-manifest.json +136 -3
  248. package/samples/apps/use-all-bundled/kumiko/schema.ts +2 -0
  249. package/samples/apps/use-all-bundled/kumiko.config.ts +2 -0
  250. package/samples/apps/use-all-bundled/package.json +2 -1
  251. package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +2 -0
  252. package/samples/apps/use-all-bundled/src/app/notes-feature.ts +2 -0
  253. package/samples/apps/use-all-bundled/src/bundled-lists.ts +15 -0
  254. package/samples/apps/use-all-bundled/src/run-config.ts +9 -3
  255. package/samples/recipes/auth-mfa/README.md +5 -0
  256. package/samples/recipes/auth-mfa/package.json +5 -2
  257. package/samples/recipes/auth-mfa/src/web/confirmation-code-step.tsx +49 -0
  258. package/samples/recipes/auth-mfa/tsconfig.json +8 -0
  259. package/samples/recipes/env-composition/README.md +3 -1
  260. package/samples/recipes/env-composition/src/env.ts +2 -0
  261. package/samples/recipes/golden-path/src/feature.ts +2 -1
  262. package/samples/recipes/legal-pages/README.md +1 -1
  263. package/samples/recipes/lifecycle-hooks/README.md +4 -0
  264. package/samples/recipes/lifecycle-hooks/src/feature.ts +26 -1
  265. package/samples/recipes/list-table-options/README.md +50 -0
  266. package/samples/recipes/list-table-options/package.json +11 -0
  267. package/samples/recipes/list-table-options/src/feature.ts +221 -0
  268. package/samples/recipes/pdf-audit-export/src/feature.ts +2 -1
  269. package/samples/recipes/rate-limiting/README.md +18 -0
  270. package/samples/recipes/rate-limiting/src/feature.ts +12 -0
  271. package/samples/recipes/state-machine/src/events.ts +3 -2
  272. package/samples/recipes/tenant-isolation/README.md +1 -0
  273. package/samples/recipes/tenant-isolation/src/feature.ts +36 -0
  274. package/packages/bundled-features/src/billing-foundation/consumer-protection/stored-user-email.ts +0 -12
@@ -27,6 +27,10 @@ back.
27
27
  - **`r.hook({ type: "postDelete", target: { allOf: entityRef }, handler })`** — fires after commit.
28
28
  External side effects (logs, notifications) belong here because
29
29
  failures cannot roll the delete back.
30
+ - **`ctx.scheduleAfterRollback(fn)`** — the counterpart of `scheduleAfterCommit`: runs `fn` when
31
+ the handler transaction (or the enclosing savepoint of a nested `ctx.write`) does not commit.
32
+ Use it to give back something reserved outside the transaction, like the slot in
33
+ `article:reserve-slot`. A failed COMMIT triggers it too, so write the callback idempotent.
30
34
 
31
35
  ## When to reach for it
32
36
 
@@ -13,7 +13,12 @@ import {
13
13
  defineFeature,
14
14
  type SaveContext,
15
15
  } from "@cosmicdrift/kumiko-framework/engine";
16
- import { ConflictError } from "@cosmicdrift/kumiko-framework/errors";
16
+ import {
17
+ ConflictError,
18
+ UnprocessableError,
19
+ writeFailure,
20
+ } from "@cosmicdrift/kumiko-framework/errors";
21
+ import * as z from "zod";
17
22
 
18
23
  export const articleEntity = createEntity({
19
24
  table: "read_sample_articles",
@@ -35,6 +40,9 @@ export const articleEntity = createEntity({
35
40
  // Collect hook events for testing
36
41
  export const hookLog: { type: string; data: Record<string, unknown> }[] = [];
37
42
 
43
+ // Stands in for a quota held in an external system, outside the handler transaction.
44
+ export const reservedSlots = new Set<string>();
45
+
38
46
  const editorWrite = { access: { roles: ["Admin", "Editor"] } } as const;
39
47
 
40
48
  export const articlesFeature = defineFeature("blog", (r) => {
@@ -48,6 +56,23 @@ export const articlesFeature = defineFeature("blog", (r) => {
48
56
  );
49
57
  r.writeHandler(defineEntityDeleteHandler("article", articleEntity, editorWrite));
50
58
 
59
+ // --- scheduleAfterRollback: give back a reservation made outside the transaction when it does
60
+ // not commit. The callback must be idempotent: a failed COMMIT also triggers it.
61
+ r.writeHandler({
62
+ name: "article:reserve-slot",
63
+ schema: z.object({ slot: z.string(), reject: z.boolean() }),
64
+ access: editorWrite.access,
65
+ handler: async (event, ctx) => {
66
+ const { slot, reject } = event.payload;
67
+ reservedSlots.add(slot);
68
+ ctx.scheduleAfterRollback?.(async () => {
69
+ reservedSlots.delete(slot);
70
+ });
71
+ if (reject) return writeFailure(new UnprocessableError("slot_rejected"));
72
+ return { isSuccess: true as const, data: {} };
73
+ },
74
+ });
75
+
51
76
  // --- Validation hook on create: reject banned words + length ---
52
77
  r.hook({
53
78
  type: "validation",
@@ -0,0 +1,50 @@
1
+ # List table options
2
+
3
+ Opt-in table behaviour for entity, projection and related lists. A list without
4
+ the `table` block renders and behaves as before.
5
+
6
+ ## What it shows
7
+
8
+ - `table.rememberState`: search, sort, page size, facets and date range survive
9
+ leaving and re-entering the list within the browser session.
10
+ - `table.resetAll`: one button resets everything that differs from the defaults.
11
+ - `table.columnPicker`: the view menu lets the user hide and reorder columns;
12
+ `hidden: true` on a column starts it hidden.
13
+ - `table.density`: row density default and an optional user choice.
14
+ - `table.pinning`: the user pins columns left or right in the view menu;
15
+ `pinned: "left"` on a column starts it pinned.
16
+ - `table.resizing`: columns can be resized (table-layout fixed); `width` and
17
+ `minWidth` on a column are start values in px.
18
+ - `table.keyboardNavigation`: rows are one tab stop, arrow keys move between rows,
19
+ Space selects the focused row.
20
+ - `table.export`: an Export button downloads the whole filtered list as an XLSX
21
+ file; `maxRows` lowers the default limit of 10000 rows.
22
+ Reference columns export their label and timestamps use the browser time zone.
23
+ - `bulkActions`: row checkboxes and a selection bar. "Delete" runs the delete
24
+ row action once per selected row; done tasks are skipped.
25
+ - `table.grouping`: the server groups the list by status; each header shows the
26
+ group's count and estimate sum, "done" starts collapsed and the view menu
27
+ offers grouping by priority or none.
28
+ - `table.totals`: a footer row with the estimate sum over the filtered list.
29
+ - `facets.estimateHours: { display: "range" }`: from/to inputs for a number field.
30
+ - The same block on a `projectionList`, and `task:workload`, a hand-written list
31
+ query that keeps grouping and totals by accepting `aggregate`
32
+ (`entityListSchema`) and answering it with `answerListAggregate`.
33
+
34
+ ## Source
35
+
36
+ Feature entry point: `src/feature.ts`.
37
+
38
+ ## Tests
39
+
40
+ ```bash
41
+ cd samples/recipes/list-table-options
42
+ bun test
43
+ ```
44
+
45
+ The test boots the feature through the boot validator and checks that a
46
+ misspelled key, a hidden column without the picker, an unknown density, a pinned column
47
+ without `pinning`, a width without `resizing` and a bulk action on a navigate
48
+ row action are rejected.
49
+ `workload.integration.test.ts` runs the hand-written query over HTTP and checks
50
+ the grouped sums and the filtered total.
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "@cosmicdrift/kumiko-sample-list-table-options",
3
+ "description": "Opt-in table behaviour of a list: remembered search/sort/filters, one reset button, column picker with hidden columns, row density, server-side groups with sums, a totals row and a number range facet.",
4
+ "private": true,
5
+ "dependencies": {
6
+ "@cosmicdrift/kumiko-framework": "workspace:*"
7
+ },
8
+ "devDependencies": {
9
+ "@cosmicdrift/kumiko-testing": "workspace:*"
10
+ }
11
+ }
@@ -0,0 +1,221 @@
1
+ import {
2
+ answerListAggregate,
3
+ buildEntityTable,
4
+ createEventStoreExecutor,
5
+ } from "@cosmicdrift/kumiko-framework/db";
6
+ import {
7
+ createDateField,
8
+ createEntity,
9
+ createNumberField,
10
+ createSelectField,
11
+ createTextField,
12
+ defineFeature,
13
+ entityListSchema,
14
+ type FeatureDefinition,
15
+ } from "@cosmicdrift/kumiko-framework/engine";
16
+ import type { ListTableOptions } from "@cosmicdrift/kumiko-framework/ui-types";
17
+ import { z } from "zod";
18
+
19
+ export const taskEntity = createEntity({
20
+ table: "read_list_table_tasks",
21
+ fields: {
22
+ title: createTextField({
23
+ personal: false,
24
+ reason: "is_business_data",
25
+ required: true,
26
+ searchable: true,
27
+ sortable: true,
28
+ }),
29
+ status: createSelectField({
30
+ options: ["open", "inProgress", "blocked", "done"] as const,
31
+ default: "open",
32
+ sortable: true,
33
+ filterable: true,
34
+ }),
35
+ priority: createSelectField({
36
+ options: ["low", "medium", "high"] as const,
37
+ default: "medium",
38
+ sortable: true,
39
+ filterable: true,
40
+ }),
41
+ dueDate: createDateField({ sortable: true }),
42
+ assignee: createTextField({ personal: false, reason: "is_business_data", sortable: true }),
43
+ estimateHours: createNumberField({ sortable: true, filterable: true }),
44
+ },
45
+ });
46
+
47
+ export const openAccess = {
48
+ access: {
49
+ openToAll: { reason: "recipe: any signed-in user manages the tasks; no per-user ownership" },
50
+ },
51
+ } as const;
52
+
53
+ // Overridable so a test can feed the boot validator a block that must be rejected.
54
+ export function createTaskBoardFeature(
55
+ taskTable: ListTableOptions = {
56
+ rememberState: true,
57
+ resetAll: true,
58
+ columnPicker: true,
59
+ density: { default: "comfortable", userChoice: true },
60
+ pinning: true,
61
+ resizing: true,
62
+ keyboardNavigation: true,
63
+ export: { xlsx: true, maxRows: 5000 },
64
+ grouping: { field: "status", collapsedWhen: "done", userChoice: true, fields: ["priority"] },
65
+ totals: [{ field: "estimateHours", kind: "sum" }],
66
+ },
67
+ ): FeatureDefinition {
68
+ return defineFeature("task-board", (r) => {
69
+ r.crud("task", taskEntity, { write: openAccess, read: openAccess });
70
+
71
+ r.screen({
72
+ id: "task-list",
73
+ type: "entityList",
74
+ entity: "task",
75
+ columns: [
76
+ // Starts pinned left; width and minWidth are start values for resizing.
77
+ { field: "title", pinned: "left", width: 280, minWidth: 160 },
78
+ "status",
79
+ "priority",
80
+ { field: "dueDate", hideOnNarrow: true },
81
+ { field: "assignee", hideOnNarrow: true },
82
+ // Starts hidden; the user can add it back in the view menu.
83
+ { field: "estimateHours", hidden: true, hideOnNarrow: true },
84
+ ],
85
+ facets: { status: { display: "chips" }, priority: {}, estimateHours: { display: "range" } },
86
+ defaultSort: { field: "dueDate", dir: "asc" },
87
+ searchable: true,
88
+ table: taskTable,
89
+ rowActions: [
90
+ { kind: "navigate", id: "open", label: "Open", screen: "task-edit", rowClick: true },
91
+ {
92
+ kind: "writeHandler",
93
+ id: "delete",
94
+ label: "Delete",
95
+ handler: "task-board:write:task:delete",
96
+ style: "danger",
97
+ // Done tasks are skipped by the bulk run and counted as skipped.
98
+ visible: { field: "status", ne: "done" },
99
+ },
100
+ ],
101
+ // Row checkboxes and a selection bar; each selected row gets its own delete write.
102
+ bulkActions: [{ id: "delete", rowAction: "delete" }],
103
+ access: openAccess.access,
104
+ });
105
+
106
+ r.screen({
107
+ id: "task-edit",
108
+ type: "entityEdit",
109
+ entity: "task",
110
+ dormant: true,
111
+ listScreenId: "task-list",
112
+ layout: {
113
+ sections: [
114
+ {
115
+ columns: 2,
116
+ fields: ["title", "assignee", "status", "priority", "dueDate", "estimateHours"],
117
+ },
118
+ ],
119
+ },
120
+ });
121
+
122
+ // The same block works on a list over any query.
123
+ r.queryHandler({
124
+ name: "task:overdue",
125
+ schema: z.object({}),
126
+ handler: async () => ({
127
+ rows: [{ id: "t1", title: "Quarterly VAT filing", assignee: "Ines", daysLate: 3 }],
128
+ nextCursor: null,
129
+ }),
130
+ access: openAccess.access,
131
+ outputSchema: z.object({
132
+ rows: z.array(
133
+ z.object({
134
+ id: z.string(),
135
+ title: z.string(),
136
+ assignee: z.string(),
137
+ daysLate: z.number(),
138
+ }),
139
+ ),
140
+ nextCursor: z.string().nullable(),
141
+ }),
142
+ });
143
+
144
+ r.screen({
145
+ id: "task-overdue",
146
+ type: "projectionList",
147
+ query: "task-board:query:task:overdue",
148
+ columns: ["title", "assignee", "daysLate"],
149
+ table: { rememberState: true, density: { userChoice: true } },
150
+ });
151
+
152
+ // A hand-written list query keeps grouping and totals when it accepts
153
+ // `aggregate` (entityListSchema carries it) and answers it on the server.
154
+ const taskExecutor = createEventStoreExecutor(
155
+ buildEntityTable("task", taskEntity),
156
+ taskEntity,
157
+ {
158
+ entityName: "task",
159
+ },
160
+ );
161
+ r.queryHandler({
162
+ name: "task:workload",
163
+ schema: entityListSchema,
164
+ handler: async (query, ctx) => {
165
+ const parentVisibility = { entities: ctx.registry.getAllEntities() };
166
+ const listPayload = query.payload;
167
+ if (listPayload.aggregate !== undefined) {
168
+ return answerListAggregate(
169
+ taskExecutor,
170
+ { ...listPayload, aggregate: listPayload.aggregate },
171
+ query.user,
172
+ ctx.db,
173
+ { parentVisibility },
174
+ {
175
+ entity: taskEntity,
176
+ resolveEntity: (name) => ctx.registry.getEntity(name),
177
+ viewer: { user: query.user, parentVisibility },
178
+ },
179
+ );
180
+ }
181
+ return taskExecutor.list(listPayload, query.user, ctx.db, { parentVisibility });
182
+ },
183
+ access: openAccess.access,
184
+ });
185
+
186
+ r.screen({
187
+ id: "task-workload",
188
+ type: "projectionList",
189
+ query: "task-board:query:task:workload",
190
+ columns: ["title", "assignee", "status", "estimateHours"],
191
+ defaultSort: { field: "title", dir: "asc" },
192
+ table: {
193
+ grouping: { field: "assignee" },
194
+ totals: [{ field: "estimateHours", kind: "sum" }],
195
+ },
196
+ });
197
+
198
+ r.nav({ id: "tasks", label: "task-board:nav.tasks", icon: "list", order: 10 });
199
+ r.nav({
200
+ id: "task-list",
201
+ label: "task-board:nav.list",
202
+ parent: "task-board:nav:tasks",
203
+ screen: "task-board:screen:task-list",
204
+ order: 10,
205
+ });
206
+ r.nav({
207
+ id: "task-overdue",
208
+ label: "task-board:nav.overdue",
209
+ parent: "task-board:nav:tasks",
210
+ screen: "task-board:screen:task-overdue",
211
+ order: 20,
212
+ });
213
+ r.nav({
214
+ id: "task-workload",
215
+ label: "task-board:nav.workload",
216
+ parent: "task-board:nav:tasks",
217
+ screen: "task-board:screen:task-workload",
218
+ order: 30,
219
+ });
220
+ });
221
+ }
@@ -15,6 +15,7 @@ import {
15
15
  createTextField,
16
16
  defineFeature,
17
17
  defineWriteHandler,
18
+ MAX_LIST_LIMIT,
18
19
  } from "@cosmicdrift/kumiko-framework/engine";
19
20
  import { UnprocessableError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
20
21
  import {
@@ -51,7 +52,7 @@ function toFileNameSegment(text: string): string {
51
52
  }
52
53
 
53
54
  // One row above the limit tells "exactly full" from "cut off"; entity lists cap at 200 rows.
54
- const AUDIT_ROW_LIMIT = 100;
55
+ const AUDIT_ROW_LIMIT = MAX_LIST_LIMIT - 1;
55
56
 
56
57
  const access = { access: { roles: ["Admin", "User"] } } as const;
57
58
 
@@ -28,6 +28,24 @@ exhausted) and no bucket is charged. Each `per` may appear once; an empty list f
28
28
  An anonymous handler needs every bucket to be ip-keyed, and `{ disabled: true, reason }`
29
29
  stays a single declaration.
30
30
 
31
+ ## Cost per payload element
32
+
33
+ A bulk handler books one token per item instead of one per request. Declare `rateLimitCost`
34
+ next to `rateLimit`:
35
+
36
+ ```ts illustration
37
+ r.writeHandler({
38
+ // ...
39
+ rateLimit: { per: "tenant+handler", limit: 20, windowSeconds: 600 },
40
+ rateLimitCost: (payload) => payload.files.length,
41
+ });
42
+ ```
43
+
44
+ The cost is computed from the validated payload and booked on every declared bucket, so an
45
+ invalid payload costs nothing and a rejected request consumes nothing. Results below 1 count
46
+ as 1. Because it books after the parse, boot rejects `rateLimitCost` on anonymous or ip-limited
47
+ handlers.
48
+
31
49
  ## Per-payload limits
32
50
 
33
51
  A public write handler that mails an address typed by the caller is also limited per
@@ -33,5 +33,17 @@ export function createRateLimitShowcaseFeature(): FeatureDefinition {
33
33
  { per: "tenant+handler", limit: 3, windowSeconds: 3600 },
34
34
  ],
35
35
  });
36
+
37
+ // Bulk upload: a request books one token per item instead of one per request. The cost
38
+ // comes from the validated payload, so a schema-invalid request costs nothing; a request
39
+ // that would exceed the remaining budget is rejected and consumes nothing.
40
+ r.writeHandler({
41
+ name: "bulk-upload",
42
+ schema: z.object({ files: z.array(z.string()).min(1).max(20) }),
43
+ handler: async () => ({ isSuccess: true as const, data: {} }),
44
+ access: { roles: ["Admin", "User"] },
45
+ rateLimit: { per: "tenant+handler", limit: 20, windowSeconds: 600 },
46
+ rateLimitCost: (payload) => payload.files.length,
47
+ });
36
48
  });
37
49
  }
@@ -5,6 +5,7 @@
5
5
 
6
6
  import type { EventDef } from "@cosmicdrift/kumiko-framework/engine";
7
7
  import { z } from "zod";
8
+ import { INVOICE_STATES } from "./entities/invoice.ts";
8
9
 
9
10
  export const FEATURE_NAME = "billing";
10
11
  export const ENTITY_NAME = "invoice";
@@ -36,8 +37,8 @@ export const INVOICE_CREATED_EVENT = `${ENTITY_NAME}.created` as const;
36
37
 
37
38
  export const emptyEventPayload = z.object({});
38
39
  export const statusForcedPayload = z.object({
39
- newStatus: z.enum(["draft", "sent", "paid", "cancelled"]),
40
- fromStatus: z.enum(["draft", "sent", "paid", "cancelled"]),
40
+ newStatus: z.enum(INVOICE_STATES),
41
+ fromStatus: z.enum(INVOICE_STATES),
41
42
  });
42
43
 
43
44
  // The EventDefs r.defineEvent returns inside the feature setup. Handlers receive them so that
@@ -5,6 +5,7 @@ Multi-tenant data isolation by default.
5
5
  ## What it shows
6
6
 
7
7
  - Tenant filter on ordinary handlers
8
+ - `ctx.db.upsert`: insert-or-update on a tenant-scoped, non-event-sourced table. `conflictTarget` must contain `tenantId` and match a declared unique index (here `tenant_id, key`); event-fed tables are rejected
8
9
  - What breaks if you forget tenancy (and how the framework prevents it)
9
10
 
10
11
  ## Source
@@ -1,4 +1,6 @@
1
+ import { defineUnmanagedTable } from "@cosmicdrift/kumiko-framework/db";
1
2
  import { createEntity, createTextField, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
3
+ import * as z from "zod";
2
4
 
3
5
  export const noteEntity = createEntity({
4
6
  table: "read_sample_notes",
@@ -8,6 +10,26 @@ export const noteEntity = createEntity({
8
10
  },
9
11
  });
10
12
 
13
+ // Not event-sourced: ctx.db.upsert rejects executor-managed tables.
14
+ export const preferenceTable = defineUnmanagedTable({
15
+ tableName: "sample_tenant_preferences",
16
+ columns: [
17
+ {
18
+ name: "id",
19
+ pgType: "uuid",
20
+ notNull: true,
21
+ primaryKey: true,
22
+ defaultSql: "gen_random_uuid()",
23
+ },
24
+ { name: "tenant_id", pgType: "uuid", notNull: true },
25
+ { name: "key", pgType: "text", notNull: true },
26
+ { name: "value", pgType: "text", notNull: true },
27
+ ],
28
+ indexes: [
29
+ { name: "sample_tenant_preferences_tenant_key", columns: ["tenant_id", "key"], unique: true },
30
+ ],
31
+ });
32
+
11
33
  const adminWrite = { access: { roles: ["Admin"] } } as const;
12
34
  const openRead = {
13
35
  access: {
@@ -25,4 +47,18 @@ export const noteFeature = defineFeature("notes", (r) => {
25
47
  read: openRead,
26
48
  verbs: { update: false, delete: false, restore: false },
27
49
  });
50
+
51
+ r.storeTable(preferenceTable, { reason: "settings.tenant_preferences" });
52
+
53
+ r.writeHandler({
54
+ name: "preference:set",
55
+ schema: z.object({ key: z.string().min(1), value: z.string() }),
56
+ handler: async (event, ctx) => {
57
+ const row = await ctx.db.upsert<{ value: string }>(preferenceTable, event.payload, {
58
+ conflictTarget: ["tenantId", "key"],
59
+ });
60
+ return { isSuccess: true, data: { value: row?.value } };
61
+ },
62
+ ...adminWrite,
63
+ });
28
64
  });
@@ -1,12 +0,0 @@
1
- import { PII_ERASED_SENTINEL } from "@cosmicdrift/kumiko-framework/crypto";
2
- import { decryptStoredPii } from "../../shared/index.ts";
3
-
4
- // findForAuth returns the stored column, which is ciphertext under an active KMS;
5
- // channel-email refuses to mail a ciphertext address. Null for an erased subject.
6
- export async function openStoredUserEmail(
7
- stored: string,
8
- fallbackRequestId: string,
9
- ): Promise<string | null> {
10
- const email = await decryptStoredPii(stored, "email", fallbackRequestId);
11
- return email === PII_ERASED_SENTINEL ? null : email;
12
- }