@cosmicdrift/kumiko-samples 0.362.0 → 0.363.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 (252) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +17 -6
  3. package/packages/bundled-features/src/admin-shell/changes.json +6 -0
  4. package/packages/bundled-features/src/admin-shell/feature.ts +8 -1
  5. package/packages/bundled-features/src/agent-tools/agent-manifest.ts +3 -4
  6. package/packages/bundled-features/src/agent-tools/filter-ops.ts +1 -3
  7. package/packages/bundled-features/src/agent-tools/tool-catalog.ts +1 -3
  8. package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +2 -1
  9. package/packages/bundled-features/src/auth-email-password/changes.json +14 -0
  10. package/packages/bundled-features/src/auth-email-password/email-templates.ts +12 -138
  11. package/packages/bundled-features/src/auth-email-password/feature.ts +30 -9
  12. package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +4 -5
  13. package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +2 -5
  14. package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +2 -5
  15. package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +2 -5
  16. package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +3 -4
  17. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +3 -12
  18. package/packages/bundled-features/src/auth-email-password/index.ts +2 -17
  19. package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +30 -22
  20. package/packages/bundled-features/src/auth-email-password/mail-notifications.ts +158 -0
  21. package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -3
  22. package/packages/bundled-features/src/auth-foundation/types.ts +8 -18
  23. package/packages/bundled-features/src/auth-mfa/feature.ts +8 -10
  24. package/packages/bundled-features/src/auth-mfa/hmac-token-codec.ts +4 -4
  25. package/packages/bundled-features/src/auth-mfa/mfa-challenge-token.ts +3 -3
  26. package/packages/bundled-features/src/auth-mfa/mfa-preauth-setup-token.ts +3 -3
  27. package/packages/bundled-features/src/auth-mfa/mfa-setup-token.ts +8 -8
  28. package/packages/bundled-features/src/billing-foundation/changes.json +7 -0
  29. package/packages/bundled-features/src/billing-foundation/constants.ts +5 -4
  30. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +38 -44
  31. package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +3 -3
  32. package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +115 -0
  33. package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +2 -0
  34. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +30 -30
  35. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts +2 -2
  36. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +1 -4
  37. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +2 -2
  38. package/packages/bundled-features/src/billing-foundation/get-subscription-for-tenant.ts +2 -2
  39. package/packages/bundled-features/src/billing-foundation/handlers/create-portal-session.write.ts +4 -9
  40. package/packages/bundled-features/src/billing-foundation/handlers/list-subscriptions.query.ts +1 -1
  41. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +2 -2
  42. package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +1 -1
  43. package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +3 -6
  44. package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
  45. package/packages/bundled-features/src/cap-overview/web/cap-usage-cell.tsx +6 -4
  46. package/packages/bundled-features/src/channel-email/changes.json +19 -0
  47. package/packages/bundled-features/src/channel-email/email-channel.ts +122 -29
  48. package/packages/bundled-features/src/channel-email/feature.ts +6 -3
  49. package/packages/bundled-features/src/channel-email/index.ts +7 -1
  50. package/packages/bundled-features/src/channel-email/pii-guard.ts +13 -0
  51. package/packages/bundled-features/src/channel-email/smtp-transport.ts +15 -1
  52. package/packages/bundled-features/src/channel-email/types.ts +23 -0
  53. package/packages/bundled-features/src/channel-in-app/handlers/mark-read.write.ts +2 -2
  54. package/packages/bundled-features/src/config/handlers/set.write.ts +1 -5
  55. package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +2 -2
  56. package/packages/bundled-features/src/custom-fields/lib/value-schema.ts +10 -11
  57. package/packages/bundled-features/src/custom-fields/wire-user-data-rights.ts +7 -6
  58. package/packages/bundled-features/src/data-retention/changes.json +6 -0
  59. package/packages/bundled-features/src/data-retention/index.ts +1 -0
  60. package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +8 -6
  61. package/packages/bundled-features/src/delivery/attachment-ref.ts +84 -0
  62. package/packages/bundled-features/src/delivery/changes.json +48 -0
  63. package/packages/bundled-features/src/delivery/channel-context.ts +129 -4
  64. package/packages/bundled-features/src/delivery/constants.ts +2 -0
  65. package/packages/bundled-features/src/delivery/delivery-service.ts +104 -26
  66. package/packages/bundled-features/src/delivery/feature.ts +1 -1
  67. package/packages/bundled-features/src/delivery/index.ts +17 -2
  68. package/packages/bundled-features/src/delivery/jobs.ts +48 -5
  69. package/packages/bundled-features/src/delivery/notify-factory.ts +14 -9
  70. package/packages/bundled-features/src/delivery/testing.ts +9 -1
  71. package/packages/bundled-features/src/delivery/types.ts +28 -18
  72. package/packages/bundled-features/src/delivery/unsubscribe.ts +5 -2
  73. package/packages/bundled-features/src/derivatives-sharp/render.ts +1 -22
  74. package/packages/bundled-features/src/document-ingest-foundation/pages.ts +6 -3
  75. package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +4 -4
  76. package/packages/bundled-features/src/file-foundation/index.ts +7 -0
  77. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +86 -0
  78. package/packages/bundled-features/src/folders/web/folder-manager.tsx +5 -2
  79. package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +11 -11
  80. package/packages/bundled-features/src/form-draft/db/queries/draft-count.ts +2 -2
  81. package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +2 -2
  82. package/packages/bundled-features/src/form-draft/release-file-refs.ts +3 -2
  83. package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
  84. package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +6 -6
  85. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +1 -1
  86. package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +1 -1
  87. package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +2 -2
  88. package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +7 -7
  89. package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +2 -6
  90. package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -13
  91. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -6
  92. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +40 -25
  93. package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +4 -3
  94. package/packages/bundled-features/src/jobs/handlers/catalog.query.ts +1 -1
  95. package/packages/bundled-features/src/ledger/recurring.ts +1 -3
  96. package/packages/bundled-features/src/managed-pages/branding.ts +33 -9
  97. package/packages/bundled-features/src/managed-pages/changes.json +6 -0
  98. package/packages/bundled-features/src/managed-pages/feature.ts +1 -0
  99. package/packages/bundled-features/src/managed-pages/i18n.ts +1 -0
  100. package/packages/bundled-features/src/managed-pages/screens/branding-screen.ts +8 -0
  101. package/packages/bundled-features/src/page-render/branding.ts +3 -0
  102. package/packages/bundled-features/src/page-render/markdown.ts +1 -1
  103. package/packages/bundled-features/src/personal-access-tokens/feature.ts +2 -4
  104. package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +2 -1
  105. package/packages/bundled-features/src/personal-access-tokens/index.ts +1 -1
  106. package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +2 -1
  107. package/packages/bundled-features/src/renderer-foundation/README.md +11 -10
  108. package/packages/bundled-features/src/renderer-foundation/api.ts +14 -14
  109. package/packages/bundled-features/src/renderer-foundation/changes.json +39 -0
  110. package/packages/bundled-features/src/renderer-foundation/constants.ts +6 -8
  111. package/packages/bundled-features/src/renderer-foundation/feature.ts +7 -7
  112. package/packages/bundled-features/src/renderer-foundation/index.ts +13 -2
  113. package/packages/bundled-features/src/renderer-foundation/types.ts +132 -35
  114. package/packages/bundled-features/src/renderer-pdf/feature.ts +7 -1
  115. package/packages/bundled-features/src/sessions/db/queries/cleanup.ts +2 -2
  116. package/packages/bundled-features/src/sessions/feature.ts +2 -4
  117. package/packages/bundled-features/src/sessions/handlers/list.query.ts +2 -1
  118. package/packages/bundled-features/src/shared/attachment-file-name.ts +39 -0
  119. package/packages/bundled-features/src/shared/index.ts +6 -0
  120. package/packages/bundled-features/src/shared/is-tenant-db.ts +1 -1
  121. package/packages/bundled-features/src/shared/mail-template-body.ts +25 -0
  122. package/packages/bundled-features/src/shared/sharp-input-limits.ts +26 -0
  123. package/packages/bundled-features/src/shared/signup-handover.ts +4 -4
  124. package/packages/bundled-features/src/tags/handlers/assign-tag.write.ts +5 -1
  125. package/packages/bundled-features/src/tags/web/tag-chip.tsx +2 -1
  126. package/packages/bundled-features/src/template-resolver/README.md +63 -7
  127. package/packages/bundled-features/src/template-resolver/api.ts +83 -8
  128. package/packages/bundled-features/src/template-resolver/changes.json +118 -0
  129. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +214 -0
  130. package/packages/bundled-features/src/template-resolver/constants.ts +39 -11
  131. package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +335 -0
  132. package/packages/bundled-features/src/template-resolver/document-schema.ts +323 -0
  133. package/packages/bundled-features/src/template-resolver/feature.ts +56 -6
  134. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +188 -0
  135. package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +30 -2
  136. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +142 -0
  137. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +72 -0
  138. package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +83 -0
  139. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +3 -3
  140. package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +42 -0
  141. package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +22 -0
  142. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +108 -0
  143. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +5 -7
  144. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +250 -0
  145. package/packages/bundled-features/src/template-resolver/handlers/preview.query.ts +25 -0
  146. package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +58 -0
  147. package/packages/bundled-features/src/template-resolver/handlers/resource-link-pdf.write.ts +34 -0
  148. package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +50 -0
  149. package/packages/bundled-features/src/template-resolver/handlers/resource-shared.ts +24 -0
  150. package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +135 -0
  151. package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +106 -0
  152. package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +32 -3
  153. package/packages/bundled-features/src/template-resolver/handlers/shared.ts +7 -2
  154. package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +184 -0
  155. package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +183 -0
  156. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +55 -0
  157. package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +57 -10
  158. package/packages/bundled-features/src/template-resolver/handlers/upsert-system.write.ts +19 -71
  159. package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +15 -76
  160. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +17 -0
  161. package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +116 -0
  162. package/packages/bundled-features/src/template-resolver/index.ts +100 -0
  163. package/packages/bundled-features/src/template-resolver/layout-regions.ts +206 -0
  164. package/packages/bundled-features/src/template-resolver/linked-resources.ts +102 -0
  165. package/packages/bundled-features/src/template-resolver/preview-display-html.ts +36 -0
  166. package/packages/bundled-features/src/template-resolver/print-document.ts +58 -0
  167. package/packages/bundled-features/src/template-resolver/qualified-names.ts +30 -0
  168. package/packages/bundled-features/src/template-resolver/record-mapping.ts +26 -0
  169. package/packages/bundled-features/src/template-resolver/resource-image.ts +47 -0
  170. package/packages/bundled-features/src/template-resolver/seeding.ts +147 -17
  171. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +416 -0
  172. package/packages/bundled-features/src/template-resolver/table.ts +14 -0
  173. package/packages/bundled-features/src/template-resolver/template-bundle.ts +79 -0
  174. package/packages/bundled-features/src/template-resolver/template-diff.ts +78 -0
  175. package/packages/bundled-features/src/template-resolver/template-meta.ts +129 -0
  176. package/packages/bundled-features/src/template-resolver/template-preview.ts +573 -0
  177. package/packages/bundled-features/src/template-resolver/template-resources.ts +74 -0
  178. package/packages/bundled-features/src/template-resolver/template-usage.ts +48 -0
  179. package/packages/bundled-features/src/template-resolver/template-validation.ts +229 -0
  180. package/packages/bundled-features/src/template-resolver/testing.ts +5 -5
  181. package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +9 -6
  182. package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +162 -0
  183. package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +28 -0
  184. package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +850 -0
  185. package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +61 -0
  186. package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +26 -0
  187. package/packages/bundled-features/src/template-resolver/web/i18n.ts +100 -0
  188. package/packages/bundled-features/src/template-resolver/web/index.ts +19 -0
  189. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor-lazy.tsx +36 -0
  190. package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +169 -0
  191. package/packages/bundled-features/src/template-resolver/web/placeholder-text.ts +37 -0
  192. package/packages/bundled-features/src/template-resolver/web/use-stable-by-key.ts +14 -0
  193. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -2
  194. package/packages/bundled-features/src/tenant/seeding.ts +41 -7
  195. package/packages/bundled-features/src/tenant-lifecycle/boot-checks.ts +1 -1
  196. package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +2 -1
  197. package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +4 -4
  198. package/packages/bundled-features/src/user/handlers/detail.query.ts +3 -5
  199. package/packages/bundled-features/src/user/roles-input-schema.ts +2 -2
  200. package/packages/bundled-features/src/user-data-rights/changes.json +7 -0
  201. package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +2 -2
  202. package/packages/bundled-features/src/user-data-rights/email-templates.ts +5 -149
  203. package/packages/bundled-features/src/user-data-rights/feature.ts +72 -35
  204. package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +2 -1
  205. package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +5 -11
  206. package/packages/bundled-features/src/user-data-rights/lib/default-mailers.ts +134 -53
  207. package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +2 -2
  208. package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +157 -0
  209. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
  210. package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +2 -1
  211. package/packages/bundled-features/src/user-profile/web/profile-screen.tsx +2 -1
  212. package/packages/bundled-features/src/waitlist/changes.json +7 -0
  213. package/packages/bundled-features/src/waitlist/feature.ts +17 -0
  214. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +4 -5
  215. package/packages/bundled-features/src/waitlist/mail.ts +69 -18
  216. package/packages/bundled-features/src/workflow-runner/event-trigger.ts +1 -2
  217. package/samples/apps/ui-walkthrough/package.json +2 -0
  218. package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +9 -0
  219. package/samples/apps/ui-walkthrough/src/app/client.tsx +6 -0
  220. package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +154 -0
  221. package/samples/apps/ui-walkthrough/src/app/server.ts +5 -1
  222. package/samples/apps/ui-walkthrough/src/app/shell.tsx +12 -1
  223. package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +354 -0
  224. package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +68 -0
  225. package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +18 -0
  226. package/samples/apps/ui-walkthrough/src/features/billing/index.ts +2 -0
  227. package/samples/apps/ui-walkthrough/src/features/billing/web/index.ts +7 -0
  228. package/samples/apps/ui-walkthrough/src/run-config.ts +37 -1
  229. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +0 -2
  230. package/samples/apps/use-all-bundled/feature-manifest.json +115 -20
  231. package/samples/apps/use-all-bundled/kumiko/migrations/0045_template-resource-meta.rebuild.json +4 -0
  232. package/samples/apps/use-all-bundled/kumiko/migrations/0046_pdf-rendered.rebuild.json +4 -0
  233. package/samples/apps/use-all-bundled/package.json +2 -0
  234. package/samples/apps/use-all-bundled/playwright.config.ts +38 -3
  235. package/samples/apps/use-all-bundled/schema/generate.ts +13 -3
  236. package/samples/apps/use-all-bundled/schema/schema.generated.ts +3 -0
  237. package/samples/apps/use-all-bundled/scripts/check-coverage.ts +3 -0
  238. package/samples/apps/use-all-bundled/src/app/auth-constants.ts +3 -0
  239. package/samples/apps/use-all-bundled/src/app/client.tsx +4 -0
  240. package/samples/apps/use-all-bundled/src/app/seed-documents.ts +128 -0
  241. package/samples/apps/use-all-bundled/src/app/seed.ts +4 -0
  242. package/samples/apps/use-all-bundled/src/run-config.ts +11 -11
  243. package/samples/recipes/delivery-notifications/README.md +26 -25
  244. package/samples/recipes/delivery-notifications/package.json +2 -1
  245. package/samples/recipes/delivery-notifications/src/feature.ts +64 -58
  246. package/samples/recipes/seed-migration/README.md +1 -1
  247. package/packages/bundled-features/src/renderer-simple/changes.json +0 -27
  248. package/packages/bundled-features/src/renderer-simple/feature.ts +0 -66
  249. package/packages/bundled-features/src/renderer-simple/index.ts +0 -8
  250. package/packages/bundled-features/src/renderer-simple/mail-blocks.ts +0 -138
  251. package/packages/bundled-features/src/renderer-simple/resolve-variables.ts +0 -57
  252. package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +0 -304
