@antelopejs/dms-mailing 0.9.1 → 0.10.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 +11 -11
- package/dist/data/read-only-routes.d.ts +14 -1
- package/dist/data/sends-table.d.ts +14 -1
- package/dist/data/suppressions-table.d.ts +14 -1
- package/dist/data/templates-table.d.ts +15 -2
- package/dist/db/models/send-events.model.d.ts +3 -3
- package/dist/db/models/sends.model.d.ts +6 -4
- package/dist/db/models/sends.model.js +26 -2
- package/dist/db/models/sends.model.js.map +1 -1
- package/dist/db/models/settings.model.d.ts +3 -3
- package/dist/db/models/suppressions.model.d.ts +3 -3
- package/dist/db/models/template-versions.model.d.ts +3 -3
- package/dist/db/models/template-versions.model.js +2 -3
- package/dist/db/models/template-versions.model.js.map +1 -1
- package/dist/db/models/templates.model.d.ts +4 -3
- package/dist/db/models/templates.model.js +14 -6
- package/dist/db/models/templates.model.js.map +1 -1
- package/dist/db/models/webhook-status.model.d.ts +3 -3
- package/dist/db/tables/templates.table.d.ts +4 -1
- package/dist/hooks/tenant-export.js +5 -1
- package/dist/hooks/tenant-export.js.map +1 -1
- package/dist/pages/editor.js +1 -1
- package/dist/pages/editor.js.map +1 -1
- package/dist/routes/metrics.js +1 -2
- package/dist/routes/metrics.js.map +1 -1
- package/dist/routes/sends.js +2 -1
- package/dist/routes/sends.js.map +1 -1
- package/dist/routes/templates.d.ts +2 -2
- package/dist/routes/templates.js +4 -4
- package/dist/routes/templates.js.map +1 -1
- package/dist/services/attention.d.ts +1 -1
- package/dist/services/attention.js +3 -3
- package/dist/services/attention.js.map +1 -1
- package/dist/services/metrics.js +5 -1
- package/dist/services/metrics.js.map +1 -1
- package/dist/services/settings.js +2 -1
- package/dist/services/settings.js.map +1 -1
- package/dist/services/template-attention.d.ts +1 -1
- package/dist/services/template-attention.js +2 -4
- package/dist/services/template-attention.js.map +1 -1
- package/dist/services/templates.js +10 -8
- package/dist/services/templates.js.map +1 -1
- package/dist/validation/declarations.schema.d.ts +1 -1
- package/dist/validation/stored.schema.d.ts +12 -0
- package/dist/validation/stored.schema.js +198 -0
- package/dist/validation/stored.schema.js.map +1 -0
- package/frontend-vue/app/composables/useEditorContext.ts +1 -1
- package/frontend-vue/build/backend-boundary.ts +7 -2
- package/frontend-vue/dms.frontend.ts +1 -4
- package/frontend-vue/eslint.config.mjs +9 -1
- package/frontend-vue/package.json +1 -1
- package/frontend-vue/pnpm-lock.yaml +24 -7
- package/frontend-vue/tests/backend-boundary.test.ts +4 -4
- package/frontend-vue/tests/column-displays.test.ts +13 -12
- package/frontend-vue/tests/footer-block.test.ts +1 -1
- package/frontend-vue/tests/template-gallery-display.test.ts +6 -5
- package/frontend-vue/tests/template-overlays.test.ts +4 -12
- package/frontend-vue/tsconfig.typecheck.json +1 -2
- package/package.json +16 -16
- package/src/db/models/sends.model.ts +34 -4
- package/src/db/models/template-versions.model.ts +8 -6
- package/src/db/models/templates.model.ts +25 -9
- package/src/hooks/tenant-export.ts +5 -2
- package/src/pages/editor.ts +1 -1
- package/src/routes/metrics.ts +1 -2
- package/src/routes/sends.ts +2 -3
- package/src/routes/templates.ts +9 -9
- package/src/services/attention.ts +6 -6
- package/src/services/metrics.ts +3 -1
- package/src/services/settings.ts +9 -3
- package/src/services/template-attention.ts +4 -5
- package/src/services/templates.ts +35 -16
- package/src/validation/stored.schema.ts +257 -0
- /package/frontend-vue/app/components/{Editor.vue → TemplateEditor.vue} +0 -0
- /package/frontend-vue/app/components/blocks/{Button.vue → BlockButton.vue} +0 -0
- /package/frontend-vue/app/components/blocks/{Code.vue → BlockCode.vue} +0 -0
- /package/frontend-vue/app/components/blocks/{Divider.vue → BlockDivider.vue} +0 -0
- /package/frontend-vue/app/components/blocks/{Footer.vue → BlockFooter.vue} +0 -0
- /package/frontend-vue/app/components/blocks/{Heading.vue → BlockHeading.vue} +0 -0
- /package/frontend-vue/app/components/blocks/{Hero.vue → BlockHero.vue} +0 -0
- /package/frontend-vue/app/components/blocks/{List.vue → BlockList.vue} +0 -0
- /package/frontend-vue/app/components/blocks/{Paragraph.vue → BlockParagraph.vue} +0 -0
- /package/frontend-vue/app/components/blocks/{Total.vue → BlockTotal.vue} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/dms-mailing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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,23 +46,23 @@
|
|
|
46
46
|
"registry": "https://registry.npmjs.org/"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@antelopejs/interface-api": ">=0.3.
|
|
50
|
-
"@antelopejs/interface-api-util": ">=0.3.
|
|
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.
|
|
57
|
-
"@antelopejs/interface-email": ">=0.0
|
|
49
|
+
"@antelopejs/interface-api": ">=0.3.3 <0.4.0",
|
|
50
|
+
"@antelopejs/interface-api-util": ">=0.3.2 <0.4.0",
|
|
51
|
+
"@antelopejs/interface-core": ">=0.5.0 <0.6.0",
|
|
52
|
+
"@antelopejs/interface-data-api": ">=0.8.0 <0.9.0",
|
|
53
|
+
"@antelopejs/interface-database": ">=0.4.0 <0.5.0",
|
|
54
|
+
"@antelopejs/interface-database-decorators": ">=0.5.0 <0.6.0",
|
|
55
|
+
"@antelopejs/interface-dms": ">=0.14.0 <0.15.0",
|
|
56
|
+
"@antelopejs/interface-dms-mailing": ">=0.11.0 <0.12.0",
|
|
57
|
+
"@antelopejs/interface-email": ">=0.2.0 <0.3.0",
|
|
58
58
|
"node-cron": "^4.2.1",
|
|
59
59
|
"zod": "~3.24.2"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@antelopejs/core": "
|
|
63
|
-
"@antelopejs/dms": ">=0.
|
|
64
|
-
"@antelopejs/mongodb": "
|
|
65
|
-
"@antelopejs/tooling-configs": "
|
|
62
|
+
"@antelopejs/core": "~1.23.2",
|
|
63
|
+
"@antelopejs/dms": ">=0.20.0 <0.21.0",
|
|
64
|
+
"@antelopejs/mongodb": "~1.8.1",
|
|
65
|
+
"@antelopejs/tooling-configs": ">=0.7.1 <0.8.0",
|
|
66
66
|
"@types/chai": "^4.3.20",
|
|
67
67
|
"@types/mocha": "^10",
|
|
68
68
|
"@types/node": "^24.0.0",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"typescript": "^6.0.3"
|
|
78
78
|
},
|
|
79
79
|
"optionalDependencies": {
|
|
80
|
-
"@antelopejs/interface-dms-automation": ">=0.6.0 <
|
|
80
|
+
"@antelopejs/interface-dms-automation": ">=0.6.0 <0.7.0"
|
|
81
81
|
},
|
|
82
82
|
"antelopeJs": {
|
|
83
83
|
"baseUrl": "dist",
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"typecheck": "tsc --noEmit",
|
|
99
99
|
"knip": "knip --dependencies",
|
|
100
100
|
"knip:all": "knip",
|
|
101
|
-
"release": "pnpm --filter @antelopejs/interface-dms-mailing build && pnpm --dir ../.. install --frozen-lockfile && release-it",
|
|
101
|
+
"release": "pnpm --dir ../.. run typecheck && pnpm --filter @antelopejs/interface-dms-mailing build && pnpm --dir ../.. install --frozen-lockfile && release-it",
|
|
102
102
|
"test": "pnpm run test:backend && pnpm run test:layer",
|
|
103
103
|
"test:backend": "([ -n \"$DMS_PREBUILT\" ] || pnpm run build) && ajs module test .",
|
|
104
104
|
"test:layer": "cd frontend-vue && pnpm test",
|
|
@@ -24,8 +24,28 @@ export type ImportedSendRow = Omit<
|
|
|
24
24
|
|
|
25
25
|
const MAX_MUTATION_ATTEMPTS = 5;
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const SEND_SUMMARY_FIELDS = [
|
|
28
|
+
"_id",
|
|
29
|
+
"createdAt",
|
|
30
|
+
"templateSlug",
|
|
31
|
+
"recipientEmail",
|
|
32
|
+
"status",
|
|
33
|
+
"stage",
|
|
34
|
+
"latencyMs",
|
|
35
|
+
"opens",
|
|
36
|
+
"clicks",
|
|
37
|
+
"isTest",
|
|
38
|
+
"error",
|
|
39
|
+
"source",
|
|
40
|
+
"locale",
|
|
41
|
+
"requestedLocale",
|
|
42
|
+
] as const;
|
|
43
|
+
|
|
44
|
+
/** A send as the figures read it: only the fields they count. */
|
|
45
|
+
export type SendSummary = Pick<
|
|
46
|
+
MailingSend,
|
|
47
|
+
(typeof SEND_SUMMARY_FIELDS)[number]
|
|
48
|
+
>;
|
|
29
49
|
|
|
30
50
|
type SendActivity = Pick<
|
|
31
51
|
MailingSend,
|
|
@@ -122,7 +142,7 @@ export class SendModel extends BasicDataModel(
|
|
|
122
142
|
return this.table
|
|
123
143
|
.filter((row) => row.key("createdAt").ge(from))
|
|
124
144
|
.filter((row) => row.key("createdAt").lt(to))
|
|
125
|
-
.
|
|
145
|
+
.pluck(...SEND_SUMMARY_FIELDS)
|
|
126
146
|
.run();
|
|
127
147
|
}
|
|
128
148
|
|
|
@@ -259,7 +279,7 @@ export class SendModel extends BasicDataModel(
|
|
|
259
279
|
return this.table
|
|
260
280
|
.getAll(templateSlug, "templateSlug")
|
|
261
281
|
.filter((row) => row.key("createdAt").ge(from))
|
|
262
|
-
.
|
|
282
|
+
.pluck(...SEND_SUMMARY_FIELDS)
|
|
263
283
|
.run();
|
|
264
284
|
}
|
|
265
285
|
|
|
@@ -270,4 +290,14 @@ export class SendModel extends BasicDataModel(
|
|
|
270
290
|
.count()
|
|
271
291
|
.run();
|
|
272
292
|
}
|
|
293
|
+
|
|
294
|
+
/** The slugs of the real sends made from `from` until `to`. */
|
|
295
|
+
listSentSlugs(from: Date, to: Date): Promise<string[]> {
|
|
296
|
+
return this.table
|
|
297
|
+
.filter((row) => row.key("createdAt").ge(from))
|
|
298
|
+
.filter((row) => row.key("createdAt").lt(to))
|
|
299
|
+
.filter((row) => row.key("isTest").ne(true))
|
|
300
|
+
.distinct("templateSlug")
|
|
301
|
+
.run();
|
|
302
|
+
}
|
|
273
303
|
}
|
|
@@ -4,6 +4,10 @@ import {
|
|
|
4
4
|
type MailingTemplate,
|
|
5
5
|
MailingTemplateVersion,
|
|
6
6
|
} from "../tables";
|
|
7
|
+
import {
|
|
8
|
+
parseStored,
|
|
9
|
+
storedVersionRowSchema,
|
|
10
|
+
} from "../../validation/stored.schema";
|
|
7
11
|
import { TemplateModel } from "./templates.model";
|
|
8
12
|
|
|
9
13
|
/** The latest committed history row, even before its separate insert completes. */
|
|
@@ -11,13 +15,11 @@ export function committedVersionOf(
|
|
|
11
15
|
template: Pick<MailingTemplate, "json_published_version">,
|
|
12
16
|
): MailingTemplateVersion | undefined {
|
|
13
17
|
if (!template.json_published_version) return undefined;
|
|
14
|
-
|
|
15
|
-
// JSON.stringify of the version row they insert; its publishedAt reads back as a string, and the next line restores the Date.
|
|
16
|
-
const row = JSON.parse(
|
|
18
|
+
return parseStored(
|
|
17
19
|
template.json_published_version,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
storedVersionRowSchema,
|
|
21
|
+
"a version row",
|
|
22
|
+
);
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
export class TemplateVersionModel extends BasicDataModel(
|
|
@@ -35,7 +35,9 @@ export class TemplateModel extends BasicDataModel(
|
|
|
35
35
|
) {
|
|
36
36
|
/** The row of `fields`, its name written in every language it holds. */
|
|
37
37
|
static fromFields(fields: TemplateFields): MailingTemplate {
|
|
38
|
-
|
|
38
|
+
const row = TemplateModel.fromPlainData(fields);
|
|
39
|
+
row.localize("*", ["name"]);
|
|
40
|
+
return row;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
/**
|
|
@@ -57,19 +59,30 @@ export class TemplateModel extends BasicDataModel(
|
|
|
57
59
|
return this.insert(TemplateModel.fromFields(fields));
|
|
58
60
|
}
|
|
59
61
|
|
|
62
|
+
/**
|
|
63
|
+
* The stored form of a patch. The row's modifier container (`_internal`,
|
|
64
|
+
* where the localized name lives) comes out empty when the patch has no
|
|
65
|
+
* name, and the database merges a row shallowly, so that empty container
|
|
66
|
+
* would wipe the stored name: it is left out then.
|
|
67
|
+
*/
|
|
68
|
+
private static storedPatch(row: MailingTemplate, fields: TemplateFields) {
|
|
69
|
+
const stored = TemplateModel.toDatabase(row);
|
|
70
|
+
if (fields.name === undefined) delete stored[MODIFIER_CONTAINER];
|
|
71
|
+
return stored;
|
|
72
|
+
}
|
|
73
|
+
|
|
60
74
|
/**
|
|
61
75
|
* Updates a template, a name given holding every language. Every write of
|
|
62
|
-
* a template goes through here rather than `update
|
|
63
|
-
*
|
|
64
|
-
* lives) empty when the patch has no name, and the database merges a row
|
|
65
|
-
* shallowly, so that empty container would wipe the stored name.
|
|
76
|
+
* a template goes through here rather than `update`, which would store the
|
|
77
|
+
* patch's empty modifier container over the name.
|
|
66
78
|
*/
|
|
67
79
|
updateFields(id: string, fields: TemplateFields): Promise<number> {
|
|
68
80
|
const row = TemplateModel.fromFields(fields);
|
|
69
81
|
triggerEvent(row, "update");
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
82
|
+
return this.table
|
|
83
|
+
.get(id)
|
|
84
|
+
.update(TemplateModel.storedPatch(row, fields))
|
|
85
|
+
.run();
|
|
73
86
|
}
|
|
74
87
|
|
|
75
88
|
/** Publishes only over the exact semantic revision that was inspected. */
|
|
@@ -77,7 +90,10 @@ export class TemplateModel extends BasicDataModel(
|
|
|
77
90
|
template: MailingTemplate,
|
|
78
91
|
patch: TemplateFields,
|
|
79
92
|
): Promise<boolean> {
|
|
80
|
-
const stored = TemplateModel.
|
|
93
|
+
const stored = TemplateModel.storedPatch(
|
|
94
|
+
TemplateModel.fromFields(patch),
|
|
95
|
+
patch,
|
|
96
|
+
);
|
|
81
97
|
const outcome = await this.table
|
|
82
98
|
.atomicMutation(template._id, {
|
|
83
99
|
type: "update",
|
|
@@ -102,8 +102,11 @@ async function loadPage<T extends object>(
|
|
|
102
102
|
|
|
103
103
|
/** A template as exported: its name in every language. */
|
|
104
104
|
const templateCodec: RowCodec<MailingTemplate> = {
|
|
105
|
-
fromDatabase: (
|
|
106
|
-
TemplateModel.fromDatabase(
|
|
105
|
+
fromDatabase: (stored) => {
|
|
106
|
+
const row = TemplateModel.fromDatabase(stored);
|
|
107
|
+
row?.localize("*", ["name"]);
|
|
108
|
+
return row;
|
|
109
|
+
},
|
|
107
110
|
};
|
|
108
111
|
|
|
109
112
|
export function exportedCollections(tenantId: string): ExportedCollection[] {
|
package/src/pages/editor.ts
CHANGED
|
@@ -25,7 +25,7 @@ export class MailingEditorController extends PageController(
|
|
|
25
25
|
},
|
|
26
26
|
DefaultLayout({ fullWidth: true, hideHeader: true, fillHeight: true }),
|
|
27
27
|
) {
|
|
28
|
-
static editor = CustomComponent("
|
|
28
|
+
static editor = CustomComponent("MailingTemplateEditor").meta({
|
|
29
29
|
name: "$dms_mailing.permissions.editor.name",
|
|
30
30
|
description: "$dms_mailing.permissions.editor.description",
|
|
31
31
|
icon: "i-ph-pencil-simple",
|
package/src/routes/metrics.ts
CHANGED
|
@@ -324,11 +324,10 @@ export class MetricsController extends Controller(`${API_BASE_PATH}/metrics`) {
|
|
|
324
324
|
findSettings(this.tenantId),
|
|
325
325
|
]);
|
|
326
326
|
const sends = forAudience(rows, this.audience);
|
|
327
|
-
const excluded = rows.filter((row) => !sends.includes(row));
|
|
328
327
|
const items = buildAttentionItems({
|
|
329
328
|
templates,
|
|
330
329
|
sends,
|
|
331
|
-
|
|
330
|
+
excludedCount: rows.length - sends.length,
|
|
332
331
|
windowEnd: range.to,
|
|
333
332
|
tracking,
|
|
334
333
|
isSenderSet: Boolean(settings?.senderEmail),
|
package/src/routes/sends.ts
CHANGED
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
} from "../services/templates";
|
|
71
71
|
import type { SendStatus, SuppressionSummary, TemplateStatus } from "../types";
|
|
72
72
|
import type { JsonObject } from "../types/json";
|
|
73
|
+
import { parseJsonObject } from "../validation/json";
|
|
73
74
|
import {
|
|
74
75
|
type previewVersionSchema,
|
|
75
76
|
realSendSchema,
|
|
@@ -578,9 +579,7 @@ function requestedVersionOf(
|
|
|
578
579
|
}
|
|
579
580
|
|
|
580
581
|
function variablesOf(send: MailingSend): JsonObject {
|
|
581
|
-
|
|
582
|
-
// JSON.stringify(send.variables) (services/send-import.ts), both a JsonObject.
|
|
583
|
-
return JSON.parse(send.json_variables || "{}") as JsonObject;
|
|
582
|
+
return parseJsonObject(send.json_variables || "{}");
|
|
584
583
|
}
|
|
585
584
|
|
|
586
585
|
function summarize(template: MailingTemplate): SendTemplateSummary {
|
package/src/routes/templates.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
Parameter,
|
|
7
7
|
Post,
|
|
8
8
|
Put,
|
|
9
|
+
type QueryValue,
|
|
9
10
|
type RequestContext,
|
|
10
11
|
} from "@antelopejs/interface-api";
|
|
11
12
|
import { assert, assertValidation } from "@antelopejs/interface-api-util";
|
|
@@ -32,7 +33,7 @@ import {
|
|
|
32
33
|
} from "../db";
|
|
33
34
|
import { INVALID_TEMPLATE, saveDetails } from "../data/templates-table";
|
|
34
35
|
import { TemplatesPageController } from "../pages/templates";
|
|
35
|
-
import { forAudience, statsWindowStart } from "../services/metrics";
|
|
36
|
+
import { forAudience, groupBy, statsWindowStart } from "../services/metrics";
|
|
36
37
|
import { refreshTemplateAttention } from "../services/template-attention";
|
|
37
38
|
import { renderEmailHtml } from "../services/render";
|
|
38
39
|
import { categoriesIn } from "../services/categories";
|
|
@@ -203,14 +204,14 @@ export class TemplatesController extends Controller(
|
|
|
203
204
|
@Get("/starters/:id/preview")
|
|
204
205
|
async starterPreview(
|
|
205
206
|
@Parameter("id", "param") id: string,
|
|
206
|
-
@Parameter("locale", "query") requested
|
|
207
|
+
@Parameter("locale", "query") requested: QueryValue,
|
|
207
208
|
) {
|
|
208
209
|
const starter = findStarter(id);
|
|
209
210
|
assert(starter, HTTP_NOT_FOUND, STARTER_NOT_FOUND);
|
|
210
211
|
const settings = await getSettings(this.tenantId);
|
|
211
212
|
const locale = pickLocale(
|
|
212
213
|
starter.content,
|
|
213
|
-
requested || undefined,
|
|
214
|
+
requested === true || requested === "" ? undefined : requested,
|
|
214
215
|
settings.fallbackLocale,
|
|
215
216
|
);
|
|
216
217
|
const localeContent = localeContentOf(starter.content, locale);
|
|
@@ -234,14 +235,13 @@ export class TemplatesController extends Controller(
|
|
|
234
235
|
this.templates.getAll(),
|
|
235
236
|
this.sends.listBetween(statsWindowStart(), new Date()),
|
|
236
237
|
]);
|
|
237
|
-
const
|
|
238
|
+
const bySlug = groupBy(
|
|
239
|
+
forAudience(rows, BUSINESS_AUDIENCE),
|
|
240
|
+
(send) => send.templateSlug,
|
|
241
|
+
);
|
|
238
242
|
return {
|
|
239
243
|
items: templates.map((template) =>
|
|
240
|
-
statsOf(
|
|
241
|
-
template._id,
|
|
242
|
-
template.slug,
|
|
243
|
-
sends.filter((send) => send.templateSlug === template.slug),
|
|
244
|
-
),
|
|
244
|
+
statsOf(template._id, template.slug, bySlug.get(template.slug) ?? []),
|
|
245
245
|
),
|
|
246
246
|
};
|
|
247
247
|
}
|
|
@@ -124,13 +124,13 @@ function unusedLiveItem(window: AttentionWindow): AttentionItem[] {
|
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
126
|
* Every send the Overview window holds, split the way the business view counts
|
|
127
|
-
* them: `sends` are the ones behind the figures, `
|
|
128
|
-
* business audience drops.
|
|
127
|
+
* them: `sends` are the ones behind the figures, `excludedCount` the test sends
|
|
128
|
+
* the business audience drops.
|
|
129
129
|
*/
|
|
130
130
|
export interface AttentionWindow {
|
|
131
131
|
templates: MailingTemplate[];
|
|
132
132
|
sends: SendSummary[];
|
|
133
|
-
|
|
133
|
+
excludedCount: number;
|
|
134
134
|
/** When the window closes. */
|
|
135
135
|
windowEnd: Date;
|
|
136
136
|
/** What the configured provider can report; `null` when it is unreachable. */
|
|
@@ -140,7 +140,7 @@ export interface AttentionWindow {
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
function testOnlyWindowItem(window: AttentionWindow): AttentionItem[] {
|
|
143
|
-
if (window.sends.length || !window.
|
|
143
|
+
if (window.sends.length || !window.excludedCount) return [];
|
|
144
144
|
return [
|
|
145
145
|
{
|
|
146
146
|
id: "test-only-window",
|
|
@@ -150,8 +150,8 @@ function testOnlyWindowItem(window: AttentionWindow): AttentionItem[] {
|
|
|
150
150
|
description: "$dms_mailing.attention.test_only_window.description",
|
|
151
151
|
action: "$dms_mailing.attention.test_only_window.action",
|
|
152
152
|
to: SENDS_PAGE,
|
|
153
|
-
count: window.
|
|
154
|
-
params: { count: window.
|
|
153
|
+
count: window.excludedCount,
|
|
154
|
+
params: { count: window.excludedCount },
|
|
155
155
|
},
|
|
156
156
|
];
|
|
157
157
|
}
|
package/src/services/metrics.ts
CHANGED
|
@@ -276,7 +276,9 @@ export function groupBy<T>(
|
|
|
276
276
|
const groups = new Map<string, T[]>();
|
|
277
277
|
for (const item of items) {
|
|
278
278
|
const id = key(item);
|
|
279
|
-
|
|
279
|
+
const group = groups.get(id);
|
|
280
|
+
if (group) group.push(item);
|
|
281
|
+
else groups.set(id, [item]);
|
|
280
282
|
}
|
|
281
283
|
return groups;
|
|
282
284
|
}
|
package/src/services/settings.ts
CHANGED
|
@@ -10,6 +10,10 @@ import {
|
|
|
10
10
|
} from "../types";
|
|
11
11
|
import type { JsonObject } from "../types/json";
|
|
12
12
|
import type { SettingsPatch } from "../validation/settings.schema";
|
|
13
|
+
import {
|
|
14
|
+
parseStored,
|
|
15
|
+
storedCategoriesSchema,
|
|
16
|
+
} from "../validation/stored.schema";
|
|
13
17
|
|
|
14
18
|
export const WEBHOOK_SECRET_BYTES = 24;
|
|
15
19
|
|
|
@@ -48,9 +52,11 @@ function toValues(row: MailingSettings): MailingSettingsValues {
|
|
|
48
52
|
replyTo: row.replyTo,
|
|
49
53
|
webhookSecret: row.webhookSecret,
|
|
50
54
|
webhookSecretSource: storedSecretSource(row.webhookSecretSource),
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
categories: parseStored(
|
|
56
|
+
row.json_categories,
|
|
57
|
+
storedCategoriesSchema,
|
|
58
|
+
"template categories",
|
|
59
|
+
),
|
|
54
60
|
};
|
|
55
61
|
}
|
|
56
62
|
|
|
@@ -5,13 +5,13 @@ import {
|
|
|
5
5
|
type TemplateAttentionFields,
|
|
6
6
|
TemplateModel,
|
|
7
7
|
} from "../db";
|
|
8
|
-
import {
|
|
8
|
+
import type { TemplateAttentionReason } from "../types";
|
|
9
9
|
import {
|
|
10
10
|
knownLocalesOf,
|
|
11
11
|
type TemplateAttentionContext,
|
|
12
12
|
templateAttentionReasons,
|
|
13
13
|
} from "./attention";
|
|
14
|
-
import {
|
|
14
|
+
import { statsWindowStart } from "./metrics";
|
|
15
15
|
|
|
16
16
|
/** How the stored `attention` field separates its reasons. */
|
|
17
17
|
const REASON_SEPARATOR = ",";
|
|
@@ -31,14 +31,13 @@ async function attentionContext(
|
|
|
31
31
|
templates: MailingTemplate[],
|
|
32
32
|
): Promise<TemplateAttentionContext> {
|
|
33
33
|
const now = Date.now();
|
|
34
|
-
const
|
|
34
|
+
const slugs = await GetModel(SendModel, tenantId).listSentSlugs(
|
|
35
35
|
statsWindowStart(now),
|
|
36
36
|
new Date(now),
|
|
37
37
|
);
|
|
38
|
-
const real = forAudience(sends, BUSINESS_AUDIENCE);
|
|
39
38
|
return {
|
|
40
39
|
now,
|
|
41
|
-
sentSlugs: new Set(
|
|
40
|
+
sentSlugs: new Set(slugs),
|
|
42
41
|
knownLocales: knownLocalesOf(templates),
|
|
43
42
|
};
|
|
44
43
|
}
|
|
@@ -28,6 +28,13 @@ import type {
|
|
|
28
28
|
} from "../types";
|
|
29
29
|
import { DEFAULT_FALLBACK_LOCALE } from "../types";
|
|
30
30
|
import type { JsonObject } from "../types/json";
|
|
31
|
+
import { parseJsonObject } from "../validation/json";
|
|
32
|
+
import {
|
|
33
|
+
parseStored,
|
|
34
|
+
storedChangesSchema,
|
|
35
|
+
storedContentSchema,
|
|
36
|
+
storedVariablesSchema,
|
|
37
|
+
} from "../validation/stored.schema";
|
|
31
38
|
|
|
32
39
|
export interface TemplateAuthor {
|
|
33
40
|
name?: string;
|
|
@@ -74,8 +81,11 @@ export async function refuseTakenSlug<T>(write: Promise<T>): Promise<T> {
|
|
|
74
81
|
|
|
75
82
|
/** The content customers receive: the last published version. */
|
|
76
83
|
export function parseContent(template: MailingTemplate): TemplateContent {
|
|
77
|
-
|
|
78
|
-
|
|
84
|
+
return parseStored(
|
|
85
|
+
template.json_content || EMPTY_CONTENT,
|
|
86
|
+
storedContentSchema,
|
|
87
|
+
"template content",
|
|
88
|
+
);
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
export function hasDraft(template: MailingTemplate): boolean {
|
|
@@ -85,8 +95,11 @@ export function hasDraft(template: MailingTemplate): boolean {
|
|
|
85
95
|
/** The content the editor works on: the draft, or the published content. */
|
|
86
96
|
export function workingContent(template: MailingTemplate): TemplateContent {
|
|
87
97
|
if (!template.json_draft) return parseContent(template);
|
|
88
|
-
|
|
89
|
-
|
|
98
|
+
return parseStored(
|
|
99
|
+
template.json_draft,
|
|
100
|
+
storedContentSchema,
|
|
101
|
+
"template content",
|
|
102
|
+
);
|
|
90
103
|
}
|
|
91
104
|
|
|
92
105
|
/** The version customers receive; 0 for a template never published. */
|
|
@@ -105,8 +118,11 @@ export function isPublished(template: MailingTemplate): boolean {
|
|
|
105
118
|
export function parseVariables(
|
|
106
119
|
template: MailingTemplate,
|
|
107
120
|
): VariableDefinition[] {
|
|
108
|
-
|
|
109
|
-
|
|
121
|
+
return parseStored(
|
|
122
|
+
template.json_variables,
|
|
123
|
+
storedVariablesSchema,
|
|
124
|
+
"variable declarations",
|
|
125
|
+
);
|
|
110
126
|
}
|
|
111
127
|
|
|
112
128
|
/**
|
|
@@ -118,8 +134,11 @@ export function workingVariables(
|
|
|
118
134
|
): VariableDefinition[] {
|
|
119
135
|
if (!hasDraft(template) || !template.json_variables_draft)
|
|
120
136
|
return parseVariables(template);
|
|
121
|
-
|
|
122
|
-
|
|
137
|
+
return parseStored(
|
|
138
|
+
template.json_variables_draft,
|
|
139
|
+
storedVariablesSchema,
|
|
140
|
+
"variable declarations",
|
|
141
|
+
);
|
|
123
142
|
}
|
|
124
143
|
|
|
125
144
|
/** A template's working state: what a publish would ship. */
|
|
@@ -175,8 +194,7 @@ export function pendingChanges(template: MailingTemplate): TemplateChange[] {
|
|
|
175
194
|
}
|
|
176
195
|
|
|
177
196
|
export function parseTestData(template: MailingTemplate): JsonObject {
|
|
178
|
-
|
|
179
|
-
return JSON.parse(template.json_test_data) as JsonObject;
|
|
197
|
+
return parseJsonObject(template.json_test_data);
|
|
180
198
|
}
|
|
181
199
|
|
|
182
200
|
interface FooterLabels {
|
|
@@ -297,16 +315,18 @@ export async function discardDraft(
|
|
|
297
315
|
export function parseVersionChanges(
|
|
298
316
|
row: MailingTemplateVersion,
|
|
299
317
|
): TemplateChange[] {
|
|
300
|
-
|
|
301
|
-
return JSON.parse(row.json_changes) as TemplateChange[];
|
|
318
|
+
return parseStored(row.json_changes, storedChangesSchema, "version changes");
|
|
302
319
|
}
|
|
303
320
|
|
|
304
321
|
/** The declarations a published version shipped with. */
|
|
305
322
|
export function parseVersionVariables(
|
|
306
323
|
row: MailingTemplateVersion,
|
|
307
324
|
): VariableDefinition[] {
|
|
308
|
-
|
|
309
|
-
|
|
325
|
+
return parseStored(
|
|
326
|
+
row.json_variables,
|
|
327
|
+
storedVariablesSchema,
|
|
328
|
+
"variable declarations",
|
|
329
|
+
);
|
|
310
330
|
}
|
|
311
331
|
|
|
312
332
|
/**
|
|
@@ -461,8 +481,7 @@ export async function contentOfVersion(
|
|
|
461
481
|
export function parseVersionContent(
|
|
462
482
|
row: MailingTemplateVersion,
|
|
463
483
|
): TemplateContent {
|
|
464
|
-
|
|
465
|
-
return JSON.parse(row.json_content) as TemplateContent;
|
|
484
|
+
return parseStored(row.json_content, storedContentSchema, "template content");
|
|
466
485
|
}
|
|
467
486
|
|
|
468
487
|
/**
|