@antelopejs/dms-mailing 0.7.0 → 0.8.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 +28 -10
- package/dist/constants.d.ts +0 -2
- package/dist/constants.js +1 -3
- package/dist/constants.js.map +1 -1
- package/dist/crons/jobs.d.ts +1 -1
- package/dist/data/body.js +2 -1
- package/dist/data/body.js.map +1 -1
- package/dist/data/templates-table.js +5 -11
- package/dist/data/templates-table.js.map +1 -1
- package/dist/db/models/index.d.ts +0 -1
- package/dist/db/models/index.js +0 -1
- package/dist/db/models/index.js.map +1 -1
- package/dist/db/models/send-events.model.js +2 -3
- package/dist/db/models/send-events.model.js.map +1 -1
- package/dist/db/models/sends.model.js +2 -3
- package/dist/db/models/sends.model.js.map +1 -1
- package/dist/db/models/template-versions.model.js.map +1 -1
- package/dist/db/models/templates.model.js +4 -10
- package/dist/db/models/templates.model.js.map +1 -1
- package/dist/db/tables/index.d.ts +0 -1
- package/dist/db/tables/index.js +0 -1
- package/dist/db/tables/index.js.map +1 -1
- package/dist/db/tables/settings.table.d.ts +0 -1
- package/dist/db/tables/settings.table.js +0 -4
- package/dist/db/tables/settings.table.js.map +1 -1
- package/dist/db/tables/templates.table.d.ts +0 -3
- package/dist/db/tables/templates.table.js +1 -13
- package/dist/db/tables/templates.table.js.map +1 -1
- package/dist/engine/changes.js +2 -1
- package/dist/engine/changes.js.map +1 -1
- package/dist/engine/conditions.js +2 -3
- package/dist/engine/conditions.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.js +6 -6
- package/dist/engine/formats.js.map +1 -1
- package/dist/engine/paths.js +2 -3
- package/dist/engine/paths.js.map +1 -1
- package/dist/engine/system-variables.js +1 -1
- package/dist/engine/system-variables.js.map +1 -1
- package/dist/engine/tokens.js.map +1 -1
- package/dist/hooks/index.d.ts +0 -1
- package/dist/hooks/index.js +0 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/implementations/dms-mailing/index.d.ts +3 -4
- package/dist/implementations/dms-mailing/index.js +8 -9
- package/dist/implementations/dms-mailing/index.js.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/pages/sends.js.map +1 -1
- package/dist/pages/settings/form.js +8 -0
- package/dist/pages/settings/form.js.map +1 -1
- package/dist/pages/toolbar.js.map +1 -1
- package/dist/routes/events.js +2 -2
- package/dist/routes/events.js.map +1 -1
- package/dist/routes/metrics.js +6 -5
- package/dist/routes/metrics.js.map +1 -1
- package/dist/routes/sends.js +1 -1
- package/dist/routes/sends.js.map +1 -1
- package/dist/routes/settings.js +6 -6
- package/dist/routes/settings.js.map +1 -1
- package/dist/routes/templates.js +6 -10
- package/dist/routes/templates.js.map +1 -1
- package/dist/services/attention.js +2 -1
- package/dist/services/attention.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/declarations.js +42 -184
- package/dist/services/declarations.js.map +1 -1
- package/dist/services/events.js +11 -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.map +1 -1
- package/dist/services/render.js.map +1 -1
- package/dist/services/send-import.js +7 -5
- package/dist/services/send-import.js.map +1 -1
- package/dist/services/send.js +4 -3
- package/dist/services/send.js.map +1 -1
- package/dist/services/settings.d.ts +1 -1
- package/dist/services/settings.js +1 -5
- package/dist/services/settings.js.map +1 -1
- 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 -2
- package/dist/services/templates.js +54 -29
- package/dist/services/templates.js.map +1 -1
- package/dist/types/settings.d.ts +2 -0
- package/dist/types/settings.js +8 -1
- package/dist/types/settings.js.map +1 -1
- package/dist/validation/declarations.schema.d.ts +16 -16
- package/dist/validation/declarations.schema.js +2 -2
- package/dist/validation/declarations.schema.js.map +1 -1
- package/dist/validation/guards.d.ts +4 -0
- package/dist/validation/guards.js +19 -0
- package/dist/validation/guards.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.d.ts +16 -16
- package/dist/validation/settings.schema.js +1 -1
- package/dist/validation/settings.schema.js.map +1 -1
- package/dist/validation/templates.schema.d.ts +19 -18
- 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 +210 -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 +8 -13
- package/skills/dms-mailing/SKILL.md +11 -17
- package/skills/dms-mailing/cases.json +49 -0
- package/src/constants.ts +0 -2
- package/src/crons/jobs.ts +1 -1
- package/src/data/body.ts +2 -1
- package/src/data/templates-table.ts +15 -22
- package/src/db/models/index.ts +0 -1
- package/src/db/models/send-events.model.ts +2 -2
- package/src/db/models/sends.model.ts +7 -8
- package/src/db/models/template-versions.model.ts +5 -1
- package/src/db/models/templates.model.ts +9 -16
- package/src/db/tables/index.ts +0 -1
- package/src/db/tables/settings.table.ts +0 -8
- package/src/db/tables/templates.table.ts +2 -16
- package/src/engine/changes.ts +3 -2
- package/src/engine/conditions.ts +2 -4
- package/src/engine/fallback-html.ts +13 -13
- package/src/engine/formats.ts +6 -6
- package/src/engine/paths.ts +4 -3
- package/src/engine/system-variables.ts +4 -3
- package/src/engine/tokens.ts +1 -2
- package/src/hooks/index.ts +0 -1
- package/src/implementations/dms-mailing/index.ts +5 -18
- package/src/index.ts +1 -6
- package/src/pages/sends.ts +5 -3
- package/src/pages/settings/form.ts +16 -0
- package/src/pages/templates.ts +1 -1
- package/src/pages/toolbar.ts +4 -2
- package/src/routes/events.ts +3 -6
- package/src/routes/metrics.ts +7 -5
- package/src/routes/sends.ts +10 -18
- package/src/routes/settings.ts +8 -9
- package/src/routes/suppressions.ts +1 -1
- package/src/routes/templates.ts +16 -18
- package/src/services/attention.ts +4 -3
- package/src/services/categories.ts +6 -6
- package/src/services/checked-send.ts +17 -19
- package/src/services/declarations.ts +62 -318
- package/src/services/events.ts +16 -13
- package/src/services/localized-text.ts +4 -2
- package/src/services/metrics.ts +2 -2
- package/src/services/notify.ts +1 -1
- package/src/services/render.ts +5 -1
- package/src/services/send-import.ts +18 -13
- package/src/services/send.ts +7 -7
- package/src/services/settings.ts +3 -10
- package/src/services/starters.ts +6 -6
- package/src/services/template-stats.ts +11 -4
- package/src/services/templates.ts +69 -52
- package/src/types/settings.ts +20 -0
- package/src/validation/declarations.schema.ts +2 -2
- package/src/validation/guards.ts +25 -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 +2 -1
- package/dist/db/models/template-slugs.model.d.ts +0 -27
- package/dist/db/models/template-slugs.model.js +0 -43
- package/dist/db/models/template-slugs.model.js.map +0 -1
- package/dist/db/tables/template-slugs.table.d.ts +0 -7
- package/dist/db/tables/template-slugs.table.js +0 -35
- package/dist/db/tables/template-slugs.table.js.map +0 -1
- package/dist/hooks/database-ready.d.ts +0 -7
- package/dist/hooks/database-ready.js +0 -26
- package/dist/hooks/database-ready.js.map +0 -1
- package/dist/services/fingerprint.d.ts +0 -1
- package/dist/services/fingerprint.js +0 -20
- package/dist/services/fingerprint.js.map +0 -1
- package/frontend-vue/tests/i18n-editor.test.ts +0 -50
- package/frontend-vue/tests/i18n.test.ts +0 -138
- package/src/db/models/template-slugs.model.ts +0 -61
- package/src/db/tables/template-slugs.table.ts +0 -29
- package/src/hooks/database-ready.ts +0 -47
- package/src/services/fingerprint.ts +0 -22
|
@@ -1,21 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
declareTemplateCategory,
|
|
1
|
+
export {
|
|
2
|
+
declareTemplate as DeclareTemplate,
|
|
3
|
+
declareTemplateCategory as DeclareTemplateCategory,
|
|
5
4
|
} from "../../services/declarations";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
export { importSends as ImportSends } from "../../services/send-import";
|
|
6
|
+
export { declareSender as DeclareSender } from "../../services/sender-declaration";
|
|
9
7
|
export { recordEmailEvent as RecordEmailEvent } from "../../services/events";
|
|
10
8
|
export { sendUnlessSuppressed as SendTemplate } from "../../services/checked-send";
|
|
11
|
-
|
|
12
|
-
/*
|
|
13
|
-
* Projects call these from their `start()`, which runs in parallel with the
|
|
14
|
-
* DMS's: they wait for the database the DMS opens there rather than fail.
|
|
15
|
-
*/
|
|
16
|
-
export const DeclareTemplate = afterDatabaseReady(declareTemplate);
|
|
17
|
-
export const DeclareTemplateCategory = afterDatabaseReady(
|
|
18
|
-
declareTemplateCategory,
|
|
19
|
-
);
|
|
20
|
-
export const DeclareSender = afterDatabaseReady(declareSender);
|
|
21
|
-
export const ImportSends = afterDatabaseReady(importSends);
|
package/src/index.ts
CHANGED
|
@@ -14,11 +14,7 @@ import {
|
|
|
14
14
|
scheduleJob,
|
|
15
15
|
stopJob,
|
|
16
16
|
} from "./crons";
|
|
17
|
-
import {
|
|
18
|
-
registerDatabaseReady,
|
|
19
|
-
registerTenantExport,
|
|
20
|
-
unregisterTenantExport,
|
|
21
|
-
} from "./hooks";
|
|
17
|
+
import { registerTenantExport, unregisterTenantExport } from "./hooks";
|
|
22
18
|
import { registerRealtimeTopics } from "./realtime";
|
|
23
19
|
import { frontendModule } from "./frontend-module";
|
|
24
20
|
|
|
@@ -34,7 +30,6 @@ const CRON_JOBS = [RETENTION_JOB, ATTENTION_JOB];
|
|
|
34
30
|
let scheduledJobs: ScheduledJob[] = [];
|
|
35
31
|
|
|
36
32
|
export async function construct(config: DmsMailingConfig = {}): Promise<void> {
|
|
37
|
-
registerDatabaseReady();
|
|
38
33
|
setConfig(config);
|
|
39
34
|
ImplementInterface(
|
|
40
35
|
await import("@antelopejs/interface-dms-mailing"),
|
package/src/pages/sends.ts
CHANGED
|
@@ -313,11 +313,13 @@ export class SendsPageController extends PageController(
|
|
|
313
313
|
permissionMeta("send_drawer", "i-ph-info"),
|
|
314
314
|
),
|
|
315
315
|
title: "$dms_mailing.sends.actions.details",
|
|
316
|
-
|
|
316
|
+
/** Names the open send: its recipient and the template it used. */
|
|
317
317
|
rowTitle: "$dms_mailing.sends.drawer.heading",
|
|
318
318
|
positionLabel: "$dms_mailing.sends.drawer.position",
|
|
319
|
-
|
|
320
|
-
|
|
319
|
+
/**
|
|
320
|
+
* A side panel keeps the open send in the URL and words a missing
|
|
321
|
+
* one by default; a drawer does both only when asked.
|
|
322
|
+
*/
|
|
321
323
|
deepLink: true,
|
|
322
324
|
notFound: {},
|
|
323
325
|
},
|
|
@@ -7,6 +7,8 @@ import { ComponentBuilder } from "@antelopejs/interface-dms/component";
|
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { API_BASE_PATH } from "../../constants";
|
|
9
9
|
import { TemplateCategoriesType } from "../../data-types";
|
|
10
|
+
import { missingVariablesPolicy } from "../../services/settings";
|
|
11
|
+
import { MODULE_DEFAULT_SETTINGS } from "../../types";
|
|
10
12
|
import {
|
|
11
13
|
MAX_RETENTION_DAYS,
|
|
12
14
|
MIN_RETENTION_DAYS,
|
|
@@ -37,6 +39,18 @@ const field = (id: string) => ({
|
|
|
37
39
|
description: key(`fields.${id}.description`),
|
|
38
40
|
});
|
|
39
41
|
|
|
42
|
+
/**
|
|
43
|
+
* A field holding a module default offers it: "Module default · Use default"
|
|
44
|
+
* while it holds something else. The retention does not: one click there
|
|
45
|
+
* could shorten it, and lowering it deletes sends, which its input asks to
|
|
46
|
+
* confirm instead. Nor do the categories: putting the default list back
|
|
47
|
+
* would move every template of a custom category.
|
|
48
|
+
*/
|
|
49
|
+
const moduleDefault = (defaultValue: string) => ({
|
|
50
|
+
defaultValue,
|
|
51
|
+
offersDefault: true,
|
|
52
|
+
});
|
|
53
|
+
|
|
40
54
|
const section = (
|
|
41
55
|
id: string,
|
|
42
56
|
icon: string,
|
|
@@ -84,12 +98,14 @@ const guards = section("guards", "i-ph-shield-check", [
|
|
|
84
98
|
],
|
|
85
99
|
}),
|
|
86
100
|
required: true,
|
|
101
|
+
...moduleDefault(missingVariablesPolicy(MODULE_DEFAULT_SETTINGS)),
|
|
87
102
|
},
|
|
88
103
|
{
|
|
89
104
|
...field("fallbackLocale"),
|
|
90
105
|
type: new DefaultDataTypes.StringType({}),
|
|
91
106
|
inputComponent: CustomComponent("MailingLocaleSelect").serializeSync(),
|
|
92
107
|
required: true,
|
|
108
|
+
...moduleDefault(MODULE_DEFAULT_SETTINGS.fallbackLocale),
|
|
93
109
|
},
|
|
94
110
|
]);
|
|
95
111
|
|
package/src/pages/templates.ts
CHANGED
|
@@ -146,7 +146,7 @@ export class TemplatesPageController extends PageController(
|
|
|
146
146
|
deepLink: true,
|
|
147
147
|
target: {
|
|
148
148
|
type: "drawer",
|
|
149
|
-
|
|
149
|
+
/** On the right, as the gallery opens it (`useTemplateDrawer`). */
|
|
150
150
|
direction: "right",
|
|
151
151
|
component: templateDrawer,
|
|
152
152
|
title: "$dms_mailing.templates.actions.details",
|
package/src/pages/toolbar.ts
CHANGED
|
@@ -65,8 +65,10 @@ export function mailingToolbar(
|
|
|
65
65
|
id: period.scope,
|
|
66
66
|
presets: period.presets,
|
|
67
67
|
defaultPreset: period.defaultPreset,
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
/**
|
|
69
|
+
* More than one comparison: `pageToolbar()` draws them as a "vs …" menu
|
|
70
|
+
* on the first one, the previous period.
|
|
71
|
+
*/
|
|
70
72
|
comparisons: MAILING_COMPARISONS,
|
|
71
73
|
},
|
|
72
74
|
}).meta({
|
package/src/routes/events.ts
CHANGED
|
@@ -17,15 +17,12 @@ import {
|
|
|
17
17
|
secretRefusal,
|
|
18
18
|
WEBHOOK_SECRET_HEADER,
|
|
19
19
|
} from "../services/webhook-status";
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
type SendEventType,
|
|
23
|
-
type WebhookRefusal,
|
|
24
|
-
} from "../types";
|
|
20
|
+
import type { WebhookRefusal } from "../types";
|
|
21
|
+
import { sendEventTypeSchema } from "../validation/send-event-type.schema";
|
|
25
22
|
|
|
26
23
|
const eventSchema = z.object({
|
|
27
24
|
messageId: z.string().min(1),
|
|
28
|
-
type:
|
|
25
|
+
type: sendEventTypeSchema,
|
|
29
26
|
at: z.coerce.date().optional(),
|
|
30
27
|
details: z.record(z.unknown()).optional(),
|
|
31
28
|
});
|
package/src/routes/metrics.ts
CHANGED
|
@@ -83,18 +83,20 @@ export interface TopMetric {
|
|
|
83
83
|
minDeliveries: number;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
const OPEN_RATE_METRIC: TopMetric = {
|
|
87
|
+
extract: (totals) => totals.openRate,
|
|
88
|
+
minDeliveries: MIN_RANKED_DELIVERIES,
|
|
89
|
+
};
|
|
90
|
+
|
|
86
91
|
const TOP_METRICS: Record<string, TopMetric> = {
|
|
87
|
-
"open-rate":
|
|
88
|
-
extract: (totals) => totals.openRate,
|
|
89
|
-
minDeliveries: MIN_RANKED_DELIVERIES,
|
|
90
|
-
},
|
|
92
|
+
"open-rate": OPEN_RATE_METRIC,
|
|
91
93
|
"click-rate": {
|
|
92
94
|
extract: (totals) => totals.clickRate,
|
|
93
95
|
minDeliveries: MIN_RANKED_DELIVERIES,
|
|
94
96
|
},
|
|
95
97
|
sends: { extract: (totals) => totals.sends, minDeliveries: 0 },
|
|
96
98
|
};
|
|
97
|
-
const DEFAULT_TOP_METRIC =
|
|
99
|
+
const DEFAULT_TOP_METRIC = OPEN_RATE_METRIC;
|
|
98
100
|
|
|
99
101
|
interface FunnelStepDefinition {
|
|
100
102
|
id: string;
|
package/src/routes/sends.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from "@antelopejs/interface-api";
|
|
11
11
|
import { assert, assertValidation } from "@antelopejs/interface-api-util";
|
|
12
12
|
import { AuthUserWithPermission } from "@antelopejs/interface-dms/guards";
|
|
13
|
-
import { ZodError } from "zod";
|
|
13
|
+
import { ZodError, type z } from "zod";
|
|
14
14
|
import { getRequestTenantId } from "@antelopejs/interface-dms/request-tenant";
|
|
15
15
|
import { TenantScopedModel } from "@antelopejs/interface-dms/tenant-scoped-model";
|
|
16
16
|
import type { User } from "@antelopejs/interface-dms/auth/db";
|
|
@@ -68,13 +68,9 @@ import {
|
|
|
68
68
|
requireTemplate,
|
|
69
69
|
snapshotOfVersion,
|
|
70
70
|
} from "../services/templates";
|
|
71
|
-
import type {
|
|
72
|
-
SendStatus,
|
|
73
|
-
SuppressionSummary,
|
|
74
|
-
TemplateContent,
|
|
75
|
-
TemplateStatus,
|
|
76
|
-
} from "../types";
|
|
71
|
+
import type { SendStatus, SuppressionSummary, TemplateStatus } from "../types";
|
|
77
72
|
import {
|
|
73
|
+
type previewVersionSchema,
|
|
78
74
|
realSendSchema,
|
|
79
75
|
replayFailedSchema,
|
|
80
76
|
replaySchema,
|
|
@@ -98,12 +94,6 @@ const DRAFT_VERSION = 0;
|
|
|
98
94
|
/** Renders as the recipient got it, not as the editor previews it. */
|
|
99
95
|
const SEND_MODE: InterpolationMode = "send";
|
|
100
96
|
|
|
101
|
-
interface TestSendInput {
|
|
102
|
-
locale: string;
|
|
103
|
-
content?: TemplateContent;
|
|
104
|
-
data?: Record<string, unknown>;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
97
|
export interface SendTemplateSummary {
|
|
108
98
|
id: string;
|
|
109
99
|
name: string;
|
|
@@ -274,9 +264,9 @@ export class SendsController extends Controller(API_BASE_PATH) {
|
|
|
274
264
|
tenantId: this.tenantId,
|
|
275
265
|
to: input.to,
|
|
276
266
|
allowSuppressed: [this.user.email],
|
|
277
|
-
locale:
|
|
278
|
-
variables:
|
|
279
|
-
content:
|
|
267
|
+
locale: input.locale,
|
|
268
|
+
variables: input.data,
|
|
269
|
+
content: input.content,
|
|
280
270
|
version: requestedVersionOf(input.version),
|
|
281
271
|
isTest: true,
|
|
282
272
|
source: TEST_SEND_SOURCE,
|
|
@@ -578,8 +568,10 @@ export class SendsController extends Controller(API_BASE_PATH) {
|
|
|
578
568
|
* The version a test of a published version records: a number names one,
|
|
579
569
|
* "draft" or "published" let the send decide.
|
|
580
570
|
*/
|
|
581
|
-
function requestedVersionOf(
|
|
582
|
-
|
|
571
|
+
function requestedVersionOf(
|
|
572
|
+
version: z.infer<typeof previewVersionSchema> | undefined,
|
|
573
|
+
): number | undefined {
|
|
574
|
+
return version === "draft" || version === "published" ? undefined : version;
|
|
583
575
|
}
|
|
584
576
|
|
|
585
577
|
function variablesOf(send: MailingSend): Record<string, unknown> {
|
package/src/routes/settings.ts
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
settingsPatchSchema,
|
|
49
49
|
} from "../validation/settings.schema";
|
|
50
50
|
import { retentionLimit } from "../crons/retention";
|
|
51
|
+
import { isJsonObject } from "../validation/guards";
|
|
51
52
|
|
|
52
53
|
const INVALID_SETTINGS = "$dms_mailing.errors.invalid_settings";
|
|
53
54
|
const CATEGORIES_REASSIGNED = "$dms_mailing.settings.categories.reassigned";
|
|
@@ -61,9 +62,9 @@ const NEXT_RUN_MINUTE = 15;
|
|
|
61
62
|
const RETENTION_DAYS_PARAM = "days";
|
|
62
63
|
|
|
63
64
|
/** A settings field the form shows under another name. */
|
|
64
|
-
const FORM_FIELD_OF
|
|
65
|
-
blockOnMissingVariables
|
|
66
|
-
|
|
65
|
+
const FORM_FIELD_OF = new Map([
|
|
66
|
+
["blockOnMissingVariables", "missingVariables"],
|
|
67
|
+
]);
|
|
67
68
|
|
|
68
69
|
const retentionDaysSchema = z.coerce
|
|
69
70
|
.number()
|
|
@@ -141,12 +142,12 @@ function webhookUrl(
|
|
|
141
142
|
/** The issue of a refused write, under the form field it belongs to. */
|
|
142
143
|
function formIssue(issue: z.ZodIssue): z.ZodIssue {
|
|
143
144
|
const [field, ...rest] = issue.path;
|
|
144
|
-
const formField = FORM_FIELD_OF
|
|
145
|
+
const formField = FORM_FIELD_OF.get(String(field));
|
|
145
146
|
return formField ? { ...issue, path: [formField, ...rest] } : issue;
|
|
146
147
|
}
|
|
147
148
|
|
|
148
149
|
/** A refused write: the generic message, and each issue for its field. */
|
|
149
|
-
function invalidSettings(error: unknown)
|
|
150
|
+
function invalidSettings(error: unknown) {
|
|
150
151
|
if (!(error instanceof z.ZodError)) return INVALID_SETTINGS;
|
|
151
152
|
return { message: INVALID_SETTINGS, issues: error.issues.map(formIssue) };
|
|
152
153
|
}
|
|
@@ -236,11 +237,9 @@ export class MailingSettingsController extends Controller(
|
|
|
236
237
|
/** The body as a checked patch; 400 for anything else. */
|
|
237
238
|
private readPatch(raw: Buffer): SettingsPatch {
|
|
238
239
|
const body: unknown = parseBody(raw);
|
|
239
|
-
|
|
240
|
-
typeof body === "object" && body !== null && !Array.isArray(body);
|
|
241
|
-
assert(isPatch, HTTP_BAD_REQUEST, INVALID_SETTINGS);
|
|
240
|
+
assert(isJsonObject(body), HTTP_BAD_REQUEST, INVALID_SETTINGS);
|
|
242
241
|
return assertValidation(
|
|
243
|
-
clearedToEmpty(body
|
|
242
|
+
clearedToEmpty(body),
|
|
244
243
|
(value) => settingsPatchSchema.parse(value),
|
|
245
244
|
invalidSettings,
|
|
246
245
|
);
|
|
@@ -33,7 +33,7 @@ const manualSuppressionSchema = z.object({
|
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
/** A refused entry: the generic message, and each issue under its field. */
|
|
36
|
-
function invalidSuppression(error: unknown)
|
|
36
|
+
function invalidSuppression(error: unknown) {
|
|
37
37
|
if (!(error instanceof z.ZodError)) return INVALID_SUPPRESSION;
|
|
38
38
|
return { message: INVALID_SUPPRESSION, issues: error.issues };
|
|
39
39
|
}
|
package/src/routes/templates.ts
CHANGED
|
@@ -47,7 +47,6 @@ import {
|
|
|
47
47
|
type TemplateStats,
|
|
48
48
|
} from "../services/template-stats";
|
|
49
49
|
import {
|
|
50
|
-
assertSlugClaimed,
|
|
51
50
|
contentFieldsOf,
|
|
52
51
|
discardDraft,
|
|
53
52
|
displayName,
|
|
@@ -70,6 +69,7 @@ import {
|
|
|
70
69
|
workingSnapshot,
|
|
71
70
|
workingContent,
|
|
72
71
|
workingVariables,
|
|
72
|
+
refuseTakenSlug,
|
|
73
73
|
requireTemplate,
|
|
74
74
|
} from "../services/templates";
|
|
75
75
|
import {
|
|
@@ -89,7 +89,6 @@ import {
|
|
|
89
89
|
} from "../validation/templates.schema";
|
|
90
90
|
|
|
91
91
|
const STARTER_NOT_FOUND = "$dms_mailing.errors.starter_not_found";
|
|
92
|
-
const DUPLICATE_SLUG = "$dms_mailing.errors.duplicate_slug";
|
|
93
92
|
const INVALID_CONTENT = "$dms_mailing.errors.invalid_content";
|
|
94
93
|
const INVALID_TRANSITION = "$dms_mailing.errors.invalid_transition";
|
|
95
94
|
const NOTHING_TO_PUBLISH = "$dms_mailing.errors.nothing_to_publish";
|
|
@@ -353,7 +352,7 @@ export class TemplatesController extends Controller(
|
|
|
353
352
|
(value) => saveContentSchema.parse(value),
|
|
354
353
|
() => INVALID_CONTENT,
|
|
355
354
|
);
|
|
356
|
-
const content = input
|
|
355
|
+
const { content } = input;
|
|
357
356
|
const template = await requireTemplate(this.templates, id);
|
|
358
357
|
assertCurrentDraft(template, input.revision);
|
|
359
358
|
const variables = input.variables ?? workingVariables(template);
|
|
@@ -515,7 +514,7 @@ export class TemplatesController extends Controller(
|
|
|
515
514
|
const { content, variables } = input.content
|
|
516
515
|
? {
|
|
517
516
|
...workingSnapshot(template),
|
|
518
|
-
content: input.content
|
|
517
|
+
content: input.content,
|
|
519
518
|
}
|
|
520
519
|
: await this.previewSnapshot(template, input.version);
|
|
521
520
|
const settings = await getSettings(this.tenantId);
|
|
@@ -543,9 +542,9 @@ export class TemplatesController extends Controller(
|
|
|
543
542
|
version: "draft" | "published" | number | undefined,
|
|
544
543
|
): Promise<TemplateDraft> | TemplateDraft {
|
|
545
544
|
if (version === "published") return publishedSnapshot(template);
|
|
546
|
-
if (
|
|
547
|
-
return
|
|
548
|
-
return
|
|
545
|
+
if (version === undefined || version === "draft")
|
|
546
|
+
return workingSnapshot(template);
|
|
547
|
+
return snapshotOfVersion(this.tenantId, template, version);
|
|
549
548
|
}
|
|
550
549
|
|
|
551
550
|
/**
|
|
@@ -573,17 +572,16 @@ export class TemplatesController extends Controller(
|
|
|
573
572
|
duplicateSchema.parse(value),
|
|
574
573
|
);
|
|
575
574
|
const source = await requireTemplate(this.templates, id);
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
await assertSlugClaimed(this.tenantId, slug, copyId as string);
|
|
575
|
+
const [copyId] = await refuseTakenSlug(
|
|
576
|
+
this.templates.insertFields({
|
|
577
|
+
slug,
|
|
578
|
+
name: withTextIn(undefined, this.locale, name),
|
|
579
|
+
category: source.category,
|
|
580
|
+
status: "draft",
|
|
581
|
+
...contentFieldsOf(source),
|
|
582
|
+
updatedBy: this.author,
|
|
583
|
+
}),
|
|
584
|
+
);
|
|
587
585
|
await refreshTemplateAttention(this.tenantId);
|
|
588
586
|
return { id: copyId };
|
|
589
587
|
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type TemplateAttentionReason,
|
|
12
12
|
} from "../types";
|
|
13
13
|
import { isDeliveryUntracked, type ProviderTracking } from "./metrics";
|
|
14
|
+
import { isNumber } from "../validation/guards";
|
|
14
15
|
|
|
15
16
|
export type AttentionTone = "error" | "warning" | "neutral";
|
|
16
17
|
|
|
@@ -238,7 +239,7 @@ function namesOf(templates: MailingTemplate[]): ComposedTextParam {
|
|
|
238
239
|
};
|
|
239
240
|
}
|
|
240
241
|
|
|
241
|
-
function problemBreakdown(problems: SendSummary[])
|
|
242
|
+
function problemBreakdown(problems: SendSummary[]) {
|
|
242
243
|
const countOf = (status: string) =>
|
|
243
244
|
problems.filter((send) => send.status === status).length;
|
|
244
245
|
return {
|
|
@@ -334,9 +335,9 @@ const COUNT_PARAM = "count";
|
|
|
334
335
|
function feedParams(
|
|
335
336
|
item: AttentionItem,
|
|
336
337
|
): Record<string, ComposedTextParam> | undefined {
|
|
337
|
-
const params
|
|
338
|
+
const params = { ...item.params };
|
|
338
339
|
const count = item.count ?? item.params?.[COUNT_PARAM];
|
|
339
|
-
if (
|
|
340
|
+
if (isNumber(count)) {
|
|
340
341
|
params[COUNT_PARAM] = { type: "count", value: count };
|
|
341
342
|
}
|
|
342
343
|
return Object.keys(params).length ? params : undefined;
|
|
@@ -48,10 +48,10 @@ export function reassignRemovedCategories(
|
|
|
48
48
|
moves: ReadonlyMap<string, string> = new Map(),
|
|
49
49
|
): CategoryReassignment[] {
|
|
50
50
|
const kept = new Set(categories.map((category) => category.id));
|
|
51
|
-
return templates
|
|
52
|
-
|
|
53
|
-
.
|
|
54
|
-
templateId: template._id,
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
return templates.flatMap((template) => {
|
|
52
|
+
const { category } = template;
|
|
53
|
+
return category && !kept.has(category)
|
|
54
|
+
? [{ templateId: template._id, category: moves.get(category) ?? "" }]
|
|
55
|
+
: [];
|
|
56
|
+
});
|
|
57
57
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
sendTemplate,
|
|
11
11
|
} from "./send";
|
|
12
12
|
import { suppressionsOf } from "./suppressions";
|
|
13
|
+
import { isText } from "../validation/guards";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* A send the module checks against the suppression list. `allowSuppressed`
|
|
@@ -24,8 +25,7 @@ export const SUPPRESSED_ERROR_PREFIX = "$dms_mailing.errors.suppressed.";
|
|
|
24
25
|
|
|
25
26
|
const NO_LATENCY = null;
|
|
26
27
|
|
|
27
|
-
const addressOf = (to: EmailAddress): string =>
|
|
28
|
-
typeof to === "string" ? to : to.email;
|
|
28
|
+
const addressOf = (to: EmailAddress): string => (isText(to) ? to : to.email);
|
|
29
29
|
|
|
30
30
|
const toRecipients = (to: EmailAddress | EmailAddress[]): EmailAddress[] =>
|
|
31
31
|
Array.isArray(to) ? to : [to];
|
|
@@ -68,16 +68,10 @@ function suppressedMessage(
|
|
|
68
68
|
async function recordSuppressed(
|
|
69
69
|
slug: string,
|
|
70
70
|
params: CheckedSendParams,
|
|
71
|
-
blocked: EmailAddress[],
|
|
72
|
-
rows: Map<string, MailingSuppression>,
|
|
71
|
+
blocked: { to: EmailAddress; row: MailingSuppression }[],
|
|
73
72
|
): Promise<SendTemplateResult[]> {
|
|
74
73
|
const prepared = await prepare(params.tenantId, slug, params);
|
|
75
|
-
const messages = blocked.map((to) =>
|
|
76
|
-
suppressedMessage(
|
|
77
|
-
to,
|
|
78
|
-
rows.get(normalizeAddress(addressOf(to))) as MailingSuppression,
|
|
79
|
-
),
|
|
80
|
-
);
|
|
74
|
+
const messages = blocked.map(({ to, row }) => suppressedMessage(to, row));
|
|
81
75
|
return recordAll(prepared, params, messages);
|
|
82
76
|
}
|
|
83
77
|
|
|
@@ -103,21 +97,25 @@ export async function sendUnlessSuppressed(
|
|
|
103
97
|
const recipients = toRecipients(params.to);
|
|
104
98
|
const rows = await blockedRecipients(params, recipients);
|
|
105
99
|
if (!rows.size) return sendTemplate(slug, params);
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
const blocked = recipients.
|
|
109
|
-
|
|
100
|
+
const rowOf = (to: EmailAddress) => rows.get(normalizeAddress(addressOf(to)));
|
|
101
|
+
const isBlocked = (to: EmailAddress): boolean => rowOf(to) !== undefined;
|
|
102
|
+
const blocked = recipients.flatMap((to) => {
|
|
103
|
+
const row = rowOf(to);
|
|
104
|
+
return row ? [{ to, row }] : [];
|
|
105
|
+
});
|
|
106
|
+
const suppressedResults = await recordSuppressed(slug, params, blocked);
|
|
110
107
|
const sentResults = await sendAllowed(
|
|
111
108
|
slug,
|
|
112
109
|
params,
|
|
113
110
|
recipients.filter((to) => !isBlocked(to)),
|
|
114
111
|
);
|
|
112
|
+
let nextSuppressed = 0;
|
|
113
|
+
let nextSent = 0;
|
|
115
114
|
return aggregateResults(
|
|
116
|
-
recipients.map(
|
|
117
|
-
(to)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
: sentResults.shift()) as SendTemplateResult,
|
|
115
|
+
recipients.map((to) =>
|
|
116
|
+
isBlocked(to)
|
|
117
|
+
? suppressedResults[nextSuppressed++]
|
|
118
|
+
: sentResults[nextSent++],
|
|
121
119
|
),
|
|
122
120
|
);
|
|
123
121
|
}
|