@antelopejs/dms-mailing 0.9.2 → 0.10.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 +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 +3 -3
- 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/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 +2 -2
- package/dist/routes/templates.js.map +1 -1
- package/dist/services/settings.js +2 -1
- package/dist/services/settings.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/build/components/FormFieldRow.vue +1 -1
- package/frontend-vue/app/build/components/TemplateDrawerPerformance.vue +3 -3
- package/frontend-vue/app/build/components/TemplateDrawerVariables.vue +4 -4
- package/frontend-vue/app/components/CategoriesInput.vue +2 -2
- package/frontend-vue/app/components/CategoryInput.vue +1 -1
- package/frontend-vue/app/components/FunnelCard.vue +4 -4
- package/frontend-vue/app/components/LocaleSelect.vue +1 -1
- package/frontend-vue/app/components/NewTemplateModal.vue +1 -1
- package/frontend-vue/app/components/OverviewGate.vue +6 -6
- package/frontend-vue/app/components/RealSendModal.vue +6 -6
- package/frontend-vue/app/components/RenderingModal.vue +2 -2
- package/frontend-vue/app/components/RetentionInput.vue +1 -1
- package/frontend-vue/app/components/SendDrawer.vue +9 -9
- package/frontend-vue/app/components/StarterGallery.vue +2 -2
- package/frontend-vue/app/components/SystemVariables.vue +1 -1
- package/frontend-vue/app/components/TemplateCard.vue +2 -2
- package/frontend-vue/app/components/TemplateDrawer.vue +4 -4
- package/frontend-vue/app/components/TemplateGallery.vue +2 -2
- package/frontend-vue/app/components/TestSendModal.vue +4 -4
- package/frontend-vue/app/components/VersionPreview.vue +3 -3
- package/frontend-vue/app/components/editor/EditorBlockFields.vue +1 -1
- package/frontend-vue/app/components/editor/EditorCompare.vue +1 -1
- package/frontend-vue/app/components/editor/EditorEnvelope.vue +2 -2
- package/frontend-vue/app/components/editor/EditorLifecycle.vue +1 -1
- package/frontend-vue/app/components/editor/EditorPaletteGroup.vue +1 -1
- package/frontend-vue/app/components/editor/EditorPublishReview.vue +4 -4
- package/frontend-vue/app/components/editor/EditorTemplatePane.vue +2 -2
- package/frontend-vue/app/components/editor/EditorTestDataPane.vue +1 -1
- package/frontend-vue/app/components/editor/EditorVariablesPane.vue +1 -1
- package/frontend-vue/app/components/editor/EditorViewSwitches.vue +2 -2
- package/frontend-vue/app/components/editor/EditorVisibility.vue +1 -1
- 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 +2 -2
- package/frontend-vue/pnpm-lock.yaml +24 -7
- package/frontend-vue/tests/backend-boundary.test.ts +4 -4
- package/frontend-vue/tests/categories-input.test.ts +1 -1
- package/frontend-vue/tests/column-displays.test.ts +13 -12
- package/frontend-vue/tests/dms-component-names.test.ts +12 -0
- package/frontend-vue/tests/footer-block.test.ts +1 -1
- package/frontend-vue/tests/funnel-card.test.ts +3 -3
- package/frontend-vue/tests/helpers/component-tags.ts +58 -0
- package/frontend-vue/tests/helpers/nuxt-globals.ts +1 -1
- package/frontend-vue/tests/overview-gate.test.ts +1 -1
- package/frontend-vue/tests/retention-input.test.ts +2 -2
- package/frontend-vue/tests/send-drawer.test.ts +2 -2
- package/frontend-vue/tests/send-modals.test.ts +1 -1
- package/frontend-vue/tests/template-drawer.test.ts +6 -6
- package/frontend-vue/tests/template-gallery-display.test.ts +6 -5
- package/frontend-vue/tests/template-gallery.test.ts +3 -3
- package/frontend-vue/tests/template-overlays.test.ts +4 -12
- package/package.json +16 -16
- 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/sends.ts +2 -3
- package/src/routes/templates.ts +3 -2
- package/src/services/settings.ts +9 -3
- 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
|
@@ -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/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";
|
|
@@ -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);
|
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
|
|
|
@@ -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
|
/**
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { MailingTemplateVersion } from "../db/tables";
|
|
3
|
+
import type { TemplateChange, TemplateChangeKind } from "../engine/changes";
|
|
4
|
+
import type {
|
|
5
|
+
Block,
|
|
6
|
+
BlockType,
|
|
7
|
+
Condition,
|
|
8
|
+
ConditionOperator,
|
|
9
|
+
TemplateCategory,
|
|
10
|
+
TemplateContent,
|
|
11
|
+
TextAlign,
|
|
12
|
+
VariableDefinition,
|
|
13
|
+
VariableType,
|
|
14
|
+
} from "../types";
|
|
15
|
+
|
|
16
|
+
/** What a stored JSON column holds, checked when it is read back. */
|
|
17
|
+
type StoredSchema<T> = z.ZodType<T, z.ZodTypeDef, unknown>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* `text`, a stored JSON column, parsed and read with `schema`: a
|
|
21
|
+
* `SyntaxError` when it is not JSON, a `TypeError` naming `what` when it does
|
|
22
|
+
* not hold one, as `parseJsonObject` refuses.
|
|
23
|
+
*/
|
|
24
|
+
export function parseStored<T>(
|
|
25
|
+
text: string,
|
|
26
|
+
schema: StoredSchema<T>,
|
|
27
|
+
what: string,
|
|
28
|
+
): T {
|
|
29
|
+
const value: unknown = JSON.parse(text);
|
|
30
|
+
const read = schema.safeParse(value);
|
|
31
|
+
if (!read.success) throw new TypeError(`expected ${what}`);
|
|
32
|
+
return read.data;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A string that is one of the keys of `members`, which lists every member. */
|
|
36
|
+
function oneOf<T extends string>(members: Record<T, true>): StoredSchema<T> {
|
|
37
|
+
return z
|
|
38
|
+
.string()
|
|
39
|
+
.refine((value): value is T => Object.hasOwn(members, value));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const conditionOperators = {
|
|
43
|
+
truthy: true,
|
|
44
|
+
falsy: true,
|
|
45
|
+
eq: true,
|
|
46
|
+
ne: true,
|
|
47
|
+
gt: true,
|
|
48
|
+
lt: true,
|
|
49
|
+
} satisfies Record<ConditionOperator, true>;
|
|
50
|
+
|
|
51
|
+
const textAligns = {
|
|
52
|
+
left: true,
|
|
53
|
+
center: true,
|
|
54
|
+
right: true,
|
|
55
|
+
} satisfies Record<TextAlign, true>;
|
|
56
|
+
|
|
57
|
+
const variableTypes = {
|
|
58
|
+
string: true,
|
|
59
|
+
text: true,
|
|
60
|
+
number: true,
|
|
61
|
+
money: true,
|
|
62
|
+
date: true,
|
|
63
|
+
url: true,
|
|
64
|
+
boolean: true,
|
|
65
|
+
array: true,
|
|
66
|
+
} satisfies Record<VariableType, true>;
|
|
67
|
+
|
|
68
|
+
const blockTypes = {
|
|
69
|
+
hero: true,
|
|
70
|
+
heading: true,
|
|
71
|
+
paragraph: true,
|
|
72
|
+
code: true,
|
|
73
|
+
list: true,
|
|
74
|
+
total: true,
|
|
75
|
+
button: true,
|
|
76
|
+
if: true,
|
|
77
|
+
divider: true,
|
|
78
|
+
footer: true,
|
|
79
|
+
} satisfies Record<BlockType, true>;
|
|
80
|
+
|
|
81
|
+
const changeKinds = {
|
|
82
|
+
locale_added: true,
|
|
83
|
+
locale_removed: true,
|
|
84
|
+
subject: true,
|
|
85
|
+
preheader: true,
|
|
86
|
+
block_added: true,
|
|
87
|
+
block_removed: true,
|
|
88
|
+
block_changed: true,
|
|
89
|
+
blocks_reordered: true,
|
|
90
|
+
variable_added: true,
|
|
91
|
+
variable_removed: true,
|
|
92
|
+
variable_changed: true,
|
|
93
|
+
} satisfies Record<TemplateChangeKind, true>;
|
|
94
|
+
|
|
95
|
+
const storedConditionSchema: StoredSchema<Condition> = z
|
|
96
|
+
.object({
|
|
97
|
+
path: z.string(),
|
|
98
|
+
operator: oneOf(conditionOperators),
|
|
99
|
+
value: z.string(),
|
|
100
|
+
})
|
|
101
|
+
.passthrough();
|
|
102
|
+
|
|
103
|
+
const align = oneOf(textAligns);
|
|
104
|
+
const base = { id: z.string(), visibleIf: storedConditionSchema.nullable() };
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* A block as the module stores it: the shape of {@link Block} and nothing
|
|
108
|
+
* more, without the bounds an edit is held to, so every block the module
|
|
109
|
+
* wrote reads back unchanged.
|
|
110
|
+
*/
|
|
111
|
+
const storedBlockSchema: StoredSchema<Block> = z.lazy(() => storedBlockUnion);
|
|
112
|
+
|
|
113
|
+
/** Built once: the lazy schema above returns it for every nested block. */
|
|
114
|
+
const storedBlockUnion = z.discriminatedUnion("type", [
|
|
115
|
+
z
|
|
116
|
+
.object({
|
|
117
|
+
...base,
|
|
118
|
+
type: z.literal("hero"),
|
|
119
|
+
imageUrl: z.string(),
|
|
120
|
+
alt: z.string(),
|
|
121
|
+
})
|
|
122
|
+
.passthrough(),
|
|
123
|
+
z
|
|
124
|
+
.object({
|
|
125
|
+
...base,
|
|
126
|
+
type: z.literal("heading"),
|
|
127
|
+
text: z.string(),
|
|
128
|
+
align,
|
|
129
|
+
size: z.number(),
|
|
130
|
+
})
|
|
131
|
+
.passthrough(),
|
|
132
|
+
z
|
|
133
|
+
.object({
|
|
134
|
+
...base,
|
|
135
|
+
type: z.literal("paragraph"),
|
|
136
|
+
text: z.string(),
|
|
137
|
+
align,
|
|
138
|
+
})
|
|
139
|
+
.passthrough(),
|
|
140
|
+
z
|
|
141
|
+
.object({ ...base, type: z.literal("code"), text: z.string() })
|
|
142
|
+
.passthrough(),
|
|
143
|
+
z
|
|
144
|
+
.object({
|
|
145
|
+
...base,
|
|
146
|
+
type: z.literal("list"),
|
|
147
|
+
source: z.string(),
|
|
148
|
+
labelPath: z.string(),
|
|
149
|
+
valuePath: z.string(),
|
|
150
|
+
})
|
|
151
|
+
.passthrough(),
|
|
152
|
+
z
|
|
153
|
+
.object({
|
|
154
|
+
...base,
|
|
155
|
+
type: z.literal("total"),
|
|
156
|
+
label: z.string(),
|
|
157
|
+
value: z.string(),
|
|
158
|
+
})
|
|
159
|
+
.passthrough(),
|
|
160
|
+
z
|
|
161
|
+
.object({
|
|
162
|
+
...base,
|
|
163
|
+
type: z.literal("button"),
|
|
164
|
+
text: z.string(),
|
|
165
|
+
href: z.string(),
|
|
166
|
+
align,
|
|
167
|
+
})
|
|
168
|
+
.passthrough(),
|
|
169
|
+
z
|
|
170
|
+
.object({
|
|
171
|
+
...base,
|
|
172
|
+
type: z.literal("if"),
|
|
173
|
+
condition: storedConditionSchema,
|
|
174
|
+
children: z.array(storedBlockSchema),
|
|
175
|
+
elseChildren: z.array(storedBlockSchema).nullable(),
|
|
176
|
+
})
|
|
177
|
+
.passthrough(),
|
|
178
|
+
z.object({ ...base, type: z.literal("divider") }).passthrough(),
|
|
179
|
+
z
|
|
180
|
+
.object({
|
|
181
|
+
...base,
|
|
182
|
+
type: z.literal("footer"),
|
|
183
|
+
text: z.string(),
|
|
184
|
+
unsubscribeLabel: z.string(),
|
|
185
|
+
preferencesLabel: z.string(),
|
|
186
|
+
})
|
|
187
|
+
.passthrough(),
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
/** Stored {@link TemplateContent}: `json_content`, `json_draft`, a version's. */
|
|
191
|
+
export const storedContentSchema: StoredSchema<TemplateContent> = z
|
|
192
|
+
.object({
|
|
193
|
+
locales: z.record(
|
|
194
|
+
z
|
|
195
|
+
.object({
|
|
196
|
+
subject: z.string(),
|
|
197
|
+
preheader: z.string(),
|
|
198
|
+
blocks: z.array(storedBlockSchema),
|
|
199
|
+
})
|
|
200
|
+
.passthrough(),
|
|
201
|
+
),
|
|
202
|
+
})
|
|
203
|
+
.passthrough();
|
|
204
|
+
|
|
205
|
+
/** Stored declarations: `json_variables`, `json_variables_draft`, a version's. */
|
|
206
|
+
export const storedVariablesSchema: StoredSchema<VariableDefinition[]> =
|
|
207
|
+
z.array(
|
|
208
|
+
z
|
|
209
|
+
.object({
|
|
210
|
+
path: z.string(),
|
|
211
|
+
type: oneOf(variableTypes),
|
|
212
|
+
required: z.boolean(),
|
|
213
|
+
})
|
|
214
|
+
.passthrough(),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
/** A version's `json_changes`, as {@link TemplateChange} lists them. */
|
|
218
|
+
export const storedChangesSchema: StoredSchema<TemplateChange[]> = z.array(
|
|
219
|
+
z
|
|
220
|
+
.object({
|
|
221
|
+
locale: z.string(),
|
|
222
|
+
kind: oneOf(changeKinds),
|
|
223
|
+
blockId: z.string().optional(),
|
|
224
|
+
blockType: oneOf(blockTypes).optional(),
|
|
225
|
+
variable: z.string().optional(),
|
|
226
|
+
})
|
|
227
|
+
.passthrough(),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* A template's `json_published_version`: the version row it inserts, whose
|
|
232
|
+
* `publishedAt` JSON wrote as a string.
|
|
233
|
+
*/
|
|
234
|
+
export const storedVersionRowSchema: StoredSchema<MailingTemplateVersion> = z
|
|
235
|
+
.object({
|
|
236
|
+
_id: z.string(),
|
|
237
|
+
templateId: z.string(),
|
|
238
|
+
version: z.number(),
|
|
239
|
+
json_content: z.string(),
|
|
240
|
+
json_variables: z.string(),
|
|
241
|
+
locales: z.string(),
|
|
242
|
+
publishedAt: z.string().transform((at) => new Date(at)),
|
|
243
|
+
publishedBy: z.string(),
|
|
244
|
+
json_changes: z.string(),
|
|
245
|
+
})
|
|
246
|
+
.passthrough();
|
|
247
|
+
|
|
248
|
+
/** The settings' `json_categories`, each label one text per language. */
|
|
249
|
+
export const storedCategoriesSchema: StoredSchema<TemplateCategory[]> = z.array(
|
|
250
|
+
z
|
|
251
|
+
.object({
|
|
252
|
+
id: z.string(),
|
|
253
|
+
label: z.record(z.string()),
|
|
254
|
+
icon: z.string(),
|
|
255
|
+
})
|
|
256
|
+
.passthrough(),
|
|
257
|
+
);
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|