@@ -0,0 +1,250 @@
1
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
2
+ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
3
+ import * as z from "zod";
4
+ import { DEFAULT_MAIL_LAYOUT_SLUG, FALLBACK_LOCALE, type TemplateStatus } from "../constants.js";
5
+ import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
6
+ import { collectTemplateUsage } from "../template-usage.js";
7
+ import { storedRecord } from "./template-gates.js";
8
+
9
+ const OVERVIEW_KINDS = ["mail", "document-pdf", "mail-layout", "pdf-layout"] as const;
10
+ type OverviewKind = (typeof OVERVIEW_KINDS)[number];
11
+
12
+ const OVERVIEW_ROW_LIMIT = 5000;
13
+
14
+ export type OverviewOrigin = "tenant" | "system";
15
+ export type OverviewKindGroup = "mail" | "pdf" | "layout";
16
+
17
+ export type OverviewUsage = {
18
+ readonly qualifiedName: string;
19
+ readonly feature: string;
20
+ readonly notification: string;
21
+ readonly trigger: string | null;
22
+ };
23
+
24
+ export type OverviewRow = {
25
+ readonly id: string;
26
+ readonly slug: string;
27
+ readonly kind: OverviewKind;
28
+ readonly kindGroup: OverviewKindGroup;
29
+ readonly title: string;
30
+ readonly origin: OverviewOrigin;
31
+ readonly locales: readonly string[];
32
+ readonly status: TemplateStatus;
33
+ readonly version: number;
34
+ readonly systemVersion: number | null;
35
+ readonly layoutSlug: string | null;
36
+ readonly modifiedAt: string;
37
+ readonly usedBy: readonly OverviewUsage[];
38
+ readonly usedByCount: number;
39
+ };
40
+
41
+ export type OverviewResponse = {
42
+ readonly rows: readonly OverviewRow[];
43
+ readonly nextCursor: null;
44
+ readonly total: number;
45
+ };
46
+
47
+ const SORT_FIELDS = ["title", "slug", "kind", "origin", "status", "modifiedAt"] as const;
48
+ type SortField = (typeof SORT_FIELDS)[number];
49
+
50
+ const KIND_GROUP: Readonly<Record<OverviewKind, OverviewKindGroup>> = {
51
+ mail: "mail",
52
+ "document-pdf": "pdf",
53
+ "mail-layout": "layout",
54
+ "pdf-layout": "layout",
55
+ };
56
+
57
+ function isOverviewKind(kind: string): kind is OverviewKind {
58
+ return OVERVIEW_KINDS.some((candidate) => candidate === kind);
59
+ }
60
+
61
+ function isTemplateStatus(status: string): status is TemplateStatus {
62
+ return status === "draft" || status === "active" || status === "archived";
63
+ }
64
+
65
+ function layoutSlugOf(row: TemplateResourceRow): string {
66
+ const layoutSlug = storedRecord(row.meta)["layoutSlug"];
67
+ return typeof layoutSlug === "string" ? layoutSlug : DEFAULT_MAIL_LAYOUT_SLUG;
68
+ }
69
+
70
+ function preferredRow(rows: readonly TemplateResourceRow[]): TemplateResourceRow | undefined {
71
+ return (
72
+ rows.find((row) => row.locale === FALLBACK_LOCALE) ??
73
+ [...rows].sort((a, b) => a.locale.localeCompare(b.locale))[0]
74
+ );
75
+ }
76
+
77
+ type EffectiveRows = { readonly origin: OverviewOrigin; readonly rows: TemplateResourceRow[] };
78
+
79
+ function effectiveRowsOf(rows: readonly TemplateResourceRow[]): EffectiveRows | null {
80
+ const tenantRows = rows.filter((row) => row.scope === "tenant");
81
+ const systemRows = rows.filter((row) => row.scope === "system");
82
+ const live = (list: TemplateResourceRow[]) => list.filter((row) => row.status !== "archived");
83
+ const liveTenant = live(tenantRows);
84
+ if (liveTenant.length > 0) return { origin: "tenant", rows: liveTenant };
85
+ if (systemRows.length > 0) {
86
+ const liveSystem = live(systemRows);
87
+ return { origin: "system", rows: liveSystem.length > 0 ? liveSystem : systemRows };
88
+ }
89
+ return tenantRows.length > 0 ? { origin: "tenant", rows: tenantRows } : null;
90
+ }
91
+
92
+ function modifiedAtOf(row: TemplateResourceRow): string {
93
+ return (row.modifiedAt ?? row.insertedAt).toString();
94
+ }
95
+
96
+ function splitDocumentSlug(slug: string): { feature: string; notification: string } {
97
+ const separator = slug.indexOf(":");
98
+ return separator === -1
99
+ ? { feature: "", notification: slug }
100
+ : { feature: slug.slice(0, separator), notification: slug.slice(separator + 1) };
101
+ }
102
+
103
+ function compareBy(field: SortField, a: OverviewRow, b: OverviewRow): number {
104
+ return String(a[field]).localeCompare(String(b[field]));
105
+ }
106
+
107
+ function isSortField(value: string): value is SortField {
108
+ return SORT_FIELDS.some((candidate) => candidate === value);
109
+ }
110
+
111
+ type OverviewDraft = Omit<OverviewRow, "usedBy" | "usedByCount">;
112
+ type OverviewFilter = {
113
+ readonly field: "kindGroup" | "kind" | "origin" | "status";
114
+ readonly value: readonly string[];
115
+ };
116
+
117
+ function groupBySlugAndKind(
118
+ rows: readonly TemplateResourceRow[],
119
+ ): Map<string, TemplateResourceRow[]> {
120
+ const groups = new Map<string, TemplateResourceRow[]>();
121
+ for (const row of rows) {
122
+ if (!isOverviewKind(row.kind)) continue;
123
+ const key = `${row.kind}|${row.slug}`;
124
+ groups.set(key, [...(groups.get(key) ?? []), row]);
125
+ }
126
+ return groups;
127
+ }
128
+
129
+ function buildDrafts(groups: ReadonlyMap<string, readonly TemplateResourceRow[]>): {
130
+ readonly drafts: OverviewDraft[];
131
+ readonly layoutSlugByDocument: Map<string, string>;
132
+ } {
133
+ const layoutSlugByDocument = new Map<string, string>();
134
+ const drafts: OverviewDraft[] = [];
135
+ for (const [id, groupRows] of groups) {
136
+ const effective = effectiveRowsOf(groupRows);
137
+ const preferred = effective === null ? undefined : preferredRow(effective.rows);
138
+ if (effective === null || preferred === undefined) continue;
139
+ if (!isOverviewKind(preferred.kind) || !isTemplateStatus(preferred.status)) continue;
140
+ const systemSameLocale = groupRows.find(
141
+ (row) => row.scope === "system" && row.locale === preferred.locale,
142
+ );
143
+ const isDocument = preferred.kind === "mail" || preferred.kind === "document-pdf";
144
+ const layoutSlug = isDocument ? layoutSlugOf(preferred) : null;
145
+ if (layoutSlug !== null && preferred.kind === "document-pdf") {
146
+ layoutSlugByDocument.set(preferred.slug, layoutSlug);
147
+ }
148
+ drafts.push({
149
+ id,
150
+ slug: preferred.slug,
151
+ kind: preferred.kind,
152
+ kindGroup: KIND_GROUP[preferred.kind],
153
+ title: preferred.title ?? preferred.slug,
154
+ origin: effective.origin,
155
+ locales: [...new Set(effective.rows.map((row) => row.locale))].sort(),
156
+ status: preferred.status,
157
+ version: preferred.version,
158
+ systemVersion: systemSameLocale?.version ?? null,
159
+ layoutSlug,
160
+ modifiedAt: modifiedAtOf(preferred),
161
+ });
162
+ }
163
+ return { drafts, layoutSlugByDocument };
164
+ }
165
+
166
+ function withUsage(
167
+ draft: OverviewDraft,
168
+ layoutSlugByDocument: ReadonlyMap<string, string>,
169
+ registry: Parameters<typeof collectTemplateUsage>[0],
170
+ ): OverviewRow {
171
+ let usedBy: OverviewUsage[] = [];
172
+ if (draft.kind === "mail" || draft.kind === "mail-layout") {
173
+ usedBy = collectTemplateUsage(registry, draft.slug, draft.kind).map((entry) => ({
174
+ qualifiedName: entry.qualifiedName,
175
+ feature: entry.feature,
176
+ notification: entry.notification,
177
+ trigger: entry.trigger,
178
+ }));
179
+ } else if (draft.kind === "pdf-layout") {
180
+ usedBy = [...layoutSlugByDocument]
181
+ .filter(([, layoutSlug]) => layoutSlug === draft.slug)
182
+ .map(([documentSlug]) => ({
183
+ qualifiedName: documentSlug,
184
+ ...splitDocumentSlug(documentSlug),
185
+ trigger: null,
186
+ }));
187
+ }
188
+ return { ...draft, usedBy, usedByCount: usedBy.length };
189
+ }
190
+
191
+ function filterRows(
192
+ rows: readonly OverviewRow[],
193
+ search: string | undefined,
194
+ filters: readonly OverviewFilter[],
195
+ ): OverviewRow[] {
196
+ const needle = search?.trim().toLowerCase() ?? "";
197
+ return rows.filter((row) => {
198
+ if (needle !== "" && !`${row.title} ${row.slug}`.toLowerCase().includes(needle)) return false;
199
+ return filters.every((filter) => filter.value.includes(String(row[filter.field])));
200
+ });
201
+ }
202
+
203
+ const filterSchema = z.object({
204
+ field: z.enum(["kindGroup", "kind", "origin", "status"]),
205
+ op: z.literal("in"),
206
+ value: z.array(z.string()),
207
+ });
208
+
209
+ export const overviewQuery = defineQueryHandler({
210
+ name: "overview",
211
+ schema: z.object({
212
+ search: z.string().optional(),
213
+ filters: z.array(filterSchema).optional(),
214
+ sort: z.string().optional(),
215
+ sortDirection: z.enum(["asc", "desc"]).optional(),
216
+ limit: z.number().int().min(1).max(200).default(50),
217
+ offset: z.number().int().min(0).optional(),
218
+ totalCount: z.boolean().optional(),
219
+ }),
220
+ access: { roles: ["TenantAdmin", "SystemAdmin"] },
221
+ description:
222
+ "Lists mail, PDF and layout templates as one row per slug and kind for the caller's tenant: where the effective version comes from (own override or system), its locales, status and version, the system version it overrides, the layout it uses and which notifications or templates use it; use it to browse and filter all templates at a glance.",
223
+ handler: async (query, ctx) => {
224
+ const payload = query.payload;
225
+ const rows = await selectMany<TemplateResourceRow>(
226
+ ctx.db,
227
+ templateResourcesTable,
228
+ { kind: { in: [...OVERVIEW_KINDS] } },
229
+ { limit: OVERVIEW_ROW_LIMIT },
230
+ );
231
+
232
+ const { drafts, layoutSlugByDocument } = buildDrafts(groupBySlugAndKind(rows));
233
+ const overviewRows = drafts.map((draft) =>
234
+ withUsage(draft, layoutSlugByDocument, ctx.registry),
235
+ );
236
+ const filtered = filterRows(overviewRows, payload.search, payload.filters ?? []);
237
+
238
+ const sortField: SortField =
239
+ payload.sort !== undefined && isSortField(payload.sort) ? payload.sort : "modifiedAt";
240
+ const direction = (payload.sortDirection ?? "desc") === "desc" ? -1 : 1;
241
+ filtered.sort((a, b) => direction * compareBy(sortField, a, b) || a.id.localeCompare(b.id));
242
+
243
+ const offset = payload.offset ?? 0;
244
+ return {
245
+ rows: filtered.slice(offset, offset + payload.limit),
246
+ nextCursor: null,
247
+ total: filtered.length,
248
+ } satisfies OverviewResponse;
249
+ },
250
+ });
@@ -0,0 +1,25 @@
1
+ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
2
+ import * as z from "zod";
3
+ import { renderTemplatePreview } from "../template-preview.js";
4
+ import { localeSchema, templateSlugSchema } from "./shared.js";
5
+
6
+ export const previewQuery = defineQueryHandler({
7
+ name: "preview",
8
+ schema: z.object({
9
+ slug: templateSlugSchema,
10
+ kind: z.enum(["mail", "document-pdf"]),
11
+ locale: localeSchema,
12
+ /** Without content the stored template is previewed. */
13
+ content: z.string().max(200_000).optional(),
14
+ meta: z.unknown().optional(),
15
+ variableSchema: z.record(z.string(), z.unknown()).default({}),
16
+ }),
17
+ access: { roles: ["TenantAdmin", "SystemAdmin"] },
18
+ description:
19
+ "Renders a template draft, or the stored template when no content is given, with sample values taken from the declared variable examples, without saving or sending anything: returns the mail subject, html and plain text with its inline images (a PDF template returns its body and layout regions instead), the sample values used, and checks for unresolved images, missing plain text, mail size and unused required placeholders; a draft that fails the write rules is rejected with the same problems as a save.",
20
+ handler: async (query, ctx) => {
21
+ const outcome = await renderTemplatePreview(ctx, query.user, query.payload);
22
+ if (!outcome.ok) throw outcome.error;
23
+ return outcome.preview;
24
+ },
25
+ });
@@ -0,0 +1,58 @@
1
+ import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
+ import { defineWriteHandler, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { NotFoundError, ValidationError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
4
+ import * as z from "zod";
5
+ import type { TemplateResourceRow } from "../table.js";
6
+ import { templateResourcesTable } from "../table.js";
7
+ import { executor } from "./shared.js";
8
+
9
+ export const resetToSystemWrite = defineWriteHandler({
10
+ name: "reset-to-system",
11
+ schema: z.object({ id: z.string().min(1) }),
12
+ access: { roles: ["TenantAdmin", "SystemAdmin"] },
13
+ description:
14
+ "Retires a tenant override by archiving it so the resolver serves the active system template again; the override and its content survive and can be published again. Refused when there is no active system template to fall back to.",
15
+ handler: async (event, ctx) => {
16
+ const existing = await fetchOne<TemplateResourceRow>(ctx.db, templateResourcesTable, {
17
+ id: event.payload.id,
18
+ });
19
+ if (!existing || existing.tenantId !== event.user.tenantId || existing.scope !== "tenant") {
20
+ return writeFailure(new NotFoundError("template-resource", event.payload.id));
21
+ }
22
+ const systemRow = await fetchOne<TemplateResourceRow>(ctx.db, templateResourcesTable, {
23
+ tenantId: SYSTEM_TENANT_ID,
24
+ slug: existing.slug,
25
+ kind: existing.kind,
26
+ locale: existing.locale,
27
+ status: "active",
28
+ });
29
+ if (!systemRow) {
30
+ return writeFailure(
31
+ new ValidationError({
32
+ fields: [
33
+ {
34
+ path: "id",
35
+ code: "no_system_template",
36
+ i18nKey: "templateResolver.errors.noSystemTemplate",
37
+ },
38
+ ],
39
+ }),
40
+ );
41
+ }
42
+ const response = {
43
+ isSuccess: true as const,
44
+ data: {
45
+ id: String(existing.id),
46
+ status: "archived" as const,
47
+ systemTemplateId: String(systemRow.id),
48
+ },
49
+ };
50
+ if (existing.status === "archived") return response;
51
+ const result = await executor.update(
52
+ { id: existing.id, version: existing.version, changes: { status: "archived" } },
53
+ event.user,
54
+ ctx.db,
55
+ );
56
+ return result.isSuccess ? response : result;
57
+ },
58
+ });
@@ -0,0 +1,34 @@
1
+ import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
2
+ import * as z from "zod";
3
+ import { linkTemplatePdfAttachment } from "../store-template-resource.js";
4
+ import { resourceKeySchema, resourceTargetTenantId } from "./resource-shared.js";
5
+
6
+ export const resourceLinkPdfWrite = defineWriteHandler({
7
+ name: "resource-link-pdf",
8
+ schema: z.object({
9
+ templateId: z.string().min(1),
10
+ key: resourceKeySchema,
11
+ /** Slug of a document-pdf template of the tenant or the system. */
12
+ templateSlug: z.string().min(1),
13
+ /** Name pattern of the attached PDF; may hold {{placeholders}}, must end with .pdf. */
14
+ fileName: z.string().min(1),
15
+ /** Cross-tenant target — SystemAdmin only. */
16
+ tenantIdOverride: z.string().min(1).optional(),
17
+ }),
18
+ access: { roles: ["TenantAdmin", "SystemAdmin"] },
19
+ description:
20
+ "Links a PDF rendered from a document-pdf template into a mail template under the key. Every mail sent from the template then carries that PDF, rendered at send time with the mail's own variables and locale; nothing is uploaded or stored. fileName is a pattern that may use the mail's {{placeholders}} and must end with .pdf. The document template must exist for the tenant or the system. Counts toward the 10 attachments per mail template together with uploaded files.",
21
+ handler: async (event, ctx) => {
22
+ const { templateId, key, templateSlug, fileName } = event.payload;
23
+ const tenantId = resourceTargetTenantId(event.payload.tenantIdOverride, event.user.tenantId);
24
+ return linkTemplatePdfAttachment({
25
+ ctx,
26
+ user: event.user,
27
+ tenantId,
28
+ templateId,
29
+ key,
30
+ templateSlug,
31
+ fileName,
32
+ });
33
+ },
34
+ });
@@ -0,0 +1,50 @@
1
+ import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
+ import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
4
+ import * as z from "zod";
5
+ import { requireForTenant } from "../../shared/index.js";
6
+ import { parseLinkedResourceEntries } from "../linked-resources.js";
7
+ import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
8
+ import { resourceKeySchema, resourceTargetTenantId } from "./resource-shared.js";
9
+ import { executor } from "./shared.js";
10
+
11
+ // The FileRef itself stays: copy-locale and tenant overrides can share ids and there is
12
+ // no reference count to tell when the last user is gone.
13
+ export const resourceRemoveWrite = defineWriteHandler({
14
+ name: "resource-remove",
15
+ schema: z.object({
16
+ templateId: z.string().min(1),
17
+ key: resourceKeySchema,
18
+ /** Cross-tenant target — SystemAdmin only. */
19
+ tenantIdOverride: z.string().min(1).optional(),
20
+ }),
21
+ access: { roles: ["TenantAdmin", "SystemAdmin"] },
22
+ description:
23
+ "Unlinks a resource key from a template, whether it holds an image, an attached file or a linked PDF template; removing a key the template does not have succeeds without a change.",
24
+ handler: async (event, ctx) => {
25
+ const { templateId, key } = event.payload;
26
+ const tenantId = resourceTargetTenantId(event.payload.tenantIdOverride, event.user.tenantId);
27
+ const { db, streamTenantId } = requireForTenant(ctx, tenantId);
28
+ const row = await fetchOne<TemplateResourceRow>(db, templateResourcesTable, {
29
+ tenantId,
30
+ id: templateId,
31
+ });
32
+ if (row === undefined) return writeFailure(new NotFoundError("template-resource", templateId));
33
+
34
+ const { [key]: removed, ...remaining } = parseLinkedResourceEntries(row.linkedResources);
35
+ if (removed === undefined) return { isSuccess: true as const, data: { key, removed: false } };
36
+
37
+ const updated = await executor.update(
38
+ {
39
+ id: row.id,
40
+ version: row.version,
41
+ changes: { linkedResources: JSON.stringify(remaining) },
42
+ },
43
+ event.user,
44
+ db,
45
+ { streamTenantId },
46
+ );
47
+ if (!updated.isSuccess) return updated;
48
+ return { isSuccess: true as const, data: { key, removed: true } };
49
+ },
50
+ });
@@ -0,0 +1,24 @@
1
+ import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
2
+ import * as z from "zod";
3
+ import { isDocumentResourceKey } from "../document-schema.js";
4
+
5
+ export const RESOURCE_MAX_BYTES_CONFIG_KEY = "template-resolver:config:resource-max-bytes";
6
+ export const DEFAULT_RESOURCE_MAX_BYTES = 2 * 1024 * 1024;
7
+
8
+ export const resourceKeySchema = z.string().min(1).max(64).refine(isDocumentResourceKey, {
9
+ message: "resource key must be letters, digits, dot, dash or underscore",
10
+ });
11
+
12
+ export const RESOURCE_PURPOSES = ["image", "attachment"] as const;
13
+ export type ResourcePurpose = (typeof RESOURCE_PURPOSES)[number];
14
+
15
+ export const MAX_FILE_ATTACHMENTS_PER_TEMPLATE = 10;
16
+
17
+ export function resourceTargetTenantId(
18
+ tenantIdOverride: string | undefined,
19
+ sessionTenantId: TenantId,
20
+ ): TenantId {
21
+ // @cast-boundary engine-payload — override is a zod-validated string; forTenant
22
+ // rejects it for non-SystemAdmins.
23
+ return (tenantIdOverride ?? sessionTenantId) as TenantId;
24
+ }
@@ -0,0 +1,135 @@
1
+ import { defineWriteHandler, type WriteResult } from "@cosmicdrift/kumiko-framework/engine";
2
+ import {
3
+ NotFoundError,
4
+ UnprocessableError,
5
+ writeFailure,
6
+ } from "@cosmicdrift/kumiko-framework/errors";
7
+ import * as z from "zod";
8
+ import {
9
+ FileRefTooLargeError,
10
+ fileContextForTenant,
11
+ readFileRefContent,
12
+ } from "../../file-foundation/index.js";
13
+ import { attachmentFileNameSchema } from "../../shared/index.js";
14
+ import { TemplateResolverErrors } from "../qualified-names.js";
15
+ import {
16
+ readTemplateResourceMaxBytes,
17
+ type StoredTemplateAttachment,
18
+ type StoredTemplateResource,
19
+ storeTemplateAttachment,
20
+ storeTemplateResource,
21
+ } from "../store-template-resource.js";
22
+ import { RESOURCE_PURPOSES, resourceKeySchema, resourceTargetTenantId } from "./resource-shared.js";
23
+
24
+ type AttachmentTarget = {
25
+ readonly templateId: string;
26
+ readonly key: string;
27
+ readonly fileName: string;
28
+ };
29
+
30
+ function attachmentTargetOf(input: {
31
+ readonly templateId: string | undefined;
32
+ readonly key: string | undefined;
33
+ readonly fileName: string | undefined;
34
+ }): AttachmentTarget | null {
35
+ const { templateId, key, fileName } = input;
36
+ if (templateId === undefined || key === undefined || fileName === undefined) return null;
37
+ const parsedName = attachmentFileNameSchema.safeParse(fileName);
38
+ return parsedName.success ? { templateId, key, fileName: parsedName.data } : null;
39
+ }
40
+
41
+ // Purpose "image": stores a re-encoded image as a FileRef and, with templateId + key, links it into
42
+ // the template; without templateId it only creates the FileRef (the branding logo upload path).
43
+ // Purpose "attachment": stores the file as uploaded and links it as a mail attachment.
44
+ export const resourceUploadWrite = defineWriteHandler({
45
+ name: "resource-upload",
46
+ schema: z
47
+ .object({
48
+ templateId: z.string().min(1).optional(),
49
+ key: resourceKeySchema.optional(),
50
+ /** An unclaimed upload of the caller (POST /api/files without entityType). */
51
+ sourceFileRefId: z.uuid(),
52
+ /** Cross-tenant target — SystemAdmin only. */
53
+ tenantIdOverride: z.string().min(1).optional(),
54
+ /** "attachment" links a PDF/PNG/JPEG as a file attached to every mail of the template. */
55
+ purpose: z.enum(RESOURCE_PURPOSES).default("image"),
56
+ /** Name the recipient sees; required for purpose "attachment". */
57
+ fileName: z.string().optional(),
58
+ })
59
+ .refine(
60
+ (p) => p.purpose === "attachment" || (p.templateId === undefined) === (p.key === undefined),
61
+ {
62
+ message: "templateId and key must be given together",
63
+ path: ["key"],
64
+ },
65
+ ),
66
+ access: { roles: ["TenantAdmin", "SystemAdmin"] },
67
+ description:
68
+ "Turns a file uploaded via POST /api/files (multipart, no entityType) into a template resource. With purpose image (default) the upload is checked against the resource-max-bytes cap, re-encoded to a supported image format and stored as a new file; with templateId and key it is linked into that template under the key, without them only the file is created. With purpose attachment, templateId, key and fileName are required: a PDF, PNG or JPEG is stored unchanged and linked into a mail template as a file attached to every mail sent from it, at most 10 per template. A cap above the files route's maxUploadSize has no effect.",
69
+ handler: async (
70
+ event,
71
+ ctx,
72
+ ): Promise<WriteResult<StoredTemplateResource | StoredTemplateAttachment>> => {
73
+ const { templateId, key, sourceFileRefId, purpose, fileName } = event.payload;
74
+ const attachmentTarget =
75
+ purpose === "attachment" ? attachmentTargetOf({ templateId, key, fileName }) : null;
76
+ if (purpose === "attachment" && attachmentTarget === null) {
77
+ return writeFailure(
78
+ new UnprocessableError(TemplateResolverErrors.attachmentInvalid, {
79
+ i18nKey: "templateResolver.errors.attachment.invalid",
80
+ details: { code: "target_required" },
81
+ }),
82
+ );
83
+ }
84
+ const maxBytes = await readTemplateResourceMaxBytes(ctx.config, ctx.registry);
85
+
86
+ // The source is read from the session tenant only: with tenantIdOverride the upload
87
+ // sits in the admin's own tenant while the normalized file lands in the target tenant.
88
+ // Only the caller's own unclaimed upload qualifies, so a record-attached image of
89
+ // someone else cannot be copied into a template.
90
+ let source: Awaited<ReturnType<typeof readFileRefContent>>;
91
+ try {
92
+ source = await readFileRefContent({
93
+ db: ctx.db,
94
+ filesForTenant: (tid) => fileContextForTenant(ctx, tid),
95
+ fileRefId: sourceFileRefId,
96
+ allowedTenantIds: [event.user.tenantId],
97
+ unclaimedUploadOf: event.user.id,
98
+ maxBytes,
99
+ });
100
+ } catch (cause) {
101
+ if (cause instanceof FileRefTooLargeError) {
102
+ return writeFailure(
103
+ new UnprocessableError(TemplateResolverErrors.resourceTooLarge, {
104
+ i18nKey: "templateResolver.errors.resource.tooLarge",
105
+ details: { maxBytes, size: cause.size },
106
+ }),
107
+ );
108
+ }
109
+ throw cause;
110
+ }
111
+ if (source === null) return writeFailure(new NotFoundError("file-ref", sourceFileRefId));
112
+
113
+ // The source upload stays as an unclaimed file: bundled-features cannot reach the fileRef executor to delete it.
114
+ const tenantId = resourceTargetTenantId(event.payload.tenantIdOverride, event.user.tenantId);
115
+ if (attachmentTarget !== null) {
116
+ return storeTemplateAttachment({
117
+ ctx,
118
+ user: event.user,
119
+ tenantId,
120
+ bytes: source.content,
121
+ maxBytes,
122
+ ...attachmentTarget,
123
+ });
124
+ }
125
+ return storeTemplateResource({
126
+ ctx,
127
+ user: event.user,
128
+ tenantId,
129
+ bytes: source.content,
130
+ maxBytes,
131
+ ...(templateId !== undefined && { templateId }),
132
+ ...(key !== undefined && { key }),
133
+ });
134
+ },
135
+ });