@cosmicdrift/kumiko-samples 0.364.0 → 0.365.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/LICENSE +1 -1
- package/package.json +1 -1
- package/packages/bundled-features/package.json +1 -1
- package/packages/bundled-features/src/audit/changes.json +12 -0
- package/packages/bundled-features/src/audit/feature.ts +1 -1
- package/packages/bundled-features/src/audit/run-escape-hatch-retention.ts +63 -11
- package/packages/bundled-features/src/auth-email-password/bootstrap.ts +1 -3
- package/packages/bundled-features/src/auth-email-password/changes.json +18 -0
- package/packages/bundled-features/src/auth-email-password/feature.ts +4 -7
- package/packages/bundled-features/src/auth-email-password/handlers/invite-accept-with-login.write.ts +2 -3
- package/packages/bundled-features/src/auth-email-password/handlers/invite-create.write.ts +6 -4
- package/packages/bundled-features/src/auth-email-password/handlers/switch-tenant-mfa-gate.write.ts +2 -2
- package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +7 -3
- package/packages/bundled-features/src/auth-email-password/mail-i18n.ts +10 -0
- package/packages/bundled-features/src/billing-foundation/changes.json +12 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +8 -1
- package/packages/bundled-features/src/billing-foundation/i18n.ts +6 -0
- package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +10 -2
- package/packages/bundled-features/src/cap-counter/book-cap-usage.ts +20 -8
- package/packages/bundled-features/src/cap-counter/cap-reservation.ts +7 -2
- package/packages/bundled-features/src/cap-counter/changes.json +13 -0
- package/packages/bundled-features/src/cap-counter/handlers/increment-rolling.write.ts +2 -0
- package/packages/bundled-features/src/cap-overview/changes.json +6 -0
- package/packages/bundled-features/src/cap-overview/handlers/tenant-caps-list.query.ts +2 -1
- package/packages/bundled-features/src/cap-overview/handlers/tenant-options.query.ts +2 -1
- package/packages/bundled-features/src/channel-discord/discord-channel.ts +4 -2
- package/packages/bundled-features/src/channel-email/changes.json +18 -0
- package/packages/bundled-features/src/channel-email/email-channel.ts +30 -20
- package/packages/bundled-features/src/channel-email/smtp-transport.ts +7 -1
- package/packages/bundled-features/src/channel-slack/slack-channel.ts +4 -2
- package/packages/bundled-features/src/channel-teams/teams-channel.ts +4 -2
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +7 -1
- package/packages/bundled-features/src/custom-fields/db/queries/retention.ts +2 -2
- package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +4 -5
- package/packages/bundled-features/src/delivery/attachment-ref.ts +18 -1
- package/packages/bundled-features/src/delivery/changes.json +12 -0
- package/packages/bundled-features/src/delivery/delivery-service.ts +93 -92
- package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
- package/packages/bundled-features/src/feature-toggles/changes.json +7 -0
- package/packages/bundled-features/src/feature-toggles/toggle-runtime.ts +26 -12
- package/packages/bundled-features/src/files-tenant-data/index.ts +3 -1
- package/packages/bundled-features/src/form-draft/db/queries/owned-file-refs.ts +12 -3
- package/packages/bundled-features/src/form-draft/handlers/discard.write.ts +1 -1
- package/packages/bundled-features/src/jobs/changes.json +12 -0
- package/packages/bundled-features/src/jobs/job-run-logger.ts +5 -2
- package/packages/bundled-features/src/metrics/changes.json +6 -0
- package/packages/bundled-features/src/metrics/filters.ts +28 -12
- package/packages/bundled-features/src/personal-access-tokens/changes.json +6 -0
- package/packages/bundled-features/src/personal-access-tokens/revoke-for-user.ts +7 -8
- package/packages/bundled-features/src/secrets/changes.json +6 -0
- package/packages/bundled-features/src/secrets/handlers/delete.write.ts +3 -1
- package/packages/bundled-features/src/secrets/write-gate.ts +8 -2
- package/packages/bundled-features/src/shared/index.ts +8 -0
- package/packages/bundled-features/src/shared/resource-image-formats.ts +31 -0
- package/packages/bundled-features/src/shared/sharp-input-limits.ts +7 -8
- package/packages/bundled-features/src/step-dispatcher/webhook-runner.ts +2 -2
- package/packages/bundled-features/src/template-resolver/changes.json +81 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +78 -12
- package/packages/bundled-features/src/template-resolver/compile-steps-block.ts +68 -0
- package/packages/bundled-features/src/template-resolver/constants.ts +11 -1
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +3 -0
- package/packages/bundled-features/src/template-resolver/document-schema.ts +7 -1
- package/packages/bundled-features/src/template-resolver/document-steps.ts +114 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +12 -0
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +81 -39
- package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +4 -3
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +111 -61
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +1 -0
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts +25 -15
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +12 -1
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +12 -2
- package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +48 -9
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +4 -1
- package/packages/bundled-features/src/template-resolver/index.ts +6 -1
- package/packages/bundled-features/src/template-resolver/layout-appearance.ts +25 -9
- package/packages/bundled-features/src/template-resolver/mail-constants.ts +4 -0
- package/packages/bundled-features/src/template-resolver/resource-image.ts +9 -16
- package/packages/bundled-features/src/template-resolver/seeding.ts +2 -0
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +390 -124
- package/packages/bundled-features/src/template-resolver/template-bundle.ts +4 -0
- package/packages/bundled-features/src/template-resolver/template-draft.ts +1 -1
- package/packages/bundled-features/src/template-resolver/template-meta.ts +8 -0
- package/packages/bundled-features/src/template-resolver/template-preview.ts +1 -1
- package/packages/bundled-features/src/template-resolver/template-resources.ts +2 -43
- package/packages/bundled-features/src/template-resolver/template-validation.ts +15 -9
- package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +34 -0
- package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +3 -2
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +56 -11
- package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +6 -3
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +7 -0
- package/packages/bundled-features/src/template-resolver/web/index.ts +1 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +14 -5
- package/packages/bundled-features/src/tenant/changes.json +6 -0
- package/packages/bundled-features/src/tenant/handlers/update-member-roles.write.ts +3 -8
- package/packages/bundled-features/src/tenant/seeding.ts +33 -27
- package/packages/bundled-features/src/tenant-handover/move-entity-graph.ts +14 -3
- package/packages/bundled-features/src/tenant-lifecycle/changes.json +6 -0
- package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +9 -1
- package/packages/bundled-features/src/tenant-lifecycle/stages.ts +3 -2
- package/packages/bundled-features/src/tier-engine/changes.json +12 -0
- package/packages/bundled-features/src/tier-engine/constants.ts +9 -0
- package/packages/bundled-features/src/tier-engine/entity.ts +9 -4
- package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +8 -5
- package/packages/bundled-features/src/tier-engine/index.ts +2 -0
- package/packages/bundled-features/src/user/constants.ts +1 -0
- package/packages/bundled-features/src/user/invited-global-roles.ts +2 -4
- package/packages/bundled-features/src/user-data-rights/changes.json +6 -0
- package/packages/bundled-features/src/user-data-rights/email-templates.ts +0 -1
- package/packages/bundled-features/src/user-data-rights/i18n.ts +5 -0
- package/packages/bundled-features/src/user-data-rights/lib/update-user-lifecycle.ts +13 -2
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +7 -1
- package/packages/bundled-features/src/waitlist/changes.json +18 -0
- package/packages/bundled-features/src/waitlist/feature.ts +1 -1
- package/packages/bundled-features/src/waitlist/handlers/invite.write.ts +35 -3
- package/packages/bundled-features/src/waitlist/handlers/reject.write.ts +2 -17
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +33 -9
- package/packages/bundled-features/src/waitlist/i18n.ts +9 -0
- package/packages/bundled-features/src/waitlist/lib.ts +20 -0
- package/packages/bundled-features/src/workflow-runner/feature.ts +1 -1
- package/samples/apps/ui-walkthrough/src/run-config.ts +14 -1
- package/samples/apps/use-all-bundled/README.md +3 -13
- package/samples/apps/use-all-bundled/feature-manifest.json +3 -3
- package/samples/apps/use-all-bundled/scripts/check-coverage.ts +34 -118
- package/samples/apps/use-all-bundled/src/app/notes-feature.ts +1 -1
- package/samples/apps/use-all-bundled/src/run-config.ts +0 -3
- package/samples/recipes/auth-mfa/README.md +2 -1
- package/samples/recipes/auth-mfa/src/feature.ts +1 -0
- package/samples/recipes/custom-handlers/README.md +4 -3
- package/samples/recipes/custom-handlers/src/feature.ts +21 -12
- package/samples/recipes/dashboard-stats/README.md +38 -0
- package/samples/recipes/dashboard-stats/package.json +8 -0
- package/samples/recipes/dashboard-stats/src/feature.ts +104 -0
- package/samples/recipes/legal-pages/README.md +2 -2
- package/samples/recipes/managed-pages/README.md +2 -2
- package/samples/recipes/pdf-audit-export/README.md +2 -1
- package/samples/recipes/pdf-audit-export/package.json +1 -1
- package/samples/recipes/pdf-audit-export/src/feature.ts +68 -48
- package/samples/recipes/workflow-engine/src/db/queries/suspended-runs.ts +2 -2
- package/samples/apps/use-all-bundled/schema/generate.ts +0 -617
- package/samples/apps/use-all-bundled/schema/migrations/meta/0000_snapshot.json +0 -3055
- package/samples/apps/use-all-bundled/schema/migrations/meta/_journal.json +0 -13
- package/samples/apps/use-all-bundled/schema/schema.generated.ts +0 -94
- package/samples/apps/use-all-bundled/schema/schema.ts +0 -9
|
@@ -2,7 +2,10 @@ import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
|
2
2
|
import {
|
|
3
3
|
defineWriteHandler,
|
|
4
4
|
type HandlerContext,
|
|
5
|
+
type SessionUser,
|
|
5
6
|
SYSTEM_TENANT_ID,
|
|
7
|
+
type TenantId,
|
|
8
|
+
type WriteResult,
|
|
6
9
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
7
10
|
import {
|
|
8
11
|
AccessDeniedError,
|
|
@@ -13,12 +16,16 @@ import {
|
|
|
13
16
|
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
14
17
|
import * as z from "zod";
|
|
15
18
|
import { requireForTenant } from "../../shared/index.js";
|
|
16
|
-
import { CONTENT_FORMATS } from "../constants.js";
|
|
17
|
-
import { parseLinkedResourceEntries } from "../linked-resources.js";
|
|
19
|
+
import { CONTENT_FORMATS, type ContentFormat } from "../constants.js";
|
|
20
|
+
import { type LinkedResourceEntry, parseLinkedResourceEntries } from "../linked-resources.js";
|
|
18
21
|
import { TemplateResolverErrors } from "../qualified-names.js";
|
|
22
|
+
import {
|
|
23
|
+
createLinkedResourceCopies,
|
|
24
|
+
prepareLinkedResourceCopiesBeforeTransaction,
|
|
25
|
+
} from "../store-template-resource.js";
|
|
19
26
|
import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
|
|
20
|
-
import { toWorkingRow } from "../template-draft.js";
|
|
21
|
-
import { attachTemplateResources
|
|
27
|
+
import { toWorkingRow, type WorkingRow } from "../template-draft.js";
|
|
28
|
+
import { attachTemplateResources } from "../template-resources.js";
|
|
22
29
|
import { resolveTemplateVariables } from "../template-validation.js";
|
|
23
30
|
import { targetExistsIssue } from "./bundle-import.js";
|
|
24
31
|
import { kindSchema, localeSchema, templateSlugSchema } from "./shared.js";
|
|
@@ -62,58 +69,109 @@ async function findCopySource(
|
|
|
62
69
|
});
|
|
63
70
|
}
|
|
64
71
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
scope: z.enum(["tenant", "system"]).default("tenant"),
|
|
75
|
-
}),
|
|
76
|
-
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
77
|
-
description:
|
|
78
|
-
"Copies the text of one locale of a template into another locale as a draft override of the caller's tenant, taking the tenant's own row or else the system default as the source; use it as the starting point for a translation. It refuses an existing target unless overwrite is set. With scope system (SystemAdmin only) both source and target are system-default rows and the target lands as a system draft; any other caller gets a 403.",
|
|
79
|
-
handler: async (event, ctx) => {
|
|
80
|
-
const { slug, kind, fromLocale, toLocale, overwrite, scope } = event.payload;
|
|
81
|
-
if (scope === "system" && !event.user.roles.includes("SystemAdmin")) {
|
|
82
|
-
return writeFailure(
|
|
83
|
-
new AccessDeniedError({
|
|
84
|
-
i18nKey: "errors.access.denied",
|
|
85
|
-
details: { reason: TemplateResolverErrors.systemAdminRequired },
|
|
86
|
-
}),
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
const tenantId = scope === "system" ? SYSTEM_TENANT_ID : event.user.tenantId;
|
|
90
|
-
const refused = refuseTenantScopeUnderSystemTenant(scope, tenantId);
|
|
91
|
-
if (refused !== null) return refused;
|
|
72
|
+
const copyLocaleSchema = z.object({
|
|
73
|
+
slug: templateSlugSchema,
|
|
74
|
+
kind: kindSchema,
|
|
75
|
+
fromLocale: localeSchema,
|
|
76
|
+
toLocale: localeSchema,
|
|
77
|
+
overwrite: z.boolean().default(false),
|
|
78
|
+
/** "system" copies between system-default rows; SystemAdmin only. */
|
|
79
|
+
scope: z.enum(["tenant", "system"]).default("tenant"),
|
|
80
|
+
});
|
|
92
81
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
82
|
+
type CopyRequest = {
|
|
83
|
+
readonly scope: CopyScope;
|
|
84
|
+
readonly tenantId: TenantId;
|
|
85
|
+
readonly source: WorkingRow;
|
|
86
|
+
readonly contentFormat: ContentFormat;
|
|
87
|
+
readonly sourceResources: Readonly<Record<string, LinkedResourceEntry>>;
|
|
88
|
+
// A system source's files belong to the system tenant: the tenant gets its own copies so
|
|
89
|
+
// previews, exports and the tenant's override keep working independent of the system row.
|
|
90
|
+
readonly sourceIsForeign: boolean;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Runs before the upload (reject early, store nothing) and again in the transaction on the fresh rows.
|
|
94
|
+
async function checkCopyRequest(
|
|
95
|
+
ctx: WriteCtx,
|
|
96
|
+
event: {
|
|
97
|
+
readonly user: SessionUser;
|
|
98
|
+
readonly payload: z.infer<typeof copyLocaleSchema>;
|
|
99
|
+
},
|
|
100
|
+
): Promise<WriteResult<CopyRequest>> {
|
|
101
|
+
const { slug, kind, fromLocale, toLocale, overwrite, scope } = event.payload;
|
|
102
|
+
if (scope === "system" && !event.user.roles.includes("SystemAdmin")) {
|
|
103
|
+
return writeFailure(
|
|
104
|
+
new AccessDeniedError({
|
|
105
|
+
i18nKey: "errors.access.denied",
|
|
106
|
+
details: { reason: TemplateResolverErrors.systemAdminRequired },
|
|
107
|
+
}),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const tenantId = scope === "system" ? SYSTEM_TENANT_ID : event.user.tenantId;
|
|
111
|
+
const refused = refuseTenantScopeUnderSystemTenant(scope, tenantId);
|
|
112
|
+
if (refused !== null) return refused;
|
|
113
|
+
|
|
114
|
+
const readDb = readDbFor(ctx, scope);
|
|
115
|
+
const storedSource = await findCopySource(ctx, {
|
|
116
|
+
scope,
|
|
117
|
+
tenantId,
|
|
118
|
+
slug,
|
|
119
|
+
kind,
|
|
120
|
+
locale: fromLocale,
|
|
121
|
+
});
|
|
122
|
+
const source = storedSource && toWorkingRow(storedSource, tenantId);
|
|
123
|
+
const contentFormat = source?.contentFormat;
|
|
124
|
+
if (!source || !isOneOf(CONTENT_FORMATS, contentFormat)) {
|
|
125
|
+
return writeFailure(new NotFoundError("template-resource", `${slug}/${fromLocale}`));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!overwrite) {
|
|
129
|
+
const target = await fetchOne<TemplateResourceRow>(readDb, templateResourcesTable, {
|
|
96
130
|
tenantId,
|
|
97
131
|
slug,
|
|
98
132
|
kind,
|
|
99
|
-
locale:
|
|
133
|
+
locale: toLocale,
|
|
100
134
|
});
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return writeFailure(new NotFoundError("template-resource", `${slug}/${fromLocale}`));
|
|
135
|
+
if (target) {
|
|
136
|
+
return writeFailure(new ValidationError({ fields: [targetExistsIssue(toLocale)] }));
|
|
104
137
|
}
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
isSuccess: true,
|
|
141
|
+
data: {
|
|
142
|
+
scope,
|
|
143
|
+
tenantId,
|
|
144
|
+
source,
|
|
145
|
+
contentFormat,
|
|
146
|
+
sourceResources: parseLinkedResourceEntries(source.linkedResources),
|
|
147
|
+
sourceIsForeign: source.tenantId !== tenantId,
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
105
151
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
152
|
+
export const copyLocaleWrite = defineWriteHandler({
|
|
153
|
+
name: "copy-locale",
|
|
154
|
+
schema: copyLocaleSchema,
|
|
155
|
+
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
156
|
+
description:
|
|
157
|
+
"Copies the text of one locale of a template into another locale as a draft override of the caller's tenant, taking the tenant's own row or else the system default as the source; use it as the starting point for a translation. It refuses an existing target unless overwrite is set. With scope system (SystemAdmin only) both source and target are system-default rows and the target lands as a system draft; any other caller gets a 403.",
|
|
158
|
+
beforeTransaction: async (event, ctx) => {
|
|
159
|
+
const request = await checkCopyRequest(ctx, event);
|
|
160
|
+
if (!request.isSuccess) return request;
|
|
161
|
+
if (!request.data.sourceIsForeign) return { isSuccess: true, data: [] };
|
|
162
|
+
return prepareLinkedResourceCopiesBeforeTransaction({
|
|
163
|
+
ctx,
|
|
164
|
+
user: event.user,
|
|
165
|
+
tenantId: request.data.tenantId,
|
|
166
|
+
resources: request.data.sourceResources,
|
|
167
|
+
});
|
|
168
|
+
},
|
|
169
|
+
handler: async (event, ctx) => {
|
|
170
|
+
const { slug, kind, toLocale } = event.payload;
|
|
171
|
+
const request = await checkCopyRequest(ctx, event);
|
|
172
|
+
if (!request.isSuccess) return request;
|
|
173
|
+
const { scope, tenantId, source, contentFormat, sourceResources, sourceIsForeign } =
|
|
174
|
+
request.data;
|
|
117
175
|
|
|
118
176
|
const variableSchema = storedRecord(source.variableSchema);
|
|
119
177
|
const written = await writeTemplateRow({
|
|
@@ -128,7 +186,7 @@ export const copyLocaleWrite = defineWriteHandler({
|
|
|
128
186
|
kind,
|
|
129
187
|
locale: toLocale,
|
|
130
188
|
content: source.content,
|
|
131
|
-
contentFormat
|
|
189
|
+
contentFormat,
|
|
132
190
|
variableSchema,
|
|
133
191
|
meta: parseStoredJson(source.meta),
|
|
134
192
|
title: source.title,
|
|
@@ -136,17 +194,9 @@ export const copyLocaleWrite = defineWriteHandler({
|
|
|
136
194
|
},
|
|
137
195
|
});
|
|
138
196
|
if (!written.isSuccess) return written;
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const resources =
|
|
143
|
-
source.tenantId === tenantId
|
|
144
|
-
? { isSuccess: true as const, data: sourceResources }
|
|
145
|
-
: await duplicateLinkedResourceFiles(ctx, {
|
|
146
|
-
user: event.user,
|
|
147
|
-
tenantId,
|
|
148
|
-
resources: sourceResources,
|
|
149
|
-
});
|
|
197
|
+
const resources = sourceIsForeign
|
|
198
|
+
? await createLinkedResourceCopies({ ctx, user: event.user, tenantId }, event.prepared)
|
|
199
|
+
: { isSuccess: true as const, data: sourceResources };
|
|
150
200
|
if (!resources.isSuccess) return resources;
|
|
151
201
|
const attached = await attachTemplateResources(ctx, {
|
|
152
202
|
user: event.user,
|
|
@@ -24,6 +24,7 @@ export const diffToSystemQuery = defineQueryHandler({
|
|
|
24
24
|
slug: tenantRow.slug,
|
|
25
25
|
kind: tenantRow.kind,
|
|
26
26
|
locale: tenantRow.locale,
|
|
27
|
+
status: "active",
|
|
27
28
|
});
|
|
28
29
|
if (!systemRow || isHiddenSystemDraft(query.user, systemRow))
|
|
29
30
|
throw new NotFoundError("template-resource", tenantRow.slug);
|
package/packages/bundled-features/src/template-resolver/handlers/import-bundle-system.write.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { defineWriteHandler, SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framew
|
|
|
2
2
|
import { ValidationError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
3
|
import * as z from "zod";
|
|
4
4
|
import { templateBundleSchema } from "../template-bundle.js";
|
|
5
|
-
import { importTemplateBundle } from "./bundle-import.js";
|
|
5
|
+
import { importTemplateBundle, prepareTemplateBundleBeforeTransaction } from "./bundle-import.js";
|
|
6
6
|
|
|
7
7
|
export const importBundleSystemWrite = defineWriteHandler({
|
|
8
8
|
name: "import-bundle-system",
|
|
@@ -12,24 +12,23 @@ export const importBundleSystemWrite = defineWriteHandler({
|
|
|
12
12
|
agent: { risk: "high" as const },
|
|
13
13
|
description:
|
|
14
14
|
"Imports a template bundle as system-default templates, active straight away and overwriting existing system rows; SystemAdmin only. Bundles with resource files need a caller in the system tenant, because the files are stored in the caller's file space.",
|
|
15
|
-
|
|
15
|
+
beforeTransaction: async (event, ctx) => {
|
|
16
16
|
const { bundle } = event.payload;
|
|
17
|
+
// Must run before any upload.
|
|
17
18
|
if (Object.keys(bundle.resources).length > 0 && event.user.tenantId !== SYSTEM_TENANT_ID) {
|
|
18
|
-
return
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}),
|
|
29
|
-
),
|
|
19
|
+
return writeFailure(
|
|
20
|
+
new ValidationError({
|
|
21
|
+
fields: [
|
|
22
|
+
{
|
|
23
|
+
path: "bundle.resources",
|
|
24
|
+
code: "system_resources_need_system_tenant",
|
|
25
|
+
i18nKey: "templateResolver.errors.template.systemResourcesNeedSystemTenant",
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
}),
|
|
30
29
|
);
|
|
31
30
|
}
|
|
32
|
-
return
|
|
31
|
+
return prepareTemplateBundleBeforeTransaction({
|
|
33
32
|
ctx,
|
|
34
33
|
user: event.user,
|
|
35
34
|
tenantId: SYSTEM_TENANT_ID,
|
|
@@ -39,4 +38,15 @@ export const importBundleSystemWrite = defineWriteHandler({
|
|
|
39
38
|
overwrite: true,
|
|
40
39
|
});
|
|
41
40
|
},
|
|
41
|
+
handler: (event, ctx) =>
|
|
42
|
+
importTemplateBundle({
|
|
43
|
+
ctx,
|
|
44
|
+
user: event.user,
|
|
45
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
46
|
+
scope: "system",
|
|
47
|
+
status: "active",
|
|
48
|
+
bundle: event.payload.bundle,
|
|
49
|
+
overwrite: true,
|
|
50
|
+
prepared: event.prepared,
|
|
51
|
+
}),
|
|
42
52
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import * as z from "zod";
|
|
3
3
|
import { templateBundleSchema } from "../template-bundle.js";
|
|
4
|
-
import { importTemplateBundle } from "./bundle-import.js";
|
|
4
|
+
import { importTemplateBundle, prepareTemplateBundleBeforeTransaction } from "./bundle-import.js";
|
|
5
5
|
|
|
6
6
|
export const importBundleWrite = defineWriteHandler({
|
|
7
7
|
name: "import-bundle",
|
|
@@ -9,6 +9,16 @@ export const importBundleWrite = defineWriteHandler({
|
|
|
9
9
|
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
10
10
|
description:
|
|
11
11
|
"Imports a template bundle (all locales plus resource files, as produced by export-bundle) into the caller's tenant as drafts; it refuses a locale that already has an override unless overwrite is set, and publish makes the drafts live.",
|
|
12
|
+
beforeTransaction: (event, ctx) =>
|
|
13
|
+
prepareTemplateBundleBeforeTransaction({
|
|
14
|
+
ctx,
|
|
15
|
+
user: event.user,
|
|
16
|
+
tenantId: event.user.tenantId,
|
|
17
|
+
scope: "tenant",
|
|
18
|
+
status: "draft",
|
|
19
|
+
bundle: event.payload.bundle,
|
|
20
|
+
overwrite: event.payload.overwrite,
|
|
21
|
+
}),
|
|
12
22
|
handler: (event, ctx) =>
|
|
13
23
|
importTemplateBundle({
|
|
14
24
|
ctx,
|
|
@@ -18,5 +28,6 @@ export const importBundleWrite = defineWriteHandler({
|
|
|
18
28
|
status: "draft",
|
|
19
29
|
bundle: event.payload.bundle,
|
|
20
30
|
overwrite: event.payload.overwrite,
|
|
31
|
+
prepared: event.prepared,
|
|
21
32
|
}),
|
|
22
33
|
});
|
|
@@ -52,6 +52,8 @@ export type OverviewResponse = {
|
|
|
52
52
|
readonly rows: readonly OverviewRow[];
|
|
53
53
|
readonly nextCursor: null;
|
|
54
54
|
readonly total: number;
|
|
55
|
+
/** True when the tenant has more template rows than the overview reads; rows and total are then incomplete. */
|
|
56
|
+
readonly truncated: boolean;
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
const SORT_FIELDS = ["title", "slug", "kind", "origin", "status", "modifiedAt"] as const;
|
|
@@ -285,12 +287,19 @@ export const overviewQuery = defineQueryHandler({
|
|
|
285
287
|
"Lists mail, PDF and layout templates as one row per slug and kind for the caller's tenant (optionally only the mails and PDFs using one layout slug): where the effective version comes from (own override or system), its locales, status and version (the served one; `draftVersion` is set when an unpublished draft waits beside it, `activeSince` says since when the served version applies), the system version it overrides, the layout it uses and which notifications or templates use it; use it to browse and filter all templates at a glance.",
|
|
286
288
|
handler: async (query, ctx) => {
|
|
287
289
|
const payload = query.payload;
|
|
288
|
-
const
|
|
290
|
+
const fetchedRows = await selectMany<TemplateResourceRow>(
|
|
289
291
|
ctx.db,
|
|
290
292
|
templateResourcesTable,
|
|
291
293
|
{ kind: { in: [...OVERVIEW_KINDS] } },
|
|
292
|
-
{ limit: OVERVIEW_ROW_LIMIT },
|
|
294
|
+
{ orderBy: { col: "id", direction: "asc" }, limit: OVERVIEW_ROW_LIMIT + 1 },
|
|
293
295
|
);
|
|
296
|
+
const truncated = fetchedRows.length > OVERVIEW_ROW_LIMIT;
|
|
297
|
+
const rows = truncated ? fetchedRows.slice(0, OVERVIEW_ROW_LIMIT) : fetchedRows;
|
|
298
|
+
if (truncated) {
|
|
299
|
+
ctx.log?.warn("template overview: row limit reached, result is incomplete", {
|
|
300
|
+
limit: OVERVIEW_ROW_LIMIT,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
294
303
|
|
|
295
304
|
const visibleRows = rows.filter((row) => !isHiddenSystemDraft(query.user, row));
|
|
296
305
|
const { drafts, layoutSlugByDocument } = buildDrafts(
|
|
@@ -318,6 +327,7 @@ export const overviewQuery = defineQueryHandler({
|
|
|
318
327
|
rows: filtered.slice(offset, offset + payload.limit),
|
|
319
328
|
nextCursor: null,
|
|
320
329
|
total: filtered.length,
|
|
330
|
+
truncated,
|
|
321
331
|
} satisfies OverviewResponse;
|
|
322
332
|
},
|
|
323
333
|
});
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type BeforeTransactionResult,
|
|
3
|
+
defineWriteHandler,
|
|
4
|
+
type WriteResult,
|
|
5
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
2
6
|
import {
|
|
3
7
|
NotFoundError,
|
|
4
8
|
UnprocessableError,
|
|
@@ -13,11 +17,14 @@ import {
|
|
|
13
17
|
import { attachmentFileNameSchema } from "../../shared/index.js";
|
|
14
18
|
import { TemplateResolverErrors } from "../qualified-names.js";
|
|
15
19
|
import {
|
|
20
|
+
type PreparedTemplateFile,
|
|
21
|
+
prepareTemplateAttachmentBeforeTransaction,
|
|
22
|
+
prepareTemplateResourceBeforeTransaction,
|
|
16
23
|
readTemplateResourceMaxBytes,
|
|
17
24
|
type StoredTemplateAttachment,
|
|
18
25
|
type StoredTemplateResource,
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
storePreparedTemplateAttachment,
|
|
27
|
+
storePreparedTemplateResource,
|
|
21
28
|
} from "../store-template-resource.js";
|
|
22
29
|
import { RESOURCE_PURPOSES, resourceKeySchema, resourceTargetTenantId } from "./resource-shared.js";
|
|
23
30
|
|
|
@@ -66,10 +73,7 @@ export const resourceUploadWrite = defineWriteHandler({
|
|
|
66
73
|
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
67
74
|
description:
|
|
68
75
|
"Turns a file uploaded via POST /api/files (multipart, no entityType) into a template resource. With purpose image (default) the upload is checked against the resource-max-bytes cap, re-encoded to a supported image format and stored as a new file; with templateId and key it is linked into that template under the key, without them only the file is created. With purpose attachment, templateId, key and fileName are required: a PDF, PNG or JPEG is stored unchanged and linked into a mail template as a file attached to every mail sent from it, at most 10 per template. A cap above the files route's maxUploadSize has no effect.",
|
|
69
|
-
|
|
70
|
-
event,
|
|
71
|
-
ctx,
|
|
72
|
-
): Promise<WriteResult<StoredTemplateResource | StoredTemplateAttachment>> => {
|
|
76
|
+
beforeTransaction: async (event, ctx): Promise<BeforeTransactionResult<PreparedTemplateFile>> => {
|
|
73
77
|
const { templateId, key, sourceFileRefId, purpose, fileName } = event.payload;
|
|
74
78
|
const attachmentTarget =
|
|
75
79
|
purpose === "attachment" ? attachmentTargetOf({ templateId, key, fileName }) : null;
|
|
@@ -113,7 +117,7 @@ export const resourceUploadWrite = defineWriteHandler({
|
|
|
113
117
|
// The source upload stays as an unclaimed file: bundled-features cannot reach the fileRef executor to delete it.
|
|
114
118
|
const tenantId = resourceTargetTenantId(event.payload.tenantIdOverride, event.user.tenantId);
|
|
115
119
|
if (attachmentTarget !== null) {
|
|
116
|
-
return
|
|
120
|
+
return prepareTemplateAttachmentBeforeTransaction({
|
|
117
121
|
ctx,
|
|
118
122
|
user: event.user,
|
|
119
123
|
tenantId,
|
|
@@ -122,7 +126,7 @@ export const resourceUploadWrite = defineWriteHandler({
|
|
|
122
126
|
...attachmentTarget,
|
|
123
127
|
});
|
|
124
128
|
}
|
|
125
|
-
return
|
|
129
|
+
return prepareTemplateResourceBeforeTransaction({
|
|
126
130
|
ctx,
|
|
127
131
|
user: event.user,
|
|
128
132
|
tenantId,
|
|
@@ -132,4 +136,39 @@ export const resourceUploadWrite = defineWriteHandler({
|
|
|
132
136
|
...(key !== undefined && { key }),
|
|
133
137
|
});
|
|
134
138
|
},
|
|
139
|
+
handler: async (
|
|
140
|
+
event,
|
|
141
|
+
ctx,
|
|
142
|
+
): Promise<WriteResult<StoredTemplateResource | StoredTemplateAttachment>> => {
|
|
143
|
+
const { templateId, key } = event.payload;
|
|
144
|
+
const tenantId = resourceTargetTenantId(event.payload.tenantIdOverride, event.user.tenantId);
|
|
145
|
+
const { prepared } = event;
|
|
146
|
+
if (prepared.kind === "attachment") {
|
|
147
|
+
// The phase accepts attachments only with a complete target, so these are set.
|
|
148
|
+
if (templateId === undefined || key === undefined) {
|
|
149
|
+
return writeFailure(
|
|
150
|
+
new UnprocessableError(TemplateResolverErrors.attachmentInvalid, {
|
|
151
|
+
i18nKey: "templateResolver.errors.attachment.invalid",
|
|
152
|
+
details: { code: "target_required" },
|
|
153
|
+
}),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return storePreparedTemplateAttachment({
|
|
157
|
+
ctx,
|
|
158
|
+
user: event.user,
|
|
159
|
+
tenantId,
|
|
160
|
+
prepared,
|
|
161
|
+
templateId,
|
|
162
|
+
key,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return storePreparedTemplateResource({
|
|
166
|
+
ctx,
|
|
167
|
+
user: event.user,
|
|
168
|
+
tenantId,
|
|
169
|
+
prepared,
|
|
170
|
+
...(templateId !== undefined && { templateId }),
|
|
171
|
+
...(key !== undefined && { key }),
|
|
172
|
+
});
|
|
173
|
+
},
|
|
135
174
|
});
|
|
@@ -10,6 +10,7 @@ import type { ContentFormat, UpsertKind } from "../constants.js";
|
|
|
10
10
|
import type { TemplateWriteScope } from "../layout-regions.js";
|
|
11
11
|
import { imageResourceKeysOf } from "../linked-resources.js";
|
|
12
12
|
import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
|
|
13
|
+
import { isSystemAdmin } from "../template-draft.js";
|
|
13
14
|
import { resolveTemplateVariables, systemPlaceholdersFor } from "../template-validation.js";
|
|
14
15
|
import { contentFormatSchema, kindSchema, localeSchema, templateSlugSchema } from "./shared.js";
|
|
15
16
|
import { evaluateTemplateWrite } from "./template-gates.js";
|
|
@@ -53,7 +54,9 @@ export async function validateTemplateDraft(
|
|
|
53
54
|
payload: TemplateDraftInput,
|
|
54
55
|
) {
|
|
55
56
|
const variables = resolveTemplateVariables(ctx.registry, payload.slug, payload.variableSchema);
|
|
56
|
-
const { slug, kind,
|
|
57
|
+
const { slug, kind, locale } = payload;
|
|
58
|
+
// The dry run must not promise what the caller's write handlers would refuse.
|
|
59
|
+
const scope: TemplateWriteScope = isSystemAdmin(user) ? payload.scope : "tenant";
|
|
57
60
|
const known = [
|
|
58
61
|
...(await knownResourceKeys(ctx.db, {
|
|
59
62
|
tenantId: SYSTEM_TENANT_ID,
|
|
@@ -16,6 +16,7 @@ export {
|
|
|
16
16
|
CONTENT_FORMATS,
|
|
17
17
|
type ContentFormat,
|
|
18
18
|
DEFAULT_MAIL_LAYOUT_SLUG,
|
|
19
|
+
DEFAULT_PDF_LAYOUT_SLUG,
|
|
19
20
|
DOCUMENT_HTML_KINDS,
|
|
20
21
|
type DocumentHtmlKind,
|
|
21
22
|
FALLBACK_LOCALE,
|
|
@@ -110,9 +111,11 @@ export {
|
|
|
110
111
|
} from "./qualified-names.js";
|
|
111
112
|
export { mapRecordToVariables } from "./record-mapping.js";
|
|
112
113
|
export {
|
|
114
|
+
type PreparedTemplateImage,
|
|
115
|
+
prepareTemplateResourceBeforeTransaction,
|
|
113
116
|
readTemplateResourceMaxBytes,
|
|
114
117
|
type StoredTemplateResource,
|
|
115
|
-
|
|
118
|
+
storePreparedTemplateResource,
|
|
116
119
|
} from "./store-template-resource.js";
|
|
117
120
|
export {
|
|
118
121
|
type TemplateResourceRow,
|
|
@@ -124,9 +127,11 @@ export {
|
|
|
124
127
|
isMailTemplateMeta,
|
|
125
128
|
type LayoutTemplateMeta,
|
|
126
129
|
MAIL_CATEGORIES,
|
|
130
|
+
MAIL_TONES,
|
|
127
131
|
type MailCategory,
|
|
128
132
|
type MailLayoutMeta,
|
|
129
133
|
type MailTemplateMeta,
|
|
134
|
+
type MailTone,
|
|
130
135
|
type PdfTemplateMeta,
|
|
131
136
|
parseTemplateMeta,
|
|
132
137
|
pdfAttachmentFileNamePatternSchema,
|
|
@@ -15,26 +15,42 @@ const FONT_FAMILIES = {
|
|
|
15
15
|
serif: "Georgia, 'Times New Roman', serif",
|
|
16
16
|
} as const;
|
|
17
17
|
|
|
18
|
-
const
|
|
18
|
+
const DEFAULT_BUTTON_COLOR = "#1f5468";
|
|
19
19
|
const BUTTON_BASE = "display:inline-block;padding:10px 20px;font-weight:600;text-decoration:none";
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
type ButtonStyle = NonNullable<MailLayoutMeta["buttonStyle"]>;
|
|
22
|
+
|
|
23
|
+
function buttonStyleCss(style: ButtonStyle, color: string): string {
|
|
24
|
+
switch (style) {
|
|
25
|
+
case "filled-rounded":
|
|
26
|
+
return `${BUTTON_BASE};border-radius:6px;background-color:${color};color:#ffffff`;
|
|
27
|
+
case "filled-square":
|
|
28
|
+
return `${BUTTON_BASE};background-color:${color};color:#ffffff`;
|
|
29
|
+
case "outline":
|
|
30
|
+
return `${BUTTON_BASE};border:2px solid ${color};border-radius:6px;color:${color}`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
26
33
|
|
|
27
34
|
export function isHexColor(value: string): boolean {
|
|
28
35
|
return HEX_COLOR_PATTERN.test(value);
|
|
29
36
|
}
|
|
30
37
|
|
|
31
|
-
|
|
38
|
+
// The branding accent colors the buttons only where the layout opted into the branding accent line.
|
|
39
|
+
export function mailLayoutAppearance(
|
|
40
|
+
meta: MailLayoutMeta | unknown,
|
|
41
|
+
brandingAccentColor?: string,
|
|
42
|
+
): MailLayoutAppearance {
|
|
32
43
|
const parsed = readMailLayoutMeta(meta);
|
|
44
|
+
const accentLine = (parsed.accentLine ?? "none") === "branding";
|
|
45
|
+
const buttonColor =
|
|
46
|
+
accentLine && brandingAccentColor !== undefined && isHexColor(brandingAccentColor)
|
|
47
|
+
? brandingAccentColor
|
|
48
|
+
: DEFAULT_BUTTON_COLOR;
|
|
33
49
|
return {
|
|
34
50
|
width: parsed.width ?? 600,
|
|
35
51
|
fontFamily: FONT_FAMILIES[parsed.font ?? "sans"],
|
|
36
52
|
outerBackground: parsed.outerBackground ?? "#f5f5f5",
|
|
37
|
-
accentLine
|
|
38
|
-
buttonStyle:
|
|
53
|
+
accentLine,
|
|
54
|
+
buttonStyle: buttonStyleCss(parsed.buttonStyle ?? "filled-rounded", buttonColor),
|
|
39
55
|
};
|
|
40
56
|
}
|
|
@@ -1,27 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
assertNotSvg,
|
|
3
|
+
isResourceImageFormat,
|
|
4
|
+
RESOURCE_IMAGE_FORMATS,
|
|
5
|
+
type ResourceImageContentType,
|
|
6
|
+
SHARP_INPUT_OPTIONS,
|
|
7
|
+
} from "../shared/index.js";
|
|
2
8
|
|
|
3
9
|
const MAX_RESOURCE_WIDTH = 1200;
|
|
4
10
|
|
|
5
|
-
const CONTENT_TYPE_BY_FORMAT = {
|
|
6
|
-
png: "image/png",
|
|
7
|
-
jpeg: "image/jpeg",
|
|
8
|
-
gif: "image/gif",
|
|
9
|
-
webp: "image/webp",
|
|
10
|
-
} as const;
|
|
11
|
-
|
|
12
|
-
type ResourceImageFormat = keyof typeof CONTENT_TYPE_BY_FORMAT;
|
|
13
|
-
|
|
14
11
|
export type NormalizedResourceImage = {
|
|
15
12
|
readonly data: Uint8Array;
|
|
16
|
-
readonly contentType:
|
|
13
|
+
readonly contentType: ResourceImageContentType;
|
|
17
14
|
readonly width: number;
|
|
18
15
|
readonly height: number;
|
|
19
16
|
};
|
|
20
17
|
|
|
21
|
-
function isResourceImageFormat(format: string | undefined): format is ResourceImageFormat {
|
|
22
|
-
return format !== undefined && Object.hasOwn(CONTENT_TYPE_BY_FORMAT, format);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
18
|
// Re-encodes an uploaded image: the stored content type comes from the decoded format, never
|
|
26
19
|
// from the client, and re-encoding drops metadata and anything appended to the file.
|
|
27
20
|
// sharp is imported lazily because every app loads template-resolver, few of them need an image decoder.
|
|
@@ -40,7 +33,7 @@ export async function normalizeResourceImage(data: Uint8Array): Promise<Normaliz
|
|
|
40
33
|
const { data: output, info } = await pipeline.toBuffer({ resolveWithObject: true });
|
|
41
34
|
return {
|
|
42
35
|
data: new Uint8Array(output),
|
|
43
|
-
contentType:
|
|
36
|
+
contentType: RESOURCE_IMAGE_FORMATS[format].contentType,
|
|
44
37
|
width: info.width,
|
|
45
38
|
height: info.height,
|
|
46
39
|
};
|
|
@@ -155,6 +155,8 @@ export async function seedSystemTemplate(
|
|
|
155
155
|
current?.title === rowFields.title &&
|
|
156
156
|
current?.variableSchema === rowFields.variableSchema &&
|
|
157
157
|
current?.meta === rowFields.meta &&
|
|
158
|
+
current?.status === rowFields.status &&
|
|
159
|
+
current?.parentTemplateId === rowFields.parentTemplateId &&
|
|
158
160
|
(linkedResources === undefined || current?.linkedResources === linkedResources)
|
|
159
161
|
) {
|
|
160
162
|
return { id: String(row.id) };
|