@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,151 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import * as z from "zod";
|
|
4
|
+
import { FALLBACK_LOCALE } from "../constants.js";
|
|
5
|
+
import { type LayoutRegions, readLayoutRegions } from "../layout-regions.js";
|
|
6
|
+
import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
|
|
7
|
+
import {
|
|
8
|
+
type ActiveInfo,
|
|
9
|
+
draftViewerTenantId,
|
|
10
|
+
isHiddenSystemDraft,
|
|
11
|
+
toWorkingRow,
|
|
12
|
+
type WorkingRow,
|
|
13
|
+
} from "../template-draft.js";
|
|
14
|
+
import { type MailLayoutMeta, readMailLayoutMeta } from "../template-meta.js";
|
|
15
|
+
import { localeSchema, templateSlugSchema } from "./shared.js";
|
|
16
|
+
import { parseStoredJson } from "./template-gates.js";
|
|
17
|
+
|
|
18
|
+
export type LayoutSourceRow = {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly version: number;
|
|
21
|
+
readonly status: string;
|
|
22
|
+
readonly locale: string;
|
|
23
|
+
readonly active: ActiveInfo | null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type LayoutSourceResponse = {
|
|
27
|
+
readonly tenant: LayoutSourceRow | null;
|
|
28
|
+
readonly system: (LayoutSourceRow & { readonly contentFormat: string }) | null;
|
|
29
|
+
readonly regions: LayoutRegions;
|
|
30
|
+
readonly effectiveFormat: "html" | "mjml" | null;
|
|
31
|
+
readonly mjmlSource: string | null;
|
|
32
|
+
/** Appearance of the effective mail layout; invalid stored meta reads as defaults (empty). */
|
|
33
|
+
readonly meta: MailLayoutMeta;
|
|
34
|
+
readonly locale: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const EMPTY_REGIONS: LayoutRegions = { header: "", signature: "", footer: "" };
|
|
38
|
+
|
|
39
|
+
function pickByLocale(
|
|
40
|
+
rows: readonly TemplateResourceRow[],
|
|
41
|
+
requested: string | undefined,
|
|
42
|
+
): TemplateResourceRow | null {
|
|
43
|
+
const live = rows.filter((row) => row.status !== "archived");
|
|
44
|
+
const candidates = live.length > 0 ? live : rows;
|
|
45
|
+
return (
|
|
46
|
+
candidates.find((row) => row.locale === requested) ??
|
|
47
|
+
candidates.find((row) => row.locale === FALLBACK_LOCALE) ??
|
|
48
|
+
candidates[0] ??
|
|
49
|
+
null
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function toSourceRow(row: WorkingRow): LayoutSourceRow {
|
|
54
|
+
return {
|
|
55
|
+
id: String(row.id),
|
|
56
|
+
version: row.version,
|
|
57
|
+
status: row.status,
|
|
58
|
+
locale: row.locale,
|
|
59
|
+
active: row.active,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function layoutFormat(row: WorkingRow): "html" | "mjml" | null {
|
|
64
|
+
return row.contentFormat === "html" || row.contentFormat === "mjml" ? row.contentFormat : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function pickEditedRows(
|
|
68
|
+
rows: readonly TemplateResourceRow[],
|
|
69
|
+
user: { readonly tenantId: string; readonly roles: readonly string[] },
|
|
70
|
+
locale: string | undefined,
|
|
71
|
+
): {
|
|
72
|
+
readonly tenantRow: WorkingRow | null;
|
|
73
|
+
readonly systemRow: WorkingRow | null;
|
|
74
|
+
} {
|
|
75
|
+
const tenantRow = user.roles.includes("SystemAdmin")
|
|
76
|
+
? null
|
|
77
|
+
: pickByLocale(
|
|
78
|
+
rows.filter((row) => row.scope === "tenant"),
|
|
79
|
+
locale,
|
|
80
|
+
);
|
|
81
|
+
const systemRow = pickByLocale(
|
|
82
|
+
rows.filter((row) => row.scope === "system" && !isHiddenSystemDraft(user, row)),
|
|
83
|
+
locale,
|
|
84
|
+
);
|
|
85
|
+
return {
|
|
86
|
+
tenantRow: tenantRow && toWorkingRow(tenantRow, user.tenantId),
|
|
87
|
+
// Only a SystemAdmin works on the system draft; a tenant sees the served system version.
|
|
88
|
+
systemRow: systemRow && toWorkingRow(systemRow, draftViewerTenantId(user, systemRow)),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readEffectiveRegions(
|
|
93
|
+
effective: WorkingRow | null,
|
|
94
|
+
effectiveFormat: "html" | "mjml" | null,
|
|
95
|
+
): LayoutRegions {
|
|
96
|
+
return effective !== null && effectiveFormat === "html"
|
|
97
|
+
? readLayoutRegions(effective.content ?? "")
|
|
98
|
+
: EMPTY_REGIONS;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function readMjmlSource(systemRow: WorkingRow | null, isSystemAdmin: boolean): string | null {
|
|
102
|
+
const exposesMjml = isSystemAdmin && systemRow !== null && layoutFormat(systemRow) === "mjml";
|
|
103
|
+
return exposesMjml ? (systemRow.content ?? "") : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildLayoutSource(input: {
|
|
107
|
+
readonly kind: "mail-layout" | "pdf-layout";
|
|
108
|
+
readonly locale: string | undefined;
|
|
109
|
+
readonly isSystemAdmin: boolean;
|
|
110
|
+
readonly tenantRow: WorkingRow | null;
|
|
111
|
+
readonly systemRow: WorkingRow | null;
|
|
112
|
+
}): LayoutSourceResponse {
|
|
113
|
+
const { kind, locale, isSystemAdmin, tenantRow, systemRow } = input;
|
|
114
|
+
const effective = tenantRow ?? systemRow;
|
|
115
|
+
const effectiveFormat = effective === null ? null : layoutFormat(effective);
|
|
116
|
+
return {
|
|
117
|
+
tenant: tenantRow === null ? null : toSourceRow(tenantRow),
|
|
118
|
+
system:
|
|
119
|
+
systemRow === null
|
|
120
|
+
? null
|
|
121
|
+
: { ...toSourceRow(systemRow), contentFormat: systemRow.contentFormat },
|
|
122
|
+
regions: readEffectiveRegions(effective, effectiveFormat),
|
|
123
|
+
effectiveFormat,
|
|
124
|
+
mjmlSource: readMjmlSource(systemRow, isSystemAdmin),
|
|
125
|
+
meta:
|
|
126
|
+
kind === "mail-layout" ? readMailLayoutMeta(parseStoredJson(effective?.meta ?? null)) : {},
|
|
127
|
+
locale: effective?.locale ?? locale ?? FALLBACK_LOCALE,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const layoutSourceQuery = defineQueryHandler({
|
|
132
|
+
name: "layout-source",
|
|
133
|
+
schema: z.object({
|
|
134
|
+
kind: z.enum(["mail-layout", "pdf-layout"]),
|
|
135
|
+
slug: templateSlugSchema.default("default"),
|
|
136
|
+
locale: localeSchema.optional(),
|
|
137
|
+
}),
|
|
138
|
+
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
139
|
+
description:
|
|
140
|
+
"Reads the layout the caller edits: the tenant's own version and the system version of one layout slug (each the open draft if there is one, else the stored version, with `active` naming the served version), the header, signature and footer regions of the effective html version, and the locale a save has to use; a SystemAdmin edits the system version and also receives its mjml source. Use it to load the layout editor.",
|
|
141
|
+
handler: async (query, ctx): Promise<LayoutSourceResponse> => {
|
|
142
|
+
const { kind, slug, locale } = query.payload;
|
|
143
|
+
const isSystemAdmin = query.user.roles.includes("SystemAdmin");
|
|
144
|
+
const rows = await selectMany<TemplateResourceRow>(ctx.db, templateResourcesTable, {
|
|
145
|
+
kind,
|
|
146
|
+
slug,
|
|
147
|
+
});
|
|
148
|
+
const { tenantRow, systemRow } = pickEditedRows(rows, query.user, locale);
|
|
149
|
+
return buildLayoutSource({ kind, locale, isSystemAdmin, tenantRow, systemRow });
|
|
150
|
+
},
|
|
151
|
+
});
|
|
@@ -3,10 +3,10 @@ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
|
3
3
|
import * as z from "zod";
|
|
4
4
|
import { TEMPLATE_KINDS, TEMPLATE_STATUSES } from "../constants.js";
|
|
5
5
|
import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
|
|
6
|
+
import { isHiddenSystemDraft } from "../template-draft.js";
|
|
6
7
|
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// Spalten mit scope-marker. SystemAdmin sieht alle (auch andere Tenants).
|
|
8
|
+
// ctx.db is tenant-scoped even for SystemAdmin: every caller sees its own
|
|
9
|
+
// tenant's rows plus the system defaults, never another tenant's rows.
|
|
10
10
|
export const listQuery = defineQueryHandler({
|
|
11
11
|
name: "list",
|
|
12
12
|
schema: z.object({
|
|
@@ -21,10 +21,9 @@ export const listQuery = defineQueryHandler({
|
|
|
21
21
|
handler: async (query, ctx) => {
|
|
22
22
|
const where: Record<string, unknown> = {};
|
|
23
23
|
|
|
24
|
-
// includeSystem=false narrows to own tenant AT THE DB —
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// the DB keeps the limit meaningful (no post-filter starvation).
|
|
24
|
+
// includeSystem=false narrows to own tenant AT THE DB — TenantDb permits
|
|
25
|
+
// narrowing within its enforced [own, SYSTEM] scope. Filtering at the DB
|
|
26
|
+
// keeps the limit meaningful (no post-filter starvation).
|
|
28
27
|
if (!query.payload.includeSystem) {
|
|
29
28
|
where["tenantId"] = query.user.tenantId;
|
|
30
29
|
}
|
|
@@ -45,16 +44,18 @@ export const listQuery = defineQueryHandler({
|
|
|
45
44
|
limit: 500,
|
|
46
45
|
})) as TemplateResourceRow[];
|
|
47
46
|
|
|
48
|
-
return rows
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
47
|
+
return rows
|
|
48
|
+
.filter((row) => !isHiddenSystemDraft(query.user, row))
|
|
49
|
+
.map((row) => ({
|
|
50
|
+
id: String(row.id),
|
|
51
|
+
tenantId: row.tenantId,
|
|
52
|
+
slug: row.slug,
|
|
53
|
+
kind: row.kind,
|
|
54
|
+
locale: row.locale,
|
|
55
|
+
scope: row.scope,
|
|
56
|
+
status: row.status,
|
|
57
|
+
contentFormat: row.contentFormat,
|
|
58
|
+
modifiedAt: row.modifiedAt,
|
|
59
|
+
}));
|
|
59
60
|
},
|
|
60
61
|
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { TemplateResourceRow } from "../table.js";
|
|
2
|
+
import { diffTemplateBlocks, splitTemplateBlocks } from "../template-diff.js";
|
|
3
|
+
import { servedVersionOf } from "../template-draft.js";
|
|
4
|
+
|
|
5
|
+
function parseStoredMeta(raw: string | null): Readonly<Record<string, unknown>> {
|
|
6
|
+
if (raw === null) return {};
|
|
7
|
+
try {
|
|
8
|
+
const parsed: unknown = JSON.parse(raw);
|
|
9
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
10
|
+
? Object.fromEntries(Object.entries(parsed))
|
|
11
|
+
: {};
|
|
12
|
+
} catch {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function diffMeta(
|
|
18
|
+
system: Readonly<Record<string, unknown>>,
|
|
19
|
+
tenant: Readonly<Record<string, unknown>>,
|
|
20
|
+
) {
|
|
21
|
+
const keys = [...new Set([...Object.keys(system), ...Object.keys(tenant)])].sort();
|
|
22
|
+
return keys
|
|
23
|
+
.filter((key) => JSON.stringify(system[key]) !== JSON.stringify(tenant[key]))
|
|
24
|
+
.map((key) => ({ key, tenant: tenant[key], system: system[key] }));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Shared by the tenant's own diff and the SystemAdmin's per-tenant diff in the overrides drawer.
|
|
28
|
+
export function buildOverrideDiff(tenantRow: TemplateResourceRow, systemRow: TemplateResourceRow) {
|
|
29
|
+
const blocks = diffTemplateBlocks(
|
|
30
|
+
splitTemplateBlocks(systemRow.content ?? "", systemRow.contentFormat),
|
|
31
|
+
splitTemplateBlocks(tenantRow.content ?? "", tenantRow.contentFormat),
|
|
32
|
+
);
|
|
33
|
+
const meta = diffMeta(parseStoredMeta(systemRow.meta), parseStoredMeta(tenantRow.meta));
|
|
34
|
+
return {
|
|
35
|
+
tenant: {
|
|
36
|
+
templateId: String(tenantRow.id),
|
|
37
|
+
templateVersion: servedVersionOf(tenantRow),
|
|
38
|
+
contentFormat: tenantRow.contentFormat,
|
|
39
|
+
},
|
|
40
|
+
system: {
|
|
41
|
+
templateId: String(systemRow.id),
|
|
42
|
+
templateVersion: servedVersionOf(systemRow),
|
|
43
|
+
contentFormat: systemRow.contentFormat,
|
|
44
|
+
},
|
|
45
|
+
identical: meta.length === 0 && blocks.every((block) => block.change === "same"),
|
|
46
|
+
blocks,
|
|
47
|
+
meta,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import * as z from "zod";
|
|
4
|
+
import { DEFAULT_MAIL_LAYOUT_SLUG, FALLBACK_LOCALE, type TemplateStatus } from "../constants.js";
|
|
5
|
+
import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
|
|
6
|
+
import {
|
|
7
|
+
activeSinceOf,
|
|
8
|
+
activeVersionOf,
|
|
9
|
+
draftViewerTenantId,
|
|
10
|
+
isHiddenSystemDraft,
|
|
11
|
+
parseTemplateDraft,
|
|
12
|
+
servedVersionOf,
|
|
13
|
+
} from "../template-draft.js";
|
|
14
|
+
import { collectTemplateUsage, type EffectiveMailLayoutSlugs } from "../template-usage.js";
|
|
15
|
+
import { storedRecord } from "./template-gates.js";
|
|
16
|
+
|
|
17
|
+
const OVERVIEW_KINDS = ["mail", "document-pdf", "mail-layout", "pdf-layout"] as const;
|
|
18
|
+
type OverviewKind = (typeof OVERVIEW_KINDS)[number];
|
|
19
|
+
|
|
20
|
+
const OVERVIEW_ROW_LIMIT = 5000;
|
|
21
|
+
|
|
22
|
+
export type OverviewOrigin = "tenant" | "system";
|
|
23
|
+
export type OverviewKindGroup = "mail" | "pdf" | "layout";
|
|
24
|
+
|
|
25
|
+
export type OverviewUsage = {
|
|
26
|
+
readonly qualifiedName: string;
|
|
27
|
+
readonly feature: string;
|
|
28
|
+
readonly notification: string;
|
|
29
|
+
readonly trigger: string | null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type OverviewRow = {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly slug: string;
|
|
35
|
+
readonly kind: OverviewKind;
|
|
36
|
+
readonly kindGroup: OverviewKindGroup;
|
|
37
|
+
readonly title: string;
|
|
38
|
+
readonly origin: OverviewOrigin;
|
|
39
|
+
readonly locales: readonly string[];
|
|
40
|
+
readonly status: TemplateStatus;
|
|
41
|
+
readonly version: number;
|
|
42
|
+
readonly draftVersion: number | null;
|
|
43
|
+
readonly activeSince: string | null;
|
|
44
|
+
readonly systemVersion: number | null;
|
|
45
|
+
readonly layoutSlug: string | null;
|
|
46
|
+
readonly modifiedAt: string;
|
|
47
|
+
readonly usedBy: readonly OverviewUsage[];
|
|
48
|
+
readonly usedByCount: number;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type OverviewResponse = {
|
|
52
|
+
readonly rows: readonly OverviewRow[];
|
|
53
|
+
readonly nextCursor: null;
|
|
54
|
+
readonly total: number;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const SORT_FIELDS = ["title", "slug", "kind", "origin", "status", "modifiedAt"] as const;
|
|
58
|
+
type SortField = (typeof SORT_FIELDS)[number];
|
|
59
|
+
|
|
60
|
+
const KIND_GROUP: Readonly<Record<OverviewKind, OverviewKindGroup>> = {
|
|
61
|
+
mail: "mail",
|
|
62
|
+
"document-pdf": "pdf",
|
|
63
|
+
"mail-layout": "layout",
|
|
64
|
+
"pdf-layout": "layout",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function isOverviewKind(kind: string): kind is OverviewKind {
|
|
68
|
+
return OVERVIEW_KINDS.some((candidate) => candidate === kind);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isTemplateStatus(status: string): status is TemplateStatus {
|
|
72
|
+
return status === "draft" || status === "active" || status === "archived";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function layoutSlugOf(row: TemplateResourceRow): string {
|
|
76
|
+
const layoutSlug = storedRecord(row.meta)["layoutSlug"];
|
|
77
|
+
return typeof layoutSlug === "string" ? layoutSlug : DEFAULT_MAIL_LAYOUT_SLUG;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function preferredRow(rows: readonly TemplateResourceRow[]): TemplateResourceRow | undefined {
|
|
81
|
+
return (
|
|
82
|
+
rows.find((row) => row.locale === FALLBACK_LOCALE) ??
|
|
83
|
+
[...rows].sort((a, b) => a.locale.localeCompare(b.locale))[0]
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
type EffectiveRows = { readonly origin: OverviewOrigin; readonly rows: TemplateResourceRow[] };
|
|
88
|
+
|
|
89
|
+
function effectiveRowsOf(rows: readonly TemplateResourceRow[]): EffectiveRows | null {
|
|
90
|
+
const tenantRows = rows.filter((row) => row.scope === "tenant");
|
|
91
|
+
const systemRows = rows.filter((row) => row.scope === "system");
|
|
92
|
+
const live = (list: TemplateResourceRow[]) => list.filter((row) => row.status !== "archived");
|
|
93
|
+
const liveTenant = live(tenantRows);
|
|
94
|
+
if (liveTenant.length > 0) return { origin: "tenant", rows: liveTenant };
|
|
95
|
+
if (systemRows.length > 0) {
|
|
96
|
+
const liveSystem = live(systemRows);
|
|
97
|
+
return { origin: "system", rows: liveSystem.length > 0 ? liveSystem : systemRows };
|
|
98
|
+
}
|
|
99
|
+
return tenantRows.length > 0 ? { origin: "tenant", rows: tenantRows } : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function effectiveMailLayoutSlugs(
|
|
103
|
+
rows: readonly TemplateResourceRow[],
|
|
104
|
+
): EffectiveMailLayoutSlugs {
|
|
105
|
+
const byLocale = new Map<string, TemplateResourceRow[]>();
|
|
106
|
+
for (const row of rows) {
|
|
107
|
+
if (row.kind !== "mail") continue;
|
|
108
|
+
const key = `${row.slug}\u0000${row.locale}`;
|
|
109
|
+
byLocale.set(key, [...(byLocale.get(key) ?? []), row]);
|
|
110
|
+
}
|
|
111
|
+
const bySlug = new Map<string, Map<string, string>>();
|
|
112
|
+
for (const group of byLocale.values()) {
|
|
113
|
+
const effective = effectiveRowsOf(group)?.rows[0];
|
|
114
|
+
// All-archived: the renderer falls back to the code-registered template.
|
|
115
|
+
if (effective === undefined || effective.status === "archived") continue;
|
|
116
|
+
const locales = bySlug.get(effective.slug) ?? new Map<string, string>();
|
|
117
|
+
locales.set(effective.locale, layoutSlugOf(effective));
|
|
118
|
+
bySlug.set(effective.slug, locales);
|
|
119
|
+
}
|
|
120
|
+
return bySlug;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function modifiedAtOf(row: TemplateResourceRow): string {
|
|
124
|
+
return (row.modifiedAt ?? row.insertedAt).toString();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function splitDocumentSlug(slug: string): { feature: string; notification: string } {
|
|
128
|
+
const separator = slug.indexOf(":");
|
|
129
|
+
return separator === -1
|
|
130
|
+
? { feature: "", notification: slug }
|
|
131
|
+
: { feature: slug.slice(0, separator), notification: slug.slice(separator + 1) };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function compareBy(field: SortField, a: OverviewRow, b: OverviewRow): number {
|
|
135
|
+
return String(a[field]).localeCompare(String(b[field]));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isSortField(value: string): value is SortField {
|
|
139
|
+
return SORT_FIELDS.some((candidate) => candidate === value);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
type OverviewDraft = Omit<OverviewRow, "usedBy" | "usedByCount">;
|
|
143
|
+
type OverviewFilter = {
|
|
144
|
+
readonly field: "kindGroup" | "kind" | "origin" | "status";
|
|
145
|
+
readonly value: readonly string[];
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
function groupBySlugAndKind(
|
|
149
|
+
rows: readonly TemplateResourceRow[],
|
|
150
|
+
): Map<string, TemplateResourceRow[]> {
|
|
151
|
+
const groups = new Map<string, TemplateResourceRow[]>();
|
|
152
|
+
for (const row of rows) {
|
|
153
|
+
if (!isOverviewKind(row.kind)) continue;
|
|
154
|
+
const key = `${row.kind}|${row.slug}`;
|
|
155
|
+
groups.set(key, [...(groups.get(key) ?? []), row]);
|
|
156
|
+
}
|
|
157
|
+
return groups;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Another tenant's draft, including a system draft seen by a tenant, is never announced.
|
|
161
|
+
function draftVersionFor(
|
|
162
|
+
user: { readonly tenantId: string; readonly roles: readonly string[] },
|
|
163
|
+
row: TemplateResourceRow,
|
|
164
|
+
): number | null {
|
|
165
|
+
const isOwnRow = row.tenantId === draftViewerTenantId(user, row);
|
|
166
|
+
return isOwnRow && parseTemplateDraft(row.draft) !== null ? row.version : null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Version columns of one overview row: the served version, a waiting draft and the system version it overrides.
|
|
170
|
+
function versionFieldsOf(
|
|
171
|
+
user: { readonly tenantId: string; readonly roles: readonly string[] },
|
|
172
|
+
preferred: TemplateResourceRow,
|
|
173
|
+
groupRows: readonly TemplateResourceRow[],
|
|
174
|
+
): Pick<OverviewDraft, "version" | "draftVersion" | "activeSince" | "systemVersion"> {
|
|
175
|
+
const isActive = preferred.status === "active";
|
|
176
|
+
const systemSameLocale = groupRows.find(
|
|
177
|
+
(row) => row.scope === "system" && row.locale === preferred.locale,
|
|
178
|
+
);
|
|
179
|
+
return {
|
|
180
|
+
version: servedVersionOf(preferred),
|
|
181
|
+
draftVersion: draftVersionFor(user, preferred),
|
|
182
|
+
activeSince: isActive ? activeSinceOf(preferred).toString() : null,
|
|
183
|
+
systemVersion: systemSameLocale === undefined ? null : activeVersionOf(systemSameLocale),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function buildDrafts(
|
|
188
|
+
groups: ReadonlyMap<string, readonly TemplateResourceRow[]>,
|
|
189
|
+
user: { readonly tenantId: string; readonly roles: readonly string[] },
|
|
190
|
+
): {
|
|
191
|
+
readonly drafts: OverviewDraft[];
|
|
192
|
+
readonly layoutSlugByDocument: Map<string, string>;
|
|
193
|
+
} {
|
|
194
|
+
const layoutSlugByDocument = new Map<string, string>();
|
|
195
|
+
const drafts: OverviewDraft[] = [];
|
|
196
|
+
for (const [id, groupRows] of groups) {
|
|
197
|
+
const effective = effectiveRowsOf(groupRows);
|
|
198
|
+
const preferred = effective === null ? undefined : preferredRow(effective.rows);
|
|
199
|
+
if (effective === null || preferred === undefined) continue;
|
|
200
|
+
if (!isOverviewKind(preferred.kind) || !isTemplateStatus(preferred.status)) continue;
|
|
201
|
+
const isDocument = preferred.kind === "mail" || preferred.kind === "document-pdf";
|
|
202
|
+
const layoutSlug = isDocument ? layoutSlugOf(preferred) : null;
|
|
203
|
+
if (layoutSlug !== null && preferred.kind === "document-pdf") {
|
|
204
|
+
layoutSlugByDocument.set(preferred.slug, layoutSlug);
|
|
205
|
+
}
|
|
206
|
+
drafts.push({
|
|
207
|
+
id,
|
|
208
|
+
slug: preferred.slug,
|
|
209
|
+
kind: preferred.kind,
|
|
210
|
+
kindGroup: KIND_GROUP[preferred.kind],
|
|
211
|
+
title: preferred.title ?? preferred.slug,
|
|
212
|
+
origin: effective.origin,
|
|
213
|
+
locales: [...new Set(effective.rows.map((row) => row.locale))].sort(),
|
|
214
|
+
status: preferred.status,
|
|
215
|
+
...versionFieldsOf(user, preferred, groupRows),
|
|
216
|
+
layoutSlug,
|
|
217
|
+
modifiedAt: modifiedAtOf(preferred),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return { drafts, layoutSlugByDocument };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function withUsage(
|
|
224
|
+
draft: OverviewDraft,
|
|
225
|
+
layoutSlugByDocument: ReadonlyMap<string, string>,
|
|
226
|
+
registry: Parameters<typeof collectTemplateUsage>[0],
|
|
227
|
+
effectiveLayouts: EffectiveMailLayoutSlugs,
|
|
228
|
+
): OverviewRow {
|
|
229
|
+
let usedBy: OverviewUsage[] = [];
|
|
230
|
+
if (draft.kind === "mail" || draft.kind === "mail-layout") {
|
|
231
|
+
usedBy = collectTemplateUsage(registry, draft.slug, draft.kind, effectiveLayouts).map(
|
|
232
|
+
(entry) => ({
|
|
233
|
+
qualifiedName: entry.qualifiedName,
|
|
234
|
+
feature: entry.feature,
|
|
235
|
+
notification: entry.notification,
|
|
236
|
+
trigger: entry.trigger,
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
} else if (draft.kind === "pdf-layout") {
|
|
240
|
+
usedBy = [...layoutSlugByDocument]
|
|
241
|
+
.filter(([, layoutSlug]) => layoutSlug === draft.slug)
|
|
242
|
+
.map(([documentSlug]) => ({
|
|
243
|
+
qualifiedName: documentSlug,
|
|
244
|
+
...splitDocumentSlug(documentSlug),
|
|
245
|
+
trigger: null,
|
|
246
|
+
}));
|
|
247
|
+
}
|
|
248
|
+
return { ...draft, usedBy, usedByCount: usedBy.length };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function filterRows(
|
|
252
|
+
rows: readonly OverviewRow[],
|
|
253
|
+
search: string | undefined,
|
|
254
|
+
filters: readonly OverviewFilter[],
|
|
255
|
+
layoutSlug: string | undefined,
|
|
256
|
+
): OverviewRow[] {
|
|
257
|
+
const needle = search?.trim().toLowerCase() ?? "";
|
|
258
|
+
return rows.filter((row) => {
|
|
259
|
+
if (layoutSlug !== undefined && row.layoutSlug !== layoutSlug) return false;
|
|
260
|
+
if (needle !== "" && !`${row.title} ${row.slug}`.toLowerCase().includes(needle)) return false;
|
|
261
|
+
return filters.every((filter) => filter.value.includes(String(row[filter.field])));
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const filterSchema = z.object({
|
|
266
|
+
field: z.enum(["kindGroup", "kind", "origin", "status"]),
|
|
267
|
+
op: z.literal("in"),
|
|
268
|
+
value: z.array(z.string()),
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
export const overviewQuery = defineQueryHandler({
|
|
272
|
+
name: "overview",
|
|
273
|
+
schema: z.object({
|
|
274
|
+
search: z.string().optional(),
|
|
275
|
+
filters: z.array(filterSchema).optional(),
|
|
276
|
+
layoutSlug: z.string().optional(),
|
|
277
|
+
sort: z.string().optional(),
|
|
278
|
+
sortDirection: z.enum(["asc", "desc"]).optional(),
|
|
279
|
+
limit: z.number().int().min(1).max(200).default(50),
|
|
280
|
+
offset: z.number().int().min(0).optional(),
|
|
281
|
+
totalCount: z.boolean().optional(),
|
|
282
|
+
}),
|
|
283
|
+
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
284
|
+
description:
|
|
285
|
+
"Lists mail, PDF and layout templates as one row per slug and kind for the caller's tenant (optionally only the mails and PDFs using one layout slug): where the effective version comes from (own override or system), its locales, status and version (the served one; `draftVersion` is set when an unpublished draft waits beside it, `activeSince` says since when the served version applies), the system version it overrides, the layout it uses and which notifications or templates use it; use it to browse and filter all templates at a glance.",
|
|
286
|
+
handler: async (query, ctx) => {
|
|
287
|
+
const payload = query.payload;
|
|
288
|
+
const rows = await selectMany<TemplateResourceRow>(
|
|
289
|
+
ctx.db,
|
|
290
|
+
templateResourcesTable,
|
|
291
|
+
{ kind: { in: [...OVERVIEW_KINDS] } },
|
|
292
|
+
{ limit: OVERVIEW_ROW_LIMIT },
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const visibleRows = rows.filter((row) => !isHiddenSystemDraft(query.user, row));
|
|
296
|
+
const { drafts, layoutSlugByDocument } = buildDrafts(
|
|
297
|
+
groupBySlugAndKind(visibleRows),
|
|
298
|
+
query.user,
|
|
299
|
+
);
|
|
300
|
+
const effectiveLayouts = effectiveMailLayoutSlugs(visibleRows);
|
|
301
|
+
const overviewRows = drafts.map((draft) =>
|
|
302
|
+
withUsage(draft, layoutSlugByDocument, ctx.registry, effectiveLayouts),
|
|
303
|
+
);
|
|
304
|
+
const filtered = filterRows(
|
|
305
|
+
overviewRows,
|
|
306
|
+
payload.search,
|
|
307
|
+
payload.filters ?? [],
|
|
308
|
+
payload.layoutSlug,
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
const sortField: SortField =
|
|
312
|
+
payload.sort !== undefined && isSortField(payload.sort) ? payload.sort : "modifiedAt";
|
|
313
|
+
const direction = (payload.sortDirection ?? "desc") === "desc" ? -1 : 1;
|
|
314
|
+
filtered.sort((a, b) => direction * compareBy(sortField, a, b) || a.id.localeCompare(b.id));
|
|
315
|
+
|
|
316
|
+
const offset = payload.offset ?? 0;
|
|
317
|
+
return {
|
|
318
|
+
rows: filtered.slice(offset, offset + payload.limit),
|
|
319
|
+
nextCursor: null,
|
|
320
|
+
total: filtered.length,
|
|
321
|
+
} satisfies OverviewResponse;
|
|
322
|
+
},
|
|
323
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { renderTemplatePreview } from "../template-preview.js";
|
|
4
|
+
import { localeSchema, templateSlugSchema } from "./shared.js";
|
|
5
|
+
|
|
6
|
+
export const previewQuery = defineQueryHandler({
|
|
7
|
+
name: "preview",
|
|
8
|
+
schema: z.object({
|
|
9
|
+
slug: templateSlugSchema,
|
|
10
|
+
kind: z.enum(["mail", "document-pdf"]),
|
|
11
|
+
locale: localeSchema,
|
|
12
|
+
/** Without content the stored template is previewed. */
|
|
13
|
+
content: z.string().max(200_000).optional(),
|
|
14
|
+
meta: z.unknown().optional(),
|
|
15
|
+
variableSchema: z.record(z.string(), z.unknown()).default({}),
|
|
16
|
+
}),
|
|
17
|
+
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
18
|
+
description:
|
|
19
|
+
"Renders a template draft, or the stored template when no content is given, with sample values taken from the declared variable examples, without saving or sending anything: returns the mail subject, html and plain text with its inline images (a PDF template returns its body and layout regions instead), the sample values used, and checks for unresolved images, missing plain text, mail size and unused required placeholders; a draft that fails the write rules is rejected with the same problems as a save.",
|
|
20
|
+
handler: async (query, ctx) => {
|
|
21
|
+
const outcome = await renderTemplatePreview(ctx, query.user, query.payload);
|
|
22
|
+
if (!outcome.ok) throw outcome.error;
|
|
23
|
+
return outcome.preview;
|
|
24
|
+
},
|
|
25
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { defineWriteHandler, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { NotFoundError, ValidationError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
|
+
import * as z from "zod";
|
|
5
|
+
import type { TemplateResourceRow } from "../table.js";
|
|
6
|
+
import { templateResourcesTable } from "../table.js";
|
|
7
|
+
import { executor } from "./shared.js";
|
|
8
|
+
|
|
9
|
+
export const resetToSystemWrite = defineWriteHandler({
|
|
10
|
+
name: "reset-to-system",
|
|
11
|
+
schema: z.object({ id: z.string().min(1) }),
|
|
12
|
+
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
13
|
+
description:
|
|
14
|
+
"Retires a tenant override by archiving it so the resolver serves the active system template again; the override and its content survive and can be published again. Refused when there is no active system template to fall back to.",
|
|
15
|
+
handler: async (event, ctx) => {
|
|
16
|
+
const existing = await fetchOne<TemplateResourceRow>(ctx.db, templateResourcesTable, {
|
|
17
|
+
id: event.payload.id,
|
|
18
|
+
});
|
|
19
|
+
if (!existing || existing.tenantId !== event.user.tenantId || existing.scope !== "tenant") {
|
|
20
|
+
return writeFailure(new NotFoundError("template-resource", event.payload.id));
|
|
21
|
+
}
|
|
22
|
+
const systemRow = await fetchOne<TemplateResourceRow>(ctx.db, templateResourcesTable, {
|
|
23
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
24
|
+
slug: existing.slug,
|
|
25
|
+
kind: existing.kind,
|
|
26
|
+
locale: existing.locale,
|
|
27
|
+
status: "active",
|
|
28
|
+
});
|
|
29
|
+
if (!systemRow) {
|
|
30
|
+
return writeFailure(
|
|
31
|
+
new ValidationError({
|
|
32
|
+
fields: [
|
|
33
|
+
{
|
|
34
|
+
path: "id",
|
|
35
|
+
code: "no_system_template",
|
|
36
|
+
i18nKey: "templateResolver.errors.noSystemTemplate",
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
const response = {
|
|
43
|
+
isSuccess: true as const,
|
|
44
|
+
data: {
|
|
45
|
+
id: String(existing.id),
|
|
46
|
+
status: "archived" as const,
|
|
47
|
+
systemTemplateId: String(systemRow.id),
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
if (existing.status === "archived") return response;
|
|
51
|
+
const result = await executor.update(
|
|
52
|
+
{ id: existing.id, version: existing.version, changes: { status: "archived" } },
|
|
53
|
+
event.user,
|
|
54
|
+
ctx.db,
|
|
55
|
+
);
|
|
56
|
+
return result.isSuccess ? response : result;
|
|
57
|
+
},
|
|
58
|
+
});
|