@cosmicdrift/kumiko-samples 0.364.0 → 0.365.1
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/changes.json +7 -0
- 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/changes.json +8 -1
- package/packages/bundled-features/src/step-dispatcher/constants.ts +22 -0
- package/packages/bundled-features/src/step-dispatcher/deliver-dispatch-request.ts +236 -0
- package/packages/bundled-features/src/step-dispatcher/feature.ts +65 -118
- package/packages/bundled-features/src/step-dispatcher/webhook-runner.ts +40 -14
- 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 +9 -3
- 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 +4 -4
- 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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import type {
|
|
3
|
+
BeforeTransactionResult,
|
|
3
4
|
HandlerContext,
|
|
4
5
|
SessionUser,
|
|
5
6
|
TenantId,
|
|
@@ -16,9 +17,13 @@ import type { LinkedResourceEntry } from "../linked-resources.js";
|
|
|
16
17
|
import {
|
|
17
18
|
attachmentLimitFailure,
|
|
18
19
|
attachmentNeedsMailTemplate,
|
|
20
|
+
createPreparedBundleResourceEntry,
|
|
21
|
+
type PreparedTemplateFile,
|
|
22
|
+
prepareTemplateAttachmentBeforeTransaction,
|
|
23
|
+
prepareTemplateResourceBeforeTransaction,
|
|
24
|
+
type Release,
|
|
19
25
|
readTemplateResourceMaxBytes,
|
|
20
|
-
|
|
21
|
-
storeTemplateResource,
|
|
26
|
+
releaseAll,
|
|
22
27
|
} from "../store-template-resource.js";
|
|
23
28
|
import { type TemplateResourceRow, templateResourcesTable } from "../table.js";
|
|
24
29
|
import type { TemplateBundle } from "../template-bundle.js";
|
|
@@ -92,64 +97,101 @@ async function collectBundleIssues(
|
|
|
92
97
|
return issues;
|
|
93
98
|
}
|
|
94
99
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
):
|
|
98
|
-
const linked: Record<string, LinkedResourceEntry> = {};
|
|
99
|
-
const pdfLinks = Object.entries(args.bundle.pdfAttachments);
|
|
100
|
+
export type PreparedBundleResources = ReadonlyMap<string, PreparedTemplateFile>;
|
|
101
|
+
|
|
102
|
+
function checkBundleAttachments(bundle: TemplateBundle): WriteResult<null> {
|
|
100
103
|
const attachmentCount =
|
|
101
|
-
Object.values(
|
|
102
|
-
|
|
103
|
-
if (attachmentCount > 0 &&
|
|
104
|
+
Object.values(bundle.resources).filter((resource) => resource.purpose === "attachment").length +
|
|
105
|
+
Object.keys(bundle.pdfAttachments).length;
|
|
106
|
+
if (attachmentCount > 0 && bundle.kind !== "mail") return attachmentNeedsMailTemplate();
|
|
104
107
|
if (attachmentCount > MAX_FILE_ATTACHMENTS_PER_TEMPLATE) return attachmentLimitFailure();
|
|
105
|
-
|
|
106
|
-
|
|
108
|
+
return { isSuccess: true, data: null };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// beforeTransaction half: validates the bundle and uploads every resource file to the target
|
|
112
|
+
// tenant's storage. A failed or throwing upload releases the ones before it.
|
|
113
|
+
export async function prepareTemplateBundleBeforeTransaction(
|
|
114
|
+
args: BundleImport & { readonly status: BundleImportStatus },
|
|
115
|
+
): Promise<BeforeTransactionResult<PreparedBundleResources>> {
|
|
116
|
+
const refused = refuseTenantScopeUnderSystemTenant(args.scope, args.tenantId);
|
|
117
|
+
if (refused !== null) return refused;
|
|
118
|
+
const issues = await collectBundleIssues(args, args.status);
|
|
119
|
+
if (issues.length > 0) return writeFailure(new ValidationError({ fields: [...issues] }));
|
|
120
|
+
const attachmentCheck = checkBundleAttachments(args.bundle);
|
|
121
|
+
if (!attachmentCheck.isSuccess) return attachmentCheck;
|
|
122
|
+
|
|
107
123
|
const maxBytes = await readTemplateResourceMaxBytes(args.ctx.config, args.ctx.registry);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
124
|
+
const prepared = new Map<string, PreparedTemplateFile>();
|
|
125
|
+
const releases: Release[] = [];
|
|
126
|
+
try {
|
|
127
|
+
// A bundle is untrusted input like an upload: the same re-encoding decides the stored
|
|
128
|
+
// content type, so a declared image/png can't smuggle SVG or HTML into a mail or PDF.
|
|
129
|
+
for (const [key, resource] of Object.entries(args.bundle.resources)) {
|
|
130
|
+
const bytes = Uint8Array.from(Buffer.from(resource.base64, "base64"));
|
|
131
|
+
const common = {
|
|
114
132
|
ctx: args.ctx,
|
|
115
133
|
user: args.user,
|
|
116
134
|
tenantId: args.tenantId,
|
|
117
135
|
bytes,
|
|
118
136
|
maxBytes,
|
|
119
137
|
key,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
138
|
+
};
|
|
139
|
+
const result =
|
|
140
|
+
resource.purpose === "attachment"
|
|
141
|
+
? await prepareTemplateAttachmentBeforeTransaction({
|
|
142
|
+
...common,
|
|
143
|
+
fileName: resource.fileName,
|
|
144
|
+
})
|
|
145
|
+
: await prepareTemplateResourceBeforeTransaction(common);
|
|
146
|
+
if (!result.isSuccess) {
|
|
147
|
+
await releaseAll(releases);
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
if (result.release) releases.push(result.release);
|
|
151
|
+
prepared.set(key, result.data);
|
|
125
152
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
153
|
+
} catch (cause) {
|
|
154
|
+
await releaseAll(releases);
|
|
155
|
+
throw cause;
|
|
156
|
+
}
|
|
157
|
+
return { isSuccess: true, data: prepared, release: () => releaseAll(releases) };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function linkPreparedBundleResources(
|
|
161
|
+
args: BundleImport,
|
|
162
|
+
prepared: PreparedBundleResources,
|
|
163
|
+
): Promise<WriteResult<Record<string, LinkedResourceEntry>>> {
|
|
164
|
+
const linked: Record<string, LinkedResourceEntry> = {};
|
|
165
|
+
// The document template is not looked up: a bundle may carry it later than the mail.
|
|
166
|
+
for (const [key, link] of Object.entries(args.bundle.pdfAttachments)) {
|
|
167
|
+
linked[key] = { kind: "rendered-pdf", ...link };
|
|
168
|
+
}
|
|
169
|
+
for (const [key, file] of prepared) {
|
|
170
|
+
const entry = await createPreparedBundleResourceEntry(
|
|
171
|
+
{ ctx: args.ctx, user: args.user, tenantId: args.tenantId },
|
|
172
|
+
file,
|
|
173
|
+
);
|
|
174
|
+
if (!entry.isSuccess) return entry;
|
|
175
|
+
linked[key] = entry.data;
|
|
136
176
|
}
|
|
137
177
|
return { isSuccess: true, data: linked };
|
|
138
178
|
}
|
|
139
179
|
|
|
140
|
-
// Validates every locale first,
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
// for the resources before it stay as unlinked FileRefs, like a replaced upload.
|
|
180
|
+
// Validates every locale first, so a bad locale is refused before anything is written.
|
|
181
|
+
// The resource files are uploaded in the beforeTransaction phase; a rolled-back transaction
|
|
182
|
+
// releases them there. A crash between the upload and the commit leaves the binaries unreferenced.
|
|
144
183
|
export async function importTemplateBundle(
|
|
145
|
-
args: BundleImport & {
|
|
184
|
+
args: BundleImport & {
|
|
185
|
+
readonly status: BundleImportStatus;
|
|
186
|
+
readonly prepared: PreparedBundleResources;
|
|
187
|
+
},
|
|
146
188
|
): Promise<WriteResult<{ ids: Record<string, string> }>> {
|
|
147
189
|
const refused = refuseTenantScopeUnderSystemTenant(args.scope, args.tenantId);
|
|
148
190
|
if (refused !== null) return refused;
|
|
149
191
|
const issues = await collectBundleIssues(args, args.status);
|
|
150
192
|
if (issues.length > 0) return writeFailure(new ValidationError({ fields: [...issues] }));
|
|
151
193
|
|
|
152
|
-
const linked = await
|
|
194
|
+
const linked = await linkPreparedBundleResources(args, args.prepared);
|
|
153
195
|
if (!linked.isSuccess) return linked;
|
|
154
196
|
|
|
155
197
|
const variableSchema = args.bundle.variableSchema ?? {};
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
import { ValidationError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
8
8
|
import * as z from "zod";
|
|
9
9
|
import { requireForTenant } from "../../shared/index.js";
|
|
10
|
-
import {
|
|
10
|
+
import { isCollectionRefusedKind } from "../constants.js";
|
|
11
11
|
import { documentViolationsToFieldIssues, sanitizeTemplateBody } from "../document-sanitizer.js";
|
|
12
12
|
import {
|
|
13
13
|
type CollectionEntryRow,
|
|
@@ -43,12 +43,13 @@ export function makeCollectionSetWrite(collection: ContentCollectionDefinition)
|
|
|
43
43
|
? `Creates or overwrites the caller's own "${collection.id}" entry at a slug and locale, live on save with no draft stage; it always writes the acting user's entry, never another user's.`
|
|
44
44
|
: `Creates or overwrites the tenant-wide "${collection.id}" entry at a slug and locale, live on save with no draft stage; the change is visible to everyone who may reach this shared collection.`,
|
|
45
45
|
handler: async (event, ctx) => {
|
|
46
|
-
|
|
46
|
+
// Defense in depth: createTemplateResolverFeature already refuses these kinds at mount.
|
|
47
|
+
if (isCollectionRefusedKind(collection.kind)) {
|
|
47
48
|
return writeFailure(
|
|
48
49
|
new ValidationError({
|
|
49
50
|
fields: [
|
|
50
51
|
{
|
|
51
|
-
path: "
|
|
52
|
+
path: "",
|
|
52
53
|
code: "kind_not_allowed",
|
|
53
54
|
i18nKey: "templateResolver.errors.collection.kindNotAllowed",
|
|
54
55
|
params: { detail: collection.kind },
|
|
@@ -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
|
});
|