@cosmicdrift/kumiko-samples 0.362.0 → 0.363.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 +17 -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/auth-email-password/changes.json +14 -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/invite-create.write.ts +4 -5
- 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/token-request-handler.ts +3 -12
- 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-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 +7 -0
- package/packages/bundled-features/src/billing-foundation/constants.ts +5 -4
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +38 -44
- 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 +115 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-consent.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +30 -30
- 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-request.ts +2 -2
- package/packages/bundled-features/src/billing-foundation/get-subscription-for-tenant.ts +2 -2
- 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 +2 -2
- package/packages/bundled-features/src/billing-foundation/handlers/process-payment-event.write.ts +1 -1
- package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +3 -6
- 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 +122 -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/handlers/set.write.ts +1 -5
- package/packages/bundled-features/src/custom-fields/db/queries/user-data-rights.ts +2 -2
- 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 +84 -0
- package/packages/bundled-features/src/delivery/changes.json +48 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +129 -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 +17 -2
- package/packages/bundled-features/src/delivery/jobs.ts +48 -5
- 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 +28 -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-foundation/index.ts +7 -0
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +86 -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/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-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 +40 -25
- package/packages/bundled-features/src/inbound-provider-imap/imap-client.ts +4 -3
- package/packages/bundled-features/src/jobs/handlers/catalog.query.ts +1 -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 +132 -35
- package/packages/bundled-features/src/renderer-pdf/feature.ts +7 -1
- 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/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 +25 -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/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 +63 -7
- package/packages/bundled-features/src/template-resolver/api.ts +83 -8
- package/packages/bundled-features/src/template-resolver/changes.json +118 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +214 -0
- package/packages/bundled-features/src/template-resolver/constants.ts +39 -11
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +335 -0
- package/packages/bundled-features/src/template-resolver/document-schema.ts +323 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +56 -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 +142 -0
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +72 -0
- package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +83 -0
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +3 -3
- 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 +108 -0
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +5 -7
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +250 -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 +106 -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 +184 -0
- package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +183 -0
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +55 -0
- package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +57 -10
- 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 +15 -76
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +17 -0
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +116 -0
- package/packages/bundled-features/src/template-resolver/index.ts +100 -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/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 +30 -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 +147 -17
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +416 -0
- package/packages/bundled-features/src/template-resolver/table.ts +14 -0
- 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-meta.ts +129 -0
- package/packages/bundled-features/src/template-resolver/template-preview.ts +573 -0
- package/packages/bundled-features/src/template-resolver/template-resources.ts +74 -0
- package/packages/bundled-features/src/template-resolver/template-usage.ts +48 -0
- package/packages/bundled-features/src/template-resolver/template-validation.ts +229 -0
- package/packages/bundled-features/src/template-resolver/testing.ts +5 -5
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +9 -6
- package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +162 -0
- package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +28 -0
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +850 -0
- package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +61 -0
- package/packages/bundled-features/src/template-resolver/web/editor-handle.ts +26 -0
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +100 -0
- package/packages/bundled-features/src/template-resolver/web/index.ts +19 -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 +169 -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/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/roles-input-schema.ts +2 -2
- package/packages/bundled-features/src/user-data-rights/changes.json +7 -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/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/workflow-runner/event-trigger.ts +1 -2
- package/samples/apps/ui-walkthrough/package.json +2 -0
- 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 +154 -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 +354 -0
- package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +68 -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 +37 -1
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +0 -2
- package/samples/apps/use-all-bundled/feature-manifest.json +115 -20
- 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
|
@@ -4,8 +4,11 @@ import {
|
|
|
4
4
|
defineWriteHandler,
|
|
5
5
|
type TenantId,
|
|
6
6
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
|
+
import { ValidationError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
7
8
|
import * as z from "zod";
|
|
8
9
|
import { requireForTenant } from "../../shared/index.js";
|
|
10
|
+
import { COLLECTION_REFUSED_KINDS } from "../constants.js";
|
|
11
|
+
import { documentViolationsToFieldIssues, sanitizeTemplateBody } from "../document-sanitizer.js";
|
|
9
12
|
import {
|
|
10
13
|
type CollectionEntryRow,
|
|
11
14
|
collectionStore,
|
|
@@ -40,6 +43,20 @@ export function makeCollectionSetWrite(collection: ContentCollectionDefinition)
|
|
|
40
43
|
? `Creates or overwrites the caller's own "${collection.id}" entry at a slug and locale, live on save with no draft stage; it always writes the acting user's entry, never another user's.`
|
|
41
44
|
: `Creates or overwrites the tenant-wide "${collection.id}" entry at a slug and locale, live on save with no draft stage; the change is visible to everyone who may reach this shared collection.`,
|
|
42
45
|
handler: async (event, ctx) => {
|
|
46
|
+
if (COLLECTION_REFUSED_KINDS.includes(collection.kind)) {
|
|
47
|
+
return writeFailure(
|
|
48
|
+
new ValidationError({
|
|
49
|
+
fields: [
|
|
50
|
+
{
|
|
51
|
+
path: "kind",
|
|
52
|
+
code: "kind_not_allowed",
|
|
53
|
+
i18nKey: "templateResolver.errors.collection.kindNotAllowed",
|
|
54
|
+
params: { detail: collection.kind },
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
43
60
|
const override = event.payload.tenantIdOverride;
|
|
44
61
|
// @cast-boundary engine-payload — override is a zod-validated string, the
|
|
45
62
|
// user's own tenantId is already TenantId-branded.
|
|
@@ -50,6 +67,17 @@ export function makeCollectionSetWrite(collection: ContentCollectionDefinition)
|
|
|
50
67
|
// someone else's signature.
|
|
51
68
|
const owner = store.scopeOf(event.user);
|
|
52
69
|
|
|
70
|
+
const body = sanitizeTemplateBody({
|
|
71
|
+
kind: collection.kind,
|
|
72
|
+
contentFormat: event.payload.contentFormat,
|
|
73
|
+
content: event.payload.content,
|
|
74
|
+
});
|
|
75
|
+
if (!body.ok) {
|
|
76
|
+
return writeFailure(
|
|
77
|
+
new ValidationError({ fields: documentViolationsToFieldIssues(body.violations) }),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
53
81
|
const existing = await fetchOne<CollectionEntryRow>(db, store.table, {
|
|
54
82
|
tenantId,
|
|
55
83
|
slug: event.payload.slug,
|
|
@@ -68,7 +96,7 @@ export function makeCollectionSetWrite(collection: ContentCollectionDefinition)
|
|
|
68
96
|
// the template upserts put on the row.
|
|
69
97
|
changes: {
|
|
70
98
|
title: event.payload.title,
|
|
71
|
-
content:
|
|
99
|
+
content: body.content,
|
|
72
100
|
contentFormat: event.payload.contentFormat,
|
|
73
101
|
folder: event.payload.folder ?? null,
|
|
74
102
|
},
|
|
@@ -91,7 +119,7 @@ export function makeCollectionSetWrite(collection: ContentCollectionDefinition)
|
|
|
91
119
|
kind: collection.kind,
|
|
92
120
|
locale: event.payload.locale,
|
|
93
121
|
title: event.payload.title,
|
|
94
|
-
content:
|
|
122
|
+
content: body.content,
|
|
95
123
|
contentFormat: event.payload.contentFormat,
|
|
96
124
|
folder: event.payload.folder ?? null,
|
|
97
125
|
...store.createDefaults(tenantId),
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import {
|
|
3
|
+
defineWriteHandler,
|
|
4
|
+
type HandlerContext,
|
|
5
|
+
SYSTEM_TENANT_ID,
|
|
6
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
|
+
import {
|
|
8
|
+
AccessDeniedError,
|
|
9
|
+
NotFoundError,
|
|
10
|
+
ValidationError,
|
|
11
|
+
writeFailure,
|
|
12
|
+
} from "@cosmicdrift/kumiko-framework/errors";
|
|
13
|
+
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
14
|
+
import * as z from "zod";
|
|
15
|
+
import { requireForTenant } from "../../shared/index.js";
|
|
16
|
+
import { CONTENT_FORMATS } from "../constants.js";
|
|
17
|
+
import { parseLinkedResourceEntries } from "../linked-resources.js";
|
|
18
|
+
import { TemplateResolverErrors } from "../qualified-names.js";
|
|
19
|
+
import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
|
|
20
|
+
import { attachTemplateResources } from "../template-resources.js";
|
|
21
|
+
import { resolveTemplateVariables } from "../template-validation.js";
|
|
22
|
+
import { targetExistsIssue } from "./bundle-import.js";
|
|
23
|
+
import { kindSchema, localeSchema, templateSlugSchema } from "./shared.js";
|
|
24
|
+
import { parseStoredJson, storedRecord } from "./template-gates.js";
|
|
25
|
+
import { refuseTenantScopeUnderSystemTenant, writeTemplateRow } from "./template-write.js";
|
|
26
|
+
|
|
27
|
+
type CopyScope = "tenant" | "system";
|
|
28
|
+
type WriteCtx = HandlerContext;
|
|
29
|
+
|
|
30
|
+
function readDbFor(ctx: WriteCtx, scope: CopyScope) {
|
|
31
|
+
return scope === "system" ? requireForTenant(ctx, SYSTEM_TENANT_ID).db : ctx.db;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// A tenant copy may start from the system default; a system copy only from system rows.
|
|
35
|
+
async function findCopySource(
|
|
36
|
+
ctx: WriteCtx,
|
|
37
|
+
where: {
|
|
38
|
+
scope: CopyScope;
|
|
39
|
+
tenantId: string;
|
|
40
|
+
slug: string;
|
|
41
|
+
kind: string;
|
|
42
|
+
locale: string;
|
|
43
|
+
},
|
|
44
|
+
) {
|
|
45
|
+
const { scope, tenantId, slug, kind, locale } = where;
|
|
46
|
+
const readDb = readDbFor(ctx, scope);
|
|
47
|
+
const own = await fetchOne<TemplateResourceRow>(readDb, templateResourcesTable, {
|
|
48
|
+
tenantId,
|
|
49
|
+
slug,
|
|
50
|
+
kind,
|
|
51
|
+
locale,
|
|
52
|
+
});
|
|
53
|
+
if (own || scope === "system") return own;
|
|
54
|
+
return fetchOne<TemplateResourceRow>(readDb, templateResourcesTable, {
|
|
55
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
56
|
+
slug,
|
|
57
|
+
kind,
|
|
58
|
+
locale,
|
|
59
|
+
status: "active",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const copyLocaleWrite = defineWriteHandler({
|
|
64
|
+
name: "copy-locale",
|
|
65
|
+
schema: z.object({
|
|
66
|
+
slug: templateSlugSchema,
|
|
67
|
+
kind: kindSchema,
|
|
68
|
+
fromLocale: localeSchema,
|
|
69
|
+
toLocale: localeSchema,
|
|
70
|
+
overwrite: z.boolean().default(false),
|
|
71
|
+
/** "system" copies between system-default rows; SystemAdmin only. */
|
|
72
|
+
scope: z.enum(["tenant", "system"]).default("tenant"),
|
|
73
|
+
}),
|
|
74
|
+
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
75
|
+
description:
|
|
76
|
+
"Copies the text of one locale of a template into another locale as a draft override of the caller's tenant, taking the tenant's own row or else the system default as the source; use it as the starting point for a translation. It refuses an existing target unless overwrite is set. With scope system (SystemAdmin only) both source and target are system-default rows and the target lands as a system draft; any other caller gets a 403.",
|
|
77
|
+
handler: async (event, ctx) => {
|
|
78
|
+
const { slug, kind, fromLocale, toLocale, overwrite, scope } = event.payload;
|
|
79
|
+
if (scope === "system" && !event.user.roles.includes("SystemAdmin")) {
|
|
80
|
+
return writeFailure(
|
|
81
|
+
new AccessDeniedError({
|
|
82
|
+
i18nKey: "errors.access.denied",
|
|
83
|
+
details: { reason: TemplateResolverErrors.systemAdminRequired },
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
const tenantId = scope === "system" ? SYSTEM_TENANT_ID : event.user.tenantId;
|
|
88
|
+
const refused = refuseTenantScopeUnderSystemTenant(scope, tenantId);
|
|
89
|
+
if (refused !== null) return refused;
|
|
90
|
+
|
|
91
|
+
const readDb = readDbFor(ctx, scope);
|
|
92
|
+
const source = await findCopySource(ctx, { scope, tenantId, slug, kind, locale: fromLocale });
|
|
93
|
+
if (!source || !isOneOf(CONTENT_FORMATS, source.contentFormat)) {
|
|
94
|
+
return writeFailure(new NotFoundError("template-resource", `${slug}/${fromLocale}`));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!overwrite) {
|
|
98
|
+
const target = await fetchOne<TemplateResourceRow>(readDb, templateResourcesTable, {
|
|
99
|
+
tenantId,
|
|
100
|
+
slug,
|
|
101
|
+
kind,
|
|
102
|
+
locale: toLocale,
|
|
103
|
+
});
|
|
104
|
+
if (target) {
|
|
105
|
+
return writeFailure(new ValidationError({ fields: [targetExistsIssue(toLocale)] }));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const variableSchema = storedRecord(source.variableSchema);
|
|
110
|
+
const written = await writeTemplateRow({
|
|
111
|
+
ctx,
|
|
112
|
+
user: event.user,
|
|
113
|
+
tenantId,
|
|
114
|
+
scope,
|
|
115
|
+
status: "draft",
|
|
116
|
+
variables: resolveTemplateVariables(ctx.registry, slug, variableSchema),
|
|
117
|
+
payload: {
|
|
118
|
+
slug,
|
|
119
|
+
kind,
|
|
120
|
+
locale: toLocale,
|
|
121
|
+
content: source.content,
|
|
122
|
+
contentFormat: source.contentFormat,
|
|
123
|
+
variableSchema,
|
|
124
|
+
meta: parseStoredJson(source.meta),
|
|
125
|
+
title: source.title,
|
|
126
|
+
folder: source.folder,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
if (!written.isSuccess) return written;
|
|
130
|
+
// A system source carries no files of its own: its resource keys are known to the tenant row anyway.
|
|
131
|
+
if (source.tenantId === tenantId) {
|
|
132
|
+
const attached = await attachTemplateResources(ctx, {
|
|
133
|
+
user: event.user,
|
|
134
|
+
tenantId,
|
|
135
|
+
templateId: written.data.id,
|
|
136
|
+
resources: parseLinkedResourceEntries(source.linkedResources),
|
|
137
|
+
});
|
|
138
|
+
if (!attached.isSuccess) return attached;
|
|
139
|
+
}
|
|
140
|
+
return written;
|
|
141
|
+
},
|
|
142
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { defineQueryHandler, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { NotFoundError } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
|
+
import * as z from "zod";
|
|
5
|
+
import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
|
|
6
|
+
import { diffTemplateBlocks, splitTemplateBlocks } from "../template-diff.js";
|
|
7
|
+
|
|
8
|
+
function parseStoredMeta(raw: string | null): Readonly<Record<string, unknown>> {
|
|
9
|
+
if (raw === null) return {};
|
|
10
|
+
try {
|
|
11
|
+
const parsed: unknown = JSON.parse(raw);
|
|
12
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
13
|
+
? Object.fromEntries(Object.entries(parsed))
|
|
14
|
+
: {};
|
|
15
|
+
} catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function diffMeta(
|
|
21
|
+
system: Readonly<Record<string, unknown>>,
|
|
22
|
+
tenant: Readonly<Record<string, unknown>>,
|
|
23
|
+
) {
|
|
24
|
+
const keys = [...new Set([...Object.keys(system), ...Object.keys(tenant)])].sort();
|
|
25
|
+
return keys
|
|
26
|
+
.filter((key) => JSON.stringify(system[key]) !== JSON.stringify(tenant[key]))
|
|
27
|
+
.map((key) => ({ key, tenant: tenant[key], system: system[key] }));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const diffToSystemQuery = defineQueryHandler({
|
|
31
|
+
name: "diff-to-system",
|
|
32
|
+
schema: z.object({ id: z.string().min(1) }),
|
|
33
|
+
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
34
|
+
description:
|
|
35
|
+
"Compares one tenant override with the system template of the same slug, kind and locale block by block and lists the changed meta settings; use it to show what an override changes before resetting it to the system version.",
|
|
36
|
+
handler: async (query, ctx) => {
|
|
37
|
+
const tenantRow = await fetchOne<TemplateResourceRow>(ctx.db, templateResourcesTable, {
|
|
38
|
+
id: query.payload.id,
|
|
39
|
+
});
|
|
40
|
+
if (!tenantRow || tenantRow.tenantId !== query.user.tenantId || tenantRow.scope !== "tenant") {
|
|
41
|
+
throw new NotFoundError("template-resource", query.payload.id);
|
|
42
|
+
}
|
|
43
|
+
const systemRow = await fetchOne<TemplateResourceRow>(ctx.db, templateResourcesTable, {
|
|
44
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
45
|
+
slug: tenantRow.slug,
|
|
46
|
+
kind: tenantRow.kind,
|
|
47
|
+
locale: tenantRow.locale,
|
|
48
|
+
});
|
|
49
|
+
if (!systemRow) throw new NotFoundError("template-resource", tenantRow.slug);
|
|
50
|
+
|
|
51
|
+
const blocks = diffTemplateBlocks(
|
|
52
|
+
splitTemplateBlocks(systemRow.content ?? "", systemRow.contentFormat),
|
|
53
|
+
splitTemplateBlocks(tenantRow.content ?? "", tenantRow.contentFormat),
|
|
54
|
+
);
|
|
55
|
+
const meta = diffMeta(parseStoredMeta(systemRow.meta), parseStoredMeta(tenantRow.meta));
|
|
56
|
+
return {
|
|
57
|
+
tenant: {
|
|
58
|
+
templateId: String(tenantRow.id),
|
|
59
|
+
templateVersion: tenantRow.version,
|
|
60
|
+
contentFormat: tenantRow.contentFormat,
|
|
61
|
+
},
|
|
62
|
+
system: {
|
|
63
|
+
templateId: String(systemRow.id),
|
|
64
|
+
templateVersion: systemRow.version,
|
|
65
|
+
contentFormat: systemRow.contentFormat,
|
|
66
|
+
},
|
|
67
|
+
identical: meta.length === 0 && blocks.every((block) => block.change === "same"),
|
|
68
|
+
blocks,
|
|
69
|
+
meta,
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import { defineQueryHandler, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { NotFoundError } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
|
+
import * as z from "zod";
|
|
5
|
+
import {
|
|
6
|
+
isFileAttachmentEntry,
|
|
7
|
+
isPdfAttachmentLink,
|
|
8
|
+
type LinkedResourceEntry,
|
|
9
|
+
parseLinkedResourceEntries,
|
|
10
|
+
} from "../linked-resources.js";
|
|
11
|
+
import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
|
|
12
|
+
import type { TemplateBundle } from "../template-bundle.js";
|
|
13
|
+
import { readTemplateResource } from "../template-resources.js";
|
|
14
|
+
import { kindSchema, templateSlugSchema } from "./shared.js";
|
|
15
|
+
import { parseStoredJson, storedRecord } from "./template-gates.js";
|
|
16
|
+
|
|
17
|
+
export const exportBundleQuery = defineQueryHandler({
|
|
18
|
+
name: "export-bundle",
|
|
19
|
+
schema: z.object({
|
|
20
|
+
slug: templateSlugSchema,
|
|
21
|
+
kind: kindSchema,
|
|
22
|
+
scope: z.enum(["tenant", "system"]).default("tenant"),
|
|
23
|
+
}),
|
|
24
|
+
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
25
|
+
description:
|
|
26
|
+
"Exports every locale of one template (the caller's own rows, or the active system rows) together with its linked resource files as a self-contained bundle that import-bundle can read back, for example to move a template between tenants or environments.",
|
|
27
|
+
handler: async (query, ctx) => {
|
|
28
|
+
const { slug, kind, scope } = query.payload;
|
|
29
|
+
const found = await selectMany<TemplateResourceRow>(
|
|
30
|
+
ctx.db,
|
|
31
|
+
templateResourcesTable,
|
|
32
|
+
scope === "system"
|
|
33
|
+
? { tenantId: SYSTEM_TENANT_ID, slug, kind, scope: "system", status: "active" }
|
|
34
|
+
: { tenantId: query.user.tenantId, slug, kind, scope: "tenant" },
|
|
35
|
+
);
|
|
36
|
+
const rows = found
|
|
37
|
+
.filter((row) => row.status !== "archived")
|
|
38
|
+
.sort((a, b) => a.locale.localeCompare(b.locale));
|
|
39
|
+
const first = rows[0];
|
|
40
|
+
if (!first) throw new NotFoundError("template-resource", slug);
|
|
41
|
+
|
|
42
|
+
const locales: Record<string, { body: string; meta?: unknown }> = {};
|
|
43
|
+
const entryByKey = new Map<string, LinkedResourceEntry>();
|
|
44
|
+
for (const row of rows) {
|
|
45
|
+
const meta = parseStoredJson(row.meta);
|
|
46
|
+
locales[row.locale] = { body: row.content ?? "", ...(meta === undefined ? {} : { meta }) };
|
|
47
|
+
for (const [key, entry] of Object.entries(parseLinkedResourceEntries(row.linkedResources))) {
|
|
48
|
+
if (!entryByKey.has(key)) entryByKey.set(key, entry);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const resources: TemplateBundle["resources"] = {};
|
|
53
|
+
const pdfAttachments: TemplateBundle["pdfAttachments"] = {};
|
|
54
|
+
for (const [key, entry] of entryByKey) {
|
|
55
|
+
if (isPdfAttachmentLink(entry)) {
|
|
56
|
+
pdfAttachments[key] = { templateSlug: entry.templateSlug, fileName: entry.fileName };
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const attachment = isFileAttachmentEntry(entry);
|
|
60
|
+
const file = await readTemplateResource(
|
|
61
|
+
{ db: ctx.db, files: ctx.files, user: query.user },
|
|
62
|
+
attachment ? entry.fileRefId : entry,
|
|
63
|
+
);
|
|
64
|
+
if (!file) throw new NotFoundError("template-resource-file", key);
|
|
65
|
+
resources[key] = {
|
|
66
|
+
fileName: attachment ? entry.fileName : file.fileName,
|
|
67
|
+
contentType: file.contentType,
|
|
68
|
+
base64: Buffer.from(file.bytes).toString("base64"),
|
|
69
|
+
...(attachment && { purpose: "attachment" as const }),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
slug,
|
|
75
|
+
kind,
|
|
76
|
+
contentFormat: first.contentFormat,
|
|
77
|
+
variableSchema: storedRecord(first.variableSchema),
|
|
78
|
+
locales,
|
|
79
|
+
resources,
|
|
80
|
+
pdfAttachments,
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
});
|
|
@@ -6,7 +6,7 @@ import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
|
|
|
6
6
|
// Admin-Lookup für UI-Edit-Flow. Returnt das raw Template inkl. draft/
|
|
7
7
|
// archived Status. Tenant-Isolation: User sieht nur Templates des
|
|
8
8
|
// eigenen Tenants oder SYSTEM_TENANT (system-defaults sind public-
|
|
9
|
-
// readable).
|
|
9
|
+
// readable); ctx.db is tenant-scoped for every role, so the check below is defense in depth. Anonymous bleibt aus — Public-
|
|
10
10
|
// Read geht über resolveTemplate aus der API, nicht hier.
|
|
11
11
|
export const findByIdQuery = defineQueryHandler({
|
|
12
12
|
name: "find-by-id",
|
|
@@ -19,10 +19,9 @@ export const findByIdQuery = defineQueryHandler({
|
|
|
19
19
|
id: query.payload.id,
|
|
20
20
|
});
|
|
21
21
|
if (!row) return null;
|
|
22
|
-
const isSystemAdmin = query.user.roles.includes("SystemAdmin");
|
|
23
22
|
const isOwnTenant = row.tenantId === query.user.tenantId;
|
|
24
23
|
const isSystemTemplate = row.scope === "system";
|
|
25
|
-
if (!
|
|
24
|
+
if (!isOwnTenant && !isSystemTemplate) return null;
|
|
26
25
|
|
|
27
26
|
return {
|
|
28
27
|
id: String(row.id),
|
|
@@ -35,6 +34,7 @@ export const findByIdQuery = defineQueryHandler({
|
|
|
35
34
|
contentFormat: row.contentFormat,
|
|
36
35
|
variableSchema: row.variableSchema,
|
|
37
36
|
linkedResources: row.linkedResources,
|
|
37
|
+
meta: row.meta,
|
|
38
38
|
scope: row.scope,
|
|
39
39
|
parentTemplateId: row.parentTemplateId,
|
|
40
40
|
status: row.status,
|
package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { defineWriteHandler, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { ValidationError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
|
+
import * as z from "zod";
|
|
4
|
+
import { templateBundleSchema } from "../template-bundle.js";
|
|
5
|
+
import { importTemplateBundle } from "./bundle-import.js";
|
|
6
|
+
|
|
7
|
+
export const importBundleSystemWrite = defineWriteHandler({
|
|
8
|
+
name: "import-bundle-system",
|
|
9
|
+
schema: z.object({ bundle: templateBundleSchema }),
|
|
10
|
+
access: { roles: ["SystemAdmin"] },
|
|
11
|
+
// A system template change reaches every tenant without an override.
|
|
12
|
+
agent: { risk: "high" as const },
|
|
13
|
+
description:
|
|
14
|
+
"Imports a template bundle as system-default templates, active straight away and overwriting existing system rows; SystemAdmin only. Bundles with resource files need a caller in the system tenant, because the files are stored in the caller's file space.",
|
|
15
|
+
handler: (event, ctx) => {
|
|
16
|
+
const { bundle } = event.payload;
|
|
17
|
+
if (Object.keys(bundle.resources).length > 0 && event.user.tenantId !== SYSTEM_TENANT_ID) {
|
|
18
|
+
return Promise.resolve(
|
|
19
|
+
writeFailure(
|
|
20
|
+
new ValidationError({
|
|
21
|
+
fields: [
|
|
22
|
+
{
|
|
23
|
+
path: "bundle.resources",
|
|
24
|
+
code: "system_resources_need_system_tenant",
|
|
25
|
+
i18nKey: "templateResolver.errors.template.systemResourcesNeedSystemTenant",
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
}),
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return importTemplateBundle({
|
|
33
|
+
ctx,
|
|
34
|
+
user: event.user,
|
|
35
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
36
|
+
scope: "system",
|
|
37
|
+
status: "active",
|
|
38
|
+
bundle,
|
|
39
|
+
overwrite: true,
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
import { templateBundleSchema } from "../template-bundle.js";
|
|
4
|
+
import { importTemplateBundle } from "./bundle-import.js";
|
|
5
|
+
|
|
6
|
+
export const importBundleWrite = defineWriteHandler({
|
|
7
|
+
name: "import-bundle",
|
|
8
|
+
schema: z.object({ bundle: templateBundleSchema, overwrite: z.boolean().default(false) }),
|
|
9
|
+
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
10
|
+
description:
|
|
11
|
+
"Imports a template bundle (all locales plus resource files, as produced by export-bundle) into the caller's tenant as drafts; it refuses a locale that already has an override unless overwrite is set, and publish makes the drafts live.",
|
|
12
|
+
handler: (event, ctx) =>
|
|
13
|
+
importTemplateBundle({
|
|
14
|
+
ctx,
|
|
15
|
+
user: event.user,
|
|
16
|
+
tenantId: event.user.tenantId,
|
|
17
|
+
scope: "tenant",
|
|
18
|
+
status: "draft",
|
|
19
|
+
bundle: event.payload.bundle,
|
|
20
|
+
overwrite: event.payload.overwrite,
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
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 { localeSchema, templateSlugSchema } from "./shared.js";
|
|
8
|
+
|
|
9
|
+
export type LayoutSourceRow = {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
readonly version: number;
|
|
12
|
+
readonly status: string;
|
|
13
|
+
readonly locale: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type LayoutSourceResponse = {
|
|
17
|
+
readonly tenant: LayoutSourceRow | null;
|
|
18
|
+
readonly system: (LayoutSourceRow & { readonly contentFormat: string }) | null;
|
|
19
|
+
readonly regions: LayoutRegions;
|
|
20
|
+
readonly effectiveFormat: "html" | "mjml" | null;
|
|
21
|
+
readonly mjmlSource: string | null;
|
|
22
|
+
readonly locale: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const EMPTY_REGIONS: LayoutRegions = { header: "", signature: "", footer: "" };
|
|
26
|
+
|
|
27
|
+
function pickByLocale(
|
|
28
|
+
rows: readonly TemplateResourceRow[],
|
|
29
|
+
requested: string | undefined,
|
|
30
|
+
): TemplateResourceRow | null {
|
|
31
|
+
const live = rows.filter((row) => row.status !== "archived");
|
|
32
|
+
const candidates = live.length > 0 ? live : rows;
|
|
33
|
+
return (
|
|
34
|
+
candidates.find((row) => row.locale === requested) ??
|
|
35
|
+
candidates.find((row) => row.locale === FALLBACK_LOCALE) ??
|
|
36
|
+
candidates[0] ??
|
|
37
|
+
null
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function toSourceRow(row: TemplateResourceRow): LayoutSourceRow {
|
|
42
|
+
return { id: String(row.id), version: row.version, status: row.status, locale: row.locale };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function layoutFormat(row: TemplateResourceRow): "html" | "mjml" | null {
|
|
46
|
+
return row.contentFormat === "html" || row.contentFormat === "mjml" ? row.contentFormat : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function pickEditedRows(
|
|
50
|
+
rows: readonly TemplateResourceRow[],
|
|
51
|
+
isSystemAdmin: boolean,
|
|
52
|
+
locale: string | undefined,
|
|
53
|
+
): {
|
|
54
|
+
readonly tenantRow: TemplateResourceRow | null;
|
|
55
|
+
readonly systemRow: TemplateResourceRow | null;
|
|
56
|
+
} {
|
|
57
|
+
const tenantRow = isSystemAdmin
|
|
58
|
+
? null
|
|
59
|
+
: pickByLocale(
|
|
60
|
+
rows.filter((row) => row.scope === "tenant"),
|
|
61
|
+
locale,
|
|
62
|
+
);
|
|
63
|
+
const systemRow = pickByLocale(
|
|
64
|
+
rows.filter((row) => row.scope === "system"),
|
|
65
|
+
locale,
|
|
66
|
+
);
|
|
67
|
+
return { tenantRow, systemRow };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const layoutSourceQuery = defineQueryHandler({
|
|
71
|
+
name: "layout-source",
|
|
72
|
+
schema: z.object({
|
|
73
|
+
kind: z.enum(["mail-layout", "pdf-layout"]),
|
|
74
|
+
slug: templateSlugSchema.default("default"),
|
|
75
|
+
locale: localeSchema.optional(),
|
|
76
|
+
}),
|
|
77
|
+
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
78
|
+
description:
|
|
79
|
+
"Reads the layout the caller edits: the tenant's own version and the system version of one layout slug, the header, signature and footer regions of the effective html version, and the locale a save has to use; a SystemAdmin edits the system version and also receives its mjml source. Use it to load the layout editor.",
|
|
80
|
+
handler: async (query, ctx): Promise<LayoutSourceResponse> => {
|
|
81
|
+
const { kind, slug, locale } = query.payload;
|
|
82
|
+
const isSystemAdmin = query.user.roles.includes("SystemAdmin");
|
|
83
|
+
const rows = await selectMany<TemplateResourceRow>(ctx.db, templateResourcesTable, {
|
|
84
|
+
kind,
|
|
85
|
+
slug,
|
|
86
|
+
});
|
|
87
|
+
const { tenantRow, systemRow } = pickEditedRows(rows, isSystemAdmin, locale);
|
|
88
|
+
const effective = tenantRow ?? systemRow;
|
|
89
|
+
const effectiveFormat = effective === null ? null : layoutFormat(effective);
|
|
90
|
+
return {
|
|
91
|
+
tenant: tenantRow === null ? null : toSourceRow(tenantRow),
|
|
92
|
+
system:
|
|
93
|
+
systemRow === null
|
|
94
|
+
? null
|
|
95
|
+
: { ...toSourceRow(systemRow), contentFormat: systemRow.contentFormat },
|
|
96
|
+
regions:
|
|
97
|
+
effective !== null && effectiveFormat === "html"
|
|
98
|
+
? readLayoutRegions(effective.content ?? "")
|
|
99
|
+
: EMPTY_REGIONS,
|
|
100
|
+
effectiveFormat,
|
|
101
|
+
mjmlSource:
|
|
102
|
+
isSystemAdmin && systemRow !== null && layoutFormat(systemRow) === "mjml"
|
|
103
|
+
? (systemRow.content ?? "")
|
|
104
|
+
: null,
|
|
105
|
+
locale: effective?.locale ?? locale ?? FALLBACK_LOCALE,
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
});
|
|
@@ -4,9 +4,8 @@ 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
6
|
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// Spalten mit scope-marker. SystemAdmin sieht alle (auch andere Tenants).
|
|
7
|
+
// ctx.db is tenant-scoped even for SystemAdmin: every caller sees its own
|
|
8
|
+
// tenant's rows plus the system defaults, never another tenant's rows.
|
|
10
9
|
export const listQuery = defineQueryHandler({
|
|
11
10
|
name: "list",
|
|
12
11
|
schema: z.object({
|
|
@@ -21,10 +20,9 @@ export const listQuery = defineQueryHandler({
|
|
|
21
20
|
handler: async (query, ctx) => {
|
|
22
21
|
const where: Record<string, unknown> = {};
|
|
23
22
|
|
|
24
|
-
// includeSystem=false narrows to own tenant AT THE DB —
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
// the DB keeps the limit meaningful (no post-filter starvation).
|
|
23
|
+
// includeSystem=false narrows to own tenant AT THE DB — TenantDb permits
|
|
24
|
+
// narrowing within its enforced [own, SYSTEM] scope. Filtering at the DB
|
|
25
|
+
// keeps the limit meaningful (no post-filter starvation).
|
|
28
26
|
if (!query.payload.includeSystem) {
|
|
29
27
|
where["tenantId"] = query.user.tenantId;
|
|
30
28
|
}
|