@cosmicdrift/kumiko-bundled-features 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/dist/agent-tools/index.d.ts +1 -1
- package/dist/agent-tools/index.js +1 -1
- package/dist/agent-tools/tool-dispatch.d.ts +12 -7
- package/dist/agent-tools/tool-dispatch.js +42 -31
- package/dist/audit/feature.js +1 -0
- package/dist/audit/handlers/audit-events.js +1 -1
- package/dist/auth-email-password/handlers/request-account-unlock.write.d.ts +1 -0
- package/dist/auth-email-password/handlers/request-account-unlock.write.js +1 -0
- package/dist/auth-email-password/handlers/request-email-verification.write.d.ts +1 -0
- package/dist/auth-email-password/handlers/request-email-verification.write.js +1 -0
- package/dist/auth-email-password/handlers/request-password-reset.write.d.ts +1 -0
- package/dist/auth-email-password/handlers/request-password-reset.write.js +1 -0
- package/dist/auth-email-password/handlers/signup-request.write.d.ts +1 -0
- package/dist/auth-email-password/handlers/signup-request.write.js +4 -0
- package/dist/auth-email-password/handlers/token-request-handler.d.ts +2 -0
- package/dist/auth-email-password/handlers/token-request-handler.js +5 -0
- package/dist/auth-email-password/magic-link-mail.d.ts +2 -0
- package/dist/auth-email-password/magic-link-mail.js +7 -3
- package/dist/auth-email-password/signup-activation.d.ts +1 -0
- package/dist/auth-email-password/signup-activation.js +1 -0
- package/dist/auth-email-password/web/auth-client.d.ts +2 -2
- package/dist/auth-email-password/web/auth-client.js +8 -4
- package/dist/auth-email-password/web/auth-redirect.d.ts +5 -1
- package/dist/auth-email-password/web/auth-redirect.js +20 -25
- package/dist/auth-email-password/web/default-auth-pages-gate.js +6 -3
- package/dist/auth-email-password/web/forgot-password-screen.d.ts +4 -1
- package/dist/auth-email-password/web/forgot-password-screen.js +7 -2
- package/dist/auth-email-password/web/index.d.ts +1 -0
- package/dist/auth-email-password/web/index.js +1 -0
- package/dist/auth-email-password/web/login-screen.js +11 -6
- package/dist/auth-email-password/web/reset-password-screen.js +7 -1
- package/dist/auth-email-password/web/return-path.d.ts +4 -0
- package/dist/auth-email-password/web/return-path.js +34 -0
- package/dist/auth-email-password/web/session.js +6 -1
- package/dist/auth-email-password/web/signup-complete-screen.js +11 -3
- package/dist/auth-email-password/web/signup-screen.d.ts +4 -1
- package/dist/auth-email-password/web/signup-screen.js +8 -3
- package/dist/auth-mfa/config.d.ts +1 -1
- package/dist/auth-mfa/config.js +3 -7
- package/dist/auth-mfa/feature-exports.d.ts +2 -2
- package/dist/auth-mfa/feature.js +8 -0
- package/dist/auth-mfa/mfa-status-checker.js +5 -3
- package/dist/auth-mfa/web/i18n.js +5 -1
- package/dist/auth-mfa/web/mfa-code-or-recovery-field.d.ts +11 -0
- package/dist/auth-mfa/web/mfa-code-or-recovery-field.js +12 -0
- package/dist/auth-mfa/web/mfa-disable-dialog.js +14 -3
- package/dist/auth-mfa/web/mfa-regenerate-recovery-dialog.js +14 -3
- package/dist/auth-mfa/web/mfa-setup-preauth-screen.js +10 -5
- package/dist/auth-mfa/web/mfa-verify-screen.js +26 -5
- package/dist/billing-foundation/checkout-core.d.ts +9 -14
- package/dist/billing-foundation/checkout-core.js +24 -39
- package/dist/billing-foundation/consumer-protection/confirmation-mail.js +8 -2
- package/dist/billing-foundation/consumer-protection/consent-gate.d.ts +1 -0
- package/dist/billing-foundation/consumer-protection/consent-gate.js +1 -0
- package/dist/billing-foundation/consumer-protection/issue-confirmation.js +9 -3
- package/dist/billing-foundation/consumer-protection/mail-notifications.js +3 -1
- package/dist/billing-foundation/consumer-protection/termination-receipt-limit.d.ts +4 -2
- package/dist/billing-foundation/consumer-protection/termination-receipt-limit.js +6 -5
- package/dist/billing-foundation/consumer-protection/termination-record.d.ts +5 -1
- package/dist/billing-foundation/consumer-protection/termination-record.js +27 -17
- package/dist/billing-foundation/consumer-protection/termination-request.js +12 -7
- package/dist/billing-foundation/consumer-protection/termination-revoke.js +12 -7
- package/dist/billing-foundation/consumer-protection/termination-texts.d.ts +1 -1
- package/dist/billing-foundation/consumer-protection/termination-texts.js +3 -0
- package/dist/billing-foundation/consumer-protection/withdrawal-period.d.ts +5 -2
- package/dist/billing-foundation/consumer-protection/withdrawal-period.js +8 -9
- package/dist/billing-foundation/events.d.ts +1 -0
- package/dist/billing-foundation/events.js +1 -0
- package/dist/billing-foundation/feature.js +3 -1
- package/dist/billing-foundation/handlers/billing-plans.query.js +35 -5
- package/dist/billing-foundation/handlers/create-checkout-session.write.js +1 -0
- package/dist/billing-foundation/handlers/plan-switch-preview.query.d.ts +13 -0
- package/dist/billing-foundation/handlers/plan-switch-preview.query.js +48 -0
- package/dist/billing-foundation/handlers/process-event.write.d.ts +6 -0
- package/dist/billing-foundation/handlers/process-event.write.js +8 -2
- package/dist/billing-foundation/handlers/start-plan-checkout.write.js +32 -5
- package/dist/billing-foundation/handlers/switch-plan.write.d.ts +2 -2
- package/dist/billing-foundation/handlers/switch-plan.write.js +56 -64
- package/dist/billing-foundation/handlers/sync-subscription.write.js +12 -1
- package/dist/billing-foundation/i18n.js +22 -0
- package/dist/billing-foundation/index.d.ts +1 -1
- package/dist/billing-foundation/plan-catalog.d.ts +21 -5
- package/dist/billing-foundation/plan-catalog.js +25 -16
- package/dist/billing-foundation/plan-switch.d.ts +12 -0
- package/dist/billing-foundation/plan-switch.js +60 -0
- package/dist/billing-foundation/types.d.ts +57 -1
- package/dist/billing-foundation/types.js +9 -0
- package/dist/billing-foundation/validate-options.js +17 -0
- package/dist/billing-foundation/web/billing-plans-panel.js +78 -32
- package/dist/billing-foundation/web/checkout-consent-dialog.d.ts +17 -3
- package/dist/billing-foundation/web/checkout-consent-dialog.js +14 -10
- package/dist/billing-foundation/web/constants.d.ts +4 -0
- package/dist/billing-foundation/web/constants.js +4 -0
- package/dist/billing-foundation/web/plan-price.d.ts +15 -0
- package/dist/billing-foundation/web/plan-price.js +32 -0
- package/dist/billing-foundation/web/plan-switch-confirm-dialog.d.ts +15 -0
- package/dist/billing-foundation/web/plan-switch-confirm-dialog.js +66 -0
- package/dist/cap-counter/cap-reservation.js +20 -7
- package/dist/cap-counter/enforce-cap.js +2 -1
- package/dist/cap-counter/feature.js +1 -0
- package/dist/cap-counter/stock-cap-guard.js +7 -1
- package/dist/cap-overview/feature.js +1 -0
- package/dist/channel-email/feature.js +1 -1
- package/dist/config/handlers/cascade.query.d.ts +5 -0
- package/dist/config/handlers/cascade.query.js +4 -3
- package/dist/config/handlers/set.write.js +12 -2
- package/dist/config/handlers/values.query.d.ts +5 -0
- package/dist/config/handlers/values.query.js +4 -3
- package/dist/config/write-helpers.d.ts +1 -0
- package/dist/config/write-helpers.js +18 -9
- package/dist/crypto-shredding/handlers/forget-subject.write.js +14 -11
- package/dist/custom-fields/lib/parse-serialized-field.js +1 -1
- package/dist/delivery/channel-context.js +6 -3
- package/dist/delivery/db/queries/preferences.js +2 -2
- package/dist/delivery/delivery-service.js +22 -7
- package/dist/delivery/feature.js +1 -0
- package/dist/delivery/handlers/preferences.query.d.ts +1 -1
- package/dist/delivery/handlers/preferences.query.js +20 -1
- package/dist/delivery/handlers/unsubscribe-page-chrome.query.js +1 -1
- package/dist/delivery/index.d.ts +1 -0
- package/dist/delivery/index.js +1 -0
- package/dist/delivery/system-channel-context.d.ts +15 -0
- package/dist/delivery/system-channel-context.js +5 -0
- package/dist/delivery/unsubscribe-link.d.ts +3 -1
- package/dist/delivery/unsubscribe-page-strings.d.ts +1 -0
- package/dist/delivery/unsubscribe-page-strings.js +1 -0
- package/dist/delivery/unsubscribe-page.d.ts +2 -0
- package/dist/delivery/unsubscribe-page.js +5 -0
- package/dist/delivery/unsubscribe.js +25 -7
- package/dist/document-ingest-foundation/append-missing-extract-event.d.ts +21 -0
- package/dist/document-ingest-foundation/append-missing-extract-event.js +42 -0
- package/dist/document-ingest-foundation/events.d.ts +11 -0
- package/dist/document-ingest-foundation/events.js +11 -0
- package/dist/document-ingest-foundation/index.d.ts +2 -1
- package/dist/document-ingest-foundation/index.js +2 -1
- package/dist/feature-toggles/feature.js +1 -0
- package/dist/file-foundation/index.d.ts +1 -1
- package/dist/file-foundation/index.js +1 -1
- package/dist/file-foundation/read-file-ref-content.d.ts +3 -0
- package/dist/file-foundation/read-file-ref-content.js +6 -1
- package/dist/file-provider-s3/env-named-feature.d.ts +0 -2
- package/dist/form-draft/db/queries/cleanup.js +2 -2
- package/dist/form-draft/decrypt-draft.js +5 -1
- package/dist/host-routing/dispatch.js +11 -20
- package/dist/host-routing/validate-options.js +3 -0
- package/dist/inbound-mail-foundation/feature.js +1 -0
- package/dist/inbound-mail-foundation/handlers/update-account.write.d.ts +2 -2
- package/dist/inbound-mail-foundation/handlers/update-account.write.js +12 -4
- package/dist/inbound-mail-foundation/i18n.js +0 -1
- package/dist/inbound-mail-foundation/screens.js +0 -3
- package/dist/inbound-mail-foundation/types.d.ts +4 -3
- package/dist/inbound-mail-foundation/watch-supervisor.js +28 -11
- package/dist/jobs/feature.d.ts +1 -0
- package/dist/jobs/feature.js +9 -4
- package/dist/jobs/handlers/tenant-failures.query.d.ts +1 -1
- package/dist/jobs/handlers/tenant-failures.query.js +45 -41
- package/dist/jobs/handlers/tenant-runs.query.d.ts +1 -1
- package/dist/jobs/handlers/tenant-runs.query.js +54 -50
- package/dist/jobs/job-run-logger.js +5 -4
- package/dist/ledger/web/index.js +1 -1
- package/dist/legal-pages/seed.d.ts +3 -1
- package/dist/legal-pages/seed.js +2 -2
- package/dist/mail-foundation/feature.d.ts +3 -3
- package/dist/mail-foundation/feature.js +3 -0
- package/dist/mail-transport-smtp/config-keys.js +1 -1
- package/dist/mail-transport-smtp/feature.js +6 -5
- package/dist/managed-pages/constants.d.ts +1 -0
- package/dist/managed-pages/constants.js +1 -0
- package/dist/managed-pages/feature.js +3 -1
- package/dist/managed-pages/index.d.ts +1 -0
- package/dist/managed-pages/index.js +1 -0
- package/dist/notes-history/handlers/add-note.write.js +1 -1
- package/dist/personal-access-tokens/feature.d.ts +1 -1
- package/dist/personal-access-tokens/feature.js +5 -0
- package/dist/renderer-pdf/render-table-pdf.d.ts +1 -1
- package/dist/renderer-pdf/render-table-pdf.js +22 -10
- package/dist/secrets/write-gate.js +8 -2
- package/dist/sessions/feature.d.ts +2 -2
- package/dist/sessions/feature.js +5 -0
- package/dist/sessions/index.d.ts +1 -1
- package/dist/sessions/index.js +1 -1
- package/dist/sessions/session-callbacks.d.ts +0 -2
- package/dist/sessions/session-callbacks.js +1 -3
- package/dist/shared/decrypt-stored-pii.d.ts +1 -0
- package/dist/shared/decrypt-stored-pii.js +6 -1
- package/dist/shared/index.d.ts +1 -1
- package/dist/shared/index.js +1 -1
- package/dist/shared/mail-template-body.d.ts +6 -2
- package/dist/shared/mail-template-body.js +7 -4
- package/dist/shared/purge-files-reaching-subject.d.ts +1 -0
- package/dist/shared/purge-files-reaching-subject.js +19 -2
- package/dist/shared/run-in-sub-transaction.js +5 -5
- package/dist/step-dispatcher/deliver-dispatch-request.js +16 -3
- package/dist/step-dispatcher/feature.js +1 -0
- package/dist/step-dispatcher/mail-runner.d.ts +1 -0
- package/dist/subscription-dodo/client.d.ts +17 -0
- package/dist/subscription-dodo/client.js +31 -0
- package/dist/subscription-dodo/constants.d.ts +12 -0
- package/dist/subscription-dodo/constants.js +19 -0
- package/dist/subscription-dodo/feature.d.ts +37 -0
- package/dist/subscription-dodo/feature.js +144 -0
- package/dist/subscription-dodo/index.d.ts +2 -0
- package/dist/subscription-dodo/index.js +4 -0
- package/dist/subscription-dodo/plugin-methods.d.ts +23 -0
- package/dist/subscription-dodo/plugin-methods.js +260 -0
- package/dist/subscription-dodo/runtime.d.ts +27 -0
- package/dist/subscription-dodo/runtime.js +89 -0
- package/dist/subscription-dodo/verify-webhook.d.ts +37 -0
- package/dist/subscription-dodo/verify-webhook.js +189 -0
- package/dist/subscription-stripe/feature.js +1 -0
- package/dist/subscription-stripe/runtime.js +7 -6
- package/dist/tags/feature.js +1 -0
- package/dist/template-resolver/api.d.ts +3 -2
- package/dist/template-resolver/api.js +24 -2
- package/dist/template-resolver/compile-document-body.js +1 -0
- package/dist/template-resolver/document-ops.d.ts +5 -0
- package/dist/template-resolver/document-ops.js +18 -2
- package/dist/template-resolver/feature.js +4 -4
- package/dist/template-resolver/handlers/bundle-import.js +3 -3
- package/dist/template-resolver/handlers/copy-locale.write.d.ts +5 -1
- package/dist/template-resolver/handlers/copy-locale.write.js +22 -7
- package/dist/template-resolver/handlers/diff-to-system.query.js +3 -2
- package/dist/template-resolver/handlers/document-base.js +3 -2
- package/dist/template-resolver/handlers/find-by-id.query.js +3 -2
- package/dist/template-resolver/handlers/layout-source.query.d.ts +4 -0
- package/dist/template-resolver/handlers/layout-source.query.js +26 -6
- package/dist/template-resolver/handlers/list.query.js +5 -6
- package/dist/template-resolver/handlers/overview.query.d.ts +3 -0
- package/dist/template-resolver/handlers/overview.query.js +21 -10
- package/dist/template-resolver/handlers/patch-draft.write.js +1 -1
- package/dist/template-resolver/handlers/send-test-status.query.d.ts +2 -1
- package/dist/template-resolver/handlers/send-test-status.query.js +18 -15
- package/dist/template-resolver/handlers/tenant-override-counts.query.js +1 -3
- package/dist/template-resolver/handlers/tenant-override-selection.d.ts +2 -1
- package/dist/template-resolver/handlers/tenant-overrides.query.js +28 -5
- package/dist/template-resolver/handlers/usage.query.d.ts +2 -1
- package/dist/template-resolver/handlers/usage.query.js +26 -16
- package/dist/template-resolver/handlers/validate.query.js +7 -2
- package/dist/template-resolver/seeding.d.ts +1 -0
- package/dist/template-resolver/seeding.js +31 -12
- package/dist/template-resolver/store-template-resource.d.ts +1 -0
- package/dist/template-resolver/store-template-resource.js +65 -10
- package/dist/template-resolver/template-delivery-policy.d.ts +0 -5
- package/dist/template-resolver/template-delivery-policy.js +0 -6
- package/dist/template-resolver/template-meta.js +7 -4
- package/dist/template-resolver/template-preview.js +4 -3
- package/dist/template-resolver/web/constants.d.ts +2 -2
- package/dist/template-resolver/web/constants.js +3 -3
- package/dist/template-resolver/web/i18n.js +3 -0
- package/dist/template-resolver/web/index.d.ts +1 -0
- package/dist/template-resolver/web/index.js +1 -0
- package/dist/template-resolver/web/mail-layout-constants.d.ts +5 -0
- package/dist/template-resolver/web/mail-layout-constants.js +7 -0
- package/dist/template-resolver/web/qualified-names.d.ts +1 -0
- package/dist/template-resolver/web/qualified-names.js +1 -0
- package/dist/tenant/feature.js +1 -0
- package/dist/tenant/handlers/member-directory.query.d.ts +1 -0
- package/dist/tenant/handlers/member-directory.query.js +10 -3
- package/dist/tenant/handlers/team-list.query.js +3 -3
- package/dist/tenant/handlers/tenant-directory.query.d.ts +1 -0
- package/dist/tenant/handlers/tenant-directory.query.js +8 -4
- package/dist/tenant-lifecycle/constants.d.ts +1 -0
- package/dist/tenant-lifecycle/constants.js +4 -0
- package/dist/tenant-lifecycle/run-tenant-destroy.js +2 -2
- package/dist/tenant-lifecycle/stages.js +22 -1
- package/dist/tier-engine/handlers/set-tenant-tier.write.d.ts +2 -3
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +4 -1
- package/dist/tier-engine/web/constants.js +2 -2
- package/dist/user/feature.js +1 -0
- package/dist/user-data-rights/feature.js +1 -0
- package/dist/user-data-rights/index.d.ts +1 -1
- package/dist/user-data-rights/index.js +1 -1
- package/dist/user-data-rights/run-export-jobs.js +3 -4
- package/dist/user-data-rights/run-forget-cleanup.js +2 -1
- package/dist/user-data-rights/web/constants.d.ts +6 -0
- package/dist/user-data-rights/web/constants.js +6 -0
- package/dist/user-data-rights/web/index.d.ts +1 -0
- package/dist/user-data-rights/web/index.js +1 -0
- package/dist/waitlist/feature.js +1 -0
- package/dist/waitlist/handlers/export-purge.write.js +2 -1
- package/dist/waitlist/handlers/submit.write.js +14 -10
- package/dist/waitlist/import.d.ts +3 -1
- package/dist/waitlist/import.js +26 -2
- package/dist/waitlist/legacy-migrate.js +18 -7
- package/dist/waitlist/link-on-signup.js +18 -6
- package/dist/waitlist/options.d.ts +5 -1
- package/dist/workflow-runner/handlers/resume-run.write.js +1 -1
- package/package.json +19 -9
- package/src/agent-tools/changes.json +12 -0
- package/src/audit/changes.json +12 -0
- package/src/auth-email-password/changes.json +12 -0
- package/src/auth-mfa/changes.json +12 -0
- package/src/billing-foundation/changes.json +55 -0
- package/src/cap-counter/changes.json +30 -0
- package/src/channel-email/changes.json +6 -0
- package/src/config/changes.json +18 -0
- package/src/crypto-shredding/changes.json +18 -0
- package/src/delivery/changes.json +42 -0
- package/src/document-ingest-foundation/changes.json +6 -0
- package/src/form-draft/changes.json +6 -0
- package/src/host-routing/changes.json +12 -0
- package/src/inbound-mail-foundation/changes.json +18 -0
- package/src/jobs/changes.json +13 -0
- package/src/mail-foundation/changes.json +6 -0
- package/src/mail-transport-smtp/changes.json +6 -0
- package/src/managed-pages/changes.json +6 -0
- package/src/renderer-pdf/changes.json +6 -0
- package/src/secrets/changes.json +6 -0
- package/src/sessions/changes.json +7 -0
- package/src/step-dispatcher/changes.json +12 -0
- package/src/subscription-dodo/changes.json +14 -0
- package/src/subscription-stripe/changes.json +12 -0
- package/src/template-resolver/changes.json +84 -0
- package/src/tenant/changes.json +6 -0
- package/src/tenant-lifecycle/changes.json +12 -0
- package/src/tier-engine/changes.json +6 -0
- package/src/user-data-rights/changes.json +12 -0
- package/src/waitlist/changes.json +36 -0
- package/dist/billing-foundation/consumer-protection/stored-user-email.d.ts +0 -1
- package/dist/billing-foundation/consumer-protection/stored-user-email.js +0 -8
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { selectMany } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
-
import { defineQueryHandler, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { defineQueryHandler, MAX_LIST_LIMIT, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import * as z from "zod";
|
|
4
4
|
import { TenantQueries } from "../../tenant/index.js";
|
|
5
5
|
import { templateResourcesTable } from "../table.js";
|
|
@@ -10,8 +10,31 @@ import { loadEnabledTenantIds, pickBestRowPerTenant, TENANT_OVERRIDES_REASON, }
|
|
|
10
10
|
const directorySchema = z.object({
|
|
11
11
|
rows: z.array(z.object({ id: z.string(), label: z.string() })),
|
|
12
12
|
});
|
|
13
|
-
function
|
|
14
|
-
|
|
13
|
+
function chunksOf(items, size) {
|
|
14
|
+
const chunks = [];
|
|
15
|
+
for (let start = 0; start < items.length; start += size) {
|
|
16
|
+
chunks.push(items.slice(start, start + size));
|
|
17
|
+
}
|
|
18
|
+
return chunks;
|
|
19
|
+
}
|
|
20
|
+
// Looks up only the ids the overrides need, so labels never depend on the directory's default cap.
|
|
21
|
+
async function labelsFor(ctx, queryName, ids) {
|
|
22
|
+
const labels = new Map();
|
|
23
|
+
for (const chunk of chunksOf([...ids], MAX_LIST_LIMIT)) {
|
|
24
|
+
const raw = await ctx.query(queryName, { ids: chunk });
|
|
25
|
+
for (const row of directorySchema.parse(raw).rows)
|
|
26
|
+
labels.set(row.id, row.label);
|
|
27
|
+
}
|
|
28
|
+
return labels;
|
|
29
|
+
}
|
|
30
|
+
function changerIdsOf(rows) {
|
|
31
|
+
const ids = new Set();
|
|
32
|
+
for (const row of rows) {
|
|
33
|
+
const changedById = row.modifiedById ?? row.insertedById;
|
|
34
|
+
if (changedById !== null)
|
|
35
|
+
ids.add(changedById);
|
|
36
|
+
}
|
|
37
|
+
return ids;
|
|
15
38
|
}
|
|
16
39
|
function statusOf(row) {
|
|
17
40
|
return row.status === "active" ? "active" : "draft";
|
|
@@ -55,10 +78,10 @@ export const tenantOverridesQuery = defineQueryHandler({
|
|
|
55
78
|
const bestByTenant = pickBestRowPerTenant(found, enabledTenantIds);
|
|
56
79
|
const best = [...bestByTenant.values()];
|
|
57
80
|
const tenantLabels = hasTenantFeature
|
|
58
|
-
?
|
|
81
|
+
? await labelsFor(ctx, TenantQueries.tenantDirectory, new Set(best.map((row) => row.tenantId)))
|
|
59
82
|
: new Map();
|
|
60
83
|
const userLabels = hasTenantFeature
|
|
61
|
-
?
|
|
84
|
+
? await labelsFor(ctx, TenantQueries.memberDirectory, changerIdsOf(best))
|
|
62
85
|
: new Map();
|
|
63
86
|
const rows = best
|
|
64
87
|
.map((row) => toOverrideRow(row, tenantLabels, userLabels))
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
|
-
|
|
2
|
+
import type { TemplateEditPolicy } from "../template-edit-policy.ts";
|
|
3
|
+
export declare function makeUsageQuery(policy: TemplateEditPolicy): import("@cosmicdrift/kumiko-types/define-handler").QueryHandlerDefinition<"usage", z.ZodObject<{
|
|
3
4
|
slug: z.ZodString;
|
|
4
5
|
kind: z.ZodEnum<{
|
|
5
6
|
"ai-prompt": "ai-prompt";
|
|
@@ -6,20 +6,30 @@ import { deliveryPolicyOf } from "../template-delivery-policy.js";
|
|
|
6
6
|
import { templateSlugSchema } from "../template-meta.js";
|
|
7
7
|
import { collectTemplateUsage } from "../template-usage.js";
|
|
8
8
|
import { TEMPLATE_KINDS } from "../web/constants.js";
|
|
9
|
-
import { effectiveMailLayoutSlugs } from "./overview.query.js";
|
|
9
|
+
import { effectiveMailLayoutSlugs, isRowVisibleToCaller } from "./overview.query.js";
|
|
10
10
|
const MAIL_ROW_LIMIT = 5000;
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
});
|
|
11
|
+
export function makeUsageQuery(policy) {
|
|
12
|
+
return defineQueryHandler({
|
|
13
|
+
name: "usage",
|
|
14
|
+
schema: z.object({ slug: templateSlugSchema, kind: z.enum(TEMPLATE_KINDS) }),
|
|
15
|
+
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
16
|
+
description: "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.",
|
|
17
|
+
handler: async (query, ctx) => {
|
|
18
|
+
const { slug, kind } = query.payload;
|
|
19
|
+
if (kind !== "mail-layout")
|
|
20
|
+
return { usedBy: collectTemplateUsage(ctx.registry, slug, kind) };
|
|
21
|
+
const fetchedRows = await selectMany(ctx.db, templateResourcesTable, { kind: "mail" }, { orderBy: { col: "id", direction: "asc" }, limit: MAIL_ROW_LIMIT + 1 });
|
|
22
|
+
if (fetchedRows.length > MAIL_ROW_LIMIT) {
|
|
23
|
+
ctx.log?.warn("template usage: row limit reached, result is incomplete", {
|
|
24
|
+
limit: MAIL_ROW_LIMIT,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const visibleRows = fetchedRows
|
|
28
|
+
.slice(0, MAIL_ROW_LIMIT)
|
|
29
|
+
.filter((row) => isRowVisibleToCaller(policy, query.user, ctx.registry, row));
|
|
30
|
+
return {
|
|
31
|
+
usedBy: collectTemplateUsage(ctx.registry, slug, kind, effectiveMailLayoutSlugs(visibleRows, deliveryPolicyOf(ctx.registry))),
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { selectMany } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { defineQueryHandler, SYSTEM_TENANT_ID, } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import * as z from "zod";
|
|
4
|
+
import { isRowDeliveredTo } from "../api.js";
|
|
4
5
|
import { imageResourceKeysOf } from "../linked-resources.js";
|
|
5
6
|
import { templateResourcesTable } from "../table.js";
|
|
6
|
-
import { deliveryPolicyOf
|
|
7
|
+
import { deliveryPolicyOf } from "../template-delivery-policy.js";
|
|
7
8
|
import { isSystemAdmin } from "../template-draft.js";
|
|
8
9
|
import { resolveTemplateVariables, systemPlaceholdersFor } from "../template-validation.js";
|
|
9
10
|
import { contentFormatSchema, kindSchema, localeSchema, templateSlugSchema } from "./shared.js";
|
|
@@ -31,7 +32,11 @@ export async function knownResourceKeysFor(ctx, user, where) {
|
|
|
31
32
|
locale,
|
|
32
33
|
})),
|
|
33
34
|
...(scope === "tenant" &&
|
|
34
|
-
|
|
35
|
+
(await isRowDeliveredTo(ctx.db, deliveryPolicyOf(ctx.registry), user, {
|
|
36
|
+
tenantId: user.tenantId,
|
|
37
|
+
kind,
|
|
38
|
+
slug,
|
|
39
|
+
}))
|
|
35
40
|
? await knownResourceKeys(ctx.db, { tenantId: user.tenantId, slug, kind, scope, locale })
|
|
36
41
|
: []),
|
|
37
42
|
];
|
|
@@ -39,10 +39,16 @@ async function resolveExistingForEventStoreSeed(db, tdb, existingRow, tenantId)
|
|
|
39
39
|
}
|
|
40
40
|
return { id, version: streamVersion };
|
|
41
41
|
}
|
|
42
|
+
// Namespace prefix plus path, no whitespace: keeps free text from passing as a key that the UI
|
|
43
|
+
// would then show raw.
|
|
44
|
+
const I18N_KEY_FORMAT = /^[^\s:]+:\S+$/;
|
|
42
45
|
function seedTitleColumn(opts, label) {
|
|
43
46
|
if (opts.title !== undefined && opts.titleI18nKey !== undefined) {
|
|
44
47
|
throw new Error(`${label} rejected: set either title or titleI18nKey, not both`);
|
|
45
48
|
}
|
|
49
|
+
if (opts.titleI18nKey !== undefined && !I18N_KEY_FORMAT.test(opts.titleI18nKey)) {
|
|
50
|
+
throw new Error(`${label} rejected: titleI18nKey "${opts.titleI18nKey}" is not a "namespace:key" i18n key`);
|
|
51
|
+
}
|
|
46
52
|
return opts.titleI18nKey ?? opts.title ?? null;
|
|
47
53
|
}
|
|
48
54
|
export async function seedSystemTemplate(db, opts) {
|
|
@@ -101,16 +107,18 @@ export async function seedSystemTemplate(db, opts) {
|
|
|
101
107
|
current?.title === rowFields.title &&
|
|
102
108
|
current?.variableSchema === rowFields.variableSchema &&
|
|
103
109
|
current?.meta === rowFields.meta &&
|
|
104
|
-
current?.status === rowFields.status &&
|
|
105
110
|
current?.parentTemplateId === rowFields.parentTemplateId &&
|
|
106
111
|
(linkedResources === undefined || current?.linkedResources === linkedResources)) {
|
|
107
112
|
return { id: String(row.id) };
|
|
108
113
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
// An archived system row is a SystemAdmin decision (reversible with publish-system) that a
|
|
115
|
+
// pod-boot re-seed must not undo; content still follows the code. The seed replaces the
|
|
116
|
+
// content, so an open draft on the old content is dropped with it.
|
|
117
|
+
const { status: _status, ...contentFields } = rowFields;
|
|
118
|
+
const changes = current?.status === "archived"
|
|
119
|
+
? { ...contentFields, status: "archived", draft: null }
|
|
120
|
+
: { ...rowFields, ...activeStampChanges(row), draft: null };
|
|
121
|
+
const result = await executor.update({ id: row.id, version: row.version, changes }, by, tdb);
|
|
114
122
|
if (!result.isSuccess) {
|
|
115
123
|
throw new Error(`seedSystemTemplate update failed: ${JSON.stringify(result)}`);
|
|
116
124
|
}
|
|
@@ -191,8 +199,10 @@ async function seedSystemTemplateBundleWithResources(db, bundle, opts) {
|
|
|
191
199
|
if (!dispatcher) {
|
|
192
200
|
throw new Error(`${label}: bundles with resources need a file provider; pass opts.dispatcher (the dispatcher of the seed deps) so the files are stored through the import-bundle-system write`);
|
|
193
201
|
}
|
|
194
|
-
|
|
195
|
-
|
|
202
|
+
const skipExisting = (opts.ifExists ?? "skip") === "skip";
|
|
203
|
+
const existingIds = {};
|
|
204
|
+
let bundleToImport = bundle;
|
|
205
|
+
if (skipExisting) {
|
|
196
206
|
for (const locale of Object.keys(bundle.locales)) {
|
|
197
207
|
const row = await fetchOne(db, templateResourcesTable, {
|
|
198
208
|
tenantId: SYSTEM_TENANT_ID,
|
|
@@ -206,12 +216,18 @@ async function seedSystemTemplateBundleWithResources(db, bundle, opts) {
|
|
|
206
216
|
if (Object.keys(existingIds).length === Object.keys(bundle.locales).length) {
|
|
207
217
|
return { ids: existingIds };
|
|
208
218
|
}
|
|
219
|
+
// import-bundle-system always overwrites, so only the missing locales go in: a
|
|
220
|
+
// hand-edited system row of an already seeded locale must survive the new-locale boot.
|
|
221
|
+
bundleToImport = {
|
|
222
|
+
...bundle,
|
|
223
|
+
locales: Object.fromEntries(Object.entries(bundle.locales).filter(([locale]) => !(locale in existingIds))),
|
|
224
|
+
};
|
|
209
225
|
}
|
|
210
|
-
const result = await dispatcher.write(TemplateResolverHandlers.importBundleSystem, { bundle }, opts.by ?? createSystemUser(SYSTEM_TENANT_ID, ["SystemAdmin"]));
|
|
226
|
+
const result = await dispatcher.write(TemplateResolverHandlers.importBundleSystem, { bundle: bundleToImport }, opts.by ?? createSystemUser(SYSTEM_TENANT_ID, ["SystemAdmin"]));
|
|
211
227
|
if (!result.isSuccess) {
|
|
212
228
|
throw new Error(`${label} failed: ${result.error.code}: ${result.error.message}`);
|
|
213
229
|
}
|
|
214
|
-
return { ids: importedIds(result.data) };
|
|
230
|
+
return { ids: { ...existingIds, ...importedIds(result.data) } };
|
|
215
231
|
}
|
|
216
232
|
function importedIds(data) {
|
|
217
233
|
const ids = isPlainObject(data) ? data["ids"] : undefined;
|
|
@@ -279,11 +295,13 @@ export async function seedTextBlock(db, opts) {
|
|
|
279
295
|
status: "active",
|
|
280
296
|
};
|
|
281
297
|
const applyUpdate = async (row, current) => {
|
|
298
|
+
// No folder option keeps the row's folder, so a re-seed cannot drop it from the content tree.
|
|
299
|
+
const folder = opts.folder === undefined ? (current?.folder ?? null) : fields.folder;
|
|
282
300
|
// Skip no-op updates so a legal re-seed on every pod boot does not bump
|
|
283
301
|
// version/modifiedAt (ETag, "last changed", event-store growth).
|
|
284
302
|
if (current?.title === fields.title &&
|
|
285
303
|
current?.content === fields.content &&
|
|
286
|
-
current?.folder ===
|
|
304
|
+
current?.folder === folder) {
|
|
287
305
|
return { id: String(row.id) };
|
|
288
306
|
}
|
|
289
307
|
const result = await executor.update({
|
|
@@ -292,7 +310,7 @@ export async function seedTextBlock(db, opts) {
|
|
|
292
310
|
changes: {
|
|
293
311
|
title: fields.title,
|
|
294
312
|
content: fields.content,
|
|
295
|
-
folder
|
|
313
|
+
folder,
|
|
296
314
|
// A re-seed must not revive an archived block; only a served one gets a new served version.
|
|
297
315
|
...(current?.status === "active" && servedStampChanges(row)),
|
|
298
316
|
},
|
|
@@ -353,6 +371,7 @@ export async function seedLegalContentFromJson(db, blocks, opts = {}) {
|
|
|
353
371
|
locale: block.locale,
|
|
354
372
|
title: block.title,
|
|
355
373
|
content: block.content,
|
|
374
|
+
...(opts.folder !== undefined && { folder: opts.folder }),
|
|
356
375
|
ifExists: "update",
|
|
357
376
|
by,
|
|
358
377
|
});
|
|
@@ -42,6 +42,7 @@ export declare function prepareTemplateResourceBeforeTransaction(args: {
|
|
|
42
42
|
}): Promise<BeforeTransactionResult<PreparedTemplateImage>>;
|
|
43
43
|
export type Release = () => Promise<void>;
|
|
44
44
|
export declare function releaseAll(releases: readonly Release[]): Promise<void>;
|
|
45
|
+
export declare function releaseAfterFailure(ctx: HandlerContext, releases: readonly Release[]): Promise<void>;
|
|
45
46
|
export type PreparedLinkedResourceCopy = {
|
|
46
47
|
readonly key: string;
|
|
47
48
|
readonly entry: LinkedResourceEntry;
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { fetchOne, selectMany } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { NotFoundError, UnprocessableError, writeFailure, } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
3
|
import { createFileRefFromUploadedBytes, uploadFileBytesBeforeTransaction, } from "@cosmicdrift/kumiko-framework/files";
|
|
4
|
-
import { fileContextForTenant } from "../file-foundation/index.js";
|
|
4
|
+
import { fileContextForTenant, readFileRefContent } from "../file-foundation/index.js";
|
|
5
5
|
import { extensionForResourceContentType, requireForTenant } from "../shared/index.js";
|
|
6
6
|
import { DEFAULT_RESOURCE_MAX_BYTES, MAX_FILE_ATTACHMENTS_PER_TEMPLATE, RESOURCE_MAX_BYTES_CONFIG_KEY, } from "./handlers/resource-shared.js";
|
|
7
7
|
import { executor } from "./handlers/shared.js";
|
|
8
|
-
import { attachmentEntriesOf, parseLinkedResourceEntries, templatePdfAttachmentLinkSchema, } from "./linked-resources.js";
|
|
8
|
+
import { attachmentEntriesOf, isFileAttachmentEntry, parseLinkedResourceEntries, templatePdfAttachmentLinkSchema, } from "./linked-resources.js";
|
|
9
9
|
import { normalizeResourceImage } from "./resource-image.js";
|
|
10
10
|
import { templateResourcesTable } from "./table.js";
|
|
11
|
-
import { readTemplateResource } from "./template-resources.js";
|
|
12
11
|
import { ACTIVE_TEMPLATE_STATUS, SYSTEM_TENANT_ID } from "./web/constants.js";
|
|
13
12
|
import { TemplateResolverErrors } from "./web/qualified-names.js";
|
|
14
13
|
export async function readTemplateResourceMaxBytes(config, registry) {
|
|
@@ -24,6 +23,9 @@ function keyInUse() {
|
|
|
24
23
|
details: { code: "key_in_use" },
|
|
25
24
|
}));
|
|
26
25
|
}
|
|
26
|
+
function isImageKey(links, key) {
|
|
27
|
+
return typeof links[key] === "string";
|
|
28
|
+
}
|
|
27
29
|
function isAttachmentKey(links, key) {
|
|
28
30
|
const occupant = links[key];
|
|
29
31
|
return occupant !== undefined && typeof occupant !== "string";
|
|
@@ -129,6 +131,49 @@ export async function releaseAll(releases) {
|
|
|
129
131
|
if (rejected?.status === "rejected")
|
|
130
132
|
throw rejected.reason;
|
|
131
133
|
}
|
|
134
|
+
// Failure paths: a failing release must not replace the upload error the caller needs to see.
|
|
135
|
+
export async function releaseAfterFailure(ctx, releases) {
|
|
136
|
+
try {
|
|
137
|
+
await releaseAll(releases);
|
|
138
|
+
}
|
|
139
|
+
catch (releaseError) {
|
|
140
|
+
ctx.log?.warn("template upload rollback: releasing stored files failed", {
|
|
141
|
+
error: releaseError instanceof Error ? releaseError.message : String(releaseError),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// The source row's own fileName is personal data and may be ciphertext, so the copy is named from
|
|
146
|
+
// the link entry or the key.
|
|
147
|
+
function copiedFileName(key, entry, contentType) {
|
|
148
|
+
if (typeof entry !== "string" && isFileAttachmentEntry(entry))
|
|
149
|
+
return entry.fileName;
|
|
150
|
+
return `${key}.${extensionForResourceContentType(contentType) ?? "bin"}`;
|
|
151
|
+
}
|
|
152
|
+
function sourceUnreadable() {
|
|
153
|
+
return writeFailure(new UnprocessableError(TemplateResolverErrors.resourceSourceUnreadable, {
|
|
154
|
+
i18nKey: "templateResolver.errors.resource.sourceUnreadable",
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
// Undefined: the fileRef row exists but its bytes cannot be read, which must not look like a
|
|
158
|
+
// deleted file. The provider is chosen by the tenant of the fileRef, not by the caller's.
|
|
159
|
+
async function readCopySource(ctx, fileRefId) {
|
|
160
|
+
try {
|
|
161
|
+
const found = await readFileRefContent({
|
|
162
|
+
db: ctx.db,
|
|
163
|
+
filesForTenant: (tid) => fileContextForTenant(ctx, ctx.user.tenantId, tid),
|
|
164
|
+
fileRefId,
|
|
165
|
+
allowedTenantIds: [SYSTEM_TENANT_ID, ctx.user.tenantId],
|
|
166
|
+
});
|
|
167
|
+
return found === null ? null : { contentType: found.contentType, bytes: found.content };
|
|
168
|
+
}
|
|
169
|
+
catch (cause) {
|
|
170
|
+
ctx.log?.warn("template copy: source file could not be read", {
|
|
171
|
+
fileRefId,
|
|
172
|
+
errorClass: cause instanceof Error ? cause.name : typeof cause,
|
|
173
|
+
});
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
132
177
|
// beforeTransaction half: copies the file behind every entry into the target tenant's storage, so a
|
|
133
178
|
// template copied from the system default keeps working once the tenant exports it or measures its
|
|
134
179
|
// attachments. An entry whose source file is gone is left out: it could not be resolved at render
|
|
@@ -143,12 +188,21 @@ export async function prepareLinkedResourceCopiesBeforeTransaction(args) {
|
|
|
143
188
|
copies.push({ key, entry });
|
|
144
189
|
continue;
|
|
145
190
|
}
|
|
146
|
-
const
|
|
147
|
-
if (
|
|
191
|
+
const source = await readCopySource(ctx, typeof entry === "string" ? entry : entry.fileRefId);
|
|
192
|
+
if (source === undefined) {
|
|
193
|
+
await releaseAfterFailure(ctx, releases);
|
|
194
|
+
return sourceUnreadable();
|
|
195
|
+
}
|
|
196
|
+
// skip: the source fileRef is deleted or unknown, so there is nothing to copy
|
|
197
|
+
if (source === null)
|
|
148
198
|
continue;
|
|
149
|
-
const uploaded = await uploadToTenant({ ctx, user, tenantId }, {
|
|
199
|
+
const uploaded = await uploadToTenant({ ctx, user, tenantId }, {
|
|
200
|
+
fileName: copiedFileName(key, entry, source.contentType),
|
|
201
|
+
mimeType: source.contentType,
|
|
202
|
+
data: source.bytes,
|
|
203
|
+
});
|
|
150
204
|
if (!uploaded.isSuccess) {
|
|
151
|
-
await
|
|
205
|
+
await releaseAfterFailure(ctx, releases);
|
|
152
206
|
return uploaded;
|
|
153
207
|
}
|
|
154
208
|
if (uploaded.release)
|
|
@@ -157,7 +211,7 @@ export async function prepareLinkedResourceCopiesBeforeTransaction(args) {
|
|
|
157
211
|
}
|
|
158
212
|
}
|
|
159
213
|
catch (cause) {
|
|
160
|
-
await
|
|
214
|
+
await releaseAfterFailure(ctx, releases);
|
|
161
215
|
throw cause;
|
|
162
216
|
}
|
|
163
217
|
return { isSuccess: true, data: copies, release: () => releaseAll(releases) };
|
|
@@ -275,8 +329,7 @@ async function checkAttachmentTarget(args) {
|
|
|
275
329
|
if (otherAttachmentCount(linked, key) >= MAX_FILE_ATTACHMENTS_PER_TEMPLATE) {
|
|
276
330
|
return attachmentLimitFailure();
|
|
277
331
|
}
|
|
278
|
-
|
|
279
|
-
if (occupant !== undefined && typeof occupant === "string")
|
|
332
|
+
if (isImageKey(linked, key))
|
|
280
333
|
return keyInUse();
|
|
281
334
|
return { isSuccess: true, data: { row, linked } };
|
|
282
335
|
}
|
|
@@ -387,6 +440,8 @@ export async function linkTemplatePdfAttachment(args) {
|
|
|
387
440
|
if (row.kind !== "mail")
|
|
388
441
|
return attachmentNeedsMailTemplate();
|
|
389
442
|
const linked = parseLinkedResourceEntries(row.linkedResources);
|
|
443
|
+
if (isImageKey(linked, key))
|
|
444
|
+
return keyInUse();
|
|
390
445
|
if (otherAttachmentCount(linked, key) >= MAX_FILE_ATTACHMENTS_PER_TEMPLATE) {
|
|
391
446
|
return attachmentLimitFailure();
|
|
392
447
|
}
|
|
@@ -13,10 +13,5 @@ export declare function isServedByCollectionOnly(policy: TemplateDeliveryPolicy,
|
|
|
13
13
|
export declare function isRowServed(policy: TemplateDeliveryPolicy, row: DeliveryTarget & {
|
|
14
14
|
readonly tenantId: string;
|
|
15
15
|
}): boolean;
|
|
16
|
-
export declare function isRowServedTo(policy: TemplateDeliveryPolicy, user: {
|
|
17
|
-
readonly roles: readonly string[];
|
|
18
|
-
}, row: DeliveryTarget & {
|
|
19
|
-
readonly tenantId: string;
|
|
20
|
-
}): boolean;
|
|
21
16
|
export declare function deliveryPolicyOf(registry: Registry): TemplateDeliveryPolicy;
|
|
22
17
|
export {};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { isPlainObject, propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
|
-
import { isSystemAdmin } from "./template-draft.js";
|
|
3
2
|
import { SYSTEM_TENANT_ID } from "./web/constants.js";
|
|
4
3
|
import { TEMPLATE_RESOLVER_FEATURE } from "./web/qualified-names.js";
|
|
5
4
|
const NOTHING_UNLOCKED = { tenantEditable: [], collectionKinds: [] };
|
|
@@ -16,11 +15,6 @@ export function isServedByCollectionOnly(policy, target) {
|
|
|
16
15
|
export function isRowServed(policy, row) {
|
|
17
16
|
return row.tenantId === SYSTEM_TENANT_ID || isTenantOverrideServed(policy, row);
|
|
18
17
|
}
|
|
19
|
-
// A SystemAdmin sees every stored row, so a locked override can still be reset; everyone else
|
|
20
|
-
// sees only what is delivered, which keeps a locked override out of the template-resolver queries for a TenantAdmin (the audit log keeps the history).
|
|
21
|
-
export function isRowServedTo(policy, user, row) {
|
|
22
|
-
return isSystemAdmin(user) || isRowServed(policy, row);
|
|
23
|
-
}
|
|
24
18
|
function isDeliveryTarget(value) {
|
|
25
19
|
return (typeof propertyOf(value, "kind") === "string" && typeof propertyOf(value, "slug") === "string");
|
|
26
20
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
import { HEX_COLOR_PATTERN, PDF_ORIENTATIONS, PDF_PAGE_FORMATS, } from "./web/constants.js";
|
|
3
3
|
import { MAIL_TONES } from "./web/mail-constants.js";
|
|
4
|
+
import { MAIL_LAYOUT_ACCENT_LINES, MAIL_LAYOUT_BUTTON_STYLES, MAIL_LAYOUT_FONTS, MAIL_LAYOUT_WIDTHS, } from "./web/mail-layout-constants.js";
|
|
4
5
|
// One or more kebab segments joined by ":" — notification templates are seeded
|
|
5
6
|
// under their qualified name `feature:notify:name`.
|
|
6
7
|
export const templateSlugSchema = z
|
|
@@ -66,12 +67,14 @@ const pdfMetaSchema = z
|
|
|
66
67
|
.optional(),
|
|
67
68
|
})
|
|
68
69
|
.strict();
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
const MAIL_LAYOUT_WIDTH_LITERALS = [
|
|
71
|
+
z.literal(MAIL_LAYOUT_WIDTHS[0]),
|
|
72
|
+
z.literal(MAIL_LAYOUT_WIDTHS[1]),
|
|
73
|
+
z.literal(MAIL_LAYOUT_WIDTHS[2]),
|
|
74
|
+
];
|
|
72
75
|
const mailLayoutMetaSchema = z
|
|
73
76
|
.object({
|
|
74
|
-
width: z.union(
|
|
77
|
+
width: z.union(MAIL_LAYOUT_WIDTH_LITERALS).optional(),
|
|
75
78
|
font: z.enum(MAIL_LAYOUT_FONTS).optional(),
|
|
76
79
|
outerBackground: z.string().regex(HEX_COLOR_PATTERN, "must be a #rrggbb color").optional(),
|
|
77
80
|
accentLine: z.enum(MAIL_LAYOUT_ACCENT_LINES).optional(),
|
|
@@ -8,7 +8,7 @@ import * as z from "zod";
|
|
|
8
8
|
import { createEmailAttachmentReader, createTemplateMailRenderer } from "../delivery/index.js";
|
|
9
9
|
import { fileContextForTenant } from "../file-foundation/index.js";
|
|
10
10
|
import { collectRendererPlugins, createRendererFoundationApi, RendererError, } from "../renderer-foundation/index.js";
|
|
11
|
-
import { createTemplateResolverApi, TemplateNotFoundError } from "./api.js";
|
|
11
|
+
import { createTemplateResolverApi, isRowDeliveredTo, TemplateNotFoundError, } from "./api.js";
|
|
12
12
|
import { collectDocumentResourceKeys } from "./compile-document-body.js";
|
|
13
13
|
import { parseStoredJson } from "./handlers/template-gates.js";
|
|
14
14
|
import { validateTemplateDraft } from "./handlers/validate.query.js";
|
|
@@ -16,7 +16,7 @@ import { readLayoutRegions, sanitizeLayoutHtml, } from "./layout-regions.js";
|
|
|
16
16
|
import { parseLinkedResourceEntries } from "./linked-resources.js";
|
|
17
17
|
import { buildDisplayHtml } from "./preview-display-html.js";
|
|
18
18
|
import { templateResourcesTable } from "./table.js";
|
|
19
|
-
import { deliveryPolicyOf
|
|
19
|
+
import { deliveryPolicyOf } from "./template-delivery-policy.js";
|
|
20
20
|
import { isMailTemplateMeta, parseTemplateMeta, readPdfTemplateMeta, } from "./template-meta.js";
|
|
21
21
|
import { DEFAULT_MAIL_LAYOUT_SLUG, DEFAULT_PDF_LAYOUT_SLUG, MAIL_PREVIEW_MAX_BYTES, SYSTEM_TENANT_ID, } from "./web/constants.js";
|
|
22
22
|
import { TemplateResolverErrors } from "./web/qualified-names.js";
|
|
@@ -149,7 +149,8 @@ export function estimateMailBytes(args) {
|
|
|
149
149
|
async function loadEditedRowResources(ctx, user, input) {
|
|
150
150
|
const rows = await selectMany(ctx.db, templateResourcesTable, { tenantId: user.tenantId, slug: input.slug, kind: "mail", locale: input.locale }, { limit: 1 });
|
|
151
151
|
const row = rows[0];
|
|
152
|
-
if (row !== undefined &&
|
|
152
|
+
if (row !== undefined &&
|
|
153
|
+
!(await isRowDeliveredTo(ctx.db, deliveryPolicyOf(ctx.registry), user, row))) {
|
|
153
154
|
return undefined;
|
|
154
155
|
}
|
|
155
156
|
return row === undefined ? undefined : parseLinkedResourceEntries(row.linkedResources);
|
|
@@ -10,7 +10,7 @@ export declare const UPSERT_KINDS: readonly ["mail", "document-pdf", "image-snap
|
|
|
10
10
|
export type UpsertKind = (typeof UPSERT_KINDS)[number];
|
|
11
11
|
export declare const TEXT_BLOCK_KIND: "text-block";
|
|
12
12
|
export declare const COLLECTION_REFUSED_KINDS: readonly ["mail", "mail-layout", "pdf-layout"];
|
|
13
|
-
export declare function isCollectionRefusedKind(kind:
|
|
13
|
+
export declare function isCollectionRefusedKind(kind: unknown): kind is (typeof COLLECTION_REFUSED_KINDS)[number];
|
|
14
14
|
export declare const CONTENT_FORMATS: readonly ["markdown", "mjml", "html", "plain"];
|
|
15
15
|
export type ContentFormat = (typeof CONTENT_FORMATS)[number];
|
|
16
16
|
export declare const TEMPLATE_SCOPES: readonly ["system", "tenant"];
|
|
@@ -18,8 +18,8 @@ export type TemplateScope = (typeof TEMPLATE_SCOPES)[number];
|
|
|
18
18
|
export declare const TEMPLATE_STATUSES: readonly ["draft", "active", "archived"];
|
|
19
19
|
export type TemplateStatus = (typeof TEMPLATE_STATUSES)[number];
|
|
20
20
|
export declare const ACTIVE_TEMPLATE_STATUS = "active";
|
|
21
|
+
export { HEX_COLOR_PATTERN } from "./mail-layout-constants.ts";
|
|
21
22
|
export { PDF_DEFAULT_MARGIN_MM, PDF_FOOTER_MARGIN_MM, PDF_HEADER_MARGIN_MM, PDF_ORIENTATIONS, PDF_PAGE_FORMATS, type PdfOrientation, type PdfPageFormat, } from "./pdf-constants.ts";
|
|
22
|
-
export declare const HEX_COLOR_PATTERN: RegExp;
|
|
23
23
|
export declare const DEFAULT_MAIL_LAYOUT_SLUG = "default";
|
|
24
24
|
export declare const DEFAULT_PDF_LAYOUT_SLUG = "default";
|
|
25
25
|
export declare const MAIL_PREVIEW_MAX_BYTES: number;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// RenderKind identifies the consumer class of a template. Plugin renderers in
|
|
5
5
|
// `renderer-foundation` match on kind; the resolver here is kind-agnostic: it
|
|
6
6
|
// only loads, and the content format (markdown/mjml/html) decides who renders it.
|
|
7
|
+
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
7
8
|
export const RENDER_KINDS = ["mail", "document-pdf", "image-snapshot"];
|
|
8
9
|
// Kinds whose html body is a rich document: the server rebuilds it through the
|
|
9
10
|
// document schema on every write (see document-sanitizer.ts).
|
|
@@ -44,15 +45,14 @@ export const COLLECTION_REFUSED_KINDS = [
|
|
|
44
45
|
"pdf-layout",
|
|
45
46
|
];
|
|
46
47
|
export function isCollectionRefusedKind(kind) {
|
|
47
|
-
return COLLECTION_REFUSED_KINDS
|
|
48
|
+
return isOneOf(COLLECTION_REFUSED_KINDS, kind);
|
|
48
49
|
}
|
|
49
50
|
export const CONTENT_FORMATS = ["markdown", "mjml", "html", "plain"];
|
|
50
51
|
export const TEMPLATE_SCOPES = ["system", "tenant"];
|
|
51
52
|
export const TEMPLATE_STATUSES = ["draft", "active", "archived"];
|
|
52
53
|
export const ACTIVE_TEMPLATE_STATUS = "active";
|
|
54
|
+
export { HEX_COLOR_PATTERN } from "./mail-layout-constants.js";
|
|
53
55
|
export { PDF_DEFAULT_MARGIN_MM, PDF_FOOTER_MARGIN_MM, PDF_HEADER_MARGIN_MM, PDF_ORIENTATIONS, PDF_PAGE_FORMATS, } from "./pdf-constants.js";
|
|
54
|
-
// Colors end up inside MJML attributes, so only a strict hex is ever written there.
|
|
55
|
-
export const HEX_COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
|
|
56
56
|
export const DEFAULT_MAIL_LAYOUT_SLUG = "default";
|
|
57
57
|
export const DEFAULT_PDF_LAYOUT_SLUG = "default";
|
|
58
58
|
// The mail transport enforces no size limit of its own; 10 MiB is a common provider ceiling.
|
|
@@ -72,6 +72,8 @@ export const defaultTranslations = {
|
|
|
72
72
|
"templateResolver.errors.patch.blockRemoved": "The block {blockId} was already removed by an earlier step.",
|
|
73
73
|
"templateResolver.errors.patch.textNotFound": "The text {find} was not found in the block.",
|
|
74
74
|
"templateResolver.errors.patch.textAmbiguous": "The text {find} occurs {count} times in the block; use a longer text.",
|
|
75
|
+
"templateResolver.errors.patch.textSpansFormatting": "The text {find} crosses a formatting change in the block; replace each formatted part on its own.",
|
|
76
|
+
"templateResolver.errors.patch.placeholderNotReplaceable": "The placeholder {find} cannot be replaced as text; set the text around it with another operation or remove the block.",
|
|
75
77
|
"templateResolver.errors.patch.blockNotAllowed": "The block type {type} is not available in {kind} templates.",
|
|
76
78
|
"templateResolver.errors.patch.blockTypeMismatch": "This step needs a {expected} block, but the block is a {actual}.",
|
|
77
79
|
"templateResolver.errors.patch.invalidResourceKey": "The image key {resource} is not valid.",
|
|
@@ -86,6 +88,7 @@ export const defaultTranslations = {
|
|
|
86
88
|
"templateResolver.errors.testMail.noAddress": "Your account has no email address to send the test mail to.",
|
|
87
89
|
"templateResolver.errors.testMail.failed": "The test mail could not be sent.",
|
|
88
90
|
"templateResolver.errors.resource.tooLarge": "The image is too large.",
|
|
91
|
+
"templateResolver.errors.resource.sourceUnreadable": "A file of the source template could not be read, so nothing was copied.",
|
|
89
92
|
"templateResolver.errors.resource.unsupportedImage": "This file is not a supported image. Use PNG, JPEG, WebP or GIF.",
|
|
90
93
|
"templateResolver.errors.template.systemResourcesNeedSystemTenant": "Resource files of system templates can only be imported or exported by a user of the system tenant.",
|
|
91
94
|
"templateResolver.errors.template.bundleLocalesDiffer": "The locales of this template differ in content format or variables, so they cannot be exported as one bundle.",
|
|
@@ -2,6 +2,7 @@ export { textBlocksClient } from "./client-plugin.tsx";
|
|
|
2
2
|
export { DocumentContentEditor } from "./document-editor.tsx";
|
|
3
3
|
export { type DocumentEditorChrome, type DocumentEditorHandle, type DocumentEditorProps, type DocumentResourcePreview, type DocumentToolbarBlock, MAIL_DOCUMENT_BLOCKS, PDF_DOCUMENT_BLOCKS, } from "./document-toolbar-blocks.ts";
|
|
4
4
|
export { MAIL_TONES, type MailTone } from "./mail-constants.ts";
|
|
5
|
+
export { HEX_COLOR_PATTERN, MAIL_LAYOUT_ACCENT_LINES, MAIL_LAYOUT_BUTTON_STYLES, MAIL_LAYOUT_FONTS, MAIL_LAYOUT_WIDTHS, } from "./mail-layout-constants.ts";
|
|
5
6
|
export { PDF_DEFAULT_MARGIN_MM, PDF_FOOTER_MARGIN_MM, PDF_HEADER_MARGIN_MM, PDF_ORIENTATIONS, PDF_PAGE_FORMATS, } from "./pdf-constants.ts";
|
|
6
7
|
export { PlaceholderLineEditor, type PlaceholderLineEditorProps, } from "./placeholder-line-editor-lazy.tsx";
|
|
7
8
|
export { TemplateResolverErrors, TemplateResolverHandlers, TemplateResolverQueries, } from "./qualified-names.ts";
|
|
@@ -7,6 +7,7 @@ export { textBlocksClient } from "./client-plugin.js";
|
|
|
7
7
|
export { DocumentContentEditor } from "./document-editor.js";
|
|
8
8
|
export { MAIL_DOCUMENT_BLOCKS, PDF_DOCUMENT_BLOCKS, } from "./document-toolbar-blocks.js";
|
|
9
9
|
export { MAIL_TONES } from "./mail-constants.js";
|
|
10
|
+
export { HEX_COLOR_PATTERN, MAIL_LAYOUT_ACCENT_LINES, MAIL_LAYOUT_BUTTON_STYLES, MAIL_LAYOUT_FONTS, MAIL_LAYOUT_WIDTHS, } from "./mail-layout-constants.js";
|
|
10
11
|
export { PDF_DEFAULT_MARGIN_MM, PDF_FOOTER_MARGIN_MM, PDF_HEADER_MARGIN_MM, PDF_ORIENTATIONS, PDF_PAGE_FORMATS, } from "./pdf-constants.js";
|
|
11
12
|
export { PlaceholderLineEditor, } from "./placeholder-line-editor-lazy.js";
|
|
12
13
|
export { TemplateResolverErrors, TemplateResolverHandlers, TemplateResolverQueries, } from "./qualified-names.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const MAIL_LAYOUT_WIDTHS: readonly [560, 600, 640];
|
|
2
|
+
export declare const MAIL_LAYOUT_FONTS: readonly ["sans", "serif"];
|
|
3
|
+
export declare const MAIL_LAYOUT_ACCENT_LINES: readonly ["branding", "none"];
|
|
4
|
+
export declare const MAIL_LAYOUT_BUTTON_STYLES: readonly ["filled-rounded", "filled-square", "outline"];
|
|
5
|
+
export declare const HEX_COLOR_PATTERN: RegExp;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// No imports: web/ re-exports these, and template-meta.ts pulls in zod and the framework engine.
|
|
2
|
+
export const MAIL_LAYOUT_WIDTHS = [560, 600, 640];
|
|
3
|
+
export const MAIL_LAYOUT_FONTS = ["sans", "serif"];
|
|
4
|
+
export const MAIL_LAYOUT_ACCENT_LINES = ["branding", "none"];
|
|
5
|
+
export const MAIL_LAYOUT_BUTTON_STYLES = ["filled-rounded", "filled-square", "outline"];
|
|
6
|
+
// Colors end up inside MJML attributes, so only a strict hex is ever written there.
|
|
7
|
+
export const HEX_COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
|
|
@@ -53,6 +53,7 @@ export declare const TemplateResolverErrors: {
|
|
|
53
53
|
readonly bundleLocalesDiffer: "bundle_locales_differ";
|
|
54
54
|
readonly resourceTooLarge: "template_resource_too_large";
|
|
55
55
|
readonly resourceUnsupportedImage: "template_resource_unsupported_image";
|
|
56
|
+
readonly resourceSourceUnreadable: "template_resource_source_unreadable";
|
|
56
57
|
readonly attachmentUnsupportedType: "template_attachment_unsupported_type";
|
|
57
58
|
readonly attachmentLimit: "template_attachment_limit";
|
|
58
59
|
readonly attachmentInvalid: "template_attachment_invalid";
|
|
@@ -63,6 +63,7 @@ export const TemplateResolverErrors = {
|
|
|
63
63
|
bundleLocalesDiffer: "bundle_locales_differ",
|
|
64
64
|
resourceTooLarge: "template_resource_too_large",
|
|
65
65
|
resourceUnsupportedImage: "template_resource_unsupported_image",
|
|
66
|
+
resourceSourceUnreadable: "template_resource_source_unreadable",
|
|
66
67
|
attachmentUnsupportedType: "template_attachment_unsupported_type",
|
|
67
68
|
attachmentLimit: "template_attachment_limit",
|
|
68
69
|
attachmentInvalid: "template_attachment_invalid",
|
package/dist/tenant/feature.js
CHANGED
|
@@ -31,6 +31,7 @@ export function collectAssignableAppRoles(features) {
|
|
|
31
31
|
// --- Feature ---
|
|
32
32
|
export function createTenantFeature(options) {
|
|
33
33
|
return defineFeature("tenant", (r) => {
|
|
34
|
+
r.adoptsBundledLists();
|
|
34
35
|
r.describe("Registers the three core multi-tenancy entities \u2014 `tenant`, `tenant-membership`, and `tenant-invitation` (DB tables `read_tenants`, `read_tenant_memberships`, and `read_tenant_invitations`) \u2014 along with write handlers for create/update/disable/enable/addMember/removeMember/updateMemberRoles and the matching queries. It also declares a set of per-tenant config keys (companyName, timezone, locale, SMTP credentials) and system-only keys (priceModel, maxUsers) via `r.config({ keys: { ... } })`. Use this feature in every multi-tenant app; membership resolution and invitation flows depend on it, and `auth-email-password` requires it.");
|
|
35
36
|
r.uiHints({
|
|
36
37
|
displayLabel: "Multi-Tenant Core",
|
|
@@ -2,6 +2,7 @@ import * as z from "zod";
|
|
|
2
2
|
export declare const memberDirectoryQuery: import("@cosmicdrift/kumiko-framework/engine").PagedQueryHandlerDefinition<"memberDirectory", z.ZodObject<{
|
|
3
3
|
limit: z.ZodDefault<z.ZodNumber>;
|
|
4
4
|
search: z.ZodOptional<z.ZodString>;
|
|
5
|
+
ids: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
5
6
|
}, z.core.$strip>, {
|
|
6
7
|
id: string;
|
|
7
8
|
label: string;
|