@cosmicdrift/kumiko-samples 0.387.0 → 0.388.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 +6 -0
- package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +9 -9
- package/packages/bundled-features/src/audit/changes.json +12 -0
- package/packages/bundled-features/src/audit/handlers/audit-events.ts +1 -1
- package/packages/bundled-features/src/auth-mfa/changes.json +6 -0
- 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/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 +5 -1
- package/packages/bundled-features/src/cap-counter/changes.json +18 -0
- package/packages/bundled-features/src/cap-counter/enforce-cap.ts +2 -1
- package/packages/bundled-features/src/cap-counter/stock-cap-guard.ts +8 -1
- 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 +12 -0
- package/packages/bundled-features/src/config/handlers/cascade.query.ts +4 -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 +6 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +10 -9
- package/packages/bundled-features/src/delivery/changes.json +30 -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/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/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/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 +6 -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/inbound-mail-foundation/changes.json +18 -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 +14 -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/legal-pages/README.md +1 -1
- 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/personal-access-tokens/feature.ts +4 -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/sessions/feature.ts +7 -2
- 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/step-dispatcher/changes.json +6 -0
- package/packages/bundled-features/src/step-dispatcher/feature.ts +1 -0
- 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/template-resolver/api.ts +31 -4
- package/packages/bundled-features/src/template-resolver/changes.json +48 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +1 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +2 -2
- 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/list.query.ts +18 -19
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +15 -5
- 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 +32 -14
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +22 -4
- 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/index.ts +7 -0
- package/packages/bundled-features/src/template-resolver/web/mail-layout-constants.ts +8 -0
- package/packages/bundled-features/src/tenant/changes.json +6 -0
- package/packages/bundled-features/src/tenant/handlers/member-directory.query.ts +9 -3
- package/packages/bundled-features/src/tenant/handlers/tenant-directory.query.ts +7 -4
- package/packages/bundled-features/src/tenant-lifecycle/changes.json +6 -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/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-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/waitlist/changes.json +18 -0
- package/packages/bundled-features/src/waitlist/handlers/export-purge.write.ts +2 -1
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +5 -5
- package/packages/bundled-features/src/waitlist/import.ts +5 -0
- package/packages/bundled-features/src/waitlist/legacy-migrate.ts +22 -3
- package/packages/bundled-features/src/waitlist/link-on-signup.ts +20 -10
- package/packages/bundled-features/src/waitlist/options.ts +3 -0
- 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/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 +120 -3
- 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/legal-pages/README.md +1 -1
- package/samples/recipes/rate-limiting/README.md +18 -0
- package/samples/recipes/rate-limiting/src/feature.ts +12 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/stored-user-email.ts +0 -12
|
@@ -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
|
});
|
|
@@ -155,6 +155,19 @@ function effectiveRowsOf(
|
|
|
155
155
|
: null;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
export function isRowVisibleToCaller(
|
|
159
|
+
policy: TemplateEditPolicy,
|
|
160
|
+
user: Parameters<typeof isHiddenSystemDraft>[0],
|
|
161
|
+
registry: Parameters<typeof isSystemOnlyTemplateHiddenFrom>[1],
|
|
162
|
+
row: TemplateResourceRow,
|
|
163
|
+
): boolean {
|
|
164
|
+
return (
|
|
165
|
+
!isHiddenSystemDraft(user, row) &&
|
|
166
|
+
!isSystemOnlyTemplateHiddenFrom(user, registry, row) &&
|
|
167
|
+
mayEditTemplate(policy, user, row)
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
158
171
|
export function effectiveMailLayoutSlugs(
|
|
159
172
|
rows: readonly TemplateResourceRow[],
|
|
160
173
|
policy: TemplateDeliveryPolicy,
|
|
@@ -385,11 +398,8 @@ export function makeOverviewQuery(policy: TemplateEditPolicy) {
|
|
|
385
398
|
});
|
|
386
399
|
}
|
|
387
400
|
|
|
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),
|
|
401
|
+
const visibleRows = rows.filter((row) =>
|
|
402
|
+
isRowVisibleToCaller(policy, query.user, ctx.registry, row),
|
|
393
403
|
);
|
|
394
404
|
const deliveryPolicy = deliveryPolicyOf(ctx.registry);
|
|
395
405
|
const { drafts, layoutSlugByDocument } = buildDrafts(
|
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
|
];
|
|
@@ -85,10 +85,19 @@ export type SeedSystemTemplateOptions = {
|
|
|
85
85
|
readonly ifExists?: SeedIfExists;
|
|
86
86
|
};
|
|
87
87
|
|
|
88
|
+
// Namespace prefix plus path, no whitespace: keeps free text from passing as a key that the UI
|
|
89
|
+
// would then show raw.
|
|
90
|
+
const I18N_KEY_FORMAT = /^[^\s:]+:\S+$/;
|
|
91
|
+
|
|
88
92
|
function seedTitleColumn(opts: SeedSystemTemplateOptions, label: string): string | null {
|
|
89
93
|
if (opts.title !== undefined && opts.titleI18nKey !== undefined) {
|
|
90
94
|
throw new Error(`${label} rejected: set either title or titleI18nKey, not both`);
|
|
91
95
|
}
|
|
96
|
+
if (opts.titleI18nKey !== undefined && !I18N_KEY_FORMAT.test(opts.titleI18nKey)) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`${label} rejected: titleI18nKey "${opts.titleI18nKey}" is not a "namespace:key" i18n key`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
92
101
|
return opts.titleI18nKey ?? opts.title ?? null;
|
|
93
102
|
}
|
|
94
103
|
|
|
@@ -167,21 +176,20 @@ export async function seedSystemTemplate(
|
|
|
167
176
|
current?.title === rowFields.title &&
|
|
168
177
|
current?.variableSchema === rowFields.variableSchema &&
|
|
169
178
|
current?.meta === rowFields.meta &&
|
|
170
|
-
current?.status === rowFields.status &&
|
|
171
179
|
current?.parentTemplateId === rowFields.parentTemplateId &&
|
|
172
180
|
(linkedResources === undefined || current?.linkedResources === linkedResources)
|
|
173
181
|
) {
|
|
174
182
|
return { id: String(row.id) };
|
|
175
183
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
);
|
|
184
|
+
// An archived system row is a SystemAdmin decision (reversible with publish-system) that a
|
|
185
|
+
// pod-boot re-seed must not undo; content still follows the code. The seed replaces the
|
|
186
|
+
// content, so an open draft on the old content is dropped with it.
|
|
187
|
+
const { status: _status, ...contentFields } = rowFields;
|
|
188
|
+
const changes =
|
|
189
|
+
current?.status === "archived"
|
|
190
|
+
? { ...contentFields, status: "archived" as const, draft: null }
|
|
191
|
+
: { ...rowFields, ...activeStampChanges(row), draft: null };
|
|
192
|
+
const result = await executor.update({ id: row.id, version: row.version, changes }, by, tdb);
|
|
185
193
|
if (!result.isSuccess) {
|
|
186
194
|
throw new Error(`seedSystemTemplate update failed: ${JSON.stringify(result)}`);
|
|
187
195
|
}
|
|
@@ -292,8 +300,10 @@ async function seedSystemTemplateBundleWithResources(
|
|
|
292
300
|
`${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`,
|
|
293
301
|
);
|
|
294
302
|
}
|
|
295
|
-
|
|
296
|
-
|
|
303
|
+
const skipExisting = (opts.ifExists ?? "skip") === "skip";
|
|
304
|
+
const existingIds: Record<string, string> = {};
|
|
305
|
+
let bundleToImport = bundle;
|
|
306
|
+
if (skipExisting) {
|
|
297
307
|
for (const locale of Object.keys(bundle.locales)) {
|
|
298
308
|
const row = await fetchOne<TemplateResourceRow>(db, templateResourcesTable, {
|
|
299
309
|
tenantId: SYSTEM_TENANT_ID,
|
|
@@ -306,16 +316,24 @@ async function seedSystemTemplateBundleWithResources(
|
|
|
306
316
|
if (Object.keys(existingIds).length === Object.keys(bundle.locales).length) {
|
|
307
317
|
return { ids: existingIds };
|
|
308
318
|
}
|
|
319
|
+
// import-bundle-system always overwrites, so only the missing locales go in: a
|
|
320
|
+
// hand-edited system row of an already seeded locale must survive the new-locale boot.
|
|
321
|
+
bundleToImport = {
|
|
322
|
+
...bundle,
|
|
323
|
+
locales: Object.fromEntries(
|
|
324
|
+
Object.entries(bundle.locales).filter(([locale]) => !(locale in existingIds)),
|
|
325
|
+
),
|
|
326
|
+
};
|
|
309
327
|
}
|
|
310
328
|
const result = await dispatcher.write(
|
|
311
329
|
TemplateResolverHandlers.importBundleSystem,
|
|
312
|
-
{ bundle },
|
|
330
|
+
{ bundle: bundleToImport },
|
|
313
331
|
opts.by ?? createSystemUser(SYSTEM_TENANT_ID, ["SystemAdmin"]),
|
|
314
332
|
);
|
|
315
333
|
if (!result.isSuccess) {
|
|
316
334
|
throw new Error(`${label} failed: ${result.error.code}: ${result.error.message}`);
|
|
317
335
|
}
|
|
318
|
-
return { ids: importedIds(result.data) };
|
|
336
|
+
return { ids: { ...existingIds, ...importedIds(result.data) } };
|
|
319
337
|
}
|
|
320
338
|
|
|
321
339
|
function importedIds(data: unknown): Record<string, string> {
|
|
@@ -87,6 +87,10 @@ function keyInUse(): WriteResult<never> {
|
|
|
87
87
|
);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
function isImageKey(links: Readonly<Record<string, LinkedResourceEntry>>, key: string) {
|
|
91
|
+
return typeof links[key] === "string";
|
|
92
|
+
}
|
|
93
|
+
|
|
90
94
|
function isAttachmentKey(links: Readonly<Record<string, LinkedResourceEntry>>, key: string) {
|
|
91
95
|
const occupant = links[key];
|
|
92
96
|
return occupant !== undefined && typeof occupant !== "string";
|
|
@@ -265,6 +269,20 @@ export async function releaseAll(releases: readonly Release[]): Promise<void> {
|
|
|
265
269
|
if (rejected?.status === "rejected") throw rejected.reason;
|
|
266
270
|
}
|
|
267
271
|
|
|
272
|
+
// Failure paths: a failing release must not replace the upload error the caller needs to see.
|
|
273
|
+
export async function releaseAfterFailure(
|
|
274
|
+
ctx: HandlerContext,
|
|
275
|
+
releases: readonly Release[],
|
|
276
|
+
): Promise<void> {
|
|
277
|
+
try {
|
|
278
|
+
await releaseAll(releases);
|
|
279
|
+
} catch (releaseError) {
|
|
280
|
+
ctx.log?.warn("template upload rollback: releasing stored files failed", {
|
|
281
|
+
error: releaseError instanceof Error ? releaseError.message : String(releaseError),
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
268
286
|
// `upload` is absent for a rendered-PDF link, which carries no file.
|
|
269
287
|
export type PreparedLinkedResourceCopy = {
|
|
270
288
|
readonly key: string;
|
|
@@ -301,14 +319,14 @@ export async function prepareLinkedResourceCopiesBeforeTransaction(args: {
|
|
|
301
319
|
{ fileName: file.fileName, mimeType: file.contentType, data: file.bytes },
|
|
302
320
|
);
|
|
303
321
|
if (!uploaded.isSuccess) {
|
|
304
|
-
await
|
|
322
|
+
await releaseAfterFailure(ctx, releases);
|
|
305
323
|
return uploaded;
|
|
306
324
|
}
|
|
307
325
|
if (uploaded.release) releases.push(uploaded.release);
|
|
308
326
|
copies.push({ key, entry, upload: uploaded.data });
|
|
309
327
|
}
|
|
310
328
|
} catch (cause) {
|
|
311
|
-
await
|
|
329
|
+
await releaseAfterFailure(ctx, releases);
|
|
312
330
|
throw cause;
|
|
313
331
|
}
|
|
314
332
|
return { isSuccess: true, data: copies, release: () => releaseAll(releases) };
|
|
@@ -481,8 +499,7 @@ async function checkAttachmentTarget(args: {
|
|
|
481
499
|
if (otherAttachmentCount(linked, key) >= MAX_FILE_ATTACHMENTS_PER_TEMPLATE) {
|
|
482
500
|
return attachmentLimitFailure();
|
|
483
501
|
}
|
|
484
|
-
|
|
485
|
-
if (occupant !== undefined && typeof occupant === "string") return keyInUse();
|
|
502
|
+
if (isImageKey(linked, key)) return keyInUse();
|
|
486
503
|
return { isSuccess: true, data: { row, linked } };
|
|
487
504
|
}
|
|
488
505
|
|
|
@@ -638,6 +655,7 @@ export async function linkTemplatePdfAttachment(args: {
|
|
|
638
655
|
if (row.kind !== "mail") return attachmentNeedsMailTemplate();
|
|
639
656
|
|
|
640
657
|
const linked = parseLinkedResourceEntries(row.linkedResources);
|
|
658
|
+
if (isImageKey(linked, key)) return keyInUse();
|
|
641
659
|
if (otherAttachmentCount(linked, key) >= MAX_FILE_ATTACHMENTS_PER_TEMPLATE) {
|
|
642
660
|
return attachmentLimitFailure();
|
|
643
661
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import { isPlainObject, propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
3
|
-
import { isSystemAdmin } from "./template-draft.ts";
|
|
4
3
|
import { SYSTEM_TENANT_ID } from "./web/constants.ts";
|
|
5
4
|
import { TEMPLATE_RESOLVER_FEATURE } from "./web/qualified-names.ts";
|
|
6
5
|
|
|
@@ -45,16 +44,6 @@ export function isRowServed(
|
|
|
45
44
|
return row.tenantId === SYSTEM_TENANT_ID || isTenantOverrideServed(policy, row);
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
// A SystemAdmin sees every stored row, so a locked override can still be reset; everyone else
|
|
49
|
-
// 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).
|
|
50
|
-
export function isRowServedTo(
|
|
51
|
-
policy: TemplateDeliveryPolicy,
|
|
52
|
-
user: { readonly roles: readonly string[] },
|
|
53
|
-
row: DeliveryTarget & { readonly tenantId: string },
|
|
54
|
-
): boolean {
|
|
55
|
-
return isSystemAdmin(user) || isRowServed(policy, row);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
47
|
function isDeliveryTarget(value: unknown): value is DeliveryTarget {
|
|
59
48
|
return (
|
|
60
49
|
typeof propertyOf(value, "kind") === "string" && typeof propertyOf(value, "slug") === "string"
|
|
@@ -6,6 +6,12 @@ import {
|
|
|
6
6
|
type TemplateKind,
|
|
7
7
|
} from "./web/constants.ts";
|
|
8
8
|
import { MAIL_TONES, type MailTone } from "./web/mail-constants.ts";
|
|
9
|
+
import {
|
|
10
|
+
MAIL_LAYOUT_ACCENT_LINES,
|
|
11
|
+
MAIL_LAYOUT_BUTTON_STYLES,
|
|
12
|
+
MAIL_LAYOUT_FONTS,
|
|
13
|
+
MAIL_LAYOUT_WIDTHS,
|
|
14
|
+
} from "./web/mail-layout-constants.ts";
|
|
9
15
|
|
|
10
16
|
// One or more kebab segments joined by ":" — notification templates are seeded
|
|
11
17
|
// under their qualified name `feature:notify:name`.
|
|
@@ -89,13 +95,15 @@ const pdfMetaSchema = z
|
|
|
89
95
|
})
|
|
90
96
|
.strict();
|
|
91
97
|
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
98
|
+
const MAIL_LAYOUT_WIDTH_LITERALS = [
|
|
99
|
+
z.literal(MAIL_LAYOUT_WIDTHS[0]),
|
|
100
|
+
z.literal(MAIL_LAYOUT_WIDTHS[1]),
|
|
101
|
+
z.literal(MAIL_LAYOUT_WIDTHS[2]),
|
|
102
|
+
] as const;
|
|
95
103
|
|
|
96
104
|
const mailLayoutMetaSchema = z
|
|
97
105
|
.object({
|
|
98
|
-
width: z.union(
|
|
106
|
+
width: z.union(MAIL_LAYOUT_WIDTH_LITERALS).optional(),
|
|
99
107
|
font: z.enum(MAIL_LAYOUT_FONTS).optional(),
|
|
100
108
|
outerBackground: z.string().regex(HEX_COLOR_PATTERN, "must be a #rrggbb color").optional(),
|
|
101
109
|
accentLine: z.enum(MAIL_LAYOUT_ACCENT_LINES).optional(),
|
|
@@ -28,7 +28,12 @@ import {
|
|
|
28
28
|
RendererError,
|
|
29
29
|
type RendererPlugin,
|
|
30
30
|
} from "../renderer-foundation/index.ts";
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
createTemplateResolverApi,
|
|
33
|
+
isRowDeliveredTo,
|
|
34
|
+
TemplateNotFoundError,
|
|
35
|
+
type TemplateResource,
|
|
36
|
+
} from "./api.ts";
|
|
32
37
|
import { collectDocumentResourceKeys } from "./compile-document-body.ts";
|
|
33
38
|
import { parseStoredJson } from "./handlers/template-gates.ts";
|
|
34
39
|
import { validateTemplateDraft } from "./handlers/validate.query.ts";
|
|
@@ -41,7 +46,7 @@ import {
|
|
|
41
46
|
import { type LinkedResourceEntry, parseLinkedResourceEntries } from "./linked-resources.ts";
|
|
42
47
|
import { buildDisplayHtml } from "./preview-display-html.ts";
|
|
43
48
|
import { type TemplateResourceRow, templateResourcesTable } from "./table.ts";
|
|
44
|
-
import { deliveryPolicyOf
|
|
49
|
+
import { deliveryPolicyOf } from "./template-delivery-policy.ts";
|
|
45
50
|
import {
|
|
46
51
|
isMailTemplateMeta,
|
|
47
52
|
type MailTemplateMeta,
|
|
@@ -327,7 +332,10 @@ async function loadEditedRowResources(
|
|
|
327
332
|
{ limit: 1 },
|
|
328
333
|
);
|
|
329
334
|
const row = rows[0];
|
|
330
|
-
if (
|
|
335
|
+
if (
|
|
336
|
+
row !== undefined &&
|
|
337
|
+
!(await isRowDeliveredTo(ctx.db, deliveryPolicyOf(ctx.registry), user, row))
|
|
338
|
+
) {
|
|
331
339
|
return undefined;
|
|
332
340
|
}
|
|
333
341
|
return row === undefined ? undefined : parseLinkedResourceEntries(row.linkedResources);
|
|
@@ -4,6 +4,8 @@
|
|
|
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";
|
|
8
|
+
|
|
7
9
|
export const RENDER_KINDS = ["mail", "document-pdf", "image-snapshot"] as const;
|
|
8
10
|
export type RenderKind = (typeof RENDER_KINDS)[number];
|
|
9
11
|
|
|
@@ -55,8 +57,10 @@ export const COLLECTION_REFUSED_KINDS = [
|
|
|
55
57
|
"pdf-layout",
|
|
56
58
|
] as const satisfies readonly TemplateKind[];
|
|
57
59
|
|
|
58
|
-
export function isCollectionRefusedKind(
|
|
59
|
-
|
|
60
|
+
export function isCollectionRefusedKind(
|
|
61
|
+
kind: unknown,
|
|
62
|
+
): kind is (typeof COLLECTION_REFUSED_KINDS)[number] {
|
|
63
|
+
return isOneOf(COLLECTION_REFUSED_KINDS, kind);
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
export const CONTENT_FORMATS = ["markdown", "mjml", "html", "plain"] as const;
|
|
@@ -69,6 +73,7 @@ export const TEMPLATE_STATUSES = ["draft", "active", "archived"] as const;
|
|
|
69
73
|
export type TemplateStatus = (typeof TEMPLATE_STATUSES)[number];
|
|
70
74
|
export const ACTIVE_TEMPLATE_STATUS = "active" satisfies TemplateStatus;
|
|
71
75
|
|
|
76
|
+
export { HEX_COLOR_PATTERN } from "./mail-layout-constants.ts";
|
|
72
77
|
export {
|
|
73
78
|
PDF_DEFAULT_MARGIN_MM,
|
|
74
79
|
PDF_FOOTER_MARGIN_MM,
|
|
@@ -79,9 +84,6 @@ export {
|
|
|
79
84
|
type PdfPageFormat,
|
|
80
85
|
} from "./pdf-constants.ts";
|
|
81
86
|
|
|
82
|
-
// Colors end up inside MJML attributes, so only a strict hex is ever written there.
|
|
83
|
-
export const HEX_COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
|
|
84
|
-
|
|
85
87
|
export const DEFAULT_MAIL_LAYOUT_SLUG = "default";
|
|
86
88
|
export const DEFAULT_PDF_LAYOUT_SLUG = "default";
|
|
87
89
|
|
|
@@ -16,6 +16,13 @@ export {
|
|
|
16
16
|
PDF_DOCUMENT_BLOCKS,
|
|
17
17
|
} from "./document-toolbar-blocks.ts";
|
|
18
18
|
export { MAIL_TONES, type MailTone } from "./mail-constants.ts";
|
|
19
|
+
export {
|
|
20
|
+
HEX_COLOR_PATTERN,
|
|
21
|
+
MAIL_LAYOUT_ACCENT_LINES,
|
|
22
|
+
MAIL_LAYOUT_BUTTON_STYLES,
|
|
23
|
+
MAIL_LAYOUT_FONTS,
|
|
24
|
+
MAIL_LAYOUT_WIDTHS,
|
|
25
|
+
} from "./mail-layout-constants.ts";
|
|
19
26
|
export {
|
|
20
27
|
PDF_DEFAULT_MARGIN_MM,
|
|
21
28
|
PDF_FOOTER_MARGIN_MM,
|
|
@@ -0,0 +1,8 @@
|
|
|
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] as const;
|
|
3
|
+
export const MAIL_LAYOUT_FONTS = ["sans", "serif"] as const;
|
|
4
|
+
export const MAIL_LAYOUT_ACCENT_LINES = ["branding", "none"] as const;
|
|
5
|
+
export const MAIL_LAYOUT_BUTTON_STYLES = ["filled-rounded", "filled-square", "outline"] as const;
|
|
6
|
+
|
|
7
|
+
// Colors end up inside MJML attributes, so only a strict hex is ever written there.
|
|
8
|
+
export const HEX_COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.388.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Tenant directory search contract documented",
|
|
6
|
+
"detail": "A search returns the first limit matches in tenant-key order, sorted by label afterwards; the handler description now says so."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.383.0",
|
|
4
10
|
"type": "improvement",
|