@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
|
// @runtime client
|
|
2
2
|
import type { ContentEditorProps } from "@cosmicdrift/kumiko-renderer";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
3
4
|
|
|
4
5
|
export type DocumentToolbarBlock =
|
|
5
6
|
| "image"
|
|
@@ -8,7 +9,8 @@ export type DocumentToolbarBlock =
|
|
|
8
9
|
| "divider"
|
|
9
10
|
| "repeat"
|
|
10
11
|
| "pageBreak"
|
|
11
|
-
| "align"
|
|
12
|
+
| "align"
|
|
13
|
+
| "steps";
|
|
12
14
|
|
|
13
15
|
/** Imperative access for an outside panel (placeholder list, resource list). */
|
|
14
16
|
export type DocumentEditorHandle = {
|
|
@@ -39,8 +41,13 @@ export type DocumentEditorProps = ContentEditorProps & {
|
|
|
39
41
|
readonly chrome?: DocumentEditorChrome;
|
|
40
42
|
/** "compact" renders the placeholder and block buttons icon-only with a tooltip, for narrow columns. Default "full". */
|
|
41
43
|
readonly toolbar?: "full" | "compact";
|
|
42
|
-
/**
|
|
43
|
-
readonly
|
|
44
|
+
/** Canvas only: wraps the editable area in a caller-owned page (letterhead, margins) instead of the default mail paper. */
|
|
45
|
+
readonly renderPaper?: (editable: ReactNode) => ReactNode;
|
|
46
|
+
/** Canvas only: renders the toolbar (and its insert panel) into this element instead of above the editor;
|
|
47
|
+
* null renders none. Lets several region editors share one toolbar that follows the focused one. */
|
|
48
|
+
readonly toolbarHost?: HTMLElement | null;
|
|
49
|
+
/** Called with the handle once the editor exists and with null when it is destroyed (unmount or rebuild), so a held handle never outlives its editor. */
|
|
50
|
+
readonly onEditorReady?: (handle: DocumentEditorHandle | null) => void;
|
|
44
51
|
};
|
|
45
52
|
|
|
46
53
|
export const MAIL_DOCUMENT_BLOCKS = [
|
|
@@ -49,6 +56,7 @@ export const MAIL_DOCUMENT_BLOCKS = [
|
|
|
49
56
|
"button",
|
|
50
57
|
"divider",
|
|
51
58
|
"repeat",
|
|
59
|
+
"steps",
|
|
52
60
|
] as const satisfies readonly DocumentToolbarBlock[];
|
|
53
61
|
|
|
54
62
|
export const PDF_DOCUMENT_BLOCKS = [
|
|
@@ -57,5 +65,11 @@ export const PDF_DOCUMENT_BLOCKS = [
|
|
|
57
65
|
"table",
|
|
58
66
|
"divider",
|
|
59
67
|
"repeat",
|
|
68
|
+
"steps",
|
|
60
69
|
"pageBreak",
|
|
61
70
|
] as const satisfies readonly DocumentToolbarBlock[];
|
|
71
|
+
|
|
72
|
+
/** Block set for a template-resource kind; every kind except "document-pdf" gets the mail set. */
|
|
73
|
+
export function documentBlocksForKind(kind: string | undefined): readonly DocumentToolbarBlock[] {
|
|
74
|
+
return kind === "document-pdf" ? PDF_DOCUMENT_BLOCKS : MAIL_DOCUMENT_BLOCKS;
|
|
75
|
+
}
|
|
@@ -17,6 +17,7 @@ export const defaultTranslations: TranslationsByLocale = {
|
|
|
17
17
|
"template-resolver.documentEditor.divider": "Divider",
|
|
18
18
|
"template-resolver.documentEditor.heading3": "Heading 3",
|
|
19
19
|
"template-resolver.documentEditor.image": "Image",
|
|
20
|
+
"template-resolver.documentEditor.imageBlock": "Image · inline",
|
|
20
21
|
"template-resolver.documentEditor.imageAlt": "Alt text (optional)",
|
|
21
22
|
"template-resolver.documentEditor.insert": "Insert",
|
|
22
23
|
"template-resolver.documentEditor.insertPlaceholder": "Placeholder",
|
|
@@ -36,13 +37,19 @@ export const defaultTranslations: TranslationsByLocale = {
|
|
|
36
37
|
"template-resolver.documentEditor.alignJustify": "Justified",
|
|
37
38
|
"template-resolver.documentEditor.repeatBarList": "Repeat · {list}",
|
|
38
39
|
"template-resolver.documentEditor.repeatHeader": "Repeat header row",
|
|
39
|
-
"template-resolver.documentEditor.keepRowsTogether": "Keep rows together",
|
|
40
40
|
"template-resolver.documentEditor.paragraph": "Paragraph",
|
|
41
41
|
"template-resolver.documentEditor.paragraphStyle": "Paragraph style",
|
|
42
42
|
"template-resolver.documentEditor.placeholder": "Write your text…",
|
|
43
43
|
"template-resolver.documentEditor.removeLink": "Remove link",
|
|
44
44
|
"template-resolver.documentEditor.repeat": "Repeat",
|
|
45
45
|
"template-resolver.documentEditor.repeatList": "List to repeat",
|
|
46
|
+
"template-resolver.documentEditor.steps": "Steps",
|
|
47
|
+
"template-resolver.documentEditor.stepsVariable": "Current step from",
|
|
48
|
+
"template-resolver.documentEditor.stepsList": "Steps, separated by commas (value=label)",
|
|
49
|
+
"template-resolver.documentEditor.stepsDefault":
|
|
50
|
+
"reported=Reported, investigating=Investigating, resolved=Resolved",
|
|
51
|
+
"template-resolver.documentEditor.invalidSteps":
|
|
52
|
+
"Enter 1 to 8 different steps; values use letters, digits, - and _.",
|
|
46
53
|
"template-resolver.documentEditor.resourceKey": "Resource key",
|
|
47
54
|
"template-resolver.documentEditor.table": "Table",
|
|
48
55
|
"template-resolver.editor.titleLabel": "Title",
|
|
@@ -82,11 +89,31 @@ export const defaultTranslations: TranslationsByLocale = {
|
|
|
82
89
|
"templateResolver.errors.resource.unsupportedImage":
|
|
83
90
|
"This file is not a supported image. Use PNG, JPEG, WebP or GIF.",
|
|
84
91
|
"templateResolver.errors.template.systemResourcesNeedSystemTenant":
|
|
85
|
-
"Resource files can only be imported
|
|
92
|
+
"Resource files of system templates can only be imported or exported by a user of the system tenant.",
|
|
93
|
+
"templateResolver.errors.template.bundleLocalesDiffer":
|
|
94
|
+
"The locales of this template differ in content format or variables, so they cannot be exported as one bundle.",
|
|
86
95
|
"templateResolver.errors.collection.kindNotAllowed":
|
|
87
96
|
"“{detail}” templates are written through their own editor, not through a collection.",
|
|
88
97
|
"templateResolver.errors.noSystemTemplate": "There is no active system template to reset to.",
|
|
89
|
-
"templateResolver.errors.
|
|
98
|
+
"templateResolver.errors.noDraft": "There is no draft to discard.",
|
|
99
|
+
"templateResolver.errors.template.invalidMeta": "The template settings are invalid.",
|
|
100
|
+
"templateResolver.errors.template.meta.subject.required": "The subject is missing.",
|
|
101
|
+
"templateResolver.errors.template.meta.subject.invalid":
|
|
102
|
+
"The subject is invalid (at most 200 characters, no line breaks).",
|
|
103
|
+
"templateResolver.errors.template.meta.previewText.required": "The preview text is missing.",
|
|
104
|
+
"templateResolver.errors.template.meta.previewText.invalid":
|
|
105
|
+
"The preview text is invalid (at most 200 characters, no line breaks).",
|
|
106
|
+
"templateResolver.errors.template.meta.senderName.required": "The sender name is missing.",
|
|
107
|
+
"templateResolver.errors.template.meta.senderName.invalid":
|
|
108
|
+
"The sender name is invalid (at most 80 characters, no line breaks).",
|
|
109
|
+
"templateResolver.errors.template.meta.replyTo.required": "The reply-to address is missing.",
|
|
110
|
+
"templateResolver.errors.template.meta.replyTo.invalid":
|
|
111
|
+
"The reply-to address is not a valid email address.",
|
|
112
|
+
"templateResolver.errors.template.meta.layoutSlug.required": "The layout is missing.",
|
|
113
|
+
"templateResolver.errors.template.meta.layoutSlug.invalid": "The chosen layout is invalid.",
|
|
114
|
+
"templateResolver.errors.template.meta.fileName.required": "The file name is missing.",
|
|
115
|
+
"templateResolver.errors.template.meta.fileName.invalid":
|
|
116
|
+
"The file name is invalid: no slashes, quotes, semicolons or two dots in a row.",
|
|
90
117
|
"templateResolver.errors.template.unknownPlaceholders": "Unknown placeholders: {detail}",
|
|
91
118
|
"templateResolver.errors.template.missingRequiredPlaceholders":
|
|
92
119
|
"Required placeholders are not used: {detail}",
|
|
@@ -112,7 +139,8 @@ export const defaultTranslations: TranslationsByLocale = {
|
|
|
112
139
|
"There is no PDF template “{slug}” for this tenant or the system.",
|
|
113
140
|
"template-resolver.documentEditor.htmlSource": "HTML source",
|
|
114
141
|
"template-resolver.documentEditor.removeBlock": "Remove block",
|
|
115
|
-
"template-resolver.documentEditor.
|
|
142
|
+
"template-resolver.documentEditor.list": "List",
|
|
143
|
+
"template-resolver.documentEditor.more": "More",
|
|
116
144
|
"template-resolver.documentEditor.showHtml": "HTML",
|
|
117
145
|
"template-resolver.documentEditor.underline": "Underline",
|
|
118
146
|
"template-resolver.editor.saveFailed": "Save failed.",
|
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
// `@cosmicdrift/kumiko-bundled-features/template-resolver` and has no React or
|
|
6
6
|
// DOM deps.
|
|
7
7
|
|
|
8
|
+
export { MAIL_TONES, type MailTone } from "../mail-constants.js";
|
|
9
|
+
export {
|
|
10
|
+
PDF_DEFAULT_MARGIN_MM,
|
|
11
|
+
PDF_FOOTER_MARGIN_MM,
|
|
12
|
+
PDF_HEADER_MARGIN_MM,
|
|
13
|
+
PDF_ORIENTATIONS,
|
|
14
|
+
PDF_PAGE_FORMATS,
|
|
15
|
+
} from "../pdf-constants.js";
|
|
8
16
|
export {
|
|
9
17
|
TemplateResolverErrors,
|
|
10
18
|
TemplateResolverHandlers,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { cn } from "@cosmicdrift/kumiko-renderer-web";
|
|
8
8
|
import { Extension, Node } from "@tiptap/core";
|
|
9
|
-
import { UndoRedo } from "@tiptap/extensions";
|
|
9
|
+
import { Placeholder, UndoRedo } from "@tiptap/extensions";
|
|
10
10
|
import { EditorContent, useEditor } from "@tiptap/react";
|
|
11
11
|
import StarterKit from "@tiptap/starter-kit";
|
|
12
12
|
import { type ReactNode, useMemo, useState } from "react";
|
|
@@ -26,7 +26,11 @@ export type PlaceholderLineEditorProps = {
|
|
|
26
26
|
/** Placeholders shown as system chips (a different tint). */
|
|
27
27
|
readonly systemVariables?: readonly string[];
|
|
28
28
|
readonly readOnly?: boolean;
|
|
29
|
-
|
|
29
|
+
/** Drops the input frame so the line sits flat inside a label/value row. */
|
|
30
|
+
readonly flat?: boolean;
|
|
31
|
+
/** Hint shown while the line is empty. */
|
|
32
|
+
readonly placeholder?: string;
|
|
33
|
+
readonly onEditorReady?: (handle: DocumentEditorHandle | null) => void;
|
|
30
34
|
readonly "aria-label": string;
|
|
31
35
|
};
|
|
32
36
|
|
|
@@ -50,6 +54,8 @@ function LineEditorBody({
|
|
|
50
54
|
variableLabels,
|
|
51
55
|
systemVariables,
|
|
52
56
|
readOnly,
|
|
57
|
+
flat,
|
|
58
|
+
placeholder,
|
|
53
59
|
onEditorReady,
|
|
54
60
|
ariaLabel,
|
|
55
61
|
}: {
|
|
@@ -59,7 +65,9 @@ function LineEditorBody({
|
|
|
59
65
|
readonly variableLabels: Readonly<Record<string, string>>;
|
|
60
66
|
readonly systemVariables: readonly string[];
|
|
61
67
|
readonly readOnly: boolean;
|
|
62
|
-
readonly
|
|
68
|
+
readonly flat: boolean;
|
|
69
|
+
readonly placeholder: string | undefined;
|
|
70
|
+
readonly onEditorReady: ((handle: DocumentEditorHandle | null) => void) | undefined;
|
|
63
71
|
readonly ariaLabel: string;
|
|
64
72
|
}): ReactNode {
|
|
65
73
|
const extensions = useMemo(
|
|
@@ -88,12 +96,13 @@ function LineEditorBody({
|
|
|
88
96
|
undoRedo: false,
|
|
89
97
|
}),
|
|
90
98
|
DocumentVariable.extend({
|
|
91
|
-
addNodeView: () => createVariableView(variableLabels, systemVariables),
|
|
99
|
+
addNodeView: () => createVariableView(variableLabels, systemVariables, flat),
|
|
92
100
|
}),
|
|
93
101
|
UndoRedo,
|
|
94
102
|
BlockLineBreaks,
|
|
103
|
+
...(placeholder === undefined ? [] : [Placeholder.configure({ placeholder })]),
|
|
95
104
|
],
|
|
96
|
-
[variableLabels, systemVariables],
|
|
105
|
+
[variableLabels, systemVariables, flat, placeholder],
|
|
97
106
|
);
|
|
98
107
|
|
|
99
108
|
const editor = useEditor({
|
|
@@ -111,6 +120,7 @@ function LineEditorBody({
|
|
|
111
120
|
transformPastedHTML: (html) => singleLine(html.replace(/<[^>]*>/g, " ")),
|
|
112
121
|
},
|
|
113
122
|
onCreate: ({ editor: created }) => onEditorReady?.(createEditorHandle(created)),
|
|
123
|
+
onDestroy: () => onEditorReady?.(null),
|
|
114
124
|
onUpdate: ({ editor: e }) => onChange(contentToLine(e.getJSON().content?.[0]?.content)),
|
|
115
125
|
});
|
|
116
126
|
|
|
@@ -118,9 +128,11 @@ function LineEditorBody({
|
|
|
118
128
|
<EditorContent
|
|
119
129
|
editor={editor}
|
|
120
130
|
className={cn(
|
|
121
|
-
"flex
|
|
131
|
+
"flex items-center bg-transparent text-base md:text-sm",
|
|
132
|
+
flat ? "min-h-10" : "min-h-9 rounded-md border border-input px-3",
|
|
122
133
|
"[&_.ProseMirror]:w-full [&_.ProseMirror]:whitespace-nowrap [&_.ProseMirror]:outline-none",
|
|
123
134
|
"[&_p]:m-0",
|
|
135
|
+
"[&_.is-editor-empty]:before:pointer-events-none [&_.is-editor-empty]:before:float-left [&_.is-editor-empty]:before:h-0 [&_.is-editor-empty]:before:text-muted-foreground [&_.is-editor-empty]:before:content-[attr(data-placeholder)]",
|
|
124
136
|
readOnly && "opacity-60",
|
|
125
137
|
)}
|
|
126
138
|
/>
|
|
@@ -134,6 +146,8 @@ export default function PlaceholderLineEditor({
|
|
|
134
146
|
variableLabels = NO_LABELS,
|
|
135
147
|
systemVariables = NO_NAMES,
|
|
136
148
|
readOnly = false,
|
|
149
|
+
flat = false,
|
|
150
|
+
placeholder,
|
|
137
151
|
onEditorReady,
|
|
138
152
|
"aria-label": ariaLabel,
|
|
139
153
|
}: PlaceholderLineEditorProps): ReactNode {
|
|
@@ -152,7 +166,7 @@ export default function PlaceholderLineEditor({
|
|
|
152
166
|
|
|
153
167
|
return (
|
|
154
168
|
<LineEditorBody
|
|
155
|
-
key={`${epoch}-${readOnly}-${JSON.stringify(stableLabels)}`}
|
|
169
|
+
key={`${epoch}-${readOnly}-${JSON.stringify(stableLabels)}-${stableSystem.join("\n")}`}
|
|
156
170
|
id={id}
|
|
157
171
|
initialValue={value}
|
|
158
172
|
onChange={(next) => {
|
|
@@ -162,6 +176,8 @@ export default function PlaceholderLineEditor({
|
|
|
162
176
|
variableLabels={stableLabels}
|
|
163
177
|
systemVariables={stableSystem}
|
|
164
178
|
readOnly={readOnly}
|
|
179
|
+
flat={flat}
|
|
180
|
+
placeholder={placeholder}
|
|
165
181
|
onEditorReady={onEditorReady}
|
|
166
182
|
ariaLabel={ariaLabel}
|
|
167
183
|
/>
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.365.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Post-save seed hooks use the after-commit notify factory",
|
|
6
|
+
"detail": "afterCommit seed hooks now get a notify function bound to the signup dispatcher's job runner when the dispatcher provides one, falling back to the regular factory. The afterCommit hook context is built only when the hook actually runs."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.357.0",
|
|
4
10
|
"type": "improvement",
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
mayOverrideTenant,
|
|
10
10
|
mergeAssignedRoles,
|
|
11
11
|
parseTenantId,
|
|
12
|
-
type SessionUser,
|
|
13
12
|
SYSTEM_ROLE,
|
|
14
13
|
type TenantId,
|
|
15
14
|
withResponseData,
|
|
@@ -50,10 +49,6 @@ function resolveTargetTenantId(
|
|
|
50
49
|
return parseTenantId(payloadTenantId) ?? undefined;
|
|
51
50
|
}
|
|
52
51
|
|
|
53
|
-
function foreignStreamTenant(user: SessionUser, targetTenantId: TenantId): TenantId | undefined {
|
|
54
|
-
return targetTenantId === user.tenantId ? undefined : targetTenantId;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
52
|
// Editing your OWN roles keeps the current session: roles are re-derived per
|
|
58
53
|
// request, so the new roles apply at once without a re-login. Other sessions
|
|
59
54
|
// of that user and any foreign edit still revoke all.
|
|
@@ -195,8 +190,8 @@ export const updateMemberRolesWrite = defineWriteHandler({
|
|
|
195
190
|
}
|
|
196
191
|
|
|
197
192
|
// The membership stream lives on the membership's tenant (where seedTenantMembership/
|
|
198
|
-
// invite-accept wrote it);
|
|
199
|
-
//
|
|
193
|
+
// invite-accept wrote it); streamTenantId addresses it with the operator as actor, also
|
|
194
|
+
// without membership there.
|
|
200
195
|
const result = await executor.update(
|
|
201
196
|
{
|
|
202
197
|
id: row["id"] as string, // @cast-boundary db-row
|
|
@@ -205,7 +200,7 @@ export const updateMemberRolesWrite = defineWriteHandler({
|
|
|
205
200
|
},
|
|
206
201
|
event.user,
|
|
207
202
|
db,
|
|
208
|
-
{ streamTenantId:
|
|
203
|
+
{ streamTenantId: targetTenantId },
|
|
209
204
|
);
|
|
210
205
|
|
|
211
206
|
return withResponseData(result, {
|
|
@@ -101,26 +101,6 @@ export type SeedTenantHooks = {
|
|
|
101
101
|
readonly context: AppContext;
|
|
102
102
|
};
|
|
103
103
|
|
|
104
|
-
// Shared by seedTenant/seedTenantMembership/auth-email-password's
|
|
105
|
-
// seedUserWithPassword: fires the same entity-scoped postSave hooks the
|
|
106
|
-
// real dispatcher would (registry.getEntityPostSaveHooks by entityName),
|
|
107
|
-
// just without the full HTTP/JWT roundtrip a fixture doesn't need. Fresh
|
|
108
|
-
// lifecycle-hooks instance per call, no systemHooks — only feature-
|
|
109
|
-
// registered entity/handler hooks fire (search-index/SSE system hooks are
|
|
110
|
-
// wired at server-boot and out of reach here).
|
|
111
|
-
//
|
|
112
|
-
// Tenant re-scope (#1566): when `targetTenantId` is set we rebuild the
|
|
113
|
-
// tenant-bound pieces of the context (`_tenantId`, `db`, `notify`,
|
|
114
|
-
// `hasFeature`, `user.tenantId`) — a shallow `{ ...ctx, _tenantId }` left
|
|
115
|
-
// closures over the caller's tenant (self-signup is anonymous/system).
|
|
116
|
-
// `query`/`write` bridges still share the caller's identity; hooks that
|
|
117
|
-
// need a full identity switch use `writeAs`/`queryAs`.
|
|
118
|
-
//
|
|
119
|
-
// afterCommit (#1566): when `hooks.context.scheduleAfterCommit` is present
|
|
120
|
-
// (HandlerContext from a live write), afterCommit hooks are queued onto the
|
|
121
|
-
// dispatcher's sink and flush post-commit. Without a sink (fixture / plain
|
|
122
|
-
// Connection) they still fire immediately — there is no outer TX to wait for.
|
|
123
|
-
//
|
|
124
104
|
function buildSeedHookNotify(
|
|
125
105
|
context: AppContext,
|
|
126
106
|
scopedUser: SessionUser,
|
|
@@ -129,7 +109,11 @@ function buildSeedHookNotify(
|
|
|
129
109
|
): AppContext["notify"] {
|
|
130
110
|
const notifyFactory = context._notifyFactory;
|
|
131
111
|
if (!notifyFactory) return context.notify;
|
|
132
|
-
|
|
112
|
+
const notifyAfterCommit = (context._notifyAfterCommitFactory ?? notifyFactory)(
|
|
113
|
+
scopedUser,
|
|
114
|
+
targetTenantId,
|
|
115
|
+
);
|
|
116
|
+
if (notifyTiming === "inline") return notifyAfterCommit;
|
|
133
117
|
// Still inside the caller's write tx: delivery DB work must not wait for an app-pool slot.
|
|
134
118
|
const notifyInsideTransaction = (context._notifyInTransactionFactory ?? notifyFactory)(
|
|
135
119
|
scopedUser,
|
|
@@ -140,12 +124,31 @@ function buildSeedHookNotify(
|
|
|
140
124
|
return createDeferringNotify({
|
|
141
125
|
registry: context.registry,
|
|
142
126
|
tenantId: targetTenantId,
|
|
143
|
-
notifyAfterCommit
|
|
127
|
+
notifyAfterCommit,
|
|
144
128
|
notifyNow: notifyInsideTransaction,
|
|
145
129
|
scheduleAfterCommit,
|
|
146
130
|
});
|
|
147
131
|
}
|
|
148
132
|
|
|
133
|
+
// Shared by seedTenant/seedTenantMembership/auth-email-password's
|
|
134
|
+
// seedUserWithPassword: fires the same entity-scoped postSave hooks the
|
|
135
|
+
// real dispatcher would (registry.getEntityPostSaveHooks by entityName),
|
|
136
|
+
// just without the full HTTP/JWT roundtrip a fixture doesn't need. Fresh
|
|
137
|
+
// lifecycle-hooks instance per call, no systemHooks — only feature-
|
|
138
|
+
// registered entity/handler hooks fire (search-index/SSE system hooks are
|
|
139
|
+
// wired at server-boot and out of reach here).
|
|
140
|
+
//
|
|
141
|
+
// Tenant re-scope (#1566): when `targetTenantId` is set we rebuild the
|
|
142
|
+
// tenant-bound pieces of the context (`_tenantId`, `db`, `notify`,
|
|
143
|
+
// `hasFeature`, `user.tenantId`) — a shallow `{ ...ctx, _tenantId }` left
|
|
144
|
+
// closures over the caller's tenant (self-signup is anonymous/system).
|
|
145
|
+
// `query`/`write` bridges still share the caller's identity; hooks that
|
|
146
|
+
// need a full identity switch use `writeAs`/`queryAs`.
|
|
147
|
+
//
|
|
148
|
+
// afterCommit (#1566): when `hooks.context.scheduleAfterCommit` is present
|
|
149
|
+
// (HandlerContext from a live write), afterCommit hooks are queued onto the
|
|
150
|
+
// dispatcher's sink and flush post-commit. Without a sink (fixture / plain
|
|
151
|
+
// Connection) they still fire immediately — there is no outer TX to wait for.
|
|
149
152
|
// `runner` comes from the caller, which already holds a declared DbRunner —
|
|
150
153
|
// a TenantDb exposes no runner back out to feature code.
|
|
151
154
|
function scopeSeedHookContext(
|
|
@@ -219,13 +222,16 @@ export async function fireEntityPostSave(
|
|
|
219
222
|
target === undefined
|
|
220
223
|
? hooks.context
|
|
221
224
|
: scopeSeedHookContext(hooks.context, target.tenantId, target.db, "deferUntilCommit");
|
|
222
|
-
const afterCommitContext =
|
|
223
|
-
target === undefined
|
|
224
|
-
? hooks.context
|
|
225
|
-
: scopeSeedHookContext(hooks.context, target.tenantId, target.db, "inline");
|
|
226
225
|
await lifecycle.runPostSave(pseudoType, entityData, scopedContext, HookPhases.inTransaction);
|
|
227
226
|
const runAfterCommit = () =>
|
|
228
|
-
lifecycle.runPostSave(
|
|
227
|
+
lifecycle.runPostSave(
|
|
228
|
+
pseudoType,
|
|
229
|
+
entityData,
|
|
230
|
+
target === undefined
|
|
231
|
+
? hooks.context
|
|
232
|
+
: scopeSeedHookContext(hooks.context, target.tenantId, target.db, "inline"),
|
|
233
|
+
HookPhases.afterCommit,
|
|
234
|
+
);
|
|
229
235
|
if (scopedContext.scheduleAfterCommit) {
|
|
230
236
|
scopedContext.scheduleAfterCommit(runAfterCommit);
|
|
231
237
|
} else {
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
executeRawQuery,
|
|
29
29
|
extractTableName,
|
|
30
30
|
physicalColumnName,
|
|
31
|
+
toPostgresArrayLiteral,
|
|
31
32
|
} from "@cosmicdrift/kumiko-framework/db";
|
|
32
33
|
import { MAX_TRANSFER_DEPTH, type Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
33
34
|
import { UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
|
|
@@ -79,7 +80,12 @@ async function moveFileRefs(args: {
|
|
|
79
80
|
args.db,
|
|
80
81
|
`UPDATE ${FILE_REFS_TABLE} SET tenant_id = $1 ` +
|
|
81
82
|
`WHERE tenant_id = $2 AND entity_type = $3 AND entity_id = ANY($4) RETURNING id`,
|
|
82
|
-
[
|
|
83
|
+
[
|
|
84
|
+
args.destinationTenantId,
|
|
85
|
+
args.sourceTenantId,
|
|
86
|
+
args.entityType,
|
|
87
|
+
toPostgresArrayLiteral(args.entityIds),
|
|
88
|
+
],
|
|
83
89
|
);
|
|
84
90
|
const movedIds = rows.map((row) => row.id);
|
|
85
91
|
if (movedIds.length === 0) return 0;
|
|
@@ -126,7 +132,12 @@ async function moveChildRows(args: {
|
|
|
126
132
|
`UPDATE "${args.tableName}" SET "${args.tenantCol}" = $1 ` +
|
|
127
133
|
`WHERE "${args.typeCol}" = $2 AND "${args.idCol}" = ANY($3) AND "${args.tenantCol}" = $4 ` +
|
|
128
134
|
`RETURNING "${args.pkCol}" AS id`,
|
|
129
|
-
[
|
|
135
|
+
[
|
|
136
|
+
args.destinationTenantId,
|
|
137
|
+
args.parentEntityType,
|
|
138
|
+
toPostgresArrayLiteral(args.parentRowIds),
|
|
139
|
+
args.sourceTenantId,
|
|
140
|
+
],
|
|
130
141
|
);
|
|
131
142
|
return rows.map((row) => row.id);
|
|
132
143
|
}
|
|
@@ -149,7 +160,7 @@ async function moveReferencedChildRows(args: {
|
|
|
149
160
|
`UPDATE "${args.tableName}" SET "${args.tenantCol}" = $1 ` +
|
|
150
161
|
`WHERE "${args.refCol}" = ANY($2) AND "${args.tenantCol}" = $3 ` +
|
|
151
162
|
`RETURNING "${args.pkCol}" AS id`,
|
|
152
|
-
[args.destinationTenantId, args.parentRowIds, args.sourceTenantId],
|
|
163
|
+
[args.destinationTenantId, toPostgresArrayLiteral(args.parentRowIds), args.sourceTenantId],
|
|
153
164
|
);
|
|
154
165
|
return rows.map((row) => row.id);
|
|
155
166
|
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.365.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "One sweep tick shares a single stage budget between all destroying tenants",
|
|
6
|
+
"detail": "The destruction sweep used to give every destroying tenant its own full stage budget, so a few large tenants stretched one tick far beyond the minute cron window and the following ticks were skipped. The tick budget is now divided evenly over the destroying tenants. Each tenant's slice starts when that tenant's turn begins, so an overrun by one tenant never leaves the next ones with a zero slice. runNextDestructionStage accepts an optional deadlineAt."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.354.0",
|
|
4
10
|
"type": "improvement",
|
|
@@ -290,6 +290,7 @@ export async function runNextDestructionStage(args: {
|
|
|
290
290
|
readonly escapeHatchAuditSink?: EscapeHatchAuditSink;
|
|
291
291
|
readonly escapeHatchAuditLog?: Logger;
|
|
292
292
|
readonly actor?: string;
|
|
293
|
+
readonly deadlineAt?: number;
|
|
293
294
|
}): Promise<StageTickResult> {
|
|
294
295
|
const events = await loadAggregate(args.db, args.tenantId, args.tenantId);
|
|
295
296
|
const { completed, abandoned, attemptsByStage } = replayStageState(events);
|
|
@@ -316,7 +317,7 @@ export async function runNextDestructionStage(args: {
|
|
|
316
317
|
escapeHatchAuditSink: args.escapeHatchAuditSink,
|
|
317
318
|
escapeHatchAuditLog: args.escapeHatchAuditLog,
|
|
318
319
|
actor: args.actor,
|
|
319
|
-
deadlineAt: Date.now() + DESTRUCTION_STAGE_TICK_BUDGET_MS,
|
|
320
|
+
deadlineAt: args.deadlineAt ?? Date.now() + DESTRUCTION_STAGE_TICK_BUDGET_MS,
|
|
320
321
|
};
|
|
321
322
|
|
|
322
323
|
let version = lastEventVersion(events);
|
|
@@ -456,9 +457,16 @@ export async function runTenantDestructionSweep(args: {
|
|
|
456
457
|
|
|
457
458
|
let advanced = 0;
|
|
458
459
|
const destroying = await selectMany<{ id: string }>(args.db, tenantTable, destroyingWhere);
|
|
460
|
+
// One budget per sweep tick, split evenly so a large tenant cannot eat the
|
|
461
|
+
// window and starve the ones behind it. Each slice starts when its tenant
|
|
462
|
+
// starts: anchoring to a shared tick deadline would hand tenants after an
|
|
463
|
+
// overrun a zero slice, and a no-progress stage counts as a failed attempt.
|
|
464
|
+
const sliceMs = DESTRUCTION_STAGE_TICK_BUDGET_MS / Math.max(1, destroying.length);
|
|
459
465
|
for (const row of destroying) {
|
|
466
|
+
const tenantDeadlineAt = Date.now() + sliceMs;
|
|
460
467
|
try {
|
|
461
468
|
const result = await runNextDestructionStage({
|
|
469
|
+
deadlineAt: tenantDeadlineAt,
|
|
462
470
|
db: args.db,
|
|
463
471
|
registry: args.registry,
|
|
464
472
|
tenantId: row.id as TenantId,
|
|
@@ -88,8 +88,9 @@ const tenantMembershipCrud = createEventStoreExecutor(
|
|
|
88
88
|
},
|
|
89
89
|
);
|
|
90
90
|
|
|
91
|
-
// Every hook
|
|
92
|
-
//
|
|
91
|
+
// Every hook runs each tick (they are idempotent) but all share one ctx.deadlineAt: an
|
|
92
|
+
// earlier hook that keeps reporting done:false can use up the budget, so later chunking
|
|
93
|
+
// hooks time out with nothing processed until it finishes.
|
|
93
94
|
function newOutcomeAccumulator(): {
|
|
94
95
|
add: (result: TenantDestroyHookResult | void) => void;
|
|
95
96
|
outcome: () => StageRunOutcome;
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.365.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "set-tenant-tier accepts an optional source so a manual grant can return to billing control",
|
|
6
|
+
"detail": "The handler payload takes source (manual, billing or default, default manual). Writing the row with source billing makes the subscription tier sync treat it as billing-driven again, so the next webhook overwrites the tier. The handler stays SystemAdmin only."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.365.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "tier-assignment source is a select field of TierAssignmentSources",
|
|
12
|
+
"detail": "The source column keeps its text storage, but the entity now validates it against manual, billing and default, so a typo can no longer bypass the manual-grant protection of the billing sync. TierAssignmentSource and isTierAssignmentSource are exported; the billing sync narrows the stored value with the guard."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.364.0",
|
|
16
|
+
"type": "fix",
|
|
17
|
+
"title": "Tier cache: stale single reload cannot overwrite a newer full resync",
|
|
18
|
+
"detail": "A single-tenant reload that started before a full resync and resolved after it could restore an outdated tier. The resync now advances the per-tenant ticket for deleted and adopted tenants too, and assignment changes are published remote-only."
|
|
19
|
+
},
|
|
2
20
|
{
|
|
3
21
|
"version": "0.354.0",
|
|
4
22
|
"type": "fix",
|
|
@@ -22,6 +22,15 @@ export const TierAssignmentSources = {
|
|
|
22
22
|
default: "default",
|
|
23
23
|
} as const;
|
|
24
24
|
|
|
25
|
+
export type TierAssignmentSource =
|
|
26
|
+
(typeof TierAssignmentSources)[keyof typeof TierAssignmentSources];
|
|
27
|
+
|
|
28
|
+
const TIER_ASSIGNMENT_SOURCE_VALUES: readonly string[] = Object.values(TierAssignmentSources);
|
|
29
|
+
|
|
30
|
+
export function isTierAssignmentSource(value: unknown): value is TierAssignmentSource {
|
|
31
|
+
return typeof value === "string" && TIER_ASSIGNMENT_SOURCE_VALUES.includes(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
// Qualified query handler names.
|
|
26
35
|
export const TierEngineQueries = {
|
|
27
36
|
list: "tier-engine:query:tier-assignment:list",
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createEntity,
|
|
3
|
+
createSelectField,
|
|
4
|
+
createTextField,
|
|
5
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
6
|
+
import { type TierAssignmentSource, TierAssignmentSources } from "./constants.js";
|
|
2
7
|
|
|
3
8
|
// tier-assignment — pro Plattform-Tenant genau ein Aggregat. Aggregate-ID
|
|
4
9
|
// wird deterministisch aus tenantId abgeleitet (uuidv5, siehe aggregate-id.ts).
|
|
@@ -35,9 +40,9 @@ export const tierAssignmentEntity = createEntity({
|
|
|
35
40
|
}),
|
|
36
41
|
// Origin of the assignment, see TierAssignmentSources. Optional for back-compat with
|
|
37
42
|
// rows without source. Protects manual grants from being overwritten by the billing sync.
|
|
38
|
-
source:
|
|
43
|
+
source: createSelectField({
|
|
44
|
+
options: Object.values(TierAssignmentSources),
|
|
39
45
|
required: false,
|
|
40
|
-
maxLength: 20,
|
|
41
46
|
personal: false,
|
|
42
47
|
reason: "technical_reference",
|
|
43
48
|
}),
|
|
@@ -48,6 +53,6 @@ export type TierAssignmentRow = {
|
|
|
48
53
|
readonly id: string;
|
|
49
54
|
readonly version: number;
|
|
50
55
|
readonly tier: string;
|
|
51
|
-
readonly source:
|
|
56
|
+
readonly source: TierAssignmentSource | null;
|
|
52
57
|
readonly tenantId: string;
|
|
53
58
|
};
|
|
@@ -338,7 +338,8 @@ export function createTierEngineFeature<
|
|
|
338
338
|
};
|
|
339
339
|
const publishAssignmentChange = (tenantId: TenantId): void => {
|
|
340
340
|
const message: TierAssignmentSyncMessage = { tenantId };
|
|
341
|
-
|
|
341
|
+
// The writer already updated its own cache; only other pods need to re-read.
|
|
342
|
+
busHolder.bus?.publish(CACHE_SYNC_TOPICS.tierAssignment, message, { remoteOnly: true });
|
|
342
343
|
};
|
|
343
344
|
|
|
344
345
|
// Trial: zeit-abgeleitet aus tenant.inserted_at (≈ Signup), live am
|
|
@@ -533,10 +534,17 @@ export function createTierEngineFeature<
|
|
|
533
534
|
const isNewerThanSnapshot = (tenantId: TenantId): boolean =>
|
|
534
535
|
(latestTicket.get(tenantId) ?? 0) > startedAtTicket;
|
|
535
536
|
for (const tenantId of [...cache.keys()]) {
|
|
536
|
-
if (!loaded.has(tenantId) && !isNewerThanSnapshot(tenantId))
|
|
537
|
+
if (!loaded.has(tenantId) && !isNewerThanSnapshot(tenantId)) {
|
|
538
|
+
cache.delete(tenantId);
|
|
539
|
+
latestTicket.set(tenantId, startedAtTicket);
|
|
540
|
+
}
|
|
537
541
|
}
|
|
538
542
|
for (const [tenantId, features] of loaded) {
|
|
539
|
-
if (!isNewerThanSnapshot(tenantId))
|
|
543
|
+
if (!isNewerThanSnapshot(tenantId)) {
|
|
544
|
+
cache.set(tenantId, features);
|
|
545
|
+
// Makes a single reload that started before this snapshot discard its older result.
|
|
546
|
+
latestTicket.set(tenantId, startedAtTicket);
|
|
547
|
+
}
|
|
540
548
|
}
|
|
541
549
|
};
|
|
542
550
|
|