@cosmicdrift/kumiko-samples 0.363.0 → 0.365.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/LICENSE +1 -1
- package/package.json +1 -1
- package/packages/bundled-features/package.json +7 -1
- package/packages/bundled-features/src/audit/changes.json +18 -0
- package/packages/bundled-features/src/audit/feature.ts +1 -1
- package/packages/bundled-features/src/audit/handlers/list.query.ts +10 -5
- package/packages/bundled-features/src/audit/run-escape-hatch-retention.ts +63 -11
- package/packages/bundled-features/src/auth-email-password/bootstrap.ts +30 -11
- package/packages/bundled-features/src/auth-email-password/changes.json +55 -0
- package/packages/bundled-features/src/auth-email-password/email-rate-limits.ts +16 -0
- package/packages/bundled-features/src/auth-email-password/feature.ts +4 -7
- package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +6 -2
- package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +2 -3
- package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +6 -4
- package/packages/bundled-features/src/auth-email-password/handlers/logout.write.ts +7 -2
- package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +2 -1
- package/packages/bundled-features/src/auth-email-password/handlers/switch-tenant-mfa-gate.write.ts +2 -2
- package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +2 -1
- package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +7 -3
- package/packages/bundled-features/src/auth-email-password/mail-i18n.ts +10 -0
- package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +24 -5
- package/packages/bundled-features/src/billing-foundation/changes.json +48 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +57 -5
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +22 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +4 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +11 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +9 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +41 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +18 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +35 -19
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +9 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +35 -0
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +11 -3
- package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +10 -4
- package/packages/bundled-features/src/billing-foundation/i18n.ts +9 -0
- package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +27 -7
- package/packages/bundled-features/src/billing-foundation/types.ts +7 -2
- package/packages/bundled-features/src/cap-counter/book-cap-usage.ts +20 -8
- package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +2 -0
- package/packages/bundled-features/src/cap-counter/cap-reservation.ts +7 -2
- package/packages/bundled-features/src/cap-counter/changes.json +19 -0
- package/packages/bundled-features/src/cap-counter/enforce-cap.ts +75 -5
- package/packages/bundled-features/src/cap-counter/handlers/increment-rolling.write.ts +2 -0
- package/packages/bundled-features/src/cap-counter/index.ts +1 -0
- package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +3 -0
- package/packages/bundled-features/src/cap-overview/changes.json +6 -0
- package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
- package/packages/bundled-features/src/cap-overview/handlers/tenant-options.query.ts +2 -1
- package/packages/bundled-features/src/channel-discord/discord-channel.ts +4 -2
- package/packages/bundled-features/src/channel-email/changes.json +18 -0
- package/packages/bundled-features/src/channel-email/email-channel.ts +47 -24
- package/packages/bundled-features/src/channel-email/smtp-transport.ts +7 -1
- package/packages/bundled-features/src/channel-slack/slack-channel.ts +4 -2
- package/packages/bundled-features/src/channel-teams/teams-channel.ts +4 -2
- package/packages/bundled-features/src/config/changes.json +6 -0
- package/packages/bundled-features/src/config/handlers/set.write.ts +2 -2
- package/packages/bundled-features/src/config/resolver.ts +15 -12
- package/packages/bundled-features/src/config/write-helpers.ts +23 -0
- package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +54 -27
- package/packages/bundled-features/src/custom-fields/changes.json +6 -0
- package/packages/bundled-features/src/custom-fields/db/queries/retention.ts +2 -2
- package/packages/bundled-features/src/custom-fields/handlers/update-tenant-field.write.ts +0 -1
- package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +4 -5
- package/packages/bundled-features/src/delivery/attachment-ref.ts +39 -1
- package/packages/bundled-features/src/delivery/changes.json +30 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +31 -8
- package/packages/bundled-features/src/delivery/delivery-service.ts +93 -92
- package/packages/bundled-features/src/delivery/index.ts +2 -0
- package/packages/bundled-features/src/delivery/jobs.ts +51 -14
- package/packages/bundled-features/src/delivery/types.ts +7 -1
- package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
- package/packages/bundled-features/src/feature-toggles/changes.json +7 -0
- package/packages/bundled-features/src/feature-toggles/toggle-runtime.ts +26 -12
- package/packages/bundled-features/src/file-derivatives/handlers/public-variant.query.ts +1 -1
- package/packages/bundled-features/src/file-foundation/changes.json +6 -0
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +6 -3
- package/packages/bundled-features/src/files-tenant-data/changes.json +6 -0
- package/packages/bundled-features/src/files-tenant-data/hooks.ts +4 -0
- package/packages/bundled-features/src/files-tenant-data/index.ts +3 -1
- package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +12 -3
- package/packages/bundled-features/src/form-draft/handlers/discard.write.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +6 -0
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +13 -8
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +3 -1
- package/packages/bundled-features/src/jobs/changes.json +31 -0
- package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +12 -4
- package/packages/bundled-features/src/jobs/feature.ts +7 -4
- package/packages/bundled-features/src/jobs/handlers/list.query.ts +3 -1
- package/packages/bundled-features/src/jobs/handlers/stale-run-sweep.job.ts +5 -2
- package/packages/bundled-features/src/jobs/job-run-logger.ts +56 -18
- package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +7 -2
- package/packages/bundled-features/src/ledger/changes.json +7 -0
- package/packages/bundled-features/src/ledger/entity.ts +3 -1
- package/packages/bundled-features/src/metrics/changes.json +6 -0
- package/packages/bundled-features/src/metrics/filters.ts +28 -12
- package/packages/bundled-features/src/personal-access-tokens/changes.json +6 -0
- package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +7 -8
- package/packages/bundled-features/src/renderer-foundation/types.ts +2 -0
- package/packages/bundled-features/src/renderer-pdf/changes.json +12 -0
- package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +162 -82
- package/packages/bundled-features/src/secrets/changes.json +12 -0
- package/packages/bundled-features/src/secrets/feature.ts +4 -3
- package/packages/bundled-features/src/secrets/handlers/delete.write.ts +3 -1
- package/packages/bundled-features/src/secrets/secrets-context.ts +23 -3
- package/packages/bundled-features/src/secrets/write-gate.ts +8 -2
- package/packages/bundled-features/src/shared/changes.json +6 -0
- package/packages/bundled-features/src/shared/chunked-entity-migration.ts +4 -1
- package/packages/bundled-features/src/shared/index.ts +8 -0
- package/packages/bundled-features/src/shared/mail-template-body.ts +17 -0
- package/packages/bundled-features/src/shared/resource-image-formats.ts +31 -0
- package/packages/bundled-features/src/shared/sharp-input-limits.ts +7 -8
- package/packages/bundled-features/src/step-dispatcher/webhook-runner.ts +2 -2
- package/packages/bundled-features/src/subscription-stripe/changes.json +6 -0
- package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +19 -11
- package/packages/bundled-features/src/subscription-stripe/verify-webhook.ts +3 -1
- package/packages/bundled-features/src/template-resolver/README.md +4 -2
- package/packages/bundled-features/src/template-resolver/api.ts +3 -2
- package/packages/bundled-features/src/template-resolver/changes.json +149 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +148 -18
- package/packages/bundled-features/src/template-resolver/compile-steps-block.ts +68 -0
- package/packages/bundled-features/src/template-resolver/constants.ts +23 -3
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +3 -0
- package/packages/bundled-features/src/template-resolver/document-schema.ts +33 -5
- package/packages/bundled-features/src/template-resolver/document-steps.ts +114 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +16 -1
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +81 -39
- package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +4 -3
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +121 -53
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +6 -44
- package/packages/bundled-features/src/template-resolver/handlers/discard-draft.write.ts +52 -0
- package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +56 -1
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +6 -3
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +25 -15
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +12 -1
- package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +73 -30
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +14 -11
- package/packages/bundled-features/src/template-resolver/handlers/override-diff.ts +49 -0
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +102 -19
- package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +49 -10
- package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +39 -10
- package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +14 -2
- package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +54 -4
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +96 -14
- package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +51 -24
- package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +16 -2
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +4 -1
- package/packages/bundled-features/src/template-resolver/index.ts +18 -2
- package/packages/bundled-features/src/template-resolver/layout-appearance.ts +56 -0
- package/packages/bundled-features/src/template-resolver/mail-constants.ts +4 -0
- package/packages/bundled-features/src/template-resolver/pdf-constants.ts +12 -0
- package/packages/bundled-features/src/template-resolver/qualified-names.ts +4 -0
- package/packages/bundled-features/src/template-resolver/resource-image.ts +9 -16
- package/packages/bundled-features/src/template-resolver/seeding.ts +27 -4
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +390 -124
- package/packages/bundled-features/src/template-resolver/table.ts +13 -1
- package/packages/bundled-features/src/template-resolver/template-bundle.ts +4 -0
- package/packages/bundled-features/src/template-resolver/template-draft.ts +106 -0
- package/packages/bundled-features/src/template-resolver/template-meta.ts +95 -24
- package/packages/bundled-features/src/template-resolver/template-preview.ts +160 -57
- package/packages/bundled-features/src/template-resolver/template-resources.ts +3 -4
- package/packages/bundled-features/src/template-resolver/template-usage.ts +31 -11
- package/packages/bundled-features/src/template-resolver/template-validation.ts +16 -9
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +12 -8
- package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +97 -10
- package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +15 -5
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +405 -145
- package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +17 -3
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +32 -4
- package/packages/bundled-features/src/template-resolver/web/index.ts +8 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +23 -7
- package/packages/bundled-features/src/tenant/changes.json +6 -0
- package/packages/bundled-features/src/tenant/handlers/update-member-roles.write.ts +3 -8
- package/packages/bundled-features/src/tenant/seeding.ts +33 -27
- package/packages/bundled-features/src/tenant-handover/move-entity-graph.ts +14 -3
- package/packages/bundled-features/src/tenant-lifecycle/changes.json +6 -0
- package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +9 -1
- package/packages/bundled-features/src/tenant-lifecycle/stages.ts +3 -2
- package/packages/bundled-features/src/tier-engine/changes.json +18 -0
- package/packages/bundled-features/src/tier-engine/constants.ts +9 -0
- package/packages/bundled-features/src/tier-engine/entity.ts +9 -4
- package/packages/bundled-features/src/tier-engine/feature.ts +11 -3
- package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +8 -5
- package/packages/bundled-features/src/tier-engine/index.ts +2 -0
- package/packages/bundled-features/src/user/changes.json +6 -0
- package/packages/bundled-features/src/user/constants.ts +1 -0
- package/packages/bundled-features/src/user/invited-global-roles.ts +2 -4
- package/packages/bundled-features/src/user/resolve-display-names.ts +8 -15
- package/packages/bundled-features/src/user/seeding.ts +19 -5
- package/packages/bundled-features/src/user-data-rights/changes.json +12 -0
- package/packages/bundled-features/src/user-data-rights/email-templates.ts +0 -1
- package/packages/bundled-features/src/user-data-rights/i18n.ts +5 -0
- package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +13 -2
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +7 -1
- package/packages/bundled-features/src/user-profile/changes.json +1 -1
- package/packages/bundled-features/src/waitlist/changes.json +18 -0
- package/packages/bundled-features/src/waitlist/feature.ts +1 -1
- package/packages/bundled-features/src/waitlist/handlers/invite.write.ts +35 -3
- package/packages/bundled-features/src/waitlist/handlers/reject.write.ts +2 -17
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +33 -9
- package/packages/bundled-features/src/waitlist/i18n.ts +9 -0
- package/packages/bundled-features/src/waitlist/lib.ts +20 -0
- package/packages/bundled-features/src/waitlist/options.ts +1 -1
- package/packages/bundled-features/src/workflow-runner/changes.json +6 -0
- package/packages/bundled-features/src/workflow-runner/event-trigger.ts +2 -1
- package/packages/bundled-features/src/workflow-runner/feature.ts +1 -1
- package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +7 -1
- package/samples/apps/ui-walkthrough/package.json +9 -0
- package/samples/apps/ui-walkthrough/playwright.config.ts +40 -7
- package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +87 -2
- package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +246 -30
- package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +4 -0
- package/samples/apps/ui-walkthrough/src/run-config.ts +18 -1
- package/samples/apps/use-all-bundled/README.md +3 -13
- package/samples/apps/use-all-bundled/feature-manifest.json +7 -5
- package/samples/apps/use-all-bundled/scripts/check-coverage.ts +34 -118
- package/samples/apps/use-all-bundled/src/app/notes-feature.ts +1 -1
- package/samples/apps/use-all-bundled/src/run-config.ts +0 -3
- package/samples/recipes/auth-mfa/README.md +2 -1
- package/samples/recipes/auth-mfa/src/feature.ts +1 -0
- package/samples/recipes/custom-handlers/README.md +4 -3
- package/samples/recipes/custom-handlers/src/feature.ts +21 -12
- package/samples/recipes/dashboard-stats/README.md +38 -0
- package/samples/recipes/dashboard-stats/package.json +8 -0
- package/samples/recipes/dashboard-stats/src/feature.ts +104 -0
- package/samples/recipes/legal-pages/README.md +2 -2
- package/samples/recipes/managed-pages/README.md +2 -2
- package/samples/recipes/pdf-audit-export/README.md +2 -1
- package/samples/recipes/pdf-audit-export/package.json +1 -1
- package/samples/recipes/pdf-audit-export/src/feature.ts +68 -48
- package/samples/recipes/workflow-engine/src/db/queries/suspended-runs.ts +2 -2
- package/samples/apps/use-all-bundled/schema/generate.ts +0 -617
- package/samples/apps/use-all-bundled/schema/migrations/meta/0000_snapshot.json +0 -3055
- package/samples/apps/use-all-bundled/schema/migrations/meta/_journal.json +0 -13
- package/samples/apps/use-all-bundled/schema/schema.generated.ts +0 -94
- package/samples/apps/use-all-bundled/schema/schema.ts +0 -9
|
@@ -4,13 +4,23 @@ import * as z from "zod";
|
|
|
4
4
|
import { FALLBACK_LOCALE } from "../constants.js";
|
|
5
5
|
import { type LayoutRegions, readLayoutRegions } from "../layout-regions.js";
|
|
6
6
|
import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
|
|
7
|
+
import {
|
|
8
|
+
type ActiveInfo,
|
|
9
|
+
draftViewerTenantId,
|
|
10
|
+
isHiddenSystemDraft,
|
|
11
|
+
toWorkingRow,
|
|
12
|
+
type WorkingRow,
|
|
13
|
+
} from "../template-draft.js";
|
|
14
|
+
import { type MailLayoutMeta, readMailLayoutMeta } from "../template-meta.js";
|
|
7
15
|
import { localeSchema, templateSlugSchema } from "./shared.js";
|
|
16
|
+
import { parseStoredJson } from "./template-gates.js";
|
|
8
17
|
|
|
9
18
|
export type LayoutSourceRow = {
|
|
10
19
|
readonly id: string;
|
|
11
20
|
readonly version: number;
|
|
12
21
|
readonly status: string;
|
|
13
22
|
readonly locale: string;
|
|
23
|
+
readonly active: ActiveInfo | null;
|
|
14
24
|
};
|
|
15
25
|
|
|
16
26
|
export type LayoutSourceResponse = {
|
|
@@ -19,6 +29,8 @@ export type LayoutSourceResponse = {
|
|
|
19
29
|
readonly regions: LayoutRegions;
|
|
20
30
|
readonly effectiveFormat: "html" | "mjml" | null;
|
|
21
31
|
readonly mjmlSource: string | null;
|
|
32
|
+
/** Appearance of the effective mail layout; invalid stored meta reads as defaults (empty). */
|
|
33
|
+
readonly meta: MailLayoutMeta;
|
|
22
34
|
readonly locale: string;
|
|
23
35
|
};
|
|
24
36
|
|
|
@@ -38,33 +50,82 @@ function pickByLocale(
|
|
|
38
50
|
);
|
|
39
51
|
}
|
|
40
52
|
|
|
41
|
-
function toSourceRow(row:
|
|
42
|
-
return {
|
|
53
|
+
function toSourceRow(row: WorkingRow): LayoutSourceRow {
|
|
54
|
+
return {
|
|
55
|
+
id: String(row.id),
|
|
56
|
+
version: row.version,
|
|
57
|
+
status: row.status,
|
|
58
|
+
locale: row.locale,
|
|
59
|
+
active: row.active,
|
|
60
|
+
};
|
|
43
61
|
}
|
|
44
62
|
|
|
45
|
-
function layoutFormat(row:
|
|
63
|
+
function layoutFormat(row: WorkingRow): "html" | "mjml" | null {
|
|
46
64
|
return row.contentFormat === "html" || row.contentFormat === "mjml" ? row.contentFormat : null;
|
|
47
65
|
}
|
|
48
66
|
|
|
49
67
|
function pickEditedRows(
|
|
50
68
|
rows: readonly TemplateResourceRow[],
|
|
51
|
-
|
|
69
|
+
user: { readonly tenantId: string; readonly roles: readonly string[] },
|
|
52
70
|
locale: string | undefined,
|
|
53
71
|
): {
|
|
54
|
-
readonly tenantRow:
|
|
55
|
-
readonly systemRow:
|
|
72
|
+
readonly tenantRow: WorkingRow | null;
|
|
73
|
+
readonly systemRow: WorkingRow | null;
|
|
56
74
|
} {
|
|
57
|
-
const tenantRow =
|
|
75
|
+
const tenantRow = user.roles.includes("SystemAdmin")
|
|
58
76
|
? null
|
|
59
77
|
: pickByLocale(
|
|
60
78
|
rows.filter((row) => row.scope === "tenant"),
|
|
61
79
|
locale,
|
|
62
80
|
);
|
|
63
81
|
const systemRow = pickByLocale(
|
|
64
|
-
rows.filter((row) => row.scope === "system"),
|
|
82
|
+
rows.filter((row) => row.scope === "system" && !isHiddenSystemDraft(user, row)),
|
|
65
83
|
locale,
|
|
66
84
|
);
|
|
67
|
-
return {
|
|
85
|
+
return {
|
|
86
|
+
tenantRow: tenantRow && toWorkingRow(tenantRow, user.tenantId),
|
|
87
|
+
// Only a SystemAdmin works on the system draft; a tenant sees the served system version.
|
|
88
|
+
systemRow: systemRow && toWorkingRow(systemRow, draftViewerTenantId(user, systemRow)),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readEffectiveRegions(
|
|
93
|
+
effective: WorkingRow | null,
|
|
94
|
+
effectiveFormat: "html" | "mjml" | null,
|
|
95
|
+
): LayoutRegions {
|
|
96
|
+
return effective !== null && effectiveFormat === "html"
|
|
97
|
+
? readLayoutRegions(effective.content ?? "")
|
|
98
|
+
: EMPTY_REGIONS;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function readMjmlSource(systemRow: WorkingRow | null, isSystemAdmin: boolean): string | null {
|
|
102
|
+
const exposesMjml = isSystemAdmin && systemRow !== null && layoutFormat(systemRow) === "mjml";
|
|
103
|
+
return exposesMjml ? (systemRow.content ?? "") : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildLayoutSource(input: {
|
|
107
|
+
readonly kind: "mail-layout" | "pdf-layout";
|
|
108
|
+
readonly locale: string | undefined;
|
|
109
|
+
readonly isSystemAdmin: boolean;
|
|
110
|
+
readonly tenantRow: WorkingRow | null;
|
|
111
|
+
readonly systemRow: WorkingRow | null;
|
|
112
|
+
}): LayoutSourceResponse {
|
|
113
|
+
const { kind, locale, isSystemAdmin, tenantRow, systemRow } = input;
|
|
114
|
+
const effective = tenantRow ?? systemRow;
|
|
115
|
+
const effectiveFormat = effective === null ? null : layoutFormat(effective);
|
|
116
|
+
return {
|
|
117
|
+
tenant: tenantRow === null ? null : toSourceRow(tenantRow),
|
|
118
|
+
system:
|
|
119
|
+
systemRow === null
|
|
120
|
+
? null
|
|
121
|
+
: { ...toSourceRow(systemRow), contentFormat: systemRow.contentFormat },
|
|
122
|
+
regions: readEffectiveRegions(effective, effectiveFormat),
|
|
123
|
+
effectiveFormat,
|
|
124
|
+
mjmlSource: readMjmlSource(systemRow, isSystemAdmin),
|
|
125
|
+
meta:
|
|
126
|
+
kind === "mail-layout" ? readMailLayoutMeta(parseStoredJson(effective?.meta ?? null)) : {},
|
|
127
|
+
locale: effective?.locale ?? locale ?? FALLBACK_LOCALE,
|
|
128
|
+
};
|
|
68
129
|
}
|
|
69
130
|
|
|
70
131
|
export const layoutSourceQuery = defineQueryHandler({
|
|
@@ -76,7 +137,7 @@ export const layoutSourceQuery = defineQueryHandler({
|
|
|
76
137
|
}),
|
|
77
138
|
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
78
139
|
description:
|
|
79
|
-
"Reads the layout the caller edits: the tenant's own version and the system version of one layout slug, the header, signature and footer regions of the effective html version, and the locale a save has to use; a SystemAdmin edits the system version and also receives its mjml source. Use it to load the layout editor.",
|
|
140
|
+
"Reads the layout the caller edits: the tenant's own version and the system version of one layout slug (each the open draft if there is one, else the stored version, with `active` naming the served version), the header, signature and footer regions of the effective html version, and the locale a save has to use; a SystemAdmin edits the system version and also receives its mjml source. Use it to load the layout editor.",
|
|
80
141
|
handler: async (query, ctx): Promise<LayoutSourceResponse> => {
|
|
81
142
|
const { kind, slug, locale } = query.payload;
|
|
82
143
|
const isSystemAdmin = query.user.roles.includes("SystemAdmin");
|
|
@@ -84,25 +145,7 @@ export const layoutSourceQuery = defineQueryHandler({
|
|
|
84
145
|
kind,
|
|
85
146
|
slug,
|
|
86
147
|
});
|
|
87
|
-
const { tenantRow, systemRow } = pickEditedRows(rows,
|
|
88
|
-
|
|
89
|
-
const effectiveFormat = effective === null ? null : layoutFormat(effective);
|
|
90
|
-
return {
|
|
91
|
-
tenant: tenantRow === null ? null : toSourceRow(tenantRow),
|
|
92
|
-
system:
|
|
93
|
-
systemRow === null
|
|
94
|
-
? null
|
|
95
|
-
: { ...toSourceRow(systemRow), contentFormat: systemRow.contentFormat },
|
|
96
|
-
regions:
|
|
97
|
-
effective !== null && effectiveFormat === "html"
|
|
98
|
-
? readLayoutRegions(effective.content ?? "")
|
|
99
|
-
: EMPTY_REGIONS,
|
|
100
|
-
effectiveFormat,
|
|
101
|
-
mjmlSource:
|
|
102
|
-
isSystemAdmin && systemRow !== null && layoutFormat(systemRow) === "mjml"
|
|
103
|
-
? (systemRow.content ?? "")
|
|
104
|
-
: null,
|
|
105
|
-
locale: effective?.locale ?? locale ?? FALLBACK_LOCALE,
|
|
106
|
-
};
|
|
148
|
+
const { tenantRow, systemRow } = pickEditedRows(rows, query.user, locale);
|
|
149
|
+
return buildLayoutSource({ kind, locale, isSystemAdmin, tenantRow, systemRow });
|
|
107
150
|
},
|
|
108
151
|
});
|
|
@@ -3,6 +3,7 @@ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
|
3
3
|
import * as z from "zod";
|
|
4
4
|
import { TEMPLATE_KINDS, TEMPLATE_STATUSES } from "../constants.js";
|
|
5
5
|
import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
|
|
6
|
+
import { isHiddenSystemDraft } from "../template-draft.js";
|
|
6
7
|
|
|
7
8
|
// ctx.db is tenant-scoped even for SystemAdmin: every caller sees its own
|
|
8
9
|
// tenant's rows plus the system defaults, never another tenant's rows.
|
|
@@ -43,16 +44,18 @@ export const listQuery = defineQueryHandler({
|
|
|
43
44
|
limit: 500,
|
|
44
45
|
})) as TemplateResourceRow[];
|
|
45
46
|
|
|
46
|
-
return rows
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
47
|
+
return rows
|
|
48
|
+
.filter((row) => !isHiddenSystemDraft(query.user, row))
|
|
49
|
+
.map((row) => ({
|
|
50
|
+
id: String(row.id),
|
|
51
|
+
tenantId: row.tenantId,
|
|
52
|
+
slug: row.slug,
|
|
53
|
+
kind: row.kind,
|
|
54
|
+
locale: row.locale,
|
|
55
|
+
scope: row.scope,
|
|
56
|
+
status: row.status,
|
|
57
|
+
contentFormat: row.contentFormat,
|
|
58
|
+
modifiedAt: row.modifiedAt,
|
|
59
|
+
}));
|
|
57
60
|
},
|
|
58
61
|
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { TemplateResourceRow } from "../table.js";
|
|
2
|
+
import { diffTemplateBlocks, splitTemplateBlocks } from "../template-diff.js";
|
|
3
|
+
import { servedVersionOf } from "../template-draft.js";
|
|
4
|
+
|
|
5
|
+
function parseStoredMeta(raw: string | null): Readonly<Record<string, unknown>> {
|
|
6
|
+
if (raw === null) return {};
|
|
7
|
+
try {
|
|
8
|
+
const parsed: unknown = JSON.parse(raw);
|
|
9
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
10
|
+
? Object.fromEntries(Object.entries(parsed))
|
|
11
|
+
: {};
|
|
12
|
+
} catch {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function diffMeta(
|
|
18
|
+
system: Readonly<Record<string, unknown>>,
|
|
19
|
+
tenant: Readonly<Record<string, unknown>>,
|
|
20
|
+
) {
|
|
21
|
+
const keys = [...new Set([...Object.keys(system), ...Object.keys(tenant)])].sort();
|
|
22
|
+
return keys
|
|
23
|
+
.filter((key) => JSON.stringify(system[key]) !== JSON.stringify(tenant[key]))
|
|
24
|
+
.map((key) => ({ key, tenant: tenant[key], system: system[key] }));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Shared by the tenant's own diff and the SystemAdmin's per-tenant diff in the overrides drawer.
|
|
28
|
+
export function buildOverrideDiff(tenantRow: TemplateResourceRow, systemRow: TemplateResourceRow) {
|
|
29
|
+
const blocks = diffTemplateBlocks(
|
|
30
|
+
splitTemplateBlocks(systemRow.content ?? "", systemRow.contentFormat),
|
|
31
|
+
splitTemplateBlocks(tenantRow.content ?? "", tenantRow.contentFormat),
|
|
32
|
+
);
|
|
33
|
+
const meta = diffMeta(parseStoredMeta(systemRow.meta), parseStoredMeta(tenantRow.meta));
|
|
34
|
+
return {
|
|
35
|
+
tenant: {
|
|
36
|
+
templateId: String(tenantRow.id),
|
|
37
|
+
templateVersion: servedVersionOf(tenantRow),
|
|
38
|
+
contentFormat: tenantRow.contentFormat,
|
|
39
|
+
},
|
|
40
|
+
system: {
|
|
41
|
+
templateId: String(systemRow.id),
|
|
42
|
+
templateVersion: servedVersionOf(systemRow),
|
|
43
|
+
contentFormat: systemRow.contentFormat,
|
|
44
|
+
},
|
|
45
|
+
identical: meta.length === 0 && blocks.every((block) => block.change === "same"),
|
|
46
|
+
blocks,
|
|
47
|
+
meta,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -3,7 +3,15 @@ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
|
3
3
|
import * as z from "zod";
|
|
4
4
|
import { DEFAULT_MAIL_LAYOUT_SLUG, FALLBACK_LOCALE, type TemplateStatus } from "../constants.js";
|
|
5
5
|
import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
activeSinceOf,
|
|
8
|
+
activeVersionOf,
|
|
9
|
+
draftViewerTenantId,
|
|
10
|
+
isHiddenSystemDraft,
|
|
11
|
+
parseTemplateDraft,
|
|
12
|
+
servedVersionOf,
|
|
13
|
+
} from "../template-draft.js";
|
|
14
|
+
import { collectTemplateUsage, type EffectiveMailLayoutSlugs } from "../template-usage.js";
|
|
7
15
|
import { storedRecord } from "./template-gates.js";
|
|
8
16
|
|
|
9
17
|
const OVERVIEW_KINDS = ["mail", "document-pdf", "mail-layout", "pdf-layout"] as const;
|
|
@@ -31,6 +39,8 @@ export type OverviewRow = {
|
|
|
31
39
|
readonly locales: readonly string[];
|
|
32
40
|
readonly status: TemplateStatus;
|
|
33
41
|
readonly version: number;
|
|
42
|
+
readonly draftVersion: number | null;
|
|
43
|
+
readonly activeSince: string | null;
|
|
34
44
|
readonly systemVersion: number | null;
|
|
35
45
|
readonly layoutSlug: string | null;
|
|
36
46
|
readonly modifiedAt: string;
|
|
@@ -42,6 +52,8 @@ export type OverviewResponse = {
|
|
|
42
52
|
readonly rows: readonly OverviewRow[];
|
|
43
53
|
readonly nextCursor: null;
|
|
44
54
|
readonly total: number;
|
|
55
|
+
/** True when the tenant has more template rows than the overview reads; rows and total are then incomplete. */
|
|
56
|
+
readonly truncated: boolean;
|
|
45
57
|
};
|
|
46
58
|
|
|
47
59
|
const SORT_FIELDS = ["title", "slug", "kind", "origin", "status", "modifiedAt"] as const;
|
|
@@ -89,6 +101,27 @@ function effectiveRowsOf(rows: readonly TemplateResourceRow[]): EffectiveRows |
|
|
|
89
101
|
return tenantRows.length > 0 ? { origin: "tenant", rows: tenantRows } : null;
|
|
90
102
|
}
|
|
91
103
|
|
|
104
|
+
export function effectiveMailLayoutSlugs(
|
|
105
|
+
rows: readonly TemplateResourceRow[],
|
|
106
|
+
): EffectiveMailLayoutSlugs {
|
|
107
|
+
const byLocale = new Map<string, TemplateResourceRow[]>();
|
|
108
|
+
for (const row of rows) {
|
|
109
|
+
if (row.kind !== "mail") continue;
|
|
110
|
+
const key = `${row.slug}\u0000${row.locale}`;
|
|
111
|
+
byLocale.set(key, [...(byLocale.get(key) ?? []), row]);
|
|
112
|
+
}
|
|
113
|
+
const bySlug = new Map<string, Map<string, string>>();
|
|
114
|
+
for (const group of byLocale.values()) {
|
|
115
|
+
const effective = effectiveRowsOf(group)?.rows[0];
|
|
116
|
+
// All-archived: the renderer falls back to the code-registered template.
|
|
117
|
+
if (effective === undefined || effective.status === "archived") continue;
|
|
118
|
+
const locales = bySlug.get(effective.slug) ?? new Map<string, string>();
|
|
119
|
+
locales.set(effective.locale, layoutSlugOf(effective));
|
|
120
|
+
bySlug.set(effective.slug, locales);
|
|
121
|
+
}
|
|
122
|
+
return bySlug;
|
|
123
|
+
}
|
|
124
|
+
|
|
92
125
|
function modifiedAtOf(row: TemplateResourceRow): string {
|
|
93
126
|
return (row.modifiedAt ?? row.insertedAt).toString();
|
|
94
127
|
}
|
|
@@ -126,7 +159,37 @@ function groupBySlugAndKind(
|
|
|
126
159
|
return groups;
|
|
127
160
|
}
|
|
128
161
|
|
|
129
|
-
|
|
162
|
+
// Another tenant's draft, including a system draft seen by a tenant, is never announced.
|
|
163
|
+
function draftVersionFor(
|
|
164
|
+
user: { readonly tenantId: string; readonly roles: readonly string[] },
|
|
165
|
+
row: TemplateResourceRow,
|
|
166
|
+
): number | null {
|
|
167
|
+
const isOwnRow = row.tenantId === draftViewerTenantId(user, row);
|
|
168
|
+
return isOwnRow && parseTemplateDraft(row.draft) !== null ? row.version : null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Version columns of one overview row: the served version, a waiting draft and the system version it overrides.
|
|
172
|
+
function versionFieldsOf(
|
|
173
|
+
user: { readonly tenantId: string; readonly roles: readonly string[] },
|
|
174
|
+
preferred: TemplateResourceRow,
|
|
175
|
+
groupRows: readonly TemplateResourceRow[],
|
|
176
|
+
): Pick<OverviewDraft, "version" | "draftVersion" | "activeSince" | "systemVersion"> {
|
|
177
|
+
const isActive = preferred.status === "active";
|
|
178
|
+
const systemSameLocale = groupRows.find(
|
|
179
|
+
(row) => row.scope === "system" && row.locale === preferred.locale,
|
|
180
|
+
);
|
|
181
|
+
return {
|
|
182
|
+
version: servedVersionOf(preferred),
|
|
183
|
+
draftVersion: draftVersionFor(user, preferred),
|
|
184
|
+
activeSince: isActive ? activeSinceOf(preferred).toString() : null,
|
|
185
|
+
systemVersion: systemSameLocale === undefined ? null : activeVersionOf(systemSameLocale),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function buildDrafts(
|
|
190
|
+
groups: ReadonlyMap<string, readonly TemplateResourceRow[]>,
|
|
191
|
+
user: { readonly tenantId: string; readonly roles: readonly string[] },
|
|
192
|
+
): {
|
|
130
193
|
readonly drafts: OverviewDraft[];
|
|
131
194
|
readonly layoutSlugByDocument: Map<string, string>;
|
|
132
195
|
} {
|
|
@@ -137,9 +200,6 @@ function buildDrafts(groups: ReadonlyMap<string, readonly TemplateResourceRow[]>
|
|
|
137
200
|
const preferred = effective === null ? undefined : preferredRow(effective.rows);
|
|
138
201
|
if (effective === null || preferred === undefined) continue;
|
|
139
202
|
if (!isOverviewKind(preferred.kind) || !isTemplateStatus(preferred.status)) continue;
|
|
140
|
-
const systemSameLocale = groupRows.find(
|
|
141
|
-
(row) => row.scope === "system" && row.locale === preferred.locale,
|
|
142
|
-
);
|
|
143
203
|
const isDocument = preferred.kind === "mail" || preferred.kind === "document-pdf";
|
|
144
204
|
const layoutSlug = isDocument ? layoutSlugOf(preferred) : null;
|
|
145
205
|
if (layoutSlug !== null && preferred.kind === "document-pdf") {
|
|
@@ -154,8 +214,7 @@ function buildDrafts(groups: ReadonlyMap<string, readonly TemplateResourceRow[]>
|
|
|
154
214
|
origin: effective.origin,
|
|
155
215
|
locales: [...new Set(effective.rows.map((row) => row.locale))].sort(),
|
|
156
216
|
status: preferred.status,
|
|
157
|
-
|
|
158
|
-
systemVersion: systemSameLocale?.version ?? null,
|
|
217
|
+
...versionFieldsOf(user, preferred, groupRows),
|
|
159
218
|
layoutSlug,
|
|
160
219
|
modifiedAt: modifiedAtOf(preferred),
|
|
161
220
|
});
|
|
@@ -167,15 +226,18 @@ function withUsage(
|
|
|
167
226
|
draft: OverviewDraft,
|
|
168
227
|
layoutSlugByDocument: ReadonlyMap<string, string>,
|
|
169
228
|
registry: Parameters<typeof collectTemplateUsage>[0],
|
|
229
|
+
effectiveLayouts: EffectiveMailLayoutSlugs,
|
|
170
230
|
): OverviewRow {
|
|
171
231
|
let usedBy: OverviewUsage[] = [];
|
|
172
232
|
if (draft.kind === "mail" || draft.kind === "mail-layout") {
|
|
173
|
-
usedBy = collectTemplateUsage(registry, draft.slug, draft.kind).map(
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
233
|
+
usedBy = collectTemplateUsage(registry, draft.slug, draft.kind, effectiveLayouts).map(
|
|
234
|
+
(entry) => ({
|
|
235
|
+
qualifiedName: entry.qualifiedName,
|
|
236
|
+
feature: entry.feature,
|
|
237
|
+
notification: entry.notification,
|
|
238
|
+
trigger: entry.trigger,
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
179
241
|
} else if (draft.kind === "pdf-layout") {
|
|
180
242
|
usedBy = [...layoutSlugByDocument]
|
|
181
243
|
.filter(([, layoutSlug]) => layoutSlug === draft.slug)
|
|
@@ -192,9 +254,11 @@ function filterRows(
|
|
|
192
254
|
rows: readonly OverviewRow[],
|
|
193
255
|
search: string | undefined,
|
|
194
256
|
filters: readonly OverviewFilter[],
|
|
257
|
+
layoutSlug: string | undefined,
|
|
195
258
|
): OverviewRow[] {
|
|
196
259
|
const needle = search?.trim().toLowerCase() ?? "";
|
|
197
260
|
return rows.filter((row) => {
|
|
261
|
+
if (layoutSlug !== undefined && row.layoutSlug !== layoutSlug) return false;
|
|
198
262
|
if (needle !== "" && !`${row.title} ${row.slug}`.toLowerCase().includes(needle)) return false;
|
|
199
263
|
return filters.every((filter) => filter.value.includes(String(row[filter.field])));
|
|
200
264
|
});
|
|
@@ -211,6 +275,7 @@ export const overviewQuery = defineQueryHandler({
|
|
|
211
275
|
schema: z.object({
|
|
212
276
|
search: z.string().optional(),
|
|
213
277
|
filters: z.array(filterSchema).optional(),
|
|
278
|
+
layoutSlug: z.string().optional(),
|
|
214
279
|
sort: z.string().optional(),
|
|
215
280
|
sortDirection: z.enum(["asc", "desc"]).optional(),
|
|
216
281
|
limit: z.number().int().min(1).max(200).default(50),
|
|
@@ -219,21 +284,38 @@ export const overviewQuery = defineQueryHandler({
|
|
|
219
284
|
}),
|
|
220
285
|
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
221
286
|
description:
|
|
222
|
-
"Lists mail, PDF and layout templates as one row per slug and kind for the caller's tenant: where the effective version comes from (own override or system), its locales, status and version, the system version it overrides, the layout it uses and which notifications or templates use it; use it to browse and filter all templates at a glance.",
|
|
287
|
+
"Lists mail, PDF and layout templates as one row per slug and kind for the caller's tenant (optionally only the mails and PDFs using one layout slug): where the effective version comes from (own override or system), its locales, status and version (the served one; `draftVersion` is set when an unpublished draft waits beside it, `activeSince` says since when the served version applies), the system version it overrides, the layout it uses and which notifications or templates use it; use it to browse and filter all templates at a glance.",
|
|
223
288
|
handler: async (query, ctx) => {
|
|
224
289
|
const payload = query.payload;
|
|
225
|
-
const
|
|
290
|
+
const fetchedRows = await selectMany<TemplateResourceRow>(
|
|
226
291
|
ctx.db,
|
|
227
292
|
templateResourcesTable,
|
|
228
293
|
{ kind: { in: [...OVERVIEW_KINDS] } },
|
|
229
|
-
{ limit: OVERVIEW_ROW_LIMIT },
|
|
294
|
+
{ orderBy: { col: "id", direction: "asc" }, limit: OVERVIEW_ROW_LIMIT + 1 },
|
|
230
295
|
);
|
|
296
|
+
const truncated = fetchedRows.length > OVERVIEW_ROW_LIMIT;
|
|
297
|
+
const rows = truncated ? fetchedRows.slice(0, OVERVIEW_ROW_LIMIT) : fetchedRows;
|
|
298
|
+
if (truncated) {
|
|
299
|
+
ctx.log?.warn("template overview: row limit reached, result is incomplete", {
|
|
300
|
+
limit: OVERVIEW_ROW_LIMIT,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
231
303
|
|
|
232
|
-
const
|
|
304
|
+
const visibleRows = rows.filter((row) => !isHiddenSystemDraft(query.user, row));
|
|
305
|
+
const { drafts, layoutSlugByDocument } = buildDrafts(
|
|
306
|
+
groupBySlugAndKind(visibleRows),
|
|
307
|
+
query.user,
|
|
308
|
+
);
|
|
309
|
+
const effectiveLayouts = effectiveMailLayoutSlugs(visibleRows);
|
|
233
310
|
const overviewRows = drafts.map((draft) =>
|
|
234
|
-
withUsage(draft, layoutSlugByDocument, ctx.registry),
|
|
311
|
+
withUsage(draft, layoutSlugByDocument, ctx.registry, effectiveLayouts),
|
|
312
|
+
);
|
|
313
|
+
const filtered = filterRows(
|
|
314
|
+
overviewRows,
|
|
315
|
+
payload.search,
|
|
316
|
+
payload.filters ?? [],
|
|
317
|
+
payload.layoutSlug,
|
|
235
318
|
);
|
|
236
|
-
const filtered = filterRows(overviewRows, payload.search, payload.filters ?? []);
|
|
237
319
|
|
|
238
320
|
const sortField: SortField =
|
|
239
321
|
payload.sort !== undefined && isSortField(payload.sort) ? payload.sort : "modifiedAt";
|
|
@@ -245,6 +327,7 @@ export const overviewQuery = defineQueryHandler({
|
|
|
245
327
|
rows: filtered.slice(offset, offset + payload.limit),
|
|
246
328
|
nextCursor: null,
|
|
247
329
|
total: filtered.length,
|
|
330
|
+
truncated,
|
|
248
331
|
} satisfies OverviewResponse;
|
|
249
332
|
},
|
|
250
333
|
});
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type BeforeTransactionResult,
|
|
3
|
+
defineWriteHandler,
|
|
4
|
+
type WriteResult,
|
|
5
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
2
6
|
import {
|
|
3
7
|
NotFoundError,
|
|
4
8
|
UnprocessableError,
|
|
@@ -13,11 +17,14 @@ import {
|
|
|
13
17
|
import { attachmentFileNameSchema } from "../../shared/index.js";
|
|
14
18
|
import { TemplateResolverErrors } from "../qualified-names.js";
|
|
15
19
|
import {
|
|
20
|
+
type PreparedTemplateFile,
|
|
21
|
+
prepareTemplateAttachmentBeforeTransaction,
|
|
22
|
+
prepareTemplateResourceBeforeTransaction,
|
|
16
23
|
readTemplateResourceMaxBytes,
|
|
17
24
|
type StoredTemplateAttachment,
|
|
18
25
|
type StoredTemplateResource,
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
storePreparedTemplateAttachment,
|
|
27
|
+
storePreparedTemplateResource,
|
|
21
28
|
} from "../store-template-resource.js";
|
|
22
29
|
import { RESOURCE_PURPOSES, resourceKeySchema, resourceTargetTenantId } from "./resource-shared.js";
|
|
23
30
|
|
|
@@ -66,10 +73,7 @@ export const resourceUploadWrite = defineWriteHandler({
|
|
|
66
73
|
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
67
74
|
description:
|
|
68
75
|
"Turns a file uploaded via POST /api/files (multipart, no entityType) into a template resource. With purpose image (default) the upload is checked against the resource-max-bytes cap, re-encoded to a supported image format and stored as a new file; with templateId and key it is linked into that template under the key, without them only the file is created. With purpose attachment, templateId, key and fileName are required: a PDF, PNG or JPEG is stored unchanged and linked into a mail template as a file attached to every mail sent from it, at most 10 per template. A cap above the files route's maxUploadSize has no effect.",
|
|
69
|
-
|
|
70
|
-
event,
|
|
71
|
-
ctx,
|
|
72
|
-
): Promise<WriteResult<StoredTemplateResource | StoredTemplateAttachment>> => {
|
|
76
|
+
beforeTransaction: async (event, ctx): Promise<BeforeTransactionResult<PreparedTemplateFile>> => {
|
|
73
77
|
const { templateId, key, sourceFileRefId, purpose, fileName } = event.payload;
|
|
74
78
|
const attachmentTarget =
|
|
75
79
|
purpose === "attachment" ? attachmentTargetOf({ templateId, key, fileName }) : null;
|
|
@@ -91,7 +95,7 @@ export const resourceUploadWrite = defineWriteHandler({
|
|
|
91
95
|
try {
|
|
92
96
|
source = await readFileRefContent({
|
|
93
97
|
db: ctx.db,
|
|
94
|
-
filesForTenant: (tid) => fileContextForTenant(ctx, tid),
|
|
98
|
+
filesForTenant: (tid) => fileContextForTenant(ctx, ctx.user.tenantId, tid),
|
|
95
99
|
fileRefId: sourceFileRefId,
|
|
96
100
|
allowedTenantIds: [event.user.tenantId],
|
|
97
101
|
unclaimedUploadOf: event.user.id,
|
|
@@ -113,7 +117,7 @@ export const resourceUploadWrite = defineWriteHandler({
|
|
|
113
117
|
// The source upload stays as an unclaimed file: bundled-features cannot reach the fileRef executor to delete it.
|
|
114
118
|
const tenantId = resourceTargetTenantId(event.payload.tenantIdOverride, event.user.tenantId);
|
|
115
119
|
if (attachmentTarget !== null) {
|
|
116
|
-
return
|
|
120
|
+
return prepareTemplateAttachmentBeforeTransaction({
|
|
117
121
|
ctx,
|
|
118
122
|
user: event.user,
|
|
119
123
|
tenantId,
|
|
@@ -122,7 +126,7 @@ export const resourceUploadWrite = defineWriteHandler({
|
|
|
122
126
|
...attachmentTarget,
|
|
123
127
|
});
|
|
124
128
|
}
|
|
125
|
-
return
|
|
129
|
+
return prepareTemplateResourceBeforeTransaction({
|
|
126
130
|
ctx,
|
|
127
131
|
user: event.user,
|
|
128
132
|
tenantId,
|
|
@@ -132,4 +136,39 @@ export const resourceUploadWrite = defineWriteHandler({
|
|
|
132
136
|
...(key !== undefined && { key }),
|
|
133
137
|
});
|
|
134
138
|
},
|
|
139
|
+
handler: async (
|
|
140
|
+
event,
|
|
141
|
+
ctx,
|
|
142
|
+
): Promise<WriteResult<StoredTemplateResource | StoredTemplateAttachment>> => {
|
|
143
|
+
const { templateId, key } = event.payload;
|
|
144
|
+
const tenantId = resourceTargetTenantId(event.payload.tenantIdOverride, event.user.tenantId);
|
|
145
|
+
const { prepared } = event;
|
|
146
|
+
if (prepared.kind === "attachment") {
|
|
147
|
+
// The phase accepts attachments only with a complete target, so these are set.
|
|
148
|
+
if (templateId === undefined || key === undefined) {
|
|
149
|
+
return writeFailure(
|
|
150
|
+
new UnprocessableError(TemplateResolverErrors.attachmentInvalid, {
|
|
151
|
+
i18nKey: "templateResolver.errors.attachment.invalid",
|
|
152
|
+
details: { code: "target_required" },
|
|
153
|
+
}),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return storePreparedTemplateAttachment({
|
|
157
|
+
ctx,
|
|
158
|
+
user: event.user,
|
|
159
|
+
tenantId,
|
|
160
|
+
prepared,
|
|
161
|
+
templateId,
|
|
162
|
+
key,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return storePreparedTemplateResource({
|
|
166
|
+
ctx,
|
|
167
|
+
user: event.user,
|
|
168
|
+
tenantId,
|
|
169
|
+
prepared,
|
|
170
|
+
...(templateId !== undefined && { templateId }),
|
|
171
|
+
...(key !== undefined && { key }),
|
|
172
|
+
});
|
|
173
|
+
},
|
|
135
174
|
});
|
|
@@ -28,6 +28,11 @@ function testMailFailed(code: "channel_error" | "not_sent") {
|
|
|
28
28
|
);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function errorCodeOf(error: unknown): string | undefined {
|
|
32
|
+
const code = error instanceof Error ? Reflect.get(error, "code") : undefined;
|
|
33
|
+
return typeof code === "string" ? code : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
31
36
|
// The only recipient is the caller's own address, resolved server-side, so the handler can never mail a third party.
|
|
32
37
|
export const sendTestWrite = defineWriteHandler({
|
|
33
38
|
name: "send-test",
|
|
@@ -37,18 +42,24 @@ export const sendTestWrite = defineWriteHandler({
|
|
|
37
42
|
content: z.string().max(200_000).optional(),
|
|
38
43
|
meta: z.unknown().optional(),
|
|
39
44
|
variableSchema: z.record(z.string(), z.unknown()).default({}),
|
|
45
|
+
includeAttachments: z.boolean().default(true),
|
|
46
|
+
prefixSubject: z.boolean().default(true),
|
|
40
47
|
}),
|
|
41
48
|
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
42
49
|
rateLimit: { per: "user+handler", ...TEST_MAIL_RATE_LIMIT },
|
|
43
50
|
description:
|
|
44
|
-
"Sends the rendered preview of a mail template draft, or of the stored template when no content is given, to the calling user's own address with the subject prefixed by [TEST], carrying the inline images and the files attached to the template; there is no recipient input, the sample values come from the declared variable examples, and a draft that fails the write rules is rejected without sending.",
|
|
51
|
+
"Sends the rendered preview of a mail template draft, or of the stored template when no content is given, to the calling user's own address with the subject prefixed by [TEST] unless prefixSubject is false, carrying the inline images and, unless includeAttachments is false, the files attached to the template; there is no recipient input, the sample values come from the declared variable examples, and a draft that fails the write rules is rejected without sending.",
|
|
45
52
|
handler: async (event, ctx) => {
|
|
53
|
+
const { includeAttachments, prefixSubject, ...previewPayload } = event.payload;
|
|
46
54
|
const outcome = await renderTemplatePreview(ctx, event.user, {
|
|
47
|
-
...
|
|
55
|
+
...previewPayload,
|
|
48
56
|
kind: "mail",
|
|
49
57
|
});
|
|
50
58
|
if (!outcome.ok) return writeFailure(outcome.error);
|
|
51
|
-
const { preview,
|
|
59
|
+
const { preview, sender } = outcome;
|
|
60
|
+
const mailAttachments = includeAttachments
|
|
61
|
+
? outcome.mailAttachments
|
|
62
|
+
: outcome.mailAttachments.filter((ref) => ref.kind === "inline");
|
|
52
63
|
|
|
53
64
|
const channel = emailChannelOf(ctx.registry);
|
|
54
65
|
if (channel === undefined) {
|
|
@@ -69,7 +80,7 @@ export const sendTestWrite = defineWriteHandler({
|
|
|
69
80
|
ctx.registry,
|
|
70
81
|
event.user.tenantId,
|
|
71
82
|
ctx.config,
|
|
72
|
-
(tenantId) => fileContextForTenant(ctx, tenantId),
|
|
83
|
+
(tenantId) => fileContextForTenant(ctx, ctx.user.tenantId, tenantId),
|
|
73
84
|
),
|
|
74
85
|
};
|
|
75
86
|
const address = (await channel.resolve?.(event.user.id, channelContext)) ?? null;
|
|
@@ -81,8 +92,8 @@ export const sendTestWrite = defineWriteHandler({
|
|
|
81
92
|
);
|
|
82
93
|
}
|
|
83
94
|
|
|
84
|
-
const subject = `${TEST_MAIL_SUBJECT_PREFIX}${preview.subject ?? ""}`;
|
|
85
|
-
let
|
|
95
|
+
const subject = `${prefixSubject ? TEST_MAIL_SUBJECT_PREFIX : ""}${preview.subject ?? ""}`;
|
|
96
|
+
let sentStatus: string;
|
|
86
97
|
try {
|
|
87
98
|
const result = await channel.send(
|
|
88
99
|
address,
|
|
@@ -94,13 +105,31 @@ export const sendTestWrite = defineWriteHandler({
|
|
|
94
105
|
locale: event.payload.locale,
|
|
95
106
|
},
|
|
96
107
|
channelContext,
|
|
97
|
-
{
|
|
108
|
+
{
|
|
109
|
+
subject,
|
|
110
|
+
html: preview.html,
|
|
111
|
+
text: preview.text ?? "",
|
|
112
|
+
...sender,
|
|
113
|
+
attachments: mailAttachments,
|
|
114
|
+
},
|
|
98
115
|
);
|
|
99
|
-
|
|
100
|
-
} catch {
|
|
116
|
+
sentStatus = result.status;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
// Class and code only: the message can carry the address or a provider response body.
|
|
119
|
+
ctx.log?.warn("template test mail: channel threw", {
|
|
120
|
+
tenantId: event.user.tenantId,
|
|
121
|
+
errorClass: error instanceof Error ? error.name : typeof error,
|
|
122
|
+
errorCode: errorCodeOf(error),
|
|
123
|
+
});
|
|
101
124
|
return testMailFailed("channel_error");
|
|
102
125
|
}
|
|
103
|
-
if (
|
|
126
|
+
if (sentStatus !== "sent") {
|
|
127
|
+
ctx.log?.warn("template test mail: channel did not send", {
|
|
128
|
+
tenantId: event.user.tenantId,
|
|
129
|
+
status: sentStatus,
|
|
130
|
+
});
|
|
131
|
+
return testMailFailed("not_sent");
|
|
132
|
+
}
|
|
104
133
|
return { isSuccess: true as const, data: { subject, checks: preview.checks } };
|
|
105
134
|
},
|
|
106
135
|
});
|