@antelopejs/dms-mailing 0.3.3 → 0.4.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 (705) hide show
  1. package/README.md +232 -22
  2. package/dist/automation/email-event-trigger.js +2 -2
  3. package/dist/automation/email-event-trigger.js.map +1 -1
  4. package/dist/automation/send-template-action.js +2 -2
  5. package/dist/automation/send-template-action.js.map +1 -1
  6. package/dist/config.js +3 -1
  7. package/dist/config.js.map +1 -1
  8. package/dist/constants.d.ts +10 -1
  9. package/dist/constants.js +15 -3
  10. package/dist/constants.js.map +1 -1
  11. package/dist/crons/attention.d.ts +3 -0
  12. package/dist/crons/attention.js +19 -0
  13. package/dist/crons/attention.js.map +1 -0
  14. package/dist/crons/index.d.ts +2 -0
  15. package/dist/crons/index.js +2 -0
  16. package/dist/crons/index.js.map +1 -1
  17. package/dist/crons/jobs.d.ts +12 -0
  18. package/dist/crons/jobs.js +27 -0
  19. package/dist/crons/jobs.js.map +1 -0
  20. package/dist/crons/retention.d.ts +2 -4
  21. package/dist/crons/retention.js +7 -22
  22. package/dist/crons/retention.js.map +1 -1
  23. package/dist/data/displays.d.ts +5 -0
  24. package/dist/data/displays.js +23 -0
  25. package/dist/data/displays.js.map +1 -0
  26. package/dist/data/index.d.ts +1 -0
  27. package/dist/data/index.js +1 -0
  28. package/dist/data/index.js.map +1 -1
  29. package/dist/data/locale-fallback.d.ts +3 -0
  30. package/dist/data/locale-fallback.js +20 -0
  31. package/dist/data/locale-fallback.js.map +1 -0
  32. package/dist/data/read-only-routes.d.ts +7 -0
  33. package/dist/data/read-only-routes.js +13 -0
  34. package/dist/data/read-only-routes.js.map +1 -0
  35. package/dist/data/sends-table.d.ts +19 -5
  36. package/dist/data/sends-table.js +185 -17
  37. package/dist/data/sends-table.js.map +1 -1
  38. package/dist/data/suppressions-table.d.ts +24 -0
  39. package/dist/data/suppressions-table.js +120 -0
  40. package/dist/data/suppressions-table.js.map +1 -0
  41. package/dist/data/templates-table.d.ts +20 -2
  42. package/dist/data/templates-table.js +188 -31
  43. package/dist/data/templates-table.js.map +1 -1
  44. package/dist/data-types/categories-type.d.ts +11 -13
  45. package/dist/data-types/categories-type.js +3 -3
  46. package/dist/data-types/categories-type.js.map +1 -1
  47. package/dist/data-types/category-type.js +1 -1
  48. package/dist/data-types/category-type.js.map +1 -1
  49. package/dist/db/models/index.d.ts +3 -0
  50. package/dist/db/models/index.js +3 -0
  51. package/dist/db/models/index.js.map +1 -1
  52. package/dist/db/models/sends.model.d.ts +12 -1
  53. package/dist/db/models/sends.model.js +64 -0
  54. package/dist/db/models/sends.model.js.map +1 -1
  55. package/dist/db/models/settings.model.d.ts +0 -1
  56. package/dist/db/models/settings.model.js +0 -7
  57. package/dist/db/models/settings.model.js.map +1 -1
  58. package/dist/db/models/suppressions.model.d.ts +28 -0
  59. package/dist/db/models/suppressions.model.js +46 -0
  60. package/dist/db/models/suppressions.model.js.map +1 -0
  61. package/dist/db/models/template-versions.model.d.ts +24 -0
  62. package/dist/db/models/template-versions.model.js +17 -0
  63. package/dist/db/models/template-versions.model.js.map +1 -0
  64. package/dist/db/models/templates.model.d.ts +2 -0
  65. package/dist/db/models/templates.model.js +3 -0
  66. package/dist/db/models/templates.model.js.map +1 -1
  67. package/dist/db/models/webhook-status.model.d.ts +24 -0
  68. package/dist/db/models/webhook-status.model.js +21 -0
  69. package/dist/db/models/webhook-status.model.js.map +1 -0
  70. package/dist/db/tables/index.d.ts +3 -0
  71. package/dist/db/tables/index.js +3 -0
  72. package/dist/db/tables/index.js.map +1 -1
  73. package/dist/db/tables/send-events.table.d.ts +2 -2
  74. package/dist/db/tables/send-events.table.js.map +1 -1
  75. package/dist/db/tables/sends.table.d.ts +10 -2
  76. package/dist/db/tables/sends.table.js +34 -1
  77. package/dist/db/tables/sends.table.js.map +1 -1
  78. package/dist/db/tables/suppressions.table.d.ts +12 -0
  79. package/dist/db/tables/suppressions.table.js +53 -0
  80. package/dist/db/tables/suppressions.table.js.map +1 -0
  81. package/dist/db/tables/template-versions.table.d.ts +13 -0
  82. package/dist/db/tables/template-versions.table.js +59 -0
  83. package/dist/db/tables/template-versions.table.js.map +1 -0
  84. package/dist/db/tables/templates.table.d.ts +11 -0
  85. package/dist/db/tables/templates.table.js +45 -0
  86. package/dist/db/tables/templates.table.js.map +1 -1
  87. package/dist/db/tables/webhook-status.table.d.ts +14 -0
  88. package/dist/db/tables/webhook-status.table.js +58 -0
  89. package/dist/db/tables/webhook-status.table.js.map +1 -0
  90. package/dist/engine/changes.d.ts +12 -0
  91. package/dist/engine/changes.js +107 -0
  92. package/dist/engine/changes.js.map +1 -0
  93. package/dist/engine/conditions.js +0 -2
  94. package/dist/engine/conditions.js.map +1 -1
  95. package/dist/engine/coverage.d.ts +1 -0
  96. package/dist/engine/coverage.js +6 -2
  97. package/dist/engine/coverage.js.map +1 -1
  98. package/dist/engine/fallback-html.d.ts +1 -1
  99. package/dist/engine/fallback-html.js +51 -34
  100. package/dist/engine/fallback-html.js.map +1 -1
  101. package/dist/engine/index.d.ts +3 -0
  102. package/dist/engine/index.js +3 -0
  103. package/dist/engine/index.js.map +1 -1
  104. package/dist/engine/plain-text.d.ts +3 -0
  105. package/dist/engine/plain-text.js +60 -0
  106. package/dist/engine/plain-text.js.map +1 -0
  107. package/dist/engine/resolve.d.ts +2 -1
  108. package/dist/engine/resolve.js +17 -9
  109. package/dist/engine/resolve.js.map +1 -1
  110. package/dist/engine/system-variables.d.ts +10 -0
  111. package/dist/engine/system-variables.js +70 -0
  112. package/dist/engine/system-variables.js.map +1 -0
  113. package/dist/engine/tokens.d.ts +2 -1
  114. package/dist/engine/tokens.js +6 -2
  115. package/dist/engine/tokens.js.map +1 -1
  116. package/dist/hooks/tenant-export.js +13 -1
  117. package/dist/hooks/tenant-export.js.map +1 -1
  118. package/dist/implementations/dms-mailing/index.d.ts +1 -1
  119. package/dist/implementations/dms-mailing/index.js +2 -2
  120. package/dist/implementations/dms-mailing/index.js.map +1 -1
  121. package/dist/index.js +13 -11
  122. package/dist/index.js.map +1 -1
  123. package/dist/pages/editor.js +9 -2
  124. package/dist/pages/editor.js.map +1 -1
  125. package/dist/pages/index.d.ts +1 -0
  126. package/dist/pages/index.js +1 -0
  127. package/dist/pages/index.js.map +1 -1
  128. package/dist/pages/module.d.ts +2 -0
  129. package/dist/pages/module.js +26 -2
  130. package/dist/pages/module.js.map +1 -1
  131. package/dist/pages/overview.d.ts +2 -6
  132. package/dist/pages/overview.js +109 -52
  133. package/dist/pages/overview.js.map +1 -1
  134. package/dist/pages/sends.d.ts +3 -4
  135. package/dist/pages/sends.js +197 -60
  136. package/dist/pages/sends.js.map +1 -1
  137. package/dist/pages/settings/form.js +114 -53
  138. package/dist/pages/settings/form.js.map +1 -1
  139. package/dist/pages/settings/page.d.ts +0 -1
  140. package/dist/pages/settings/page.js +8 -11
  141. package/dist/pages/settings/page.js.map +1 -1
  142. package/dist/pages/suppressions.d.ts +9 -0
  143. package/dist/pages/suppressions.js +151 -0
  144. package/dist/pages/suppressions.js.map +1 -0
  145. package/dist/pages/templates.d.ts +0 -1
  146. package/dist/pages/templates.js +114 -40
  147. package/dist/pages/templates.js.map +1 -1
  148. package/dist/permissions.js +2 -2
  149. package/dist/permissions.js.map +1 -1
  150. package/dist/quick-actions.js +7 -4
  151. package/dist/quick-actions.js.map +1 -1
  152. package/dist/routes/events.d.ts +8 -5
  153. package/dist/routes/events.js +55 -24
  154. package/dist/routes/events.js.map +1 -1
  155. package/dist/routes/index.d.ts +2 -0
  156. package/dist/routes/index.js +2 -0
  157. package/dist/routes/index.js.map +1 -1
  158. package/dist/routes/metrics.d.ts +60 -23
  159. package/dist/routes/metrics.js +231 -83
  160. package/dist/routes/metrics.js.map +1 -1
  161. package/dist/routes/period.d.ts +2 -1
  162. package/dist/routes/period.js +9 -3
  163. package/dist/routes/period.js.map +1 -1
  164. package/dist/routes/sends.d.ts +49 -8
  165. package/dist/routes/sends.js +239 -24
  166. package/dist/routes/sends.js.map +1 -1
  167. package/dist/routes/settings.d.ts +44 -5
  168. package/dist/routes/settings.js +190 -15
  169. package/dist/routes/settings.js.map +1 -1
  170. package/dist/routes/suppressions.d.ts +16 -0
  171. package/dist/routes/suppressions.js +92 -0
  172. package/dist/routes/suppressions.js.map +1 -0
  173. package/dist/routes/template-versions.d.ts +28 -0
  174. package/dist/routes/template-versions.js +87 -0
  175. package/dist/routes/template-versions.js.map +1 -0
  176. package/dist/routes/templates.d.ts +60 -6
  177. package/dist/routes/templates.js +204 -35
  178. package/dist/routes/templates.js.map +1 -1
  179. package/dist/services/attention.d.ts +19 -6
  180. package/dist/services/attention.js +177 -12
  181. package/dist/services/attention.js.map +1 -1
  182. package/dist/services/catalog.d.ts +3 -0
  183. package/dist/services/catalog.js +25 -0
  184. package/dist/services/catalog.js.map +1 -0
  185. package/dist/services/categories.d.ts +8 -32
  186. package/dist/services/categories.js +14 -25
  187. package/dist/services/categories.js.map +1 -1
  188. package/dist/services/checked-send.d.ts +9 -0
  189. package/dist/services/checked-send.js +54 -0
  190. package/dist/services/checked-send.js.map +1 -0
  191. package/dist/services/events.d.ts +5 -0
  192. package/dist/services/events.js +102 -9
  193. package/dist/services/events.js.map +1 -1
  194. package/dist/services/metrics.d.ts +17 -6
  195. package/dist/services/metrics.js +57 -18
  196. package/dist/services/metrics.js.map +1 -1
  197. package/dist/services/notify.d.ts +18 -10
  198. package/dist/services/notify.js +82 -37
  199. package/dist/services/notify.js.map +1 -1
  200. package/dist/services/provider-banner.d.ts +14 -0
  201. package/dist/services/provider-banner.js +115 -0
  202. package/dist/services/provider-banner.js.map +1 -0
  203. package/dist/services/render.js.map +1 -1
  204. package/dist/services/send-stat-items.d.ts +7 -0
  205. package/dist/services/send-stat-items.js +127 -0
  206. package/dist/services/send-stat-items.js.map +1 -0
  207. package/dist/services/send-stats.d.ts +23 -0
  208. package/dist/services/send-stats.js +63 -0
  209. package/dist/services/send-stats.js.map +1 -0
  210. package/dist/services/send.d.ts +18 -3
  211. package/dist/services/send.js +81 -17
  212. package/dist/services/send.js.map +1 -1
  213. package/dist/services/settings.d.ts +5 -0
  214. package/dist/services/settings.js +28 -7
  215. package/dist/services/settings.js.map +1 -1
  216. package/dist/services/starters.d.ts +15 -0
  217. package/dist/services/starters.js +264 -0
  218. package/dist/services/starters.js.map +1 -0
  219. package/dist/services/suppressions.d.ts +20 -0
  220. package/dist/services/suppressions.js +53 -0
  221. package/dist/services/suppressions.js.map +1 -0
  222. package/dist/services/template-attention.d.ts +4 -0
  223. package/dist/services/template-attention.js +43 -0
  224. package/dist/services/template-attention.js.map +1 -0
  225. package/dist/services/template-stats.d.ts +27 -0
  226. package/dist/services/template-stats.js +59 -0
  227. package/dist/services/template-stats.js.map +1 -0
  228. package/dist/services/templates.d.ts +31 -4
  229. package/dist/services/templates.js +240 -21
  230. package/dist/services/templates.js.map +1 -1
  231. package/dist/services/webhook-status.d.ts +27 -0
  232. package/dist/services/webhook-status.js +134 -0
  233. package/dist/services/webhook-status.js.map +1 -0
  234. package/dist/types/blocks.d.ts +1 -0
  235. package/dist/types/index.d.ts +2 -0
  236. package/dist/types/index.js +2 -0
  237. package/dist/types/index.js.map +1 -1
  238. package/dist/types/sends.d.ts +10 -1
  239. package/dist/types/sends.js +35 -2
  240. package/dist/types/sends.js.map +1 -1
  241. package/dist/types/suppressions.d.ts +9 -0
  242. package/dist/types/suppressions.js +10 -0
  243. package/dist/types/suppressions.js.map +1 -0
  244. package/dist/types/webhook.d.ts +17 -0
  245. package/dist/types/webhook.js +3 -0
  246. package/dist/types/webhook.js.map +1 -0
  247. package/dist/validation/settings.schema.d.ts +144 -9
  248. package/dist/validation/settings.schema.js +113 -16
  249. package/dist/validation/settings.schema.js.map +1 -1
  250. package/dist/validation/templates.schema.d.ts +137 -10
  251. package/dist/validation/templates.schema.js +28 -2
  252. package/dist/validation/templates.schema.js.map +1 -1
  253. package/frontend-vue/app/build/components/FormFieldRow.vue +90 -0
  254. package/frontend-vue/app/build/components/FormRows.vue +26 -0
  255. package/frontend-vue/app/build/components/RequiredLegend.vue +11 -0
  256. package/frontend-vue/app/components/CategoriesInput.vue +323 -77
  257. package/frontend-vue/app/components/CategoryInput.vue +13 -14
  258. package/frontend-vue/app/components/CategoryLabel.vue +34 -0
  259. package/frontend-vue/app/components/DuplicateModal.vue +201 -0
  260. package/frontend-vue/app/components/Editor.vue +55 -20
  261. package/frontend-vue/app/components/FixAddressModal.vue +133 -0
  262. package/frontend-vue/app/components/FunnelCard.vue +146 -43
  263. package/frontend-vue/app/components/LocaleSelect.vue +98 -0
  264. package/frontend-vue/app/components/NewTemplateModal.vue +388 -102
  265. package/frontend-vue/app/components/NewTemplateSourceTile.vue +70 -0
  266. package/frontend-vue/app/components/OverviewGate.vue +220 -0
  267. package/frontend-vue/app/components/PageHeader.vue +98 -0
  268. package/frontend-vue/app/components/RealSendModal.vue +555 -0
  269. package/frontend-vue/app/components/RecipientsField.vue +41 -0
  270. package/frontend-vue/app/components/RenderingModal.vue +189 -0
  271. package/frontend-vue/app/components/RetentionInput.vue +212 -0
  272. package/frontend-vue/app/components/SendDrawer.vue +542 -209
  273. package/frontend-vue/app/components/StarterGallery.vue +153 -0
  274. package/frontend-vue/app/components/SystemVariables.vue +63 -0
  275. package/frontend-vue/app/components/TemplateCard.vue +218 -57
  276. package/frontend-vue/app/components/TemplateDrawer.vue +785 -274
  277. package/frontend-vue/app/components/TemplateGallery.vue +312 -56
  278. package/frontend-vue/app/components/TemplatePreviewFrame.vue +112 -22
  279. package/frontend-vue/app/components/TestSendModal.vue +266 -95
  280. package/frontend-vue/app/components/TokenText.vue +61 -10
  281. package/frontend-vue/app/components/VariablePath.vue +20 -0
  282. package/frontend-vue/app/components/VersionHistory.vue +196 -0
  283. package/frontend-vue/app/components/VersionPreview.vue +164 -0
  284. package/frontend-vue/app/components/WebhookStatus.vue +130 -0
  285. package/frontend-vue/app/components/blocks/Button.vue +1 -1
  286. package/frontend-vue/app/components/blocks/Code.vue +1 -1
  287. package/frontend-vue/app/components/blocks/Footer.vue +56 -7
  288. package/frontend-vue/app/components/blocks/Heading.vue +1 -1
  289. package/frontend-vue/app/components/blocks/Hero.vue +13 -7
  290. package/frontend-vue/app/components/blocks/List.vue +50 -6
  291. package/frontend-vue/app/components/blocks/Paragraph.vue +1 -1
  292. package/frontend-vue/app/components/blocks/Total.vue +3 -3
  293. package/frontend-vue/app/components/editor/EditorAutocompleteMenu.vue +50 -0
  294. package/frontend-vue/app/components/editor/EditorBlock.vue +37 -90
  295. package/frontend-vue/app/components/editor/EditorBlockFields.vue +140 -0
  296. package/frontend-vue/app/components/editor/EditorBlockInspector.vue +92 -0
  297. package/frontend-vue/app/components/editor/EditorBlockList.vue +32 -25
  298. package/frontend-vue/app/components/editor/EditorBlockPicker.vue +58 -0
  299. package/frontend-vue/app/components/editor/EditorBlockTools.vue +71 -0
  300. package/frontend-vue/app/components/editor/EditorCanvas.vue +105 -70
  301. package/frontend-vue/app/components/editor/EditorCompare.vue +211 -0
  302. package/frontend-vue/app/components/editor/EditorConditionFields.vue +56 -46
  303. package/frontend-vue/app/components/editor/EditorConditionInspector.vue +54 -0
  304. package/frontend-vue/app/components/editor/EditorEnvelope.vue +77 -0
  305. package/frontend-vue/app/components/editor/EditorIfBlock.vue +36 -86
  306. package/frontend-vue/app/components/editor/EditorInsertPoint.vue +64 -0
  307. package/frontend-vue/app/components/editor/EditorInspector.vue +80 -0
  308. package/frontend-vue/app/components/editor/EditorLeftPane.vue +103 -0
  309. package/frontend-vue/app/components/editor/EditorLifecycle.vue +67 -0
  310. package/frontend-vue/app/components/editor/EditorLoading.vue +23 -0
  311. package/frontend-vue/app/components/editor/EditorLocaleSwitch.vue +61 -0
  312. package/frontend-vue/app/components/editor/EditorMissingLocale.vue +72 -48
  313. package/frontend-vue/app/components/editor/EditorMissingPolicy.vue +42 -0
  314. package/frontend-vue/app/components/editor/EditorNotice.vue +127 -0
  315. package/frontend-vue/app/components/editor/EditorOutline.vue +65 -0
  316. package/frontend-vue/app/components/editor/EditorPalette.vue +95 -42
  317. package/frontend-vue/app/components/editor/EditorPaletteGroup.vue +66 -0
  318. package/frontend-vue/app/components/editor/EditorPublishReview.vue +102 -0
  319. package/frontend-vue/app/components/editor/EditorSaveState.vue +88 -0
  320. package/frontend-vue/app/components/editor/EditorShortcutHint.vue +17 -0
  321. package/frontend-vue/app/components/editor/EditorSimChip.vue +51 -0
  322. package/frontend-vue/app/components/editor/EditorTemplatePane.vue +131 -0
  323. package/frontend-vue/app/components/editor/EditorTestDataPane.vue +98 -0
  324. package/frontend-vue/app/components/editor/EditorTitle.vue +82 -0
  325. package/frontend-vue/app/components/editor/EditorTokenField.vue +224 -0
  326. package/frontend-vue/app/components/editor/EditorToolbar.vue +34 -134
  327. package/frontend-vue/app/components/editor/EditorVariableForm.vue +100 -0
  328. package/frontend-vue/app/components/editor/EditorVariableRow.vue +87 -0
  329. package/frontend-vue/app/components/editor/EditorVariablesPane.vue +161 -0
  330. package/frontend-vue/app/components/editor/EditorViewSwitches.vue +76 -0
  331. package/frontend-vue/app/components/editor/EditorVisibility.vue +101 -0
  332. package/frontend-vue/app/components/editor/EditorWorkspace.vue +62 -149
  333. package/frontend-vue/app/composables/useBlockContext.ts +16 -0
  334. package/frontend-vue/app/composables/useEditorActions.ts +381 -0
  335. package/frontend-vue/app/composables/useEditorAnnouncements.ts +51 -0
  336. package/frontend-vue/app/composables/useEditorContext.ts +30 -23
  337. package/frontend-vue/app/composables/useEditorSession.ts +299 -0
  338. package/frontend-vue/app/composables/useEditorShortcuts.ts +81 -0
  339. package/frontend-vue/app/composables/useLocalizedDefaults.ts +17 -0
  340. package/frontend-vue/app/composables/useMailingApi.ts +142 -27
  341. package/frontend-vue/app/composables/useOverviewSetup.ts +13 -0
  342. package/frontend-vue/app/composables/useReplayToast.ts +72 -0
  343. package/frontend-vue/app/composables/useTemplateCategories.ts +35 -0
  344. package/frontend-vue/app/composables/useTemplateDrawer.ts +196 -62
  345. package/frontend-vue/app/composables/useTemplateEditor.ts +318 -86
  346. package/frontend-vue/app/emails/EmailMailingTemplate.vue +16 -4
  347. package/frontend-vue/app/emails/components/MailingBlocks.vue +96 -38
  348. package/frontend-vue/app/emails/components/MailingTextLines.vue +20 -0
  349. package/frontend-vue/app/plugins/column-displays.ts +65 -0
  350. package/frontend-vue/app/plugins/{template-gallery-display.client.ts → template-gallery-display.ts} +1 -1
  351. package/frontend-vue/app/types/mailing.ts +193 -18
  352. package/frontend-vue/app/utils/api-error.ts +33 -0
  353. package/frontend-vue/app/utils/autocomplete.ts +120 -0
  354. package/frontend-vue/app/utils/blocks/components.ts +10 -10
  355. package/frontend-vue/app/utils/blocks/dnd.ts +7 -3
  356. package/frontend-vue/app/utils/blocks/factory.ts +199 -38
  357. package/frontend-vue/app/utils/blocks/fields.ts +39 -19
  358. package/frontend-vue/app/utils/blocks/index.ts +1 -0
  359. package/frontend-vue/app/utils/blocks/outline.ts +91 -0
  360. package/frontend-vue/app/utils/blocks/tree.ts +114 -2
  361. package/frontend-vue/app/utils/categories.ts +201 -15
  362. package/frontend-vue/app/utils/changes.ts +152 -0
  363. package/frontend-vue/app/utils/conditions.ts +25 -19
  364. package/frontend-vue/app/utils/editor-checks.ts +266 -0
  365. package/frontend-vue/app/utils/editor-route.ts +43 -4
  366. package/frontend-vue/app/utils/funnel.ts +74 -0
  367. package/frontend-vue/app/utils/gallery.ts +179 -1
  368. package/frontend-vue/app/utils/history.ts +66 -0
  369. package/frontend-vue/app/utils/lifecycle.ts +185 -0
  370. package/frontend-vue/app/utils/locales.ts +107 -2
  371. package/frontend-vue/app/utils/overview-setup.ts +83 -0
  372. package/frontend-vue/app/utils/pages.ts +5 -0
  373. package/frontend-vue/app/utils/paths.ts +51 -15
  374. package/frontend-vue/app/utils/period.ts +62 -22
  375. package/frontend-vue/app/utils/problems.ts +181 -0
  376. package/frontend-vue/app/utils/recipients.ts +49 -0
  377. package/frontend-vue/app/utils/rendering-title.ts +23 -0
  378. package/frontend-vue/app/utils/send-actions.ts +207 -0
  379. package/frontend-vue/app/utils/send-link.ts +28 -0
  380. package/frontend-vue/app/utils/send-snippet.ts +97 -0
  381. package/frontend-vue/app/utils/send-stats.ts +24 -0
  382. package/frontend-vue/app/utils/send-values.ts +156 -0
  383. package/frontend-vue/app/utils/shortcuts.ts +113 -0
  384. package/frontend-vue/app/utils/slug.ts +82 -0
  385. package/frontend-vue/app/utils/starters.ts +23 -0
  386. package/frontend-vue/app/utils/suppression.ts +112 -0
  387. package/frontend-vue/app/utils/system-variables.ts +165 -0
  388. package/frontend-vue/app/utils/template-attention.ts +104 -0
  389. package/frontend-vue/app/utils/timeline.ts +221 -19
  390. package/frontend-vue/app/utils/tokens.ts +115 -13
  391. package/frontend-vue/app/utils/variable-types.ts +23 -0
  392. package/frontend-vue/app/utils/variables.ts +208 -1
  393. package/frontend-vue/app/utils/version-history.ts +53 -0
  394. package/frontend-vue/app/utils/webhook-status.ts +170 -0
  395. package/frontend-vue/dms.frontend.ts +19 -11
  396. package/frontend-vue/i18n/locales/mailing-en-GB.json +1173 -161
  397. package/frontend-vue/i18n/locales/mailing-fr-FR.json +1220 -208
  398. package/frontend-vue/package.json +4 -1
  399. package/frontend-vue/pnpm-lock.yaml +46 -161
  400. package/frontend-vue/tests/api-error.test.ts +27 -0
  401. package/frontend-vue/tests/autocomplete.test.ts +84 -0
  402. package/frontend-vue/tests/block-components.test.ts +1 -1
  403. package/frontend-vue/tests/block-fields.test.ts +12 -2
  404. package/frontend-vue/tests/blocks-factory.test.ts +46 -0
  405. package/frontend-vue/tests/blocks-outline.test.ts +102 -0
  406. package/frontend-vue/tests/blocks-tree.test.ts +28 -1
  407. package/frontend-vue/tests/categories-input.test.ts +139 -0
  408. package/frontend-vue/tests/categories.test.ts +148 -15
  409. package/frontend-vue/tests/changes.test.ts +136 -0
  410. package/frontend-vue/tests/column-displays.test.ts +73 -0
  411. package/frontend-vue/tests/conditions.test.ts +49 -19
  412. package/frontend-vue/tests/dnd.test.ts +10 -0
  413. package/frontend-vue/tests/editor-block-list.test.ts +24 -2
  414. package/frontend-vue/tests/editor-checks.test.ts +178 -0
  415. package/frontend-vue/tests/editor-route.test.ts +37 -9
  416. package/frontend-vue/tests/editor-session.test.ts +206 -0
  417. package/frontend-vue/tests/footer-block.test.ts +88 -0
  418. package/frontend-vue/tests/funnel-card.test.ts +116 -0
  419. package/frontend-vue/tests/funnel.test.ts +81 -0
  420. package/frontend-vue/tests/gallery.test.ts +124 -2
  421. package/frontend-vue/tests/helpers/nuxt-globals.ts +232 -0
  422. package/frontend-vue/tests/history.test.ts +48 -0
  423. package/frontend-vue/tests/i18n-editor.test.ts +38 -0
  424. package/frontend-vue/tests/i18n.test.ts +55 -0
  425. package/frontend-vue/tests/lifecycle.test.ts +145 -0
  426. package/frontend-vue/tests/locales.test.ts +60 -0
  427. package/frontend-vue/tests/localized-defaults.test.ts +54 -0
  428. package/frontend-vue/tests/new-template-modal.test.ts +210 -0
  429. package/frontend-vue/tests/overview-gate.test.ts +146 -0
  430. package/frontend-vue/tests/overview-setup.test.ts +78 -0
  431. package/frontend-vue/tests/paths.test.ts +23 -0
  432. package/frontend-vue/tests/period.test.ts +59 -10
  433. package/frontend-vue/tests/problems.test.ts +153 -0
  434. package/frontend-vue/tests/recipients.test.ts +47 -0
  435. package/frontend-vue/tests/rendering-title.test.ts +24 -0
  436. package/frontend-vue/tests/replay-toast.test.ts +67 -0
  437. package/frontend-vue/tests/retention-input.test.ts +129 -0
  438. package/frontend-vue/tests/send-actions.test.ts +128 -0
  439. package/frontend-vue/tests/send-modals.test.ts +415 -0
  440. package/frontend-vue/tests/send-snippet.test.ts +51 -0
  441. package/frontend-vue/tests/send-stats.test.ts +12 -0
  442. package/frontend-vue/tests/send-values.test.ts +119 -0
  443. package/frontend-vue/tests/shortcuts.test.ts +75 -0
  444. package/frontend-vue/tests/slug.test.ts +88 -0
  445. package/frontend-vue/tests/suppression.test.ts +159 -0
  446. package/frontend-vue/tests/system-variables.test.ts +133 -0
  447. package/frontend-vue/tests/template-attention.test.ts +123 -0
  448. package/frontend-vue/tests/template-drawer.test.ts +449 -0
  449. package/frontend-vue/tests/template-editor.test.ts +218 -9
  450. package/frontend-vue/tests/template-gallery-display.test.ts +32 -0
  451. package/frontend-vue/tests/template-gallery.test.ts +370 -0
  452. package/frontend-vue/tests/template-overlays.test.ts +125 -0
  453. package/frontend-vue/tests/timeline.test.ts +175 -20
  454. package/frontend-vue/tests/tokens.test.ts +79 -1
  455. package/frontend-vue/tests/variable-path.test.ts +22 -0
  456. package/frontend-vue/tests/variable-types.test.ts +20 -0
  457. package/frontend-vue/tests/variables.test.ts +90 -2
  458. package/frontend-vue/tests/version-history.test.ts +41 -0
  459. package/frontend-vue/tests/webhook-status.test.ts +108 -0
  460. package/package.json +11 -9
  461. package/src/automation/email-event-trigger.ts +3 -3
  462. package/src/automation/send-template-action.ts +7 -5
  463. package/src/config.ts +3 -1
  464. package/src/constants.ts +18 -2
  465. package/src/crons/attention.ts +21 -0
  466. package/src/crons/index.ts +2 -0
  467. package/src/crons/jobs.ts +35 -0
  468. package/src/crons/retention.ts +7 -22
  469. package/src/data/displays.ts +18 -0
  470. package/src/data/index.ts +1 -0
  471. package/src/data/locale-fallback.ts +26 -0
  472. package/src/data/read-only-routes.ts +14 -0
  473. package/src/data/sends-table.ts +204 -19
  474. package/src/data/suppressions-table.ts +127 -0
  475. package/src/data/templates-table.ts +295 -63
  476. package/src/data-types/categories-type.ts +4 -5
  477. package/src/data-types/category-type.ts +1 -1
  478. package/src/db/models/index.ts +3 -0
  479. package/src/db/models/sends.model.ts +102 -1
  480. package/src/db/models/settings.model.ts +2 -12
  481. package/src/db/models/suppressions.model.ts +53 -0
  482. package/src/db/models/template-versions.model.ts +23 -0
  483. package/src/db/models/templates.model.ts +17 -0
  484. package/src/db/models/webhook-status.model.ts +25 -0
  485. package/src/db/tables/index.ts +3 -0
  486. package/src/db/tables/send-events.table.ts +6 -2
  487. package/src/db/tables/sends.table.ts +37 -2
  488. package/src/db/tables/suppressions.table.ts +46 -0
  489. package/src/db/tables/template-versions.table.ts +44 -0
  490. package/src/db/tables/templates.table.ts +67 -5
  491. package/src/db/tables/webhook-status.table.ts +44 -0
  492. package/src/engine/changes.ts +198 -0
  493. package/src/engine/conditions.ts +1 -7
  494. package/src/engine/coverage.ts +7 -2
  495. package/src/engine/fallback-html.ts +80 -42
  496. package/src/engine/index.ts +3 -0
  497. package/src/engine/plain-text.ts +86 -0
  498. package/src/engine/resolve.ts +49 -15
  499. package/src/engine/system-variables.ts +124 -0
  500. package/src/engine/tokens.ts +13 -1
  501. package/src/hooks/tenant-export.ts +21 -2
  502. package/src/implementations/dms-mailing/index.ts +1 -1
  503. package/src/index.ts +29 -13
  504. package/src/pages/editor.ts +13 -2
  505. package/src/pages/index.ts +6 -3
  506. package/src/pages/module.ts +31 -2
  507. package/src/pages/overview.ts +187 -128
  508. package/src/pages/sends.ts +271 -112
  509. package/src/pages/settings/form.ts +144 -54
  510. package/src/pages/settings/page.ts +9 -17
  511. package/src/pages/suppressions.ts +157 -0
  512. package/src/pages/templates.ts +139 -42
  513. package/src/permissions.ts +6 -4
  514. package/src/quick-actions.ts +12 -5
  515. package/src/routes/events.ts +110 -33
  516. package/src/routes/index.ts +2 -0
  517. package/src/routes/metrics.ts +348 -98
  518. package/src/routes/period.ts +23 -6
  519. package/src/routes/sends.ts +413 -41
  520. package/src/routes/settings.ts +291 -26
  521. package/src/routes/suppressions.ts +87 -0
  522. package/src/routes/template-versions.ts +116 -0
  523. package/src/routes/templates.ts +334 -48
  524. package/src/services/attention.ts +283 -30
  525. package/src/services/catalog.ts +28 -0
  526. package/src/services/categories.ts +28 -27
  527. package/src/services/checked-send.ts +123 -0
  528. package/src/services/events.ts +174 -20
  529. package/src/services/metrics.ts +137 -28
  530. package/src/services/notify.ts +126 -44
  531. package/src/services/provider-banner.ts +165 -0
  532. package/src/services/render.ts +1 -0
  533. package/src/services/send-stat-items.ts +160 -0
  534. package/src/services/send-stats.ts +122 -0
  535. package/src/services/send.ts +193 -31
  536. package/src/services/settings.ts +52 -8
  537. package/src/services/starters.ts +383 -0
  538. package/src/services/suppressions.ts +116 -0
  539. package/src/services/template-attention.ts +82 -0
  540. package/src/services/template-stats.ts +97 -0
  541. package/src/services/templates.ts +400 -37
  542. package/src/services/webhook-status.ts +202 -0
  543. package/src/types/blocks.ts +6 -0
  544. package/src/types/index.ts +2 -0
  545. package/src/types/sends.ts +64 -2
  546. package/src/types/suppressions.ts +27 -0
  547. package/src/types/webhook.ts +29 -0
  548. package/src/validation/settings.schema.ts +173 -17
  549. package/src/validation/templates.schema.ts +53 -1
  550. package/dist/antelope-modules.d.ts +0 -32
  551. package/dist/antelope-modules.js +0 -36
  552. package/dist/antelope-modules.js.map +0 -1
  553. package/dist/test/antelope.test.d.ts +0 -2
  554. package/dist/test/antelope.test.js +0 -180
  555. package/dist/test/antelope.test.js.map +0 -1
  556. package/dist/test/helpers/http.d.ts +0 -4
  557. package/dist/test/helpers/http.js +0 -28
  558. package/dist/test/helpers/http.js.map +0 -1
  559. package/dist/test/helpers/owner.d.ts +0 -5
  560. package/dist/test/helpers/owner.js +0 -30
  561. package/dist/test/helpers/owner.js.map +0 -1
  562. package/dist/test/integration/events.test.d.ts +0 -1
  563. package/dist/test/integration/events.test.js +0 -143
  564. package/dist/test/integration/events.test.js.map +0 -1
  565. package/dist/test/integration/retention.test.d.ts +0 -1
  566. package/dist/test/integration/retention.test.js +0 -291
  567. package/dist/test/integration/retention.test.js.map +0 -1
  568. package/dist/test/integration/sends.test.d.ts +0 -1
  569. package/dist/test/integration/sends.test.js +0 -210
  570. package/dist/test/integration/sends.test.js.map +0 -1
  571. package/dist/test/integration/settings.test.d.ts +0 -1
  572. package/dist/test/integration/settings.test.js +0 -137
  573. package/dist/test/integration/settings.test.js.map +0 -1
  574. package/dist/test/integration/setup.test.d.ts +0 -1
  575. package/dist/test/integration/setup.test.js +0 -32
  576. package/dist/test/integration/setup.test.js.map +0 -1
  577. package/dist/test/integration/templates.test.d.ts +0 -1
  578. package/dist/test/integration/templates.test.js +0 -233
  579. package/dist/test/integration/templates.test.js.map +0 -1
  580. package/dist/test/integration/tenant-export.test.d.ts +0 -1
  581. package/dist/test/integration/tenant-export.test.js +0 -85
  582. package/dist/test/integration/tenant-export.test.js.map +0 -1
  583. package/dist/test/unit/attention.test.d.ts +0 -1
  584. package/dist/test/unit/attention.test.js +0 -109
  585. package/dist/test/unit/attention.test.js.map +0 -1
  586. package/dist/test/unit/automation.test.d.ts +0 -1
  587. package/dist/test/unit/automation.test.js +0 -104
  588. package/dist/test/unit/automation.test.js.map +0 -1
  589. package/dist/test/unit/categories.test.d.ts +0 -1
  590. package/dist/test/unit/categories.test.js +0 -38
  591. package/dist/test/unit/categories.test.js.map +0 -1
  592. package/dist/test/unit/engine/conditions.test.d.ts +0 -1
  593. package/dist/test/unit/engine/conditions.test.js +0 -23
  594. package/dist/test/unit/engine/conditions.test.js.map +0 -1
  595. package/dist/test/unit/engine/coverage.test.d.ts +0 -1
  596. package/dist/test/unit/engine/coverage.test.js +0 -106
  597. package/dist/test/unit/engine/coverage.test.js.map +0 -1
  598. package/dist/test/unit/engine/fallback-html.test.d.ts +0 -1
  599. package/dist/test/unit/engine/fallback-html.test.js +0 -78
  600. package/dist/test/unit/engine/fallback-html.test.js.map +0 -1
  601. package/dist/test/unit/engine/paths.test.d.ts +0 -1
  602. package/dist/test/unit/engine/paths.test.js +0 -33
  603. package/dist/test/unit/engine/paths.test.js.map +0 -1
  604. package/dist/test/unit/engine/resolve.test.d.ts +0 -1
  605. package/dist/test/unit/engine/resolve.test.js +0 -157
  606. package/dist/test/unit/engine/resolve.test.js.map +0 -1
  607. package/dist/test/unit/engine/tokens.test.d.ts +0 -1
  608. package/dist/test/unit/engine/tokens.test.js +0 -26
  609. package/dist/test/unit/engine/tokens.test.js.map +0 -1
  610. package/dist/test/unit/events.test.d.ts +0 -1
  611. package/dist/test/unit/events.test.js +0 -13
  612. package/dist/test/unit/events.test.js.map +0 -1
  613. package/dist/test/unit/metrics.test.d.ts +0 -1
  614. package/dist/test/unit/metrics.test.js +0 -108
  615. package/dist/test/unit/metrics.test.js.map +0 -1
  616. package/dist/test/unit/notify.test.d.ts +0 -1
  617. package/dist/test/unit/notify.test.js +0 -49
  618. package/dist/test/unit/notify.test.js.map +0 -1
  619. package/dist/test/unit/period.test.d.ts +0 -1
  620. package/dist/test/unit/period.test.js +0 -61
  621. package/dist/test/unit/period.test.js.map +0 -1
  622. package/dist/test/unit/permissions.test.d.ts +0 -1
  623. package/dist/test/unit/permissions.test.js +0 -27
  624. package/dist/test/unit/permissions.test.js.map +0 -1
  625. package/dist/test/unit/provider.test.d.ts +0 -1
  626. package/dist/test/unit/provider.test.js +0 -47
  627. package/dist/test/unit/provider.test.js.map +0 -1
  628. package/dist/test/unit/realtime.test.d.ts +0 -1
  629. package/dist/test/unit/realtime.test.js +0 -13
  630. package/dist/test/unit/realtime.test.js.map +0 -1
  631. package/dist/test/unit/render.test.d.ts +0 -1
  632. package/dist/test/unit/render.test.js +0 -23
  633. package/dist/test/unit/render.test.js.map +0 -1
  634. package/dist/test/unit/retention.test.d.ts +0 -1
  635. package/dist/test/unit/retention.test.js +0 -66
  636. package/dist/test/unit/retention.test.js.map +0 -1
  637. package/dist/test/unit/send-batch.test.d.ts +0 -1
  638. package/dist/test/unit/send-batch.test.js +0 -46
  639. package/dist/test/unit/send-batch.test.js.map +0 -1
  640. package/dist/test/unit/send.test.d.ts +0 -1
  641. package/dist/test/unit/send.test.js +0 -120
  642. package/dist/test/unit/send.test.js.map +0 -1
  643. package/dist/test/unit/smoke.test.d.ts +0 -1
  644. package/dist/test/unit/smoke.test.js +0 -10
  645. package/dist/test/unit/smoke.test.js.map +0 -1
  646. package/dist/test/unit/status.test.d.ts +0 -1
  647. package/dist/test/unit/status.test.js +0 -16
  648. package/dist/test/unit/status.test.js.map +0 -1
  649. package/dist/test/unit/templates.test.d.ts +0 -1
  650. package/dist/test/unit/templates.test.js +0 -48
  651. package/dist/test/unit/templates.test.js.map +0 -1
  652. package/dist/test/unit/tenant-export.test.d.ts +0 -1
  653. package/dist/test/unit/tenant-export.test.js +0 -59
  654. package/dist/test/unit/tenant-export.test.js.map +0 -1
  655. package/dist/test/unit/webhook-secret.test.d.ts +0 -1
  656. package/dist/test/unit/webhook-secret.test.js +0 -43
  657. package/dist/test/unit/webhook-secret.test.js.map +0 -1
  658. package/frontend-vue/app/components/AttentionCard.vue +0 -78
  659. package/frontend-vue/app/components/HtmlPreviewModal.vue +0 -20
  660. package/frontend-vue/app/components/ProviderChip.vue +0 -39
  661. package/frontend-vue/app/components/editor/EditorBlockSettings.vue +0 -186
  662. package/frontend-vue/app/components/editor/EditorDataPanel.vue +0 -130
  663. package/frontend-vue/app/components/editor/EditorDraftBar.vue +0 -35
  664. package/frontend-vue/app/components/editor/EditorRail.vue +0 -58
  665. package/frontend-vue/app/components/editor/EditorSubjectSettings.vue +0 -37
  666. package/frontend-vue/app/components/editor/EditorTemplateSettings.vue +0 -87
  667. package/frontend-vue/app/components/editor/EditorTestSendModal.vue +0 -66
  668. package/frontend-vue/app/components/editor/EditorVariables.vue +0 -163
  669. package/frontend-vue/app/composables/useMailingPeriodData.ts +0 -39
  670. package/src/antelope-modules.ts +0 -48
  671. package/src/test/antelope.test.ts +0 -190
  672. package/src/test/helpers/http.ts +0 -23
  673. package/src/test/helpers/owner.ts +0 -37
  674. package/src/test/integration/events.test.ts +0 -159
  675. package/src/test/integration/retention.test.ts +0 -363
  676. package/src/test/integration/sends.test.ts +0 -233
  677. package/src/test/integration/settings.test.ts +0 -128
  678. package/src/test/integration/setup.test.ts +0 -35
  679. package/src/test/integration/templates.test.ts +0 -272
  680. package/src/test/integration/tenant-export.test.ts +0 -125
  681. package/src/test/unit/attention.test.ts +0 -129
  682. package/src/test/unit/automation.test.ts +0 -130
  683. package/src/test/unit/categories.test.ts +0 -51
  684. package/src/test/unit/engine/conditions.test.ts +0 -35
  685. package/src/test/unit/engine/coverage.test.ts +0 -118
  686. package/src/test/unit/engine/fallback-html.test.ts +0 -86
  687. package/src/test/unit/engine/paths.test.ts +0 -39
  688. package/src/test/unit/engine/resolve.test.ts +0 -176
  689. package/src/test/unit/engine/tokens.test.ts +0 -26
  690. package/src/test/unit/events.test.ts +0 -19
  691. package/src/test/unit/metrics.test.ts +0 -170
  692. package/src/test/unit/notify.test.ts +0 -71
  693. package/src/test/unit/period.test.ts +0 -91
  694. package/src/test/unit/permissions.test.ts +0 -30
  695. package/src/test/unit/provider.test.ts +0 -57
  696. package/src/test/unit/realtime.test.ts +0 -11
  697. package/src/test/unit/render.test.ts +0 -23
  698. package/src/test/unit/retention.test.ts +0 -91
  699. package/src/test/unit/send-batch.test.ts +0 -72
  700. package/src/test/unit/send.test.ts +0 -161
  701. package/src/test/unit/smoke.test.ts +0 -8
  702. package/src/test/unit/status.test.ts +0 -14
  703. package/src/test/unit/templates.test.ts +0 -52
  704. package/src/test/unit/tenant-export.test.ts +0 -88
  705. package/src/test/unit/webhook-secret.test.ts +0 -51
