@cosmicdrift/kumiko-samples 0.363.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 +7 -1
- package/packages/bundled-features/src/audit/changes.json +18 -0
- package/packages/bundled-features/src/audit/feature.ts +1 -1
- package/packages/bundled-features/src/audit/handlers/list.query.ts +10 -5
- package/packages/bundled-features/src/audit/run-escape-hatch-retention.ts +63 -11
- package/packages/bundled-features/src/auth-email-password/bootstrap.ts +30 -11
- package/packages/bundled-features/src/auth-email-password/changes.json +55 -0
- package/packages/bundled-features/src/auth-email-password/email-rate-limits.ts +16 -0
- package/packages/bundled-features/src/auth-email-password/feature.ts +4 -7
- package/packages/bundled-features/src/auth-email-password/handlers/change-password.write.ts +6 -2
- 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/logout.write.ts +7 -2
- package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +2 -1
- 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/handlers/token-request-handler.ts +2 -1
- 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/auth-email-password/web/auth-gate.tsx +24 -5
- package/packages/bundled-features/src/billing-foundation/changes.json +48 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +57 -5
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +22 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +4 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/register-termination.ts +11 -1
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-mail.ts +9 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +41 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +18 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +35 -19
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +9 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +35 -0
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +11 -3
- package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +10 -4
- package/packages/bundled-features/src/billing-foundation/i18n.ts +9 -0
- package/packages/bundled-features/src/billing-foundation/subscription-tier-sync.ts +27 -7
- package/packages/bundled-features/src/billing-foundation/types.ts +7 -2
- package/packages/bundled-features/src/cap-counter/book-cap-usage.ts +20 -8
- package/packages/bundled-features/src/cap-counter/calendar-cap-usage-reservation.ts +2 -0
- package/packages/bundled-features/src/cap-counter/cap-reservation.ts +7 -2
- package/packages/bundled-features/src/cap-counter/changes.json +19 -0
- package/packages/bundled-features/src/cap-counter/enforce-cap.ts +75 -5
- package/packages/bundled-features/src/cap-counter/handlers/increment-rolling.write.ts +2 -0
- package/packages/bundled-features/src/cap-counter/index.ts +1 -0
- package/packages/bundled-features/src/cap-counter/with-cap-enforcement.ts +3 -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 +47 -24
- 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/config/changes.json +6 -0
- package/packages/bundled-features/src/config/handlers/set.write.ts +2 -2
- package/packages/bundled-features/src/config/resolver.ts +15 -12
- package/packages/bundled-features/src/config/write-helpers.ts +23 -0
- package/packages/bundled-features/src/crypto-shredding/changes.json +6 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +54 -27
- package/packages/bundled-features/src/custom-fields/changes.json +6 -0
- package/packages/bundled-features/src/custom-fields/db/queries/retention.ts +2 -2
- package/packages/bundled-features/src/custom-fields/handlers/update-tenant-field.write.ts +0 -1
- package/packages/bundled-features/src/data-retention/run-retention-cleanup.ts +4 -5
- package/packages/bundled-features/src/delivery/attachment-ref.ts +39 -1
- package/packages/bundled-features/src/delivery/changes.json +30 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +31 -8
- package/packages/bundled-features/src/delivery/delivery-service.ts +93 -92
- package/packages/bundled-features/src/delivery/index.ts +2 -0
- package/packages/bundled-features/src/delivery/jobs.ts +51 -14
- package/packages/bundled-features/src/delivery/types.ts +7 -1
- 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/file-derivatives/handlers/public-variant.query.ts +1 -1
- package/packages/bundled-features/src/file-foundation/changes.json +6 -0
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +6 -3
- package/packages/bundled-features/src/files-tenant-data/changes.json +6 -0
- package/packages/bundled-features/src/files-tenant-data/hooks.ts +4 -0
- 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/inbound-mail-foundation/changes.json +6 -0
- package/packages/bundled-features/src/inbound-mail-foundation/oauth-access-token.ts +13 -8
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +3 -1
- package/packages/bundled-features/src/jobs/changes.json +31 -0
- package/packages/bundled-features/src/jobs/db/queries/stale-run-sweep.ts +12 -4
- package/packages/bundled-features/src/jobs/feature.ts +7 -4
- package/packages/bundled-features/src/jobs/handlers/list.query.ts +3 -1
- package/packages/bundled-features/src/jobs/handlers/stale-run-sweep.job.ts +5 -2
- package/packages/bundled-features/src/jobs/job-run-logger.ts +56 -18
- package/packages/bundled-features/src/jobs/tenant-job-run-table.ts +7 -2
- package/packages/bundled-features/src/ledger/changes.json +7 -0
- package/packages/bundled-features/src/ledger/entity.ts +3 -1
- 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/renderer-foundation/types.ts +2 -0
- package/packages/bundled-features/src/renderer-pdf/changes.json +12 -0
- package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +162 -82
- package/packages/bundled-features/src/secrets/changes.json +12 -0
- package/packages/bundled-features/src/secrets/feature.ts +4 -3
- package/packages/bundled-features/src/secrets/handlers/delete.write.ts +3 -1
- package/packages/bundled-features/src/secrets/secrets-context.ts +23 -3
- package/packages/bundled-features/src/secrets/write-gate.ts +8 -2
- package/packages/bundled-features/src/shared/changes.json +6 -0
- package/packages/bundled-features/src/shared/chunked-entity-migration.ts +4 -1
- package/packages/bundled-features/src/shared/index.ts +8 -0
- package/packages/bundled-features/src/shared/mail-template-body.ts +17 -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/subscription-stripe/changes.json +6 -0
- package/packages/bundled-features/src/subscription-stripe/plugin-methods.ts +19 -11
- package/packages/bundled-features/src/subscription-stripe/verify-webhook.ts +3 -1
- package/packages/bundled-features/src/template-resolver/README.md +4 -2
- package/packages/bundled-features/src/template-resolver/api.ts +3 -2
- package/packages/bundled-features/src/template-resolver/changes.json +149 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +148 -18
- package/packages/bundled-features/src/template-resolver/compile-steps-block.ts +68 -0
- package/packages/bundled-features/src/template-resolver/constants.ts +23 -3
- package/packages/bundled-features/src/template-resolver/document-sanitizer.ts +3 -0
- package/packages/bundled-features/src/template-resolver/document-schema.ts +33 -5
- package/packages/bundled-features/src/template-resolver/document-steps.ts +114 -0
- package/packages/bundled-features/src/template-resolver/feature.ts +16 -1
- 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 +121 -53
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +6 -44
- package/packages/bundled-features/src/template-resolver/handlers/discard-draft.write.ts +52 -0
- package/packages/bundled-features/src/template-resolver/handlers/export-bundle.query.ts +56 -1
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +6 -3
- 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/layout-source.query.ts +73 -30
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +14 -11
- package/packages/bundled-features/src/template-resolver/handlers/override-diff.ts +49 -0
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +102 -19
- package/packages/bundled-features/src/template-resolver/handlers/resource-upload.write.ts +49 -10
- package/packages/bundled-features/src/template-resolver/handlers/send-test.write.ts +39 -10
- package/packages/bundled-features/src/template-resolver/handlers/template-gates.ts +14 -2
- package/packages/bundled-features/src/template-resolver/handlers/template-write.ts +54 -4
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +96 -14
- package/packages/bundled-features/src/template-resolver/handlers/toggle-status.write.ts +51 -24
- package/packages/bundled-features/src/template-resolver/handlers/upsert-tenant.write.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +16 -2
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +4 -1
- package/packages/bundled-features/src/template-resolver/index.ts +18 -2
- package/packages/bundled-features/src/template-resolver/layout-appearance.ts +56 -0
- package/packages/bundled-features/src/template-resolver/mail-constants.ts +4 -0
- package/packages/bundled-features/src/template-resolver/pdf-constants.ts +12 -0
- package/packages/bundled-features/src/template-resolver/qualified-names.ts +4 -0
- package/packages/bundled-features/src/template-resolver/resource-image.ts +9 -16
- package/packages/bundled-features/src/template-resolver/seeding.ts +27 -4
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +390 -124
- package/packages/bundled-features/src/template-resolver/table.ts +13 -1
- package/packages/bundled-features/src/template-resolver/template-bundle.ts +4 -0
- package/packages/bundled-features/src/template-resolver/template-draft.ts +106 -0
- package/packages/bundled-features/src/template-resolver/template-meta.ts +95 -24
- package/packages/bundled-features/src/template-resolver/template-preview.ts +160 -57
- package/packages/bundled-features/src/template-resolver/template-resources.ts +3 -4
- package/packages/bundled-features/src/template-resolver/template-usage.ts +31 -11
- package/packages/bundled-features/src/template-resolver/template-validation.ts +16 -9
- package/packages/bundled-features/src/template-resolver/web/client-plugin.tsx +12 -8
- package/packages/bundled-features/src/template-resolver/web/document-editor-extensions.ts +97 -10
- package/packages/bundled-features/src/template-resolver/web/document-editor.tsx +15 -5
- package/packages/bundled-features/src/template-resolver/web/document-tiptap-editor.tsx +405 -145
- package/packages/bundled-features/src/template-resolver/web/document-toolbar-blocks.ts +17 -3
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +32 -4
- package/packages/bundled-features/src/template-resolver/web/index.ts +8 -0
- package/packages/bundled-features/src/template-resolver/web/placeholder-line-editor.tsx +23 -7
- 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 +18 -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/feature.ts +11 -3
- 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/changes.json +6 -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/resolve-display-names.ts +8 -15
- package/packages/bundled-features/src/user/seeding.ts +19 -5
- package/packages/bundled-features/src/user-data-rights/changes.json +12 -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/user-profile/changes.json +1 -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/waitlist/options.ts +1 -1
- package/packages/bundled-features/src/workflow-runner/changes.json +6 -0
- package/packages/bundled-features/src/workflow-runner/event-trigger.ts +2 -1
- package/packages/bundled-features/src/workflow-runner/feature.ts +1 -1
- package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +7 -1
- package/samples/apps/ui-walkthrough/package.json +9 -0
- package/samples/apps/ui-walkthrough/playwright.config.ts +40 -7
- package/samples/apps/ui-walkthrough/src/app/seed-templates.ts +87 -2
- package/samples/apps/ui-walkthrough/src/app/template-seed-data.ts +246 -30
- package/samples/apps/ui-walkthrough/src/features/billing/feature.ts +4 -0
- package/samples/apps/ui-walkthrough/src/run-config.ts +18 -1
- package/samples/apps/use-all-bundled/README.md +3 -13
- package/samples/apps/use-all-bundled/feature-manifest.json +7 -5
- 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, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
2
|
import type {
|
|
3
|
+
BeforeTransactionResult,
|
|
3
4
|
ConfigAccessor,
|
|
4
5
|
HandlerContext,
|
|
5
6
|
Registry,
|
|
@@ -12,11 +13,15 @@ import {
|
|
|
12
13
|
UnprocessableError,
|
|
13
14
|
writeFailure,
|
|
14
15
|
} from "@cosmicdrift/kumiko-framework/errors";
|
|
15
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
type CreateFileRefInput,
|
|
18
|
+
createFileRefFromUploadedBytes,
|
|
19
|
+
type UploadedFileBytes,
|
|
20
|
+
uploadFileBytesBeforeTransaction,
|
|
21
|
+
} from "@cosmicdrift/kumiko-framework/files";
|
|
16
22
|
import { fileContextForTenant } from "../file-foundation/index.js";
|
|
17
|
-
import
|
|
18
|
-
import {
|
|
19
|
-
import { SYSTEM_TENANT_ID } from "./constants.js";
|
|
23
|
+
import { extensionForResourceContentType, requireForTenant } from "../shared/index.js";
|
|
24
|
+
import { ACTIVE_TEMPLATE_STATUS, SYSTEM_TENANT_ID } from "./constants.js";
|
|
20
25
|
import {
|
|
21
26
|
DEFAULT_RESOURCE_MAX_BYTES,
|
|
22
27
|
MAX_FILE_ATTACHMENTS_PER_TEMPLATE,
|
|
@@ -32,13 +37,7 @@ import {
|
|
|
32
37
|
import { TemplateResolverErrors } from "./qualified-names.js";
|
|
33
38
|
import { normalizeResourceImage } from "./resource-image.js";
|
|
34
39
|
import { type TemplateResourceRow, templateResourcesTable } from "./table.js";
|
|
35
|
-
|
|
36
|
-
const EXTENSION_BY_CONTENT_TYPE: Readonly<Record<string, string>> = {
|
|
37
|
-
"image/png": "png",
|
|
38
|
-
"image/jpeg": "jpg",
|
|
39
|
-
"image/gif": "gif",
|
|
40
|
-
"image/webp": "webp",
|
|
41
|
-
};
|
|
40
|
+
import { readTemplateResource } from "./template-resources.js";
|
|
42
41
|
|
|
43
42
|
export async function readTemplateResourceMaxBytes(
|
|
44
43
|
config: ConfigAccessor | undefined,
|
|
@@ -60,82 +59,311 @@ export type StoredTemplateResource = {
|
|
|
60
59
|
readonly size: number;
|
|
61
60
|
};
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
export type PreparedTemplateImage = {
|
|
63
|
+
readonly kind: "image";
|
|
64
|
+
readonly contentType: string;
|
|
65
|
+
readonly width: number;
|
|
66
|
+
readonly height: number;
|
|
67
|
+
readonly size: number;
|
|
68
|
+
readonly upload: UploadedFileBytes;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export type PreparedTemplateAttachment = {
|
|
72
|
+
readonly kind: "attachment";
|
|
73
|
+
readonly contentType: string;
|
|
74
|
+
readonly size: number;
|
|
75
|
+
readonly fileName: string;
|
|
76
|
+
readonly upload: UploadedFileBytes;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type PreparedTemplateFile = PreparedTemplateImage | PreparedTemplateAttachment;
|
|
80
|
+
|
|
81
|
+
function keyInUse(): WriteResult<never> {
|
|
82
|
+
return writeFailure(
|
|
83
|
+
new UnprocessableError(TemplateResolverErrors.attachmentInvalid, {
|
|
84
|
+
i18nKey: "templateResolver.errors.attachment.invalid",
|
|
85
|
+
details: { code: "key_in_use" },
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isAttachmentKey(links: Readonly<Record<string, LinkedResourceEntry>>, key: string) {
|
|
91
|
+
const occupant = links[key];
|
|
92
|
+
return occupant !== undefined && typeof occupant !== "string";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// The upload goes to the target tenant's storage and the release checks that tenant's fileRef rows,
|
|
96
|
+
// so both receive the target tenant's db and identity.
|
|
97
|
+
function uploadToTenant(
|
|
98
|
+
args: {
|
|
99
|
+
readonly ctx: HandlerContext;
|
|
100
|
+
readonly user: SessionUser;
|
|
101
|
+
readonly tenantId: TenantId;
|
|
102
|
+
},
|
|
103
|
+
input: CreateFileRefInput,
|
|
104
|
+
): Promise<BeforeTransactionResult<UploadedFileBytes>> {
|
|
105
|
+
const { ctx, user, tenantId } = args;
|
|
106
|
+
return uploadFileBytesBeforeTransaction(
|
|
107
|
+
{
|
|
108
|
+
db: requireForTenant(ctx, tenantId).db,
|
|
109
|
+
files: fileContextForTenant(ctx, ctx.user.tenantId, tenantId),
|
|
110
|
+
registry: ctx.registry,
|
|
111
|
+
user: { ...user, tenantId },
|
|
112
|
+
},
|
|
113
|
+
input,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
type TenantScope = {
|
|
118
|
+
readonly ctx: HandlerContext;
|
|
119
|
+
readonly user: SessionUser;
|
|
120
|
+
readonly tenantId: TenantId;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
function createFileRefFromPrepared(scope: TenantScope, upload: UploadedFileBytes) {
|
|
124
|
+
const { ctx, user, tenantId } = scope;
|
|
125
|
+
return createFileRefFromUploadedBytes(
|
|
126
|
+
{ db: requireForTenant(ctx, tenantId).db, user: { ...user, tenantId } },
|
|
127
|
+
upload,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function linkImage(args: {
|
|
66
132
|
readonly ctx: HandlerContext;
|
|
67
133
|
readonly user: SessionUser;
|
|
68
134
|
readonly tenantId: TenantId;
|
|
69
|
-
readonly
|
|
70
|
-
readonly
|
|
71
|
-
readonly
|
|
72
|
-
readonly
|
|
73
|
-
}): Promise<WriteResult<
|
|
74
|
-
const { ctx, user, tenantId,
|
|
135
|
+
readonly row: TemplateResourceRow;
|
|
136
|
+
readonly key: string;
|
|
137
|
+
readonly links: Readonly<Record<string, LinkedResourceEntry>>;
|
|
138
|
+
readonly fileRefId: string;
|
|
139
|
+
}): Promise<WriteResult<null>> {
|
|
140
|
+
const { ctx, user, tenantId, row, key, links, fileRefId } = args;
|
|
141
|
+
const { db, streamTenantId } = requireForTenant(ctx, tenantId);
|
|
142
|
+
// The replaced FileRef stays: copy-locale and tenant overrides can share ids and
|
|
143
|
+
// there is no reference count to tell when the last user is gone.
|
|
144
|
+
const updated = await executor.update(
|
|
145
|
+
{
|
|
146
|
+
id: row.id,
|
|
147
|
+
version: row.version,
|
|
148
|
+
changes: { linkedResources: JSON.stringify({ ...links, [key]: fileRefId }) },
|
|
149
|
+
},
|
|
150
|
+
user,
|
|
151
|
+
db,
|
|
152
|
+
{ streamTenantId },
|
|
153
|
+
);
|
|
154
|
+
return updated.isSuccess ? { isSuccess: true, data: null } : updated;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
type NormalizedUpload = {
|
|
158
|
+
readonly image: Awaited<ReturnType<typeof normalizeResourceImage>>;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
async function normalizeUpload(
|
|
162
|
+
bytes: Uint8Array,
|
|
163
|
+
maxBytes: number,
|
|
164
|
+
): Promise<{ readonly failure: WriteResult<never> } | NormalizedUpload> {
|
|
75
165
|
if (bytes.length > maxBytes) {
|
|
76
|
-
return
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
166
|
+
return {
|
|
167
|
+
failure: writeFailure(
|
|
168
|
+
new UnprocessableError(TemplateResolverErrors.resourceTooLarge, {
|
|
169
|
+
i18nKey: "templateResolver.errors.resource.tooLarge",
|
|
170
|
+
details: { maxBytes, size: bytes.length },
|
|
171
|
+
}),
|
|
172
|
+
),
|
|
173
|
+
};
|
|
82
174
|
}
|
|
83
|
-
let image: Awaited<ReturnType<typeof normalizeResourceImage>>;
|
|
84
175
|
try {
|
|
85
|
-
image
|
|
176
|
+
return { image: await normalizeResourceImage(Buffer.from(bytes)) };
|
|
86
177
|
} catch (cause) {
|
|
87
|
-
return
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
178
|
+
return {
|
|
179
|
+
failure: writeFailure(
|
|
180
|
+
new UnprocessableError(TemplateResolverErrors.resourceUnsupportedImage, {
|
|
181
|
+
i18nKey: "templateResolver.errors.resource.unsupportedImage",
|
|
182
|
+
cause: cause instanceof Error ? cause : undefined,
|
|
183
|
+
}),
|
|
184
|
+
),
|
|
185
|
+
};
|
|
93
186
|
}
|
|
187
|
+
}
|
|
94
188
|
|
|
95
|
-
|
|
189
|
+
type ImageTarget = {
|
|
190
|
+
readonly row: TemplateResourceRow | undefined;
|
|
191
|
+
readonly links: Readonly<Record<string, LinkedResourceEntry>>;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Runs before the upload (reject early, store nothing) and again in the transaction on the fresh row.
|
|
195
|
+
async function checkImageTarget(args: {
|
|
196
|
+
readonly ctx: HandlerContext;
|
|
197
|
+
readonly tenantId: TenantId;
|
|
198
|
+
readonly templateId?: string | undefined;
|
|
199
|
+
readonly key?: string | undefined;
|
|
200
|
+
}): Promise<WriteResult<ImageTarget>> {
|
|
201
|
+
const { ctx, tenantId, templateId, key } = args;
|
|
96
202
|
let row: TemplateResourceRow | undefined;
|
|
97
203
|
if (templateId !== undefined) {
|
|
98
|
-
row = await fetchOne<TemplateResourceRow>(
|
|
99
|
-
tenantId,
|
|
100
|
-
|
|
101
|
-
|
|
204
|
+
row = await fetchOne<TemplateResourceRow>(
|
|
205
|
+
requireForTenant(ctx, tenantId).db,
|
|
206
|
+
templateResourcesTable,
|
|
207
|
+
{ tenantId, id: templateId },
|
|
208
|
+
);
|
|
102
209
|
if (row === undefined) return writeFailure(new NotFoundError("template-resource", templateId));
|
|
103
210
|
}
|
|
211
|
+
const links = row === undefined ? {} : parseLinkedResourceEntries(row.linkedResources);
|
|
212
|
+
if (key !== undefined && isAttachmentKey(links, key)) return keyInUse();
|
|
213
|
+
return { isSuccess: true, data: { row, links } };
|
|
214
|
+
}
|
|
104
215
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
216
|
+
// beforeTransaction half: normalizes the image and uploads it to the target tenant's storage.
|
|
217
|
+
// The caller is responsible for authorization; the function trusts tenantId.
|
|
218
|
+
export async function prepareTemplateResourceBeforeTransaction(args: {
|
|
219
|
+
readonly ctx: HandlerContext;
|
|
220
|
+
readonly user: SessionUser;
|
|
221
|
+
readonly tenantId: TenantId;
|
|
222
|
+
readonly bytes: Uint8Array;
|
|
223
|
+
readonly maxBytes: number;
|
|
224
|
+
readonly templateId?: string;
|
|
225
|
+
readonly key?: string;
|
|
226
|
+
}): Promise<BeforeTransactionResult<PreparedTemplateImage>> {
|
|
227
|
+
const { ctx, user, tenantId, bytes, maxBytes, templateId, key } = args;
|
|
228
|
+
const normalized = await normalizeUpload(bytes, maxBytes);
|
|
229
|
+
if ("failure" in normalized) return normalized.failure;
|
|
230
|
+
const { image } = normalized;
|
|
231
|
+
|
|
232
|
+
const target = await checkImageTarget({ ctx, tenantId, templateId, key });
|
|
233
|
+
if (!target.isSuccess) return target;
|
|
234
|
+
|
|
235
|
+
const uploaded = await uploadToTenant(
|
|
236
|
+
{ ctx, user, tenantId },
|
|
112
237
|
{
|
|
113
238
|
// Neutral name: the source's file name may be personal data.
|
|
114
|
-
fileName: `${key ?? "resource"}.${
|
|
239
|
+
fileName: `${key ?? "resource"}.${extensionForResourceContentType(image.contentType) ?? "bin"}`,
|
|
115
240
|
mimeType: image.contentType,
|
|
116
241
|
data: image.data,
|
|
117
242
|
},
|
|
118
243
|
);
|
|
244
|
+
if (!uploaded.isSuccess) return uploaded;
|
|
245
|
+
return {
|
|
246
|
+
isSuccess: true,
|
|
247
|
+
data: {
|
|
248
|
+
kind: "image",
|
|
249
|
+
contentType: image.contentType,
|
|
250
|
+
width: image.width,
|
|
251
|
+
height: image.height,
|
|
252
|
+
size: image.data.length,
|
|
253
|
+
upload: uploaded.data,
|
|
254
|
+
},
|
|
255
|
+
...(uploaded.release && { release: uploaded.release }),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export type Release = () => Promise<void>;
|
|
260
|
+
|
|
261
|
+
// Tries every release, so one failing delete does not strand the other uploads; the first rejection is rethrown.
|
|
262
|
+
export async function releaseAll(releases: readonly Release[]): Promise<void> {
|
|
263
|
+
const settled = await Promise.allSettled(releases.map((release) => release()));
|
|
264
|
+
const rejected = settled.find((outcome) => outcome.status === "rejected");
|
|
265
|
+
if (rejected?.status === "rejected") throw rejected.reason;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// `upload` is absent for a rendered-PDF link, which carries no file.
|
|
269
|
+
export type PreparedLinkedResourceCopy = {
|
|
270
|
+
readonly key: string;
|
|
271
|
+
readonly entry: LinkedResourceEntry;
|
|
272
|
+
readonly upload?: UploadedFileBytes;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// beforeTransaction half: copies the file behind every entry into the target tenant's storage, so a
|
|
276
|
+
// template copied from the system default keeps working once the tenant exports it or measures its
|
|
277
|
+
// attachments. An entry whose source file is gone is left out: it could not be resolved at render
|
|
278
|
+
// time either. A failed or throwing upload releases the ones before it.
|
|
279
|
+
export async function prepareLinkedResourceCopiesBeforeTransaction(args: {
|
|
280
|
+
readonly ctx: HandlerContext;
|
|
281
|
+
readonly user: SessionUser;
|
|
282
|
+
readonly tenantId: TenantId;
|
|
283
|
+
readonly resources: Readonly<Record<string, LinkedResourceEntry>>;
|
|
284
|
+
}): Promise<BeforeTransactionResult<readonly PreparedLinkedResourceCopy[]>> {
|
|
285
|
+
const { ctx, user, tenantId, resources } = args;
|
|
286
|
+
const copies: PreparedLinkedResourceCopy[] = [];
|
|
287
|
+
const releases: Release[] = [];
|
|
288
|
+
try {
|
|
289
|
+
for (const [key, entry] of Object.entries(resources)) {
|
|
290
|
+
if (typeof entry !== "string" && entry.kind === "rendered-pdf") {
|
|
291
|
+
copies.push({ key, entry });
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const file = await readTemplateResource(
|
|
295
|
+
ctx,
|
|
296
|
+
typeof entry === "string" ? entry : entry.fileRefId,
|
|
297
|
+
);
|
|
298
|
+
if (file === null) continue;
|
|
299
|
+
const uploaded = await uploadToTenant(
|
|
300
|
+
{ ctx, user, tenantId },
|
|
301
|
+
{ fileName: file.fileName, mimeType: file.contentType, data: file.bytes },
|
|
302
|
+
);
|
|
303
|
+
if (!uploaded.isSuccess) {
|
|
304
|
+
await releaseAll(releases);
|
|
305
|
+
return uploaded;
|
|
306
|
+
}
|
|
307
|
+
if (uploaded.release) releases.push(uploaded.release);
|
|
308
|
+
copies.push({ key, entry, upload: uploaded.data });
|
|
309
|
+
}
|
|
310
|
+
} catch (cause) {
|
|
311
|
+
await releaseAll(releases);
|
|
312
|
+
throw cause;
|
|
313
|
+
}
|
|
314
|
+
return { isSuccess: true, data: copies, release: () => releaseAll(releases) };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Transaction half: creates the target tenant's fileRef rows and returns the entries pointing at them.
|
|
318
|
+
export async function createLinkedResourceCopies(
|
|
319
|
+
scope: TenantScope,
|
|
320
|
+
prepared: readonly PreparedLinkedResourceCopy[],
|
|
321
|
+
): Promise<WriteResult<Record<string, LinkedResourceEntry>>> {
|
|
322
|
+
const linked: Record<string, LinkedResourceEntry> = {};
|
|
323
|
+
for (const { key, entry, upload } of prepared) {
|
|
324
|
+
if (typeof entry !== "string" && entry.kind === "rendered-pdf") {
|
|
325
|
+
linked[key] = entry;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
// skip: a file entry without a copy must not carry the source tenant's fileRefId over
|
|
329
|
+
if (upload === undefined) continue;
|
|
330
|
+
const created = await createFileRefFromPrepared(scope, upload);
|
|
331
|
+
if (!created.isSuccess) return created;
|
|
332
|
+
linked[key] =
|
|
333
|
+
typeof entry === "string" ? created.data.id : { ...entry, fileRefId: created.data.id };
|
|
334
|
+
}
|
|
335
|
+
return { isSuccess: true, data: linked };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Transaction half: creates the fileRef row for the prepared upload and, with templateId + key,
|
|
339
|
+
// links it into the target tenant's template.
|
|
340
|
+
export async function storePreparedTemplateResource(args: {
|
|
341
|
+
readonly ctx: HandlerContext;
|
|
342
|
+
readonly user: SessionUser;
|
|
343
|
+
readonly tenantId: TenantId;
|
|
344
|
+
readonly prepared: PreparedTemplateImage;
|
|
345
|
+
readonly templateId?: string;
|
|
346
|
+
readonly key?: string;
|
|
347
|
+
}): Promise<WriteResult<StoredTemplateResource>> {
|
|
348
|
+
const { ctx, user, tenantId, prepared, templateId, key } = args;
|
|
349
|
+
const target = await checkImageTarget({ ctx, tenantId, templateId, key });
|
|
350
|
+
if (!target.isSuccess) return target;
|
|
351
|
+
const { row, links } = target.data;
|
|
352
|
+
|
|
353
|
+
const created = await createFileRefFromPrepared({ ctx, user, tenantId }, prepared.upload);
|
|
119
354
|
if (!created.isSuccess) return created;
|
|
120
355
|
|
|
121
356
|
if (row !== undefined && key !== undefined) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const linkedResources = {
|
|
125
|
-
...parseLinkedResourceEntries(row.linkedResources),
|
|
126
|
-
[key]: created.data.id,
|
|
127
|
-
};
|
|
128
|
-
const updated = await executor.update(
|
|
129
|
-
{
|
|
130
|
-
id: row.id,
|
|
131
|
-
version: row.version,
|
|
132
|
-
changes: { linkedResources: JSON.stringify(linkedResources) },
|
|
133
|
-
},
|
|
357
|
+
const linked = await linkImage({
|
|
358
|
+
ctx,
|
|
134
359
|
user,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
360
|
+
tenantId,
|
|
361
|
+
row,
|
|
362
|
+
key,
|
|
363
|
+
links,
|
|
364
|
+
fileRefId: created.data.id,
|
|
365
|
+
});
|
|
366
|
+
if (!linked.isSuccess) return linked;
|
|
139
367
|
}
|
|
140
368
|
|
|
141
369
|
return {
|
|
@@ -143,10 +371,10 @@ export async function storeTemplateResource(args: {
|
|
|
143
371
|
data: {
|
|
144
372
|
fileRefId: created.data.id,
|
|
145
373
|
...(key !== undefined && { key }),
|
|
146
|
-
contentType:
|
|
147
|
-
width:
|
|
148
|
-
height:
|
|
149
|
-
size:
|
|
374
|
+
contentType: prepared.contentType,
|
|
375
|
+
width: prepared.width,
|
|
376
|
+
height: prepared.height,
|
|
377
|
+
size: prepared.size,
|
|
150
378
|
},
|
|
151
379
|
};
|
|
152
380
|
}
|
|
@@ -228,36 +456,39 @@ function inspectAttachmentBytes(
|
|
|
228
456
|
return { type };
|
|
229
457
|
}
|
|
230
458
|
|
|
231
|
-
|
|
232
|
-
|
|
459
|
+
type AttachmentTarget = {
|
|
460
|
+
readonly row: TemplateResourceRow;
|
|
461
|
+
readonly linked: Readonly<Record<string, LinkedResourceEntry>>;
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// Runs before the upload (reject early, store nothing) and again in the transaction on the fresh row.
|
|
465
|
+
async function checkAttachmentTarget(args: {
|
|
233
466
|
readonly ctx: HandlerContext;
|
|
234
|
-
readonly user: SessionUser;
|
|
235
467
|
readonly tenantId: TenantId;
|
|
236
|
-
readonly
|
|
468
|
+
readonly templateId: string;
|
|
237
469
|
readonly key: string;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
{
|
|
244
|
-
db,
|
|
245
|
-
files: fileContextForTenant(ctx, tenantId),
|
|
246
|
-
registry: ctx.registry,
|
|
247
|
-
user: { ...user, tenantId },
|
|
248
|
-
},
|
|
249
|
-
{
|
|
250
|
-
// Neutral stored name: the admin's file name may be personal data and lives in the link entry.
|
|
251
|
-
fileName: `${key}.${type.extension}`,
|
|
252
|
-
mimeType: type.contentType,
|
|
253
|
-
data: bytes,
|
|
254
|
-
},
|
|
470
|
+
}): Promise<WriteResult<AttachmentTarget>> {
|
|
471
|
+
const { ctx, tenantId, templateId, key } = args;
|
|
472
|
+
const row = await fetchOne<TemplateResourceRow>(
|
|
473
|
+
requireForTenant(ctx, tenantId).db,
|
|
474
|
+
templateResourcesTable,
|
|
475
|
+
{ tenantId, id: templateId },
|
|
255
476
|
);
|
|
256
|
-
|
|
477
|
+
if (row === undefined) return writeFailure(new NotFoundError("template-resource", templateId));
|
|
478
|
+
if (row.kind !== "mail") return attachmentNeedsMailTemplate();
|
|
479
|
+
|
|
480
|
+
const linked = parseLinkedResourceEntries(row.linkedResources);
|
|
481
|
+
if (otherAttachmentCount(linked, key) >= MAX_FILE_ATTACHMENTS_PER_TEMPLATE) {
|
|
482
|
+
return attachmentLimitFailure();
|
|
483
|
+
}
|
|
484
|
+
const occupant = linked[key];
|
|
485
|
+
if (occupant !== undefined && typeof occupant === "string") return keyInUse();
|
|
486
|
+
return { isSuccess: true, data: { row, linked } };
|
|
257
487
|
}
|
|
258
488
|
|
|
259
|
-
//
|
|
260
|
-
|
|
489
|
+
// beforeTransaction half: validates a PDF/PNG/JPEG and uploads it unchanged to the target tenant's
|
|
490
|
+
// storage. With templateId the template is checked first; a bundle import has none yet.
|
|
491
|
+
export async function prepareTemplateAttachmentBeforeTransaction(args: {
|
|
261
492
|
readonly ctx: HandlerContext;
|
|
262
493
|
readonly user: SessionUser;
|
|
263
494
|
readonly tenantId: TenantId;
|
|
@@ -265,51 +496,81 @@ export async function storeBundleAttachment(args: {
|
|
|
265
496
|
readonly maxBytes: number;
|
|
266
497
|
readonly key: string;
|
|
267
498
|
readonly fileName: string;
|
|
268
|
-
|
|
269
|
-
|
|
499
|
+
readonly templateId?: string;
|
|
500
|
+
}): Promise<BeforeTransactionResult<PreparedTemplateAttachment>> {
|
|
501
|
+
const { ctx, user, tenantId, bytes, maxBytes, key, fileName, templateId } = args;
|
|
502
|
+
const inspected = inspectAttachmentBytes(bytes, maxBytes);
|
|
270
503
|
if ("failure" in inspected) return inspected.failure;
|
|
271
|
-
const
|
|
504
|
+
const { type } = inspected;
|
|
505
|
+
|
|
506
|
+
if (templateId !== undefined) {
|
|
507
|
+
const target = await checkAttachmentTarget({ ctx, tenantId, templateId, key });
|
|
508
|
+
if (!target.isSuccess) return target;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const uploaded = await uploadToTenant(
|
|
512
|
+
{ ctx, user, tenantId },
|
|
513
|
+
{
|
|
514
|
+
// Neutral stored name: the admin's file name may be personal data and lives in the link entry.
|
|
515
|
+
fileName: `${key}.${type.extension}`,
|
|
516
|
+
mimeType: type.contentType,
|
|
517
|
+
data: bytes,
|
|
518
|
+
},
|
|
519
|
+
);
|
|
520
|
+
if (!uploaded.isSuccess) return uploaded;
|
|
521
|
+
return {
|
|
522
|
+
isSuccess: true,
|
|
523
|
+
data: {
|
|
524
|
+
kind: "attachment",
|
|
525
|
+
contentType: type.contentType,
|
|
526
|
+
size: bytes.length,
|
|
527
|
+
fileName,
|
|
528
|
+
upload: uploaded.data,
|
|
529
|
+
},
|
|
530
|
+
...(uploaded.release && { release: uploaded.release }),
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Creates the fileRef rows for a bundle's prepared uploads; the importer links the entries.
|
|
535
|
+
export async function createPreparedBundleResourceEntry(
|
|
536
|
+
scope: TenantScope,
|
|
537
|
+
prepared: PreparedTemplateFile,
|
|
538
|
+
): Promise<WriteResult<LinkedResourceEntry>> {
|
|
539
|
+
const created = await createFileRefFromPrepared(scope, prepared.upload);
|
|
272
540
|
if (!created.isSuccess) return created;
|
|
273
541
|
return {
|
|
274
542
|
isSuccess: true,
|
|
275
|
-
data:
|
|
543
|
+
data:
|
|
544
|
+
prepared.kind === "image"
|
|
545
|
+
? created.data.id
|
|
546
|
+
: { kind: "file", fileRefId: created.data.id, fileName: prepared.fileName },
|
|
276
547
|
};
|
|
277
548
|
}
|
|
278
549
|
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
export async function
|
|
550
|
+
// Transaction half: creates the fileRef row for the prepared upload and links it under `key` in a
|
|
551
|
+
// mail template of the target tenant.
|
|
552
|
+
export async function storePreparedTemplateAttachment(args: {
|
|
282
553
|
readonly ctx: HandlerContext;
|
|
283
554
|
readonly user: SessionUser;
|
|
284
555
|
readonly tenantId: TenantId;
|
|
285
|
-
readonly
|
|
286
|
-
readonly maxBytes: number;
|
|
556
|
+
readonly prepared: PreparedTemplateAttachment;
|
|
287
557
|
readonly templateId: string;
|
|
288
558
|
readonly key: string;
|
|
289
|
-
readonly fileName: string;
|
|
290
559
|
}): Promise<WriteResult<StoredTemplateAttachment>> {
|
|
291
|
-
const { ctx, user, tenantId,
|
|
292
|
-
const
|
|
293
|
-
if (
|
|
294
|
-
const {
|
|
560
|
+
const { ctx, user, tenantId, prepared, templateId, key } = args;
|
|
561
|
+
const target = await checkAttachmentTarget({ ctx, tenantId, templateId, key });
|
|
562
|
+
if (!target.isSuccess) return target;
|
|
563
|
+
const { row, linked } = target.data;
|
|
295
564
|
|
|
296
|
-
const {
|
|
297
|
-
const row = await fetchOne<TemplateResourceRow>(db, templateResourcesTable, {
|
|
298
|
-
tenantId,
|
|
299
|
-
id: templateId,
|
|
300
|
-
});
|
|
301
|
-
if (row === undefined) return writeFailure(new NotFoundError("template-resource", templateId));
|
|
302
|
-
if (row.kind !== "mail") return attachmentNeedsMailTemplate();
|
|
303
|
-
|
|
304
|
-
const linked = parseLinkedResourceEntries(row.linkedResources);
|
|
305
|
-
if (otherAttachmentCount(linked, key) >= MAX_FILE_ATTACHMENTS_PER_TEMPLATE) {
|
|
306
|
-
return attachmentLimitFailure();
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const created = await createAttachmentFileRef({ ctx, user, tenantId, bytes, key, type });
|
|
565
|
+
const created = await createFileRefFromPrepared({ ctx, user, tenantId }, prepared.upload);
|
|
310
566
|
if (!created.isSuccess) return created;
|
|
311
567
|
|
|
312
|
-
const entry: LinkedResourceEntry = {
|
|
568
|
+
const entry: LinkedResourceEntry = {
|
|
569
|
+
kind: "file",
|
|
570
|
+
fileRefId: created.data.id,
|
|
571
|
+
fileName: prepared.fileName,
|
|
572
|
+
};
|
|
573
|
+
const { db, streamTenantId } = requireForTenant(ctx, tenantId);
|
|
313
574
|
const updated = await executor.update(
|
|
314
575
|
{
|
|
315
576
|
id: row.id,
|
|
@@ -327,9 +588,9 @@ export async function storeTemplateAttachment(args: {
|
|
|
327
588
|
data: {
|
|
328
589
|
fileRefId: created.data.id,
|
|
329
590
|
key,
|
|
330
|
-
fileName,
|
|
331
|
-
contentType:
|
|
332
|
-
size:
|
|
591
|
+
fileName: prepared.fileName,
|
|
592
|
+
contentType: prepared.contentType,
|
|
593
|
+
size: prepared.size,
|
|
333
594
|
},
|
|
334
595
|
};
|
|
335
596
|
}
|
|
@@ -384,7 +645,12 @@ export async function linkTemplatePdfAttachment(args: {
|
|
|
384
645
|
const visibleTemplates = await selectMany<TemplateResourceRow>(
|
|
385
646
|
db,
|
|
386
647
|
templateResourcesTable,
|
|
387
|
-
{
|
|
648
|
+
{
|
|
649
|
+
tenantId: [tenantId, SYSTEM_TENANT_ID],
|
|
650
|
+
slug: link.data.templateSlug,
|
|
651
|
+
kind: "document-pdf",
|
|
652
|
+
status: ACTIVE_TEMPLATE_STATUS,
|
|
653
|
+
},
|
|
388
654
|
{ limit: 1 },
|
|
389
655
|
);
|
|
390
656
|
if (visibleTemplates.length === 0) {
|
|
@@ -2,8 +2,10 @@ import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
|
|
|
2
2
|
import {
|
|
3
3
|
createEntity,
|
|
4
4
|
createLongTextField,
|
|
5
|
+
createNumberField,
|
|
5
6
|
createSelectField,
|
|
6
7
|
createTextField,
|
|
8
|
+
createTimestampField,
|
|
7
9
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
8
10
|
import type { EntityEventMigration } from "@cosmicdrift/kumiko-types/fields";
|
|
9
11
|
import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
@@ -24,7 +26,9 @@ const renameMailHtmlKind: EntityEventMigration = {
|
|
|
24
26
|
// TemplateResource — strukturierte Template-Definition mit Tenant-
|
|
25
27
|
// Override-Hierarchie, Locale-Fallback und Resource-Linking via
|
|
26
28
|
// file-foundation. Pro (tenantId, slug, kind, locale) genau eine Row;
|
|
27
|
-
// scope/status/version differenzieren Lifecycle.
|
|
29
|
+
// scope/status/version differenzieren Lifecycle. Der Entwurf neben einer
|
|
30
|
+
// aktiven Fassung liegt in `draft`; die Hauptfelder bleiben die ausgelieferte
|
|
31
|
+
// Fassung.
|
|
28
32
|
//
|
|
29
33
|
// `variableSchema` + `linkedResources` sind JSON-Strings in longText
|
|
30
34
|
// (Pattern aus compliance-profiles — kein dedizierter jsonbField im
|
|
@@ -58,6 +62,11 @@ export const templateResourceEntity = createEntity({
|
|
|
58
62
|
scope: createSelectField({ required: true, options: [...TEMPLATE_SCOPES] }),
|
|
59
63
|
parentTemplateId: createTextField({ personal: false, reason: "technical_reference" }),
|
|
60
64
|
status: createSelectField({ required: true, options: [...TEMPLATE_STATUSES] }),
|
|
65
|
+
// JSON of the unpublished edit beside the active version (see template-draft.ts).
|
|
66
|
+
draft: createLongTextField({ personal: false, reason: "is_business_data" }),
|
|
67
|
+
// `version` counts every save including drafts; these two describe the served version.
|
|
68
|
+
activeVersion: createNumberField({ integer: true, min: 1 }),
|
|
69
|
+
activeSince: createTimestampField({}),
|
|
61
70
|
},
|
|
62
71
|
indexes: [
|
|
63
72
|
{
|
|
@@ -101,4 +110,7 @@ export type TemplateResourceRow = {
|
|
|
101
110
|
readonly scope: string;
|
|
102
111
|
readonly parentTemplateId: string | null;
|
|
103
112
|
readonly status: string;
|
|
113
|
+
readonly draft: string | null;
|
|
114
|
+
readonly activeVersion: number | null;
|
|
115
|
+
readonly activeSince: Temporal.Instant | null;
|
|
104
116
|
} & EntityAuditColumns;
|
|
@@ -5,6 +5,7 @@ import { templatePdfAttachmentLinkSchema } from "./linked-resources.js";
|
|
|
5
5
|
import { templateSlugSchema } from "./template-meta.js";
|
|
6
6
|
|
|
7
7
|
const MAX_RESOURCES = 20;
|
|
8
|
+
const MAX_LOCALES = 50;
|
|
8
9
|
const MAX_RESOURCE_BYTES = 2 * 1024 * 1024;
|
|
9
10
|
const MAX_BASE64_LENGTH = Math.ceil(MAX_RESOURCE_BYTES / 3) * 4;
|
|
10
11
|
|
|
@@ -52,6 +53,9 @@ export const templateBundleSchema = z
|
|
|
52
53
|
.record(bundleLocaleSchema, bundleLocaleEntrySchema)
|
|
53
54
|
.refine((locales) => Object.keys(locales).length >= 1, {
|
|
54
55
|
message: "a bundle needs at least one locale",
|
|
56
|
+
})
|
|
57
|
+
.refine((locales) => Object.keys(locales).length <= MAX_LOCALES, {
|
|
58
|
+
message: `a bundle holds at most ${MAX_LOCALES} locales`,
|
|
55
59
|
}),
|
|
56
60
|
resources: z
|
|
57
61
|
.record(
|