@cosmicdrift/kumiko-samples 0.387.0 → 0.389.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 +8 -1
- package/packages/bundled-features/src/agent-tools/changes.json +12 -0
- package/packages/bundled-features/src/agent-tools/index.ts +1 -1
- package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +48 -32
- package/packages/bundled-features/src/audit/changes.json +12 -0
- package/packages/bundled-features/src/audit/feature.ts +1 -0
- package/packages/bundled-features/src/audit/handlers/audit-events.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/changes.json +12 -0
- package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +4 -0
- package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +8 -0
- package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +16 -3
- package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -0
- package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +8 -2
- package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +23 -21
- package/packages/bundled-features/src/auth-email-password/web/default-auth-pages-gate.tsx +6 -3
- package/packages/bundled-features/src/auth-email-password/web/forgot-password-screen.tsx +11 -1
- package/packages/bundled-features/src/auth-email-password/web/index.ts +5 -0
- package/packages/bundled-features/src/auth-email-password/web/login-screen.tsx +16 -5
- package/packages/bundled-features/src/auth-email-password/web/reset-password-screen.tsx +7 -1
- package/packages/bundled-features/src/auth-email-password/web/return-path.ts +35 -0
- package/packages/bundled-features/src/auth-email-password/web/session.tsx +6 -1
- package/packages/bundled-features/src/auth-email-password/web/signup-complete-screen.tsx +11 -3
- package/packages/bundled-features/src/auth-email-password/web/signup-screen.tsx +12 -2
- package/packages/bundled-features/src/auth-mfa/changes.json +12 -0
- package/packages/bundled-features/src/auth-mfa/config.ts +3 -7
- package/packages/bundled-features/src/auth-mfa/feature-exports.ts +2 -2
- package/packages/bundled-features/src/auth-mfa/feature.ts +6 -0
- package/packages/bundled-features/src/auth-mfa/mfa-status-checker.ts +8 -5
- package/packages/bundled-features/src/auth-mfa/web/i18n.ts +6 -1
- package/packages/bundled-features/src/auth-mfa/web/mfa-code-or-recovery-field.tsx +68 -0
- package/packages/bundled-features/src/auth-mfa/web/mfa-disable-dialog.tsx +23 -13
- package/packages/bundled-features/src/auth-mfa/web/mfa-regenerate-recovery-dialog.tsx +23 -13
- package/packages/bundled-features/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +15 -6
- package/packages/bundled-features/src/auth-mfa/web/mfa-verify-screen.tsx +65 -18
- package/packages/bundled-features/src/billing-foundation/changes.json +55 -0
- package/packages/bundled-features/src/billing-foundation/checkout-core.ts +32 -42
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +8 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +14 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +3 -4
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +12 -6
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +35 -19
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +12 -8
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-revoke.ts +14 -8
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +5 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +8 -10
- package/packages/bundled-features/src/billing-foundation/events.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/feature.ts +3 -1
- package/packages/bundled-features/src/billing-foundation/handlers/billing-plans.query.ts +71 -6
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/handlers/plan-switch-preview.query.ts +65 -0
- package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +9 -2
- package/packages/bundled-features/src/billing-foundation/handlers/start-plan-checkout.write.ts +44 -8
- package/packages/bundled-features/src/billing-foundation/handlers/switch-plan.write.ts +72 -82
- package/packages/bundled-features/src/billing-foundation/handlers/sync-subscription.write.ts +14 -1
- package/packages/bundled-features/src/billing-foundation/i18n.ts +22 -0
- package/packages/bundled-features/src/billing-foundation/index.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +62 -27
- package/packages/bundled-features/src/billing-foundation/plan-switch.ts +84 -0
- package/packages/bundled-features/src/billing-foundation/types.ts +80 -1
- package/packages/bundled-features/src/billing-foundation/validate-options.ts +19 -0
- package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +119 -47
- package/packages/bundled-features/src/billing-foundation/web/checkout-consent-dialog.tsx +89 -42
- package/packages/bundled-features/src/billing-foundation/web/constants.ts +4 -0
- package/packages/bundled-features/src/billing-foundation/web/plan-price.ts +55 -0
- package/packages/bundled-features/src/billing-foundation/web/plan-switch-confirm-dialog.tsx +224 -0
- package/packages/bundled-features/src/cap-counter/cap-reservation.ts +25 -8
- package/packages/bundled-features/src/cap-counter/changes.json +30 -0
- package/packages/bundled-features/src/cap-counter/enforce-cap.ts +2 -1
- package/packages/bundled-features/src/cap-counter/feature.ts +1 -0
- package/packages/bundled-features/src/cap-counter/stock-cap-guard.ts +8 -1
- package/packages/bundled-features/src/cap-overview/feature.ts +1 -0
- package/packages/bundled-features/src/channel-email/changes.json +6 -0
- package/packages/bundled-features/src/channel-email/feature.ts +1 -1
- package/packages/bundled-features/src/config/changes.json +18 -0
- package/packages/bundled-features/src/config/handlers/cascade.query.ts +4 -2
- package/packages/bundled-features/src/config/handlers/set.write.ts +19 -2
- package/packages/bundled-features/src/config/handlers/values.query.ts +4 -2
- package/packages/bundled-features/src/config/write-helpers.ts +27 -8
- package/packages/bundled-features/src/crypto-shredding/changes.json +18 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +11 -10
- package/packages/bundled-features/src/custom-fields/lib/parse-serialized-field.ts +1 -1
- package/packages/bundled-features/src/delivery/changes.json +42 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +9 -3
- package/packages/bundled-features/src/delivery/db/queries/preferences.ts +2 -2
- package/packages/bundled-features/src/delivery/delivery-service.ts +35 -5
- package/packages/bundled-features/src/delivery/feature.ts +1 -0
- package/packages/bundled-features/src/delivery/handlers/preferences.query.ts +20 -1
- package/packages/bundled-features/src/delivery/handlers/unsubscribe-page-chrome.query.ts +1 -1
- package/packages/bundled-features/src/delivery/index.ts +5 -0
- package/packages/bundled-features/src/delivery/system-channel-context.ts +33 -0
- package/packages/bundled-features/src/delivery/unsubscribe-link.ts +3 -1
- package/packages/bundled-features/src/delivery/unsubscribe-page-strings.ts +1 -0
- package/packages/bundled-features/src/delivery/unsubscribe-page.ts +6 -0
- package/packages/bundled-features/src/delivery/unsubscribe.ts +36 -15
- package/packages/bundled-features/src/document-ingest-foundation/append-missing-extract-event.ts +84 -0
- package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
- package/packages/bundled-features/src/document-ingest-foundation/events.ts +15 -0
- package/packages/bundled-features/src/document-ingest-foundation/index.ts +11 -0
- package/packages/bundled-features/src/feature-toggles/feature.ts +1 -0
- package/packages/bundled-features/src/file-foundation/index.ts +1 -0
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +7 -1
- package/packages/bundled-features/src/file-provider-s3/env-named-feature.ts +0 -2
- package/packages/bundled-features/src/files/README.md +1 -1
- package/packages/bundled-features/src/form-draft/changes.json +6 -0
- package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +2 -2
- package/packages/bundled-features/src/form-draft/decrypt-draft.ts +4 -1
- package/packages/bundled-features/src/host-routing/README.md +1 -1
- package/packages/bundled-features/src/host-routing/changes.json +12 -0
- package/packages/bundled-features/src/host-routing/dispatch.ts +16 -17
- package/packages/bundled-features/src/host-routing/options.ts +2 -1
- package/packages/bundled-features/src/host-routing/validate-options.ts +5 -0
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +18 -0
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +1 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +17 -5
- package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +0 -1
- package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +0 -3
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -3
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +32 -16
- package/packages/bundled-features/src/jobs/changes.json +13 -0
- package/packages/bundled-features/src/jobs/feature.ts +15 -4
- package/packages/bundled-features/src/jobs/handlers/tenant-failures.query.ts +45 -40
- package/packages/bundled-features/src/jobs/handlers/tenant-runs.query.ts +63 -59
- package/packages/bundled-features/src/jobs/job-run-logger.ts +5 -4
- package/packages/bundled-features/src/ledger/web/index.ts +1 -1
- package/packages/bundled-features/src/legal-pages/README.md +1 -1
- package/packages/bundled-features/src/legal-pages/seed.ts +5 -2
- package/packages/bundled-features/src/mail-foundation/changes.json +6 -0
- package/packages/bundled-features/src/mail-foundation/feature.ts +5 -3
- package/packages/bundled-features/src/mail-transport-smtp/changes.json +6 -0
- package/packages/bundled-features/src/mail-transport-smtp/config-keys.ts +1 -1
- package/packages/bundled-features/src/mail-transport-smtp/feature.ts +9 -11
- package/packages/bundled-features/src/managed-pages/changes.json +6 -0
- package/packages/bundled-features/src/managed-pages/constants.ts +1 -0
- package/packages/bundled-features/src/managed-pages/feature.ts +3 -1
- package/packages/bundled-features/src/managed-pages/index.ts +1 -0
- package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/feature.ts +5 -1
- package/packages/bundled-features/src/renderer-pdf/changes.json +6 -0
- package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +27 -10
- package/packages/bundled-features/src/secrets/changes.json +6 -0
- package/packages/bundled-features/src/secrets/write-gate.ts +9 -2
- package/packages/bundled-features/src/sessions/changes.json +7 -0
- package/packages/bundled-features/src/sessions/feature.ts +8 -2
- package/packages/bundled-features/src/sessions/index.ts +1 -1
- package/packages/bundled-features/src/sessions/session-callbacks.ts +1 -9
- package/packages/bundled-features/src/shared/decrypt-stored-pii.ts +10 -0
- package/packages/bundled-features/src/shared/index.ts +1 -1
- package/packages/bundled-features/src/shared/mail-template-body.ts +10 -3
- package/packages/bundled-features/src/shared/purge-files-reaching-subject.ts +24 -2
- package/packages/bundled-features/src/shared/run-in-sub-transaction.ts +5 -5
- package/packages/bundled-features/src/step-dispatcher/changes.json +12 -0
- package/packages/bundled-features/src/step-dispatcher/deliver-dispatch-request.ts +17 -3
- package/packages/bundled-features/src/step-dispatcher/feature.ts +1 -0
- package/packages/bundled-features/src/step-dispatcher/mail-runner.ts +2 -1
- package/packages/bundled-features/src/subscription-dodo/changes.json +14 -0
- package/packages/bundled-features/src/subscription-dodo/client.ts +45 -0
- package/packages/bundled-features/src/subscription-dodo/constants.ts +24 -0
- package/packages/bundled-features/src/subscription-dodo/feature.ts +207 -0
- package/packages/bundled-features/src/subscription-dodo/index.ts +16 -0
- package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +345 -0
- package/packages/bundled-features/src/subscription-dodo/runtime.ts +126 -0
- package/packages/bundled-features/src/subscription-dodo/verify-webhook.ts +262 -0
- package/packages/bundled-features/src/subscription-stripe/changes.json +12 -0
- package/packages/bundled-features/src/subscription-stripe/feature.ts +1 -0
- package/packages/bundled-features/src/subscription-stripe/runtime.ts +7 -6
- package/packages/bundled-features/src/tags/feature.ts +1 -0
- package/packages/bundled-features/src/template-resolver/api.ts +31 -4
- package/packages/bundled-features/src/template-resolver/changes.json +84 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +1 -0
- package/packages/bundled-features/src/template-resolver/document-ops.ts +20 -1
- package/packages/bundled-features/src/template-resolver/feature.ts +4 -4
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +27 -5
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/document-base.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +36 -11
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +18 -19
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +45 -10
- package/packages/bundled-features/src/template-resolver/handlers/patch-draft.write.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/send-test-status.query.ts +22 -19
- package/packages/bundled-features/src/template-resolver/handlers/tenant-override-counts.query.ts +9 -5
- package/packages/bundled-features/src/template-resolver/handlers/tenant-override-selection.ts +8 -6
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +44 -5
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +37 -26
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +7 -2
- package/packages/bundled-features/src/template-resolver/seeding.ts +42 -17
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +74 -12
- package/packages/bundled-features/src/template-resolver/template-delivery-policy.ts +0 -11
- package/packages/bundled-features/src/template-resolver/template-meta.ts +12 -4
- package/packages/bundled-features/src/template-resolver/template-preview.ts +11 -3
- package/packages/bundled-features/src/template-resolver/web/constants.ts +7 -5
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +6 -0
- package/packages/bundled-features/src/template-resolver/web/index.ts +7 -0
- package/packages/bundled-features/src/template-resolver/web/mail-layout-constants.ts +8 -0
- package/packages/bundled-features/src/template-resolver/web/qualified-names.ts +1 -0
- package/packages/bundled-features/src/tenant/changes.json +6 -0
- package/packages/bundled-features/src/tenant/feature.ts +1 -0
- package/packages/bundled-features/src/tenant/handlers/member-directory.query.ts +9 -3
- package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -3
- package/packages/bundled-features/src/tenant/handlers/tenant-directory.query.ts +7 -4
- package/packages/bundled-features/src/tenant-lifecycle/changes.json +12 -0
- package/packages/bundled-features/src/tenant-lifecycle/constants.ts +5 -0
- package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +5 -1
- package/packages/bundled-features/src/tenant-lifecycle/stages.ts +35 -1
- package/packages/bundled-features/src/tier-engine/changes.json +6 -0
- package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +4 -1
- package/packages/bundled-features/src/tier-engine/web/constants.ts +3 -3
- package/packages/bundled-features/src/user/feature.ts +1 -0
- package/packages/bundled-features/src/user-data-rights/changes.json +12 -0
- package/packages/bundled-features/src/user-data-rights/feature.ts +1 -0
- package/packages/bundled-features/src/user-data-rights/index.ts +6 -1
- package/packages/bundled-features/src/user-data-rights/run-export-jobs.ts +3 -4
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +2 -1
- package/packages/bundled-features/src/user-data-rights/web/constants.ts +6 -0
- package/packages/bundled-features/src/user-data-rights/web/index.ts +1 -0
- package/packages/bundled-features/src/waitlist/changes.json +36 -0
- package/packages/bundled-features/src/waitlist/feature.ts +1 -0
- package/packages/bundled-features/src/waitlist/handlers/export-purge.write.ts +2 -1
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +28 -9
- package/packages/bundled-features/src/waitlist/import.ts +28 -3
- package/packages/bundled-features/src/waitlist/legacy-migrate.ts +24 -4
- package/packages/bundled-features/src/waitlist/link-on-signup.ts +20 -10
- package/packages/bundled-features/src/waitlist/options.ts +5 -1
- package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +1 -1
- package/samples/apps/styleguide/src/app/client.tsx +2 -0
- package/samples/apps/styleguide/src/app/seed.ts +6 -4
- package/samples/apps/styleguide/src/app/server.ts +4 -1
- package/samples/apps/styleguide/src/features/widgets/feature.ts +15 -4
- package/samples/apps/styleguide/src/features/widgets/i18n.ts +11 -0
- package/samples/apps/styleguide/src/features/widgets/web/Widgets.tsx +24 -0
- package/samples/apps/styleguide/src/features/workboard/feature.ts +185 -0
- package/samples/apps/styleguide/src/features/workboard/i18n.ts +27 -0
- package/samples/apps/styleguide/src/features/workboard/seed.ts +121 -0
- package/samples/apps/styleguide/src/features/workboard/web.ts +13 -0
- package/samples/apps/ui-walkthrough/src/features/rental/i18n.ts +2 -0
- package/samples/apps/ui-walkthrough/src/features/rental/screens.ts +9 -0
- package/samples/apps/use-all-bundled/bin/main.ts +2 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +5 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +90 -15
- package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +16 -6
- package/samples/apps/use-all-bundled/feature-manifest.json +136 -3
- package/samples/apps/use-all-bundled/kumiko/schema.ts +2 -0
- package/samples/apps/use-all-bundled/kumiko.config.ts +2 -0
- package/samples/apps/use-all-bundled/package.json +2 -1
- package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +2 -0
- package/samples/apps/use-all-bundled/src/app/notes-feature.ts +2 -0
- package/samples/apps/use-all-bundled/src/bundled-lists.ts +15 -0
- package/samples/apps/use-all-bundled/src/run-config.ts +9 -3
- package/samples/recipes/auth-mfa/README.md +5 -0
- package/samples/recipes/auth-mfa/package.json +5 -2
- package/samples/recipes/auth-mfa/src/web/confirmation-code-step.tsx +49 -0
- package/samples/recipes/auth-mfa/tsconfig.json +8 -0
- package/samples/recipes/env-composition/README.md +3 -1
- package/samples/recipes/env-composition/src/env.ts +2 -0
- package/samples/recipes/golden-path/src/feature.ts +2 -1
- package/samples/recipes/legal-pages/README.md +1 -1
- package/samples/recipes/lifecycle-hooks/README.md +4 -0
- package/samples/recipes/lifecycle-hooks/src/feature.ts +26 -1
- package/samples/recipes/list-table-options/README.md +50 -0
- package/samples/recipes/list-table-options/package.json +11 -0
- package/samples/recipes/list-table-options/src/feature.ts +221 -0
- package/samples/recipes/pdf-audit-export/src/feature.ts +2 -1
- package/samples/recipes/rate-limiting/README.md +18 -0
- package/samples/recipes/rate-limiting/src/feature.ts +12 -0
- package/samples/recipes/state-machine/src/events.ts +3 -2
- package/samples/recipes/tenant-isolation/README.md +1 -0
- package/samples/recipes/tenant-isolation/src/feature.ts +36 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/stored-user-email.ts +0 -12
|
@@ -2,8 +2,9 @@ import { fetchOne } from "@cosmicdrift/kumiko-framework/db";
|
|
|
2
2
|
import { defineQueryHandler, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { NotFoundError } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
4
|
import * as z from "zod";
|
|
5
|
+
import { isRowDeliveredTo } from "../api.ts";
|
|
5
6
|
import { type TemplateResourceRow, templateResourcesTable } from "../table.ts";
|
|
6
|
-
import { deliveryPolicyOf
|
|
7
|
+
import { deliveryPolicyOf } from "../template-delivery-policy.ts";
|
|
7
8
|
import { isHiddenSystemDraft } from "../template-draft.ts";
|
|
8
9
|
import { buildOverrideDiff } from "./override-diff.ts";
|
|
9
10
|
|
|
@@ -21,7 +22,7 @@ export const diffToSystemQuery = defineQueryHandler({
|
|
|
21
22
|
!tenantRow ||
|
|
22
23
|
tenantRow.tenantId !== query.user.tenantId ||
|
|
23
24
|
tenantRow.scope !== "tenant" ||
|
|
24
|
-
!
|
|
25
|
+
!(await isRowDeliveredTo(ctx.db, deliveryPolicyOf(ctx.registry), query.user, tenantRow))
|
|
25
26
|
) {
|
|
26
27
|
throw new NotFoundError("template-resource", query.payload.id);
|
|
27
28
|
}
|
|
@@ -5,8 +5,9 @@ import {
|
|
|
5
5
|
SYSTEM_TENANT_ID,
|
|
6
6
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
7
|
import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
|
|
8
|
+
import { isRowDeliveredTo } from "../api.ts";
|
|
8
9
|
import { type TemplateResourceRow, templateResourcesTable } from "../table.ts";
|
|
9
|
-
import { deliveryPolicyOf
|
|
10
|
+
import { deliveryPolicyOf } from "../template-delivery-policy.ts";
|
|
10
11
|
import { isHiddenSystemDraft, toWorkingRow } from "../template-draft.ts";
|
|
11
12
|
import { isSystemOnlyTemplateHiddenFrom } from "../template-system-only.ts";
|
|
12
13
|
import type { DocumentHtmlKind } from "../web/constants.ts";
|
|
@@ -50,7 +51,7 @@ export async function loadDocumentBase(
|
|
|
50
51
|
const ownIsVisible =
|
|
51
52
|
own !== undefined &&
|
|
52
53
|
!isSystemOnlyTemplateHiddenFrom(user, ctx.registry, own) &&
|
|
53
|
-
|
|
54
|
+
(await isRowDeliveredTo(ctx.db, deliveryPolicyOf(ctx.registry), user, own));
|
|
54
55
|
if (own !== undefined && ownIsVisible) {
|
|
55
56
|
const working = toWorkingRow(own, user.tenantId);
|
|
56
57
|
return {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import * as z from "zod";
|
|
4
|
+
import { isRowDeliveredTo } from "../api.ts";
|
|
4
5
|
import { type TemplateResourceRow, templateResourcesTable } from "../table.ts";
|
|
5
|
-
import { deliveryPolicyOf
|
|
6
|
+
import { deliveryPolicyOf } from "../template-delivery-policy.ts";
|
|
6
7
|
import { draftViewerTenantId, isHiddenSystemDraft, toWorkingRow } from "../template-draft.ts";
|
|
7
8
|
import { isSystemOnlyTemplateHiddenFrom } from "../template-system-only.ts";
|
|
8
9
|
|
|
@@ -25,7 +26,7 @@ export const findByIdQuery = defineQueryHandler({
|
|
|
25
26
|
!stored ||
|
|
26
27
|
isHiddenSystemDraft(query.user, stored) ||
|
|
27
28
|
isSystemOnlyTemplateHiddenFrom(query.user, ctx.registry, stored) ||
|
|
28
|
-
!
|
|
29
|
+
!(await isRowDeliveredTo(ctx.db, deliveryPolicyOf(ctx.registry), query.user, stored))
|
|
29
30
|
) {
|
|
30
31
|
return null;
|
|
31
32
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { selectMany } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { AccessDeniedError } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
4
|
import * as z from "zod";
|
|
4
5
|
import { filterDeliveredTo } from "../api.ts";
|
|
5
6
|
import { type LayoutRegions, readLayoutRegions } from "../layout-regions.ts";
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
} from "../template-draft.ts";
|
|
15
16
|
import { type MailLayoutMeta, readMailLayoutMeta } from "../template-meta.ts";
|
|
16
17
|
import { FALLBACK_LOCALE } from "../web/constants.ts";
|
|
18
|
+
import { TemplateResolverErrors } from "../web/qualified-names.ts";
|
|
17
19
|
import { localeSchema, templateSlugSchema } from "./shared.ts";
|
|
18
20
|
import { parseStoredJson } from "./template-gates.ts";
|
|
19
21
|
|
|
@@ -66,20 +68,29 @@ function layoutFormat(row: WorkingRow): "html" | "mjml" | null {
|
|
|
66
68
|
return row.contentFormat === "html" || row.contentFormat === "mjml" ? row.contentFormat : null;
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
type EditScope = "tenant" | "system";
|
|
72
|
+
|
|
69
73
|
function pickEditedRows(
|
|
70
74
|
rows: readonly TemplateResourceRow[],
|
|
71
|
-
|
|
75
|
+
caller: { readonly tenantId: string; readonly roles: readonly string[] },
|
|
76
|
+
scope: EditScope,
|
|
72
77
|
locale: string | undefined,
|
|
73
78
|
): {
|
|
74
79
|
readonly tenantRow: WorkingRow | null;
|
|
75
80
|
readonly systemRow: WorkingRow | null;
|
|
76
81
|
} {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
// A SystemAdmin editing the tenant layout sees the system version like a tenant does: served only, no draft.
|
|
83
|
+
const user =
|
|
84
|
+
scope === "system"
|
|
85
|
+
? caller
|
|
86
|
+
: { ...caller, roles: caller.roles.filter((role) => role !== "SystemAdmin") };
|
|
87
|
+
const tenantRow =
|
|
88
|
+
scope === "system"
|
|
89
|
+
? null
|
|
90
|
+
: pickByLocale(
|
|
91
|
+
rows.filter((row) => row.scope === "tenant"),
|
|
92
|
+
locale,
|
|
93
|
+
);
|
|
83
94
|
const systemRow = pickByLocale(
|
|
84
95
|
rows.filter((row) => row.scope === "system" && !isHiddenSystemDraft(user, row)),
|
|
85
96
|
locale,
|
|
@@ -136,13 +147,21 @@ export const layoutSourceQuery = defineQueryHandler({
|
|
|
136
147
|
kind: z.enum(["mail-layout", "pdf-layout"]),
|
|
137
148
|
slug: templateSlugSchema.default("default"),
|
|
138
149
|
locale: localeSchema.optional(),
|
|
150
|
+
scope: z.enum(["tenant", "system"]).optional(),
|
|
139
151
|
}),
|
|
140
152
|
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
141
153
|
description:
|
|
142
|
-
"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.",
|
|
154
|
+
"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 by default and also receives its mjml source; scope tenant loads the SystemAdmin's own tenant layout instead, and scope system is refused for everyone else. Use it to load the layout editor.",
|
|
143
155
|
handler: async (query, ctx): Promise<LayoutSourceResponse> => {
|
|
144
156
|
const { kind, slug, locale } = query.payload;
|
|
145
|
-
const
|
|
157
|
+
const callerIsSystemAdmin = query.user.roles.includes("SystemAdmin");
|
|
158
|
+
const scope = query.payload.scope ?? (callerIsSystemAdmin ? "system" : "tenant");
|
|
159
|
+
if (scope === "system" && !callerIsSystemAdmin) {
|
|
160
|
+
throw new AccessDeniedError({
|
|
161
|
+
i18nKey: "errors.access.denied",
|
|
162
|
+
details: { reason: TemplateResolverErrors.systemAdminRequired },
|
|
163
|
+
});
|
|
164
|
+
}
|
|
146
165
|
const policy = deliveryPolicyOf(ctx.registry);
|
|
147
166
|
const rows = await filterDeliveredTo(
|
|
148
167
|
ctx.db,
|
|
@@ -150,7 +169,13 @@ export const layoutSourceQuery = defineQueryHandler({
|
|
|
150
169
|
query.user,
|
|
151
170
|
await selectMany<TemplateResourceRow>(ctx.db, templateResourcesTable, { kind, slug }),
|
|
152
171
|
);
|
|
153
|
-
const { tenantRow, systemRow } = pickEditedRows(rows, query.user, locale);
|
|
154
|
-
return buildLayoutSource({
|
|
172
|
+
const { tenantRow, systemRow } = pickEditedRows(rows, query.user, scope, locale);
|
|
173
|
+
return buildLayoutSource({
|
|
174
|
+
kind,
|
|
175
|
+
locale,
|
|
176
|
+
isSystemAdmin: scope === "system",
|
|
177
|
+
tenantRow,
|
|
178
|
+
systemRow,
|
|
179
|
+
});
|
|
155
180
|
},
|
|
156
181
|
});
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { selectMany } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import * as z from "zod";
|
|
4
|
+
import { filterDeliveredTo } from "../api.ts";
|
|
4
5
|
import { type TemplateResourceRow, templateResourcesTable } from "../table.ts";
|
|
5
|
-
import { deliveryPolicyOf
|
|
6
|
+
import { deliveryPolicyOf } from "../template-delivery-policy.ts";
|
|
6
7
|
import { isHiddenSystemDraft } from "../template-draft.ts";
|
|
7
8
|
import { isSystemOnlyTemplateHiddenFrom } from "../template-system-only.ts";
|
|
8
9
|
import { TEMPLATE_KINDS, TEMPLATE_STATUSES } from "../web/constants.ts";
|
|
@@ -47,23 +48,21 @@ export const listQuery = defineQueryHandler({
|
|
|
47
48
|
})) as TemplateResourceRow[];
|
|
48
49
|
|
|
49
50
|
const deliveryPolicy = deliveryPolicyOf(ctx.registry);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
(row)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
)
|
|
57
|
-
.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
modifiedAt: row.modifiedAt,
|
|
67
|
-
}));
|
|
51
|
+
const candidates = rows.filter(
|
|
52
|
+
(row) =>
|
|
53
|
+
!isHiddenSystemDraft(query.user, row) &&
|
|
54
|
+
!isSystemOnlyTemplateHiddenFrom(query.user, ctx.registry, row),
|
|
55
|
+
);
|
|
56
|
+
return (await filterDeliveredTo(ctx.db, deliveryPolicy, query.user, candidates)).map((row) => ({
|
|
57
|
+
id: String(row.id),
|
|
58
|
+
tenantId: row.tenantId,
|
|
59
|
+
slug: row.slug,
|
|
60
|
+
kind: row.kind,
|
|
61
|
+
locale: row.locale,
|
|
62
|
+
scope: row.scope,
|
|
63
|
+
status: row.status,
|
|
64
|
+
contentFormat: row.contentFormat,
|
|
65
|
+
modifiedAt: row.modifiedAt,
|
|
66
|
+
}));
|
|
68
67
|
},
|
|
69
68
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { selectMany } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
|
+
import { createI18n } from "@cosmicdrift/kumiko-framework/i18n";
|
|
3
4
|
import * as z from "zod";
|
|
4
5
|
import { type TemplateResourceRow, templateResourcesTable } from "../table.ts";
|
|
5
6
|
import {
|
|
@@ -16,7 +17,11 @@ import {
|
|
|
16
17
|
parseTemplateDraft,
|
|
17
18
|
servedVersionOf,
|
|
18
19
|
} from "../template-draft.ts";
|
|
19
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
mayEditTemplate,
|
|
22
|
+
type TemplateEditPolicy,
|
|
23
|
+
templateEditAccess,
|
|
24
|
+
} from "../template-edit-policy.ts";
|
|
20
25
|
import { isSystemOnlyTemplateHiddenFrom } from "../template-system-only.ts";
|
|
21
26
|
import { collectTemplateUsage, type EffectiveMailLayoutSlugs } from "../template-usage.ts";
|
|
22
27
|
import {
|
|
@@ -155,6 +160,19 @@ function effectiveRowsOf(
|
|
|
155
160
|
: null;
|
|
156
161
|
}
|
|
157
162
|
|
|
163
|
+
export function isRowVisibleToCaller(
|
|
164
|
+
policy: TemplateEditPolicy,
|
|
165
|
+
user: Parameters<typeof isHiddenSystemDraft>[0],
|
|
166
|
+
registry: Parameters<typeof isSystemOnlyTemplateHiddenFrom>[1],
|
|
167
|
+
row: TemplateResourceRow,
|
|
168
|
+
): boolean {
|
|
169
|
+
return (
|
|
170
|
+
!isHiddenSystemDraft(user, row) &&
|
|
171
|
+
!isSystemOnlyTemplateHiddenFrom(user, registry, row) &&
|
|
172
|
+
mayEditTemplate(policy, user, row)
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
158
176
|
export function effectiveMailLayoutSlugs(
|
|
159
177
|
rows: readonly TemplateResourceRow[],
|
|
160
178
|
policy: TemplateDeliveryPolicy,
|
|
@@ -191,7 +209,15 @@ function splitDocumentSlug(slug: string): { feature: string; notification: strin
|
|
|
191
209
|
: { feature: slug.slice(0, separator), notification: slug.slice(separator + 1) };
|
|
192
210
|
}
|
|
193
211
|
|
|
194
|
-
|
|
212
|
+
type ShownTitle = (row: OverviewRow) => string;
|
|
213
|
+
|
|
214
|
+
function compareBy(
|
|
215
|
+
field: SortField,
|
|
216
|
+
a: OverviewRow,
|
|
217
|
+
b: OverviewRow,
|
|
218
|
+
shownTitle: ShownTitle,
|
|
219
|
+
): number {
|
|
220
|
+
if (field === "title") return shownTitle(a).localeCompare(shownTitle(b));
|
|
195
221
|
return String(a[field]).localeCompare(String(b[field]));
|
|
196
222
|
}
|
|
197
223
|
|
|
@@ -338,11 +364,17 @@ function filterRows(
|
|
|
338
364
|
search: string | undefined,
|
|
339
365
|
filters: readonly OverviewFilter[],
|
|
340
366
|
layoutSlug: string | undefined,
|
|
367
|
+
shownTitle: ShownTitle,
|
|
341
368
|
): OverviewRow[] {
|
|
342
369
|
const needle = search?.trim().toLowerCase() ?? "";
|
|
343
370
|
return rows.filter((row) => {
|
|
344
371
|
if (layoutSlug !== undefined && row.layoutSlug !== layoutSlug) return false;
|
|
345
|
-
if (
|
|
372
|
+
if (
|
|
373
|
+
needle !== "" &&
|
|
374
|
+
!`${shownTitle(row)} ${row.title} ${row.slug}`.toLowerCase().includes(needle)
|
|
375
|
+
) {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
346
378
|
return filters.every((filter) => filter.value.includes(String(row[filter.field])));
|
|
347
379
|
});
|
|
348
380
|
}
|
|
@@ -366,7 +398,7 @@ export function makeOverviewQuery(policy: TemplateEditPolicy) {
|
|
|
366
398
|
offset: z.number().int().min(0).optional(),
|
|
367
399
|
totalCount: z.boolean().optional(),
|
|
368
400
|
}),
|
|
369
|
-
access:
|
|
401
|
+
access: templateEditAccess(policy.tenantEditable),
|
|
370
402
|
description:
|
|
371
403
|
"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; templates of notifications registered with `systemOnly` are listed for SystemAdmins only, and a TenantAdmin sees only the templates the app lists in tenantEditable; a tenant override of a template that is not listed is not delivered, so the row shows the system version with `lockedOverride` true (SystemAdmin only); use it to browse and filter all templates at a glance.",
|
|
372
404
|
handler: async (query, ctx) => {
|
|
@@ -385,11 +417,8 @@ export function makeOverviewQuery(policy: TemplateEditPolicy) {
|
|
|
385
417
|
});
|
|
386
418
|
}
|
|
387
419
|
|
|
388
|
-
const visibleRows = rows.filter(
|
|
389
|
-
(row)
|
|
390
|
-
!isHiddenSystemDraft(query.user, row) &&
|
|
391
|
-
!isSystemOnlyTemplateHiddenFrom(query.user, ctx.registry, row) &&
|
|
392
|
-
mayEditTemplate(policy, query.user, row),
|
|
420
|
+
const visibleRows = rows.filter((row) =>
|
|
421
|
+
isRowVisibleToCaller(policy, query.user, ctx.registry, row),
|
|
393
422
|
);
|
|
394
423
|
const deliveryPolicy = deliveryPolicyOf(ctx.registry);
|
|
395
424
|
const { drafts, layoutSlugByDocument } = buildDrafts(
|
|
@@ -409,17 +438,23 @@ export function makeOverviewQuery(policy: TemplateEditPolicy) {
|
|
|
409
438
|
),
|
|
410
439
|
)
|
|
411
440
|
: rowsWithUsage;
|
|
441
|
+
// A seeded title can be an i18n key; search and sort must see what the admin sees.
|
|
442
|
+
const i18n = createI18n(ctx.registry, { defaultLocale: "en" });
|
|
443
|
+
const shownTitle: ShownTitle = (row) => i18n.t(row.title, ctx.locale);
|
|
412
444
|
const filtered = filterRows(
|
|
413
445
|
overviewRows,
|
|
414
446
|
payload.search,
|
|
415
447
|
payload.filters ?? [],
|
|
416
448
|
payload.layoutSlug,
|
|
449
|
+
shownTitle,
|
|
417
450
|
);
|
|
418
451
|
|
|
419
452
|
const sortField: SortField =
|
|
420
453
|
payload.sort !== undefined && isSortField(payload.sort) ? payload.sort : "modifiedAt";
|
|
421
454
|
const direction = (payload.sortDirection ?? "desc") === "desc" ? -1 : 1;
|
|
422
|
-
filtered.sort(
|
|
455
|
+
filtered.sort(
|
|
456
|
+
(a, b) => direction * compareBy(sortField, a, b, shownTitle) || a.id.localeCompare(b.id),
|
|
457
|
+
);
|
|
423
458
|
|
|
424
459
|
const offset = payload.offset ?? 0;
|
|
425
460
|
return {
|
|
@@ -40,7 +40,7 @@ export function makePatchDraftWrite(policy: TemplateEditPolicy) {
|
|
|
40
40
|
}),
|
|
41
41
|
access: templateEditAccess(policy.tenantEditable),
|
|
42
42
|
description:
|
|
43
|
-
"Changes a mail or PDF document template with structured operations instead of a full rewrite and saves the result as the caller's tenant draft (never published, an active version keeps being served until publish): insertBlock after a block id (null = at the start; paragraph, heading, image, button, divider, pageBreak), replaceText inside one block (the text must occur exactly once), removeBlock, setResource on an image block, setMeta (flat merge, null removes a key) and setVariable (adds, replaces or with null removes a variable definition; refused when a notification declares the variables). Block ids and base come from document-blocks and refer to the state that was read; if the template changed since, the call fails with version_conflict and nothing is written. A patch that removes a required placeholder is refused. A TenantAdmin may patch only templates the app lists in tenantEditable, otherwise SystemAdmin only. Returns the written row.",
|
|
43
|
+
"Changes a mail or PDF document template with structured operations instead of a full rewrite and saves the result as the caller's tenant draft (never published, an active version keeps being served until publish): insertBlock after a block id (null = at the start; paragraph, heading, image, button, divider, pageBreak), replaceText inside one block (the text must occur exactly once within one formatting run and cannot contain a {{placeholder}}: a find across a formatting change fails with text_spans_formatting, a find with a placeholder with placeholder_not_replaceable, and the formatting of the replaced text is kept), removeBlock, setResource on an image block, setMeta (flat merge, null removes a key) and setVariable (adds, replaces or with null removes a variable definition; refused when a notification declares the variables). Block ids and base come from document-blocks and refer to the state that was read; if the template changed since, the call fails with version_conflict and nothing is written. A patch that removes a required placeholder is refused. A TenantAdmin may patch only templates the app lists in tenantEditable, otherwise SystemAdmin only. Returns the written row.",
|
|
44
44
|
handler: async (event, ctx) => {
|
|
45
45
|
const { slug, kind, locale, base, ops } = event.payload;
|
|
46
46
|
const refused = refuseTemplateEdit(policy, event.user, { slug, kind });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import * as z from "zod";
|
|
3
|
+
import { type TemplateEditPolicy, templateEditAccess } from "../template-edit-policy.ts";
|
|
3
4
|
import {
|
|
4
5
|
emailChannelOf,
|
|
5
6
|
resolveTestMailAddress,
|
|
@@ -10,22 +11,24 @@ export type SendTestStatus =
|
|
|
10
11
|
| { readonly canSend: true; readonly reason: null }
|
|
11
12
|
| { readonly canSend: false; readonly reason: "no_channel" | "no_address" };
|
|
12
13
|
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
14
|
+
export function makeSendTestStatusQuery(policy: TemplateEditPolicy) {
|
|
15
|
+
return defineQueryHandler({
|
|
16
|
+
name: "send-test-status",
|
|
17
|
+
schema: z.object({}),
|
|
18
|
+
access: templateEditAccess(policy.tenantEditable),
|
|
19
|
+
description:
|
|
20
|
+
"Tells whether send-test can reach the calling user: canSend is true when an email channel is mounted and resolves an address for the caller, otherwise reason is no_channel or no_address; the address itself is never returned.",
|
|
21
|
+
handler: async (query, ctx): Promise<SendTestStatus> => {
|
|
22
|
+
const channel = emailChannelOf(ctx.registry);
|
|
23
|
+
if (channel === undefined) return { canSend: false, reason: "no_channel" };
|
|
24
|
+
const address = await resolveTestMailAddress(
|
|
25
|
+
channel,
|
|
26
|
+
testMailChannelContext(ctx, query.user),
|
|
27
|
+
query.user,
|
|
28
|
+
);
|
|
29
|
+
return address === null
|
|
30
|
+
? { canSend: false, reason: "no_address" }
|
|
31
|
+
: { canSend: true, reason: null };
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
package/packages/bundled-features/src/template-resolver/handlers/tenant-override-counts.query.ts
CHANGED
|
@@ -8,6 +8,11 @@ import {
|
|
|
8
8
|
TENANT_OVERRIDES_REASON,
|
|
9
9
|
} from "./tenant-override-selection.ts";
|
|
10
10
|
|
|
11
|
+
type OverrideCountRow = Pick<
|
|
12
|
+
TemplateResourceRow,
|
|
13
|
+
"tenantId" | "slug" | "kind" | "status" | "version"
|
|
14
|
+
>;
|
|
15
|
+
|
|
11
16
|
export const tenantOverrideCountsResultSchema = z.object({
|
|
12
17
|
totalTenants: z.number().nullable(),
|
|
13
18
|
counts: z.array(z.object({ slug: z.string(), kind: z.string(), activeOverrides: z.number() })),
|
|
@@ -21,18 +26,17 @@ export const tenantOverrideCountsQuery = defineQueryHandler({
|
|
|
21
26
|
"Counts, per template slug and kind, the enabled tenants that override it with an active version, next to the number of enabled tenants; one query for the whole template overview instead of one tenant-overrides call per row.",
|
|
22
27
|
escapeHatch: { grants: ["unsafeRaw"], reason: TENANT_OVERRIDES_REASON },
|
|
23
28
|
handler: async (_query, ctx) => {
|
|
24
|
-
const found = await selectMany<
|
|
29
|
+
const found = await selectMany<OverrideCountRow>(
|
|
25
30
|
ctx.db.unsafeRaw(),
|
|
26
31
|
templateResourcesTable,
|
|
27
|
-
{
|
|
28
|
-
|
|
29
|
-
},
|
|
32
|
+
{ scope: "tenant" },
|
|
33
|
+
{ fields: ["tenantId", "slug", "kind", "status", "version"] },
|
|
30
34
|
);
|
|
31
35
|
const enabledTenantIds = await loadEnabledTenantIds(ctx);
|
|
32
36
|
|
|
33
37
|
const rowsBySlugAndKind = new Map<
|
|
34
38
|
string,
|
|
35
|
-
{ slug: string; kind: string; rows:
|
|
39
|
+
{ slug: string; kind: string; rows: OverrideCountRow[] }
|
|
36
40
|
>();
|
|
37
41
|
for (const row of found) {
|
|
38
42
|
const key = `${row.kind}|${row.slug}`;
|
package/packages/bundled-features/src/template-resolver/handlers/tenant-override-selection.ts
CHANGED
|
@@ -21,8 +21,10 @@ export async function loadEnabledTenantIds(
|
|
|
21
21
|
return new Set(tenantIdsSchema.parse(await ctx.query(TenantQueries.activeTenantIds, {})));
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
export type OverrideCandidate = Pick<TemplateResourceRow, "tenantId" | "status" | "version">;
|
|
25
|
+
|
|
24
26
|
// One row per tenant: its active override wins over a draft, then the newest version.
|
|
25
|
-
function isBetterRow(candidate:
|
|
27
|
+
function isBetterRow(candidate: OverrideCandidate, current: OverrideCandidate): boolean {
|
|
26
28
|
if ((candidate.status === "active") !== (current.status === "active")) {
|
|
27
29
|
return candidate.status === "active";
|
|
28
30
|
}
|
|
@@ -30,18 +32,18 @@ function isBetterRow(candidate: TemplateResourceRow, current: TemplateResourceRo
|
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
function isCountedTenantRow(
|
|
33
|
-
row:
|
|
35
|
+
row: OverrideCandidate,
|
|
34
36
|
enabledTenantIds: ReadonlySet<string> | null,
|
|
35
37
|
): boolean {
|
|
36
38
|
if (row.tenantId === SYSTEM_TENANT_ID || row.status === "archived") return false;
|
|
37
39
|
return enabledTenantIds === null || enabledTenantIds.has(row.tenantId);
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
export function pickBestRowPerTenant(
|
|
41
|
-
found: readonly
|
|
42
|
+
export function pickBestRowPerTenant<TRow extends OverrideCandidate>(
|
|
43
|
+
found: readonly TRow[],
|
|
42
44
|
enabledTenantIds: ReadonlySet<string> | null,
|
|
43
|
-
): Map<string,
|
|
44
|
-
const bestByTenant = new Map<string,
|
|
45
|
+
): Map<string, TRow> {
|
|
46
|
+
const bestByTenant = new Map<string, TRow>();
|
|
45
47
|
for (const row of found) {
|
|
46
48
|
if (!isCountedTenantRow(row, enabledTenantIds)) continue;
|
|
47
49
|
const current = bestByTenant.get(row.tenantId);
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { selectMany } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
defineQueryHandler,
|
|
4
|
+
MAX_LIST_LIMIT,
|
|
5
|
+
SYSTEM_TENANT_ID,
|
|
6
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
3
7
|
import * as z from "zod";
|
|
4
8
|
import { TenantQueries } from "../../tenant/index.ts";
|
|
5
9
|
import { type TemplateResourceRow, templateResourcesTable } from "../table.ts";
|
|
@@ -18,8 +22,39 @@ const directorySchema = z.object({
|
|
|
18
22
|
|
|
19
23
|
type OverrideStatus = "active" | "draft";
|
|
20
24
|
|
|
21
|
-
function
|
|
22
|
-
|
|
25
|
+
function chunksOf<T>(items: readonly T[], size: number): T[][] {
|
|
26
|
+
const chunks: T[][] = [];
|
|
27
|
+
for (let start = 0; start < items.length; start += size) {
|
|
28
|
+
chunks.push(items.slice(start, start + size));
|
|
29
|
+
}
|
|
30
|
+
return chunks;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type DirectoryQueryContext = {
|
|
34
|
+
readonly query: (name: string, payload: { ids: string[] }) => Promise<unknown>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Looks up only the ids the overrides need, so labels never depend on the directory's default cap.
|
|
38
|
+
async function labelsFor(
|
|
39
|
+
ctx: DirectoryQueryContext,
|
|
40
|
+
queryName: string,
|
|
41
|
+
ids: ReadonlySet<string>,
|
|
42
|
+
): Promise<ReadonlyMap<string, string>> {
|
|
43
|
+
const labels = new Map<string, string>();
|
|
44
|
+
for (const chunk of chunksOf([...ids], MAX_LIST_LIMIT)) {
|
|
45
|
+
const raw = await ctx.query(queryName, { ids: chunk });
|
|
46
|
+
for (const row of directorySchema.parse(raw).rows) labels.set(row.id, row.label);
|
|
47
|
+
}
|
|
48
|
+
return labels;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function changerIdsOf(rows: readonly TemplateResourceRow[]): ReadonlySet<string> {
|
|
52
|
+
const ids = new Set<string>();
|
|
53
|
+
for (const row of rows) {
|
|
54
|
+
const changedById = row.modifiedById ?? row.insertedById;
|
|
55
|
+
if (changedById !== null) ids.add(changedById);
|
|
56
|
+
}
|
|
57
|
+
return ids;
|
|
23
58
|
}
|
|
24
59
|
|
|
25
60
|
function statusOf(row: TemplateResourceRow): OverrideStatus {
|
|
@@ -73,10 +108,14 @@ export const tenantOverridesQuery = defineQueryHandler({
|
|
|
73
108
|
const bestByTenant = pickBestRowPerTenant(found, enabledTenantIds);
|
|
74
109
|
const best = [...bestByTenant.values()];
|
|
75
110
|
const tenantLabels = hasTenantFeature
|
|
76
|
-
?
|
|
111
|
+
? await labelsFor(
|
|
112
|
+
ctx,
|
|
113
|
+
TenantQueries.tenantDirectory,
|
|
114
|
+
new Set(best.map((row) => row.tenantId)),
|
|
115
|
+
)
|
|
77
116
|
: new Map<string, string>();
|
|
78
117
|
const userLabels = hasTenantFeature
|
|
79
|
-
?
|
|
118
|
+
? await labelsFor(ctx, TenantQueries.memberDirectory, changerIdsOf(best))
|
|
80
119
|
: new Map<string, string>();
|
|
81
120
|
|
|
82
121
|
const rows = best
|
|
@@ -3,35 +3,46 @@ import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
|
3
3
|
import * as z from "zod";
|
|
4
4
|
import { type TemplateResourceRow, templateResourcesTable } from "../table.ts";
|
|
5
5
|
import { deliveryPolicyOf } from "../template-delivery-policy.ts";
|
|
6
|
+
import type { TemplateEditPolicy } from "../template-edit-policy.ts";
|
|
6
7
|
import { templateSlugSchema } from "../template-meta.ts";
|
|
7
8
|
import { collectTemplateUsage } from "../template-usage.ts";
|
|
8
9
|
import { TEMPLATE_KINDS } from "../web/constants.ts";
|
|
9
|
-
import { effectiveMailLayoutSlugs } from "./overview.query.ts";
|
|
10
|
+
import { effectiveMailLayoutSlugs, isRowVisibleToCaller } from "./overview.query.ts";
|
|
10
11
|
|
|
11
12
|
const MAIL_ROW_LIMIT = 5000;
|
|
12
13
|
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
ctx.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
14
|
+
export function makeUsageQuery(policy: TemplateEditPolicy) {
|
|
15
|
+
return defineQueryHandler({
|
|
16
|
+
name: "usage",
|
|
17
|
+
schema: z.object({ slug: templateSlugSchema, kind: z.enum(TEMPLATE_KINDS) }),
|
|
18
|
+
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
19
|
+
description:
|
|
20
|
+
"Lists the code-registered notifications that use one template: a mail template is used by the notification whose qualified name is its slug and that has a code template, a mail layout by every mail notification whose effective template (own override, else system) names it, or whose code meta does when no stored template exists, or that falls back to the default layout; use it to show what a change to a template affects.",
|
|
21
|
+
handler: async (query, ctx) => {
|
|
22
|
+
const { slug, kind } = query.payload;
|
|
23
|
+
if (kind !== "mail-layout") return { usedBy: collectTemplateUsage(ctx.registry, slug, kind) };
|
|
24
|
+
const fetchedRows = await selectMany<TemplateResourceRow>(
|
|
25
|
+
ctx.db,
|
|
26
|
+
templateResourcesTable,
|
|
27
|
+
{ kind: "mail" },
|
|
28
|
+
{ orderBy: { col: "id", direction: "asc" }, limit: MAIL_ROW_LIMIT + 1 },
|
|
29
|
+
);
|
|
30
|
+
if (fetchedRows.length > MAIL_ROW_LIMIT) {
|
|
31
|
+
ctx.log?.warn("template usage: row limit reached, result is incomplete", {
|
|
32
|
+
limit: MAIL_ROW_LIMIT,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
const visibleRows = fetchedRows
|
|
36
|
+
.slice(0, MAIL_ROW_LIMIT)
|
|
37
|
+
.filter((row) => isRowVisibleToCaller(policy, query.user, ctx.registry, row));
|
|
38
|
+
return {
|
|
39
|
+
usedBy: collectTemplateUsage(
|
|
40
|
+
ctx.registry,
|
|
41
|
+
slug,
|
|
42
|
+
kind,
|
|
43
|
+
effectiveMailLayoutSlugs(visibleRows, deliveryPolicyOf(ctx.registry)),
|
|
44
|
+
),
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -6,10 +6,11 @@ import {
|
|
|
6
6
|
SYSTEM_TENANT_ID,
|
|
7
7
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
8
8
|
import * as z from "zod";
|
|
9
|
+
import { isRowDeliveredTo } from "../api.ts";
|
|
9
10
|
import type { TemplateWriteScope } from "../layout-regions.ts";
|
|
10
11
|
import { imageResourceKeysOf } from "../linked-resources.ts";
|
|
11
12
|
import { type TemplateResourceRow, templateResourcesTable } from "../table.ts";
|
|
12
|
-
import { deliveryPolicyOf
|
|
13
|
+
import { deliveryPolicyOf } from "../template-delivery-policy.ts";
|
|
13
14
|
import { isSystemAdmin } from "../template-draft.ts";
|
|
14
15
|
import { resolveTemplateVariables, systemPlaceholdersFor } from "../template-validation.ts";
|
|
15
16
|
import type { ContentFormat, UpsertKind } from "../web/constants.ts";
|
|
@@ -69,7 +70,11 @@ export async function knownResourceKeysFor(
|
|
|
69
70
|
locale,
|
|
70
71
|
})),
|
|
71
72
|
...(scope === "tenant" &&
|
|
72
|
-
|
|
73
|
+
(await isRowDeliveredTo(ctx.db, deliveryPolicyOf(ctx.registry), user, {
|
|
74
|
+
tenantId: user.tenantId,
|
|
75
|
+
kind,
|
|
76
|
+
slug,
|
|
77
|
+
}))
|
|
73
78
|
? await knownResourceKeys(ctx.db, { tenantId: user.tenantId, slug, kind, scope, locale })
|
|
74
79
|
: []),
|
|
75
80
|
];
|