@cosmicdrift/kumiko-samples 0.362.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/package.json +1 -1
- package/packages/bundled-features/package.json +23 -6
- package/packages/bundled-features/src/admin-shell/changes.json +6 -0
- package/packages/bundled-features/src/admin-shell/feature.ts +8 -1
- package/packages/bundled-features/src/agent-tools/agent-manifest.ts +3 -4
- package/packages/bundled-features/src/agent-tools/filter-ops.ts +1 -3
- package/packages/bundled-features/src/agent-tools/tool-catalog.ts +1 -3
- package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +2 -1
- package/packages/bundled-features/src/audit/changes.json +6 -0
- package/packages/bundled-features/src/audit/handlers/list.query.ts +10 -5
- package/packages/bundled-features/src/auth-email-password/bootstrap.ts +29 -8
- package/packages/bundled-features/src/auth-email-password/changes.json +51 -0
- package/packages/bundled-features/src/auth-email-password/email-rate-limits.ts +16 -0
- package/packages/bundled-features/src/auth-email-password/email-templates.ts +12 -138
- package/packages/bundled-features/src/auth-email-password/feature.ts +30 -9
- 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-create.write.ts +4 -5
- package/packages/bundled-features/src/auth-email-password/handlers/logout.write.ts +7 -2
- package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +2 -5
- package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +2 -5
- package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +2 -5
- package/packages/bundled-features/src/auth-email-password/handlers/signup-confirm.write.ts +3 -4
- package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +2 -1
- package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +5 -13
- package/packages/bundled-features/src/auth-email-password/index.ts +2 -17
- package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +30 -22
- package/packages/bundled-features/src/auth-email-password/mail-notifications.ts +158 -0
- package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -3
- package/packages/bundled-features/src/auth-email-password/web/auth-gate.tsx +24 -5
- package/packages/bundled-features/src/auth-foundation/types.ts +8 -18
- package/packages/bundled-features/src/auth-mfa/feature.ts +8 -10
- package/packages/bundled-features/src/auth-mfa/hmac-token-codec.ts +4 -4
- package/packages/bundled-features/src/auth-mfa/mfa-challenge-token.ts +3 -3
- package/packages/bundled-features/src/auth-mfa/mfa-preauth-setup-token.ts +3 -3
- package/packages/bundled-features/src/auth-mfa/mfa-setup-token.ts +8 -8
- package/packages/bundled-features/src/billing-foundation/changes.json +43 -0
- package/packages/bundled-features/src/billing-foundation/constants.ts +5 -4
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +90 -44
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +22 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +3 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +118 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +2 -0
- 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 +39 -33
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-notify.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-pages.ts +1 -4
- 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 +10 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +36 -20
- 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/get-subscription-for-tenant.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +11 -3
- package/packages/bundled-features/src/billing-foundation/handlers/create-portal-session.write.ts +4 -9
- package/packages/bundled-features/src/billing-foundation/handlers/list-subscriptions.query.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +12 -6
- package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/i18n.ts +3 -0
- package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +20 -11
- package/packages/bundled-features/src/billing-foundation/types.ts +7 -2
- package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +2 -0
- package/packages/bundled-features/src/cap-counter/changes.json +6 -0
- package/packages/bundled-features/src/cap-counter/enforce-cap.ts +75 -5
- 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/handlers/tenant-caps-list.query.ts +2 -1
- package/packages/bundled-features/src/cap-overview/web/cap-usage-cell.tsx +6 -4
- package/packages/bundled-features/src/channel-email/changes.json +19 -0
- package/packages/bundled-features/src/channel-email/email-channel.ts +135 -29
- package/packages/bundled-features/src/channel-email/feature.ts +6 -3
- package/packages/bundled-features/src/channel-email/index.ts +7 -1
- package/packages/bundled-features/src/channel-email/pii-guard.ts +13 -0
- package/packages/bundled-features/src/channel-email/smtp-transport.ts +15 -1
- package/packages/bundled-features/src/channel-email/types.ts +23 -0
- package/packages/bundled-features/src/channel-in-app/handlers/mark-read.write.ts +2 -2
- package/packages/bundled-features/src/config/changes.json +6 -0
- package/packages/bundled-features/src/config/handlers/set.write.ts +3 -7
- 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 +48 -27
- package/packages/bundled-features/src/custom-fields/changes.json +6 -0
- package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +2 -2
- package/packages/bundled-features/src/custom-fields/handlers/update-tenant-field.write.ts +0 -1
- package/packages/bundled-features/src/custom-fields/lib/value-schema.ts +10 -11
- package/packages/bundled-features/src/custom-fields/wire-user-data-rights.ts +7 -6
- package/packages/bundled-features/src/data-retention/changes.json +6 -0
- package/packages/bundled-features/src/data-retention/index.ts +1 -0
- package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +8 -6
- package/packages/bundled-features/src/delivery/attachment-ref.ts +105 -0
- package/packages/bundled-features/src/delivery/changes.json +66 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +152 -4
- package/packages/bundled-features/src/delivery/constants.ts +2 -0
- package/packages/bundled-features/src/delivery/delivery-service.ts +104 -26
- package/packages/bundled-features/src/delivery/feature.ts +1 -1
- package/packages/bundled-features/src/delivery/index.ts +19 -2
- package/packages/bundled-features/src/delivery/jobs.ts +97 -17
- package/packages/bundled-features/src/delivery/notify-factory.ts +14 -9
- package/packages/bundled-features/src/delivery/testing.ts +9 -1
- package/packages/bundled-features/src/delivery/types.ts +34 -18
- package/packages/bundled-features/src/delivery/unsubscribe.ts +5 -2
- package/packages/bundled-features/src/derivatives-sharp/render.ts +1 -22
- package/packages/bundled-features/src/document-ingest-foundation/pages.ts +6 -3
- package/packages/bundled-features/src/feature-toggles/toggle-sync-signal.ts +4 -4
- 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/index.ts +7 -0
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +89 -0
- 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/folders/web/folder-manager.tsx +5 -2
- package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +11 -11
- package/packages/bundled-features/src/form-draft/db/queries/draft-count.ts +2 -2
- package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +2 -2
- package/packages/bundled-features/src/form-draft/release-file-refs.ts +3 -2
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +6 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/connect-account.write.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/ingest-message.write.ts +6 -6
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-messages.query.ts +1 -1
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/scope-visibility.ts +2 -2
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +13 -8
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-state.ts +7 -7
- package/packages/bundled-features/src/inbound-mail-foundation/retention-sweep.ts +2 -6
- package/packages/bundled-features/src/inbound-mail-foundation/tenant-destroy-hook.ts +5 -13
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -6
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +42 -25
- package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +4 -3
- package/packages/bundled-features/src/jobs/changes.json +19 -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/catalog.query.ts +1 -1
- 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 +52 -17
- 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/ledger/recurring.ts +1 -3
- package/packages/bundled-features/src/managed-pages/branding.ts +33 -9
- package/packages/bundled-features/src/managed-pages/changes.json +6 -0
- package/packages/bundled-features/src/managed-pages/feature.ts +1 -0
- package/packages/bundled-features/src/managed-pages/i18n.ts +1 -0
- package/packages/bundled-features/src/managed-pages/screens/branding-screen.ts +8 -0
- package/packages/bundled-features/src/page-render/branding.ts +3 -0
- package/packages/bundled-features/src/page-render/markdown.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/feature.ts +2 -4
- package/packages/bundled-features/src/personal-access-tokens/handlers/list.query.ts +2 -1
- package/packages/bundled-features/src/personal-access-tokens/index.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +2 -1
- package/packages/bundled-features/src/renderer-foundation/README.md +11 -10
- package/packages/bundled-features/src/renderer-foundation/api.ts +14 -14
- package/packages/bundled-features/src/renderer-foundation/changes.json +39 -0
- package/packages/bundled-features/src/renderer-foundation/constants.ts +6 -8
- package/packages/bundled-features/src/renderer-foundation/feature.ts +7 -7
- package/packages/bundled-features/src/renderer-foundation/index.ts +13 -2
- package/packages/bundled-features/src/renderer-foundation/types.ts +134 -35
- package/packages/bundled-features/src/renderer-pdf/changes.json +12 -0
- package/packages/bundled-features/src/renderer-pdf/feature.ts +7 -1
- package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +162 -82
- package/packages/bundled-features/src/secrets/changes.json +6 -0
- package/packages/bundled-features/src/secrets/feature.ts +4 -3
- package/packages/bundled-features/src/secrets/secrets-context.ts +23 -3
- package/packages/bundled-features/src/sessions/db/queries/cleanup.ts +2 -2
- package/packages/bundled-features/src/sessions/feature.ts +2 -4
- package/packages/bundled-features/src/sessions/handlers/list.query.ts +2 -1
- package/packages/bundled-features/src/shared/attachment-file-name.ts +39 -0
- 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 +6 -0
- package/packages/bundled-features/src/shared/is-tenant-db.ts +1 -1
- package/packages/bundled-features/src/shared/mail-template-body.ts +42 -0
- package/packages/bundled-features/src/shared/sharp-input-limits.ts +26 -0
- package/packages/bundled-features/src/shared/signup-handover.ts +4 -4
- 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/tags/handlers/assign-tag.write.ts +5 -1
- package/packages/bundled-features/src/tags/web/tag-chip.tsx +2 -1
- package/packages/bundled-features/src/template-resolver/README.md +66 -8
- package/packages/bundled-features/src/template-resolver/api.ts +85 -9
- package/packages/bundled-features/src/template-resolver/changes.json +186 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +278 -0
- package/packages/bundled-features/src/template-resolver/constants.ts +49 -11
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +335 -0
- package/packages/bundled-features/src/template-resolver/document-schema.ts +345 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +59 -6
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +188 -0
- package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +30 -2
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +160 -0
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +33 -0
- 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 +138 -0
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +9 -6
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +42 -0
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +22 -0
- package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +151 -0
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +19 -18
- package/packages/bundled-features/src/template-resolver/handlers/override-diff.ts +49 -0
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +323 -0
- package/packages/bundled-features/src/template-resolver/handlers/preview.query.ts +25 -0
- package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +58 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-link-pdf.write.ts +34 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +50 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-shared.ts +24 -0
- package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +135 -0
- package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +135 -0
- package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +32 -3
- package/packages/bundled-features/src/template-resolver/handlers/shared.ts +7 -2
- package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +196 -0
- package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +233 -0
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +137 -0
- package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +91 -17
- package/packages/bundled-features/src/template-resolver/handlers/upsert-system.write.ts +19 -71
- package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +16 -77
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +31 -0
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +116 -0
- package/packages/bundled-features/src/template-resolver/index.ts +111 -0
- package/packages/bundled-features/src/template-resolver/layout-appearance.ts +40 -0
- package/packages/bundled-features/src/template-resolver/layout-regions.ts +206 -0
- package/packages/bundled-features/src/template-resolver/linked-resources.ts +102 -0
- package/packages/bundled-features/src/template-resolver/pdf-constants.ts +12 -0
- package/packages/bundled-features/src/template-resolver/preview-display-html.ts +36 -0
- package/packages/bundled-features/src/template-resolver/print-document.ts +58 -0
- package/packages/bundled-features/src/template-resolver/qualified-names.ts +34 -0
- package/packages/bundled-features/src/template-resolver/record-mapping.ts +26 -0
- package/packages/bundled-features/src/template-resolver/resource-image.ts +47 -0
- package/packages/bundled-features/src/template-resolver/seeding.ts +171 -20
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +416 -0
- package/packages/bundled-features/src/template-resolver/table.ts +27 -1
- package/packages/bundled-features/src/template-resolver/template-bundle.ts +79 -0
- package/packages/bundled-features/src/template-resolver/template-diff.ts +78 -0
- package/packages/bundled-features/src/template-resolver/template-draft.ts +106 -0
- package/packages/bundled-features/src/template-resolver/template-meta.ts +192 -0
- package/packages/bundled-features/src/template-resolver/template-preview.ts +676 -0
- package/packages/bundled-features/src/template-resolver/template-resources.ts +114 -0
- package/packages/bundled-features/src/template-resolver/template-usage.ts +68 -0
- package/packages/bundled-features/src/template-resolver/template-validation.ts +230 -0
- package/packages/bundled-features/src/template-resolver/testing.ts +5 -5
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +20 -13
- package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +215 -0
- package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +37 -0
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +1065 -0
- package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +72 -0
- package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +26 -0
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +121 -0
- package/packages/bundled-features/src/template-resolver/web/index.ts +26 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor-lazy.tsx +36 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +176 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-text.ts +37 -0
- package/packages/bundled-features/src/template-resolver/web/use-stable-by-key.ts +14 -0
- package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -2
- package/packages/bundled-features/src/tenant/seeding.ts +41 -7
- package/packages/bundled-features/src/tenant-lifecycle/boot-checks.ts +1 -1
- package/packages/bundled-features/src/tenant-lifecycle/handlers/cancel-destruction.write.ts +2 -1
- package/packages/bundled-features/src/tier-engine/changes.json +6 -0
- package/packages/bundled-features/src/tier-engine/feature.ts +11 -3
- package/packages/bundled-features/src/user/changes.json +6 -0
- package/packages/bundled-features/src/user/db/queries/stream-tenant-backfill.ts +4 -4
- package/packages/bundled-features/src/user/handlers/detail.query.ts +3 -5
- package/packages/bundled-features/src/user/resolve-display-names.ts +8 -15
- package/packages/bundled-features/src/user/roles-input-schema.ts +2 -2
- package/packages/bundled-features/src/user/seeding.ts +19 -5
- package/packages/bundled-features/src/user-data-rights/changes.json +13 -0
- package/packages/bundled-features/src/user-data-rights/db/queries/forget-cleanup.ts +2 -2
- package/packages/bundled-features/src/user-data-rights/email-templates.ts +5 -149
- package/packages/bundled-features/src/user-data-rights/feature.ts +72 -35
- package/packages/bundled-features/src/user-data-rights/handlers/cancel-deletion.write.ts +2 -1
- package/packages/bundled-features/src/user-data-rights/handlers/request-deletion.write.ts +5 -11
- package/packages/bundled-features/src/user-data-rights/lib/default-mailers.ts +134 -53
- package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +2 -2
- package/packages/bundled-features/src/user-data-rights/mail-notifications.ts +157 -0
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +1 -1
- package/packages/bundled-features/src/user-data-rights/web/privacy-center-screen.tsx +2 -1
- package/packages/bundled-features/src/user-profile/changes.json +1 -1
- package/packages/bundled-features/src/user-profile/web/profile-screen.tsx +2 -1
- package/packages/bundled-features/src/waitlist/changes.json +7 -0
- package/packages/bundled-features/src/waitlist/feature.ts +17 -0
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +4 -5
- package/packages/bundled-features/src/waitlist/mail.ts +69 -18
- 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 +3 -3
- package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +7 -1
- package/samples/apps/ui-walkthrough/package.json +11 -0
- package/samples/apps/ui-walkthrough/playwright.config.ts +40 -7
- package/samples/apps/ui-walkthrough/src/app/auth-constants.ts +9 -0
- package/samples/apps/ui-walkthrough/src/app/client.tsx +6 -0
- package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +239 -0
- package/samples/apps/ui-walkthrough/src/app/server.ts +5 -1
- package/samples/apps/ui-walkthrough/src/app/shell.tsx +12 -1
- package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +570 -0
- package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +72 -0
- package/samples/apps/ui-walkthrough/src/features/billing/i18n.ts +18 -0
- package/samples/apps/ui-walkthrough/src/features/billing/index.ts +2 -0
- package/samples/apps/ui-walkthrough/src/features/billing/web/index.ts +7 -0
- package/samples/apps/ui-walkthrough/src/run-config.ts +41 -1
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +0 -2
- package/samples/apps/use-all-bundled/feature-manifest.json +118 -21
- package/samples/apps/use-all-bundled/kumiko/migrations/0045_template-resource-meta.rebuild.json +4 -0
- package/samples/apps/use-all-bundled/kumiko/migrations/0046_pdf-rendered.rebuild.json +4 -0
- package/samples/apps/use-all-bundled/package.json +2 -0
- package/samples/apps/use-all-bundled/playwright.config.ts +38 -3
- package/samples/apps/use-all-bundled/schema/generate.ts +13 -3
- package/samples/apps/use-all-bundled/schema/schema.generated.ts +3 -0
- package/samples/apps/use-all-bundled/scripts/check-coverage.ts +3 -0
- package/samples/apps/use-all-bundled/src/app/auth-constants.ts +3 -0
- package/samples/apps/use-all-bundled/src/app/client.tsx +4 -0
- package/samples/apps/use-all-bundled/src/app/seed-documents.ts +128 -0
- package/samples/apps/use-all-bundled/src/app/seed.ts +4 -0
- package/samples/apps/use-all-bundled/src/run-config.ts +11 -11
- package/samples/recipes/delivery-notifications/README.md +26 -25
- package/samples/recipes/delivery-notifications/package.json +2 -1
- package/samples/recipes/delivery-notifications/src/feature.ts +64 -58
- package/samples/recipes/seed-migration/README.md +1 -1
- package/packages/bundled-features/src/renderer-simple/changes.json +0 -27
- package/packages/bundled-features/src/renderer-simple/feature.ts +0 -66
- package/packages/bundled-features/src/renderer-simple/index.ts +0 -8
- package/packages/bundled-features/src/renderer-simple/mail-blocks.ts +0 -138
- package/packages/bundled-features/src/renderer-simple/resolve-variables.ts +0 -57
- package/packages/bundled-features/src/renderer-simple/simple-renderer.ts +0 -304
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import type {
|
|
3
|
+
HandlerContext,
|
|
4
|
+
SessionUser,
|
|
5
|
+
TemplateListVariableDef,
|
|
6
|
+
TemplateScalarVariableDef,
|
|
7
|
+
TemplateVariables,
|
|
8
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
9
|
+
import {
|
|
10
|
+
type KumikoError,
|
|
11
|
+
NotFoundError,
|
|
12
|
+
UnprocessableError,
|
|
13
|
+
ValidationError,
|
|
14
|
+
} from "@cosmicdrift/kumiko-framework/errors";
|
|
15
|
+
import { fileRefsTable } from "@cosmicdrift/kumiko-framework/files";
|
|
16
|
+
import { escapeHtml } from "@cosmicdrift/kumiko-headless";
|
|
17
|
+
import { createEmailAttachmentReader, createTemplateMailRenderer } from "../delivery/index.js";
|
|
18
|
+
import { fileContextForTenant } from "../file-foundation/index.js";
|
|
19
|
+
import {
|
|
20
|
+
collectRendererPlugins,
|
|
21
|
+
createRendererFoundationApi,
|
|
22
|
+
type MailAttachmentRef,
|
|
23
|
+
RendererError,
|
|
24
|
+
type RendererPlugin,
|
|
25
|
+
} from "../renderer-foundation/index.js";
|
|
26
|
+
import { createTemplateResolverApi, TemplateNotFoundError, type TemplateResource } from "./api.js";
|
|
27
|
+
import { collectDocumentResourceKeys } from "./compile-document-body.js";
|
|
28
|
+
import {
|
|
29
|
+
DEFAULT_MAIL_LAYOUT_SLUG,
|
|
30
|
+
MAIL_PREVIEW_MAX_BYTES,
|
|
31
|
+
SYSTEM_TENANT_ID,
|
|
32
|
+
type TemplateScope,
|
|
33
|
+
} from "./constants.js";
|
|
34
|
+
import { parseStoredJson } from "./handlers/template-gates.js";
|
|
35
|
+
import { validateTemplateDraft } from "./handlers/validate.query.js";
|
|
36
|
+
import {
|
|
37
|
+
type LayoutRegions,
|
|
38
|
+
readLayoutRegions,
|
|
39
|
+
sanitizeLayoutHtml,
|
|
40
|
+
type TemplateWriteScope,
|
|
41
|
+
} from "./layout-regions.js";
|
|
42
|
+
import { type LinkedResourceEntry, parseLinkedResourceEntries } from "./linked-resources.js";
|
|
43
|
+
import { buildDisplayHtml } from "./preview-display-html.js";
|
|
44
|
+
import { TemplateResolverErrors } from "./qualified-names.js";
|
|
45
|
+
import { type TemplateResourceRow, templateResourcesTable } from "./table.js";
|
|
46
|
+
import {
|
|
47
|
+
isMailTemplateMeta,
|
|
48
|
+
type MailTemplateMeta,
|
|
49
|
+
type PdfTemplateMeta,
|
|
50
|
+
parseTemplateMeta,
|
|
51
|
+
readPdfTemplateMeta,
|
|
52
|
+
} from "./template-meta.js";
|
|
53
|
+
|
|
54
|
+
const SAMPLE_LINK = "https://example.com";
|
|
55
|
+
const SAMPLE_DATE = "2026-01-15";
|
|
56
|
+
const SAMPLE_LIST_ROWS = 3;
|
|
57
|
+
const DEFAULT_PDF_LAYOUT_SLUG = "default";
|
|
58
|
+
|
|
59
|
+
function sampleScalar(name: string, def: TemplateScalarVariableDef): unknown {
|
|
60
|
+
if (def.example !== undefined) return def.example;
|
|
61
|
+
switch (def.type) {
|
|
62
|
+
case "text":
|
|
63
|
+
return `[${def.label ?? name}]`;
|
|
64
|
+
case "richText":
|
|
65
|
+
return `<p>[${escapeHtml(def.label ?? name)}]</p>`;
|
|
66
|
+
case "link":
|
|
67
|
+
return SAMPLE_LINK;
|
|
68
|
+
case "money":
|
|
69
|
+
return 0;
|
|
70
|
+
case "date":
|
|
71
|
+
return SAMPLE_DATE;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function sampleRow(
|
|
76
|
+
item: TemplateListVariableDef["item"],
|
|
77
|
+
given: Readonly<Record<string, unknown>> = {},
|
|
78
|
+
): Record<string, unknown> {
|
|
79
|
+
const row: Record<string, unknown> = {};
|
|
80
|
+
for (const [name, def] of Object.entries(item)) row[name] = sampleScalar(name, def);
|
|
81
|
+
return { ...row, ...given };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function sampleList(def: TemplateListVariableDef): Record<string, unknown>[] {
|
|
85
|
+
const rows = (def.example ?? [])
|
|
86
|
+
.slice(0, SAMPLE_LIST_ROWS)
|
|
87
|
+
.map((given) => sampleRow(def.item, given));
|
|
88
|
+
while (rows.length < SAMPLE_LIST_ROWS) rows.push(sampleRow(def.item));
|
|
89
|
+
return rows;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Declared examples win; the fallbacks are deterministic so a preview never differs between calls.
|
|
93
|
+
export function sampleValuesFor(variables: TemplateVariables): Record<string, unknown> {
|
|
94
|
+
const values: Record<string, unknown> = {};
|
|
95
|
+
for (const [name, def] of Object.entries(variables)) {
|
|
96
|
+
values[name] = def.type === "list" ? sampleList(def) : sampleScalar(name, def);
|
|
97
|
+
}
|
|
98
|
+
return values;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type PreviewCheckCode =
|
|
102
|
+
| "resources_resolved"
|
|
103
|
+
| "text_present"
|
|
104
|
+
| "size_under_cap"
|
|
105
|
+
| "required_placeholders";
|
|
106
|
+
|
|
107
|
+
export type PreviewCheck = {
|
|
108
|
+
readonly code: PreviewCheckCode;
|
|
109
|
+
readonly ok: boolean;
|
|
110
|
+
readonly i18nKey: string;
|
|
111
|
+
readonly params?: Readonly<Record<string, unknown>>;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export type PreviewAttachment =
|
|
115
|
+
| {
|
|
116
|
+
readonly kind: "inline";
|
|
117
|
+
readonly cid: string;
|
|
118
|
+
readonly name: string;
|
|
119
|
+
readonly sizeBytes: number;
|
|
120
|
+
}
|
|
121
|
+
| { readonly kind: "file"; readonly name: string; readonly sizeBytes: number }
|
|
122
|
+
// Rendered when the mail is sent, so the preview knows the name but not the size.
|
|
123
|
+
| {
|
|
124
|
+
readonly kind: "rendered-pdf";
|
|
125
|
+
readonly name: string;
|
|
126
|
+
readonly templateSlug: string;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export type PreviewLayout = {
|
|
130
|
+
readonly slug: string;
|
|
131
|
+
readonly scope: TemplateScope;
|
|
132
|
+
readonly html: string;
|
|
133
|
+
readonly regions: LayoutRegions;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export type TemplatePreviewInput = {
|
|
137
|
+
readonly slug: string;
|
|
138
|
+
readonly kind: "mail" | "document-pdf";
|
|
139
|
+
readonly locale: string;
|
|
140
|
+
readonly content?: string | undefined;
|
|
141
|
+
readonly meta?: unknown;
|
|
142
|
+
readonly variableSchema: Readonly<Record<string, unknown>>;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
type ValidationResponse = Awaited<ReturnType<typeof validateTemplateDraft>>["response"];
|
|
146
|
+
|
|
147
|
+
export type TemplatePreview = {
|
|
148
|
+
readonly kind: TemplatePreviewInput["kind"];
|
|
149
|
+
readonly subject: string | null;
|
|
150
|
+
readonly previewText: string | null;
|
|
151
|
+
readonly html: string;
|
|
152
|
+
/** Mail only: `html` with resolved inline images as data URIs, for showing in a browser. */
|
|
153
|
+
readonly displayHtml?: string;
|
|
154
|
+
readonly text: string | null;
|
|
155
|
+
readonly attachments: readonly PreviewAttachment[];
|
|
156
|
+
readonly layout: PreviewLayout | null;
|
|
157
|
+
/** Mail only: which layout version the html was rendered through. */
|
|
158
|
+
readonly mailLayout?: {
|
|
159
|
+
readonly slug: string;
|
|
160
|
+
readonly scope: TemplateScope;
|
|
161
|
+
readonly version: number;
|
|
162
|
+
} | null;
|
|
163
|
+
readonly sampleValues: Readonly<Record<string, unknown>>;
|
|
164
|
+
readonly checks: readonly PreviewCheck[];
|
|
165
|
+
readonly validation: ValidationResponse;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export type TemplatePreviewOutcome =
|
|
169
|
+
| {
|
|
170
|
+
readonly ok: true;
|
|
171
|
+
readonly preview: TemplatePreview;
|
|
172
|
+
/** What the send path needs to attach; kept out of the response shown to the editor. */
|
|
173
|
+
readonly mailAttachments: readonly MailAttachmentRef[];
|
|
174
|
+
readonly sender: { readonly fromName?: string; readonly replyTo?: string };
|
|
175
|
+
}
|
|
176
|
+
| { readonly ok: false; readonly error: KumikoError };
|
|
177
|
+
|
|
178
|
+
const utf8Bytes = (value: string): number => new TextEncoder().encode(value).length;
|
|
179
|
+
|
|
180
|
+
const failed = (error: KumikoError): TemplatePreviewOutcome => ({ ok: false, error });
|
|
181
|
+
|
|
182
|
+
async function loadStoredDraft(
|
|
183
|
+
ctx: HandlerContext,
|
|
184
|
+
user: SessionUser,
|
|
185
|
+
input: TemplatePreviewInput,
|
|
186
|
+
): Promise<TemplateResource | null> {
|
|
187
|
+
try {
|
|
188
|
+
return await createTemplateResolverApi(ctx.db).resolveTemplate({
|
|
189
|
+
tenantId: user.tenantId,
|
|
190
|
+
slug: input.slug,
|
|
191
|
+
kind: input.kind,
|
|
192
|
+
locale: input.locale,
|
|
193
|
+
});
|
|
194
|
+
} catch (e) {
|
|
195
|
+
if (e instanceof TemplateNotFoundError) return null;
|
|
196
|
+
throw e;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function describeAttachments(
|
|
201
|
+
ctx: HandlerContext,
|
|
202
|
+
refs: readonly MailAttachmentRef[],
|
|
203
|
+
): Promise<{ readonly attachments: PreviewAttachment[]; readonly missing: string[] }> {
|
|
204
|
+
const fileRefIds = refs.flatMap((ref) => (ref.kind === "rendered-pdf" ? [] : [ref.fileRefId]));
|
|
205
|
+
const rows =
|
|
206
|
+
fileRefIds.length === 0
|
|
207
|
+
? []
|
|
208
|
+
: await selectMany<{ id: string; size: number | bigint | string }>(ctx.db, fileRefsTable, {
|
|
209
|
+
id: fileRefIds,
|
|
210
|
+
});
|
|
211
|
+
const sizeById = new Map(rows.map((row) => [row.id, Number(row.size)]));
|
|
212
|
+
// An inline image is named by its content id because the stored file name is personal data and
|
|
213
|
+
// may be ciphertext; a file attachment carries the name the admin gave it.
|
|
214
|
+
const attachments = refs.map((ref) => previewAttachmentOf(ref, sizeById));
|
|
215
|
+
// A fileRef without a row has no known size: it is reported, not counted as 0 bytes.
|
|
216
|
+
const missing = refs.flatMap((ref, index) =>
|
|
217
|
+
ref.kind !== "rendered-pdf" && !sizeById.has(ref.fileRefId)
|
|
218
|
+
? [attachments[index]?.name ?? ""]
|
|
219
|
+
: [],
|
|
220
|
+
);
|
|
221
|
+
return { attachments, missing };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function previewAttachmentOf(
|
|
225
|
+
ref: MailAttachmentRef,
|
|
226
|
+
sizeById: ReadonlyMap<string, number>,
|
|
227
|
+
): PreviewAttachment {
|
|
228
|
+
switch (ref.kind) {
|
|
229
|
+
case "inline":
|
|
230
|
+
return {
|
|
231
|
+
kind: "inline",
|
|
232
|
+
cid: ref.cid,
|
|
233
|
+
name: ref.cid,
|
|
234
|
+
sizeBytes: sizeById.get(ref.fileRefId) ?? 0,
|
|
235
|
+
};
|
|
236
|
+
case "file":
|
|
237
|
+
return { kind: "file", name: ref.fileName, sizeBytes: sizeById.get(ref.fileRefId) ?? 0 };
|
|
238
|
+
case "rendered-pdf":
|
|
239
|
+
return { kind: "rendered-pdf", name: ref.fileName, templateSlug: ref.templateSlug };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Part headers and multipart boundary lines around one attachment in the MIME message.
|
|
244
|
+
export const MIME_PART_OVERHEAD_BYTES = 512;
|
|
245
|
+
|
|
246
|
+
// The provider cap applies to the encoded message: attachments travel base64-encoded.
|
|
247
|
+
export function encodedAttachmentBytes(rawBytes: number): number {
|
|
248
|
+
return Math.ceil(rawBytes / 3) * 4 + MIME_PART_OVERHEAD_BYTES;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const attachmentSizeOf = (attachment: PreviewAttachment): number =>
|
|
252
|
+
attachment.kind === "rendered-pdf" ? 0 : encodedAttachmentBytes(attachment.sizeBytes);
|
|
253
|
+
|
|
254
|
+
export function estimateMailBytes(args: {
|
|
255
|
+
readonly html: string;
|
|
256
|
+
readonly text: string;
|
|
257
|
+
readonly attachments: readonly PreviewAttachment[];
|
|
258
|
+
}): number {
|
|
259
|
+
return (
|
|
260
|
+
utf8Bytes(args.html) +
|
|
261
|
+
utf8Bytes(args.text) +
|
|
262
|
+
args.attachments.reduce((sum, attachment) => sum + attachmentSizeOf(attachment), 0)
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// The row an editor is working on, whatever its status: the stored template only resolves when
|
|
267
|
+
// active, so a draft would otherwise lose the images and attachments uploaded to it.
|
|
268
|
+
async function loadEditedRowResources(
|
|
269
|
+
ctx: HandlerContext,
|
|
270
|
+
user: SessionUser,
|
|
271
|
+
input: TemplatePreviewInput,
|
|
272
|
+
): Promise<Record<string, LinkedResourceEntry> | undefined> {
|
|
273
|
+
const rows = await selectMany<TemplateResourceRow>(
|
|
274
|
+
ctx.db,
|
|
275
|
+
templateResourcesTable,
|
|
276
|
+
{ tenantId: user.tenantId, slug: input.slug, kind: "mail", locale: input.locale },
|
|
277
|
+
{ limit: 1 },
|
|
278
|
+
);
|
|
279
|
+
const row = rows[0];
|
|
280
|
+
return row === undefined ? undefined : parseLinkedResourceEntries(row.linkedResources);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function resolvePdfLayout(
|
|
284
|
+
ctx: HandlerContext,
|
|
285
|
+
user: SessionUser,
|
|
286
|
+
slug: string,
|
|
287
|
+
locale: string,
|
|
288
|
+
): Promise<PreviewLayout | null> {
|
|
289
|
+
const api = createTemplateResolverApi(ctx.db);
|
|
290
|
+
const find = async (tenantId: string): Promise<TemplateResource | null> => {
|
|
291
|
+
try {
|
|
292
|
+
return await api.resolveLayout({ tenantId, slug, kind: "pdf-layout", locale });
|
|
293
|
+
} catch (e) {
|
|
294
|
+
if (e instanceof TemplateNotFoundError) return null;
|
|
295
|
+
throw e;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
let layout = await find(user.tenantId);
|
|
299
|
+
let html = layout?.content ?? "";
|
|
300
|
+
if (layout !== null && layout.scope !== "system") {
|
|
301
|
+
// Re-checked like the mail renderer does: a tenant row that predates the write gate must not pass.
|
|
302
|
+
const sanitized = sanitizeLayoutHtml(html);
|
|
303
|
+
if (sanitized.ok) html = sanitized.html;
|
|
304
|
+
else {
|
|
305
|
+
layout = await find(SYSTEM_TENANT_ID);
|
|
306
|
+
html = layout?.content ?? "";
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (layout === null) return null;
|
|
310
|
+
return { slug, scope: layout.scope, html, regions: readLayoutRegions(html) };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function missingResourceCheck(missing: readonly string[]): PreviewCheck {
|
|
314
|
+
return {
|
|
315
|
+
code: "resources_resolved",
|
|
316
|
+
ok: missing.length === 0,
|
|
317
|
+
i18nKey: "templateResolver.checks.resourcesResolved",
|
|
318
|
+
params: { missing },
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function requiredPlaceholdersCheck(missing: readonly string[]): PreviewCheck {
|
|
323
|
+
return {
|
|
324
|
+
code: "required_placeholders",
|
|
325
|
+
ok: missing.length === 0,
|
|
326
|
+
i18nKey: "templateResolver.checks.requiredPlaceholders",
|
|
327
|
+
params: { missing },
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function toRenderFailure(e: unknown): TemplatePreviewOutcome {
|
|
332
|
+
if (!(e instanceof RendererError)) throw e;
|
|
333
|
+
return failed(
|
|
334
|
+
new UnprocessableError(TemplateResolverErrors.previewRenderFailed, {
|
|
335
|
+
i18nKey: "templateResolver.errors.preview.renderFailed",
|
|
336
|
+
details: { code: e.code },
|
|
337
|
+
}),
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
type DocumentPreviewArgs = {
|
|
342
|
+
readonly input: TemplatePreviewInput;
|
|
343
|
+
readonly content: string;
|
|
344
|
+
readonly meta: ReturnType<typeof parseTemplateMeta>;
|
|
345
|
+
readonly variableSchema: Readonly<Record<string, unknown>>;
|
|
346
|
+
readonly sampleValues: Readonly<Record<string, unknown>>;
|
|
347
|
+
readonly validation: ValidationResponse;
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
// Only a plugin that prints documents can show the filled-in page; without one the editor
|
|
351
|
+
// still gets the body and the raw layout, just without placeholder values.
|
|
352
|
+
function documentPreviewPlugin(ctx: HandlerContext, tenantId: string): RendererPlugin | null {
|
|
353
|
+
try {
|
|
354
|
+
const plugin = createRendererFoundationApi(
|
|
355
|
+
collectRendererPlugins(ctx.registry),
|
|
356
|
+
).createRendererForTenant({ tenantId, kind: "document-pdf" });
|
|
357
|
+
return plugin.previewDocument === undefined ? null : plugin;
|
|
358
|
+
} catch (e) {
|
|
359
|
+
if (e instanceof RendererError && e.code === "no_plugin_for_kind") return null;
|
|
360
|
+
throw e;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function renderDocumentPreview(
|
|
365
|
+
ctx: HandlerContext,
|
|
366
|
+
user: SessionUser,
|
|
367
|
+
args: DocumentPreviewArgs,
|
|
368
|
+
): Promise<TemplatePreviewOutcome> {
|
|
369
|
+
const { input, content, meta, validation, sampleValues } = args;
|
|
370
|
+
const pdfMeta = meta.ok ? readPdfTemplateMeta(meta.meta) : null;
|
|
371
|
+
const requiredCheck = requiredPlaceholdersCheck(validation.missingRequired);
|
|
372
|
+
const plugin = documentPreviewPlugin(ctx, user.tenantId);
|
|
373
|
+
|
|
374
|
+
if (plugin?.previewDocument === undefined) {
|
|
375
|
+
return {
|
|
376
|
+
ok: true,
|
|
377
|
+
mailAttachments: [],
|
|
378
|
+
sender: {},
|
|
379
|
+
preview: {
|
|
380
|
+
kind: "document-pdf",
|
|
381
|
+
subject: null,
|
|
382
|
+
previewText: null,
|
|
383
|
+
html: content,
|
|
384
|
+
text: null,
|
|
385
|
+
attachments: [],
|
|
386
|
+
layout: await resolvePdfLayout(
|
|
387
|
+
ctx,
|
|
388
|
+
user,
|
|
389
|
+
pdfMeta?.layoutSlug ?? DEFAULT_PDF_LAYOUT_SLUG,
|
|
390
|
+
input.locale,
|
|
391
|
+
),
|
|
392
|
+
sampleValues,
|
|
393
|
+
checks: [missingResourceCheck(validation.warnings.map((w) => w.detail)), requiredCheck],
|
|
394
|
+
validation,
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
let document: Awaited<ReturnType<NonNullable<RendererPlugin["previewDocument"]>>>;
|
|
400
|
+
try {
|
|
401
|
+
document = await plugin.previewDocument(
|
|
402
|
+
{
|
|
403
|
+
slug: input.slug,
|
|
404
|
+
locale: input.locale,
|
|
405
|
+
variables: sampleValues,
|
|
406
|
+
draft: { content, meta: pdfMeta, variableSchema: args.variableSchema },
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
db: ctx.db,
|
|
410
|
+
registry: ctx.registry,
|
|
411
|
+
tenantId: user.tenantId,
|
|
412
|
+
...(ctx.config !== undefined && { config: ctx.config }),
|
|
413
|
+
filesForTenant: (fileTenantId) =>
|
|
414
|
+
fileContextForTenant(ctx, ctx.user.tenantId, fileTenantId),
|
|
415
|
+
},
|
|
416
|
+
);
|
|
417
|
+
} catch (e) {
|
|
418
|
+
return toRenderFailure(e);
|
|
419
|
+
}
|
|
420
|
+
const layout = document.layout;
|
|
421
|
+
return {
|
|
422
|
+
ok: true,
|
|
423
|
+
mailAttachments: [],
|
|
424
|
+
sender: {},
|
|
425
|
+
preview: {
|
|
426
|
+
kind: "document-pdf",
|
|
427
|
+
subject: null,
|
|
428
|
+
previewText: null,
|
|
429
|
+
html: document.bodyHtml,
|
|
430
|
+
text: null,
|
|
431
|
+
attachments: [],
|
|
432
|
+
layout:
|
|
433
|
+
layout === null
|
|
434
|
+
? null
|
|
435
|
+
: {
|
|
436
|
+
slug: layout.slug,
|
|
437
|
+
scope: layout.scope,
|
|
438
|
+
html: `${layout.headerHtml}${layout.signatureHtml}${layout.footerHtml}`,
|
|
439
|
+
regions: {
|
|
440
|
+
header: layout.headerHtml,
|
|
441
|
+
signature: layout.signatureHtml,
|
|
442
|
+
footer: layout.footerHtml,
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
sampleValues,
|
|
446
|
+
checks: [missingResourceCheck(document.missingResourceKeys), requiredCheck],
|
|
447
|
+
validation,
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export type DocumentDraftOutcome =
|
|
453
|
+
| {
|
|
454
|
+
readonly ok: true;
|
|
455
|
+
readonly content: string;
|
|
456
|
+
readonly meta: PdfTemplateMeta | null;
|
|
457
|
+
readonly variableSchema: Readonly<Record<string, unknown>>;
|
|
458
|
+
readonly sampleValues: Readonly<Record<string, unknown>>;
|
|
459
|
+
}
|
|
460
|
+
| { readonly ok: false; readonly error: KumikoError };
|
|
461
|
+
|
|
462
|
+
// The caller derives `scope` once from the role (SystemAdmin edits the system row), so the write
|
|
463
|
+
// gates and the print see the same row: a failing draft is rejected with the same problems as a save.
|
|
464
|
+
export async function evaluateDocumentDraft(
|
|
465
|
+
ctx: HandlerContext,
|
|
466
|
+
user: SessionUser,
|
|
467
|
+
scope: TemplateWriteScope,
|
|
468
|
+
draft: {
|
|
469
|
+
readonly slug: string;
|
|
470
|
+
readonly locale: string;
|
|
471
|
+
readonly content: string;
|
|
472
|
+
readonly meta?: unknown;
|
|
473
|
+
readonly variableSchema: Readonly<Record<string, unknown>>;
|
|
474
|
+
},
|
|
475
|
+
): Promise<DocumentDraftOutcome> {
|
|
476
|
+
const { response, evaluation } = await validateTemplateDraft(ctx, user, {
|
|
477
|
+
slug: draft.slug,
|
|
478
|
+
kind: "document-pdf",
|
|
479
|
+
contentFormat: "html",
|
|
480
|
+
content: draft.content,
|
|
481
|
+
meta: draft.meta,
|
|
482
|
+
variableSchema: draft.variableSchema,
|
|
483
|
+
locale: draft.locale,
|
|
484
|
+
scope,
|
|
485
|
+
});
|
|
486
|
+
if (!evaluation.ok) {
|
|
487
|
+
return { ok: false, error: new ValidationError({ fields: [...evaluation.issues] }) };
|
|
488
|
+
}
|
|
489
|
+
const meta = parseTemplateMeta("document-pdf", parseStoredJson(evaluation.metaJson));
|
|
490
|
+
return {
|
|
491
|
+
ok: true,
|
|
492
|
+
content: evaluation.content ?? "",
|
|
493
|
+
meta: meta.ok ? readPdfTemplateMeta(meta.meta) : null,
|
|
494
|
+
variableSchema: draft.variableSchema,
|
|
495
|
+
sampleValues: sampleValuesFor(response.variables),
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
type DraftSource = {
|
|
500
|
+
readonly content: string;
|
|
501
|
+
readonly meta: unknown;
|
|
502
|
+
readonly variableSchema: Readonly<Record<string, unknown>>;
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
// The posted draft when it carries content, else the stored template; null when none is stored.
|
|
506
|
+
async function resolveDraftSource(
|
|
507
|
+
ctx: HandlerContext,
|
|
508
|
+
user: SessionUser,
|
|
509
|
+
input: TemplatePreviewInput,
|
|
510
|
+
): Promise<DraftSource | null> {
|
|
511
|
+
if (input.content !== undefined) {
|
|
512
|
+
return { content: input.content, meta: input.meta, variableSchema: input.variableSchema };
|
|
513
|
+
}
|
|
514
|
+
const stored = await loadStoredDraft(ctx, user, input);
|
|
515
|
+
if (stored === null) return null;
|
|
516
|
+
return { content: stored.content, meta: stored.meta, variableSchema: stored.variableSchema };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async function renderMailPreview(
|
|
520
|
+
ctx: HandlerContext,
|
|
521
|
+
user: SessionUser,
|
|
522
|
+
args: DocumentPreviewArgs,
|
|
523
|
+
): Promise<TemplatePreviewOutcome> {
|
|
524
|
+
const {
|
|
525
|
+
input,
|
|
526
|
+
content,
|
|
527
|
+
meta,
|
|
528
|
+
validation,
|
|
529
|
+
sampleValues,
|
|
530
|
+
variableSchema: draftVariableSchema,
|
|
531
|
+
} = args;
|
|
532
|
+
if (!meta.ok || !isMailTemplateMeta(meta.meta)) {
|
|
533
|
+
return failed(
|
|
534
|
+
new UnprocessableError(TemplateResolverErrors.previewRenderFailed, {
|
|
535
|
+
i18nKey: "templateResolver.errors.preview.renderFailed",
|
|
536
|
+
details: { code: "invalid_meta" },
|
|
537
|
+
}),
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
const mailMeta: MailTemplateMeta = meta.meta;
|
|
541
|
+
const editedResources =
|
|
542
|
+
input.content === undefined ? undefined : await loadEditedRowResources(ctx, user, input);
|
|
543
|
+
let rendered: Awaited<ReturnType<ReturnType<typeof createTemplateMailRenderer>>>;
|
|
544
|
+
try {
|
|
545
|
+
rendered = await createTemplateMailRenderer(
|
|
546
|
+
ctx.db,
|
|
547
|
+
ctx.registry,
|
|
548
|
+
user.tenantId,
|
|
549
|
+
ctx.config,
|
|
550
|
+
)({
|
|
551
|
+
slug: input.slug,
|
|
552
|
+
locale: input.locale,
|
|
553
|
+
variables: sampleValues,
|
|
554
|
+
draft: {
|
|
555
|
+
content,
|
|
556
|
+
meta: mailMeta,
|
|
557
|
+
variableSchema: draftVariableSchema,
|
|
558
|
+
...(editedResources !== undefined && { linkedResources: editedResources }),
|
|
559
|
+
},
|
|
560
|
+
});
|
|
561
|
+
} catch (e) {
|
|
562
|
+
return toRenderFailure(e);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const mailAttachments = rendered.attachments ?? [];
|
|
566
|
+
const inline = mailAttachments.filter((ref) => ref.kind === "inline");
|
|
567
|
+
const { attachments, missing: missingAttachments } = await describeAttachments(
|
|
568
|
+
ctx,
|
|
569
|
+
mailAttachments,
|
|
570
|
+
);
|
|
571
|
+
const displayHtml = await buildDisplayHtml(
|
|
572
|
+
rendered.html,
|
|
573
|
+
inline,
|
|
574
|
+
createEmailAttachmentReader(ctx.db, ctx.registry, user.tenantId, ctx.config, (tenantId) =>
|
|
575
|
+
fileContextForTenant(ctx, ctx.user.tenantId, tenantId),
|
|
576
|
+
),
|
|
577
|
+
);
|
|
578
|
+
const resolvedCids = new Set(inline.map((ref) => ref.cid));
|
|
579
|
+
const unresolved = collectDocumentResourceKeys(content).filter((key) => !resolvedCids.has(key));
|
|
580
|
+
const totalBytes = estimateMailBytes({ html: rendered.html, text: rendered.text, attachments });
|
|
581
|
+
return {
|
|
582
|
+
ok: true,
|
|
583
|
+
mailAttachments,
|
|
584
|
+
sender: {
|
|
585
|
+
...(rendered.fromName !== undefined && { fromName: rendered.fromName }),
|
|
586
|
+
...(rendered.replyTo !== undefined && { replyTo: rendered.replyTo }),
|
|
587
|
+
},
|
|
588
|
+
preview: {
|
|
589
|
+
kind: "mail",
|
|
590
|
+
subject: rendered.subject,
|
|
591
|
+
previewText: rendered.previewText ?? null,
|
|
592
|
+
html: rendered.html,
|
|
593
|
+
displayHtml,
|
|
594
|
+
text: rendered.text,
|
|
595
|
+
attachments,
|
|
596
|
+
layout: null,
|
|
597
|
+
mailLayout:
|
|
598
|
+
rendered.layout === undefined
|
|
599
|
+
? null
|
|
600
|
+
: {
|
|
601
|
+
slug: mailMeta.layoutSlug ?? DEFAULT_MAIL_LAYOUT_SLUG,
|
|
602
|
+
scope: rendered.layout.scope,
|
|
603
|
+
version: rendered.layout.templateVersion,
|
|
604
|
+
},
|
|
605
|
+
sampleValues,
|
|
606
|
+
checks: [
|
|
607
|
+
missingResourceCheck([...unresolved, ...missingAttachments]),
|
|
608
|
+
{
|
|
609
|
+
code: "text_present",
|
|
610
|
+
ok: rendered.text.trim() !== "",
|
|
611
|
+
i18nKey: "templateResolver.checks.textPresent",
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
code: "size_under_cap",
|
|
615
|
+
ok: totalBytes <= MAIL_PREVIEW_MAX_BYTES,
|
|
616
|
+
i18nKey: "templateResolver.checks.sizeUnderCap",
|
|
617
|
+
params: { totalBytes, maxBytes: MAIL_PREVIEW_MAX_BYTES },
|
|
618
|
+
},
|
|
619
|
+
requiredPlaceholdersCheck(validation.missingRequired),
|
|
620
|
+
],
|
|
621
|
+
validation,
|
|
622
|
+
},
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Renders a draft (or, without content, the stored template) with sample values, going through
|
|
627
|
+
// the same write gates and the same mail renderer as a real send. Nothing is stored or sent.
|
|
628
|
+
export async function renderTemplatePreview(
|
|
629
|
+
ctx: HandlerContext,
|
|
630
|
+
user: SessionUser,
|
|
631
|
+
input: TemplatePreviewInput,
|
|
632
|
+
): Promise<TemplatePreviewOutcome> {
|
|
633
|
+
const draftSource = await resolveDraftSource(ctx, user, input);
|
|
634
|
+
if (draftSource === null) return failed(new NotFoundError("template-resource", input.slug));
|
|
635
|
+
const {
|
|
636
|
+
content: draftContent,
|
|
637
|
+
meta: draftMeta,
|
|
638
|
+
variableSchema: draftVariableSchema,
|
|
639
|
+
} = draftSource;
|
|
640
|
+
|
|
641
|
+
const { response: validation, evaluation } = await validateTemplateDraft(ctx, user, {
|
|
642
|
+
slug: input.slug,
|
|
643
|
+
kind: input.kind,
|
|
644
|
+
contentFormat: "html",
|
|
645
|
+
content: draftContent,
|
|
646
|
+
meta: draftMeta,
|
|
647
|
+
variableSchema: draftVariableSchema,
|
|
648
|
+
locale: input.locale,
|
|
649
|
+
scope: "tenant",
|
|
650
|
+
});
|
|
651
|
+
if (!evaluation.ok) return failed(new ValidationError({ fields: [...evaluation.issues] }));
|
|
652
|
+
|
|
653
|
+
const content = evaluation.content ?? "";
|
|
654
|
+
const meta = parseTemplateMeta(input.kind, parseStoredJson(evaluation.metaJson));
|
|
655
|
+
const sampleValues = sampleValuesFor(validation.variables);
|
|
656
|
+
|
|
657
|
+
if (input.kind === "document-pdf") {
|
|
658
|
+
return renderDocumentPreview(ctx, user, {
|
|
659
|
+
input,
|
|
660
|
+
content,
|
|
661
|
+
meta,
|
|
662
|
+
variableSchema: draftVariableSchema,
|
|
663
|
+
sampleValues,
|
|
664
|
+
validation,
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return renderMailPreview(ctx, user, {
|
|
669
|
+
input,
|
|
670
|
+
content,
|
|
671
|
+
meta,
|
|
672
|
+
variableSchema: draftVariableSchema,
|
|
673
|
+
sampleValues,
|
|
674
|
+
validation,
|
|
675
|
+
});
|
|
676
|
+
}
|