@antelopejs/dms-mailing 0.3.4 → 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.
- package/README.md +232 -22
- package/dist/automation/email-event-trigger.js +2 -2
- package/dist/automation/email-event-trigger.js.map +1 -1
- package/dist/automation/send-template-action.js +2 -2
- package/dist/automation/send-template-action.js.map +1 -1
- package/dist/config.js +3 -1
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +10 -1
- package/dist/constants.js +15 -3
- package/dist/constants.js.map +1 -1
- package/dist/crons/attention.d.ts +3 -0
- package/dist/crons/attention.js +19 -0
- package/dist/crons/attention.js.map +1 -0
- package/dist/crons/index.d.ts +2 -0
- package/dist/crons/index.js +2 -0
- package/dist/crons/index.js.map +1 -1
- package/dist/crons/jobs.d.ts +12 -0
- package/dist/crons/jobs.js +27 -0
- package/dist/crons/jobs.js.map +1 -0
- package/dist/crons/retention.d.ts +2 -4
- package/dist/crons/retention.js +7 -22
- package/dist/crons/retention.js.map +1 -1
- package/dist/data/displays.d.ts +5 -0
- package/dist/data/displays.js +23 -0
- package/dist/data/displays.js.map +1 -0
- package/dist/data/index.d.ts +1 -0
- package/dist/data/index.js +1 -0
- package/dist/data/index.js.map +1 -1
- package/dist/data/locale-fallback.d.ts +3 -0
- package/dist/data/locale-fallback.js +20 -0
- package/dist/data/locale-fallback.js.map +1 -0
- package/dist/data/read-only-routes.d.ts +7 -0
- package/dist/data/read-only-routes.js +13 -0
- package/dist/data/read-only-routes.js.map +1 -0
- package/dist/data/sends-table.d.ts +19 -5
- package/dist/data/sends-table.js +185 -17
- package/dist/data/sends-table.js.map +1 -1
- package/dist/data/suppressions-table.d.ts +24 -0
- package/dist/data/suppressions-table.js +120 -0
- package/dist/data/suppressions-table.js.map +1 -0
- package/dist/data/templates-table.d.ts +20 -2
- package/dist/data/templates-table.js +188 -31
- package/dist/data/templates-table.js.map +1 -1
- package/dist/data-types/categories-type.d.ts +11 -13
- package/dist/data-types/categories-type.js +3 -3
- package/dist/data-types/categories-type.js.map +1 -1
- package/dist/data-types/category-type.js +1 -1
- package/dist/data-types/category-type.js.map +1 -1
- package/dist/db/models/index.d.ts +3 -0
- package/dist/db/models/index.js +3 -0
- package/dist/db/models/index.js.map +1 -1
- package/dist/db/models/sends.model.d.ts +12 -1
- package/dist/db/models/sends.model.js +64 -0
- package/dist/db/models/sends.model.js.map +1 -1
- package/dist/db/models/settings.model.d.ts +0 -1
- package/dist/db/models/settings.model.js +0 -7
- package/dist/db/models/settings.model.js.map +1 -1
- package/dist/db/models/suppressions.model.d.ts +28 -0
- package/dist/db/models/suppressions.model.js +46 -0
- package/dist/db/models/suppressions.model.js.map +1 -0
- package/dist/db/models/template-versions.model.d.ts +24 -0
- package/dist/db/models/template-versions.model.js +17 -0
- package/dist/db/models/template-versions.model.js.map +1 -0
- package/dist/db/models/templates.model.d.ts +2 -0
- package/dist/db/models/templates.model.js +3 -0
- package/dist/db/models/templates.model.js.map +1 -1
- package/dist/db/models/webhook-status.model.d.ts +24 -0
- package/dist/db/models/webhook-status.model.js +21 -0
- package/dist/db/models/webhook-status.model.js.map +1 -0
- package/dist/db/tables/index.d.ts +3 -0
- package/dist/db/tables/index.js +3 -0
- package/dist/db/tables/index.js.map +1 -1
- package/dist/db/tables/send-events.table.d.ts +2 -2
- package/dist/db/tables/send-events.table.js.map +1 -1
- package/dist/db/tables/sends.table.d.ts +10 -2
- package/dist/db/tables/sends.table.js +34 -1
- package/dist/db/tables/sends.table.js.map +1 -1
- package/dist/db/tables/suppressions.table.d.ts +12 -0
- package/dist/db/tables/suppressions.table.js +53 -0
- package/dist/db/tables/suppressions.table.js.map +1 -0
- package/dist/db/tables/template-versions.table.d.ts +13 -0
- package/dist/db/tables/template-versions.table.js +59 -0
- package/dist/db/tables/template-versions.table.js.map +1 -0
- package/dist/db/tables/templates.table.d.ts +11 -0
- package/dist/db/tables/templates.table.js +45 -0
- package/dist/db/tables/templates.table.js.map +1 -1
- package/dist/db/tables/webhook-status.table.d.ts +14 -0
- package/dist/db/tables/webhook-status.table.js +58 -0
- package/dist/db/tables/webhook-status.table.js.map +1 -0
- package/dist/engine/changes.d.ts +12 -0
- package/dist/engine/changes.js +107 -0
- package/dist/engine/changes.js.map +1 -0
- package/dist/engine/conditions.js +0 -2
- package/dist/engine/conditions.js.map +1 -1
- package/dist/engine/coverage.d.ts +1 -0
- package/dist/engine/coverage.js +6 -2
- package/dist/engine/coverage.js.map +1 -1
- package/dist/engine/fallback-html.d.ts +1 -1
- package/dist/engine/fallback-html.js +51 -34
- package/dist/engine/fallback-html.js.map +1 -1
- package/dist/engine/index.d.ts +3 -0
- package/dist/engine/index.js +3 -0
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/plain-text.d.ts +3 -0
- package/dist/engine/plain-text.js +60 -0
- package/dist/engine/plain-text.js.map +1 -0
- package/dist/engine/resolve.d.ts +2 -1
- package/dist/engine/resolve.js +17 -9
- package/dist/engine/resolve.js.map +1 -1
- package/dist/engine/system-variables.d.ts +10 -0
- package/dist/engine/system-variables.js +70 -0
- package/dist/engine/system-variables.js.map +1 -0
- package/dist/engine/tokens.d.ts +2 -1
- package/dist/engine/tokens.js +6 -2
- package/dist/engine/tokens.js.map +1 -1
- package/dist/hooks/tenant-export.js +13 -1
- package/dist/hooks/tenant-export.js.map +1 -1
- package/dist/implementations/dms-mailing/index.d.ts +1 -1
- package/dist/implementations/dms-mailing/index.js +2 -2
- package/dist/implementations/dms-mailing/index.js.map +1 -1
- package/dist/index.js +13 -11
- package/dist/index.js.map +1 -1
- package/dist/pages/editor.js +9 -2
- package/dist/pages/editor.js.map +1 -1
- package/dist/pages/index.d.ts +1 -0
- package/dist/pages/index.js +1 -0
- package/dist/pages/index.js.map +1 -1
- package/dist/pages/module.d.ts +2 -0
- package/dist/pages/module.js +26 -2
- package/dist/pages/module.js.map +1 -1
- package/dist/pages/overview.d.ts +2 -6
- package/dist/pages/overview.js +109 -52
- package/dist/pages/overview.js.map +1 -1
- package/dist/pages/sends.d.ts +3 -4
- package/dist/pages/sends.js +197 -60
- package/dist/pages/sends.js.map +1 -1
- package/dist/pages/settings/form.js +114 -53
- package/dist/pages/settings/form.js.map +1 -1
- package/dist/pages/settings/page.d.ts +0 -1
- package/dist/pages/settings/page.js +8 -11
- package/dist/pages/settings/page.js.map +1 -1
- package/dist/pages/suppressions.d.ts +9 -0
- package/dist/pages/suppressions.js +151 -0
- package/dist/pages/suppressions.js.map +1 -0
- package/dist/pages/templates.d.ts +0 -1
- package/dist/pages/templates.js +114 -40
- package/dist/pages/templates.js.map +1 -1
- package/dist/permissions.js +2 -2
- package/dist/permissions.js.map +1 -1
- package/dist/quick-actions.js +7 -4
- package/dist/quick-actions.js.map +1 -1
- package/dist/routes/events.d.ts +8 -5
- package/dist/routes/events.js +55 -24
- package/dist/routes/events.js.map +1 -1
- package/dist/routes/index.d.ts +2 -0
- package/dist/routes/index.js +2 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/metrics.d.ts +60 -23
- package/dist/routes/metrics.js +231 -83
- package/dist/routes/metrics.js.map +1 -1
- package/dist/routes/period.d.ts +2 -1
- package/dist/routes/period.js +9 -3
- package/dist/routes/period.js.map +1 -1
- package/dist/routes/sends.d.ts +49 -8
- package/dist/routes/sends.js +239 -24
- package/dist/routes/sends.js.map +1 -1
- package/dist/routes/settings.d.ts +44 -5
- package/dist/routes/settings.js +190 -15
- package/dist/routes/settings.js.map +1 -1
- package/dist/routes/suppressions.d.ts +16 -0
- package/dist/routes/suppressions.js +92 -0
- package/dist/routes/suppressions.js.map +1 -0
- package/dist/routes/template-versions.d.ts +28 -0
- package/dist/routes/template-versions.js +87 -0
- package/dist/routes/template-versions.js.map +1 -0
- package/dist/routes/templates.d.ts +60 -6
- package/dist/routes/templates.js +204 -35
- package/dist/routes/templates.js.map +1 -1
- package/dist/services/attention.d.ts +19 -6
- package/dist/services/attention.js +177 -12
- package/dist/services/attention.js.map +1 -1
- package/dist/services/catalog.d.ts +3 -0
- package/dist/services/catalog.js +25 -0
- package/dist/services/catalog.js.map +1 -0
- package/dist/services/categories.d.ts +8 -32
- package/dist/services/categories.js +14 -25
- package/dist/services/categories.js.map +1 -1
- package/dist/services/checked-send.d.ts +9 -0
- package/dist/services/checked-send.js +54 -0
- package/dist/services/checked-send.js.map +1 -0
- package/dist/services/events.d.ts +5 -0
- package/dist/services/events.js +102 -9
- package/dist/services/events.js.map +1 -1
- package/dist/services/metrics.d.ts +17 -6
- package/dist/services/metrics.js +57 -18
- package/dist/services/metrics.js.map +1 -1
- package/dist/services/notify.d.ts +18 -10
- package/dist/services/notify.js +82 -37
- package/dist/services/notify.js.map +1 -1
- package/dist/services/provider-banner.d.ts +14 -0
- package/dist/services/provider-banner.js +115 -0
- package/dist/services/provider-banner.js.map +1 -0
- package/dist/services/render.js.map +1 -1
- package/dist/services/send-stat-items.d.ts +7 -0
- package/dist/services/send-stat-items.js +127 -0
- package/dist/services/send-stat-items.js.map +1 -0
- package/dist/services/send-stats.d.ts +23 -0
- package/dist/services/send-stats.js +63 -0
- package/dist/services/send-stats.js.map +1 -0
- package/dist/services/send.d.ts +18 -3
- package/dist/services/send.js +81 -17
- package/dist/services/send.js.map +1 -1
- package/dist/services/settings.d.ts +5 -0
- package/dist/services/settings.js +28 -7
- package/dist/services/settings.js.map +1 -1
- package/dist/services/starters.d.ts +15 -0
- package/dist/services/starters.js +264 -0
- package/dist/services/starters.js.map +1 -0
- package/dist/services/suppressions.d.ts +20 -0
- package/dist/services/suppressions.js +53 -0
- package/dist/services/suppressions.js.map +1 -0
- package/dist/services/template-attention.d.ts +4 -0
- package/dist/services/template-attention.js +43 -0
- package/dist/services/template-attention.js.map +1 -0
- package/dist/services/template-stats.d.ts +27 -0
- package/dist/services/template-stats.js +59 -0
- package/dist/services/template-stats.js.map +1 -0
- package/dist/services/templates.d.ts +31 -4
- package/dist/services/templates.js +240 -21
- package/dist/services/templates.js.map +1 -1
- package/dist/services/webhook-status.d.ts +27 -0
- package/dist/services/webhook-status.js +134 -0
- package/dist/services/webhook-status.js.map +1 -0
- package/dist/types/blocks.d.ts +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/sends.d.ts +10 -1
- package/dist/types/sends.js +35 -2
- package/dist/types/sends.js.map +1 -1
- package/dist/types/suppressions.d.ts +9 -0
- package/dist/types/suppressions.js +10 -0
- package/dist/types/suppressions.js.map +1 -0
- package/dist/types/webhook.d.ts +17 -0
- package/dist/types/webhook.js +3 -0
- package/dist/types/webhook.js.map +1 -0
- package/dist/validation/settings.schema.d.ts +144 -9
- package/dist/validation/settings.schema.js +113 -16
- package/dist/validation/settings.schema.js.map +1 -1
- package/dist/validation/templates.schema.d.ts +137 -10
- package/dist/validation/templates.schema.js +28 -2
- package/dist/validation/templates.schema.js.map +1 -1
- package/frontend-vue/app/build/components/FormFieldRow.vue +90 -0
- package/frontend-vue/app/build/components/FormRows.vue +26 -0
- package/frontend-vue/app/build/components/RequiredLegend.vue +11 -0
- package/frontend-vue/app/components/CategoriesInput.vue +323 -77
- package/frontend-vue/app/components/CategoryInput.vue +13 -14
- package/frontend-vue/app/components/CategoryLabel.vue +34 -0
- package/frontend-vue/app/components/DuplicateModal.vue +201 -0
- package/frontend-vue/app/components/Editor.vue +55 -20
- package/frontend-vue/app/components/FixAddressModal.vue +133 -0
- package/frontend-vue/app/components/FunnelCard.vue +146 -43
- package/frontend-vue/app/components/LocaleSelect.vue +98 -0
- package/frontend-vue/app/components/NewTemplateModal.vue +388 -102
- package/frontend-vue/app/components/NewTemplateSourceTile.vue +70 -0
- package/frontend-vue/app/components/OverviewGate.vue +220 -0
- package/frontend-vue/app/components/PageHeader.vue +98 -0
- package/frontend-vue/app/components/RealSendModal.vue +555 -0
- package/frontend-vue/app/components/RecipientsField.vue +41 -0
- package/frontend-vue/app/components/RenderingModal.vue +189 -0
- package/frontend-vue/app/components/RetentionInput.vue +212 -0
- package/frontend-vue/app/components/SendDrawer.vue +542 -209
- package/frontend-vue/app/components/StarterGallery.vue +153 -0
- package/frontend-vue/app/components/SystemVariables.vue +63 -0
- package/frontend-vue/app/components/TemplateCard.vue +218 -57
- package/frontend-vue/app/components/TemplateDrawer.vue +785 -274
- package/frontend-vue/app/components/TemplateGallery.vue +312 -56
- package/frontend-vue/app/components/TemplatePreviewFrame.vue +112 -22
- package/frontend-vue/app/components/TestSendModal.vue +266 -95
- package/frontend-vue/app/components/TokenText.vue +61 -10
- package/frontend-vue/app/components/VariablePath.vue +20 -0
- package/frontend-vue/app/components/VersionHistory.vue +196 -0
- package/frontend-vue/app/components/VersionPreview.vue +164 -0
- package/frontend-vue/app/components/WebhookStatus.vue +130 -0
- package/frontend-vue/app/components/blocks/Button.vue +1 -1
- package/frontend-vue/app/components/blocks/Code.vue +1 -1
- package/frontend-vue/app/components/blocks/Footer.vue +56 -7
- package/frontend-vue/app/components/blocks/Heading.vue +1 -1
- package/frontend-vue/app/components/blocks/Hero.vue +13 -7
- package/frontend-vue/app/components/blocks/List.vue +50 -6
- package/frontend-vue/app/components/blocks/Paragraph.vue +1 -1
- package/frontend-vue/app/components/blocks/Total.vue +3 -3
- package/frontend-vue/app/components/editor/EditorAutocompleteMenu.vue +50 -0
- package/frontend-vue/app/components/editor/EditorBlock.vue +37 -90
- package/frontend-vue/app/components/editor/EditorBlockFields.vue +140 -0
- package/frontend-vue/app/components/editor/EditorBlockInspector.vue +92 -0
- package/frontend-vue/app/components/editor/EditorBlockList.vue +32 -25
- package/frontend-vue/app/components/editor/EditorBlockPicker.vue +58 -0
- package/frontend-vue/app/components/editor/EditorBlockTools.vue +71 -0
- package/frontend-vue/app/components/editor/EditorCanvas.vue +105 -70
- package/frontend-vue/app/components/editor/EditorCompare.vue +211 -0
- package/frontend-vue/app/components/editor/EditorConditionFields.vue +56 -46
- package/frontend-vue/app/components/editor/EditorConditionInspector.vue +54 -0
- package/frontend-vue/app/components/editor/EditorEnvelope.vue +77 -0
- package/frontend-vue/app/components/editor/EditorIfBlock.vue +36 -86
- package/frontend-vue/app/components/editor/EditorInsertPoint.vue +64 -0
- package/frontend-vue/app/components/editor/EditorInspector.vue +80 -0
- package/frontend-vue/app/components/editor/EditorLeftPane.vue +103 -0
- package/frontend-vue/app/components/editor/EditorLifecycle.vue +67 -0
- package/frontend-vue/app/components/editor/EditorLoading.vue +23 -0
- package/frontend-vue/app/components/editor/EditorLocaleSwitch.vue +61 -0
- package/frontend-vue/app/components/editor/EditorMissingLocale.vue +72 -48
- package/frontend-vue/app/components/editor/EditorMissingPolicy.vue +42 -0
- package/frontend-vue/app/components/editor/EditorNotice.vue +127 -0
- package/frontend-vue/app/components/editor/EditorOutline.vue +65 -0
- package/frontend-vue/app/components/editor/EditorPalette.vue +95 -42
- package/frontend-vue/app/components/editor/EditorPaletteGroup.vue +66 -0
- package/frontend-vue/app/components/editor/EditorPublishReview.vue +102 -0
- package/frontend-vue/app/components/editor/EditorSaveState.vue +88 -0
- package/frontend-vue/app/components/editor/EditorShortcutHint.vue +17 -0
- package/frontend-vue/app/components/editor/EditorSimChip.vue +51 -0
- package/frontend-vue/app/components/editor/EditorTemplatePane.vue +131 -0
- package/frontend-vue/app/components/editor/EditorTestDataPane.vue +98 -0
- package/frontend-vue/app/components/editor/EditorTitle.vue +82 -0
- package/frontend-vue/app/components/editor/EditorTokenField.vue +224 -0
- package/frontend-vue/app/components/editor/EditorToolbar.vue +34 -134
- package/frontend-vue/app/components/editor/EditorVariableForm.vue +100 -0
- package/frontend-vue/app/components/editor/EditorVariableRow.vue +87 -0
- package/frontend-vue/app/components/editor/EditorVariablesPane.vue +161 -0
- package/frontend-vue/app/components/editor/EditorViewSwitches.vue +76 -0
- package/frontend-vue/app/components/editor/EditorVisibility.vue +101 -0
- package/frontend-vue/app/components/editor/EditorWorkspace.vue +62 -149
- package/frontend-vue/app/composables/useBlockContext.ts +16 -0
- package/frontend-vue/app/composables/useEditorActions.ts +381 -0
- package/frontend-vue/app/composables/useEditorAnnouncements.ts +51 -0
- package/frontend-vue/app/composables/useEditorContext.ts +30 -23
- package/frontend-vue/app/composables/useEditorSession.ts +299 -0
- package/frontend-vue/app/composables/useEditorShortcuts.ts +81 -0
- package/frontend-vue/app/composables/useLocalizedDefaults.ts +17 -0
- package/frontend-vue/app/composables/useMailingApi.ts +142 -27
- package/frontend-vue/app/composables/useOverviewSetup.ts +13 -0
- package/frontend-vue/app/composables/useReplayToast.ts +72 -0
- package/frontend-vue/app/composables/useTemplateCategories.ts +35 -0
- package/frontend-vue/app/composables/useTemplateDrawer.ts +196 -62
- package/frontend-vue/app/composables/useTemplateEditor.ts +318 -86
- package/frontend-vue/app/emails/EmailMailingTemplate.vue +16 -4
- package/frontend-vue/app/emails/components/MailingBlocks.vue +96 -38
- package/frontend-vue/app/emails/components/MailingTextLines.vue +20 -0
- package/frontend-vue/app/plugins/column-displays.ts +65 -0
- package/frontend-vue/app/plugins/{template-gallery-display.client.ts → template-gallery-display.ts} +1 -1
- package/frontend-vue/app/types/mailing.ts +193 -18
- package/frontend-vue/app/utils/api-error.ts +33 -0
- package/frontend-vue/app/utils/autocomplete.ts +120 -0
- package/frontend-vue/app/utils/blocks/components.ts +10 -10
- package/frontend-vue/app/utils/blocks/dnd.ts +7 -3
- package/frontend-vue/app/utils/blocks/factory.ts +199 -38
- package/frontend-vue/app/utils/blocks/fields.ts +39 -19
- package/frontend-vue/app/utils/blocks/index.ts +1 -0
- package/frontend-vue/app/utils/blocks/outline.ts +91 -0
- package/frontend-vue/app/utils/blocks/tree.ts +114 -2
- package/frontend-vue/app/utils/categories.ts +201 -15
- package/frontend-vue/app/utils/changes.ts +152 -0
- package/frontend-vue/app/utils/conditions.ts +25 -19
- package/frontend-vue/app/utils/editor-checks.ts +266 -0
- package/frontend-vue/app/utils/editor-route.ts +43 -4
- package/frontend-vue/app/utils/funnel.ts +74 -0
- package/frontend-vue/app/utils/gallery.ts +179 -1
- package/frontend-vue/app/utils/history.ts +66 -0
- package/frontend-vue/app/utils/lifecycle.ts +185 -0
- package/frontend-vue/app/utils/locales.ts +107 -2
- package/frontend-vue/app/utils/overview-setup.ts +83 -0
- package/frontend-vue/app/utils/pages.ts +5 -0
- package/frontend-vue/app/utils/paths.ts +51 -15
- package/frontend-vue/app/utils/period.ts +62 -22
- package/frontend-vue/app/utils/problems.ts +181 -0
- package/frontend-vue/app/utils/recipients.ts +49 -0
- package/frontend-vue/app/utils/rendering-title.ts +23 -0
- package/frontend-vue/app/utils/send-actions.ts +207 -0
- package/frontend-vue/app/utils/send-link.ts +28 -0
- package/frontend-vue/app/utils/send-snippet.ts +97 -0
- package/frontend-vue/app/utils/send-stats.ts +24 -0
- package/frontend-vue/app/utils/send-values.ts +156 -0
- package/frontend-vue/app/utils/shortcuts.ts +113 -0
- package/frontend-vue/app/utils/slug.ts +82 -0
- package/frontend-vue/app/utils/starters.ts +23 -0
- package/frontend-vue/app/utils/suppression.ts +112 -0
- package/frontend-vue/app/utils/system-variables.ts +165 -0
- package/frontend-vue/app/utils/template-attention.ts +104 -0
- package/frontend-vue/app/utils/timeline.ts +221 -19
- package/frontend-vue/app/utils/tokens.ts +115 -13
- package/frontend-vue/app/utils/variable-types.ts +23 -0
- package/frontend-vue/app/utils/variables.ts +208 -1
- package/frontend-vue/app/utils/version-history.ts +53 -0
- package/frontend-vue/app/utils/webhook-status.ts +170 -0
- package/frontend-vue/dms.frontend.ts +19 -11
- package/frontend-vue/i18n/locales/mailing-en-GB.json +1173 -161
- package/frontend-vue/i18n/locales/mailing-fr-FR.json +1220 -208
- package/frontend-vue/package.json +2 -2
- package/frontend-vue/pnpm-lock.yaml +46 -161
- package/frontend-vue/tests/api-error.test.ts +27 -0
- package/frontend-vue/tests/autocomplete.test.ts +84 -0
- package/frontend-vue/tests/block-components.test.ts +1 -1
- package/frontend-vue/tests/block-fields.test.ts +12 -2
- package/frontend-vue/tests/blocks-factory.test.ts +46 -0
- package/frontend-vue/tests/blocks-outline.test.ts +102 -0
- package/frontend-vue/tests/blocks-tree.test.ts +28 -1
- package/frontend-vue/tests/categories-input.test.ts +139 -0
- package/frontend-vue/tests/categories.test.ts +148 -15
- package/frontend-vue/tests/changes.test.ts +136 -0
- package/frontend-vue/tests/column-displays.test.ts +73 -0
- package/frontend-vue/tests/conditions.test.ts +49 -19
- package/frontend-vue/tests/dnd.test.ts +10 -0
- package/frontend-vue/tests/editor-block-list.test.ts +24 -2
- package/frontend-vue/tests/editor-checks.test.ts +178 -0
- package/frontend-vue/tests/editor-route.test.ts +37 -9
- package/frontend-vue/tests/editor-session.test.ts +206 -0
- package/frontend-vue/tests/footer-block.test.ts +88 -0
- package/frontend-vue/tests/funnel-card.test.ts +116 -0
- package/frontend-vue/tests/funnel.test.ts +81 -0
- package/frontend-vue/tests/gallery.test.ts +124 -2
- package/frontend-vue/tests/helpers/nuxt-globals.ts +232 -0
- package/frontend-vue/tests/history.test.ts +48 -0
- package/frontend-vue/tests/i18n-editor.test.ts +38 -0
- package/frontend-vue/tests/i18n.test.ts +55 -0
- package/frontend-vue/tests/lifecycle.test.ts +145 -0
- package/frontend-vue/tests/locales.test.ts +60 -0
- package/frontend-vue/tests/localized-defaults.test.ts +54 -0
- package/frontend-vue/tests/new-template-modal.test.ts +210 -0
- package/frontend-vue/tests/overview-gate.test.ts +146 -0
- package/frontend-vue/tests/overview-setup.test.ts +78 -0
- package/frontend-vue/tests/paths.test.ts +23 -0
- package/frontend-vue/tests/period.test.ts +59 -10
- package/frontend-vue/tests/problems.test.ts +153 -0
- package/frontend-vue/tests/recipients.test.ts +47 -0
- package/frontend-vue/tests/rendering-title.test.ts +24 -0
- package/frontend-vue/tests/replay-toast.test.ts +67 -0
- package/frontend-vue/tests/retention-input.test.ts +129 -0
- package/frontend-vue/tests/send-actions.test.ts +128 -0
- package/frontend-vue/tests/send-modals.test.ts +415 -0
- package/frontend-vue/tests/send-snippet.test.ts +51 -0
- package/frontend-vue/tests/send-stats.test.ts +12 -0
- package/frontend-vue/tests/send-values.test.ts +119 -0
- package/frontend-vue/tests/shortcuts.test.ts +75 -0
- package/frontend-vue/tests/slug.test.ts +88 -0
- package/frontend-vue/tests/suppression.test.ts +159 -0
- package/frontend-vue/tests/system-variables.test.ts +133 -0
- package/frontend-vue/tests/template-attention.test.ts +123 -0
- package/frontend-vue/tests/template-drawer.test.ts +449 -0
- package/frontend-vue/tests/template-editor.test.ts +218 -9
- package/frontend-vue/tests/template-gallery-display.test.ts +32 -0
- package/frontend-vue/tests/template-gallery.test.ts +370 -0
- package/frontend-vue/tests/template-overlays.test.ts +125 -0
- package/frontend-vue/tests/timeline.test.ts +175 -20
- package/frontend-vue/tests/tokens.test.ts +79 -1
- package/frontend-vue/tests/variable-path.test.ts +22 -0
- package/frontend-vue/tests/variable-types.test.ts +20 -0
- package/frontend-vue/tests/variables.test.ts +90 -2
- package/frontend-vue/tests/version-history.test.ts +41 -0
- package/frontend-vue/tests/webhook-status.test.ts +108 -0
- package/package.json +11 -9
- package/src/automation/email-event-trigger.ts +3 -3
- package/src/automation/send-template-action.ts +7 -5
- package/src/config.ts +3 -1
- package/src/constants.ts +18 -2
- package/src/crons/attention.ts +21 -0
- package/src/crons/index.ts +2 -0
- package/src/crons/jobs.ts +35 -0
- package/src/crons/retention.ts +7 -22
- package/src/data/displays.ts +18 -0
- package/src/data/index.ts +1 -0
- package/src/data/locale-fallback.ts +26 -0
- package/src/data/read-only-routes.ts +14 -0
- package/src/data/sends-table.ts +204 -19
- package/src/data/suppressions-table.ts +127 -0
- package/src/data/templates-table.ts +295 -63
- package/src/data-types/categories-type.ts +4 -5
- package/src/data-types/category-type.ts +1 -1
- package/src/db/models/index.ts +3 -0
- package/src/db/models/sends.model.ts +102 -1
- package/src/db/models/settings.model.ts +2 -12
- package/src/db/models/suppressions.model.ts +53 -0
- package/src/db/models/template-versions.model.ts +23 -0
- package/src/db/models/templates.model.ts +17 -0
- package/src/db/models/webhook-status.model.ts +25 -0
- package/src/db/tables/index.ts +3 -0
- package/src/db/tables/send-events.table.ts +6 -2
- package/src/db/tables/sends.table.ts +37 -2
- package/src/db/tables/suppressions.table.ts +46 -0
- package/src/db/tables/template-versions.table.ts +44 -0
- package/src/db/tables/templates.table.ts +67 -5
- package/src/db/tables/webhook-status.table.ts +44 -0
- package/src/engine/changes.ts +198 -0
- package/src/engine/conditions.ts +1 -7
- package/src/engine/coverage.ts +7 -2
- package/src/engine/fallback-html.ts +80 -42
- package/src/engine/index.ts +3 -0
- package/src/engine/plain-text.ts +86 -0
- package/src/engine/resolve.ts +49 -15
- package/src/engine/system-variables.ts +124 -0
- package/src/engine/tokens.ts +13 -1
- package/src/hooks/tenant-export.ts +21 -2
- package/src/implementations/dms-mailing/index.ts +1 -1
- package/src/index.ts +29 -13
- package/src/pages/editor.ts +13 -2
- package/src/pages/index.ts +6 -3
- package/src/pages/module.ts +31 -2
- package/src/pages/overview.ts +187 -128
- package/src/pages/sends.ts +271 -112
- package/src/pages/settings/form.ts +144 -54
- package/src/pages/settings/page.ts +9 -17
- package/src/pages/suppressions.ts +157 -0
- package/src/pages/templates.ts +139 -42
- package/src/permissions.ts +6 -4
- package/src/quick-actions.ts +12 -5
- package/src/routes/events.ts +110 -33
- package/src/routes/index.ts +2 -0
- package/src/routes/metrics.ts +348 -98
- package/src/routes/period.ts +23 -6
- package/src/routes/sends.ts +413 -41
- package/src/routes/settings.ts +291 -26
- package/src/routes/suppressions.ts +87 -0
- package/src/routes/template-versions.ts +116 -0
- package/src/routes/templates.ts +334 -48
- package/src/services/attention.ts +283 -30
- package/src/services/catalog.ts +28 -0
- package/src/services/categories.ts +28 -27
- package/src/services/checked-send.ts +123 -0
- package/src/services/events.ts +174 -20
- package/src/services/metrics.ts +137 -28
- package/src/services/notify.ts +126 -44
- package/src/services/provider-banner.ts +165 -0
- package/src/services/render.ts +1 -0
- package/src/services/send-stat-items.ts +160 -0
- package/src/services/send-stats.ts +122 -0
- package/src/services/send.ts +193 -31
- package/src/services/settings.ts +52 -8
- package/src/services/starters.ts +383 -0
- package/src/services/suppressions.ts +116 -0
- package/src/services/template-attention.ts +82 -0
- package/src/services/template-stats.ts +97 -0
- package/src/services/templates.ts +400 -37
- package/src/services/webhook-status.ts +202 -0
- package/src/types/blocks.ts +6 -0
- package/src/types/index.ts +2 -0
- package/src/types/sends.ts +64 -2
- package/src/types/suppressions.ts +27 -0
- package/src/types/webhook.ts +29 -0
- package/src/validation/settings.schema.ts +173 -17
- package/src/validation/templates.schema.ts +53 -1
- package/dist/antelope-modules.d.ts +0 -32
- package/dist/antelope-modules.js +0 -36
- package/dist/antelope-modules.js.map +0 -1
- package/dist/test/antelope.test.d.ts +0 -2
- package/dist/test/antelope.test.js +0 -180
- package/dist/test/antelope.test.js.map +0 -1
- package/dist/test/helpers/http.d.ts +0 -4
- package/dist/test/helpers/http.js +0 -28
- package/dist/test/helpers/http.js.map +0 -1
- package/dist/test/helpers/owner.d.ts +0 -5
- package/dist/test/helpers/owner.js +0 -30
- package/dist/test/helpers/owner.js.map +0 -1
- package/dist/test/integration/events.test.d.ts +0 -1
- package/dist/test/integration/events.test.js +0 -143
- package/dist/test/integration/events.test.js.map +0 -1
- package/dist/test/integration/retention.test.d.ts +0 -1
- package/dist/test/integration/retention.test.js +0 -291
- package/dist/test/integration/retention.test.js.map +0 -1
- package/dist/test/integration/sends.test.d.ts +0 -1
- package/dist/test/integration/sends.test.js +0 -210
- package/dist/test/integration/sends.test.js.map +0 -1
- package/dist/test/integration/settings.test.d.ts +0 -1
- package/dist/test/integration/settings.test.js +0 -137
- package/dist/test/integration/settings.test.js.map +0 -1
- package/dist/test/integration/setup.test.d.ts +0 -1
- package/dist/test/integration/setup.test.js +0 -32
- package/dist/test/integration/setup.test.js.map +0 -1
- package/dist/test/integration/templates.test.d.ts +0 -1
- package/dist/test/integration/templates.test.js +0 -233
- package/dist/test/integration/templates.test.js.map +0 -1
- package/dist/test/integration/tenant-export.test.d.ts +0 -1
- package/dist/test/integration/tenant-export.test.js +0 -85
- package/dist/test/integration/tenant-export.test.js.map +0 -1
- package/dist/test/unit/attention.test.d.ts +0 -1
- package/dist/test/unit/attention.test.js +0 -109
- package/dist/test/unit/attention.test.js.map +0 -1
- package/dist/test/unit/automation.test.d.ts +0 -1
- package/dist/test/unit/automation.test.js +0 -104
- package/dist/test/unit/automation.test.js.map +0 -1
- package/dist/test/unit/categories.test.d.ts +0 -1
- package/dist/test/unit/categories.test.js +0 -38
- package/dist/test/unit/categories.test.js.map +0 -1
- package/dist/test/unit/engine/conditions.test.d.ts +0 -1
- package/dist/test/unit/engine/conditions.test.js +0 -23
- package/dist/test/unit/engine/conditions.test.js.map +0 -1
- package/dist/test/unit/engine/coverage.test.d.ts +0 -1
- package/dist/test/unit/engine/coverage.test.js +0 -106
- package/dist/test/unit/engine/coverage.test.js.map +0 -1
- package/dist/test/unit/engine/fallback-html.test.d.ts +0 -1
- package/dist/test/unit/engine/fallback-html.test.js +0 -78
- package/dist/test/unit/engine/fallback-html.test.js.map +0 -1
- package/dist/test/unit/engine/paths.test.d.ts +0 -1
- package/dist/test/unit/engine/paths.test.js +0 -33
- package/dist/test/unit/engine/paths.test.js.map +0 -1
- package/dist/test/unit/engine/resolve.test.d.ts +0 -1
- package/dist/test/unit/engine/resolve.test.js +0 -157
- package/dist/test/unit/engine/resolve.test.js.map +0 -1
- package/dist/test/unit/engine/tokens.test.d.ts +0 -1
- package/dist/test/unit/engine/tokens.test.js +0 -26
- package/dist/test/unit/engine/tokens.test.js.map +0 -1
- package/dist/test/unit/events.test.d.ts +0 -1
- package/dist/test/unit/events.test.js +0 -13
- package/dist/test/unit/events.test.js.map +0 -1
- package/dist/test/unit/metrics.test.d.ts +0 -1
- package/dist/test/unit/metrics.test.js +0 -108
- package/dist/test/unit/metrics.test.js.map +0 -1
- package/dist/test/unit/notify.test.d.ts +0 -1
- package/dist/test/unit/notify.test.js +0 -49
- package/dist/test/unit/notify.test.js.map +0 -1
- package/dist/test/unit/period.test.d.ts +0 -1
- package/dist/test/unit/period.test.js +0 -61
- package/dist/test/unit/period.test.js.map +0 -1
- package/dist/test/unit/permissions.test.d.ts +0 -1
- package/dist/test/unit/permissions.test.js +0 -27
- package/dist/test/unit/permissions.test.js.map +0 -1
- package/dist/test/unit/provider.test.d.ts +0 -1
- package/dist/test/unit/provider.test.js +0 -47
- package/dist/test/unit/provider.test.js.map +0 -1
- package/dist/test/unit/realtime.test.d.ts +0 -1
- package/dist/test/unit/realtime.test.js +0 -13
- package/dist/test/unit/realtime.test.js.map +0 -1
- package/dist/test/unit/render.test.d.ts +0 -1
- package/dist/test/unit/render.test.js +0 -23
- package/dist/test/unit/render.test.js.map +0 -1
- package/dist/test/unit/retention.test.d.ts +0 -1
- package/dist/test/unit/retention.test.js +0 -66
- package/dist/test/unit/retention.test.js.map +0 -1
- package/dist/test/unit/send-batch.test.d.ts +0 -1
- package/dist/test/unit/send-batch.test.js +0 -46
- package/dist/test/unit/send-batch.test.js.map +0 -1
- package/dist/test/unit/send.test.d.ts +0 -1
- package/dist/test/unit/send.test.js +0 -120
- package/dist/test/unit/send.test.js.map +0 -1
- package/dist/test/unit/smoke.test.d.ts +0 -1
- package/dist/test/unit/smoke.test.js +0 -10
- package/dist/test/unit/smoke.test.js.map +0 -1
- package/dist/test/unit/status.test.d.ts +0 -1
- package/dist/test/unit/status.test.js +0 -16
- package/dist/test/unit/status.test.js.map +0 -1
- package/dist/test/unit/templates.test.d.ts +0 -1
- package/dist/test/unit/templates.test.js +0 -48
- package/dist/test/unit/templates.test.js.map +0 -1
- package/dist/test/unit/tenant-export.test.d.ts +0 -1
- package/dist/test/unit/tenant-export.test.js +0 -59
- package/dist/test/unit/tenant-export.test.js.map +0 -1
- package/dist/test/unit/webhook-secret.test.d.ts +0 -1
- package/dist/test/unit/webhook-secret.test.js +0 -43
- package/dist/test/unit/webhook-secret.test.js.map +0 -1
- package/frontend-vue/app/components/AttentionCard.vue +0 -78
- package/frontend-vue/app/components/HtmlPreviewModal.vue +0 -20
- package/frontend-vue/app/components/ProviderChip.vue +0 -39
- package/frontend-vue/app/components/editor/EditorBlockSettings.vue +0 -186
- package/frontend-vue/app/components/editor/EditorDataPanel.vue +0 -130
- package/frontend-vue/app/components/editor/EditorDraftBar.vue +0 -35
- package/frontend-vue/app/components/editor/EditorRail.vue +0 -58
- package/frontend-vue/app/components/editor/EditorSubjectSettings.vue +0 -37
- package/frontend-vue/app/components/editor/EditorTemplateSettings.vue +0 -87
- package/frontend-vue/app/components/editor/EditorTestSendModal.vue +0 -66
- package/frontend-vue/app/components/editor/EditorVariables.vue +0 -163
- package/frontend-vue/app/composables/useMailingPeriodData.ts +0 -39
- package/src/antelope-modules.ts +0 -48
- package/src/test/antelope.test.ts +0 -190
- package/src/test/helpers/http.ts +0 -23
- package/src/test/helpers/owner.ts +0 -37
- package/src/test/integration/events.test.ts +0 -159
- package/src/test/integration/retention.test.ts +0 -363
- package/src/test/integration/sends.test.ts +0 -233
- package/src/test/integration/settings.test.ts +0 -128
- package/src/test/integration/setup.test.ts +0 -35
- package/src/test/integration/templates.test.ts +0 -272
- package/src/test/integration/tenant-export.test.ts +0 -125
- package/src/test/unit/attention.test.ts +0 -129
- package/src/test/unit/automation.test.ts +0 -130
- package/src/test/unit/categories.test.ts +0 -51
- package/src/test/unit/engine/conditions.test.ts +0 -35
- package/src/test/unit/engine/coverage.test.ts +0 -118
- package/src/test/unit/engine/fallback-html.test.ts +0 -86
- package/src/test/unit/engine/paths.test.ts +0 -39
- package/src/test/unit/engine/resolve.test.ts +0 -176
- package/src/test/unit/engine/tokens.test.ts +0 -26
- package/src/test/unit/events.test.ts +0 -19
- package/src/test/unit/metrics.test.ts +0 -170
- package/src/test/unit/notify.test.ts +0 -71
- package/src/test/unit/period.test.ts +0 -91
- package/src/test/unit/permissions.test.ts +0 -30
- package/src/test/unit/provider.test.ts +0 -57
- package/src/test/unit/realtime.test.ts +0 -11
- package/src/test/unit/render.test.ts +0 -23
- package/src/test/unit/retention.test.ts +0 -91
- package/src/test/unit/send-batch.test.ts +0 -72
- package/src/test/unit/send.test.ts +0 -161
- package/src/test/unit/smoke.test.ts +0 -8
- package/src/test/unit/status.test.ts +0 -14
- package/src/test/unit/templates.test.ts +0 -52
- package/src/test/unit/tenant-export.test.ts +0 -88
- 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 {
|
|
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 {
|
|
35
|
-
import {
|
|
36
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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 =
|
|
195
|
+
const { seed, normalized } = parseCreation(body);
|
|
76
196
|
const tenantId = getRequestTenantId(ctx);
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
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
|
|
231
|
+
async function deleteRefusal(
|
|
114
232
|
tenantId: string,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 {
|
|
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
|
|
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("
|
|
20
|
+
return CustomComponent("MailingCategoriesInput")
|
|
22
21
|
.options(this.options)
|
|
23
22
|
.serializeSync();
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
getValidation() {
|
|
27
|
-
return
|
|
26
|
+
return categoriesSchema;
|
|
28
27
|
}
|
|
29
28
|
}
|
package/src/db/models/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
) {}
|