@antelopejs/dms-mailing 0.8.0 → 0.9.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 +41 -43
- package/dist/automation/email-event-trigger.d.ts +2 -2
- package/dist/automation/send-template-action.d.ts +11 -3
- package/dist/automation/send-template-action.js +1 -1
- package/dist/automation/send-template-action.js.map +1 -1
- package/dist/data/body.d.ts +2 -1
- package/dist/data/body.js +2 -1
- package/dist/data/body.js.map +1 -1
- package/dist/data/read-only-routes.d.ts +4 -4
- package/dist/data/sends-table.d.ts +4 -4
- package/dist/data/suppressions-table.d.ts +4 -4
- package/dist/data/templates-table.d.ts +35 -12
- package/dist/data/templates-table.js +29 -24
- package/dist/data/templates-table.js.map +1 -1
- package/dist/data-types/categories-type.d.ts +1 -2
- package/dist/data-types/categories-type.js +3 -5
- package/dist/data-types/categories-type.js.map +1 -1
- package/dist/data-types/category-type.d.ts +1 -2
- package/dist/data-types/category-type.js +3 -5
- package/dist/data-types/category-type.js.map +1 -1
- package/dist/db/models/send-events.model.d.ts +7 -5
- package/dist/db/models/sends.model.d.ts +7 -5
- package/dist/db/models/sends.model.js.map +1 -1
- package/dist/db/models/settings.model.d.ts +7 -5
- package/dist/db/models/suppressions.model.d.ts +7 -5
- package/dist/db/models/template-versions.model.d.ts +7 -5
- package/dist/db/models/template-versions.model.js +4 -3
- package/dist/db/models/template-versions.model.js.map +1 -1
- package/dist/db/models/templates.model.d.ts +7 -5
- package/dist/db/models/webhook-status.model.d.ts +7 -5
- package/dist/db/tables/settings.table.d.ts +1 -0
- package/dist/db/tables/settings.table.js +4 -0
- package/dist/db/tables/settings.table.js.map +1 -1
- package/dist/db/tables/templates.table.js.map +1 -1
- package/dist/engine/changes.js +8 -19
- package/dist/engine/changes.js.map +1 -1
- package/dist/engine/conditions.d.ts +2 -1
- package/dist/engine/conditions.js +3 -2
- package/dist/engine/conditions.js.map +1 -1
- package/dist/engine/coverage.d.ts +2 -1
- package/dist/engine/coverage.js.map +1 -1
- package/dist/engine/formats.d.ts +4 -6
- package/dist/engine/formats.js +17 -3
- package/dist/engine/formats.js.map +1 -1
- package/dist/engine/paths.d.ts +4 -2
- package/dist/engine/paths.js +15 -3
- package/dist/engine/paths.js.map +1 -1
- package/dist/engine/plain-text.js +2 -4
- package/dist/engine/plain-text.js.map +1 -1
- package/dist/engine/resolve.d.ts +2 -1
- package/dist/engine/resolve.js +1 -1
- package/dist/engine/resolve.js.map +1 -1
- package/dist/engine/system-variables.d.ts +2 -1
- package/dist/engine/system-variables.js.map +1 -1
- package/dist/engine/tokens.d.ts +2 -1
- package/dist/engine/tokens.js.map +1 -1
- package/dist/hooks/tenant-export.d.ts +1 -1
- package/dist/hooks/tenant-export.js +10 -9
- package/dist/hooks/tenant-export.js.map +1 -1
- package/dist/implementations/dms-mailing/index.d.ts +1 -0
- package/dist/implementations/dms-mailing/index.js +3 -1
- package/dist/implementations/dms-mailing/index.js.map +1 -1
- package/dist/pages/sends.js.map +1 -1
- package/dist/pages/templates.js +3 -1
- package/dist/pages/templates.js.map +1 -1
- package/dist/routes/events.js +4 -2
- package/dist/routes/events.js.map +1 -1
- package/dist/routes/index.d.ts +1 -0
- package/dist/routes/index.js +1 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/metrics.js +27 -28
- package/dist/routes/metrics.js.map +1 -1
- package/dist/routes/period.js +5 -5
- package/dist/routes/period.js.map +1 -1
- package/dist/routes/sends.js +3 -4
- package/dist/routes/sends.js.map +1 -1
- package/dist/routes/settings.d.ts +4 -3
- package/dist/routes/settings.js +20 -14
- package/dist/routes/settings.js.map +1 -1
- package/dist/routes/suppressions.js +9 -6
- package/dist/routes/suppressions.js.map +1 -1
- package/dist/routes/template-content.d.ts +27 -0
- package/dist/routes/template-content.js +3 -0
- package/dist/routes/template-content.js.map +1 -0
- package/dist/routes/templates.d.ts +7 -30
- package/dist/routes/templates.js +3 -2
- package/dist/routes/templates.js.map +1 -1
- package/dist/routes/zod-json.d.ts +3 -0
- package/dist/routes/zod-json.js +21 -0
- package/dist/routes/zod-json.js.map +1 -0
- package/dist/services/attention.js +14 -7
- package/dist/services/attention.js.map +1 -1
- package/dist/services/catalog.js +4 -2
- package/dist/services/catalog.js.map +1 -1
- package/dist/services/declaration-error.d.ts +1 -1
- package/dist/services/declaration-error.js.map +1 -1
- package/dist/services/declarations.js +4 -1
- package/dist/services/declarations.js.map +1 -1
- package/dist/services/events.js +3 -1
- package/dist/services/events.js.map +1 -1
- package/dist/services/metrics.js +2 -5
- package/dist/services/metrics.js.map +1 -1
- package/dist/services/send-import.js +4 -4
- package/dist/services/send-import.js.map +1 -1
- package/dist/services/send.js +3 -4
- package/dist/services/send.js.map +1 -1
- package/dist/services/settings.d.ts +6 -3
- package/dist/services/settings.js +24 -5
- package/dist/services/settings.js.map +1 -1
- package/dist/services/starters.d.ts +3 -2
- package/dist/services/starters.js +1 -1
- package/dist/services/starters.js.map +1 -1
- package/dist/services/templates.d.ts +2 -1
- package/dist/services/templates.js +5 -5
- package/dist/services/templates.js.map +1 -1
- package/dist/services/webhook-secret-declaration.d.ts +4 -0
- package/dist/services/webhook-secret-declaration.js +25 -0
- package/dist/services/webhook-secret-declaration.js.map +1 -0
- package/dist/services/webhook-status.d.ts +2 -2
- package/dist/services/webhook-status.js +13 -6
- package/dist/services/webhook-status.js.map +1 -1
- package/dist/types/json.d.ts +1 -0
- package/dist/types/json.js +3 -0
- package/dist/types/json.js.map +1 -0
- package/dist/types/settings.d.ts +5 -2
- package/dist/types/settings.js +6 -1
- package/dist/types/settings.js.map +1 -1
- package/dist/validation/declarations.schema.d.ts +41 -28
- package/dist/validation/declarations.schema.js +10 -4
- package/dist/validation/declarations.schema.js.map +1 -1
- package/dist/validation/guards.d.ts +0 -1
- package/dist/validation/guards.js +0 -4
- package/dist/validation/guards.js.map +1 -1
- package/dist/validation/json.d.ts +5 -0
- package/dist/validation/json.js +54 -0
- package/dist/validation/json.js.map +1 -0
- package/dist/validation/json.schema.d.ts +2 -0
- package/dist/validation/json.schema.js +9 -0
- package/dist/validation/json.schema.js.map +1 -0
- package/dist/validation/templates.schema.d.ts +25 -24
- package/dist/validation/templates.schema.js +4 -3
- package/dist/validation/templates.schema.js.map +1 -1
- package/frontend-vue/app/components/editor/EditorCanvas.vue +8 -4
- package/frontend-vue/app/composables/useSortableList.ts +2 -2
- package/frontend-vue/package.json +37 -36
- package/frontend-vue/pnpm-lock.yaml +65 -5
- package/frontend-vue/tests/editor-canvas.test.ts +142 -0
- package/frontend-vue/tests/sortable-list.test-d.ts +19 -0
- package/frontend-vue/tsconfig.typecheck.json +12 -0
- package/frontend-vue/vitest.config.ts +16 -0
- package/package.json +16 -15
- package/skills/dms-mailing/SKILL.md +5 -3
- package/src/automation/email-event-trigger.ts +2 -2
- package/src/automation/send-template-action.ts +33 -14
- package/src/data/body.ts +5 -2
- package/src/data/templates-table.ts +46 -52
- package/src/data-types/categories-type.ts +2 -2
- package/src/data-types/category-type.ts +2 -2
- package/src/db/models/sends.model.ts +2 -2
- package/src/db/models/template-versions.model.ts +5 -3
- package/src/db/tables/settings.table.ts +4 -0
- package/src/db/tables/templates.table.ts +1 -1
- package/src/engine/changes.ts +11 -29
- package/src/engine/conditions.ts +12 -4
- package/src/engine/coverage.ts +2 -1
- package/src/engine/formats.ts +31 -17
- package/src/engine/paths.ts +39 -6
- package/src/engine/plain-text.ts +2 -4
- package/src/engine/resolve.ts +12 -4
- package/src/engine/system-variables.ts +2 -1
- package/src/engine/tokens.ts +2 -1
- package/src/hooks/tenant-export.ts +15 -14
- package/src/implementations/dms-mailing/index.ts +1 -0
- package/src/pages/sends.ts +3 -2
- package/src/pages/templates.ts +3 -1
- package/src/routes/events.ts +7 -4
- package/src/routes/index.ts +1 -0
- package/src/routes/metrics.ts +30 -30
- package/src/routes/period.ts +5 -5
- package/src/routes/sends.ts +13 -8
- package/src/routes/settings.ts +34 -17
- package/src/routes/suppressions.ts +12 -8
- package/src/routes/template-content.ts +45 -0
- package/src/routes/templates.ts +11 -44
- package/src/routes/zod-json.ts +28 -0
- package/src/services/attention.ts +16 -15
- package/src/services/catalog.ts +4 -8
- package/src/services/declaration-error.ts +1 -1
- package/src/services/declarations.ts +4 -1
- package/src/services/events.ts +4 -2
- package/src/services/metrics.ts +3 -5
- package/src/services/send-import.ts +10 -8
- package/src/services/send.ts +3 -6
- package/src/services/settings.ts +55 -16
- package/src/services/starters.ts +8 -4
- package/src/services/templates.ts +16 -9
- package/src/services/webhook-secret-declaration.ts +47 -0
- package/src/services/webhook-status.ts +18 -9
- package/src/types/json.ts +1 -0
- package/src/types/settings.ts +16 -2
- package/src/validation/declarations.schema.ts +11 -3
- package/src/validation/guards.ts +0 -8
- package/src/validation/json.schema.ts +7 -0
- package/src/validation/json.ts +66 -0
- package/src/validation/templates.schema.ts +7 -3
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DeclareWebhookSecretResult,
|
|
3
|
+
WebhookSecretDeclaration,
|
|
4
|
+
WebhookSecretOutcome,
|
|
5
|
+
} from "@antelopejs/interface-dms-mailing";
|
|
6
|
+
import type { MailingSettingsValues } from "../types";
|
|
7
|
+
import { webhookSecretDeclarationSchema } from "../validation/declarations.schema";
|
|
8
|
+
import { parseDeclared } from "./declaration-error";
|
|
9
|
+
import { webhookSecretMatches } from "./events";
|
|
10
|
+
import { getSettings, saveWebhookSecret } from "./settings";
|
|
11
|
+
|
|
12
|
+
const SET: WebhookSecretOutcome = "set";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* What declaring `secret` does to the stored one: nothing when it is already
|
|
16
|
+
* stored, nothing when the workspace owner set or rotated it and the caller
|
|
17
|
+
* did not ask to replace it, else a write.
|
|
18
|
+
*/
|
|
19
|
+
export function webhookSecretOutcome(
|
|
20
|
+
stored: Pick<MailingSettingsValues, "webhookSecret" | "webhookSecretSource">,
|
|
21
|
+
secret: string,
|
|
22
|
+
replaceRotated: boolean,
|
|
23
|
+
): WebhookSecretOutcome {
|
|
24
|
+
if (webhookSecretMatches(secret, stored.webhookSecret)) return "unchanged";
|
|
25
|
+
if (stored.webhookSecretSource === "operator" && !replaceRotated)
|
|
26
|
+
return "kept";
|
|
27
|
+
return SET;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Sets the workspace's webhook secret a project ships as code. The secret is
|
|
32
|
+
* never returned, and a refusal names the rule it broke, never the value.
|
|
33
|
+
*/
|
|
34
|
+
export async function declareWebhookSecret(
|
|
35
|
+
input: WebhookSecretDeclaration,
|
|
36
|
+
): Promise<DeclareWebhookSecretResult> {
|
|
37
|
+
const { tenantId, secret, replaceRotated } = parseDeclared(
|
|
38
|
+
webhookSecretDeclarationSchema,
|
|
39
|
+
input,
|
|
40
|
+
"invalid_webhook_secret",
|
|
41
|
+
"webhook secret declaration",
|
|
42
|
+
);
|
|
43
|
+
const stored = await getSettings(tenantId);
|
|
44
|
+
const outcome = webhookSecretOutcome(stored, secret, replaceRotated);
|
|
45
|
+
if (outcome === SET) await saveWebhookSecret(tenantId, secret, "declared");
|
|
46
|
+
return { outcome };
|
|
47
|
+
}
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
WebhookRefusal,
|
|
8
8
|
WebhookStatus,
|
|
9
9
|
} from "../types";
|
|
10
|
+
import { isObject, isText } from "../validation/guards";
|
|
10
11
|
import { webhookSecretMatches } from "./events";
|
|
11
12
|
import { WEBHOOK_SECRET_HEADER } from "../constants";
|
|
12
13
|
|
|
@@ -28,11 +29,11 @@ export const WEBHOOK_REFUSAL_MESSAGES: Record<WebhookRefusal, string> = {
|
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
/** The body of a refused webhook call. */
|
|
31
|
-
export
|
|
32
|
+
export type WebhookRefusalBody = {
|
|
32
33
|
error: "webhook_refused";
|
|
33
34
|
reason: WebhookRefusal;
|
|
34
35
|
message: string;
|
|
35
|
-
}
|
|
36
|
+
};
|
|
36
37
|
|
|
37
38
|
/** What "Send a test event" found when it called the webhook URL. */
|
|
38
39
|
export interface WebhookTestOutcome {
|
|
@@ -143,17 +144,19 @@ export async function readWebhookStatus(
|
|
|
143
144
|
);
|
|
144
145
|
}
|
|
145
146
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
: String(error);
|
|
147
|
+
function isWebhookRefusal(value: unknown): value is WebhookRefusal {
|
|
148
|
+
return isText(value) && Object.hasOwn(WEBHOOK_REFUSAL_MESSAGES, value);
|
|
149
|
+
}
|
|
150
150
|
|
|
151
|
+
/** The webhook's own refusal reason in a response, not one another endpoint gives. */
|
|
151
152
|
async function refusalOf(
|
|
152
153
|
response: Response,
|
|
153
154
|
): Promise<WebhookRefusal | undefined> {
|
|
154
155
|
try {
|
|
155
|
-
const body =
|
|
156
|
-
return body.reason
|
|
156
|
+
const body: unknown = await response.json();
|
|
157
|
+
return isObject(body) && isWebhookRefusal(body.reason)
|
|
158
|
+
? body.reason
|
|
159
|
+
: undefined;
|
|
157
160
|
} catch {
|
|
158
161
|
return undefined;
|
|
159
162
|
}
|
|
@@ -192,11 +195,17 @@ export async function sendWebhookTest(
|
|
|
192
195
|
refusal: response.ok ? undefined : await refusalOf(response),
|
|
193
196
|
};
|
|
194
197
|
} catch (error) {
|
|
198
|
+
const cause = error instanceof Error ? error.cause : undefined;
|
|
199
|
+
const causeMessage =
|
|
200
|
+
isObject(cause) && isText(cause.message) ? cause.message : undefined;
|
|
195
201
|
return {
|
|
196
202
|
isReceived: false,
|
|
197
203
|
url,
|
|
198
204
|
durationMs: elapsed(),
|
|
199
|
-
networkError:
|
|
205
|
+
networkError:
|
|
206
|
+
error instanceof Error
|
|
207
|
+
? (causeMessage ?? error.message)
|
|
208
|
+
: String(error),
|
|
200
209
|
};
|
|
201
210
|
}
|
|
202
211
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { JsonObject, JsonValue } from "@antelopejs/interface-dms-mailing";
|
package/src/types/settings.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { LocalizedText } from "@antelopejs/interface-dms-mailing";
|
|
2
2
|
|
|
3
3
|
/** A template category as stored: its label written once per language. */
|
|
4
|
-
export
|
|
4
|
+
export type TemplateCategory = {
|
|
5
5
|
id: string;
|
|
6
6
|
label: LocalizedText;
|
|
7
7
|
icon: string;
|
|
8
|
-
}
|
|
8
|
+
};
|
|
9
9
|
|
|
10
10
|
/** A template category as one reader sees it: the label in their language. */
|
|
11
11
|
export interface CategoryInLocale {
|
|
@@ -14,6 +14,19 @@ export interface CategoryInLocale {
|
|
|
14
14
|
icon: string;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Who set the webhook secret: the module drew it (`generated`), a project
|
|
19
|
+
* declared it (`declared`), or the workspace owner set or rotated it in the
|
|
20
|
+
* settings (`operator`). A declaration replaces only the first two.
|
|
21
|
+
*/
|
|
22
|
+
export const WEBHOOK_SECRET_SOURCES = [
|
|
23
|
+
"generated",
|
|
24
|
+
"declared",
|
|
25
|
+
"operator",
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
export type WebhookSecretSource = (typeof WEBHOOK_SECRET_SOURCES)[number];
|
|
29
|
+
|
|
17
30
|
export interface MailingSettingsValues {
|
|
18
31
|
fallbackLocale: string;
|
|
19
32
|
logRetentionDays: number;
|
|
@@ -22,6 +35,7 @@ export interface MailingSettingsValues {
|
|
|
22
35
|
senderEmail: string;
|
|
23
36
|
replyTo: string;
|
|
24
37
|
webhookSecret: string;
|
|
38
|
+
webhookSecretSource: WebhookSecretSource;
|
|
25
39
|
categories: TemplateCategory[];
|
|
26
40
|
}
|
|
27
41
|
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
templateContentSchema,
|
|
9
9
|
variableListSchema,
|
|
10
10
|
} from "./templates.schema";
|
|
11
|
+
import { jsonObjectSchema } from "./json.schema";
|
|
11
12
|
|
|
12
13
|
const MAX_AUTHOR_LENGTH = MAX_NAME_LENGTH;
|
|
13
14
|
const MAX_IMPORT_ID_LENGTH = 200;
|
|
@@ -36,7 +37,7 @@ export const templateDeclarationSchema = z.object({
|
|
|
36
37
|
category: z.string().trim().default(""),
|
|
37
38
|
content: templateContentSchema,
|
|
38
39
|
variables: variableListSchema.default([]),
|
|
39
|
-
testData:
|
|
40
|
+
testData: jsonObjectSchema.default({}),
|
|
40
41
|
author: z.string().trim().min(1).max(MAX_AUTHOR_LENGTH).optional(),
|
|
41
42
|
});
|
|
42
43
|
|
|
@@ -67,6 +68,13 @@ export const senderDeclarationSchema = z.object({
|
|
|
67
68
|
|
|
68
69
|
export type ParsedSenderDeclaration = z.infer<typeof senderDeclarationSchema>;
|
|
69
70
|
|
|
71
|
+
/** A webhook secret declared from code: the settings form's rule. */
|
|
72
|
+
export const webhookSecretDeclarationSchema = z.object({
|
|
73
|
+
tenantId: tenantIdSchema,
|
|
74
|
+
secret: settingsSchema.shape.webhookSecret,
|
|
75
|
+
replaceRotated: z.boolean().default(false),
|
|
76
|
+
});
|
|
77
|
+
|
|
70
78
|
const recipientSchema = z.union([
|
|
71
79
|
z.string().trim().email(),
|
|
72
80
|
z.object({
|
|
@@ -78,7 +86,7 @@ const recipientSchema = z.union([
|
|
|
78
86
|
const importedEventSchema = z.object({
|
|
79
87
|
type: sendEventTypeSchema,
|
|
80
88
|
at: z.coerce.date(),
|
|
81
|
-
details:
|
|
89
|
+
details: jsonObjectSchema.optional(),
|
|
82
90
|
});
|
|
83
91
|
|
|
84
92
|
const importedSendSchema = z.object({
|
|
@@ -87,7 +95,7 @@ const importedSendSchema = z.object({
|
|
|
87
95
|
to: recipientSchema,
|
|
88
96
|
sentAt: z.coerce.date(),
|
|
89
97
|
locale: z.string().trim().min(1).optional(),
|
|
90
|
-
variables:
|
|
98
|
+
variables: jsonObjectSchema.default({}),
|
|
91
99
|
provider: z.string().optional(),
|
|
92
100
|
providerMessageId: z.string().optional(),
|
|
93
101
|
source: z.string().optional(),
|
package/src/validation/guards.ts
CHANGED
|
@@ -3,14 +3,6 @@ export function isText(value: unknown): value is string {
|
|
|
3
3
|
return typeof value === "string";
|
|
4
4
|
}
|
|
5
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
6
|
/**
|
|
15
7
|
* Whether `value` is an object, arrays included, and not `null`: reading any
|
|
16
8
|
* key of it yields a value of unknown type.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { JsonObject, JsonValue } from "../types/json";
|
|
2
|
+
import { isNumber, isObject, isText } from "./guards";
|
|
3
|
+
|
|
4
|
+
/** A step of the walk in {@link isJsonValue}: visit a node, or leave one. */
|
|
5
|
+
type JsonWalkStep = { visit: unknown } | { leave: object };
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Whether `value` is wholly JSON data: strings, numbers, booleans, null, and
|
|
9
|
+
* arrays and plain objects of the same, none of them reached through itself.
|
|
10
|
+
* Walked with a stack, so a deeply nested value cannot overflow it.
|
|
11
|
+
*/
|
|
12
|
+
export function isJsonValue(value: unknown): value is JsonValue {
|
|
13
|
+
const stack: JsonWalkStep[] = [{ visit: value }];
|
|
14
|
+
const ancestors = new Set<object>();
|
|
15
|
+
for (let step = stack.pop(); step; step = stack.pop()) {
|
|
16
|
+
if ("leave" in step) {
|
|
17
|
+
ancestors.delete(step.leave);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const item = step.visit;
|
|
21
|
+
if (
|
|
22
|
+
item === null ||
|
|
23
|
+
isText(item) ||
|
|
24
|
+
isNumber(item) ||
|
|
25
|
+
typeof item === "boolean"
|
|
26
|
+
)
|
|
27
|
+
continue;
|
|
28
|
+
if (typeof item !== "object" || ancestors.has(item)) return false;
|
|
29
|
+
const prototype: unknown = Object.getPrototypeOf(item);
|
|
30
|
+
if (
|
|
31
|
+
!Array.isArray(item) &&
|
|
32
|
+
prototype !== Object.prototype &&
|
|
33
|
+
prototype !== null
|
|
34
|
+
)
|
|
35
|
+
return false;
|
|
36
|
+
ancestors.add(item);
|
|
37
|
+
stack.push({ leave: item });
|
|
38
|
+
for (const child of Object.values(item)) stack.push({ visit: child });
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Whether `value` is wholly JSON data and an object: neither `null` nor an array. */
|
|
44
|
+
export function isJsonObject(value: unknown): value is JsonObject {
|
|
45
|
+
return (
|
|
46
|
+
typeof value === "object" &&
|
|
47
|
+
value !== null &&
|
|
48
|
+
!Array.isArray(value) &&
|
|
49
|
+
isJsonValue(value)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** `text` parsed as JSON; a `SyntaxError` when it is not. */
|
|
54
|
+
export function parseJson(text: string): JsonValue {
|
|
55
|
+
const parsed: unknown = JSON.parse(text);
|
|
56
|
+
if (!isJsonValue(parsed)) throw new TypeError("expected JSON data");
|
|
57
|
+
return parsed;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** `text` parsed as a JSON object. */
|
|
61
|
+
export function parseJsonObject(text: string): JsonObject {
|
|
62
|
+
const parsed = parseJson(text);
|
|
63
|
+
if (!isObject(parsed) || Array.isArray(parsed))
|
|
64
|
+
throw new TypeError("expected a JSON object");
|
|
65
|
+
return parsed;
|
|
66
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { MAX_NAME_LENGTH, MAX_SLUG_LENGTH, SLUG_PATTERN } from "../constants";
|
|
3
3
|
import type { Block } from "../types";
|
|
4
|
+
import { jsonObjectSchema } from "./json.schema";
|
|
4
5
|
|
|
5
6
|
const MIN_HEADING_SIZE = 12;
|
|
6
7
|
const MAX_HEADING_SIZE = 48;
|
|
@@ -149,7 +150,7 @@ export const saveContentSchema = z.object({
|
|
|
149
150
|
revision: draftRevisionSchema,
|
|
150
151
|
});
|
|
151
152
|
|
|
152
|
-
export const testDataSchema = z.object({ data:
|
|
153
|
+
export const testDataSchema = z.object({ data: jsonObjectSchema });
|
|
153
154
|
|
|
154
155
|
/**
|
|
155
156
|
* Which content a preview renders when it carries none: the working draft
|
|
@@ -166,7 +167,7 @@ export const previewSchema = z.object({
|
|
|
166
167
|
locale: z.string().min(1).optional(),
|
|
167
168
|
version: previewVersionSchema.optional(),
|
|
168
169
|
content: templateContentSchema.optional(),
|
|
169
|
-
data:
|
|
170
|
+
data: jsonObjectSchema.optional(),
|
|
170
171
|
});
|
|
171
172
|
|
|
172
173
|
/**
|
|
@@ -176,7 +177,7 @@ export const previewSchema = z.object({
|
|
|
176
177
|
export const realSendSchema = z.object({
|
|
177
178
|
to: z.array(z.string().email()).min(1).max(MAX_TEST_RECIPIENT_COUNT),
|
|
178
179
|
locale: z.string().min(1).optional(),
|
|
179
|
-
data:
|
|
180
|
+
data: jsonObjectSchema.optional(),
|
|
180
181
|
});
|
|
181
182
|
|
|
182
183
|
export const testSendSchema = previewSchema.extend({
|
|
@@ -193,6 +194,9 @@ export const templateDetailsSchema = z.object({
|
|
|
193
194
|
category: z.string().nullable().optional(),
|
|
194
195
|
});
|
|
195
196
|
|
|
197
|
+
/** The details a template's drawer or the editor saves, validated. */
|
|
198
|
+
export type TemplateDetails = z.infer<typeof templateDetailsSchema>;
|
|
199
|
+
|
|
196
200
|
export const duplicateSchema = z.object({
|
|
197
201
|
slug: slugSchema,
|
|
198
202
|
name: z.string().trim().min(1).max(MAX_NAME_LENGTH),
|