@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
|
@@ -51,7 +51,15 @@ export const TEXT_BLOCK_KIND = "text-block" as const;
|
|
|
51
51
|
// Kinds a content collection must not write: a tenant row of these kinds would
|
|
52
52
|
// shadow the system notification template or layout without the meta and
|
|
53
53
|
// placeholder gates that upsertTenant applies.
|
|
54
|
-
export const COLLECTION_REFUSED_KINDS
|
|
54
|
+
export const COLLECTION_REFUSED_KINDS = [
|
|
55
|
+
"mail",
|
|
56
|
+
"mail-layout",
|
|
57
|
+
"pdf-layout",
|
|
58
|
+
] as const satisfies readonly TemplateKind[];
|
|
59
|
+
|
|
60
|
+
export function isCollectionRefusedKind(kind: string): boolean {
|
|
61
|
+
return COLLECTION_REFUSED_KINDS.some((refused) => refused === kind);
|
|
62
|
+
}
|
|
55
63
|
|
|
56
64
|
export const CONTENT_FORMATS = ["markdown", "mjml", "html", "plain"] as const;
|
|
57
65
|
export type ContentFormat = (typeof CONTENT_FORMATS)[number];
|
|
@@ -61,11 +69,23 @@ export type TemplateScope = (typeof TEMPLATE_SCOPES)[number];
|
|
|
61
69
|
|
|
62
70
|
export const TEMPLATE_STATUSES = ["draft", "active", "archived"] as const;
|
|
63
71
|
export type TemplateStatus = (typeof TEMPLATE_STATUSES)[number];
|
|
72
|
+
export const ACTIVE_TEMPLATE_STATUS = "active" satisfies TemplateStatus;
|
|
73
|
+
|
|
74
|
+
export {
|
|
75
|
+
PDF_DEFAULT_MARGIN_MM,
|
|
76
|
+
PDF_FOOTER_MARGIN_MM,
|
|
77
|
+
PDF_HEADER_MARGIN_MM,
|
|
78
|
+
PDF_ORIENTATIONS,
|
|
79
|
+
PDF_PAGE_FORMATS,
|
|
80
|
+
type PdfOrientation,
|
|
81
|
+
type PdfPageFormat,
|
|
82
|
+
} from "./pdf-constants.js";
|
|
64
83
|
|
|
65
|
-
|
|
66
|
-
export
|
|
84
|
+
// Colors end up inside MJML attributes, so only a strict hex is ever written there.
|
|
85
|
+
export const HEX_COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
|
|
67
86
|
|
|
68
87
|
export const DEFAULT_MAIL_LAYOUT_SLUG = "default";
|
|
88
|
+
export const DEFAULT_PDF_LAYOUT_SLUG = "default";
|
|
69
89
|
|
|
70
90
|
// The mail transport enforces no size limit of its own; 10 MiB is a common provider ceiling.
|
|
71
91
|
export const MAIL_PREVIEW_MAX_BYTES = 10 * 1024 * 1024;
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
isDocumentReferenceName,
|
|
14
14
|
isDocumentResourceKey,
|
|
15
15
|
} from "./document-schema.js";
|
|
16
|
+
import { isDocumentStepKey } from "./document-steps.js";
|
|
16
17
|
|
|
17
18
|
export type DocumentViolationCode =
|
|
18
19
|
| "script_element"
|
|
@@ -138,6 +139,8 @@ const REFERENCE_ATTRIBUTES: ReadonlyArray<readonly [string, (value: string) => b
|
|
|
138
139
|
["data-variable", isDocumentReferenceName],
|
|
139
140
|
["data-each", isDocumentReferenceName],
|
|
140
141
|
["data-resource", isDocumentResourceKey],
|
|
142
|
+
["data-steps", isDocumentReferenceName],
|
|
143
|
+
["data-step", isDocumentStepKey],
|
|
141
144
|
];
|
|
142
145
|
|
|
143
146
|
function scanReferences(tag: string, element: ScanElement, out: DocumentViolation[]): void {
|
|
@@ -8,14 +8,16 @@ import { Extension, type Extensions, Node } from "@tiptap/core";
|
|
|
8
8
|
import { Link } from "@tiptap/extension-link";
|
|
9
9
|
import { Table, TableCell, TableHeader, TableRow } from "@tiptap/extension-table";
|
|
10
10
|
import StarterKit from "@tiptap/starter-kit";
|
|
11
|
+
import { createDocumentStepsNode } from "./document-steps.js";
|
|
11
12
|
|
|
12
|
-
const PLACEHOLDER_HREF = /^\{\{
|
|
13
|
+
const PLACEHOLDER_HREF = /^\{\{(.+)\}\}$/;
|
|
13
14
|
const REFERENCE_NAME = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/;
|
|
14
15
|
const RESOURCE_KEY = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
15
16
|
const ALLOWED_LINK_SCHEMES = ["https://", "http://", "mailto:", "tel:"] as const;
|
|
16
17
|
|
|
17
18
|
export function isDocumentPlaceholderHref(href: string): boolean {
|
|
18
|
-
|
|
19
|
+
const name = PLACEHOLDER_HREF.exec(href)?.[1];
|
|
20
|
+
return name !== undefined && isDocumentReferenceName(name);
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
const UNSAFE_URL_CHARACTERS = new Set(["<", ">", '"']);
|
|
@@ -70,6 +72,7 @@ export const DOCUMENT_NODE_NAMES = [
|
|
|
70
72
|
"button",
|
|
71
73
|
"divider",
|
|
72
74
|
"pageBreak",
|
|
75
|
+
"steps",
|
|
73
76
|
] as const;
|
|
74
77
|
|
|
75
78
|
export const DocumentVariable = Node.create({
|
|
@@ -130,7 +133,8 @@ export const DocumentResource = Node.create({
|
|
|
130
133
|
export const DocumentButton = Node.create({
|
|
131
134
|
name: "button",
|
|
132
135
|
group: "block",
|
|
133
|
-
|
|
136
|
+
// A placeholder in the label ("View {{pageName}}") must survive the round trip.
|
|
137
|
+
content: "(text | variable)*",
|
|
134
138
|
marks: "",
|
|
135
139
|
defining: true,
|
|
136
140
|
addAttributes() {
|
|
@@ -178,6 +182,8 @@ export const DocumentPageBreak = Node.create({
|
|
|
178
182
|
},
|
|
179
183
|
});
|
|
180
184
|
|
|
185
|
+
export const DocumentSteps = createDocumentStepsNode(isDocumentReferenceName);
|
|
186
|
+
|
|
181
187
|
// A repeat row is a table row carrying `each`: prosemirror-tables requires
|
|
182
188
|
// the tableRow node type, so a separate node type is not an option.
|
|
183
189
|
export const DocumentTableRow = TableRow.extend({
|
|
@@ -204,8 +210,6 @@ export const DocumentTableRow = TableRow.extend({
|
|
|
204
210
|
},
|
|
205
211
|
});
|
|
206
212
|
|
|
207
|
-
export const DocumentRepeatRow = DocumentTableRow;
|
|
208
|
-
|
|
209
213
|
export const DocumentTable = Table.extend({
|
|
210
214
|
renderHTML() {
|
|
211
215
|
return ["table", ["tbody", 0]];
|
|
@@ -288,6 +292,28 @@ export const DocumentTextAlign = Extension.create({
|
|
|
288
292
|
},
|
|
289
293
|
});
|
|
290
294
|
|
|
295
|
+
// Only "small" exists: the sender line above an address block. Serialized as data-size for the
|
|
296
|
+
// same reason as data-align.
|
|
297
|
+
export const DocumentTextSize = Extension.create({
|
|
298
|
+
name: "documentTextSize",
|
|
299
|
+
addGlobalAttributes() {
|
|
300
|
+
return [
|
|
301
|
+
{
|
|
302
|
+
types: ["paragraph"],
|
|
303
|
+
attributes: {
|
|
304
|
+
textSize: {
|
|
305
|
+
default: null,
|
|
306
|
+
parseHTML: (element) =>
|
|
307
|
+
element.getAttribute("data-size") === "small" ? "small" : null,
|
|
308
|
+
renderHTML: (attributes) =>
|
|
309
|
+
attributes["textSize"] === "small" ? { "data-size": "small" } : {},
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
];
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
|
|
291
317
|
export const documentExtensions: Extensions = [
|
|
292
318
|
StarterKit.configure({
|
|
293
319
|
blockquote: false,
|
|
@@ -319,5 +345,7 @@ export const documentExtensions: Extensions = [
|
|
|
319
345
|
DocumentButton,
|
|
320
346
|
DocumentDivider,
|
|
321
347
|
DocumentPageBreak,
|
|
348
|
+
DocumentSteps,
|
|
322
349
|
DocumentTextAlign,
|
|
350
|
+
DocumentTextSize,
|
|
323
351
|
];
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// A progress row: fixed stages stored in the template, the reached stage read
|
|
3
|
+
// from a variable at render time. Universal like document-schema.ts.
|
|
4
|
+
import { Node } from "@tiptap/core";
|
|
5
|
+
|
|
6
|
+
// Keys end up as Handlebars string literals in the compiled body, so they are
|
|
7
|
+
// restricted to characters that cannot close or escape a literal.
|
|
8
|
+
const STEP_KEY = /^[A-Za-z0-9][A-Za-z0-9_-]{0,39}$/;
|
|
9
|
+
export const DOCUMENT_STEPS_MAX = 8;
|
|
10
|
+
const STEP_LABEL_MAX_LENGTH = 40;
|
|
11
|
+
|
|
12
|
+
export type DocumentStep = { readonly key: string; readonly label: string };
|
|
13
|
+
|
|
14
|
+
export function isDocumentStepKey(value: string): boolean {
|
|
15
|
+
return STEP_KEY.test(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function documentStepKeyOf(label: string): string {
|
|
19
|
+
return label
|
|
20
|
+
.normalize("NFKD")
|
|
21
|
+
.replace(/\p{M}/gu, "")
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
24
|
+
.replace(/^-+|-+$/g, "")
|
|
25
|
+
.slice(0, 40);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Editor input: "value=Label" entries separated by commas; a bare label derives its value.
|
|
29
|
+
export function parseDocumentStepsInput(text: string): readonly DocumentStep[] | null {
|
|
30
|
+
const steps = text
|
|
31
|
+
.split(",")
|
|
32
|
+
.map((entry) => entry.trim())
|
|
33
|
+
.filter((entry) => entry !== "")
|
|
34
|
+
.map((entry): DocumentStep => {
|
|
35
|
+
const separator = entry.indexOf("=");
|
|
36
|
+
const label = (separator < 0 ? entry : entry.slice(separator + 1)).trim();
|
|
37
|
+
const key = separator < 0 ? documentStepKeyOf(entry) : entry.slice(0, separator).trim();
|
|
38
|
+
return { key, label: label.slice(0, STEP_LABEL_MAX_LENGTH) };
|
|
39
|
+
});
|
|
40
|
+
return areValidSteps(steps) ? steps : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function areValidSteps(steps: readonly DocumentStep[]): boolean {
|
|
44
|
+
return (
|
|
45
|
+
steps.length > 0 &&
|
|
46
|
+
steps.length <= DOCUMENT_STEPS_MAX &&
|
|
47
|
+
new Set(steps.map((step) => step.key)).size === steps.length &&
|
|
48
|
+
steps.every((step) => isDocumentStepKey(step.key) && step.label !== "")
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Duplicate or invalid keys would make the reached stage ambiguous, so the whole
|
|
53
|
+
// list is refused rather than repaired.
|
|
54
|
+
export function readDocumentSteps(element: {
|
|
55
|
+
querySelectorAll(selector: string): ArrayLike<{
|
|
56
|
+
getAttribute(name: string): string | null;
|
|
57
|
+
textContent: string | null;
|
|
58
|
+
}>;
|
|
59
|
+
}): readonly DocumentStep[] | null {
|
|
60
|
+
const steps = Array.from(element.querySelectorAll("span[data-step]")).map((span) => ({
|
|
61
|
+
key: span.getAttribute("data-step") ?? "",
|
|
62
|
+
label: (span.textContent ?? "").trim().slice(0, STEP_LABEL_MAX_LENGTH),
|
|
63
|
+
}));
|
|
64
|
+
return areValidSteps(steps) ? steps : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isDocumentStep(step: unknown): step is DocumentStep {
|
|
68
|
+
return (
|
|
69
|
+
typeof step === "object" &&
|
|
70
|
+
step !== null &&
|
|
71
|
+
"key" in step &&
|
|
72
|
+
"label" in step &&
|
|
73
|
+
typeof step.key === "string" &&
|
|
74
|
+
typeof step.label === "string"
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Node attrs are untyped JSON; anything that is not a step entry is dropped.
|
|
79
|
+
export function readDocumentStepsAttribute(value: unknown): readonly DocumentStep[] {
|
|
80
|
+
return Array.isArray(value) ? value.filter(isDocumentStep) : [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createDocumentStepsNode(isReferenceName: (value: string) => boolean) {
|
|
84
|
+
return Node.create({
|
|
85
|
+
name: "steps",
|
|
86
|
+
group: "block",
|
|
87
|
+
atom: true,
|
|
88
|
+
addAttributes() {
|
|
89
|
+
return { variable: { default: null }, steps: { default: [] } };
|
|
90
|
+
},
|
|
91
|
+
parseHTML() {
|
|
92
|
+
return [
|
|
93
|
+
{
|
|
94
|
+
tag: "div[data-steps]",
|
|
95
|
+
getAttrs: (element) => {
|
|
96
|
+
const variable = element.getAttribute("data-steps");
|
|
97
|
+
const steps = readDocumentSteps(element);
|
|
98
|
+
if (variable === null || !isReferenceName(variable) || steps === null) return false;
|
|
99
|
+
return { variable, steps };
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
},
|
|
104
|
+
renderHTML({ node }) {
|
|
105
|
+
return [
|
|
106
|
+
"div",
|
|
107
|
+
{ "data-steps": node.attrs["variable"] },
|
|
108
|
+
...readDocumentStepsAttribute(node.attrs["steps"]).map(
|
|
109
|
+
(step) => ["span", { "data-step": step.key }, step.label] as const,
|
|
110
|
+
),
|
|
111
|
+
];
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
createSystemConfig,
|
|
4
4
|
defineFeature,
|
|
5
5
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
6
|
+
import { isCollectionRefusedKind } from "./constants.js";
|
|
6
7
|
import { bySlugQuery } from "./handlers/by-slug.query.js";
|
|
7
8
|
import { byTenantQuery } from "./handlers/by-tenant.query.js";
|
|
8
9
|
import { makeCollectionItemQuery } from "./handlers/collection-item.query.js";
|
|
@@ -10,6 +11,7 @@ import { makeCollectionListQuery } from "./handlers/collection-list.query.js";
|
|
|
10
11
|
import { makeCollectionSetWrite } from "./handlers/collection-set.write.js";
|
|
11
12
|
import { copyLocaleWrite } from "./handlers/copy-locale.write.js";
|
|
12
13
|
import { diffToSystemQuery } from "./handlers/diff-to-system.query.js";
|
|
14
|
+
import { discardDraftSystemWrite, discardDraftWrite } from "./handlers/discard-draft.write.js";
|
|
13
15
|
import { exportBundleQuery } from "./handlers/export-bundle.query.js";
|
|
14
16
|
import { findByIdQuery } from "./handlers/find-by-id.query.js";
|
|
15
17
|
import { importBundleWrite } from "./handlers/import-bundle.write.js";
|
|
@@ -59,14 +61,25 @@ export type TemplateResolverOptions = {
|
|
|
59
61
|
readonly collections?: readonly ContentCollectionDefinition[];
|
|
60
62
|
};
|
|
61
63
|
|
|
64
|
+
function assertCollectionKindsAllowed(collections: readonly ContentCollectionDefinition[]): void {
|
|
65
|
+
for (const collection of collections) {
|
|
66
|
+
if (isCollectionRefusedKind(collection.kind)) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`template-resolver: collection "${collection.id}" uses kind "${collection.kind}", which collections may not write`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
62
74
|
export function createTemplateResolverFeature(opts: TemplateResolverOptions = {}) {
|
|
63
75
|
const collections = opts.collections ?? [];
|
|
76
|
+
assertCollectionKindsAllowed(collections);
|
|
64
77
|
const hasUserOwned = collections.some((c) => c.ownership === "user");
|
|
65
78
|
return defineFeature("template-resolver", (r) => {
|
|
66
79
|
r.describe(
|
|
67
80
|
[
|
|
68
81
|
"Every piece of editable text lives here, in one entity: mail bodies, notification texts, PDF document templates, AI prompts and plain text blocks. What a record is used for is the `kind` (`notification`, `mail`, `mail-layout`, `document-pdf`, `pdf-layout`, `ai-prompt`, `text-block`, `snippet`, `image-snapshot`).",
|
|
69
|
-
"Reading is one call — `ctx.templateResolver.resolveTemplate({ tenantId, slug, kind, locale })`. It walks four levels: tenant+locale, system+locale, tenant+fallback-locale, system+fallback-locale. A tenant overrides a system default by simply having its own record; no application code changes. Writing programmatically goes through the `upsertSystem`, `upsertTenant`, `publish` and `archive` write handlers; a SystemAdmin retires or restores a system default with `publishSystem` and `archiveSystem`, and a tenant drops its override with `resetToSystem`.",
|
|
82
|
+
"Reading is one call — `ctx.templateResolver.resolveTemplate({ tenantId, slug, kind, locale })`. It walks four levels: tenant+locale, system+locale, tenant+fallback-locale, system+fallback-locale. A tenant overrides a system default by simply having its own record; no application code changes. Writing programmatically goes through the `upsertSystem`, `upsertTenant`, `publish` and `archive` write handlers; a SystemAdmin retires or restores a system default with `publishSystem` and `archiveSystem`, and a tenant drops its override with `resetToSystem`. Saving a draft over an active template keeps the active version served and stores the edit beside it (column `draft`); `publish` makes it the served version, `discardDraft` / `discardDraftSystem` throws it away.",
|
|
70
83
|
"Text an editor should be able to change belongs in a collection, declared at mount: `createTemplateResolverFeature({ collections: [{ id, kind, access: { roles }, nav }] })`. It appears in the navigation, and `access` is part of the mount because a bundled feature cannot know the host's roles. Each collection gets its own `<id>-list` / `<id>-item` / `<id>-set` handlers, so the dispatcher enforces the separation.",
|
|
71
84
|
"How a collection is edited follows from `contentFormat`: `plain` gives a text area, `rich` a small WYSIWYG (bold, italic, headings, lists, links), `markdown` a text area that stores markdown text (same insertable chips as `plain`). All three offer the collection's `variableSchema` as insertable chips and a preview rendered with sample data — an editor sees what `{{firstName}}` becomes without sending a mail. An app can register its own editor for a format; the last `clientFeature` that registers a format wins (a conflict logs a warning).",
|
|
72
85
|
'A collection is tenant-wide by default. With `ownership: "user"` every user keeps their own entries — mail signatures being the obvious case. Those rows live in the separate `user-content-entry` entity and count as user data, so mounting one also requires the `template-resolver-user-data` feature and a migration on the app side.',
|
|
@@ -100,6 +113,8 @@ export function createTemplateResolverFeature(opts: TemplateResolverOptions = {}
|
|
|
100
113
|
archive: r.writeHandler(archiveWrite),
|
|
101
114
|
publishSystem: r.writeHandler(publishSystemWrite),
|
|
102
115
|
archiveSystem: r.writeHandler(archiveSystemWrite),
|
|
116
|
+
discardDraft: r.writeHandler(discardDraftWrite),
|
|
117
|
+
discardDraftSystem: r.writeHandler(discardDraftSystemWrite),
|
|
103
118
|
resetToSystem: r.writeHandler(resetToSystemWrite),
|
|
104
119
|
set: r.writeHandler(setWrite),
|
|
105
120
|
importBundle: r.writeHandler(importBundleWrite),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import type {
|
|
3
|
+
BeforeTransactionResult,
|
|
3
4
|
HandlerContext,
|
|
4
5
|
SessionUser,
|
|
5
6
|
TenantId,
|
|
@@ -16,9 +17,13 @@ import type { LinkedResourceEntry } from "../linked-resources.js";
|
|
|
16
17
|
import {
|
|
17
18
|
attachmentLimitFailure,
|
|
18
19
|
attachmentNeedsMailTemplate,
|
|
20
|
+
createPreparedBundleResourceEntry,
|
|
21
|
+
type PreparedTemplateFile,
|
|
22
|
+
prepareTemplateAttachmentBeforeTransaction,
|
|
23
|
+
prepareTemplateResourceBeforeTransaction,
|
|
24
|
+
type Release,
|
|
19
25
|
readTemplateResourceMaxBytes,
|
|
20
|
-
|
|
21
|
-
storeTemplateResource,
|
|
26
|
+
releaseAll,
|
|
22
27
|
} from "../store-template-resource.js";
|
|
23
28
|
import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
|
|
24
29
|
import type { TemplateBundle } from "../template-bundle.js";
|
|
@@ -92,64 +97,101 @@ async function collectBundleIssues(
|
|
|
92
97
|
return issues;
|
|
93
98
|
}
|
|
94
99
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
):
|
|
98
|
-
const linked: Record<string, LinkedResourceEntry> = {};
|
|
99
|
-
const pdfLinks = Object.entries(args.bundle.pdfAttachments);
|
|
100
|
+
export type PreparedBundleResources = ReadonlyMap<string, PreparedTemplateFile>;
|
|
101
|
+
|
|
102
|
+
function checkBundleAttachments(bundle: TemplateBundle): WriteResult<null> {
|
|
100
103
|
const attachmentCount =
|
|
101
|
-
Object.values(
|
|
102
|
-
|
|
103
|
-
if (attachmentCount > 0 &&
|
|
104
|
+
Object.values(bundle.resources).filter((resource) => resource.purpose === "attachment").length +
|
|
105
|
+
Object.keys(bundle.pdfAttachments).length;
|
|
106
|
+
if (attachmentCount > 0 && bundle.kind !== "mail") return attachmentNeedsMailTemplate();
|
|
104
107
|
if (attachmentCount > MAX_FILE_ATTACHMENTS_PER_TEMPLATE) return attachmentLimitFailure();
|
|
105
|
-
|
|
106
|
-
|
|
108
|
+
return { isSuccess: true, data: null };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// beforeTransaction half: validates the bundle and uploads every resource file to the target
|
|
112
|
+
// tenant's storage. A failed or throwing upload releases the ones before it.
|
|
113
|
+
export async function prepareTemplateBundleBeforeTransaction(
|
|
114
|
+
args: BundleImport & { readonly status: BundleImportStatus },
|
|
115
|
+
): Promise<BeforeTransactionResult<PreparedBundleResources>> {
|
|
116
|
+
const refused = refuseTenantScopeUnderSystemTenant(args.scope, args.tenantId);
|
|
117
|
+
if (refused !== null) return refused;
|
|
118
|
+
const issues = await collectBundleIssues(args, args.status);
|
|
119
|
+
if (issues.length > 0) return writeFailure(new ValidationError({ fields: [...issues] }));
|
|
120
|
+
const attachmentCheck = checkBundleAttachments(args.bundle);
|
|
121
|
+
if (!attachmentCheck.isSuccess) return attachmentCheck;
|
|
122
|
+
|
|
107
123
|
const maxBytes = await readTemplateResourceMaxBytes(args.ctx.config, args.ctx.registry);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
124
|
+
const prepared = new Map<string, PreparedTemplateFile>();
|
|
125
|
+
const releases: Release[] = [];
|
|
126
|
+
try {
|
|
127
|
+
// A bundle is untrusted input like an upload: the same re-encoding decides the stored
|
|
128
|
+
// content type, so a declared image/png can't smuggle SVG or HTML into a mail or PDF.
|
|
129
|
+
for (const [key, resource] of Object.entries(args.bundle.resources)) {
|
|
130
|
+
const bytes = Uint8Array.from(Buffer.from(resource.base64, "base64"));
|
|
131
|
+
const common = {
|
|
114
132
|
ctx: args.ctx,
|
|
115
133
|
user: args.user,
|
|
116
134
|
tenantId: args.tenantId,
|
|
117
135
|
bytes,
|
|
118
136
|
maxBytes,
|
|
119
137
|
key,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
138
|
+
};
|
|
139
|
+
const result =
|
|
140
|
+
resource.purpose === "attachment"
|
|
141
|
+
? await prepareTemplateAttachmentBeforeTransaction({
|
|
142
|
+
...common,
|
|
143
|
+
fileName: resource.fileName,
|
|
144
|
+
})
|
|
145
|
+
: await prepareTemplateResourceBeforeTransaction(common);
|
|
146
|
+
if (!result.isSuccess) {
|
|
147
|
+
await releaseAll(releases);
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
if (result.release) releases.push(result.release);
|
|
151
|
+
prepared.set(key, result.data);
|
|
125
152
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
153
|
+
} catch (cause) {
|
|
154
|
+
await releaseAll(releases);
|
|
155
|
+
throw cause;
|
|
156
|
+
}
|
|
157
|
+
return { isSuccess: true, data: prepared, release: () => releaseAll(releases) };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function linkPreparedBundleResources(
|
|
161
|
+
args: BundleImport,
|
|
162
|
+
prepared: PreparedBundleResources,
|
|
163
|
+
): Promise<WriteResult<Record<string, LinkedResourceEntry>>> {
|
|
164
|
+
const linked: Record<string, LinkedResourceEntry> = {};
|
|
165
|
+
// The document template is not looked up: a bundle may carry it later than the mail.
|
|
166
|
+
for (const [key, link] of Object.entries(args.bundle.pdfAttachments)) {
|
|
167
|
+
linked[key] = { kind: "rendered-pdf", ...link };
|
|
168
|
+
}
|
|
169
|
+
for (const [key, file] of prepared) {
|
|
170
|
+
const entry = await createPreparedBundleResourceEntry(
|
|
171
|
+
{ ctx: args.ctx, user: args.user, tenantId: args.tenantId },
|
|
172
|
+
file,
|
|
173
|
+
);
|
|
174
|
+
if (!entry.isSuccess) return entry;
|
|
175
|
+
linked[key] = entry.data;
|
|
136
176
|
}
|
|
137
177
|
return { isSuccess: true, data: linked };
|
|
138
178
|
}
|
|
139
179
|
|
|
140
|
-
// Validates every locale first,
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
// for the resources before it stay as unlinked FileRefs, like a replaced upload.
|
|
180
|
+
// Validates every locale first, so a bad locale is refused before anything is written.
|
|
181
|
+
// The resource files are uploaded in the beforeTransaction phase; a rolled-back transaction
|
|
182
|
+
// releases them there. A crash between the upload and the commit leaves the binaries unreferenced.
|
|
144
183
|
export async function importTemplateBundle(
|
|
145
|
-
args: BundleImport & {
|
|
184
|
+
args: BundleImport & {
|
|
185
|
+
readonly status: BundleImportStatus;
|
|
186
|
+
readonly prepared: PreparedBundleResources;
|
|
187
|
+
},
|
|
146
188
|
): Promise<WriteResult<{ ids: Record<string, string> }>> {
|
|
147
189
|
const refused = refuseTenantScopeUnderSystemTenant(args.scope, args.tenantId);
|
|
148
190
|
if (refused !== null) return refused;
|
|
149
191
|
const issues = await collectBundleIssues(args, args.status);
|
|
150
192
|
if (issues.length > 0) return writeFailure(new ValidationError({ fields: [...issues] }));
|
|
151
193
|
|
|
152
|
-
const linked = await
|
|
194
|
+
const linked = await linkPreparedBundleResources(args, args.prepared);
|
|
153
195
|
if (!linked.isSuccess) return linked;
|
|
154
196
|
|
|
155
197
|
const variableSchema = args.bundle.variableSchema ?? {};
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
import { ValidationError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
8
8
|
import * as z from "zod";
|
|
9
9
|
import { requireForTenant } from "../../shared/index.js";
|
|
10
|
-
import {
|
|
10
|
+
import { isCollectionRefusedKind } from "../constants.js";
|
|
11
11
|
import { documentViolationsToFieldIssues, sanitizeTemplateBody } from "../document-sanitizer.js";
|
|
12
12
|
import {
|
|
13
13
|
type CollectionEntryRow,
|
|
@@ -43,12 +43,13 @@ export function makeCollectionSetWrite(collection: ContentCollectionDefinition)
|
|
|
43
43
|
? `Creates or overwrites the caller's own "${collection.id}" entry at a slug and locale, live on save with no draft stage; it always writes the acting user's entry, never another user's.`
|
|
44
44
|
: `Creates or overwrites the tenant-wide "${collection.id}" entry at a slug and locale, live on save with no draft stage; the change is visible to everyone who may reach this shared collection.`,
|
|
45
45
|
handler: async (event, ctx) => {
|
|
46
|
-
|
|
46
|
+
// Defense in depth: createTemplateResolverFeature already refuses these kinds at mount.
|
|
47
|
+
if (isCollectionRefusedKind(collection.kind)) {
|
|
47
48
|
return writeFailure(
|
|
48
49
|
new ValidationError({
|
|
49
50
|
fields: [
|
|
50
51
|
{
|
|
51
|
-
path: "
|
|
52
|
+
path: "",
|
|
52
53
|
code: "kind_not_allowed",
|
|
53
54
|
i18nKey: "templateResolver.errors.collection.kindNotAllowed",
|
|
54
55
|
params: { detail: collection.kind },
|