@antelopejs/dms-mailing 0.6.1 → 0.7.1
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 +8 -7
- package/dist/constants.d.ts +1 -2
- package/dist/constants.js +2 -3
- package/dist/constants.js.map +1 -1
- package/dist/data/templates-table.d.ts +4 -1
- package/dist/data/templates-table.js +55 -20
- package/dist/data/templates-table.js.map +1 -1
- package/dist/data-types/categories-type.d.ts +5 -5
- 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/templates.model.d.ts +10 -1
- package/dist/db/models/templates.model.js +29 -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 +7 -4
- package/dist/db/tables/templates.table.js +4 -14
- package/dist/db/tables/templates.table.js.map +1 -1
- package/dist/engine/formats.d.ts +13 -0
- package/dist/engine/formats.js +85 -0
- package/dist/engine/formats.js.map +1 -0
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.js +1 -0
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/resolve.d.ts +2 -1
- package/dist/engine/resolve.js +14 -7
- package/dist/engine/resolve.js.map +1 -1
- package/dist/engine/tokens.d.ts +2 -1
- package/dist/engine/tokens.js +3 -5
- package/dist/engine/tokens.js.map +1 -1
- package/dist/frontend-module.d.ts +2 -0
- package/dist/frontend-module.js +24 -0
- package/dist/frontend-module.js.map +1 -0
- 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/hooks/tenant-export.js +4 -1
- package/dist/hooks/tenant-export.js.map +1 -1
- package/dist/implementations/dms-mailing/index.d.ts +3 -3
- package/dist/implementations/dms-mailing/index.js +8 -7
- package/dist/implementations/dms-mailing/index.js.map +1 -1
- package/dist/index.js +2 -20
- package/dist/index.js.map +1 -1
- package/dist/pages/overview.d.ts +1 -1
- package/dist/pages/overview.js +11 -13
- package/dist/pages/overview.js.map +1 -1
- package/dist/pages/sends.d.ts +1 -1
- package/dist/pages/sends.js +16 -8
- 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/templates.js +3 -8
- package/dist/pages/templates.js.map +1 -1
- package/dist/pages/toolbar.d.ts +12 -0
- package/dist/pages/toolbar.js +41 -0
- package/dist/pages/toolbar.js.map +1 -0
- package/dist/realtime.d.ts +2 -2
- package/dist/realtime.js +8 -8
- package/dist/realtime.js.map +1 -1
- package/dist/routes/metrics.d.ts +9 -2
- package/dist/routes/metrics.js +90 -35
- package/dist/routes/metrics.js.map +1 -1
- package/dist/routes/period.d.ts +1 -0
- package/dist/routes/period.js +19 -0
- package/dist/routes/period.js.map +1 -1
- package/dist/routes/sends.js +6 -3
- package/dist/routes/sends.js.map +1 -1
- package/dist/routes/settings.js +6 -7
- package/dist/routes/settings.js.map +1 -1
- package/dist/routes/templates.d.ts +8 -4
- package/dist/routes/templates.js +41 -20
- package/dist/routes/templates.js.map +1 -1
- package/dist/services/categories.d.ts +2 -1
- package/dist/services/categories.js +9 -0
- package/dist/services/categories.js.map +1 -1
- package/dist/services/declarations.js +42 -184
- package/dist/services/declarations.js.map +1 -1
- package/dist/services/events.js +1 -1
- package/dist/services/events.js.map +1 -1
- package/dist/services/localized-text.d.ts +7 -0
- package/dist/services/localized-text.js +28 -0
- package/dist/services/localized-text.js.map +1 -0
- package/dist/services/send-import.js +0 -2
- package/dist/services/send-import.js.map +1 -1
- package/dist/services/send-stat-items.js +34 -12
- package/dist/services/send-stat-items.js.map +1 -1
- package/dist/services/send-stats.d.ts +7 -0
- package/dist/services/send-stats.js +17 -5
- package/dist/services/send-stats.js.map +1 -1
- package/dist/services/send.d.ts +2 -0
- package/dist/services/send.js +11 -12
- package/dist/services/send.js.map +1 -1
- package/dist/services/sender-declaration.d.ts +2 -0
- package/dist/services/sender-declaration.js +18 -0
- package/dist/services/sender-declaration.js.map +1 -0
- 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.d.ts +2 -2
- package/dist/services/starters.js +1 -10
- package/dist/services/starters.js.map +1 -1
- package/dist/services/templates.d.ts +13 -7
- package/dist/services/templates.js +72 -33
- package/dist/services/templates.js.map +1 -1
- package/dist/services/webhook-status.d.ts +2 -1
- package/dist/services/webhook-status.js +6 -5
- package/dist/services/webhook-status.js.map +1 -1
- package/dist/types/settings.d.ts +8 -0
- package/dist/types/settings.js +33 -6
- package/dist/types/settings.js.map +1 -1
- package/dist/validation/declarations.schema.d.ts +25 -7
- package/dist/validation/declarations.schema.js +16 -3
- package/dist/validation/declarations.schema.js.map +1 -1
- package/dist/validation/settings.schema.d.ts +41 -41
- package/dist/validation/settings.schema.js +30 -6
- package/dist/validation/settings.schema.js.map +1 -1
- package/dist/validation/templates.schema.d.ts +13 -0
- package/dist/validation/templates.schema.js +6 -1
- package/dist/validation/templates.schema.js.map +1 -1
- package/frontend-vue/app/components/CategoriesInput.vue +15 -13
- package/frontend-vue/app/components/FunnelCard.vue +46 -3
- package/frontend-vue/app/components/NewTemplateModal.vue +4 -2
- package/frontend-vue/app/components/OverviewGate.vue +0 -8
- package/frontend-vue/app/components/ProviderPill.vue +50 -0
- package/frontend-vue/app/components/SendDrawer.vue +206 -183
- package/frontend-vue/app/components/StarterGallery.vue +1 -1
- package/frontend-vue/app/components/TemplateCard.vue +4 -1
- package/frontend-vue/app/components/TemplateDrawer.vue +3 -5
- package/frontend-vue/app/components/TemplateGallery.vue +57 -17
- package/frontend-vue/app/components/TemplatePreviewFrame.vue +41 -11
- package/frontend-vue/app/components/editor/EditorTemplatePane.vue +1 -3
- package/frontend-vue/app/components/editor/EditorTitle.vue +1 -7
- package/frontend-vue/app/composables/useLocalizedDefaults.ts +5 -7
- package/frontend-vue/app/composables/useMailingApi.ts +3 -3
- package/frontend-vue/app/composables/useMailingReads.ts +2 -2
- package/frontend-vue/app/composables/useTemplateCategories.ts +8 -7
- package/frontend-vue/app/types/mailing.ts +18 -0
- package/frontend-vue/app/utils/categories.ts +58 -49
- package/frontend-vue/app/utils/funnel.ts +25 -0
- package/frontend-vue/app/utils/period.ts +0 -7
- package/frontend-vue/app/utils/starters.ts +3 -1
- package/frontend-vue/app/utils/tokens.ts +6 -1
- package/frontend-vue/i18n/locales/mailing-en-GB.json +19 -28
- package/frontend-vue/i18n/locales/mailing-fr-FR.json +19 -28
- package/frontend-vue/package.json +1 -1
- package/frontend-vue/tests/categories-input.test.ts +4 -10
- package/frontend-vue/tests/categories.test.ts +55 -17
- package/frontend-vue/tests/conditions.test.ts +1 -4
- package/frontend-vue/tests/funnel-card.test.ts +27 -0
- package/frontend-vue/tests/funnel.test.ts +25 -0
- package/frontend-vue/tests/localized-defaults.test.ts +0 -29
- package/frontend-vue/tests/new-template-modal.test.ts +1 -1
- package/frontend-vue/tests/overview-gate.test.ts +1 -24
- package/frontend-vue/tests/page-reads.test.ts +9 -25
- package/frontend-vue/tests/period.test.ts +0 -12
- package/frontend-vue/tests/replay-toast.test.ts +2 -1
- package/frontend-vue/tests/send-drawer.test.ts +123 -0
- package/frontend-vue/tests/template-drawer.test.ts +1 -1
- package/frontend-vue/tests/template-gallery.test.ts +90 -3
- package/frontend-vue/tests/template-preview-frame.test.ts +92 -0
- package/package.json +8 -7
- package/skills/dms-mailing/SKILL.md +25 -18
- package/src/constants.ts +2 -2
- package/src/data/sends-table.ts +1 -1
- package/src/data/templates-table.ts +125 -33
- 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/templates.model.ts +64 -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 +12 -17
- package/src/engine/formats.ts +124 -0
- package/src/engine/index.ts +1 -0
- package/src/engine/resolve.ts +18 -7
- package/src/engine/tokens.ts +7 -5
- package/src/frontend-module.ts +36 -0
- package/src/hooks/index.ts +0 -1
- package/src/hooks/tenant-export.ts +8 -1
- package/src/implementations/dms-mailing/index.ts +5 -16
- package/src/index.ts +3 -29
- package/src/pages/overview.ts +20 -16
- package/src/pages/sends.ts +27 -11
- package/src/pages/settings/form.ts +16 -0
- package/src/pages/templates.ts +4 -8
- package/src/pages/toolbar.ts +77 -0
- package/src/realtime.ts +17 -8
- package/src/routes/metrics.ts +158 -45
- package/src/routes/period.ts +32 -0
- package/src/routes/sends.ts +13 -4
- package/src/routes/settings.ts +11 -8
- package/src/routes/templates.ts +81 -37
- package/src/services/categories.ts +14 -1
- package/src/services/declarations.ts +64 -317
- package/src/services/events.ts +1 -1
- package/src/services/localized-text.ts +53 -0
- package/src/services/send-import.ts +2 -2
- package/src/services/send-stat-items.ts +60 -13
- package/src/services/send-stats.ts +37 -5
- package/src/services/send.ts +19 -20
- package/src/services/sender-declaration.ts +27 -0
- package/src/services/settings.ts +3 -10
- package/src/services/starters.ts +3 -15
- package/src/services/templates.ts +109 -63
- package/src/services/webhook-status.ts +3 -3
- package/src/types/settings.ts +55 -5
- package/src/validation/declarations.schema.ts +29 -3
- package/src/validation/settings.schema.ts +56 -14
- package/src/validation/templates.schema.ts +10 -0
- 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/app/components/PageHeader.vue +0 -94
- package/frontend-vue/app/composables/useOverviewSetup.ts +0 -21
- 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,26 +1,18 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type {
|
|
3
|
-
DeclarationOutcome,
|
|
4
3
|
DeclareTemplateCategoryResult,
|
|
5
4
|
DeclareTemplateResult,
|
|
6
5
|
TemplateCategoryDeclaration,
|
|
7
6
|
TemplateDeclaration,
|
|
8
7
|
} from "@antelopejs/interface-dms-mailing";
|
|
9
|
-
import {
|
|
8
|
+
import { IsDuplicateKeyError } from "@antelopejs/interface-database";
|
|
10
9
|
import { GetModel } from "@antelopejs/interface-database-decorators";
|
|
11
10
|
import {
|
|
12
11
|
type MailingTemplate,
|
|
13
|
-
SettingsModel,
|
|
14
12
|
TemplateModel,
|
|
15
|
-
TemplateSlugModel,
|
|
16
13
|
TemplateVersionModel,
|
|
17
|
-
templateSlugIdOf,
|
|
18
14
|
} from "../db";
|
|
19
|
-
import type {
|
|
20
|
-
TemplateCategory,
|
|
21
|
-
TemplateContent,
|
|
22
|
-
VariableDefinition,
|
|
23
|
-
} from "../types";
|
|
15
|
+
import type { TemplateCategory, TemplateContent } from "../types";
|
|
24
16
|
import {
|
|
25
17
|
categoryDeclarationSchema,
|
|
26
18
|
type ParsedCategoryDeclaration,
|
|
@@ -29,54 +21,18 @@ import {
|
|
|
29
21
|
} from "../validation/declarations.schema";
|
|
30
22
|
import { categoriesSchema } from "../validation/settings.schema";
|
|
31
23
|
import { DeclarationError, parseDeclared } from "./declaration-error";
|
|
32
|
-
import { fingerprintOf } from "./fingerprint";
|
|
33
24
|
import { findCategory, getSettings, saveSettings } from "./settings";
|
|
25
|
+
import { refreshTemplateAttention } from "./template-attention";
|
|
34
26
|
import {
|
|
35
|
-
assertSlugClaimed,
|
|
36
|
-
blankFields,
|
|
37
|
-
hasDraft,
|
|
38
27
|
localesWithoutSubject,
|
|
39
|
-
parseContent,
|
|
40
|
-
parseTestData,
|
|
41
|
-
parseVariables,
|
|
42
28
|
publishedVersionOf,
|
|
43
|
-
|
|
29
|
+
publishedAtCreation,
|
|
44
30
|
requireTemplate,
|
|
45
|
-
workingContent,
|
|
46
|
-
workingVariables,
|
|
47
31
|
} from "./templates";
|
|
48
32
|
|
|
49
33
|
/** Who a declaration's writes are shown as by default. */
|
|
50
34
|
const DECLARATION_AUTHOR = "system";
|
|
51
35
|
|
|
52
|
-
const LIVE_STATUS = "live";
|
|
53
|
-
const LOG_PREFIX = "[dms-mailing]";
|
|
54
|
-
const MAX_DECLARATION_ATTEMPTS = 5;
|
|
55
|
-
|
|
56
|
-
/** What a declaration fingerprints: everything a user may change after it. */
|
|
57
|
-
interface DeclaredTemplateState {
|
|
58
|
-
name: string;
|
|
59
|
-
category: string;
|
|
60
|
-
content: TemplateContent;
|
|
61
|
-
variables: VariableDefinition[];
|
|
62
|
-
testData: Record<string, unknown>;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** A declaration ready to write: validated, defaulted and fingerprinted. */
|
|
66
|
-
interface TemplatePlan {
|
|
67
|
-
tenantId: string;
|
|
68
|
-
slug: string;
|
|
69
|
-
state: DeclaredTemplateState;
|
|
70
|
-
author: string;
|
|
71
|
-
hash: string;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* The values as the database gives them back: a `Date` in the test data is
|
|
76
|
-
* stored as its ISO string, and the fingerprint must compare alike.
|
|
77
|
-
*/
|
|
78
|
-
const asStored = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T;
|
|
79
|
-
|
|
80
36
|
function assertPublishable(content: TemplateContent, slug: string): void {
|
|
81
37
|
if (!Object.keys(content.locales).length)
|
|
82
38
|
throw new DeclarationError(
|
|
@@ -104,212 +60,62 @@ function assertKnownCategory(
|
|
|
104
60
|
);
|
|
105
61
|
}
|
|
106
62
|
|
|
107
|
-
function planTemplate(declaration: ParsedTemplateDeclaration): TemplatePlan {
|
|
108
|
-
const { tenantId } = declaration;
|
|
109
|
-
const { slug } = declaration;
|
|
110
|
-
const state = asStored<DeclaredTemplateState>({
|
|
111
|
-
name: declaration.name,
|
|
112
|
-
category: declaration.category,
|
|
113
|
-
content: declaration.content as TemplateContent,
|
|
114
|
-
variables: declaration.variables,
|
|
115
|
-
testData: declaration.testData,
|
|
116
|
-
});
|
|
117
|
-
assertPublishable(state.content, slug);
|
|
118
|
-
return {
|
|
119
|
-
tenantId,
|
|
120
|
-
slug,
|
|
121
|
-
state,
|
|
122
|
-
author: declaration.author ?? DECLARATION_AUTHOR,
|
|
123
|
-
hash: fingerprintOf(state),
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* The fingerprint of what the template holds now, `null` when a user took it
|
|
129
|
-
* out of the state a declaration leaves it in: live, with no draft waiting.
|
|
130
|
-
*/
|
|
131
|
-
function currentFingerprint(template: MailingTemplate): string | null {
|
|
132
|
-
if (template.status !== LIVE_STATUS || hasDraft(template)) return null;
|
|
133
|
-
return fingerprintOf({
|
|
134
|
-
name: template.name,
|
|
135
|
-
category: template.category ?? "",
|
|
136
|
-
content: parseContent(template),
|
|
137
|
-
variables: parseVariables(template),
|
|
138
|
-
testData: parseTestData(template),
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
|
|
142
63
|
function resultOf(
|
|
143
64
|
template: MailingTemplate,
|
|
144
|
-
outcome:
|
|
145
|
-
version = publishedVersionOf(template),
|
|
65
|
+
outcome: DeclareTemplateResult["outcome"],
|
|
146
66
|
): DeclareTemplateResult {
|
|
147
67
|
return {
|
|
148
68
|
templateId: template._id,
|
|
149
69
|
slug: template.slug,
|
|
150
70
|
outcome,
|
|
151
|
-
version,
|
|
71
|
+
version: publishedVersionOf(template),
|
|
152
72
|
};
|
|
153
73
|
}
|
|
154
74
|
|
|
155
75
|
/**
|
|
156
|
-
* Inserts the template
|
|
157
|
-
*
|
|
158
|
-
*
|
|
76
|
+
* Inserts the declared template already published, as version 1, in one
|
|
77
|
+
* write. `undefined` when a concurrent call created the slug first (the slug
|
|
78
|
+
* index is unique): that call's row is published too.
|
|
159
79
|
*/
|
|
160
80
|
async function createDeclared(
|
|
161
|
-
|
|
81
|
+
declaration: ParsedTemplateDeclaration,
|
|
162
82
|
): Promise<MailingTemplate | undefined> {
|
|
163
|
-
const { tenantId, slug
|
|
83
|
+
const { tenantId, slug } = declaration;
|
|
164
84
|
const model = GetModel(TemplateModel, tenantId);
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
const revision = randomUUID();
|
|
168
|
-
const [id] = await model.insert({
|
|
169
|
-
slug,
|
|
170
|
-
name: state.name,
|
|
171
|
-
category: state.category,
|
|
172
|
-
status: "draft",
|
|
173
|
-
...blankFields(state.content),
|
|
174
|
-
json_variables: JSON.stringify(state.variables),
|
|
175
|
-
json_test_data: JSON.stringify(state.testData),
|
|
176
|
-
updatedBy: author,
|
|
177
|
-
revision,
|
|
178
|
-
declarationSeedRevision: revision,
|
|
179
|
-
pendingDeclaredHash: plan.hash,
|
|
180
|
-
});
|
|
181
|
-
const templateId = id as string;
|
|
85
|
+
const author = declaration.author ?? DECLARATION_AUTHOR;
|
|
86
|
+
let template: MailingTemplate;
|
|
182
87
|
try {
|
|
183
|
-
await
|
|
88
|
+
const [id] = await model.insertFields({
|
|
89
|
+
slug,
|
|
90
|
+
name: declaration.name,
|
|
91
|
+
category: declaration.category,
|
|
92
|
+
json_test_data: JSON.stringify(declaration.testData),
|
|
93
|
+
...publishedAtCreation(
|
|
94
|
+
randomUUID(),
|
|
95
|
+
{
|
|
96
|
+
content: declaration.content as TemplateContent,
|
|
97
|
+
variables: declaration.variables,
|
|
98
|
+
},
|
|
99
|
+
author,
|
|
100
|
+
),
|
|
101
|
+
});
|
|
102
|
+
template = await requireTemplate(model, id as string);
|
|
184
103
|
} catch (error) {
|
|
185
|
-
|
|
104
|
+
// A concurrent declaration of the slug created it first: expected, the
|
|
105
|
+
// caller goes on with that template.
|
|
106
|
+
if (IsDuplicateKeyError(error)) return undefined;
|
|
186
107
|
throw error;
|
|
187
108
|
}
|
|
188
|
-
|
|
109
|
+
await GetModel(TemplateVersionModel, tenantId).materialize(template);
|
|
110
|
+
await refreshTemplateAttention(tenantId);
|
|
111
|
+
return template;
|
|
189
112
|
}
|
|
190
113
|
|
|
191
114
|
/**
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
tenantId: string,
|
|
197
|
-
slug: string,
|
|
198
|
-
): Promise<MailingTemplate | undefined> {
|
|
199
|
-
const model = GetModel(TemplateModel, tenantId);
|
|
200
|
-
const claim = await GetModel(TemplateSlugModel, tenantId).get(
|
|
201
|
-
templateSlugIdOf(tenantId, slug),
|
|
202
|
-
);
|
|
203
|
-
const winner = claim && (await model.get(claim.templateId));
|
|
204
|
-
return winner || model.getBySlug(slug);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function isUnmodifiedSeed(template: MailingTemplate): boolean {
|
|
208
|
-
return Boolean(
|
|
209
|
-
template.pendingDeclaredHash &&
|
|
210
|
-
template.declarationSeedRevision &&
|
|
211
|
-
template.revision === template.declarationSeedRevision &&
|
|
212
|
-
template.status === "draft" &&
|
|
213
|
-
template.publishedVersion === 0,
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/** Finishes the stored seed, never substituting a later caller's declaration. */
|
|
218
|
-
async function finishSeed(
|
|
219
|
-
tenantId: string,
|
|
220
|
-
template: MailingTemplate,
|
|
221
|
-
): Promise<boolean> {
|
|
222
|
-
try {
|
|
223
|
-
await assertSlugClaimed(tenantId, template.slug, template._id);
|
|
224
|
-
} catch (error) {
|
|
225
|
-
const winner = await declaredTemplate(tenantId, template.slug);
|
|
226
|
-
if (winner && winner._id !== template._id) return false;
|
|
227
|
-
throw error;
|
|
228
|
-
}
|
|
229
|
-
const settings = await getSettings(tenantId);
|
|
230
|
-
assertKnownCategory(settings.categories, template.category, template.slug);
|
|
231
|
-
return Boolean(
|
|
232
|
-
await publishSnapshot(
|
|
233
|
-
tenantId,
|
|
234
|
-
template,
|
|
235
|
-
{
|
|
236
|
-
content: workingContent(template),
|
|
237
|
-
variables: workingVariables(template),
|
|
238
|
-
},
|
|
239
|
-
template.updatedBy,
|
|
240
|
-
{
|
|
241
|
-
declaredHash: template.pendingDeclaredHash,
|
|
242
|
-
pendingDeclaredHash: "",
|
|
243
|
-
declarationSeedRevision: "",
|
|
244
|
-
},
|
|
245
|
-
),
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/** Why an existing template is left alone, `null` when it may be refreshed. */
|
|
250
|
-
function keepReason(template: MailingTemplate): string | null {
|
|
251
|
-
if (!template.declaredHash) return "it was created in the dashboard";
|
|
252
|
-
if (currentFingerprint(template) !== template.declaredHash)
|
|
253
|
-
return "a user changed it since it was declared";
|
|
254
|
-
return null;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
async function applyToExisting(
|
|
258
|
-
plan: TemplatePlan,
|
|
259
|
-
template: MailingTemplate,
|
|
260
|
-
): Promise<DeclareTemplateResult> {
|
|
261
|
-
const { tenantId, state, author } = plan;
|
|
262
|
-
for (let attempt = 0; attempt < MAX_DECLARATION_ATTEMPTS; attempt++) {
|
|
263
|
-
if (isUnmodifiedSeed(template)) {
|
|
264
|
-
const created = await finishSeed(tenantId, template);
|
|
265
|
-
if (created && template.pendingDeclaredHash === plan.hash)
|
|
266
|
-
return resultOf(template, "created", 1);
|
|
267
|
-
} else {
|
|
268
|
-
await GetModel(TemplateVersionModel, tenantId).materialize(template);
|
|
269
|
-
if (template.publishedVersion > 0 && template.declaredHash === plan.hash)
|
|
270
|
-
return resultOf(template, "unchanged");
|
|
271
|
-
const reason = keepReason(template);
|
|
272
|
-
if (reason) {
|
|
273
|
-
Logging.Info(
|
|
274
|
-
`${LOG_PREFIX} Kept template "${plan.slug}" as it is: ${reason}`,
|
|
275
|
-
);
|
|
276
|
-
return resultOf(template, "kept");
|
|
277
|
-
}
|
|
278
|
-
const settings = await getSettings(tenantId);
|
|
279
|
-
assertKnownCategory(settings.categories, state.category, plan.slug);
|
|
280
|
-
const outcome = await publishSnapshot(tenantId, template, state, author, {
|
|
281
|
-
name: state.name,
|
|
282
|
-
category: state.category,
|
|
283
|
-
json_test_data: JSON.stringify(state.testData),
|
|
284
|
-
declaredHash: plan.hash,
|
|
285
|
-
});
|
|
286
|
-
if (outcome) return resultOf(template, "updated", outcome.version);
|
|
287
|
-
}
|
|
288
|
-
const current = await declaredTemplate(tenantId, plan.slug);
|
|
289
|
-
if (!current) break;
|
|
290
|
-
template = current;
|
|
291
|
-
}
|
|
292
|
-
throw new DeclarationError(
|
|
293
|
-
"declaration_conflict",
|
|
294
|
-
`Template "${plan.slug}" changed repeatedly; retry the declaration`,
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Creates or refreshes the template a project declares from code. The rule,
|
|
300
|
-
* keyed on the slug:
|
|
301
|
-
*
|
|
302
|
-
* - no template has the slug: it is created and published (`created`);
|
|
303
|
-
* - the declaration is the one last applied: nothing is written
|
|
304
|
-
* (`unchanged`), whatever a user did to the template since;
|
|
305
|
-
* - the declaration changed and the template is still live, with no draft,
|
|
306
|
-
* and holds exactly what the last declaration wrote (name, category,
|
|
307
|
-
* content, variables, test data): the new declaration is published
|
|
308
|
-
* (`updated`);
|
|
309
|
-
* - otherwise (edited, unpublished, archived, a draft pending, or made in the
|
|
310
|
-
* dashboard): it is left as it is (`kept`).
|
|
311
|
-
*
|
|
312
|
-
* A declared template a user deleted is created again by the next call.
|
|
115
|
+
* Writes a template a project ships as code, once: call it when the tenant
|
|
116
|
+
* is provisioned. A new slug is created and published (`created`). A slug the
|
|
117
|
+
* tenant already has is left as it is (`kept`): from its creation on, the
|
|
118
|
+
* template belongs to the tenant, and a later declaration never changes it.
|
|
313
119
|
*/
|
|
314
120
|
export async function declareTemplate(
|
|
315
121
|
input: TemplateDeclaration,
|
|
@@ -320,23 +126,18 @@ export async function declareTemplate(
|
|
|
320
126
|
"invalid_template",
|
|
321
127
|
`template declaration "${String(input?.slug)}"`,
|
|
322
128
|
);
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
if (
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
)
|
|
332
|
-
return
|
|
129
|
+
const { tenantId, slug } = declaration;
|
|
130
|
+
assertPublishable(declaration.content as TemplateContent, slug);
|
|
131
|
+
const model = GetModel(TemplateModel, tenantId);
|
|
132
|
+
const existing = await model.getBySlug(slug);
|
|
133
|
+
if (existing) return resultOf(existing, "kept");
|
|
134
|
+
const settings = await getSettings(tenantId);
|
|
135
|
+
assertKnownCategory(settings.categories, declaration.category, slug);
|
|
136
|
+
const created = await createDeclared(declaration);
|
|
137
|
+
if (created) return resultOf(created, "created");
|
|
138
|
+
return resultOf((await model.getBySlug(slug)) as MailingTemplate, "kept");
|
|
333
139
|
}
|
|
334
140
|
|
|
335
|
-
type DeclaredCategories = Record<string, string>;
|
|
336
|
-
|
|
337
|
-
const categoryFingerprint = (category: Omit<TemplateCategory, "id">) =>
|
|
338
|
-
fingerprintOf({ label: category.label, icon: category.icon });
|
|
339
|
-
|
|
340
141
|
/** Category declarations of one tenant run one after the other. */
|
|
341
142
|
const categoryQueues = new Map<string, Promise<unknown>>();
|
|
342
143
|
|
|
@@ -358,84 +159,30 @@ function inTenantQueue<T>(
|
|
|
358
159
|
return next;
|
|
359
160
|
}
|
|
360
161
|
|
|
361
|
-
async function readDeclaredCategories(
|
|
362
|
-
tenantId: string,
|
|
363
|
-
): Promise<DeclaredCategories> {
|
|
364
|
-
const row = await GetModel(SettingsModel, tenantId).get(tenantId);
|
|
365
|
-
return JSON.parse(
|
|
366
|
-
row?.json_declared_categories || "{}",
|
|
367
|
-
) as DeclaredCategories;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/** The category list once the declared category is added or relabelled. */
|
|
371
|
-
function withCategory(
|
|
372
|
-
categories: TemplateCategory[],
|
|
373
|
-
declared: TemplateCategory,
|
|
374
|
-
): TemplateCategory[] {
|
|
375
|
-
const isPresent = Boolean(findCategory(categories, declared.id));
|
|
376
|
-
const next = isPresent
|
|
377
|
-
? categories.map((category) =>
|
|
378
|
-
category.id === declared.id ? declared : category,
|
|
379
|
-
)
|
|
380
|
-
: [...categories, declared];
|
|
381
|
-
return parseDeclared(
|
|
382
|
-
categoriesSchema,
|
|
383
|
-
next,
|
|
384
|
-
"invalid_category",
|
|
385
|
-
`category declaration "${declared.id}"`,
|
|
386
|
-
).map(({ id, label, icon }) => ({ id, label, icon }));
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/** What a category declaration does to the stored list, by its state. */
|
|
390
|
-
function categoryOutcome(
|
|
391
|
-
current: TemplateCategory | undefined,
|
|
392
|
-
previousHash: string | undefined,
|
|
393
|
-
hash: string,
|
|
394
|
-
): DeclarationOutcome {
|
|
395
|
-
if (!current) return previousHash ? "kept" : "created";
|
|
396
|
-
if (!previousHash) return "kept";
|
|
397
|
-
if (previousHash === hash) return "unchanged";
|
|
398
|
-
return categoryFingerprint(current) === previousHash ? "updated" : "kept";
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
const WRITING_OUTCOMES = new Set<DeclarationOutcome>(["created", "updated"]);
|
|
402
|
-
|
|
403
162
|
async function applyCategory(
|
|
404
163
|
declaration: ParsedCategoryDeclaration,
|
|
405
164
|
): Promise<DeclareTemplateCategoryResult> {
|
|
406
|
-
const { tenantId } = declaration;
|
|
407
|
-
const category = {
|
|
408
|
-
id: declaration.id,
|
|
409
|
-
label: declaration.label,
|
|
410
|
-
icon: declaration.icon,
|
|
411
|
-
};
|
|
412
|
-
const hash = categoryFingerprint(category);
|
|
165
|
+
const { tenantId, id, label, icon } = declaration;
|
|
413
166
|
const settings = await getSettings(tenantId);
|
|
414
|
-
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
167
|
+
if (findCategory(settings.categories, id)) return { id, outcome: "kept" };
|
|
168
|
+
const categories = parseDeclared(
|
|
169
|
+
categoriesSchema,
|
|
170
|
+
[...settings.categories, { id, label, icon }],
|
|
171
|
+
"invalid_category",
|
|
172
|
+
`category declaration "${id}"`,
|
|
173
|
+
).map((category) => ({
|
|
174
|
+
id: category.id,
|
|
175
|
+
label: category.label,
|
|
176
|
+
icon: category.icon,
|
|
177
|
+
}));
|
|
422
178
|
await saveSettings(tenantId, { ...settings, categories });
|
|
423
|
-
|
|
424
|
-
json_declared_categories: JSON.stringify({
|
|
425
|
-
...declared,
|
|
426
|
-
[category.id]: hash,
|
|
427
|
-
}),
|
|
428
|
-
});
|
|
429
|
-
return { id: category.id, outcome };
|
|
179
|
+
return { id, outcome: "created" };
|
|
430
180
|
}
|
|
431
181
|
|
|
432
182
|
/**
|
|
433
|
-
* Adds
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
* as last declared (`updated`); a category a user relabelled or removed
|
|
437
|
-
* since, or one that existed before any declaration (the defaults among
|
|
438
|
-
* them), is left as it is (`kept`).
|
|
183
|
+
* Adds a template category a project ships as code, once: call it when the
|
|
184
|
+
* tenant is provisioned. A new id is added (`created`); an id the workspace
|
|
185
|
+
* settings already hold, the defaults among them, is left as it is (`kept`).
|
|
439
186
|
*/
|
|
440
187
|
export function declareTemplateCategory(
|
|
441
188
|
input: TemplateCategoryDeclaration,
|
package/src/services/events.ts
CHANGED
|
@@ -135,7 +135,7 @@ async function publishEmailEvent(
|
|
|
135
135
|
send: MailingSend,
|
|
136
136
|
): Promise<void> {
|
|
137
137
|
emitEmailEvent(payload);
|
|
138
|
-
publishSendUpdated(payload.sendId);
|
|
138
|
+
publishSendUpdated(payload.tenantId, payload.sendId);
|
|
139
139
|
await notifyProblems(payload.tenantId, {
|
|
140
140
|
sendId: send._id,
|
|
141
141
|
templateSlug: send.templateSlug,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { RequestContext } from "@antelopejs/interface-api";
|
|
2
|
+
import {
|
|
3
|
+
LOCALIZED_TEXT_FALLBACK_LOCALE,
|
|
4
|
+
type LocalizedText,
|
|
5
|
+
} from "@antelopejs/interface-dms-mailing";
|
|
6
|
+
|
|
7
|
+
/** Header the DMS frontend states the reader's language in on every request. */
|
|
8
|
+
export const CONTENT_LANGUAGE_HEADER = "x-content-language";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The language to read localized texts in: the header's, else the one every
|
|
12
|
+
* localized text falls back to.
|
|
13
|
+
*
|
|
14
|
+
* @param header Value of {@link CONTENT_LANGUAGE_HEADER}
|
|
15
|
+
*/
|
|
16
|
+
export function requestLocale(header: unknown): string {
|
|
17
|
+
return typeof header === "string" && header.length > 0
|
|
18
|
+
? header
|
|
19
|
+
: LOCALIZED_TEXT_FALLBACK_LOCALE;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The language a request reads and writes localized texts in. */
|
|
23
|
+
export function localeOfRequest(ctx: RequestContext): string {
|
|
24
|
+
return requestLocale(ctx.rawRequest.headers[CONTENT_LANGUAGE_HEADER]);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A localized text in `locale`, else in the fallback language, as
|
|
29
|
+
* `@Localized({ fallbackLocale })` reads a table field.
|
|
30
|
+
*/
|
|
31
|
+
export function textIn(
|
|
32
|
+
text: LocalizedText | undefined,
|
|
33
|
+
locale: string,
|
|
34
|
+
): string {
|
|
35
|
+
return text?.[locale] ?? text?.[LOCALIZED_TEXT_FALLBACK_LOCALE] ?? "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* `text` with `value` written in `locale`. The fallback language gets the
|
|
40
|
+
* same value while it has none, so a text first written in another language
|
|
41
|
+
* still reads in every language.
|
|
42
|
+
*/
|
|
43
|
+
export function withTextIn(
|
|
44
|
+
text: LocalizedText | undefined,
|
|
45
|
+
locale: string,
|
|
46
|
+
value: string,
|
|
47
|
+
): LocalizedText {
|
|
48
|
+
return {
|
|
49
|
+
[LOCALIZED_TEXT_FALLBACK_LOCALE]: value,
|
|
50
|
+
...text,
|
|
51
|
+
[locale]: value,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -170,7 +170,8 @@ interface EntryOutcome {
|
|
|
170
170
|
/**
|
|
171
171
|
* Writes one send: its events first, under ids derived from the send's, then
|
|
172
172
|
* the send itself. A run stopped half-way leaves events no send shows yet,
|
|
173
|
-
* which the next run completes;
|
|
173
|
+
* which the next run completes; each write skips the row an earlier run
|
|
174
|
+
* wrote, and a send present means it was fully imported.
|
|
174
175
|
*/
|
|
175
176
|
async function importOne(
|
|
176
177
|
tenantId: string,
|
|
@@ -180,7 +181,6 @@ async function importOne(
|
|
|
180
181
|
): Promise<EntryOutcome> {
|
|
181
182
|
const id = importedSendIdOf(tenantId, send.importId);
|
|
182
183
|
const sends = GetModel(SendModel, tenantId);
|
|
183
|
-
if (await sends.get(id)) return { id, isImported: false };
|
|
184
184
|
const timeline = timelineOf(send);
|
|
185
185
|
const events = GetModel(SendEventModel, tenantId);
|
|
186
186
|
for (const [index, entry] of timeline.entries()) {
|