@cosmicdrift/kumiko-bundled-features 0.363.0 → 0.364.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/dist/audit/handlers/list.query.js +9 -5
- package/dist/auth-email-password/bootstrap.d.ts +8 -5
- package/dist/auth-email-password/bootstrap.js +12 -4
- package/dist/auth-email-password/email-rate-limits.d.ts +4 -0
- package/dist/auth-email-password/email-rate-limits.js +13 -0
- package/dist/auth-email-password/handlers/change-password.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/change-password.write.js +6 -2
- package/dist/auth-email-password/handlers/confirm-account-unlock.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/invite-accept-with-login.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/invite-accept.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/invite-create.write.d.ts +2 -2
- package/dist/auth-email-password/handlers/invite-signup-complete.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/login.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/logout.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/logout.write.js +6 -2
- package/dist/auth-email-password/handlers/request-account-unlock.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/request-email-verification.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/request-password-reset.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/reset-password.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/signup-confirm.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/signup-request.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/signup-request.write.js +2 -1
- package/dist/auth-email-password/handlers/switch-tenant-mfa-gate.write.d.ts +1 -1
- package/dist/auth-email-password/handlers/token-request-handler.d.ts +1 -1
- package/dist/auth-email-password/handlers/token-request-handler.js +2 -1
- package/dist/auth-email-password/handlers/verify-email.write.d.ts +1 -1
- package/dist/auth-email-password/web/auth-gate.js +16 -4
- package/dist/auth-mfa/handlers/disable.write.d.ts +1 -1
- package/dist/auth-mfa/handlers/enable-confirm-preauth.write.d.ts +1 -1
- package/dist/auth-mfa/handlers/enable-confirm.write.d.ts +1 -1
- package/dist/auth-mfa/handlers/enable-start-preauth.write.d.ts +1 -1
- package/dist/auth-mfa/handlers/enable-start.write.d.ts +1 -1
- package/dist/auth-mfa/handlers/regenerate-recovery.write.d.ts +1 -1
- package/dist/auth-mfa/handlers/verify.write.d.ts +1 -1
- package/dist/billing-foundation/consumer-protection/confirmation-mail.js +52 -5
- package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +3 -0
- package/dist/billing-foundation/consumer-protection/consent-gate.js +21 -2
- package/dist/billing-foundation/consumer-protection/mail-notifications.js +1 -1
- package/dist/billing-foundation/consumer-protection/register-termination.js +11 -1
- package/dist/billing-foundation/consumer-protection/termination-mail.d.ts +5 -3
- package/dist/billing-foundation/consumer-protection/termination-mail.js +1 -2
- package/dist/billing-foundation/consumer-protection/termination-receipt-limit.d.ts +6 -0
- package/dist/billing-foundation/consumer-protection/termination-receipt-limit.js +34 -0
- package/dist/billing-foundation/consumer-protection/termination-record.d.ts +2 -0
- package/dist/billing-foundation/consumer-protection/termination-record.js +7 -1
- package/dist/billing-foundation/consumer-protection/termination-request.js +30 -20
- package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +1 -1
- package/dist/billing-foundation/consumer-protection/termination-texts.js +6 -0
- package/dist/billing-foundation/consumer-protection/withdrawal-period.d.ts +10 -0
- package/dist/billing-foundation/consumer-protection/withdrawal-period.js +26 -0
- package/dist/billing-foundation/handlers/create-checkout-session.write.js +11 -5
- package/dist/billing-foundation/handlers/process-event.write.js +10 -6
- package/dist/billing-foundation/i18n.js +3 -0
- package/dist/billing-foundation/subscription-tier-sync.d.ts +7 -1
- package/dist/billing-foundation/subscription-tier-sync.js +13 -5
- package/dist/billing-foundation/types.d.ts +7 -2
- package/dist/cap-counter/calendar-cap-usage-reservation.js +2 -0
- package/dist/cap-counter/enforce-cap.d.ts +18 -1
- package/dist/cap-counter/enforce-cap.js +49 -6
- package/dist/cap-counter/index.d.ts +1 -1
- package/dist/cap-counter/index.js +1 -1
- package/dist/cap-counter/with-cap-enforcement.d.ts +2 -0
- package/dist/channel-email/email-channel.js +16 -4
- package/dist/channel-in-app/handlers/mark-all-read.write.d.ts +1 -1
- package/dist/channel-in-app/handlers/mark-read.write.d.ts +1 -1
- package/dist/compliance-profiles/handlers/set-profile.write.d.ts +1 -1
- package/dist/config/handlers/reset.write.d.ts +1 -1
- package/dist/config/handlers/set.write.d.ts +1 -1
- package/dist/config/handlers/set.write.js +2 -2
- package/dist/config/resolver.d.ts +1 -0
- package/dist/config/resolver.js +11 -14
- package/dist/config/write-helpers.d.ts +2 -0
- package/dist/config/write-helpers.js +19 -0
- package/dist/crypto-shredding/handlers/forget-subject.write.d.ts +1 -1
- package/dist/crypto-shredding/handlers/forget-subject.write.js +29 -19
- package/dist/custom-fields/handlers/update-tenant-field.write.js +0 -1
- package/dist/delivery/attachment-ref.d.ts +2 -0
- package/dist/delivery/attachment-ref.js +14 -0
- package/dist/delivery/channel-context.js +19 -1
- package/dist/delivery/handlers/resubscribe-address.write.d.ts +1 -1
- package/dist/delivery/handlers/resubscribe-user.write.d.ts +1 -1
- package/dist/delivery/handlers/set-preference.write.d.ts +1 -1
- package/dist/delivery/handlers/unsubscribe-address.write.d.ts +1 -1
- package/dist/delivery/handlers/unsubscribe-user.write.d.ts +1 -1
- package/dist/delivery/index.d.ts +1 -1
- package/dist/delivery/index.js +1 -1
- package/dist/delivery/jobs.js +43 -11
- package/dist/delivery/types.d.ts +7 -1
- package/dist/feature-toggles/handlers/set.write.d.ts +1 -1
- package/dist/file-derivatives/handlers/public-variant.query.js +1 -1
- package/dist/file-foundation/read-file-ref-content.d.ts +1 -1
- package/dist/file-foundation/read-file-ref-content.js +6 -4
- package/dist/files-tenant-data/hooks.js +4 -0
- package/dist/form-draft/handlers/discard.write.d.ts +1 -1
- package/dist/form-draft/handlers/save.write.d.ts +1 -1
- package/dist/inbound-mail-foundation/oauth-access-token.js +8 -3
- package/dist/inbound-mail-foundation/watch-supervisor.js +3 -1
- package/dist/jobs/db/queries/stale-run-sweep.d.ts +1 -1
- package/dist/jobs/db/queries/stale-run-sweep.js +12 -5
- package/dist/jobs/feature.js +5 -3
- package/dist/jobs/handlers/list.query.js +3 -1
- package/dist/jobs/handlers/retry.write.d.ts +1 -1
- package/dist/jobs/handlers/stale-run-sweep.job.d.ts +1 -1
- package/dist/jobs/handlers/stale-run-sweep.job.js +2 -2
- package/dist/jobs/handlers/trigger.write.d.ts +1 -1
- package/dist/jobs/job-run-logger.js +32 -10
- package/dist/jobs/tenant-job-run-table.js +3 -2
- package/dist/ledger/entity.js +3 -1
- package/dist/managed-pages/handlers/set.write.d.ts +1 -1
- package/dist/personal-access-tokens/handlers/create.write.d.ts +1 -1
- package/dist/personal-access-tokens/handlers/revoke.write.d.ts +1 -1
- package/dist/renderer-foundation/types.d.ts +2 -0
- package/dist/renderer-pdf/render-table-pdf.d.ts +27 -33
- package/dist/renderer-pdf/render-table-pdf.js +95 -34
- package/dist/secrets/feature.js +4 -3
- package/dist/secrets/handlers/delete.write.d.ts +1 -1
- package/dist/secrets/handlers/set.write.d.ts +1 -1
- package/dist/secrets/secrets-context.js +15 -3
- package/dist/sessions/handlers/revoke-all-for-user.write.d.ts +1 -1
- package/dist/sessions/handlers/revoke-all-others.write.d.ts +1 -1
- package/dist/sessions/handlers/revoke.write.d.ts +1 -1
- package/dist/shared/chunked-entity-migration.d.ts +1 -0
- package/dist/shared/chunked-entity-migration.js +1 -1
- package/dist/shared/mail-template-body.d.ts +3 -0
- package/dist/shared/mail-template-body.js +10 -0
- package/dist/subscription-stripe/plugin-methods.js +18 -8
- package/dist/subscription-stripe/verify-webhook.d.ts +1 -1
- package/dist/template-resolver/api.js +3 -2
- package/dist/template-resolver/compile-document-body.d.ts +4 -0
- package/dist/template-resolver/compile-document-body.js +58 -16
- package/dist/template-resolver/constants.d.ts +2 -2
- package/dist/template-resolver/constants.js +3 -1
- package/dist/template-resolver/document-schema.d.ts +1 -1
- package/dist/template-resolver/document-schema.js +23 -3
- package/dist/template-resolver/feature.d.ts +2 -0
- package/dist/template-resolver/feature.js +4 -1
- package/dist/template-resolver/handlers/collection-set.write.d.ts +1 -1
- package/dist/template-resolver/handlers/copy-locale.write.d.ts +1 -1
- package/dist/template-resolver/handlers/copy-locale.write.js +29 -11
- package/dist/template-resolver/handlers/diff-to-system.query.js +4 -38
- package/dist/template-resolver/handlers/discard-draft.write.d.ts +13 -0
- package/dist/template-resolver/handlers/discard-draft.write.js +37 -0
- package/dist/template-resolver/handlers/export-bundle.query.js +51 -1
- package/dist/template-resolver/handlers/find-by-id.query.d.ts +1 -0
- package/dist/template-resolver/handlers/find-by-id.query.js +6 -3
- package/dist/template-resolver/handlers/import-bundle-system.write.d.ts +1 -1
- package/dist/template-resolver/handlers/import-bundle.write.d.ts +1 -1
- package/dist/template-resolver/handlers/layout-source.query.d.ts +5 -0
- package/dist/template-resolver/handlers/layout-source.query.js +46 -23
- package/dist/template-resolver/handlers/list.query.js +4 -1
- package/dist/template-resolver/handlers/override-diff.d.ts +20 -0
- package/dist/template-resolver/handlers/override-diff.js +41 -0
- package/dist/template-resolver/handlers/overview.query.d.ts +6 -0
- package/dist/template-resolver/handlers/overview.query.js +51 -11
- package/dist/template-resolver/handlers/reset-to-system.write.d.ts +1 -1
- package/dist/template-resolver/handlers/resource-link-pdf.write.d.ts +1 -1
- package/dist/template-resolver/handlers/resource-remove.write.d.ts +1 -1
- package/dist/template-resolver/handlers/resource-upload.write.d.ts +1 -1
- package/dist/template-resolver/handlers/resource-upload.write.js +1 -1
- package/dist/template-resolver/handlers/send-test.write.d.ts +3 -1
- package/dist/template-resolver/handlers/send-test.write.js +37 -10
- package/dist/template-resolver/handlers/set.write.d.ts +1 -1
- package/dist/template-resolver/handlers/template-gates.js +13 -2
- package/dist/template-resolver/handlers/template-write.js +36 -4
- package/dist/template-resolver/handlers/tenant-overrides.query.d.ts +32 -0
- package/dist/template-resolver/handlers/tenant-overrides.query.js +80 -16
- package/dist/template-resolver/handlers/toggle-status.write.d.ts +17 -4
- package/dist/template-resolver/handlers/toggle-status.write.js +35 -19
- package/dist/template-resolver/handlers/upsert-system.write.d.ts +1 -1
- package/dist/template-resolver/handlers/upsert-tenant.write.d.ts +1 -1
- package/dist/template-resolver/handlers/upsert-tenant.write.js +1 -1
- package/dist/template-resolver/handlers/usage.query.js +11 -2
- package/dist/template-resolver/index.d.ts +4 -3
- package/dist/template-resolver/index.js +4 -3
- package/dist/template-resolver/layout-appearance.d.ts +10 -0
- package/dist/template-resolver/layout-appearance.js +26 -0
- package/dist/template-resolver/pdf-constants.d.ts +7 -0
- package/dist/template-resolver/pdf-constants.js +8 -0
- package/dist/template-resolver/qualified-names.d.ts +4 -0
- package/dist/template-resolver/qualified-names.js +4 -0
- package/dist/template-resolver/seeding.js +21 -4
- package/dist/template-resolver/store-template-resource.js +2 -2
- package/dist/template-resolver/table.d.ts +103 -0
- package/dist/template-resolver/table.js +9 -2
- package/dist/template-resolver/template-draft.d.ts +51 -0
- package/dist/template-resolver/template-draft.js +72 -0
- package/dist/template-resolver/template-meta.d.ts +30 -5
- package/dist/template-resolver/template-meta.js +60 -16
- package/dist/template-resolver/template-preview.d.ts +19 -2
- package/dist/template-resolver/template-preview.js +98 -54
- package/dist/template-resolver/template-resources.d.ts +5 -0
- package/dist/template-resolver/template-resources.js +35 -4
- package/dist/template-resolver/template-usage.d.ts +5 -1
- package/dist/template-resolver/template-usage.js +14 -9
- package/dist/template-resolver/template-validation.js +1 -0
- package/dist/template-resolver/web/client-plugin.js +4 -2
- package/dist/template-resolver/web/document-editor-extensions.d.ts +4 -1
- package/dist/template-resolver/web/document-editor-extensions.js +50 -12
- package/dist/template-resolver/web/document-editor.d.ts +2 -1
- package/dist/template-resolver/web/document-editor.js +8 -2
- package/dist/template-resolver/web/document-tiptap-editor.d.ts +1 -1
- package/dist/template-resolver/web/document-tiptap-editor.js +144 -28
- package/dist/template-resolver/web/document-toolbar-blocks.d.ts +8 -0
- package/dist/template-resolver/web/document-toolbar-blocks.js +4 -0
- package/dist/template-resolver/web/i18n.js +19 -4
- package/dist/template-resolver/web/index.d.ts +1 -0
- package/dist/template-resolver/web/index.js +1 -0
- package/dist/template-resolver/web/placeholder-line-editor.d.ts +3 -1
- package/dist/template-resolver/web/placeholder-line-editor.js +6 -6
- package/dist/tenant/handlers/add-member.write.d.ts +1 -1
- package/dist/tenant/handlers/cancel-invitation.write.d.ts +1 -1
- package/dist/tenant/handlers/create.write.d.ts +1 -1
- package/dist/tenant/handlers/remove-member.write.d.ts +1 -1
- package/dist/tenant/handlers/toggle-enabled.write.d.ts +2 -2
- package/dist/tenant/handlers/update-member-roles.write.d.ts +1 -1
- package/dist/tenant/handlers/update.write.d.ts +1 -1
- package/dist/tenant-handover/handlers/claim.write.d.ts +1 -1
- package/dist/tenant-lifecycle/handlers/cancel-destruction.write.d.ts +1 -1
- package/dist/tenant-lifecycle/handlers/request-destruction.write.d.ts +1 -1
- package/dist/tier-engine/feature.js +9 -3
- package/dist/tier-engine/handlers/set-tenant-tier.write.d.ts +1 -1
- package/dist/user/handlers/create.write.d.ts +1 -1
- package/dist/user/handlers/update.write.d.ts +1 -1
- package/dist/user/resolve-display-names.js +5 -9
- package/dist/user/seeding.js +20 -1
- package/dist/user-data-rights/handlers/cancel-deletion.write.d.ts +1 -1
- package/dist/user-data-rights/handlers/confirm-deletion-by-token.write.d.ts +1 -1
- package/dist/user-data-rights/handlers/lift-restriction.write.d.ts +1 -1
- package/dist/user-data-rights/handlers/request-deletion-by-email.write.d.ts +1 -1
- package/dist/user-data-rights/handlers/request-deletion.write.d.ts +1 -1
- package/dist/user-data-rights/handlers/request-export.write.d.ts +1 -1
- package/dist/user-data-rights/handlers/restrict-account.write.d.ts +1 -1
- package/dist/user-data-rights/handlers/run-forget-cleanup.write.d.ts +1 -1
- package/dist/user-profile/handlers/change-email.write.d.ts +1 -1
- package/dist/waitlist/handlers/invite.write.d.ts +1 -1
- package/dist/waitlist/handlers/reject.write.d.ts +1 -1
- package/dist/waitlist/handlers/submit.write.d.ts +1 -1
- package/dist/waitlist/options.js +1 -1
- package/dist/workflow-runner/event-trigger.js +2 -2
- package/dist/workflow-runner/handlers/resume-run.write.js +7 -2
- package/package.json +19 -10
- package/src/audit/changes.json +6 -0
- package/src/auth-email-password/changes.json +37 -0
- package/src/billing-foundation/changes.json +36 -0
- package/src/cap-counter/changes.json +6 -0
- package/src/config/changes.json +6 -0
- package/src/crypto-shredding/changes.json +6 -0
- package/src/custom-fields/changes.json +6 -0
- package/src/delivery/changes.json +18 -0
- package/src/file-foundation/changes.json +6 -0
- package/src/files-tenant-data/changes.json +6 -0
- package/src/inbound-mail-foundation/changes.json +6 -0
- package/src/jobs/changes.json +19 -0
- package/src/ledger/changes.json +7 -0
- package/src/renderer-pdf/changes.json +12 -0
- package/src/secrets/changes.json +6 -0
- package/src/shared/changes.json +6 -0
- package/src/subscription-stripe/changes.json +6 -0
- package/src/template-resolver/changes.json +68 -0
- package/src/tier-engine/changes.json +6 -0
- package/src/user/changes.json +6 -0
- package/src/user-data-rights/changes.json +6 -0
- package/src/user-profile/changes.json +1 -1
- package/src/workflow-runner/changes.json +6 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { diffTemplateBlocks, splitTemplateBlocks } from "../template-diff.js";
|
|
2
|
+
import { servedVersionOf } from "../template-draft.js";
|
|
3
|
+
function parseStoredMeta(raw) {
|
|
4
|
+
if (raw === null)
|
|
5
|
+
return {};
|
|
6
|
+
try {
|
|
7
|
+
const parsed = JSON.parse(raw);
|
|
8
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
9
|
+
? Object.fromEntries(Object.entries(parsed))
|
|
10
|
+
: {};
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function diffMeta(system, tenant) {
|
|
17
|
+
const keys = [...new Set([...Object.keys(system), ...Object.keys(tenant)])].sort();
|
|
18
|
+
return keys
|
|
19
|
+
.filter((key) => JSON.stringify(system[key]) !== JSON.stringify(tenant[key]))
|
|
20
|
+
.map((key) => ({ key, tenant: tenant[key], system: system[key] }));
|
|
21
|
+
}
|
|
22
|
+
// Shared by the tenant's own diff and the SystemAdmin's per-tenant diff in the overrides drawer.
|
|
23
|
+
export function buildOverrideDiff(tenantRow, systemRow) {
|
|
24
|
+
const blocks = diffTemplateBlocks(splitTemplateBlocks(systemRow.content ?? "", systemRow.contentFormat), splitTemplateBlocks(tenantRow.content ?? "", tenantRow.contentFormat));
|
|
25
|
+
const meta = diffMeta(parseStoredMeta(systemRow.meta), parseStoredMeta(tenantRow.meta));
|
|
26
|
+
return {
|
|
27
|
+
tenant: {
|
|
28
|
+
templateId: String(tenantRow.id),
|
|
29
|
+
templateVersion: servedVersionOf(tenantRow),
|
|
30
|
+
contentFormat: tenantRow.contentFormat,
|
|
31
|
+
},
|
|
32
|
+
system: {
|
|
33
|
+
templateId: String(systemRow.id),
|
|
34
|
+
templateVersion: servedVersionOf(systemRow),
|
|
35
|
+
contentFormat: systemRow.contentFormat,
|
|
36
|
+
},
|
|
37
|
+
identical: meta.length === 0 && blocks.every((block) => block.change === "same"),
|
|
38
|
+
blocks,
|
|
39
|
+
meta,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
import { type TemplateStatus } from "../constants.js";
|
|
3
|
+
import { type TemplateResourceRow } from "../table.js";
|
|
4
|
+
import { type EffectiveMailLayoutSlugs } from "../template-usage.js";
|
|
3
5
|
declare const OVERVIEW_KINDS: readonly ["mail", "document-pdf", "mail-layout", "pdf-layout"];
|
|
4
6
|
type OverviewKind = (typeof OVERVIEW_KINDS)[number];
|
|
5
7
|
export type OverviewOrigin = "tenant" | "system";
|
|
@@ -20,6 +22,8 @@ export type OverviewRow = {
|
|
|
20
22
|
readonly locales: readonly string[];
|
|
21
23
|
readonly status: TemplateStatus;
|
|
22
24
|
readonly version: number;
|
|
25
|
+
readonly draftVersion: number | null;
|
|
26
|
+
readonly activeSince: string | null;
|
|
23
27
|
readonly systemVersion: number | null;
|
|
24
28
|
readonly layoutSlug: string | null;
|
|
25
29
|
readonly modifiedAt: string;
|
|
@@ -31,6 +35,7 @@ export type OverviewResponse = {
|
|
|
31
35
|
readonly nextCursor: null;
|
|
32
36
|
readonly total: number;
|
|
33
37
|
};
|
|
38
|
+
export declare function effectiveMailLayoutSlugs(rows: readonly TemplateResourceRow[]): EffectiveMailLayoutSlugs;
|
|
34
39
|
export declare const overviewQuery: import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"overview", z.ZodObject<{
|
|
35
40
|
search: z.ZodOptional<z.ZodString>;
|
|
36
41
|
filters: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -43,6 +48,7 @@ export declare const overviewQuery: import("@cosmicdrift/kumiko-types/define-han
|
|
|
43
48
|
op: z.ZodLiteral<"in">;
|
|
44
49
|
value: z.ZodArray<z.ZodString>;
|
|
45
50
|
}, z.core.$strip>>>;
|
|
51
|
+
layoutSlug: z.ZodOptional<z.ZodString>;
|
|
46
52
|
sort: z.ZodOptional<z.ZodString>;
|
|
47
53
|
sortDirection: z.ZodOptional<z.ZodEnum<{
|
|
48
54
|
asc: "asc";
|
|
@@ -3,6 +3,7 @@ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
|
3
3
|
import * as z from "zod";
|
|
4
4
|
import { DEFAULT_MAIL_LAYOUT_SLUG, FALLBACK_LOCALE } from "../constants.js";
|
|
5
5
|
import { templateResourcesTable } from "../table.js";
|
|
6
|
+
import { activeSinceOf, activeVersionOf, draftViewerTenantId, isHiddenSystemDraft, parseTemplateDraft, servedVersionOf, } from "../template-draft.js";
|
|
6
7
|
import { collectTemplateUsage } from "../template-usage.js";
|
|
7
8
|
import { storedRecord } from "./template-gates.js";
|
|
8
9
|
const OVERVIEW_KINDS = ["mail", "document-pdf", "mail-layout", "pdf-layout"];
|
|
@@ -41,6 +42,26 @@ function effectiveRowsOf(rows) {
|
|
|
41
42
|
}
|
|
42
43
|
return tenantRows.length > 0 ? { origin: "tenant", rows: tenantRows } : null;
|
|
43
44
|
}
|
|
45
|
+
export function effectiveMailLayoutSlugs(rows) {
|
|
46
|
+
const byLocale = new Map();
|
|
47
|
+
for (const row of rows) {
|
|
48
|
+
if (row.kind !== "mail")
|
|
49
|
+
continue;
|
|
50
|
+
const key = `${row.slug}\u0000${row.locale}`;
|
|
51
|
+
byLocale.set(key, [...(byLocale.get(key) ?? []), row]);
|
|
52
|
+
}
|
|
53
|
+
const bySlug = new Map();
|
|
54
|
+
for (const group of byLocale.values()) {
|
|
55
|
+
const effective = effectiveRowsOf(group)?.rows[0];
|
|
56
|
+
// All-archived: the renderer falls back to the code-registered template.
|
|
57
|
+
if (effective === undefined || effective.status === "archived")
|
|
58
|
+
continue;
|
|
59
|
+
const locales = bySlug.get(effective.slug) ?? new Map();
|
|
60
|
+
locales.set(effective.locale, layoutSlugOf(effective));
|
|
61
|
+
bySlug.set(effective.slug, locales);
|
|
62
|
+
}
|
|
63
|
+
return bySlug;
|
|
64
|
+
}
|
|
44
65
|
function modifiedAtOf(row) {
|
|
45
66
|
return (row.modifiedAt ?? row.insertedAt).toString();
|
|
46
67
|
}
|
|
@@ -66,7 +87,23 @@ function groupBySlugAndKind(rows) {
|
|
|
66
87
|
}
|
|
67
88
|
return groups;
|
|
68
89
|
}
|
|
69
|
-
|
|
90
|
+
// Another tenant's draft, including a system draft seen by a tenant, is never announced.
|
|
91
|
+
function draftVersionFor(user, row) {
|
|
92
|
+
const isOwnRow = row.tenantId === draftViewerTenantId(user, row);
|
|
93
|
+
return isOwnRow && parseTemplateDraft(row.draft) !== null ? row.version : null;
|
|
94
|
+
}
|
|
95
|
+
// Version columns of one overview row: the served version, a waiting draft and the system version it overrides.
|
|
96
|
+
function versionFieldsOf(user, preferred, groupRows) {
|
|
97
|
+
const isActive = preferred.status === "active";
|
|
98
|
+
const systemSameLocale = groupRows.find((row) => row.scope === "system" && row.locale === preferred.locale);
|
|
99
|
+
return {
|
|
100
|
+
version: servedVersionOf(preferred),
|
|
101
|
+
draftVersion: draftVersionFor(user, preferred),
|
|
102
|
+
activeSince: isActive ? activeSinceOf(preferred).toString() : null,
|
|
103
|
+
systemVersion: systemSameLocale === undefined ? null : activeVersionOf(systemSameLocale),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function buildDrafts(groups, user) {
|
|
70
107
|
const layoutSlugByDocument = new Map();
|
|
71
108
|
const drafts = [];
|
|
72
109
|
for (const [id, groupRows] of groups) {
|
|
@@ -76,7 +113,6 @@ function buildDrafts(groups) {
|
|
|
76
113
|
continue;
|
|
77
114
|
if (!isOverviewKind(preferred.kind) || !isTemplateStatus(preferred.status))
|
|
78
115
|
continue;
|
|
79
|
-
const systemSameLocale = groupRows.find((row) => row.scope === "system" && row.locale === preferred.locale);
|
|
80
116
|
const isDocument = preferred.kind === "mail" || preferred.kind === "document-pdf";
|
|
81
117
|
const layoutSlug = isDocument ? layoutSlugOf(preferred) : null;
|
|
82
118
|
if (layoutSlug !== null && preferred.kind === "document-pdf") {
|
|
@@ -91,18 +127,17 @@ function buildDrafts(groups) {
|
|
|
91
127
|
origin: effective.origin,
|
|
92
128
|
locales: [...new Set(effective.rows.map((row) => row.locale))].sort(),
|
|
93
129
|
status: preferred.status,
|
|
94
|
-
|
|
95
|
-
systemVersion: systemSameLocale?.version ?? null,
|
|
130
|
+
...versionFieldsOf(user, preferred, groupRows),
|
|
96
131
|
layoutSlug,
|
|
97
132
|
modifiedAt: modifiedAtOf(preferred),
|
|
98
133
|
});
|
|
99
134
|
}
|
|
100
135
|
return { drafts, layoutSlugByDocument };
|
|
101
136
|
}
|
|
102
|
-
function withUsage(draft, layoutSlugByDocument, registry) {
|
|
137
|
+
function withUsage(draft, layoutSlugByDocument, registry, effectiveLayouts) {
|
|
103
138
|
let usedBy = [];
|
|
104
139
|
if (draft.kind === "mail" || draft.kind === "mail-layout") {
|
|
105
|
-
usedBy = collectTemplateUsage(registry, draft.slug, draft.kind).map((entry) => ({
|
|
140
|
+
usedBy = collectTemplateUsage(registry, draft.slug, draft.kind, effectiveLayouts).map((entry) => ({
|
|
106
141
|
qualifiedName: entry.qualifiedName,
|
|
107
142
|
feature: entry.feature,
|
|
108
143
|
notification: entry.notification,
|
|
@@ -120,9 +155,11 @@ function withUsage(draft, layoutSlugByDocument, registry) {
|
|
|
120
155
|
}
|
|
121
156
|
return { ...draft, usedBy, usedByCount: usedBy.length };
|
|
122
157
|
}
|
|
123
|
-
function filterRows(rows, search, filters) {
|
|
158
|
+
function filterRows(rows, search, filters, layoutSlug) {
|
|
124
159
|
const needle = search?.trim().toLowerCase() ?? "";
|
|
125
160
|
return rows.filter((row) => {
|
|
161
|
+
if (layoutSlug !== undefined && row.layoutSlug !== layoutSlug)
|
|
162
|
+
return false;
|
|
126
163
|
if (needle !== "" && !`${row.title} ${row.slug}`.toLowerCase().includes(needle))
|
|
127
164
|
return false;
|
|
128
165
|
return filters.every((filter) => filter.value.includes(String(row[filter.field])));
|
|
@@ -138,6 +175,7 @@ export const overviewQuery = defineQueryHandler({
|
|
|
138
175
|
schema: z.object({
|
|
139
176
|
search: z.string().optional(),
|
|
140
177
|
filters: z.array(filterSchema).optional(),
|
|
178
|
+
layoutSlug: z.string().optional(),
|
|
141
179
|
sort: z.string().optional(),
|
|
142
180
|
sortDirection: z.enum(["asc", "desc"]).optional(),
|
|
143
181
|
limit: z.number().int().min(1).max(200).default(50),
|
|
@@ -145,13 +183,15 @@ export const overviewQuery = defineQueryHandler({
|
|
|
145
183
|
totalCount: z.boolean().optional(),
|
|
146
184
|
}),
|
|
147
185
|
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
148
|
-
description: "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.",
|
|
186
|
+
description: "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.",
|
|
149
187
|
handler: async (query, ctx) => {
|
|
150
188
|
const payload = query.payload;
|
|
151
189
|
const rows = await selectMany(ctx.db, templateResourcesTable, { kind: { in: [...OVERVIEW_KINDS] } }, { limit: OVERVIEW_ROW_LIMIT });
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
const
|
|
190
|
+
const visibleRows = rows.filter((row) => !isHiddenSystemDraft(query.user, row));
|
|
191
|
+
const { drafts, layoutSlugByDocument } = buildDrafts(groupBySlugAndKind(visibleRows), query.user);
|
|
192
|
+
const effectiveLayouts = effectiveMailLayoutSlugs(visibleRows);
|
|
193
|
+
const overviewRows = drafts.map((draft) => withUsage(draft, layoutSlugByDocument, ctx.registry, effectiveLayouts));
|
|
194
|
+
const filtered = filterRows(overviewRows, payload.search, payload.filters ?? [], payload.layoutSlug);
|
|
155
195
|
const sortField = payload.sort !== undefined && isSortField(payload.sort) ? payload.sort : "modifiedAt";
|
|
156
196
|
const direction = (payload.sortDirection ?? "desc") === "desc" ? -1 : 1;
|
|
157
197
|
filtered.sort((a, b) => direction * compareBy(sortField, a, b) || a.id.localeCompare(b.id));
|
|
@@ -5,4 +5,4 @@ export declare const resetToSystemWrite: import("@cosmicdrift/kumiko-types/defin
|
|
|
5
5
|
id: string;
|
|
6
6
|
status: "archived";
|
|
7
7
|
systemTemplateId: string;
|
|
8
|
-
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
|
|
8
|
+
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
|
|
@@ -5,4 +5,4 @@ export declare const resourceLinkPdfWrite: import("@cosmicdrift/kumiko-types/def
|
|
|
5
5
|
templateSlug: z.ZodString;
|
|
6
6
|
fileName: z.ZodString;
|
|
7
7
|
tenantIdOverride: z.ZodOptional<z.ZodString>;
|
|
8
|
-
}, z.core.$strip>, import("../store-template-resource.js").LinkedTemplatePdfAttachment, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
|
|
8
|
+
}, z.core.$strip>, import("../store-template-resource.js").LinkedTemplatePdfAttachment, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
|
|
@@ -6,4 +6,4 @@ export declare const resourceRemoveWrite: import("@cosmicdrift/kumiko-types/defi
|
|
|
6
6
|
}, z.core.$strip>, {
|
|
7
7
|
key: string;
|
|
8
8
|
removed: boolean;
|
|
9
|
-
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
|
|
9
|
+
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
|
|
@@ -10,4 +10,4 @@ export declare const resourceUploadWrite: import("@cosmicdrift/kumiko-types/defi
|
|
|
10
10
|
image: "image";
|
|
11
11
|
}>>;
|
|
12
12
|
fileName: z.ZodOptional<z.ZodString>;
|
|
13
|
-
}, z.core.$strip>, StoredTemplateAttachment | StoredTemplateResource, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
|
|
13
|
+
}, z.core.$strip>, StoredTemplateAttachment | StoredTemplateResource, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
|
|
@@ -55,7 +55,7 @@ export const resourceUploadWrite = defineWriteHandler({
|
|
|
55
55
|
try {
|
|
56
56
|
source = await readFileRefContent({
|
|
57
57
|
db: ctx.db,
|
|
58
|
-
filesForTenant: (tid) => fileContextForTenant(ctx, tid),
|
|
58
|
+
filesForTenant: (tid) => fileContextForTenant(ctx, ctx.user.tenantId, tid),
|
|
59
59
|
fileRefId: sourceFileRefId,
|
|
60
60
|
allowedTenantIds: [event.user.tenantId],
|
|
61
61
|
unclaimedUploadOf: event.user.id,
|
|
@@ -5,7 +5,9 @@ export declare const sendTestWrite: import("@cosmicdrift/kumiko-types/define-han
|
|
|
5
5
|
content: z.ZodOptional<z.ZodString>;
|
|
6
6
|
meta: z.ZodOptional<z.ZodUnknown>;
|
|
7
7
|
variableSchema: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
8
|
+
includeAttachments: z.ZodDefault<z.ZodBoolean>;
|
|
9
|
+
prefixSubject: z.ZodDefault<z.ZodBoolean>;
|
|
8
10
|
}, z.core.$strip>, {
|
|
9
11
|
subject: string;
|
|
10
12
|
checks: readonly import("../template-preview.js").PreviewCheck[];
|
|
11
|
-
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
|
|
13
|
+
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
|
|
@@ -18,6 +18,10 @@ function testMailFailed(code) {
|
|
|
18
18
|
details: { code },
|
|
19
19
|
}));
|
|
20
20
|
}
|
|
21
|
+
function errorCodeOf(error) {
|
|
22
|
+
const code = error instanceof Error ? Reflect.get(error, "code") : undefined;
|
|
23
|
+
return typeof code === "string" ? code : undefined;
|
|
24
|
+
}
|
|
21
25
|
// The only recipient is the caller's own address, resolved server-side, so the handler can never mail a third party.
|
|
22
26
|
export const sendTestWrite = defineWriteHandler({
|
|
23
27
|
name: "send-test",
|
|
@@ -27,18 +31,24 @@ export const sendTestWrite = defineWriteHandler({
|
|
|
27
31
|
content: z.string().max(200_000).optional(),
|
|
28
32
|
meta: z.unknown().optional(),
|
|
29
33
|
variableSchema: z.record(z.string(), z.unknown()).default({}),
|
|
34
|
+
includeAttachments: z.boolean().default(true),
|
|
35
|
+
prefixSubject: z.boolean().default(true),
|
|
30
36
|
}),
|
|
31
37
|
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
32
38
|
rateLimit: { per: "user+handler", ...TEST_MAIL_RATE_LIMIT },
|
|
33
|
-
description: "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.",
|
|
39
|
+
description: "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.",
|
|
34
40
|
handler: async (event, ctx) => {
|
|
41
|
+
const { includeAttachments, prefixSubject, ...previewPayload } = event.payload;
|
|
35
42
|
const outcome = await renderTemplatePreview(ctx, event.user, {
|
|
36
|
-
...
|
|
43
|
+
...previewPayload,
|
|
37
44
|
kind: "mail",
|
|
38
45
|
});
|
|
39
46
|
if (!outcome.ok)
|
|
40
47
|
return writeFailure(outcome.error);
|
|
41
|
-
const { preview,
|
|
48
|
+
const { preview, sender } = outcome;
|
|
49
|
+
const mailAttachments = includeAttachments
|
|
50
|
+
? outcome.mailAttachments
|
|
51
|
+
: outcome.mailAttachments.filter((ref) => ref.kind === "inline");
|
|
42
52
|
const channel = emailChannelOf(ctx.registry);
|
|
43
53
|
if (channel === undefined) {
|
|
44
54
|
return writeFailure(new UnprocessableError(TemplateResolverErrors.testMailNoChannel, {
|
|
@@ -51,7 +61,7 @@ export const sendTestWrite = defineWriteHandler({
|
|
|
51
61
|
sseBroker: undefined,
|
|
52
62
|
tenantId: event.user.tenantId,
|
|
53
63
|
secrets: ctx.secrets,
|
|
54
|
-
readAttachment: createEmailAttachmentReader(ctx.db, ctx.registry, event.user.tenantId, ctx.config, (tenantId) => fileContextForTenant(ctx, tenantId)),
|
|
64
|
+
readAttachment: createEmailAttachmentReader(ctx.db, ctx.registry, event.user.tenantId, ctx.config, (tenantId) => fileContextForTenant(ctx, ctx.user.tenantId, tenantId)),
|
|
55
65
|
};
|
|
56
66
|
const address = (await channel.resolve?.(event.user.id, channelContext)) ?? null;
|
|
57
67
|
if (address === null) {
|
|
@@ -59,8 +69,8 @@ export const sendTestWrite = defineWriteHandler({
|
|
|
59
69
|
i18nKey: "templateResolver.errors.testMail.noAddress",
|
|
60
70
|
}));
|
|
61
71
|
}
|
|
62
|
-
const subject = `${TEST_MAIL_SUBJECT_PREFIX}${preview.subject ?? ""}`;
|
|
63
|
-
let
|
|
72
|
+
const subject = `${prefixSubject ? TEST_MAIL_SUBJECT_PREFIX : ""}${preview.subject ?? ""}`;
|
|
73
|
+
let sentStatus;
|
|
64
74
|
try {
|
|
65
75
|
const result = await channel.send(address, {
|
|
66
76
|
notificationType: TEST_MAIL_NOTIFICATION_TYPE,
|
|
@@ -68,14 +78,31 @@ export const sendTestWrite = defineWriteHandler({
|
|
|
68
78
|
body: undefined,
|
|
69
79
|
data: undefined,
|
|
70
80
|
locale: event.payload.locale,
|
|
71
|
-
}, channelContext, {
|
|
72
|
-
|
|
81
|
+
}, channelContext, {
|
|
82
|
+
subject,
|
|
83
|
+
html: preview.html,
|
|
84
|
+
text: preview.text ?? "",
|
|
85
|
+
...sender,
|
|
86
|
+
attachments: mailAttachments,
|
|
87
|
+
});
|
|
88
|
+
sentStatus = result.status;
|
|
73
89
|
}
|
|
74
|
-
catch {
|
|
90
|
+
catch (error) {
|
|
91
|
+
// Class and code only: the message can carry the address or a provider response body.
|
|
92
|
+
ctx.log?.warn("template test mail: channel threw", {
|
|
93
|
+
tenantId: event.user.tenantId,
|
|
94
|
+
errorClass: error instanceof Error ? error.name : typeof error,
|
|
95
|
+
errorCode: errorCodeOf(error),
|
|
96
|
+
});
|
|
75
97
|
return testMailFailed("channel_error");
|
|
76
98
|
}
|
|
77
|
-
if (
|
|
99
|
+
if (sentStatus !== "sent") {
|
|
100
|
+
ctx.log?.warn("template test mail: channel did not send", {
|
|
101
|
+
tenantId: event.user.tenantId,
|
|
102
|
+
status: sentStatus,
|
|
103
|
+
});
|
|
78
104
|
return testMailFailed("not_sent");
|
|
105
|
+
}
|
|
79
106
|
return { isSuccess: true, data: { subject, checks: preview.checks } };
|
|
80
107
|
},
|
|
81
108
|
});
|
|
@@ -27,4 +27,4 @@ export declare const setWrite: import("@cosmicdrift/kumiko-types/define-handler"
|
|
|
27
27
|
slug: string;
|
|
28
28
|
locale: string;
|
|
29
29
|
isNew: boolean;
|
|
30
|
-
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
|
|
30
|
+
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap, undefined>;
|
|
@@ -10,12 +10,23 @@ const PLACEHOLDER_VALIDATED_KINDS = new Set([
|
|
|
10
10
|
"document-pdf",
|
|
11
11
|
"pdf-layout",
|
|
12
12
|
]);
|
|
13
|
+
// The parser's wording is English and meant for logs; the UI gets a key per setting instead.
|
|
14
|
+
const META_FIELDS_WITH_MESSAGES = new Set([
|
|
15
|
+
"subject",
|
|
16
|
+
"previewText",
|
|
17
|
+
"senderName",
|
|
18
|
+
"replyTo",
|
|
19
|
+
"layoutSlug",
|
|
20
|
+
"fileName",
|
|
21
|
+
]);
|
|
13
22
|
function metaIssueToFieldIssue(issue) {
|
|
23
|
+
const hasMessage = META_FIELDS_WITH_MESSAGES.has(issue.field);
|
|
14
24
|
return {
|
|
15
25
|
path: issue.path,
|
|
16
26
|
code: "invalid_meta",
|
|
17
|
-
i18nKey:
|
|
18
|
-
|
|
27
|
+
i18nKey: hasMessage
|
|
28
|
+
? `templateResolver.errors.template.meta.${issue.field}.${issue.missing ? "required" : "invalid"}`
|
|
29
|
+
: "templateResolver.errors.template.invalidMeta",
|
|
19
30
|
};
|
|
20
31
|
}
|
|
21
32
|
export function placeholderIssues(validation, opts) {
|
|
@@ -3,6 +3,7 @@ import { SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
|
3
3
|
import { AccessDeniedError, ValidationError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
4
|
import { requireForTenant } from "../../shared/index.js";
|
|
5
5
|
import { templateResourcesTable } from "../table.js";
|
|
6
|
+
import { activationChanges, serializeTemplateDraft } from "../template-draft.js";
|
|
6
7
|
import { executor } from "./shared.js";
|
|
7
8
|
import { evaluateTemplateWrite, } from "./template-gates.js";
|
|
8
9
|
// The tenant-scope-under-system-tenant ban lives here so no write path can
|
|
@@ -33,6 +34,29 @@ export function gateTemplateWrite(input, status) {
|
|
|
33
34
|
}
|
|
34
35
|
return { ok: true, evaluated };
|
|
35
36
|
}
|
|
37
|
+
// A draft over an active row is stored beside it; every other write replaces the main fields.
|
|
38
|
+
function updateChanges(existing, status, fields) {
|
|
39
|
+
if (status === "active")
|
|
40
|
+
return { ...fields, ...activationChanges(existing) };
|
|
41
|
+
if (existing.status !== "active")
|
|
42
|
+
return { ...fields, status, draft: null };
|
|
43
|
+
const draft = serializeTemplateDraft({
|
|
44
|
+
content: fields.content,
|
|
45
|
+
contentFormat: fields.contentFormat,
|
|
46
|
+
variableSchema: fields.variableSchema,
|
|
47
|
+
meta: fields.meta,
|
|
48
|
+
title: fields.title,
|
|
49
|
+
folder: fields.folder,
|
|
50
|
+
parentTemplateId: fields.parentTemplateId,
|
|
51
|
+
});
|
|
52
|
+
return existing.activeVersion === null
|
|
53
|
+
? {
|
|
54
|
+
draft,
|
|
55
|
+
activeVersion: existing.version,
|
|
56
|
+
activeSince: existing.modifiedAt ?? existing.insertedAt,
|
|
57
|
+
}
|
|
58
|
+
: { draft };
|
|
59
|
+
}
|
|
36
60
|
// Shared by every template write: validate through the one pipeline, then
|
|
37
61
|
// create or update the (tenant, slug, kind, locale) row via the event-store
|
|
38
62
|
// executor. linkedResources is never part of an update; resources are attached
|
|
@@ -75,7 +99,11 @@ export async function writeTemplateRow(args) {
|
|
|
75
99
|
status,
|
|
76
100
|
};
|
|
77
101
|
if (existing) {
|
|
78
|
-
const result = await executor.update({
|
|
102
|
+
const result = await executor.update({
|
|
103
|
+
id: existing.id,
|
|
104
|
+
version: existing.version,
|
|
105
|
+
changes: updateChanges(existing, status, fields),
|
|
106
|
+
}, user, db, { streamTenantId });
|
|
79
107
|
if (!result.isSuccess)
|
|
80
108
|
return result;
|
|
81
109
|
return {
|
|
@@ -88,9 +116,13 @@ export async function writeTemplateRow(args) {
|
|
|
88
116
|
},
|
|
89
117
|
};
|
|
90
118
|
}
|
|
91
|
-
const result = await executor.create({
|
|
92
|
-
|
|
93
|
-
|
|
119
|
+
const result = await executor.create({
|
|
120
|
+
...fields,
|
|
121
|
+
linkedResources: "{}",
|
|
122
|
+
tenantId,
|
|
123
|
+
draft: null,
|
|
124
|
+
...(status === "active" ? activationChanges(null) : {}),
|
|
125
|
+
}, user, db, { streamTenantId });
|
|
94
126
|
if (!result.isSuccess)
|
|
95
127
|
return result;
|
|
96
128
|
// @cast-boundary db-row — executor.create returnt Record-row aus
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
|
+
type OverrideStatus = "active" | "draft";
|
|
2
3
|
export declare const tenantOverridesQuery: import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"tenant-overrides", z.ZodObject<{
|
|
3
4
|
slug: z.ZodString;
|
|
4
5
|
kind: z.ZodEnum<{
|
|
@@ -10,8 +11,39 @@ export declare const tenantOverridesQuery: import("@cosmicdrift/kumiko-types/def
|
|
|
10
11
|
"pdf-layout": "pdf-layout";
|
|
11
12
|
}>;
|
|
12
13
|
locale: z.ZodOptional<z.ZodString>;
|
|
14
|
+
diffTenantId: z.ZodOptional<z.ZodString>;
|
|
13
15
|
}, z.core.$strip>, {
|
|
14
16
|
activeOverrides: number;
|
|
15
17
|
draftOverrides: number;
|
|
16
18
|
totalTenants: number | null;
|
|
19
|
+
rows: {
|
|
20
|
+
tenantId: string;
|
|
21
|
+
tenantName: string;
|
|
22
|
+
templateId: string;
|
|
23
|
+
locale: string;
|
|
24
|
+
status: OverrideStatus;
|
|
25
|
+
version: number;
|
|
26
|
+
changedAt: string;
|
|
27
|
+
changedBy: string | null;
|
|
28
|
+
}[];
|
|
29
|
+
diff: {
|
|
30
|
+
tenant: {
|
|
31
|
+
templateId: string;
|
|
32
|
+
templateVersion: number;
|
|
33
|
+
contentFormat: string;
|
|
34
|
+
};
|
|
35
|
+
system: {
|
|
36
|
+
templateId: string;
|
|
37
|
+
templateVersion: number;
|
|
38
|
+
contentFormat: string;
|
|
39
|
+
};
|
|
40
|
+
identical: boolean;
|
|
41
|
+
blocks: import("../template-diff.js").TemplateDiffBlock[];
|
|
42
|
+
meta: {
|
|
43
|
+
key: string;
|
|
44
|
+
tenant: unknown;
|
|
45
|
+
system: unknown;
|
|
46
|
+
}[];
|
|
47
|
+
} | null;
|
|
17
48
|
}, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;
|
|
49
|
+
export {};
|
|
@@ -3,22 +3,72 @@ import { defineQueryHandler, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framew
|
|
|
3
3
|
import * as z from "zod";
|
|
4
4
|
import { TenantQueries } from "../../tenant/index.js";
|
|
5
5
|
import { templateResourcesTable } from "../table.js";
|
|
6
|
+
import { servedVersionOf } from "../template-draft.js";
|
|
7
|
+
import { buildOverrideDiff } from "./override-diff.js";
|
|
6
8
|
import { kindSchema, localeSchema, templateSlugSchema } from "./shared.js";
|
|
7
|
-
const TENANT_OVERRIDES_REASON = "SystemAdmin template overview
|
|
9
|
+
const TENANT_OVERRIDES_REASON = "SystemAdmin template overview lists which tenants override one system template; it reads tenant-scope template rows of every tenant (and the system row for the optional per-tenant diff), which the tenant-scoped db cannot reach";
|
|
8
10
|
const tenantIdsSchema = z.array(z.string());
|
|
11
|
+
const directorySchema = z.object({
|
|
12
|
+
rows: z.array(z.object({ id: z.string(), label: z.string() })),
|
|
13
|
+
});
|
|
14
|
+
function labelsOf(raw) {
|
|
15
|
+
return new Map(directorySchema.parse(raw).rows.map((row) => [row.id, row.label]));
|
|
16
|
+
}
|
|
17
|
+
// One row per tenant: its active override wins over a draft, then the newest version.
|
|
18
|
+
function isBetterRow(candidate, current) {
|
|
19
|
+
if ((candidate.status === "active") !== (current.status === "active")) {
|
|
20
|
+
return candidate.status === "active";
|
|
21
|
+
}
|
|
22
|
+
return candidate.version > current.version;
|
|
23
|
+
}
|
|
24
|
+
function statusOf(row) {
|
|
25
|
+
return row.status === "active" ? "active" : "draft";
|
|
26
|
+
}
|
|
27
|
+
function isCountedTenantRow(row, enabledTenantIds) {
|
|
28
|
+
if (row.tenantId === SYSTEM_TENANT_ID || row.status === "archived")
|
|
29
|
+
return false;
|
|
30
|
+
return enabledTenantIds === null || enabledTenantIds.has(row.tenantId);
|
|
31
|
+
}
|
|
32
|
+
function pickBestRowPerTenant(found, enabledTenantIds) {
|
|
33
|
+
const bestByTenant = new Map();
|
|
34
|
+
for (const row of found) {
|
|
35
|
+
if (!isCountedTenantRow(row, enabledTenantIds))
|
|
36
|
+
continue;
|
|
37
|
+
const current = bestByTenant.get(row.tenantId);
|
|
38
|
+
if (current === undefined || isBetterRow(row, current))
|
|
39
|
+
bestByTenant.set(row.tenantId, row);
|
|
40
|
+
}
|
|
41
|
+
return bestByTenant;
|
|
42
|
+
}
|
|
43
|
+
function toOverrideRow(row, tenantLabels, userLabels) {
|
|
44
|
+
const changedById = row.modifiedById ?? row.insertedById;
|
|
45
|
+
return {
|
|
46
|
+
tenantId: row.tenantId,
|
|
47
|
+
tenantName: tenantLabels.get(row.tenantId) ?? row.tenantId,
|
|
48
|
+
templateId: String(row.id),
|
|
49
|
+
locale: row.locale,
|
|
50
|
+
status: statusOf(row),
|
|
51
|
+
version: servedVersionOf(row),
|
|
52
|
+
changedAt: (row.modifiedAt ?? row.insertedAt).toString(),
|
|
53
|
+
changedBy: changedById === null ? null : (userLabels.get(changedById) ?? null),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
9
56
|
export const tenantOverridesQuery = defineQueryHandler({
|
|
10
57
|
name: "tenant-overrides",
|
|
11
58
|
schema: z.object({
|
|
12
59
|
slug: templateSlugSchema,
|
|
13
60
|
kind: kindSchema,
|
|
14
61
|
locale: localeSchema.optional(),
|
|
62
|
+
/** Adds the block diff of this tenant's override against the system template. */
|
|
63
|
+
diffTenantId: z.string().min(1).optional(),
|
|
15
64
|
}),
|
|
16
65
|
access: { roles: ["SystemAdmin"] },
|
|
17
|
-
description: "
|
|
66
|
+
description: "Lists the enabled tenants that override one template with an active or a draft version (tenant name, status, version, last change and who made it), next to the counts and the number of enabled tenants; use it before changing a system template to see who keeps their own copy. With diffTenantId it also returns the block diff of that tenant's override against the system template.",
|
|
18
67
|
escapeHatch: { grants: ["unsafeRaw"], reason: TENANT_OVERRIDES_REASON },
|
|
19
68
|
handler: async (query, ctx) => {
|
|
20
|
-
const { slug, kind, locale } = query.payload;
|
|
21
|
-
const
|
|
69
|
+
const { slug, kind, locale, diffTenantId } = query.payload;
|
|
70
|
+
const raw = ctx.db.unsafeRaw();
|
|
71
|
+
const found = await selectMany(raw, templateResourcesTable, {
|
|
22
72
|
slug,
|
|
23
73
|
kind,
|
|
24
74
|
scope: "tenant",
|
|
@@ -28,21 +78,35 @@ export const tenantOverridesQuery = defineQueryHandler({
|
|
|
28
78
|
const enabledTenantIds = hasTenantFeature
|
|
29
79
|
? new Set(tenantIdsSchema.parse(await ctx.query(TenantQueries.activeTenantIds, {})))
|
|
30
80
|
: null;
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
81
|
+
const bestByTenant = pickBestRowPerTenant(found, enabledTenantIds);
|
|
82
|
+
const best = [...bestByTenant.values()];
|
|
83
|
+
const tenantLabels = hasTenantFeature
|
|
84
|
+
? labelsOf(await ctx.query(TenantQueries.tenantDirectory, {}))
|
|
85
|
+
: new Map();
|
|
86
|
+
const userLabels = hasTenantFeature
|
|
87
|
+
? labelsOf(await ctx.query(TenantQueries.memberDirectory, {}))
|
|
88
|
+
: new Map();
|
|
89
|
+
const rows = best
|
|
90
|
+
.map((row) => toOverrideRow(row, tenantLabels, userLabels))
|
|
91
|
+
.sort((a, b) => a.tenantName.localeCompare(b.tenantName));
|
|
92
|
+
let diff = null;
|
|
93
|
+
const diffRow = diffTenantId === undefined ? undefined : bestByTenant.get(diffTenantId);
|
|
94
|
+
if (diffRow !== undefined) {
|
|
95
|
+
const [systemRow] = await selectMany(raw, templateResourcesTable, {
|
|
96
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
97
|
+
slug,
|
|
98
|
+
kind,
|
|
99
|
+
locale: diffRow.locale,
|
|
100
|
+
});
|
|
101
|
+
if (systemRow !== undefined)
|
|
102
|
+
diff = buildOverrideDiff(diffRow, systemRow);
|
|
40
103
|
}
|
|
41
|
-
const statuses = [...statusByTenant.values()];
|
|
42
104
|
return {
|
|
43
|
-
activeOverrides:
|
|
44
|
-
draftOverrides:
|
|
105
|
+
activeOverrides: best.filter((row) => row.status === "active").length,
|
|
106
|
+
draftOverrides: best.filter((row) => row.status !== "active").length,
|
|
45
107
|
totalTenants: enabledTenantIds === null ? null : enabledTenantIds.size,
|
|
108
|
+
rows,
|
|
109
|
+
diff,
|
|
46
110
|
};
|
|
47
111
|
},
|
|
48
112
|
});
|