@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
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
} from "./events";
|
|
28
28
|
import { getSettings } from "./settings";
|
|
29
29
|
import { refreshTemplateAttention } from "./template-attention";
|
|
30
|
+
import { isText } from "../validation/guards";
|
|
30
31
|
|
|
31
32
|
/** Labels an imported send in the log when its caller gives no source. */
|
|
32
33
|
const IMPORT_SOURCE = "import";
|
|
@@ -47,7 +48,7 @@ function importedSendIdOf(tenantId: string, importId: string): string {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
const addressOf = (to: ParsedImportedSend["to"]) =>
|
|
50
|
-
|
|
51
|
+
isText(to) ? { email: to } : to;
|
|
51
52
|
|
|
52
53
|
/** Chronological events, with the webhook's retry deduplication rules. */
|
|
53
54
|
function timelineOf(send: ParsedImportedSend): ImportedEvent[] {
|
|
@@ -59,11 +60,11 @@ function timelineOf(send: ParsedImportedSend): ImportedEvent[] {
|
|
|
59
60
|
const repeated = timeline.some((stored) =>
|
|
60
61
|
isSameEvent(
|
|
61
62
|
{
|
|
62
|
-
type: stored.type
|
|
63
|
+
type: stored.type,
|
|
63
64
|
at: stored.at,
|
|
64
65
|
json_details: JSON.stringify(stored.details ?? {}),
|
|
65
66
|
},
|
|
66
|
-
|
|
67
|
+
entry,
|
|
67
68
|
),
|
|
68
69
|
);
|
|
69
70
|
if (!repeated) timeline.push(entry);
|
|
@@ -87,7 +88,7 @@ function foldEvents(
|
|
|
87
88
|
const event: EmailEvent = {
|
|
88
89
|
provider: send.provider ?? IMPORT_SOURCE,
|
|
89
90
|
messageId: send.providerMessageId ?? "",
|
|
90
|
-
type: entry.type
|
|
91
|
+
type: entry.type,
|
|
91
92
|
at: entry.at,
|
|
92
93
|
details: entry.details,
|
|
93
94
|
};
|
|
@@ -141,6 +142,13 @@ function sendRow(
|
|
|
141
142
|
};
|
|
142
143
|
}
|
|
143
144
|
|
|
145
|
+
/** The refusal of an import that names a template the workspace lacks. */
|
|
146
|
+
const missingTemplate = (slug: string): DeclarationError =>
|
|
147
|
+
new DeclarationError(
|
|
148
|
+
"template_not_found",
|
|
149
|
+
`Imported sends name template "${slug}", which the workspace does not have`,
|
|
150
|
+
);
|
|
151
|
+
|
|
144
152
|
/** The templates the sends name, by slug; refused before anything is written. */
|
|
145
153
|
async function templatesOf(
|
|
146
154
|
tenantId: string,
|
|
@@ -151,11 +159,7 @@ async function templatesOf(
|
|
|
151
159
|
const templates = new Map<string, MailingTemplate>();
|
|
152
160
|
for (const slug of slugs) {
|
|
153
161
|
const template = await model.getBySlug(slug);
|
|
154
|
-
if (!template)
|
|
155
|
-
throw new DeclarationError(
|
|
156
|
-
"template_not_found",
|
|
157
|
-
`Imported sends name template "${slug}", which the workspace does not have`,
|
|
158
|
-
);
|
|
162
|
+
if (!template) throw missingTemplate(slug);
|
|
159
163
|
templates.set(slug, template);
|
|
160
164
|
}
|
|
161
165
|
return templates;
|
|
@@ -170,7 +174,8 @@ interface EntryOutcome {
|
|
|
170
174
|
/**
|
|
171
175
|
* Writes one send: its events first, under ids derived from the send's, then
|
|
172
176
|
* the send itself. A run stopped half-way leaves events no send shows yet,
|
|
173
|
-
* which the next run completes;
|
|
177
|
+
* which the next run completes; each write skips the row an earlier run
|
|
178
|
+
* wrote, and a send present means it was fully imported.
|
|
174
179
|
*/
|
|
175
180
|
async function importOne(
|
|
176
181
|
tenantId: string,
|
|
@@ -180,14 +185,13 @@ async function importOne(
|
|
|
180
185
|
): Promise<EntryOutcome> {
|
|
181
186
|
const id = importedSendIdOf(tenantId, send.importId);
|
|
182
187
|
const sends = GetModel(SendModel, tenantId);
|
|
183
|
-
if (await sends.get(id)) return { id, isImported: false };
|
|
184
188
|
const timeline = timelineOf(send);
|
|
185
189
|
const events = GetModel(SendEventModel, tenantId);
|
|
186
190
|
for (const [index, entry] of timeline.entries()) {
|
|
187
191
|
await events.insertImported({
|
|
188
192
|
_id: `${id}-${index}`,
|
|
189
193
|
sendId: id,
|
|
190
|
-
type: entry.type
|
|
194
|
+
type: entry.type,
|
|
191
195
|
at: entry.at,
|
|
192
196
|
json_details: JSON.stringify(entry.details ?? {}),
|
|
193
197
|
});
|
|
@@ -219,7 +223,8 @@ export async function importSends(
|
|
|
219
223
|
const { fallbackLocale } = await getSettings(tenantId);
|
|
220
224
|
const result: ImportSendsResult = { imported: 0, skipped: 0, sendIds: [] };
|
|
221
225
|
for (const send of sends) {
|
|
222
|
-
const template = templates.get(send.templateSlug)
|
|
226
|
+
const template = templates.get(send.templateSlug);
|
|
227
|
+
if (!template) throw missingTemplate(send.templateSlug);
|
|
223
228
|
const { id, isImported } = await importOne(
|
|
224
229
|
tenantId,
|
|
225
230
|
send,
|
package/src/services/send.ts
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
type VersionedSnapshot,
|
|
53
53
|
workingSnapshot,
|
|
54
54
|
} from "./templates";
|
|
55
|
+
import { isText } from "../validation/guards";
|
|
55
56
|
|
|
56
57
|
export class SendError extends Error {
|
|
57
58
|
constructor(
|
|
@@ -161,11 +162,10 @@ const OUTCOME_SEVERITY: Record<SendStatus, number> = {
|
|
|
161
162
|
const describeError = (error: unknown): string =>
|
|
162
163
|
error instanceof Error ? error.message : String(error);
|
|
163
164
|
|
|
164
|
-
const addressOf = (to: EmailAddress): string =>
|
|
165
|
-
typeof to === "string" ? to : to.email;
|
|
165
|
+
const addressOf = (to: EmailAddress): string => (isText(to) ? to : to.email);
|
|
166
166
|
|
|
167
167
|
const nameOf = (to: EmailAddress): string | undefined =>
|
|
168
|
-
|
|
168
|
+
isText(to) ? undefined : to.name;
|
|
169
169
|
|
|
170
170
|
const toRecipients = (to: EmailAddress | EmailAddress[]): EmailAddress[] =>
|
|
171
171
|
Array.isArray(to) ? to : [to];
|
|
@@ -468,15 +468,15 @@ async function record(
|
|
|
468
468
|
isTest: Boolean(params.isTest),
|
|
469
469
|
});
|
|
470
470
|
await GetModel(SendEventModel, tenantId).insertForActiveSend({
|
|
471
|
-
sendId
|
|
471
|
+
sendId,
|
|
472
472
|
type: status,
|
|
473
473
|
at: new Date(),
|
|
474
474
|
json_details: JSON.stringify(
|
|
475
475
|
outcome.error ? { reason: outcome.error } : {},
|
|
476
476
|
),
|
|
477
477
|
});
|
|
478
|
-
publishSendCreated(tenantId, sendId
|
|
479
|
-
return sendId
|
|
478
|
+
publishSendCreated(tenantId, sendId);
|
|
479
|
+
return sendId;
|
|
480
480
|
}
|
|
481
481
|
|
|
482
482
|
async function recordOne(
|
|
@@ -525,7 +525,7 @@ export async function recordAll(
|
|
|
525
525
|
export function aggregateResults(
|
|
526
526
|
results: SendTemplateResult[],
|
|
527
527
|
): SendTemplateResult {
|
|
528
|
-
const first = results[0]
|
|
528
|
+
const first = results[0];
|
|
529
529
|
const worst = results.reduce((left, right) =>
|
|
530
530
|
OUTCOME_SEVERITY[right.status] > OUTCOME_SEVERITY[left.status]
|
|
531
531
|
? right
|
package/src/services/settings.ts
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { GetModel } from "@antelopejs/interface-database-decorators";
|
|
3
|
-
import {
|
|
4
|
-
DEFAULT_FALLBACK_LOCALE,
|
|
5
|
-
DEFAULT_LOG_RETENTION_DAYS,
|
|
6
|
-
} from "../constants";
|
|
7
3
|
import { type MailingSettings, SettingsModel } from "../db";
|
|
8
4
|
import {
|
|
9
|
-
|
|
5
|
+
MODULE_DEFAULT_SETTINGS,
|
|
10
6
|
type MailingSettingsValues,
|
|
11
7
|
type TemplateCategory,
|
|
12
8
|
} from "../types";
|
|
@@ -15,14 +11,11 @@ export const WEBHOOK_SECRET_BYTES = 24;
|
|
|
15
11
|
|
|
16
12
|
function defaults(): MailingSettingsValues {
|
|
17
13
|
return {
|
|
18
|
-
|
|
19
|
-
logRetentionDays: DEFAULT_LOG_RETENTION_DAYS,
|
|
20
|
-
blockOnMissingVariables: false,
|
|
14
|
+
...MODULE_DEFAULT_SETTINGS,
|
|
21
15
|
senderName: "",
|
|
22
16
|
senderEmail: "",
|
|
23
17
|
replyTo: "",
|
|
24
18
|
webhookSecret: randomBytes(WEBHOOK_SECRET_BYTES).toString("hex"),
|
|
25
|
-
categories: DEFAULT_CATEGORIES,
|
|
26
19
|
};
|
|
27
20
|
}
|
|
28
21
|
|
|
@@ -106,7 +99,7 @@ const REFUSE_POLICY: MissingVariablesPolicy = "refuse";
|
|
|
106
99
|
const SEND_POLICY: MissingVariablesPolicy = "send";
|
|
107
100
|
|
|
108
101
|
export function missingVariablesPolicy(
|
|
109
|
-
values: MailingSettingsValues,
|
|
102
|
+
values: Pick<MailingSettingsValues, "blockOnMissingVariables">,
|
|
110
103
|
): MissingVariablesPolicy {
|
|
111
104
|
return values.blockOnMissingVariables ? REFUSE_POLICY : SEND_POLICY;
|
|
112
105
|
}
|
package/src/services/starters.ts
CHANGED
|
@@ -32,16 +32,16 @@ const DEFAULT_TEST_DATA_LOCALE = "en";
|
|
|
32
32
|
const STARTER_CURRENCY = "EUR";
|
|
33
33
|
|
|
34
34
|
/** The ordered item of the order starter's test data, by language. */
|
|
35
|
-
const ORDER_LINE_NAMES
|
|
36
|
-
|
|
37
|
-
fr
|
|
38
|
-
|
|
35
|
+
const ORDER_LINE_NAMES = new Map([
|
|
36
|
+
[DEFAULT_TEST_DATA_LOCALE, "Ergo chair · Graphite × 4"],
|
|
37
|
+
["fr", "Fauteuil ergonomique · Graphite × 4"],
|
|
38
|
+
]);
|
|
39
39
|
const LOCALE_PARTS = /[-_]/;
|
|
40
40
|
|
|
41
41
|
/** A test text in the language of `locale`, English when it has none. */
|
|
42
|
-
function inLanguageOf(texts:
|
|
42
|
+
function inLanguageOf(texts: ReadonlyMap<string, string>, locale: string) {
|
|
43
43
|
const language = locale.toLowerCase().split(LOCALE_PARTS)[0] ?? "";
|
|
44
|
-
return texts
|
|
44
|
+
return texts.get(language) ?? texts.get(DEFAULT_TEST_DATA_LOCALE);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/** An amount the way `locale` writes it: "€1,556.00", "1 556,00 €". */
|
|
@@ -66,18 +66,25 @@ export function sourcesOf(sends: SendSummary[]): SourceCount[] {
|
|
|
66
66
|
.slice(0, MAX_SOURCES);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
/** Whether `send` went out in another locale than the one it asked for. */
|
|
70
|
+
function isRequestedFallback(
|
|
71
|
+
send: SendSummary,
|
|
72
|
+
): send is SendSummary & { requestedLocale: string } {
|
|
70
73
|
return Boolean(send.requestedLocale && send.requestedLocale !== send.locale);
|
|
71
74
|
}
|
|
72
75
|
|
|
76
|
+
export function isFallback(send: SendSummary): boolean {
|
|
77
|
+
return isRequestedFallback(send);
|
|
78
|
+
}
|
|
79
|
+
|
|
73
80
|
export function fallbacksOf(sends: SendSummary[]): FallbackCount[] {
|
|
74
|
-
const fallbacks = sends.filter(
|
|
81
|
+
const fallbacks = sends.filter(isRequestedFallback);
|
|
75
82
|
return [
|
|
76
83
|
...groupBy(fallbacks, (send) => `${send.requestedLocale}>${send.locale}`),
|
|
77
84
|
].map(([, rows]) => {
|
|
78
|
-
const first = rows[0]
|
|
85
|
+
const first = rows[0];
|
|
79
86
|
return {
|
|
80
|
-
requested: first.requestedLocale
|
|
87
|
+
requested: first.requestedLocale,
|
|
81
88
|
used: first.locale,
|
|
82
89
|
count: rows.length,
|
|
83
90
|
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { HTTPResult } from "@antelopejs/interface-api";
|
|
3
|
+
import { IsDuplicateKeyError } from "@antelopejs/interface-database";
|
|
3
4
|
import { GetModel } from "@antelopejs/interface-database-decorators";
|
|
4
5
|
import {
|
|
5
|
-
DEFAULT_FALLBACK_LOCALE,
|
|
6
6
|
HTTP_CONFLICT,
|
|
7
7
|
HTTP_NOT_FOUND,
|
|
8
8
|
LOCALE_LIST_SEPARATOR,
|
|
@@ -12,9 +12,7 @@ import {
|
|
|
12
12
|
type MailingTemplateVersion,
|
|
13
13
|
type TemplateFields,
|
|
14
14
|
TemplateModel,
|
|
15
|
-
TemplateSlugModel,
|
|
16
15
|
TemplateVersionModel,
|
|
17
|
-
templateSlugIdOf,
|
|
18
16
|
} from "../db";
|
|
19
17
|
import { diffContents, diffVariables, type TemplateChange } from "../engine";
|
|
20
18
|
import {
|
|
@@ -28,6 +26,7 @@ import type {
|
|
|
28
26
|
TemplateContent,
|
|
29
27
|
VariableDefinition,
|
|
30
28
|
} from "../types";
|
|
29
|
+
import { DEFAULT_FALLBACK_LOCALE } from "../types";
|
|
31
30
|
|
|
32
31
|
export interface TemplateAuthor {
|
|
33
32
|
name?: string;
|
|
@@ -45,6 +44,7 @@ const STALE_DRAFT: TemplateAttentionReason = "stale_draft";
|
|
|
45
44
|
const VERSION_NOT_FOUND = "$dms_mailing.errors.version_not_found";
|
|
46
45
|
const TEMPLATE_NOT_FOUND = "$dms_mailing.errors.template_not_found";
|
|
47
46
|
const DUPLICATE_SLUG = "$dms_mailing.errors.duplicate_slug";
|
|
47
|
+
const SLUG_FIELD = "slug";
|
|
48
48
|
|
|
49
49
|
/** The template `id` names; 404 when there is none. */
|
|
50
50
|
export async function requireTemplate(
|
|
@@ -57,49 +57,18 @@ export async function requireTemplate(
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
60
|
+
* Runs a write that gives a template its slug, refused with a 409 when
|
|
61
|
+
* another template of the tenant holds it: the slug's unique index settles
|
|
62
|
+
* concurrent creations, so nothing reads the table before writing.
|
|
63
63
|
*/
|
|
64
|
-
async function
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (await claims.insertClaim(id, slug, templateId)) return true;
|
|
72
|
-
const holder = await claims.get(id);
|
|
73
|
-
if (holder?.templateId === templateId) return true;
|
|
74
|
-
if (holder) {
|
|
75
|
-
const templates = GetModel(TemplateModel, tenantId);
|
|
76
|
-
if (await templates.get(holder.templateId)) return false;
|
|
77
|
-
await claims.dropClaimOf(id, holder.templateId);
|
|
64
|
+
export async function refuseTakenSlug<T>(write: Promise<T>): Promise<T> {
|
|
65
|
+
try {
|
|
66
|
+
return await write;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (IsDuplicateKeyError(error) && error.fields.includes(SLUG_FIELD))
|
|
69
|
+
throw new HTTPResult(HTTP_CONFLICT, DUPLICATE_SLUG);
|
|
70
|
+
throw error;
|
|
78
71
|
}
|
|
79
|
-
return claims.insertClaim(id, slug, templateId);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Keeps the template just inserted if it got its slug, or removes it again
|
|
84
|
-
* and refuses it like any taken slug: two creations racing past the free-slug
|
|
85
|
-
* check both insert, and only the claim's primary key tells them apart.
|
|
86
|
-
*/
|
|
87
|
-
export async function assertSlugClaimed(
|
|
88
|
-
tenantId: string,
|
|
89
|
-
slug: string,
|
|
90
|
-
templateId: string,
|
|
91
|
-
): Promise<void> {
|
|
92
|
-
if (await claimTemplateSlug(tenantId, slug, templateId)) return;
|
|
93
|
-
await GetModel(TemplateModel, tenantId).delete(templateId);
|
|
94
|
-
throw new HTTPResult(HTTP_CONFLICT, DUPLICATE_SLUG);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Frees the slugs of deleted templates for new ones. */
|
|
98
|
-
export async function releaseTemplateSlugs(
|
|
99
|
-
tenantId: string,
|
|
100
|
-
templateIds: string[],
|
|
101
|
-
): Promise<void> {
|
|
102
|
-
await GetModel(TemplateSlugModel, tenantId).releaseTemplates(templateIds);
|
|
103
72
|
}
|
|
104
73
|
|
|
105
74
|
/** The content customers receive: the last published version. */
|
|
@@ -211,13 +180,18 @@ interface FooterLabels {
|
|
|
211
180
|
preferences: string;
|
|
212
181
|
}
|
|
213
182
|
|
|
183
|
+
const ENGLISH_FOOTER_LABELS: FooterLabels = {
|
|
184
|
+
unsubscribe: "Unsubscribe",
|
|
185
|
+
preferences: "Preferences",
|
|
186
|
+
};
|
|
187
|
+
|
|
214
188
|
/**
|
|
215
189
|
* The one piece of content the module still writes for a new template. An
|
|
216
190
|
* e-mail without an unsubscribe link is a legal problem, so a blank template
|
|
217
191
|
* ships a footer — and nothing else: no invented subject, prose or address.
|
|
218
192
|
*/
|
|
219
193
|
const FOOTER_LABELS: Record<string, FooterLabels> = {
|
|
220
|
-
en:
|
|
194
|
+
en: ENGLISH_FOOTER_LABELS,
|
|
221
195
|
fr: { unsubscribe: "Se désinscrire", preferences: "Préférences" },
|
|
222
196
|
};
|
|
223
197
|
|
|
@@ -225,7 +199,7 @@ const FOOTER_BLOCK_ID = "footer";
|
|
|
225
199
|
|
|
226
200
|
/** A footer in `locale`: its legal line and the labels of its two links. */
|
|
227
201
|
export function footerBlock(locale: string, text = ""): Block {
|
|
228
|
-
const labels = FOOTER_LABELS[locale] ??
|
|
202
|
+
const labels = FOOTER_LABELS[locale] ?? ENGLISH_FOOTER_LABELS;
|
|
229
203
|
return {
|
|
230
204
|
id: FOOTER_BLOCK_ID,
|
|
231
205
|
type: "footer",
|
|
@@ -257,16 +231,13 @@ export function isStaleRevision(
|
|
|
257
231
|
return revision !== undefined && revision !== template.draftRevision;
|
|
258
232
|
}
|
|
259
233
|
|
|
260
|
-
function draftWriteFields(
|
|
261
|
-
template: MailingTemplate,
|
|
262
|
-
author: string,
|
|
263
|
-
): TemplateFields {
|
|
234
|
+
function draftWriteFields(template: MailingTemplate, author: string) {
|
|
264
235
|
return {
|
|
265
236
|
draftRevision: template.draftRevision + 1,
|
|
266
237
|
draftUpdatedAt: new Date(),
|
|
267
238
|
draftUpdatedBy: author,
|
|
268
239
|
updatedBy: author,
|
|
269
|
-
};
|
|
240
|
+
} satisfies TemplateFields;
|
|
270
241
|
}
|
|
271
242
|
|
|
272
243
|
/** Whether `draft` is exactly what customers and callers already have. */
|
|
@@ -302,7 +273,7 @@ export async function saveDraft(
|
|
|
302
273
|
...locales,
|
|
303
274
|
});
|
|
304
275
|
await refreshAttentionIfFlagged(tenantId, template, STALE_DRAFT);
|
|
305
|
-
return fields.draftRevision
|
|
276
|
+
return fields.draftRevision;
|
|
306
277
|
}
|
|
307
278
|
|
|
308
279
|
export async function discardDraft(
|
|
@@ -528,6 +499,52 @@ export async function initializeTemplate(
|
|
|
528
499
|
await refreshTemplateAttention(tenantId);
|
|
529
500
|
}
|
|
530
501
|
|
|
502
|
+
/** The first version a template publishes. */
|
|
503
|
+
const FIRST_VERSION = 1;
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* A new template, published at once as version 1 of `draft`: its content
|
|
507
|
+
* and its history entry land in the one write that creates it, so no reader
|
|
508
|
+
* ever sees it unpublished. Materialize its version row afterwards, as a
|
|
509
|
+
* publication does.
|
|
510
|
+
*/
|
|
511
|
+
export function publishedAtCreation(
|
|
512
|
+
id: string,
|
|
513
|
+
draft: TemplateDraft,
|
|
514
|
+
author: string,
|
|
515
|
+
): TemplateFields {
|
|
516
|
+
const shipped: TemplateFields = {
|
|
517
|
+
json_content: JSON.stringify(draft.content),
|
|
518
|
+
json_variables: JSON.stringify(draft.variables),
|
|
519
|
+
locales: localesOf(draft.content),
|
|
520
|
+
publishedAt: new Date(),
|
|
521
|
+
publishedBy: author,
|
|
522
|
+
};
|
|
523
|
+
const changes = [
|
|
524
|
+
...diffContents({ locales: {} }, draft.content),
|
|
525
|
+
...diffVariables([], draft.variables),
|
|
526
|
+
];
|
|
527
|
+
return {
|
|
528
|
+
_id: id,
|
|
529
|
+
...shipped,
|
|
530
|
+
json_published_version: JSON.stringify({
|
|
531
|
+
...shipped,
|
|
532
|
+
_id: randomUUID(),
|
|
533
|
+
templateId: id,
|
|
534
|
+
version: FIRST_VERSION,
|
|
535
|
+
json_changes: JSON.stringify(changes),
|
|
536
|
+
}),
|
|
537
|
+
json_draft: "",
|
|
538
|
+
json_variables_draft: "",
|
|
539
|
+
isDraftPending: false,
|
|
540
|
+
draftRevision: FIRST_DRAFT_REVISION,
|
|
541
|
+
status: "live",
|
|
542
|
+
publishedVersion: FIRST_VERSION,
|
|
543
|
+
updatedBy: author,
|
|
544
|
+
...NO_ATTENTION,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
531
548
|
export function blankFields(content: TemplateContent): TemplateFields {
|
|
532
549
|
return {
|
|
533
550
|
json_content: "",
|
package/src/types/settings.ts
CHANGED
|
@@ -52,3 +52,23 @@ export const DEFAULT_CATEGORIES: TemplateCategory[] = [
|
|
|
52
52
|
icon: "i-ph-textbox",
|
|
53
53
|
},
|
|
54
54
|
];
|
|
55
|
+
|
|
56
|
+
export const DEFAULT_FALLBACK_LOCALE = "en";
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The module defaults: what a tenant's settings start with, and what the
|
|
60
|
+
* settings form offers to put back ("Use default"). The sender is empty and
|
|
61
|
+
* the webhook secret is drawn per tenant, so neither has one.
|
|
62
|
+
*/
|
|
63
|
+
export const MODULE_DEFAULT_SETTINGS: Pick<
|
|
64
|
+
MailingSettingsValues,
|
|
65
|
+
| "fallbackLocale"
|
|
66
|
+
| "logRetentionDays"
|
|
67
|
+
| "blockOnMissingVariables"
|
|
68
|
+
| "categories"
|
|
69
|
+
> = {
|
|
70
|
+
fallbackLocale: DEFAULT_FALLBACK_LOCALE,
|
|
71
|
+
logRetentionDays: 90,
|
|
72
|
+
blockOnMissingVariables: false,
|
|
73
|
+
categories: DEFAULT_CATEGORIES,
|
|
74
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { LOCALIZED_TEXT_FALLBACK_LOCALE } from "@antelopejs/interface-dms-mailing";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { MAX_NAME_LENGTH } from "../constants";
|
|
4
|
-
import {
|
|
4
|
+
import { sendEventTypeSchema } from "./send-event-type.schema";
|
|
5
5
|
import { categorySchema, settingsSchema } from "./settings.schema";
|
|
6
6
|
import {
|
|
7
7
|
slugSchema,
|
|
@@ -76,7 +76,7 @@ const recipientSchema = z.union([
|
|
|
76
76
|
]);
|
|
77
77
|
|
|
78
78
|
const importedEventSchema = z.object({
|
|
79
|
-
type:
|
|
79
|
+
type: sendEventTypeSchema,
|
|
80
80
|
at: z.coerce.date(),
|
|
81
81
|
details: z.record(z.unknown()).optional(),
|
|
82
82
|
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Whether `value` is a string. */
|
|
2
|
+
export function isText(value: unknown): value is string {
|
|
3
|
+
return typeof value === "string";
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Whether `value` is a JSON object as a parsed body holds one: an object,
|
|
8
|
+
* neither `null` nor an array. Its values are not checked.
|
|
9
|
+
*/
|
|
10
|
+
export function isJsonObject(value: unknown): value is Record<string, unknown> {
|
|
11
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Whether `value` is an object, arrays included, and not `null`: reading any
|
|
16
|
+
* key of it yields a value of unknown type.
|
|
17
|
+
*/
|
|
18
|
+
export function isObject(value: unknown): value is Record<string, unknown> {
|
|
19
|
+
return typeof value === "object" && value !== null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Whether `value` is a number. */
|
|
23
|
+
export function isNumber(value: unknown): value is number {
|
|
24
|
+
return typeof value === "number";
|
|
25
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { SEND_EVENT_TYPES } from "../types";
|
|
3
|
+
|
|
4
|
+
const [FIRST_EVENT_TYPE, ...OTHER_EVENT_TYPES] = SEND_EVENT_TYPES;
|
|
5
|
+
|
|
6
|
+
/** A send event's type: one of {@link SEND_EVENT_TYPES}. */
|
|
7
|
+
export const sendEventTypeSchema = z.enum([
|
|
8
|
+
FIRST_EVENT_TYPE,
|
|
9
|
+
...OTHER_EVENT_TYPES,
|
|
10
|
+
]);
|
|
@@ -123,12 +123,18 @@ function moveIssues(
|
|
|
123
123
|
kept: Set<string>,
|
|
124
124
|
): CategoryIssue[] {
|
|
125
125
|
return entries.flatMap((entry, index) =>
|
|
126
|
-
|
|
126
|
+
entry.moveTo === undefined || kept.has(entry.moveTo)
|
|
127
127
|
? []
|
|
128
128
|
: [{ message: UNKNOWN_MOVE_TARGET, path: [index, "moveTo"] }],
|
|
129
129
|
);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Adds the issues of the category rows: blank labels and duplicates in each
|
|
134
|
+
* language, a removed marketing category, a move to a category that is not
|
|
135
|
+
* kept. Each issue is reported once per row and part, whichever language
|
|
136
|
+
* shows it first.
|
|
137
|
+
*/
|
|
132
138
|
function refineCategoryEntries(
|
|
133
139
|
entries: CategoryEntry[],
|
|
134
140
|
context: z.RefinementCtx,
|
|
@@ -147,7 +153,6 @@ function refineCategoryEntries(
|
|
|
147
153
|
...moveIssues(entriesIn(entries, LOCALIZED_TEXT_FALLBACK_LOCALE), kept),
|
|
148
154
|
]
|
|
149
155
|
.filter((issue) => {
|
|
150
|
-
// One issue per row and part, whichever language shows it first.
|
|
151
156
|
const id = JSON.stringify(issue);
|
|
152
157
|
if (seen.has(id)) return false;
|
|
153
158
|
seen.add(id);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { MAX_NAME_LENGTH, MAX_SLUG_LENGTH, SLUG_PATTERN } from "../constants";
|
|
3
|
+
import type { Block } from "../types";
|
|
3
4
|
|
|
4
5
|
const MIN_HEADING_SIZE = 12;
|
|
5
6
|
const MAX_HEADING_SIZE = 48;
|
|
@@ -31,7 +32,7 @@ export const conditionSchema = z.object({
|
|
|
31
32
|
const alignSchema = z.enum(["left", "center", "right"]);
|
|
32
33
|
const base = { id: z.string().min(1), visibleIf: conditionSchema.nullable() };
|
|
33
34
|
|
|
34
|
-
export const blockSchema: z.ZodType<unknown> = z.lazy(() =>
|
|
35
|
+
export const blockSchema: z.ZodType<Block, z.ZodTypeDef, unknown> = z.lazy(() =>
|
|
35
36
|
z.discriminatedUnion("type", [
|
|
36
37
|
z.object({
|
|
37
38
|
...base,
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { MailingTemplateSlug } from "../tables";
|
|
2
|
-
export declare function templateSlugIdOf(tenantId: string, slug: string): string;
|
|
3
|
-
declare const TemplateSlugModel_base: {
|
|
4
|
-
new (database: import("@antelopejs/interface-database").SchemaInstance<any>): {
|
|
5
|
-
readonly table: import("@antelopejs/interface-database").Table<MailingTemplateSlug>;
|
|
6
|
-
readonly database: import("@antelopejs/interface-database").SchemaInstance<any>;
|
|
7
|
-
get(id: string): PromiseLike<MailingTemplateSlug | undefined>;
|
|
8
|
-
getBy(index: keyof MailingTemplateSlug, ...keys: any[]): PromiseLike<MailingTemplateSlug[]>;
|
|
9
|
-
getAll(): PromiseLike<MailingTemplateSlug[]>;
|
|
10
|
-
insert(obj: import("@antelopejs/interface-database-decorators").DeepPartial<MailingTemplateSlug> | import("@antelopejs/interface-database-decorators").DeepPartial<MailingTemplateSlug>[], options?: import("@antelopejs/interface-database-decorators").ValidateOptions): Promise<string[]>;
|
|
11
|
-
update(id: string, obj: import("@antelopejs/interface-database-decorators").DeepPartial<MailingTemplateSlug>, options?: import("@antelopejs/interface-database-decorators").ValidateOptions): Promise<number>;
|
|
12
|
-
update(obj: import("@antelopejs/interface-database-decorators").DeepPartial<MailingTemplateSlug>, options?: import("@antelopejs/interface-database-decorators").ValidateOptions): Promise<number>;
|
|
13
|
-
delete(id: string): Promise<number>;
|
|
14
|
-
};
|
|
15
|
-
readonly schemaName: string;
|
|
16
|
-
readonly tableClass: import("@antelopejs/interface-database-decorators").Constructible<MailingTemplateSlug>;
|
|
17
|
-
fromPlainData(obj: any): MailingTemplateSlug;
|
|
18
|
-
fromDatabase(obj: any): MailingTemplateSlug | undefined;
|
|
19
|
-
toDatabase(obj: any): Record<string, any>;
|
|
20
|
-
validate(obj: unknown, options?: import("@antelopejs/interface-database-decorators").ValidateInputOptions): import("@antelopejs/interface-database-decorators").ValidationResult;
|
|
21
|
-
};
|
|
22
|
-
export declare class TemplateSlugModel extends TemplateSlugModel_base {
|
|
23
|
-
insertClaim(id: string, slug: string, templateId: string): Promise<boolean>;
|
|
24
|
-
dropClaimOf(id: string, templateId: string): Promise<void>;
|
|
25
|
-
releaseTemplates(templateIds: string[]): Promise<void>;
|
|
26
|
-
}
|
|
27
|
-
export {};
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.TemplateSlugModel = void 0;
|
|
4
|
-
exports.templateSlugIdOf = templateSlugIdOf;
|
|
5
|
-
const node_crypto_1 = require("node:crypto");
|
|
6
|
-
const interface_database_decorators_1 = require("@antelopejs/interface-database-decorators");
|
|
7
|
-
const tables_1 = require("../tables");
|
|
8
|
-
const ID_LENGTH = 32;
|
|
9
|
-
function templateSlugIdOf(tenantId, slug) {
|
|
10
|
-
return (0, node_crypto_1.createHash)("sha256")
|
|
11
|
-
.update(`${tenantId}\n${slug}`)
|
|
12
|
-
.digest("hex")
|
|
13
|
-
.slice(0, ID_LENGTH);
|
|
14
|
-
}
|
|
15
|
-
class TemplateSlugModel extends (0, interface_database_decorators_1.BasicDataModel)(tables_1.MailingTemplateSlug, tables_1.MAILING_TEMPLATE_SLUGS_TABLE_NAME) {
|
|
16
|
-
async insertClaim(id, slug, templateId) {
|
|
17
|
-
try {
|
|
18
|
-
await this.insert({ _id: id, slug, templateId });
|
|
19
|
-
return true;
|
|
20
|
-
}
|
|
21
|
-
catch (error) {
|
|
22
|
-
if (await this.get(id))
|
|
23
|
-
return false;
|
|
24
|
-
throw error;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
async dropClaimOf(id, templateId) {
|
|
28
|
-
await this.table
|
|
29
|
-
.atomicMutation(id, {
|
|
30
|
-
type: "deleteIfEqual",
|
|
31
|
-
field: "templateId",
|
|
32
|
-
expectedValue: templateId,
|
|
33
|
-
})
|
|
34
|
-
.run();
|
|
35
|
-
}
|
|
36
|
-
async releaseTemplates(templateIds) {
|
|
37
|
-
if (!templateIds.length)
|
|
38
|
-
return;
|
|
39
|
-
await this.table.getAll(templateIds, "templateId").delete().run();
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
exports.TemplateSlugModel = TemplateSlugModel;
|
|
43
|
-
//# sourceMappingURL=template-slugs.model.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"template-slugs.model.js","sourceRoot":"","sources":["../../../src/db/models/template-slugs.model.ts"],"names":[],"mappings":";;;AAcA,4CAKC;AAnBD,6CAAyC;AACzC,6FAA2E;AAC3E,sCAGmB;AAEnB,MAAM,SAAS,GAAG,EAAE,CAAC;AAOrB,SAAgB,gBAAgB,CAAC,QAAgB,EAAE,IAAY;IAC7D,OAAO,IAAA,wBAAU,EAAC,QAAQ,CAAC;SACxB,MAAM,CAAC,GAAG,QAAQ,KAAK,IAAI,EAAE,CAAC;SAC9B,MAAM,CAAC,KAAK,CAAC;SACb,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;AACzB,CAAC;AAED,MAAa,iBAAkB,SAAQ,IAAA,8CAAc,EACnD,4BAAmB,EACnB,0CAAiC,CAClC;IAMC,KAAK,CAAC,WAAW,CACf,EAAU,EACV,IAAY,EACZ,UAAkB;QAElB,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;YACjD,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,MAAM,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;gBAAE,OAAO,KAAK,CAAC;YACrC,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAGD,KAAK,CAAC,WAAW,CAAC,EAAU,EAAE,UAAkB;QAC9C,MAAM,IAAI,CAAC,KAAK;aACb,cAAc,CAAC,EAAE,EAAE;YAClB,IAAI,EAAE,eAAe;YACrB,KAAK,EAAE,YAAY;YACnB,aAAa,EAAE,UAAU;SAC1B,CAAC;aACD,GAAG,EAAE,CAAC;IACX,CAAC;IAGD,KAAK,CAAC,gBAAgB,CAAC,WAAqB;QAC1C,IAAI,CAAC,WAAW,CAAC,MAAM;YAAE,OAAO;QAChC,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC;IACpE,CAAC;CACF;AAvCD,8CAuCC"}
|