@@ -3,6 +3,7 @@ import {
3
3
  HTTPResult,
4
4
  type RequestContext,
5
5
  } from "@antelopejs/interface-api";
6
+ import { assertValidation } from "@antelopejs/interface-api-util";
6
7
  import {
7
8
  DataController,
8
9
  type DataControllerCallback,
@@ -11,6 +12,8 @@ import {
11
12
  import {
12
13
  Access,
13
14
  AccessMode,
15
+ Filter,
16
+ type FilterFunction,
14
17
  Listable,
15
18
  Mandatory,
16
19
  ModelReference,
@@ -28,23 +31,61 @@ import {
28
31
  TableViewRoutes,
29
32
  } from "@antelopejs/interface-dms/base";
30
33
  import { DefaultDataTypes } from "@antelopejs/interface-dms/base/data-types/default-types";
34
+ import { DefaultDisplays } from "@antelopejs/interface-dms/base/table-view";
31
35
  import { ReadonlyBehaviorType } from "@antelopejs/interface-dms/base/types";
32
- import { HTTP_CONFLICT, HTTP_NOT_FOUND, TABLES_BASE_PATH } from "../constants";
36
+ import {
37
+ ARCHIVED_STATUS,
38
+ HTTP_BAD_REQUEST,
39
+ HTTP_CONFLICT,
40
+ HTTP_NOT_FOUND,
41
+ HTTP_UNPROCESSABLE,
42
+ MAX_NAME_LENGTH,
43
+ MAX_SLUG_LENGTH,
44
+ TABLES_BASE_PATH,
45
+ } from "../constants";
33
46
  import { TemplateCategoryType } from "../data-types";
34
- import { MailingTemplate, TemplateModel } from "../db";
35
- import { getSettings } from "../services/settings";
36
- import { displayName, initializeTemplate } from "../services/templates";
47
+ import { footerForCategory } from "../engine";
48
+ import {
49
+ MailingTemplate,
50
+ SendModel,
51
+ TemplateModel,
52
+ TemplateVersionModel,
53
+ } from "../db";
54
+ import { findCategory, getSettings } from "../services/settings";
55
+ import { findStarter, starterFields } from "../services/starters";
56
+ import { refreshTemplateAttention } from "../services/template-attention";
57
+ import {
58
+ blankContent,
59
+ blankFields,
60
+ contentFieldsOf,
61
+ displayName,
62
+ initializeTemplate,
63
+ } from "../services/templates";
64
+ import type { TemplateCategory } from "../types";
37
65
 
38
66
  import { createTemplateSchema } from "../validation/templates.schema";
39
67
  import { parseBody } from "./body";
68
+ import { CategoryDisplay, LocaleChipsDisplay } from "./displays";
40
69
 
41
70
  const DUPLICATE_SLUG = "$dms_mailing.errors.duplicate_slug";
42
71
  const SLUG_IMMUTABLE = "$dms_mailing.errors.slug_immutable";
43
72
  const SOURCE_NOT_FOUND = "$dms_mailing.errors.template_not_found";
73
+ const STARTER_NOT_FOUND = "$dms_mailing.errors.starter_not_found";
74
+ const INVALID_TEMPLATE = "$dms_mailing.errors.invalid_template";
75
+ const UNKNOWN_CATEGORY = "$dms_mailing.errors.unknown_category";
76
+ const TEMPLATE_IN_USE = "$dms_mailing.errors.template_in_use";
77
+ const TEMPLATE_WAS_SENT = "$dms_mailing.errors.template_was_sent";
78
+ const MIN_NAME_LENGTH = 1;
44
79
  const CREATE_ONLY = { edit: ReadonlyBehaviorType.disabled };
45
- // Read-only columns render as empty dashes on the create form; the four fields
46
- // a template is actually created from are the only ones worth asking for.
47
- const NEVER_ON_CREATE = { new: ReadonlyBehaviorType.hidden };
80
+ /**
81
+ * Read-only columns render as empty dashes on the create form and as raw
82
+ * internal values on the edit one: a template's name, slug and category are
83
+ * the only fields worth asking for.
84
+ */
85
+ const NEVER_IN_FORMS = {
86
+ new: ReadonlyBehaviorType.hidden,
87
+ edit: ReadonlyBehaviorType.hidden,
88
+ };
48
89
 
49
90
  const STATUS_ITEMS = ["draft", "live", "archived"].map((value) => ({
50
91
  value,
@@ -62,6 +103,85 @@ async function resolveSource(
62
103
  return source;
63
104
  }
64
105
 
106
+ interface TemplateSeed {
107
+ sourceTemplateId?: string;
108
+ starterId?: string;
109
+ category?: string;
110
+ }
111
+
112
+ /** The content fields a creation starts from: a copy, a starter or a blank page. */
113
+ async function seedFields(
114
+ tenantId: string,
115
+ seed: TemplateSeed,
116
+ fallbackLocale: string,
117
+ ): Promise<Partial<MailingTemplate>> {
118
+ const source = await resolveSource(tenantId, seed.sourceTemplateId);
119
+ if (source) return contentFieldsOf(source);
120
+ if (!seed.starterId) {
121
+ return blankFields(
122
+ footerForCategory(blankContent(fallbackLocale), seed.category),
123
+ );
124
+ }
125
+ const starter = findStarter(seed.starterId);
126
+ if (!starter) throw new HTTPResult(HTTP_NOT_FOUND, STARTER_NOT_FOUND);
127
+ return starterFields(starter, fallbackLocale);
128
+ }
129
+
130
+ const isBlank = (value: unknown) =>
131
+ value === undefined || value === null || value === "";
132
+
133
+ /** A category is left empty or picked among the ones the settings declare. */
134
+ function assertKnownCategory(
135
+ categories: TemplateCategory[],
136
+ category: unknown,
137
+ ): void {
138
+ if (isBlank(category)) return;
139
+ if (typeof category === "string" && findCategory(categories, category)) {
140
+ return;
141
+ }
142
+ throw new HTTPResult(HTTP_UNPROCESSABLE, UNKNOWN_CATEGORY);
143
+ }
144
+
145
+ const creationTime = (template: MailingTemplate) =>
146
+ new Date(template.createdAt).getTime();
147
+
148
+ /**
149
+ * Nothing in the database keeps slugs unique, so two creations racing past
150
+ * the free-slug check both land: the earliest one stays, a later one is
151
+ * removed again and refused like any taken slug.
152
+ */
153
+ async function assertFirstOfSlug(
154
+ model: TemplateModel,
155
+ slug: string,
156
+ templateId: string,
157
+ ): Promise<void> {
158
+ const [first] = (await model.getBy("slug", slug)).sort(
159
+ (left, right) =>
160
+ creationTime(left) - creationTime(right) ||
161
+ left._id.localeCompare(right._id),
162
+ );
163
+ if (!first || first._id === templateId) return;
164
+ await model.delete(templateId);
165
+ throw new HTTPResult(HTTP_CONFLICT, DUPLICATE_SLUG);
166
+ }
167
+
168
+ /** The creation's seed, and the body to store: slug and name trimmed. */
169
+ function parseCreation(body: Buffer | string) {
170
+ const raw = parseBody(body);
171
+ const seed = assertValidation(
172
+ raw,
173
+ (value) => createTemplateSchema.parse(value),
174
+ () => INVALID_TEMPLATE,
175
+ HTTP_BAD_REQUEST,
176
+ );
177
+ const normalized = JSON.stringify({
178
+ ...raw,
179
+ slug: seed.slug,
180
+ name: seed.name,
181
+ });
182
+ return { seed, normalized };
183
+ }
184
+
65
185
  function withSeeding(base: DataControllerCallback): DataControllerCallback {
66
186
  return {
67
187
  ...base,
@@ -72,82 +192,125 @@ function withSeeding(base: DataControllerCallback): DataControllerCallback {
72
192
  body: Buffer | string,
73
193
  ...rest: unknown[]
74
194
  ) {
75
- const seed = createTemplateSchema.parse(parseBody(body));
195
+ const { seed, normalized } = parseCreation(body);
76
196
  const tenantId = getRequestTenantId(ctx);
77
- const existing = await GetModel(TemplateModel, tenantId).getBySlug(
78
- seed.slug,
79
- );
80
- if (existing) throw new HTTPResult(HTTP_CONFLICT, DUPLICATE_SLUG);
81
- const source = await resolveSource(tenantId, seed.sourceTemplateId);
197
+ const model = GetModel(TemplateModel, tenantId);
198
+ if (await model.getBySlug(seed.slug)) {
199
+ throw new HTTPResult(HTTP_CONFLICT, DUPLICATE_SLUG);
200
+ }
201
+ const settings = await getSettings(tenantId);
202
+ assertKnownCategory(settings.categories, seed.category);
203
+ const fields = await seedFields(tenantId, seed, settings.fallbackLocale);
82
204
  const ids = (await base.func.call(
83
205
  this,
84
206
  ctx,
85
207
  params,
86
- body,
208
+ normalized,
87
209
  ...rest,
88
210
  )) as string[];
211
+ const templateId = ids[0] as string;
212
+ await assertFirstOfSlug(model, seed.slug, templateId);
89
213
  const user = await authenticateTenantRequest(ctx);
90
- const settings = await getSettings(tenantId);
91
- await initializeTemplate(
92
- tenantId,
93
- ids[0] as string,
94
- settings.fallbackLocale,
95
- displayName(user),
96
- source,
97
- );
214
+ await initializeTemplate(tenantId, templateId, displayName(user), fields);
98
215
  return ids;
99
216
  },
100
217
  };
101
218
  }
102
219
 
103
- interface EditParams {
104
- id: string;
220
+ /** Whether a template was ever sent, a test included. */
221
+ async function wasSent(sends: SendModel, slug: string): Promise<boolean> {
222
+ const count = await sends.table.getAll(slug, "templateSlug").count().run();
223
+ return count > 0;
105
224
  }
106
225
 
107
226
  /**
108
- * The body an edit may apply. Sends resolve templates by slug and nothing
109
- * enforces its uniqueness but the create checks, so a slug never changes once
110
- * set: a different one is refused, a missing one (which the data-api edit
111
- * would null) is kept.
227
+ * Why a template cannot go for good, `null` when it can: archived first
228
+ * (archiving explains what stops working), and never sent, since the send log
229
+ * renders past sends from it and a freed slug would inherit their figures.
112
230
  */
113
- async function withStoredSlug(
231
+ async function deleteRefusal(
114
232
  tenantId: string,
115
- templateId: string,
116
- body: Buffer | string,
117
- ): Promise<Buffer | string> {
118
- const stored = await GetModel(TemplateModel, tenantId).get(templateId);
119
- if (!stored) return body;
120
- const edit = parseBody(body);
121
- if (edit.slug !== undefined && edit.slug !== stored.slug) {
122
- throw new HTTPResult(HTTP_CONFLICT, SLUG_IMMUTABLE);
233
+ template: MailingTemplate,
234
+ ): Promise<string | null> {
235
+ if (template.status !== ARCHIVED_STATUS) return TEMPLATE_IN_USE;
236
+ const isSent = await wasSent(GetModel(SendModel, tenantId), template.slug);
237
+ return isSent ? TEMPLATE_WAS_SENT : null;
238
+ }
239
+
240
+ /** The fields of an edit the guard checks. */
241
+ interface TemplateEdit {
242
+ slug?: unknown;
243
+ category?: unknown;
244
+ name?: unknown;
245
+ }
246
+
247
+ /** The field a refused blank name is reported under. */
248
+ const BLANK_NAME = { message: INVALID_TEMPLATE, field: "name" };
249
+
250
+ /** What a guard does once the write it let through is done. */
251
+ type AfterWrite = () => Promise<void>;
252
+
253
+ /** Only an archived template never sent can go. */
254
+ export async function assertDeletable(
255
+ tenantId: string,
256
+ model: TemplateModel,
257
+ ids: string[],
258
+ ): Promise<void> {
259
+ for (const id of ids) {
260
+ const template = await model.get(id);
261
+ const refusal = template && (await deleteRefusal(tenantId, template));
262
+ if (refusal) throw new HTTPResult(HTTP_CONFLICT, refusal);
123
263
  }
124
- return JSON.stringify({ ...edit, slug: stored.slug });
125
264
  }
126
265
 
127
- function withFixedSlug(base: DataControllerCallback): DataControllerCallback {
128
- return {
129
- ...base,
130
- func: async function (
131
- this: unknown,
132
- ctx: RequestContext,
133
- params: EditParams,
134
- body: Buffer | string,
135
- ...rest: unknown[]
136
- ) {
137
- const guarded = await withStoredSlug(
138
- getRequestTenantId(ctx),
139
- params.id,
140
- body,
141
- );
142
- return base.func.call(this, ctx, params, guarded, ...rest);
143
- },
266
+ /** A deleted template takes its published versions with it. */
267
+ export function afterDelete(tenantId: string, ids: string[]): AfterWrite {
268
+ return async () => {
269
+ const versions = GetModel(TemplateVersionModel, tenantId).table;
270
+ await Promise.all(
271
+ ids.map((id) => versions.getAll(id, "templateId").delete().run()),
272
+ );
273
+ await refreshTemplateAttention(tenantId);
144
274
  };
145
275
  }
146
276
 
277
+ /**
278
+ * Sends resolve templates by slug and nothing enforces its uniqueness but the
279
+ * create checks, so a slug never changes once set; a category must be one the
280
+ * settings declare, and a name holds more than spaces.
281
+ */
282
+ export async function assertEditable(
283
+ tenantId: string,
284
+ body: TemplateEdit,
285
+ current: Pick<MailingTemplate, "slug">,
286
+ ): Promise<void> {
287
+ if (body.slug !== undefined && body.slug !== current.slug)
288
+ throw new HTTPResult(HTTP_CONFLICT, SLUG_IMMUTABLE);
289
+ if (typeof body.name === "string" && !body.name.trim())
290
+ throw new HTTPResult(HTTP_BAD_REQUEST, BLANK_NAME);
291
+ const { categories } = await getSettings(tenantId);
292
+ assertKnownCategory(categories, body.category);
293
+ }
294
+
295
+ /** Stores an edited name trimmed, once the edit is written. */
296
+ export function afterEdit(
297
+ model: TemplateModel,
298
+ id: string,
299
+ name: unknown,
300
+ ): AfterWrite | undefined {
301
+ if (typeof name !== "string" || name === name.trim()) return undefined;
302
+ return async () => {
303
+ await model.update(id, { name: name.trim() });
304
+ };
305
+ }
306
+
307
+ /** The templates the attention refresh flagged. */
308
+ const isFlagged: FilterFunction<Record<string, unknown>> = (_context, proxy) =>
309
+ proxy.default(false).eq(true);
310
+
147
311
  const templateRoutes = {
148
312
  ...TableViewRoutes.All,
149
313
  new: withSeeding(TableViewRoutes.New),
150
- edit: withFixedSlug(TableViewRoutes.Edit),
151
314
  };
152
315
 
153
316
  @RegisterDataController()
@@ -160,6 +323,9 @@ export class TemplatesTableAPI extends DataController(
160
323
  @TenantScopedModel(TemplateModel)
161
324
  declare model: TemplateModel;
162
325
 
326
+ @TenantScopedModel(SendModel)
327
+ declare sends: SendModel;
328
+
163
329
  @Select()
164
330
  @Listable()
165
331
  @Exported()
@@ -171,11 +337,14 @@ export class TemplatesTableAPI extends DataController(
171
337
  @Sortable()
172
338
  @Exported()
173
339
  @Searchable()
174
- @Mandatory("new")
340
+ @Mandatory("new", "edit")
175
341
  @Access(AccessMode.ReadWrite)
176
342
  @Column({
177
343
  name: "$dms_mailing.templates.cols.name",
178
- type: new DefaultDataTypes.StringType({}),
344
+ type: new DefaultDataTypes.StringType({
345
+ minLength: MIN_NAME_LENGTH,
346
+ maxLength: MAX_NAME_LENGTH,
347
+ }),
179
348
  })
180
349
  declare name: string;
181
350
 
@@ -188,8 +357,9 @@ export class TemplatesTableAPI extends DataController(
188
357
  @Access(AccessMode.ReadWrite)
189
358
  @Column({
190
359
  name: "$dms_mailing.templates.cols.slug",
191
- type: new DefaultDataTypes.StringType({}),
360
+ type: new DefaultDataTypes.StringType({ maxLength: MAX_SLUG_LENGTH }),
192
361
  readonlyBehavior: CREATE_ONLY,
362
+ display: new DefaultDisplays.MonoDisplay({ copy: true }),
193
363
  })
194
364
  declare slug: string;
195
365
 
@@ -200,6 +370,7 @@ export class TemplatesTableAPI extends DataController(
200
370
  name: "$dms_mailing.templates.cols.category",
201
371
  type: new TemplateCategoryType(),
202
372
  filterable: true,
373
+ display: new CategoryDisplay({}),
203
374
  })
204
375
  declare category: string;
205
376
 
@@ -211,7 +382,10 @@ export class TemplatesTableAPI extends DataController(
211
382
  name: "$dms_mailing.templates.cols.status",
212
383
  type: new DefaultDataTypes.SelectType({ items: STATUS_ITEMS }),
213
384
  filterable: true,
214
- readonlyBehavior: NEVER_ON_CREATE,
385
+ readonlyBehavior: NEVER_IN_FORMS,
386
+ display: new DefaultDisplays.StatusPillDisplay({
387
+ tones: { live: "success", draft: "neutral", archived: "warning" },
388
+ }),
215
389
  })
216
390
  declare status: string;
217
391
 
@@ -222,7 +396,12 @@ export class TemplatesTableAPI extends DataController(
222
396
  @Column({
223
397
  name: "$dms_mailing.templates.cols.updatedAt",
224
398
  type: new DefaultDataTypes.DateType({}),
225
- readonlyBehavior: NEVER_ON_CREATE,
399
+ readonlyBehavior: NEVER_IN_FORMS,
400
+ display: new DefaultDisplays.RelativeDateDisplay({
401
+ style: "day",
402
+ byField: "updatedBy",
403
+ byLabel: "$dms_mailing.templates.cols.updated_by",
404
+ }),
226
405
  })
227
406
  declare updatedAt: Date;
228
407
 
@@ -232,7 +411,8 @@ export class TemplatesTableAPI extends DataController(
232
411
  @Column({
233
412
  name: "$dms_mailing.templates.cols.locales",
234
413
  type: new DefaultDataTypes.StringType({}),
235
- readonlyBehavior: NEVER_ON_CREATE,
414
+ readonlyBehavior: NEVER_IN_FORMS,
415
+ display: new LocaleChipsDisplay({}),
236
416
  })
237
417
  declare locales: string;
238
418
 
@@ -242,7 +422,59 @@ export class TemplatesTableAPI extends DataController(
242
422
  @Column({
243
423
  name: "$dms_mailing.templates.cols.updatedBy",
244
424
  type: new DefaultDataTypes.StringType({}),
245
- readonlyBehavior: NEVER_ON_CREATE,
425
+ readonlyBehavior: NEVER_IN_FORMS,
246
426
  })
247
427
  declare updatedBy: string;
428
+
429
+ @Listable()
430
+ @Access(AccessMode.ReadOnly)
431
+ @Column({
432
+ name: "$dms_mailing.templates.cols.publishedVersion",
433
+ type: new DefaultDataTypes.NumberType({}),
434
+ readonlyBehavior: NEVER_IN_FORMS,
435
+ isVisible: false,
436
+ })
437
+ declare publishedVersion: number;
438
+
439
+ @Listable()
440
+ @Access(AccessMode.ReadOnly)
441
+ @Column({
442
+ name: "$dms_mailing.templates.cols.isDraftPending",
443
+ type: new DefaultDataTypes.BooleanType({}),
444
+ readonlyBehavior: NEVER_IN_FORMS,
445
+ isVisible: false,
446
+ })
447
+ declare isDraftPending: boolean;
448
+
449
+ @Listable()
450
+ @Access(AccessMode.ReadOnly)
451
+ @Column({
452
+ name: "$dms_mailing.templates.cols.publishedAt",
453
+ type: new DefaultDataTypes.DateType({}),
454
+ readonlyBehavior: NEVER_IN_FORMS,
455
+ isVisible: false,
456
+ })
457
+ declare publishedAt: Date;
458
+
459
+ /** Why the template needs a look (comma-separated), for the gallery cards. */
460
+ @Listable()
461
+ @Access(AccessMode.ReadOnly)
462
+ declare attention: string;
463
+
464
+ /** What the "Needs attention" tab filters on, whatever value it names. */
465
+ @Filter(isFlagged)
466
+ @Access(AccessMode.ReadOnly)
467
+ declare needsAttention: boolean;
468
+
469
+ /**
470
+ * Whether an archived template was ever sent: the gallery then offers no
471
+ * Delete, which the route would refuse. Read for archived rows only, the
472
+ * only ones Delete is offered on.
473
+ */
474
+ @Listable(["slug", "status"])
475
+ @Access(AccessMode.ReadOnly)
476
+ get hasSends(): Promise<boolean> | boolean {
477
+ if (this.status !== ARCHIVED_STATUS || !this.sends) return false;
478
+ return wasSent(this.sends, this.slug);
479
+ }
248
480
  }
@@ -4,12 +4,11 @@ import {
4
4
  RegisterDataType,
5
5
  } from "@antelopejs/interface-dms/base/data-types";
6
6
  import { TEMPLATE_CATEGORIES_TYPE_ID } from "../constants";
7
- import { CATEGORIES_SCHEMA } from "../services/categories";
7
+ import { categoriesSchema } from "../validation/settings.schema";
8
8
 
9
9
  /**
10
10
  * The tenant's template categories as a settings field — a repeatable list of
11
- * {id, label, icon} that no built-in DataType models: the input is ours, and
12
- * the validation accepts the array or the JSON string a form field stores.
11
+ * {id, label, icon} that no built-in DataType models: the input is ours.
13
12
  */
14
13
  @RegisterDataType(TEMPLATE_CATEGORIES_TYPE_ID)
15
14
  export class TemplateCategoriesType extends DataType {
@@ -18,12 +17,12 @@ export class TemplateCategoriesType extends DataType {
18
17
  }
19
18
 
20
19
  protected defaultInputComponent() {
21
- return CustomComponent("DmsMailingCategoriesInput")
20
+ return CustomComponent("MailingCategoriesInput")
22
21
  .options(this.options)
23
22
  .serializeSync();
24
23
  }
25
24
 
26
25
  getValidation() {
27
- return CATEGORIES_SCHEMA;
26
+ return categoriesSchema;
28
27
  }
29
28
  }
@@ -33,7 +33,7 @@ export class TemplateCategoryType extends DataType {
33
33
  }
34
34
 
35
35
  protected defaultInputComponent() {
36
- return CustomComponent("DmsMailingCategoryInput")
36
+ return CustomComponent("MailingCategoryInput")
37
37
  .options(this.options)
38
38
  .serializeSync();
39
39
  }
@@ -1,4 +1,7 @@
1
1
  export * from "./send-events.model";
2
2
  export * from "./sends.model";
3
3
  export * from "./settings.model";
4
+ export * from "./suppressions.model";
5
+ export * from "./template-versions.model";
4
6
  export * from "./templates.model";
7
+ export * from "./webhook-status.model";
@@ -4,6 +4,7 @@ import type {
4
4
  AtomicMutationOutcome,
5
5
  ValueProxy,
6
6
  } from "@antelopejs/interface-database";
7
+ import { isResolvedStage, stageOf } from "../../types";
7
8
  import { MAILING_SENDS_TABLE_NAME, MailingSend } from "../tables";
8
9
 
9
10
  const FIRST_ROW_OFFSET = 0;
@@ -14,14 +15,33 @@ type NewSend = Omit<
14
15
  "_id" | "createdAt" | "revision" | "isRetiring" | "lastActivityAt"
15
16
  >;
16
17
 
18
+ const MAX_MUTATION_ATTEMPTS = 5;
19
+
17
20
  /** A send as the metrics read it: everything but the rendered variables. */
18
21
  export type SendSummary = Omit<MailingSend, "json_variables">;
19
22
 
20
23
  type SendActivity = Pick<
21
24
  MailingSend,
22
- "status" | "opens" | "clicks" | "lastEventAt" | "provider"
25
+ | "status"
26
+ | "opens"
27
+ | "clicks"
28
+ | "lastEventAt"
29
+ | "provider"
30
+ | "error"
31
+ | "isSoftBounce"
23
32
  >;
24
33
 
34
+ /** Who sent a problem again, and the send that did. */
35
+ export interface SendReplay {
36
+ replayedBy: string;
37
+ resolvedBy: string;
38
+ }
39
+
40
+ type SendPatch = Partial<MailingSend>;
41
+
42
+ /** The change to make to a send's latest row, `null` to leave it alone. */
43
+ type PatchOf = (send: MailingSend) => SendPatch | null;
44
+
25
45
  function canRetire(row: ValueProxy<MailingSend>, limit: Date) {
26
46
  return row
27
47
  .key("createdAt")
@@ -134,6 +154,9 @@ export class SendModel extends BasicDataModel(
134
154
  const patch = Object.fromEntries(
135
155
  Object.entries({
136
156
  ...changes,
157
+ stage: changes.status
158
+ ? stageOf(changes.status, isResolvedStage(send.stage))
159
+ : undefined,
137
160
  lastActivityAt: new Date(),
138
161
  }).filter(([, value]) => value !== undefined),
139
162
  );
@@ -148,4 +171,82 @@ export class SendModel extends BasicDataModel(
148
171
  .run();
149
172
  return wasApplied(outcome);
150
173
  }
174
+
175
+ /**
176
+ * Takes a problem out of the Problems tab before it is sent again, so a
177
+ * second "send the failures again" skips it. `false` when it was already
178
+ * dealt with, or is gone.
179
+ */
180
+ claimResolution(sendId: string, resolvedBy: string): Promise<boolean> {
181
+ return this.mutateLatest(sendId, (send) =>
182
+ isResolvedStage(send.stage)
183
+ ? null
184
+ : {
185
+ resolvedAt: new Date(),
186
+ resolvedBy,
187
+ stage: stageOf(send.status, true),
188
+ },
189
+ );
190
+ }
191
+
192
+ /** Puts a claimed problem back when sending it again could not start. */
193
+ releaseResolution(sendId: string): Promise<boolean> {
194
+ return this.mutateLatest(sendId, (send) => ({
195
+ stage: stageOf(send.status),
196
+ }));
197
+ }
198
+
199
+ /** Links a send to the one that repeated it; a problem is then resolved. */
200
+ markReplayed(sendId: string, replay: SendReplay): Promise<boolean> {
201
+ return this.mutateLatest(sendId, (send) => ({
202
+ ...replay,
203
+ resolvedAt: new Date(),
204
+ stage: stageOf(send.status, true),
205
+ }));
206
+ }
207
+
208
+ /** Links a send again to the send it repeats. */
209
+ linkReplayOf(sendId: string, originalId: string): Promise<boolean> {
210
+ return this.mutateLatest(sendId, () => ({ replayOf: originalId }));
211
+ }
212
+
213
+ private async mutateLatest(
214
+ sendId: string,
215
+ patchOf: PatchOf,
216
+ ): Promise<boolean> {
217
+ for (let attempt = 0; attempt < MAX_MUTATION_ATTEMPTS; attempt++) {
218
+ const send = await this.getSnapshot(sendId);
219
+ if (!send || send.isRetiring) return false;
220
+ const patch = patchOf(send);
221
+ if (!patch) return false;
222
+ const outcome = await this.table
223
+ .atomicMutation(sendId, {
224
+ type: "update",
225
+ revisionField: "revision",
226
+ expectedRevision: send.revision,
227
+ nextRevision: randomUUID(),
228
+ patch,
229
+ })
230
+ .run();
231
+ if (wasApplied(outcome)) return true;
232
+ }
233
+ return false;
234
+ }
235
+
236
+ /** Sends of a template since `from`, without the rendered variables. */
237
+ listForTemplate(templateSlug: string, from: Date): Promise<SendSummary[]> {
238
+ return this.table
239
+ .getAll(templateSlug, "templateSlug")
240
+ .filter((row) => row.key("createdAt").ge(from))
241
+ .without("json_variables")
242
+ .run() as Promise<SendSummary[]>;
243
+ }
244
+
245
+ /** How many sends the retention would delete with this cutoff. */
246
+ countOlderThan(limit: Date): Promise<number> {
247
+ return this.table
248
+ .filter((row) => canRetire(row, limit))
249
+ .count()
250
+ .run();
251
+ }
151
252
  }
@@ -1,18 +1,8 @@
1
1
  import { BasicDataModel } from "@antelopejs/interface-database-decorators";
2
2
  import { MAILING_SETTINGS_TABLE_NAME, MailingSettings } from "../tables";
3
3
 
4
+ /** The tenant's settings: a single row, keyed by the tenant id. */
4
5
  export class SettingsModel extends BasicDataModel(
5
6
  MailingSettings,
6
7
  MAILING_SETTINGS_TABLE_NAME,
7
- ) {
8
- /**
9
- * The tenant's settings row, keyed by the tenant id. Rows written before the
10
- * key existed carry a random id and are still found by a scan.
11
- */
12
- async getSingleton(tenantId: string): Promise<MailingSettings | undefined> {
13
- const keyed = await this.get(tenantId);
14
- if (keyed) return keyed;
15
- const rows = await this.getAll();
16
- return rows[0];
17
- }
18
- }
8
+ ) {}