@antelopejs/dms-mailing 0.7.1 → 0.9.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 +46 -31
- package/dist/automation/email-event-trigger.d.ts +2 -2
- package/dist/automation/send-template-action.d.ts +11 -3
- package/dist/automation/send-template-action.js +1 -1
- package/dist/automation/send-template-action.js.map +1 -1
- package/dist/crons/jobs.d.ts +1 -1
- package/dist/data/body.d.ts +2 -1
- package/dist/data/body.js +4 -2
- package/dist/data/body.js.map +1 -1
- package/dist/data/read-only-routes.d.ts +4 -4
- package/dist/data/sends-table.d.ts +4 -4
- package/dist/data/suppressions-table.d.ts +4 -4
- package/dist/data/templates-table.d.ts +35 -12
- package/dist/data/templates-table.js +30 -24
- package/dist/data/templates-table.js.map +1 -1
- package/dist/data-types/categories-type.d.ts +1 -2
- package/dist/data-types/categories-type.js +3 -5
- package/dist/data-types/categories-type.js.map +1 -1
- package/dist/data-types/category-type.d.ts +1 -2
- package/dist/data-types/category-type.js +3 -5
- package/dist/data-types/category-type.js.map +1 -1
- package/dist/db/models/send-events.model.d.ts +7 -5
- package/dist/db/models/sends.model.d.ts +7 -5
- package/dist/db/models/sends.model.js.map +1 -1
- package/dist/db/models/settings.model.d.ts +7 -5
- package/dist/db/models/suppressions.model.d.ts +7 -5
- package/dist/db/models/template-versions.model.d.ts +7 -5
- package/dist/db/models/template-versions.model.js +4 -3
- package/dist/db/models/template-versions.model.js.map +1 -1
- package/dist/db/models/templates.model.d.ts +7 -5
- package/dist/db/models/templates.model.js +2 -1
- package/dist/db/models/templates.model.js.map +1 -1
- package/dist/db/models/webhook-status.model.d.ts +7 -5
- package/dist/db/tables/templates.table.js.map +1 -1
- package/dist/engine/changes.js +8 -18
- package/dist/engine/changes.js.map +1 -1
- package/dist/engine/conditions.d.ts +2 -1
- package/dist/engine/conditions.js +5 -5
- package/dist/engine/conditions.js.map +1 -1
- package/dist/engine/coverage.d.ts +2 -1
- package/dist/engine/coverage.js.map +1 -1
- package/dist/engine/fallback-html.js +8 -8
- package/dist/engine/fallback-html.js.map +1 -1
- package/dist/engine/formats.d.ts +4 -6
- package/dist/engine/formats.js +22 -8
- package/dist/engine/formats.js.map +1 -1
- package/dist/engine/paths.d.ts +4 -2
- package/dist/engine/paths.js +16 -5
- package/dist/engine/paths.js.map +1 -1
- package/dist/engine/plain-text.js +2 -4
- package/dist/engine/plain-text.js.map +1 -1
- package/dist/engine/resolve.d.ts +2 -1
- package/dist/engine/resolve.js +1 -1
- package/dist/engine/resolve.js.map +1 -1
- package/dist/engine/system-variables.d.ts +2 -1
- package/dist/engine/system-variables.js +1 -1
- package/dist/engine/system-variables.js.map +1 -1
- package/dist/engine/tokens.d.ts +2 -1
- package/dist/engine/tokens.js.map +1 -1
- package/dist/hooks/tenant-export.js +10 -9
- package/dist/hooks/tenant-export.js.map +1 -1
- package/dist/pages/sends.js.map +1 -1
- package/dist/pages/templates.js +3 -1
- package/dist/pages/templates.js.map +1 -1
- package/dist/pages/toolbar.js.map +1 -1
- package/dist/routes/events.js +6 -4
- package/dist/routes/events.js.map +1 -1
- package/dist/routes/index.d.ts +1 -0
- package/dist/routes/index.js +1 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/metrics.js +31 -31
- package/dist/routes/metrics.js.map +1 -1
- package/dist/routes/period.js +5 -5
- package/dist/routes/period.js.map +1 -1
- package/dist/routes/sends.js +4 -5
- package/dist/routes/sends.js.map +1 -1
- package/dist/routes/settings.d.ts +1 -1
- package/dist/routes/settings.js +16 -14
- package/dist/routes/settings.js.map +1 -1
- package/dist/routes/suppressions.js +9 -6
- package/dist/routes/suppressions.js.map +1 -1
- package/dist/routes/template-content.d.ts +27 -0
- package/dist/routes/template-content.js +3 -0
- package/dist/routes/template-content.js.map +1 -0
- package/dist/routes/templates.d.ts +7 -30
- package/dist/routes/templates.js +7 -6
- package/dist/routes/templates.js.map +1 -1
- package/dist/routes/zod-json.d.ts +3 -0
- package/dist/routes/zod-json.js +21 -0
- package/dist/routes/zod-json.js.map +1 -0
- package/dist/services/attention.js +16 -8
- package/dist/services/attention.js.map +1 -1
- package/dist/services/catalog.js +4 -2
- package/dist/services/catalog.js.map +1 -1
- package/dist/services/categories.js +6 -6
- package/dist/services/categories.js.map +1 -1
- package/dist/services/checked-send.js +16 -9
- package/dist/services/checked-send.js.map +1 -1
- package/dist/services/declaration-error.d.ts +1 -1
- package/dist/services/declaration-error.js.map +1 -1
- package/dist/services/declarations.js +4 -1
- package/dist/services/declarations.js.map +1 -1
- package/dist/services/events.js +13 -7
- package/dist/services/events.js.map +1 -1
- package/dist/services/localized-text.d.ts +2 -1
- package/dist/services/localized-text.js +2 -1
- package/dist/services/localized-text.js.map +1 -1
- package/dist/services/metrics.js +2 -5
- package/dist/services/metrics.js.map +1 -1
- package/dist/services/render.js.map +1 -1
- package/dist/services/send-import.js +7 -3
- package/dist/services/send-import.js.map +1 -1
- package/dist/services/send.js +7 -7
- package/dist/services/send.js.map +1 -1
- package/dist/services/settings.d.ts +4 -2
- package/dist/services/settings.js +5 -4
- package/dist/services/settings.js.map +1 -1
- package/dist/services/starters.d.ts +3 -2
- package/dist/services/starters.js +5 -5
- package/dist/services/starters.js.map +1 -1
- package/dist/services/template-stats.js +5 -2
- package/dist/services/template-stats.js.map +1 -1
- package/dist/services/templates.d.ts +2 -1
- package/dist/services/templates.js +8 -4
- package/dist/services/templates.js.map +1 -1
- package/dist/services/webhook-status.d.ts +2 -2
- package/dist/services/webhook-status.js +6 -4
- package/dist/services/webhook-status.js.map +1 -1
- package/dist/types/json.d.ts +1 -0
- package/dist/types/json.js +3 -0
- package/dist/types/json.js.map +1 -0
- package/dist/types/settings.d.ts +2 -2
- package/dist/validation/declarations.schema.d.ts +44 -44
- package/dist/validation/declarations.schema.js +6 -5
- package/dist/validation/declarations.schema.js.map +1 -1
- package/dist/validation/guards.d.ts +3 -0
- package/dist/validation/guards.js +15 -0
- package/dist/validation/guards.js.map +1 -0
- package/dist/validation/json.d.ts +5 -0
- package/dist/validation/json.js +54 -0
- package/dist/validation/json.js.map +1 -0
- package/dist/validation/json.schema.d.ts +2 -0
- package/dist/validation/json.schema.js +9 -0
- package/dist/validation/json.schema.js.map +1 -0
- package/dist/validation/send-event-type.schema.d.ts +2 -0
- package/dist/validation/send-event-type.schema.js +11 -0
- package/dist/validation/send-event-type.schema.js.map +1 -0
- package/dist/validation/settings.schema.js +1 -1
- package/dist/validation/settings.schema.js.map +1 -1
- package/dist/validation/templates.schema.d.ts +44 -42
- package/dist/validation/templates.schema.js +4 -3
- package/dist/validation/templates.schema.js.map +1 -1
- package/frontend-vue/app/build/components/TemplateDrawerFooter.vue +45 -0
- package/frontend-vue/app/build/components/TemplateDrawerPerformance.vue +102 -0
- package/frontend-vue/app/build/components/TemplateDrawerVariables.vue +110 -0
- package/frontend-vue/app/build/composables/useDrawerStats.ts +78 -0
- package/frontend-vue/app/components/DuplicateModal.vue +3 -11
- package/frontend-vue/app/components/FixAddressModal.vue +2 -1
- package/frontend-vue/app/components/NewTemplateModal.vue +3 -11
- package/frontend-vue/app/components/RealSendModal.vue +6 -17
- package/frontend-vue/app/components/RenderingModal.vue +2 -1
- package/frontend-vue/app/components/SendDrawer.vue +5 -4
- package/frontend-vue/app/components/StarterGallery.vue +3 -11
- package/frontend-vue/app/components/TemplateDrawer.vue +40 -269
- package/frontend-vue/app/components/TestSendModal.vue +7 -18
- package/frontend-vue/app/components/VersionHistory.vue +2 -7
- package/frontend-vue/app/components/WebhookStatus.vue +2 -1
- package/frontend-vue/app/components/editor/EditorWorkspace.vue +2 -7
- package/frontend-vue/build/backend-boundary.ts +122 -0
- package/frontend-vue/dms.frontend.build.ts +7 -0
- package/frontend-vue/eslint.config.mjs +13 -0
- package/frontend-vue/package.json +4 -2
- package/frontend-vue/pnpm-lock.yaml +250 -61
- package/frontend-vue/tests/backend-boundary.test.ts +203 -0
- package/frontend-vue/tests/dnd.test.ts +1 -2
- package/frontend-vue/tests/editor-block-list.test.ts +136 -53
- package/frontend-vue/tests/helpers/auth-fetch.ts +123 -0
- package/frontend-vue/tests/helpers/nuxt-globals.ts +17 -1
- package/frontend-vue/tests/new-template-modal.test.ts +41 -6
- package/frontend-vue/tests/page-reads.test.ts +26 -6
- package/frontend-vue/tests/send-actions.test.ts +0 -1
- package/frontend-vue/tests/send-drawer.test.ts +2 -6
- package/frontend-vue/tests/send-modals.test.ts +8 -5
- package/frontend-vue/tests/sortable-list.test.ts +54 -62
- package/frontend-vue/tests/suppression.test.ts +1 -2
- package/frontend-vue/tests/template-drawer.test.ts +27 -18
- package/frontend-vue/tests/template-gallery.test.ts +7 -5
- package/frontend-vue/tests/template-preview-frame.test.ts +2 -5
- package/package.json +16 -20
- package/skills/dms-mailing/SKILL.md +1 -1
- package/skills/dms-mailing/cases.json +49 -0
- package/src/automation/email-event-trigger.ts +2 -2
- package/src/automation/send-template-action.ts +33 -14
- package/src/crons/jobs.ts +1 -1
- package/src/data/body.ts +7 -3
- package/src/data/templates-table.ts +55 -56
- package/src/data-types/categories-type.ts +2 -2
- package/src/data-types/category-type.ts +2 -2
- package/src/db/models/sends.model.ts +2 -2
- package/src/db/models/template-versions.model.ts +10 -4
- package/src/db/models/templates.model.ts +6 -3
- package/src/db/tables/templates.table.ts +1 -1
- package/src/engine/changes.ts +11 -28
- package/src/engine/conditions.ts +14 -8
- package/src/engine/coverage.ts +2 -1
- package/src/engine/fallback-html.ts +13 -13
- package/src/engine/formats.ts +35 -21
- package/src/engine/paths.ts +41 -7
- package/src/engine/plain-text.ts +2 -4
- package/src/engine/resolve.ts +12 -4
- package/src/engine/system-variables.ts +6 -4
- package/src/engine/tokens.ts +3 -3
- package/src/hooks/tenant-export.ts +11 -13
- package/src/pages/sends.ts +5 -3
- package/src/pages/templates.ts +4 -2
- package/src/pages/toolbar.ts +4 -2
- package/src/routes/events.ts +10 -10
- package/src/routes/index.ts +1 -0
- package/src/routes/metrics.ts +35 -33
- package/src/routes/period.ts +5 -5
- package/src/routes/sends.ts +23 -26
- package/src/routes/settings.ts +21 -17
- package/src/routes/suppressions.ts +12 -8
- package/src/routes/template-content.ts +45 -0
- package/src/routes/templates.ts +16 -49
- package/src/routes/zod-json.ts +28 -0
- package/src/services/attention.ts +20 -18
- package/src/services/catalog.ts +4 -8
- package/src/services/categories.ts +6 -6
- package/src/services/checked-send.ts +17 -19
- package/src/services/declaration-error.ts +1 -1
- package/src/services/declarations.ts +7 -6
- package/src/services/events.ts +18 -13
- package/src/services/localized-text.ts +4 -2
- package/src/services/metrics.ts +5 -7
- package/src/services/notify.ts +1 -1
- package/src/services/render.ts +5 -1
- package/src/services/send-import.ts +16 -11
- package/src/services/send.ts +10 -13
- package/src/services/settings.ts +17 -16
- package/src/services/starters.ts +12 -8
- package/src/services/template-stats.ts +11 -4
- package/src/services/templates.ts +24 -15
- package/src/services/webhook-status.ts +13 -8
- package/src/types/json.ts +1 -0
- package/src/types/settings.ts +2 -2
- package/src/validation/declarations.schema.ts +6 -5
- package/src/validation/guards.ts +17 -0
- package/src/validation/json.schema.ts +7 -0
- package/src/validation/json.ts +66 -0
- package/src/validation/send-event-type.schema.ts +10 -0
- package/src/validation/settings.schema.ts +7 -2
- package/src/validation/templates.schema.ts +9 -4
- package/frontend-vue/tests/i18n-editor.test.ts +0 -50
- package/frontend-vue/tests/i18n.test.ts +0 -138
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/dms-mailing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "E-mail templates for the AntelopeJS DMS: a block editor with drafts and versions, a send log, delivery metrics and a suppression list.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antelopejs",
|
|
@@ -46,21 +46,23 @@
|
|
|
46
46
|
"registry": "https://registry.npmjs.org/"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@antelopejs/interface-api": ">=0.0
|
|
50
|
-
"@antelopejs/interface-api-util": ">=0.
|
|
51
|
-
"@antelopejs/interface-core": ">=0.
|
|
52
|
-
"@antelopejs/interface-data-api": ">=0.
|
|
53
|
-
"@antelopejs/interface-database": ">=0.
|
|
54
|
-
"@antelopejs/interface-database-decorators": ">=0.
|
|
55
|
-
"@antelopejs/interface-dms": ">=0.
|
|
56
|
-
"@antelopejs/interface-dms-mailing": ">=0.
|
|
49
|
+
"@antelopejs/interface-api": ">=0.3.0 <1.0.0",
|
|
50
|
+
"@antelopejs/interface-api-util": ">=0.3.0 <1.0.0",
|
|
51
|
+
"@antelopejs/interface-core": ">=0.4.0 <1.0.0",
|
|
52
|
+
"@antelopejs/interface-data-api": ">=0.6.1 <1.0.0",
|
|
53
|
+
"@antelopejs/interface-database": ">=0.3.0 <1.0.0",
|
|
54
|
+
"@antelopejs/interface-database-decorators": ">=0.2.0 <1.0.0",
|
|
55
|
+
"@antelopejs/interface-dms": ">=0.12.0 <1.0.0",
|
|
56
|
+
"@antelopejs/interface-dms-mailing": ">=0.10.0 <0.11.0",
|
|
57
57
|
"@antelopejs/interface-email": ">=0.0.6 <1.0.0",
|
|
58
58
|
"node-cron": "^4.2.1",
|
|
59
59
|
"zod": "~3.24.2"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@antelopejs/core": "
|
|
63
|
-
"@antelopejs/
|
|
62
|
+
"@antelopejs/core": "^1.20.0",
|
|
63
|
+
"@antelopejs/dms": ">=0.16.1 <1.0.0",
|
|
64
|
+
"@antelopejs/mongodb": "^1.6.2",
|
|
65
|
+
"@antelopejs/tooling-configs": "^0.2.18",
|
|
64
66
|
"@types/chai": "^4.3.20",
|
|
65
67
|
"@types/mocha": "^10",
|
|
66
68
|
"@types/node": "^24.0.0",
|
|
@@ -69,16 +71,13 @@
|
|
|
69
71
|
"chai": "^4.5.0",
|
|
70
72
|
"knip": "^6.34.0",
|
|
71
73
|
"mongodb-memory-server-core": "^11.0.1",
|
|
72
|
-
"oxfmt": "^0.66.0",
|
|
73
|
-
"oxlint": "1.81.0",
|
|
74
|
-
"oxlint-tsgolint": "^7.0.2001",
|
|
75
74
|
"release-it": "^19.2.4",
|
|
76
75
|
"release-it-changelogen": "^0.1.0",
|
|
77
76
|
"rimraf": "^6.1.3",
|
|
78
77
|
"typescript": "^6.0.3"
|
|
79
78
|
},
|
|
80
79
|
"optionalDependencies": {
|
|
81
|
-
"@antelopejs/interface-dms-automation": ">=0.
|
|
80
|
+
"@antelopejs/interface-dms-automation": ">=0.6.0 <1.0.0"
|
|
82
81
|
},
|
|
83
82
|
"antelopeJs": {
|
|
84
83
|
"baseUrl": "dist",
|
|
@@ -92,17 +91,14 @@
|
|
|
92
91
|
},
|
|
93
92
|
"dmsFrontend": "dist/frontend-module.js",
|
|
94
93
|
"scripts": {
|
|
95
|
-
"build": "rimraf dist && tsc",
|
|
94
|
+
"build": "pnpm --filter @antelopejs/interface-dms-mailing build && rimraf dist && tsc",
|
|
96
95
|
"dev": "ajs project dev -w -p playground",
|
|
97
96
|
"dev:frontend": "cd playground && pnpm frontend:dev",
|
|
98
97
|
"frontend:dev": "cd playground && pnpm frontend:dev",
|
|
99
|
-
"lint": "oxlint && oxfmt --check . && cd frontend-vue && pnpm lint && antelopejs-check-interface-ranges",
|
|
100
|
-
"lint:fix": "oxlint --fix --max-warnings=1000000 && oxfmt . && cd frontend-vue && pnpm lint:fix",
|
|
101
|
-
"format": "oxfmt . && cd frontend-vue && pnpm format",
|
|
102
98
|
"typecheck": "tsc --noEmit",
|
|
103
99
|
"knip": "knip --dependencies",
|
|
104
100
|
"knip:all": "knip",
|
|
105
|
-
"release": "pnpm --filter @antelopejs/interface-dms-mailing build && pnpm --dir
|
|
101
|
+
"release": "pnpm --filter @antelopejs/interface-dms-mailing build && pnpm --dir ../.. install --frozen-lockfile && release-it",
|
|
106
102
|
"test": "pnpm run test:backend && pnpm run test:layer",
|
|
107
103
|
"test:backend": "([ -n \"$DMS_PREBUILT\" ] || pnpm run build) && ajs module test .",
|
|
108
104
|
"test:layer": "cd frontend-vue && pnpm test",
|
|
@@ -23,7 +23,7 @@ https://dms.antelopejs.com/docs/modules/mailing (sending from code and the webho
|
|
|
23
23
|
|
|
24
24
|
- `ajs project modules add @antelopejs/dms-mailing`, plus a module implementing `@antelopejs/interface-email`
|
|
25
25
|
(`@antelopejs/nodemailer`, `config: { ethereal: true }` in dev).
|
|
26
|
-
-
|
|
26
|
+
- The DMS backend and renderer releases the module runs on are in the README compatibility block and in `engines["@antelopejs/dms-frontend"]` of `frontend-vue/package.json`;
|
|
27
27
|
0.4 is the DMS 0.8 line. Mailing 0.6 implements `@antelopejs/interface-dms-mailing` `>=0.8.0 <0.9.0`, adding declaration and import calls;
|
|
28
28
|
interface 0.9 adds `DeclareSender`; `SendTemplate` / `RecordEmailEvent` callers do not change.
|
|
29
29
|
- Module option: `useHtmlRender` (default `true`, DMS html-render chain; `false` = built-in standalone HTML).
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"cases": [
|
|
4
|
+
{
|
|
5
|
+
"id": "mailing-send-template",
|
|
6
|
+
"area": "modules",
|
|
7
|
+
"style": "clean",
|
|
8
|
+
"prompt": "Send the welcome e-mail template when a user signs up.",
|
|
9
|
+
"expect": {
|
|
10
|
+
"skills": ["dms-mailing"],
|
|
11
|
+
"acceptable": ["dms", "dms-auth"],
|
|
12
|
+
"mustMention": ["SendTemplate"]
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "mailing-bounces",
|
|
17
|
+
"area": "modules",
|
|
18
|
+
"style": "clean",
|
|
19
|
+
"prompt": "Our e-mail provider reports bounces through a webhook. How do we record them?",
|
|
20
|
+
"expect": {
|
|
21
|
+
"skills": ["dms-mailing"],
|
|
22
|
+
"acceptable": ["dms"],
|
|
23
|
+
"mustMention": ["RecordEmailEvent"]
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "mailing-missing-variable-casual",
|
|
28
|
+
"area": "modules",
|
|
29
|
+
"style": "casual",
|
|
30
|
+
"prompt": "mail send says missing variable firstName??",
|
|
31
|
+
"expect": {
|
|
32
|
+
"skills": ["dms-mailing"],
|
|
33
|
+
"acceptable": ["dms"]
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "dms-mailing-avoid-tracker",
|
|
38
|
+
"area": "modules",
|
|
39
|
+
"style": "clean",
|
|
40
|
+
"prompt": "Put the tracking script on our public website and count how many visitors click the signup button.",
|
|
41
|
+
"expect": {
|
|
42
|
+
"skills": ["dms-marketing"],
|
|
43
|
+
"acceptable": ["dms"],
|
|
44
|
+
"mustMention": ["data-website-id"],
|
|
45
|
+
"avoid": ["dms-mailing"]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
@@ -5,13 +5,13 @@ import type {
|
|
|
5
5
|
} from "@antelopejs/interface-dms-automation";
|
|
6
6
|
import { SEND_EVENT_TYPES, type SendEventType } from "../types";
|
|
7
7
|
|
|
8
|
-
export
|
|
8
|
+
export type EmailEventPayload = {
|
|
9
9
|
tenantId: string;
|
|
10
10
|
sendId: string;
|
|
11
11
|
templateSlug: string;
|
|
12
12
|
type: SendEventType;
|
|
13
13
|
recipientEmail: string;
|
|
14
|
-
}
|
|
14
|
+
};
|
|
15
15
|
|
|
16
16
|
export interface EmailEventTriggerConfig {
|
|
17
17
|
types?: SendEventType[];
|
|
@@ -3,7 +3,10 @@ import type {
|
|
|
3
3
|
JsonSchema,
|
|
4
4
|
} from "@antelopejs/interface-dms-automation";
|
|
5
5
|
import type { EmailAddress } from "@antelopejs/interface-email";
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
SendTemplateResult,
|
|
8
|
+
TemplateVariables,
|
|
9
|
+
} from "@antelopejs/interface-dms-mailing";
|
|
7
10
|
import { sendUnlessSuppressed } from "../services/checked-send";
|
|
8
11
|
import { SEND_STATUSES } from "../types";
|
|
9
12
|
|
|
@@ -12,7 +15,7 @@ export interface SendTemplateActionInput {
|
|
|
12
15
|
slug: string;
|
|
13
16
|
to: EmailAddress | EmailAddress[];
|
|
14
17
|
locale?: string;
|
|
15
|
-
variables?:
|
|
18
|
+
variables?: TemplateVariables;
|
|
16
19
|
source?: string;
|
|
17
20
|
}
|
|
18
21
|
|
|
@@ -42,12 +45,12 @@ const INPUT_SCHEMA: JsonSchema = {
|
|
|
42
45
|
required: ["tenantId", "slug", "to"],
|
|
43
46
|
};
|
|
44
47
|
|
|
45
|
-
const RESULT_PROPERTIES
|
|
48
|
+
const RESULT_PROPERTIES = {
|
|
46
49
|
sendId: { type: "string" },
|
|
47
50
|
status: { type: "string", enum: SEND_STATUSES },
|
|
48
51
|
messageId: { type: "string" },
|
|
49
52
|
error: { type: "string" },
|
|
50
|
-
};
|
|
53
|
+
} satisfies { [name: string]: JsonSchema };
|
|
51
54
|
|
|
52
55
|
const OUTPUT_SCHEMA: JsonSchema = {
|
|
53
56
|
type: "object",
|
|
@@ -61,12 +64,21 @@ const OUTPUT_SCHEMA: JsonSchema = {
|
|
|
61
64
|
},
|
|
62
65
|
};
|
|
63
66
|
|
|
64
|
-
|
|
67
|
+
/** What the action hands the graph: the send's result, as JSON. */
|
|
68
|
+
type SendTemplateOutput = {
|
|
69
|
+
sendId: string;
|
|
70
|
+
status: SendTemplateResult["status"];
|
|
71
|
+
messageId?: string;
|
|
72
|
+
error?: string;
|
|
73
|
+
results?: SendTemplateOutput[];
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const failedRecipients = (result: SendTemplateOutput): SendTemplateOutput[] =>
|
|
65
77
|
(result.results ?? [result]).filter((entry) => entry.status === "failed");
|
|
66
78
|
|
|
67
79
|
export const sendTemplateAction: ActionType<
|
|
68
80
|
SendTemplateActionInput,
|
|
69
|
-
|
|
81
|
+
SendTemplateOutput
|
|
70
82
|
> = {
|
|
71
83
|
id: SEND_TEMPLATE_ACTION_ID,
|
|
72
84
|
name: "Send e-mail template",
|
|
@@ -78,13 +90,16 @@ export const sendTemplateAction: ActionType<
|
|
|
78
90
|
const source =
|
|
79
91
|
input.source ?? `${AUTOMATION_SOURCE_PREFIX}${ctx.procedureId}`;
|
|
80
92
|
try {
|
|
81
|
-
const result = await sendUnlessSuppressed(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
const result: SendTemplateOutput = await sendUnlessSuppressed(
|
|
94
|
+
input.slug,
|
|
95
|
+
{
|
|
96
|
+
tenantId: input.tenantId,
|
|
97
|
+
to: input.to,
|
|
98
|
+
locale: input.locale,
|
|
99
|
+
variables: input.variables,
|
|
100
|
+
source,
|
|
101
|
+
},
|
|
102
|
+
);
|
|
88
103
|
const failures = failedRecipients(result);
|
|
89
104
|
if (failures.length > 0) {
|
|
90
105
|
ctx.log(
|
|
@@ -95,7 +110,11 @@ export const sendTemplateAction: ActionType<
|
|
|
95
110
|
}
|
|
96
111
|
return result;
|
|
97
112
|
} catch (error) {
|
|
98
|
-
ctx.log(
|
|
113
|
+
ctx.log(
|
|
114
|
+
"error",
|
|
115
|
+
`Template "${input.slug}" could not be sent`,
|
|
116
|
+
error instanceof Error ? error.message : String(error),
|
|
117
|
+
);
|
|
99
118
|
throw error;
|
|
100
119
|
}
|
|
101
120
|
},
|
package/src/crons/jobs.ts
CHANGED
package/src/data/body.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { HTTPResult } from "@antelopejs/interface-api";
|
|
2
2
|
import { HTTP_BAD_REQUEST } from "../constants";
|
|
3
|
+
import type { JsonValue } from "../types/json";
|
|
4
|
+
import { isText } from "../validation/guards";
|
|
5
|
+
import { parseJson } from "../validation/json";
|
|
3
6
|
|
|
4
7
|
const INVALID_BODY = "$dms_mailing.errors.invalid_body";
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
/** The JSON a request body carries, `{}` for an empty one; 400 when it is not JSON. */
|
|
10
|
+
export function parseBody(body: Buffer | string): JsonValue {
|
|
11
|
+
const raw = isText(body) ? body : body.toString("utf8");
|
|
8
12
|
if (!raw) return {};
|
|
9
13
|
try {
|
|
10
|
-
return
|
|
14
|
+
return parseJson(raw);
|
|
11
15
|
} catch {
|
|
12
16
|
throw new HTTPResult(HTTP_BAD_REQUEST, INVALID_BODY);
|
|
13
17
|
}
|
|
@@ -4,13 +4,17 @@ import {
|
|
|
4
4
|
Parameter,
|
|
5
5
|
type RequestContext,
|
|
6
6
|
} from "@antelopejs/interface-api";
|
|
7
|
-
import { assertValidation } from "@antelopejs/interface-api-util";
|
|
7
|
+
import { assert, assertValidation } from "@antelopejs/interface-api-util";
|
|
8
|
+
import { z } from "zod";
|
|
8
9
|
import {
|
|
9
10
|
DataController,
|
|
10
11
|
type DataControllerCallback,
|
|
11
12
|
RegisterDataController,
|
|
12
13
|
} from "@antelopejs/interface-data-api";
|
|
13
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
Parameters,
|
|
16
|
+
Validation,
|
|
17
|
+
} from "@antelopejs/interface-data-api/components";
|
|
14
18
|
import {
|
|
15
19
|
Access,
|
|
16
20
|
AccessMode,
|
|
@@ -79,18 +83,23 @@ import {
|
|
|
79
83
|
refuseTakenSlug,
|
|
80
84
|
} from "../services/templates";
|
|
81
85
|
import type { TemplateCategory } from "../types";
|
|
86
|
+
import type { JsonObject } from "../types/json";
|
|
82
87
|
|
|
83
88
|
import {
|
|
84
89
|
createTemplateSchema,
|
|
85
|
-
|
|
90
|
+
type TemplateDetails,
|
|
86
91
|
} from "../validation/templates.schema";
|
|
92
|
+
import { isObject, isText } from "../validation/guards";
|
|
93
|
+
import { isJsonObject } from "../validation/json";
|
|
87
94
|
import { parseBody } from "./body";
|
|
88
95
|
import { CategoryDisplay, LocaleChipsDisplay } from "./displays";
|
|
89
96
|
|
|
90
97
|
const SLUG_IMMUTABLE = "$dms_mailing.errors.slug_immutable";
|
|
91
98
|
const SOURCE_NOT_FOUND = "$dms_mailing.errors.template_not_found";
|
|
92
99
|
const STARTER_NOT_FOUND = "$dms_mailing.errors.starter_not_found";
|
|
93
|
-
|
|
100
|
+
export type { TemplateDetails };
|
|
101
|
+
|
|
102
|
+
export const INVALID_TEMPLATE = "$dms_mailing.errors.invalid_template";
|
|
94
103
|
const UNKNOWN_CATEGORY = "$dms_mailing.errors.unknown_category";
|
|
95
104
|
const TEMPLATE_IN_USE = "$dms_mailing.errors.template_in_use";
|
|
96
105
|
const TEMPLATE_WAS_SENT = "$dms_mailing.errors.template_was_sent";
|
|
@@ -146,42 +155,40 @@ async function seedFields(
|
|
|
146
155
|
return starterFields(starter, fallbackLocale);
|
|
147
156
|
}
|
|
148
157
|
|
|
149
|
-
const isBlank = (value: unknown) =>
|
|
150
|
-
value === undefined || value === null || value === "";
|
|
151
|
-
|
|
152
158
|
/** A category is left empty or picked among the ones the settings declare. */
|
|
153
159
|
function assertKnownCategory(
|
|
154
160
|
categories: TemplateCategory[],
|
|
155
|
-
category:
|
|
161
|
+
category: string | undefined,
|
|
156
162
|
): void {
|
|
157
|
-
if (
|
|
158
|
-
if (typeof category === "string" && findCategory(categories, category)) {
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
163
|
+
if (!category || findCategory(categories, category)) return;
|
|
161
164
|
throw new HTTPResult(HTTP_UNPROCESSABLE, UNKNOWN_CATEGORY);
|
|
162
165
|
}
|
|
163
166
|
|
|
164
|
-
/** The row `new` answers since interface-data-api 0.3
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
167
|
+
/** The key of the row `new` answers since interface-data-api 0.3. */
|
|
168
|
+
const createdRowSchema = z.object({ _id: z.string() });
|
|
169
|
+
|
|
170
|
+
/** The JSON object a creation sends; 400 for any other JSON. */
|
|
171
|
+
function parseCreationBody(body: Buffer): JsonObject {
|
|
172
|
+
const parsed = parseBody(body);
|
|
173
|
+
assert(isJsonObject(parsed), HTTP_BAD_REQUEST, INVALID_TEMPLATE);
|
|
174
|
+
return parsed;
|
|
168
175
|
}
|
|
169
176
|
|
|
170
177
|
/**
|
|
171
178
|
* The creation's seed, and the body to store: slug trimmed, and the name
|
|
172
179
|
* typed in the reader's language as every language of the localized field,
|
|
173
180
|
* the fallback one included until someone translates it. A body a guard
|
|
174
|
-
* already parsed arrives as an object.
|
|
181
|
+
* already parsed arrives as an object. The body to store is handed on as an
|
|
182
|
+
* object: interface-data-api 0.3 refuses a JSON string.
|
|
175
183
|
*/
|
|
176
184
|
function parseCreation(body: Validation.WrittenBody, locale: string) {
|
|
177
|
-
const raw = Buffer.isBuffer(body) ?
|
|
185
|
+
const raw = Buffer.isBuffer(body) ? parseCreationBody(body) : body;
|
|
178
186
|
const seed = assertValidation(
|
|
179
187
|
raw,
|
|
180
188
|
(value) => createTemplateSchema.parse(value),
|
|
181
189
|
() => INVALID_TEMPLATE,
|
|
182
190
|
HTTP_BAD_REQUEST,
|
|
183
191
|
);
|
|
184
|
-
// Handed as an object: interface-data-api 0.3 refuses a JSON string.
|
|
185
192
|
const normalized = {
|
|
186
193
|
...raw,
|
|
187
194
|
slug: seed.slug,
|
|
@@ -190,21 +197,22 @@ function parseCreation(body: Validation.WrittenBody, locale: string) {
|
|
|
190
197
|
return { seed, normalized };
|
|
191
198
|
}
|
|
192
199
|
|
|
193
|
-
/** What the data API locks a localized field with: the controller's key. */
|
|
194
|
-
interface LocalizedController {
|
|
195
|
-
language: string;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
200
|
/** The key that writes every language of a localized field at once. */
|
|
199
201
|
const ALL_LANGUAGES = "*";
|
|
200
202
|
|
|
203
|
+
/**
|
|
204
|
+
* The data API's `new` callback, seeding the template it creates. The
|
|
205
|
+
* module's creation dialogs send one typed name, and the stored name holds
|
|
206
|
+
* every language, written at once: the controller's language key is set to
|
|
207
|
+
* all languages before the creation.
|
|
208
|
+
*/
|
|
201
209
|
function withSeeding(base: DataControllerCallback): DataControllerCallback {
|
|
202
210
|
return {
|
|
203
211
|
...base,
|
|
204
212
|
func: async function (
|
|
205
|
-
this:
|
|
213
|
+
this: TemplatesTableAPI,
|
|
206
214
|
ctx: RequestContext,
|
|
207
|
-
params:
|
|
215
|
+
params: Parameters.NewParameters,
|
|
208
216
|
body: Validation.WrittenBody,
|
|
209
217
|
...rest: unknown[]
|
|
210
218
|
) {
|
|
@@ -213,13 +221,11 @@ function withSeeding(base: DataControllerCallback): DataControllerCallback {
|
|
|
213
221
|
const settings = await getSettings(tenantId);
|
|
214
222
|
assertKnownCategory(settings.categories, seed.category);
|
|
215
223
|
const fields = await seedFields(tenantId, seed, settings.fallbackLocale);
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
)) as CreatedRow;
|
|
222
|
-
const templateId = created._id;
|
|
224
|
+
this.language = ALL_LANGUAGES;
|
|
225
|
+
const created: unknown = await refuseTakenSlug(
|
|
226
|
+
Promise.resolve(base.func.call(this, ctx, params, normalized, ...rest)),
|
|
227
|
+
);
|
|
228
|
+
const templateId = createdRowSchema.parse(created)._id;
|
|
223
229
|
const user = await authenticateTenantRequest(ctx);
|
|
224
230
|
await initializeTemplate(tenantId, templateId, displayName(user), fields);
|
|
225
231
|
return created;
|
|
@@ -296,10 +302,17 @@ export async function assertEditable(
|
|
|
296
302
|
): Promise<void> {
|
|
297
303
|
if (body.slug !== undefined && body.slug !== current.slug)
|
|
298
304
|
throw new HTTPResult(HTTP_CONFLICT, SLUG_IMMUTABLE);
|
|
299
|
-
if (body.name !== undefined
|
|
300
|
-
|
|
305
|
+
if (body.name !== undefined) {
|
|
306
|
+
const texts = isObject(body.name) ? Object.values(body.name) : [body.name];
|
|
307
|
+
if (!texts.some((text) => isText(text) && text.trim()))
|
|
308
|
+
throw new HTTPResult(HTTP_BAD_REQUEST, BLANK_NAME);
|
|
309
|
+
}
|
|
301
310
|
const { categories } = await getSettings(tenantId);
|
|
302
|
-
|
|
311
|
+
const { category } = body;
|
|
312
|
+
if (category === undefined || category === null || category === "") return;
|
|
313
|
+
if (!isText(category))
|
|
314
|
+
throw new HTTPResult(HTTP_UNPROCESSABLE, UNKNOWN_CATEGORY);
|
|
315
|
+
assertKnownCategory(categories, category);
|
|
303
316
|
}
|
|
304
317
|
|
|
305
318
|
/**
|
|
@@ -309,14 +322,9 @@ export async function assertEditable(
|
|
|
309
322
|
export async function saveDetails(
|
|
310
323
|
ctx: RequestContext,
|
|
311
324
|
template: MailingTemplate,
|
|
312
|
-
|
|
325
|
+
details: TemplateDetails,
|
|
313
326
|
author: string,
|
|
314
327
|
): Promise<void> {
|
|
315
|
-
const details = assertValidation(
|
|
316
|
-
body,
|
|
317
|
-
(value) => templateDetailsSchema.parse(value),
|
|
318
|
-
() => INVALID_TEMPLATE,
|
|
319
|
-
);
|
|
320
328
|
const tenantId = getRequestTenantId(ctx);
|
|
321
329
|
await assertEditable(tenantId, details, template);
|
|
322
330
|
const names = TemplateModel.namesOf(template);
|
|
@@ -329,15 +337,6 @@ export async function saveDetails(
|
|
|
329
337
|
});
|
|
330
338
|
}
|
|
331
339
|
|
|
332
|
-
/**
|
|
333
|
-
* Whether an edited name holds no text: the text of the reader's language,
|
|
334
|
-
* or, from a form editing every language, all of them.
|
|
335
|
-
*/
|
|
336
|
-
function isBlankName(name: unknown): boolean {
|
|
337
|
-
const texts = typeof name === "object" && name ? Object.values(name) : [name];
|
|
338
|
-
return !texts.some((text) => typeof text === "string" && text.trim());
|
|
339
|
-
}
|
|
340
|
-
|
|
341
340
|
/** The names trimmed, blank ones left out, the fallback language filled. */
|
|
342
341
|
function tidyNames(names: LocalizedText, locale: string): LocalizedText {
|
|
343
342
|
const tidy = Object.fromEntries(
|
|
@@ -357,10 +356,8 @@ function tidyNames(names: LocalizedText, locale: string): LocalizedText {
|
|
|
357
356
|
export function afterEdit(
|
|
358
357
|
model: TemplateModel,
|
|
359
358
|
id: string,
|
|
360
|
-
name: unknown,
|
|
361
359
|
locale: string,
|
|
362
|
-
): AfterWrite
|
|
363
|
-
if (name === undefined) return undefined;
|
|
360
|
+
): AfterWrite {
|
|
364
361
|
return async () => {
|
|
365
362
|
const template = await model.get(id);
|
|
366
363
|
if (!template) return;
|
|
@@ -372,8 +369,10 @@ export function afterEdit(
|
|
|
372
369
|
}
|
|
373
370
|
|
|
374
371
|
/** The templates the attention refresh flagged. */
|
|
375
|
-
const isFlagged: FilterFunction<
|
|
376
|
-
|
|
372
|
+
const isFlagged: FilterFunction<TemplatesTableAPI, TemplatesTableAPI> = (
|
|
373
|
+
_context,
|
|
374
|
+
proxy,
|
|
375
|
+
) => proxy.default(false).eq(true);
|
|
377
376
|
|
|
378
377
|
const templateRoutes = {
|
|
379
378
|
...TableViewRoutes.All,
|
|
@@ -12,8 +12,8 @@ import { categoriesSchema } from "../validation/settings.schema";
|
|
|
12
12
|
*/
|
|
13
13
|
@RegisterDataType(TEMPLATE_CATEGORIES_TYPE_ID)
|
|
14
14
|
export class TemplateCategoriesType extends DataType {
|
|
15
|
-
constructor(
|
|
16
|
-
super([], undefined,
|
|
15
|
+
constructor() {
|
|
16
|
+
super([], undefined, {});
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
protected defaultInputComponent() {
|
|
@@ -19,7 +19,7 @@ import { TEMPLATE_CATEGORY_TYPE_ID } from "../constants";
|
|
|
19
19
|
*/
|
|
20
20
|
@RegisterDataType(TEMPLATE_CATEGORY_TYPE_ID)
|
|
21
21
|
export class TemplateCategoryType extends DataType {
|
|
22
|
-
constructor(
|
|
22
|
+
constructor() {
|
|
23
23
|
super(
|
|
24
24
|
[
|
|
25
25
|
DefaultDataCompareTypes.Is,
|
|
@@ -28,7 +28,7 @@ export class TemplateCategoryType extends DataType {
|
|
|
28
28
|
DefaultDataCompareTypes.IsNotEmpty,
|
|
29
29
|
],
|
|
30
30
|
DefaultDataCompareTypes.Is,
|
|
31
|
-
|
|
31
|
+
{},
|
|
32
32
|
);
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -123,7 +123,7 @@ export class SendModel extends BasicDataModel(
|
|
|
123
123
|
.filter((row) => row.key("createdAt").ge(from))
|
|
124
124
|
.filter((row) => row.key("createdAt").lt(to))
|
|
125
125
|
.without("json_variables")
|
|
126
|
-
.run()
|
|
126
|
+
.run();
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
listOlderThan(limit: Date, pageSize: number): Promise<MailingSend[]> {
|
|
@@ -260,7 +260,7 @@ export class SendModel extends BasicDataModel(
|
|
|
260
260
|
.getAll(templateSlug, "templateSlug")
|
|
261
261
|
.filter((row) => row.key("createdAt").ge(from))
|
|
262
262
|
.without("json_variables")
|
|
263
|
-
.run()
|
|
263
|
+
.run();
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
/** How many sends the retention would delete with this cutoff. */
|
|
@@ -11,6 +11,8 @@ export function committedVersionOf(
|
|
|
11
11
|
template: Pick<MailingTemplate, "json_published_version">,
|
|
12
12
|
): MailingTemplateVersion | undefined {
|
|
13
13
|
if (!template.json_published_version) return undefined;
|
|
14
|
+
// SAFETY: json_published_version is written only by publishSnapshot and publishedAtCreation as the
|
|
15
|
+
// JSON.stringify of the version row they insert; its publishedAt reads back as a string, and the next line restores the Date.
|
|
14
16
|
const row = JSON.parse(
|
|
15
17
|
template.json_published_version,
|
|
16
18
|
) as MailingTemplateVersion;
|
|
@@ -33,20 +35,24 @@ export class TemplateVersionModel extends BasicDataModel(
|
|
|
33
35
|
await this.insert(version);
|
|
34
36
|
} catch (error) {
|
|
35
37
|
const stored = await this.get(version._id);
|
|
38
|
+
if (!stored) throw error;
|
|
39
|
+
const storedFields = new Map(Object.entries(stored));
|
|
36
40
|
if (
|
|
37
|
-
!stored ||
|
|
38
41
|
Object.entries(version).some(
|
|
39
42
|
([key, value]) =>
|
|
40
|
-
JSON.stringify(
|
|
41
|
-
JSON.stringify(value),
|
|
43
|
+
JSON.stringify(storedFields.get(key)) !== JSON.stringify(value),
|
|
42
44
|
)
|
|
43
45
|
)
|
|
44
46
|
throw error;
|
|
45
47
|
}
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
/**
|
|
51
|
+
* The published versions of a template, newest first. It reads the
|
|
52
|
+
* template's publication before the version rows: any later publication
|
|
53
|
+
* must have materialized the version that read names.
|
|
54
|
+
*/
|
|
48
55
|
async listForTemplate(templateId: string): Promise<MailingTemplateVersion[]> {
|
|
49
|
-
// Read the publication first: any later publication must have materialized it.
|
|
50
56
|
const template = await new TemplateModel(this.database).get(templateId);
|
|
51
57
|
if (!template) return [];
|
|
52
58
|
const rows = await this.getBy("templateId", templateId);
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "@antelopejs/interface-database-decorators";
|
|
6
6
|
import type { LocalizedText } from "@antelopejs/interface-dms-mailing";
|
|
7
7
|
import { MAILING_TEMPLATES_TABLE_NAME, MailingTemplate } from "../tables";
|
|
8
|
+
import { isText } from "../../validation/guards";
|
|
8
9
|
|
|
9
10
|
/** The attention fields of a template, as the refresh writes them. */
|
|
10
11
|
export type TemplateAttentionFields = Pick<
|
|
@@ -15,7 +16,7 @@ export type TemplateAttentionFields = Pick<
|
|
|
15
16
|
const isLocalizedText = (value: unknown): value is LocalizedText =>
|
|
16
17
|
typeof value === "object" &&
|
|
17
18
|
value !== null &&
|
|
18
|
-
Object.values(value).every(
|
|
19
|
+
Object.values(value).every(isText);
|
|
19
20
|
|
|
20
21
|
/** Where a row keeps its modifier-held fields, the localized name among them. */
|
|
21
22
|
const MODIFIER_CONTAINER = "_internal";
|
|
@@ -37,9 +38,11 @@ export class TemplateModel extends BasicDataModel(
|
|
|
37
38
|
return TemplateModel.fromPlainData(fields).localize("*", ["name"]);
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* The names of `template`, one per language: unlocked with `"*"`, the
|
|
43
|
+
* localized field holds the whole per-language record.
|
|
44
|
+
*/
|
|
41
45
|
static namesOf(template: MailingTemplate): LocalizedText {
|
|
42
|
-
// Unlocked with "*", the localized field holds the whole per-language record.
|
|
43
46
|
const names: unknown = template.localize("*", ["name"]).name;
|
|
44
47
|
return isLocalizedText(names) ? names : {};
|
|
45
48
|
}
|
|
@@ -156,7 +156,7 @@ export class MailingTemplate extends Table.with(LocalizationModifier) {
|
|
|
156
156
|
|
|
157
157
|
/** Covers model updates and generated data-api edits in the same database write. */
|
|
158
158
|
class TemplateRevisionModifier extends Modifier {
|
|
159
|
-
update(object:
|
|
159
|
+
update(object: Pick<MailingTemplate, "revision">, field: "revision"): void {
|
|
160
160
|
object[field] = randomUUID();
|
|
161
161
|
}
|
|
162
162
|
}
|