@cosmicdrift/kumiko-samples 0.377.0 → 0.378.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 +1 -1
- package/packages/bundled-features/src/cap-counter/changes.json +12 -0
- package/packages/bundled-features/src/cap-counter/stock-cap-guard.ts +9 -0
- package/packages/bundled-features/src/config/web/i18n.ts +1 -0
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +6 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/list-accounts.query.ts +11 -4
- package/packages/bundled-features/src/mail-foundation/changes.json +6 -0
- package/packages/bundled-features/src/mail-foundation/i18n.ts +3 -2
- package/packages/bundled-features/src/mail-transport-smtp/i18n.ts +1 -1
- package/packages/bundled-features/src/template-resolver/changes.json +20 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +61 -3
- package/packages/bundled-features/src/template-resolver/constants.ts +3 -0
- package/packages/bundled-features/src/template-resolver/document-blocks.ts +3 -1
- package/packages/bundled-features/src/template-resolver/document-images.ts +29 -0
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +35 -0
- package/packages/bundled-features/src/template-resolver/document-schema.ts +5 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +77 -36
- package/packages/bundled-features/src/template-resolver/handlers/collection-item.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/collection-list.query.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/collection-set.write.ts +2 -6
- package/packages/bundled-features/src/template-resolver/handlers/collection-shared.ts +0 -6
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +67 -58
- package/packages/bundled-features/src/template-resolver/handlers/discard-draft.write.ts +36 -13
- package/packages/bundled-features/src/template-resolver/handlers/import-bundle.write.ts +37 -28
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +66 -62
- package/packages/bundled-features/src/template-resolver/handlers/patch-draft.write.ts +105 -96
- package/packages/bundled-features/src/template-resolver/handlers/reset-to-system.write.ts +62 -49
- package/packages/bundled-features/src/template-resolver/handlers/resource-link-pdf.write.ts +44 -29
- package/packages/bundled-features/src/template-resolver/handlers/resource-remove.write.ts +52 -36
- package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +126 -106
- package/packages/bundled-features/src/template-resolver/handlers/send-test-shared.ts +38 -0
- package/packages/bundled-features/src/template-resolver/handlers/send-test-status.query.ts +31 -0
- package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +105 -111
- package/packages/bundled-features/src/template-resolver/handlers/set.write.ts +110 -103
- package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +56 -28
- package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +38 -29
- package/packages/bundled-features/src/template-resolver/index.ts +8 -1
- package/packages/bundled-features/src/template-resolver/qualified-names.ts +2 -0
- package/packages/bundled-features/src/template-resolver/template-edit-policy.ts +91 -0
- package/packages/bundled-features/src/template-resolver/template-preview.ts +8 -2
- package/packages/bundled-features/src/template-resolver/template-validation.ts +31 -10
- package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +29 -0
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +42 -4
- package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +2 -0
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +6 -0
- package/samples/apps/ui-walkthrough/src/app/help-action.tsx +37 -0
- package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +2 -0
- package/samples/apps/ui-walkthrough/src/app/shell.tsx +6 -0
- package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +20 -0
- package/samples/apps/ui-walkthrough/src/features/rental/feature.ts +65 -1
- package/samples/apps/ui-walkthrough/src/features/rental/i18n.ts +70 -0
- package/samples/apps/ui-walkthrough/src/features/rental/invoice-receive.ts +20 -0
- package/samples/apps/ui-walkthrough/src/features/rental/lease-support.ts +15 -0
- package/samples/apps/ui-walkthrough/src/features/rental/qualified-names.ts +2 -0
- package/samples/apps/ui-walkthrough/src/features/rental/screens.ts +79 -0
- package/samples/apps/ui-walkthrough/src/features/rental/web/index.ts +2 -0
- package/samples/apps/ui-walkthrough/src/features/rental/web/upload-standard-layout.tsx +61 -0
- package/samples/apps/ui-walkthrough/src/features/tasks/i18n.ts +8 -0
- package/samples/apps/ui-walkthrough/src/run-config.ts +32 -3
- package/samples/apps/use-all-bundled/feature-manifest.json +2 -2
- package/samples/recipes/delivery-notifications/README.md +1 -1
- package/samples/recipes/embedded/README.md +8 -0
- package/samples/recipes/embedded/src/feature.ts +8 -0
- package/samples/recipes/embedded/src/handlers/invoice-receive.write.ts +38 -0
- package/samples/recipes/embedded/src/screens/invoice-receive.screen.ts +59 -0
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
parseTemplateDraft,
|
|
12
12
|
servedVersionOf,
|
|
13
13
|
} from "../template-draft.ts";
|
|
14
|
+
import { mayEditTemplate, type TemplateEditPolicy } from "../template-edit-policy.ts";
|
|
14
15
|
import { isSystemOnlyTemplateHiddenFrom } from "../template-system-only.ts";
|
|
15
16
|
import { collectTemplateUsage, type EffectiveMailLayoutSlugs } from "../template-usage.ts";
|
|
16
17
|
import { storedRecord } from "./template-gates.ts";
|
|
@@ -271,68 +272,71 @@ const filterSchema = z.object({
|
|
|
271
272
|
value: z.array(z.string()),
|
|
272
273
|
});
|
|
273
274
|
|
|
274
|
-
export
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
275
|
+
export function makeOverviewQuery(policy: TemplateEditPolicy) {
|
|
276
|
+
return defineQueryHandler({
|
|
277
|
+
name: "overview",
|
|
278
|
+
schema: z.object({
|
|
279
|
+
search: z.string().optional(),
|
|
280
|
+
filters: z.array(filterSchema).optional(),
|
|
281
|
+
layoutSlug: z.string().optional(),
|
|
282
|
+
sort: z.string().optional(),
|
|
283
|
+
sortDirection: z.enum(["asc", "desc"]).optional(),
|
|
284
|
+
limit: z.number().int().min(1).max(200).default(50),
|
|
285
|
+
offset: z.number().int().min(0).optional(),
|
|
286
|
+
totalCount: z.boolean().optional(),
|
|
287
|
+
}),
|
|
288
|
+
access: { roles: ["TenantAdmin", "SystemAdmin"] },
|
|
289
|
+
description:
|
|
290
|
+
"Lists mail, PDF and layout templates as one row per slug and kind for the caller's tenant (optionally only the mails and PDFs using one layout slug): where the effective version comes from (own override or system), its locales, status and version (the served one; `draftVersion` is set when an unpublished draft waits beside it, `activeSince` says since when the served version applies), the system version it overrides, the layout it uses and which notifications or templates use it; templates of notifications registered with `systemOnly` are listed for SystemAdmins only, and a TenantAdmin sees only the templates the app lists in tenantEditable; use it to browse and filter all templates at a glance.",
|
|
291
|
+
handler: async (query, ctx) => {
|
|
292
|
+
const payload = query.payload;
|
|
293
|
+
const fetchedRows = await selectMany<TemplateResourceRow>(
|
|
294
|
+
ctx.db,
|
|
295
|
+
templateResourcesTable,
|
|
296
|
+
{ kind: { in: [...OVERVIEW_KINDS] } },
|
|
297
|
+
{ orderBy: { col: "id", direction: "asc" }, limit: OVERVIEW_ROW_LIMIT + 1 },
|
|
298
|
+
);
|
|
299
|
+
const truncated = fetchedRows.length > OVERVIEW_ROW_LIMIT;
|
|
300
|
+
const rows = truncated ? fetchedRows.slice(0, OVERVIEW_ROW_LIMIT) : fetchedRows;
|
|
301
|
+
if (truncated) {
|
|
302
|
+
ctx.log?.warn("template overview: row limit reached, result is incomplete", {
|
|
303
|
+
limit: OVERVIEW_ROW_LIMIT,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
304
306
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
307
|
+
const visibleRows = rows.filter(
|
|
308
|
+
(row) =>
|
|
309
|
+
!isHiddenSystemDraft(query.user, row) &&
|
|
310
|
+
!isSystemOnlyTemplateHiddenFrom(query.user, ctx.registry, row) &&
|
|
311
|
+
mayEditTemplate(policy, query.user, row),
|
|
312
|
+
);
|
|
313
|
+
const { drafts, layoutSlugByDocument } = buildDrafts(
|
|
314
|
+
groupBySlugAndKind(visibleRows),
|
|
315
|
+
query.user,
|
|
316
|
+
);
|
|
317
|
+
const effectiveLayouts = effectiveMailLayoutSlugs(visibleRows);
|
|
318
|
+
const overviewRows = drafts.map((draft) =>
|
|
319
|
+
withUsage(draft, layoutSlugByDocument, ctx.registry, effectiveLayouts),
|
|
320
|
+
);
|
|
321
|
+
const filtered = filterRows(
|
|
322
|
+
overviewRows,
|
|
323
|
+
payload.search,
|
|
324
|
+
payload.filters ?? [],
|
|
325
|
+
payload.layoutSlug,
|
|
326
|
+
);
|
|
324
327
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
328
|
+
const sortField: SortField =
|
|
329
|
+
payload.sort !== undefined && isSortField(payload.sort) ? payload.sort : "modifiedAt";
|
|
330
|
+
const direction = (payload.sortDirection ?? "desc") === "desc" ? -1 : 1;
|
|
331
|
+
filtered.sort((a, b) => direction * compareBy(sortField, a, b) || a.id.localeCompare(b.id));
|
|
329
332
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
});
|
|
333
|
+
const offset = payload.offset ?? 0;
|
|
334
|
+
return {
|
|
335
|
+
rows: filtered.slice(offset, offset + payload.limit),
|
|
336
|
+
nextCursor: null,
|
|
337
|
+
total: filtered.length,
|
|
338
|
+
truncated,
|
|
339
|
+
} satisfies OverviewResponse;
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
}
|
|
@@ -10,6 +10,11 @@ import * as z from "zod";
|
|
|
10
10
|
import { DOCUMENT_HTML_KINDS } from "../constants.ts";
|
|
11
11
|
import { applyDocumentOps, documentOpsSchema } from "../document-ops.ts";
|
|
12
12
|
import { parseDocumentJson, serializeDocumentJson } from "../document-sanitizer.ts";
|
|
13
|
+
import {
|
|
14
|
+
refuseTemplateEdit,
|
|
15
|
+
type TemplateEditPolicy,
|
|
16
|
+
templateEditAccess,
|
|
17
|
+
} from "../template-edit-policy.ts";
|
|
13
18
|
import { resolveTemplateVariables } from "../template-validation.ts";
|
|
14
19
|
import { loadDocumentBase } from "./document-base.ts";
|
|
15
20
|
import { localeSchema, templateSlugSchema } from "./shared.ts";
|
|
@@ -20,106 +25,110 @@ function validationFailure(issue: FieldIssue) {
|
|
|
20
25
|
return writeFailure(new ValidationError({ fields: [issue] }));
|
|
21
26
|
}
|
|
22
27
|
|
|
23
|
-
export
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
export function makePatchDraftWrite(policy: TemplateEditPolicy) {
|
|
29
|
+
return defineWriteHandler({
|
|
30
|
+
name: "patch-draft",
|
|
31
|
+
schema: z.object({
|
|
32
|
+
slug: templateSlugSchema,
|
|
33
|
+
kind: z.enum(DOCUMENT_HTML_KINDS),
|
|
34
|
+
locale: localeSchema,
|
|
35
|
+
base: z.object({
|
|
36
|
+
scope: z.enum(["tenant", "system"]),
|
|
37
|
+
version: z.number().int().nonnegative(),
|
|
38
|
+
}),
|
|
39
|
+
ops: documentOpsSchema,
|
|
32
40
|
}),
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
access: templateEditAccess(policy.tenantEditable),
|
|
42
|
+
description:
|
|
43
|
+
"Changes a mail or PDF document template with structured operations instead of a full rewrite and saves the result as the caller's tenant draft (never published, an active version keeps being served until publish): insertBlock after a block id (null = at the start; paragraph, heading, image, button, divider, pageBreak), replaceText inside one block (the text must occur exactly once), removeBlock, setResource on an image block, setMeta (flat merge, null removes a key) and setVariable (adds, replaces or with null removes a variable definition; refused when a notification declares the variables). Block ids and base come from document-blocks and refer to the state that was read; if the template changed since, the call fails with version_conflict and nothing is written. A patch that removes a required placeholder is refused. A TenantAdmin may patch only templates the app lists in tenantEditable, otherwise SystemAdmin only. Returns the written row.",
|
|
44
|
+
handler: async (event, ctx) => {
|
|
45
|
+
const { slug, kind, locale, base, ops } = event.payload;
|
|
46
|
+
const refused = refuseTemplateEdit(policy, event.user, { slug, kind });
|
|
47
|
+
if (refused !== null) return refused;
|
|
48
|
+
const loaded = await loadDocumentBase(ctx, event.user, { slug, kind, locale });
|
|
49
|
+
if (loaded === null) return writeFailure(new NotFoundError("template-resource", slug));
|
|
50
|
+
if (loaded.base.scope !== base.scope || loaded.base.version !== base.version) {
|
|
51
|
+
return writeFailure(
|
|
52
|
+
new VersionConflictError({
|
|
53
|
+
entityId: loaded.rowId,
|
|
54
|
+
expectedVersion: base.version,
|
|
55
|
+
currentVersion: loaded.base.version,
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const variableOpIndex = ops.findIndex((op) => op.op === "setVariable");
|
|
61
|
+
if (
|
|
62
|
+
variableOpIndex !== -1 &&
|
|
63
|
+
ctx.registry.getAllNotifications().get(slug)?.variables !== undefined
|
|
64
|
+
) {
|
|
65
|
+
return validationFailure({
|
|
66
|
+
path: `ops.${variableOpIndex}`,
|
|
67
|
+
code: "variables_declared_by_feature",
|
|
68
|
+
i18nKey: "templateResolver.errors.patch.variablesDeclaredByFeature",
|
|
69
|
+
params: { slug },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
51
72
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
path: `ops.${variableOpIndex}`,
|
|
59
|
-
code: "variables_declared_by_feature",
|
|
60
|
-
i18nKey: "templateResolver.errors.patch.variablesDeclaredByFeature",
|
|
61
|
-
params: { slug },
|
|
73
|
+
const applied = applyDocumentOps({
|
|
74
|
+
doc: parseDocumentJson(loaded.content),
|
|
75
|
+
meta: loaded.meta,
|
|
76
|
+
variableSchema: loaded.variableSchema,
|
|
77
|
+
kind,
|
|
78
|
+
ops,
|
|
62
79
|
});
|
|
63
|
-
|
|
80
|
+
if (!applied.ok) return writeFailure(new ValidationError({ fields: applied.issues }));
|
|
81
|
+
const content = serializeDocumentJson(applied.doc);
|
|
64
82
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
83
|
+
const draftOf = (
|
|
84
|
+
draftContent: string,
|
|
85
|
+
meta: unknown,
|
|
86
|
+
variableSchema: Record<string, unknown>,
|
|
87
|
+
) =>
|
|
88
|
+
validateTemplateDraft(ctx, event.user, {
|
|
89
|
+
slug,
|
|
90
|
+
kind,
|
|
91
|
+
contentFormat: "html",
|
|
92
|
+
content: draftContent,
|
|
93
|
+
meta,
|
|
94
|
+
variableSchema,
|
|
95
|
+
locale,
|
|
96
|
+
scope: "tenant",
|
|
97
|
+
});
|
|
98
|
+
const before = await draftOf(loaded.content, loaded.meta, loaded.variableSchema);
|
|
99
|
+
const after = await draftOf(content, applied.meta, applied.variableSchema);
|
|
100
|
+
const alreadyMissing = new Set(before.response.missingRequired);
|
|
101
|
+
const removed = after.response.missingRequired.filter((name) => !alreadyMissing.has(name));
|
|
102
|
+
if (removed.length > 0) {
|
|
103
|
+
return validationFailure({
|
|
104
|
+
path: "ops",
|
|
105
|
+
code: "required_placeholder_removed",
|
|
106
|
+
i18nKey: "templateResolver.errors.patch.requiredPlaceholderRemoved",
|
|
107
|
+
params: { names: removed },
|
|
108
|
+
});
|
|
109
|
+
}
|
|
74
110
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
) =>
|
|
80
|
-
validateTemplateDraft(ctx, event.user, {
|
|
81
|
-
slug,
|
|
82
|
-
kind,
|
|
83
|
-
contentFormat: "html",
|
|
84
|
-
content: draftContent,
|
|
85
|
-
meta,
|
|
86
|
-
variableSchema,
|
|
87
|
-
locale,
|
|
111
|
+
return writeTemplateRow({
|
|
112
|
+
ctx,
|
|
113
|
+
user: event.user,
|
|
114
|
+
tenantId: event.user.tenantId,
|
|
88
115
|
scope: "tenant",
|
|
116
|
+
status: "draft",
|
|
117
|
+
variables: resolveTemplateVariables(ctx.registry, slug, applied.variableSchema),
|
|
118
|
+
payload: {
|
|
119
|
+
slug,
|
|
120
|
+
kind,
|
|
121
|
+
locale,
|
|
122
|
+
content,
|
|
123
|
+
contentFormat: "html",
|
|
124
|
+
variableSchema: applied.variableSchema,
|
|
125
|
+
meta: applied.meta,
|
|
126
|
+
title: loaded.title,
|
|
127
|
+
folder: loaded.folder,
|
|
128
|
+
parentTemplateId: loaded.parentTemplateId,
|
|
129
|
+
},
|
|
130
|
+
expectedVersion: loaded.base.scope === "tenant" ? loaded.base.version : null,
|
|
89
131
|
});
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const removed = after.response.missingRequired.filter((name) => !alreadyMissing.has(name));
|
|
94
|
-
if (removed.length > 0) {
|
|
95
|
-
return validationFailure({
|
|
96
|
-
path: "ops",
|
|
97
|
-
code: "required_placeholder_removed",
|
|
98
|
-
i18nKey: "templateResolver.errors.patch.requiredPlaceholderRemoved",
|
|
99
|
-
params: { names: removed },
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return writeTemplateRow({
|
|
104
|
-
ctx,
|
|
105
|
-
user: event.user,
|
|
106
|
-
tenantId: event.user.tenantId,
|
|
107
|
-
scope: "tenant",
|
|
108
|
-
status: "draft",
|
|
109
|
-
variables: resolveTemplateVariables(ctx.registry, slug, applied.variableSchema),
|
|
110
|
-
payload: {
|
|
111
|
-
slug,
|
|
112
|
-
kind,
|
|
113
|
-
locale,
|
|
114
|
-
content,
|
|
115
|
-
contentFormat: "html",
|
|
116
|
-
variableSchema: applied.variableSchema,
|
|
117
|
-
meta: applied.meta,
|
|
118
|
-
title: loaded.title,
|
|
119
|
-
folder: loaded.folder,
|
|
120
|
-
parentTemplateId: loaded.parentTemplateId,
|
|
121
|
-
},
|
|
122
|
-
expectedVersion: loaded.base.scope === "tenant" ? loaded.base.version : null,
|
|
123
|
-
});
|
|
124
|
-
},
|
|
125
|
-
});
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -4,55 +4,68 @@ import { NotFoundError, ValidationError, writeFailure } from "@cosmicdrift/kumik
|
|
|
4
4
|
import * as z from "zod";
|
|
5
5
|
import type { TemplateResourceRow } from "../table.ts";
|
|
6
6
|
import { templateResourcesTable } from "../table.ts";
|
|
7
|
+
import {
|
|
8
|
+
refuseMissingTemplate,
|
|
9
|
+
refuseTemplateEdit,
|
|
10
|
+
type TemplateEditPolicy,
|
|
11
|
+
templateEditAccess,
|
|
12
|
+
} from "../template-edit-policy.ts";
|
|
7
13
|
import { executor } from "./shared.ts";
|
|
8
14
|
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
15
|
+
export function makeResetToSystemWrite(policy: TemplateEditPolicy) {
|
|
16
|
+
return defineWriteHandler({
|
|
17
|
+
name: "reset-to-system",
|
|
18
|
+
schema: z.object({ id: z.string().min(1) }),
|
|
19
|
+
access: templateEditAccess(policy.tenantEditable),
|
|
20
|
+
description:
|
|
21
|
+
"Retires a tenant override by archiving it so the resolver serves the active system template again; the override and its content survive and can be published again. Refused when there is no active system template to fall back to. Outside the templates the app lists in tenantEditable only a SystemAdmin may use it.",
|
|
22
|
+
handler: async (event, ctx) => {
|
|
23
|
+
const existing = await fetchOne<TemplateResourceRow>(ctx.db, templateResourcesTable, {
|
|
24
|
+
id: event.payload.id,
|
|
25
|
+
});
|
|
26
|
+
if (!existing || existing.tenantId !== event.user.tenantId || existing.scope !== "tenant") {
|
|
27
|
+
return (
|
|
28
|
+
refuseMissingTemplate(policy, event.user) ??
|
|
29
|
+
writeFailure(new NotFoundError("template-resource", event.payload.id))
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
const refused = refuseTemplateEdit(policy, event.user, existing);
|
|
33
|
+
if (refused !== null) return refused;
|
|
34
|
+
const systemRow = await fetchOne<TemplateResourceRow>(ctx.db, templateResourcesTable, {
|
|
35
|
+
tenantId: SYSTEM_TENANT_ID,
|
|
36
|
+
slug: existing.slug,
|
|
37
|
+
kind: existing.kind,
|
|
38
|
+
locale: existing.locale,
|
|
39
|
+
status: "active",
|
|
40
|
+
});
|
|
41
|
+
if (!systemRow) {
|
|
42
|
+
return writeFailure(
|
|
43
|
+
new ValidationError({
|
|
44
|
+
fields: [
|
|
45
|
+
{
|
|
46
|
+
path: "id",
|
|
47
|
+
code: "no_system_template",
|
|
48
|
+
i18nKey: "templateResolver.errors.noSystemTemplate",
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
const response = {
|
|
55
|
+
isSuccess: true as const,
|
|
56
|
+
data: {
|
|
57
|
+
id: String(existing.id),
|
|
58
|
+
status: "archived" as const,
|
|
59
|
+
systemTemplateId: String(systemRow.id),
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
if (existing.status === "archived") return response;
|
|
63
|
+
const result = await executor.update(
|
|
64
|
+
{ id: existing.id, version: existing.version, changes: { status: "archived" } },
|
|
65
|
+
event.user,
|
|
66
|
+
ctx.db,
|
|
40
67
|
);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
id: String(existing.id),
|
|
46
|
-
status: "archived" as const,
|
|
47
|
-
systemTemplateId: String(systemRow.id),
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
if (existing.status === "archived") return response;
|
|
51
|
-
const result = await executor.update(
|
|
52
|
-
{ id: existing.id, version: existing.version, changes: { status: "archived" } },
|
|
53
|
-
event.user,
|
|
54
|
-
ctx.db,
|
|
55
|
-
);
|
|
56
|
-
return result.isSuccess ? response : result;
|
|
57
|
-
},
|
|
58
|
-
});
|
|
68
|
+
return result.isSuccess ? response : result;
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -1,34 +1,49 @@
|
|
|
1
1
|
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
2
|
import * as z from "zod";
|
|
3
3
|
import { linkTemplatePdfAttachment } from "../store-template-resource.ts";
|
|
4
|
+
import {
|
|
5
|
+
refuseTemplateEditById,
|
|
6
|
+
type TemplateEditPolicy,
|
|
7
|
+
templateEditAccess,
|
|
8
|
+
} from "../template-edit-policy.ts";
|
|
4
9
|
import { resourceKeySchema, resourceTargetTenantId } from "./resource-shared.ts";
|
|
5
10
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
11
|
+
export function makeResourceLinkPdfWrite(policy: TemplateEditPolicy) {
|
|
12
|
+
return defineWriteHandler({
|
|
13
|
+
name: "resource-link-pdf",
|
|
14
|
+
schema: z.object({
|
|
15
|
+
templateId: z.string().min(1),
|
|
16
|
+
key: resourceKeySchema,
|
|
17
|
+
/** Slug of a document-pdf template of the tenant or the system. */
|
|
18
|
+
templateSlug: z.string().min(1),
|
|
19
|
+
/** Name pattern of the attached PDF; may hold {{placeholders}}, must end with .pdf. */
|
|
20
|
+
fileName: z.string().min(1),
|
|
21
|
+
/** Cross-tenant target — SystemAdmin only. */
|
|
22
|
+
tenantIdOverride: z.string().min(1).optional(),
|
|
23
|
+
}),
|
|
24
|
+
access: templateEditAccess(policy.tenantEditable),
|
|
25
|
+
description:
|
|
26
|
+
"Links a PDF rendered from a document-pdf template into a mail template under the key. Every mail sent from the template then carries that PDF, rendered at send time with the mail's own variables and locale; nothing is uploaded or stored. fileName is a pattern that may use the mail's {{placeholders}} and must end with .pdf. The document template must exist for the tenant or the system. Counts toward the 10 attachments per mail template together with uploaded files. Outside the templates the app lists in tenantEditable only a SystemAdmin may use it.",
|
|
27
|
+
handler: async (event, ctx) => {
|
|
28
|
+
const { templateId, key, templateSlug, fileName } = event.payload;
|
|
29
|
+
const tenantId = resourceTargetTenantId(event.payload.tenantIdOverride, event.user.tenantId);
|
|
30
|
+
const refused = await refuseTemplateEditById(
|
|
31
|
+
ctx.db,
|
|
32
|
+
policy,
|
|
33
|
+
event.user,
|
|
34
|
+
tenantId,
|
|
35
|
+
templateId,
|
|
36
|
+
);
|
|
37
|
+
if (refused !== null) return refused;
|
|
38
|
+
return linkTemplatePdfAttachment({
|
|
39
|
+
ctx,
|
|
40
|
+
user: event.user,
|
|
41
|
+
tenantId,
|
|
42
|
+
templateId,
|
|
43
|
+
key,
|
|
44
|
+
templateSlug,
|
|
45
|
+
fileName,
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|