@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
|
@@ -15,6 +15,7 @@ import { createTemplateResolverFeature } from "@cosmicdrift/kumiko-bundled-featu
|
|
|
15
15
|
import { createDocumentDesignerFeature } from "@cosmicdrift/kumiko-document-designer";
|
|
16
16
|
import { localeDe } from "@cosmicdrift/kumiko-locale-de";
|
|
17
17
|
import { createRendererMailHtmlFeature } from "@cosmicdrift/kumiko-renderer-mail-html";
|
|
18
|
+
import { createRendererPuppeteerClientFeature } from "@cosmicdrift/kumiko-renderer-puppeteer-client";
|
|
18
19
|
import { TENANT_ADMIN_EMAIL } from "./app/auth-constants";
|
|
19
20
|
import { billingFeature } from "./features/billing";
|
|
20
21
|
import { rentalFeature } from "./features/rental";
|
|
@@ -34,7 +35,23 @@ export const APP_FEATURES = [
|
|
|
34
35
|
createDocumentDesignerFeature(),
|
|
35
36
|
// Mounted individually: composeRendererStack() would add a second template-resolver.
|
|
36
37
|
createRendererFoundationFeature(),
|
|
37
|
-
createRendererMailHtmlFeature(
|
|
38
|
+
createRendererMailHtmlFeature({
|
|
39
|
+
footer: ({ locale }) =>
|
|
40
|
+
locale.startsWith("de")
|
|
41
|
+
? {
|
|
42
|
+
text: "Gesendet über das Nordlicht Mieterportal",
|
|
43
|
+
imprint: { label: "Impressum", url: "https://nordlicht-immobilien.de/impressum" },
|
|
44
|
+
privacy: { label: "Datenschutz", url: "https://nordlicht-immobilien.de/datenschutz" },
|
|
45
|
+
}
|
|
46
|
+
: {
|
|
47
|
+
text: "Sent via the Nordlicht tenant portal",
|
|
48
|
+
imprint: { label: "Imprint", url: "https://nordlicht-immobilien.de/imprint" },
|
|
49
|
+
privacy: { label: "Privacy", url: "https://nordlicht-immobilien.de/privacy" },
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
// The PDF preview of the template editor prints through the puppeteer-service
|
|
53
|
+
// (PUPPETEER_SERVICE_URL / PUPPETEER_AUTH_TOKEN); the e2e config starts one.
|
|
54
|
+
createRendererPuppeteerClientFeature(),
|
|
38
55
|
createDeliveryFeature(),
|
|
39
56
|
// In-memory transport: the test mail of the mail editor is rendered and
|
|
40
57
|
// "sent" for real, nothing leaves the process. Every user resolves to the
|
|
@@ -33,24 +33,14 @@ Exits 0 on success; any feature-wiring fault throws and exits 1.
|
|
|
33
33
|
|
|
34
34
|
## Coverage status
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
- `auth-email-password` — auto-mounted via `composeFeatures(authOptions)` (no need to list)
|
|
39
|
-
- `files-provider-s3` — utility helpers (`createS3Provider`), kein `defineFeature`
|
|
40
|
-
- `foundation-shared` — pure utilities (`requireDefined`, `requireNonEmpty`), kein `defineFeature`
|
|
41
|
-
|
|
42
|
-
The 3 remaining are utilities or auto-mounted — they cannot be added to `APP_FEATURES` by design.
|
|
36
|
+
Every bundled-feature export is mounted except the held-back ones in `EXPECTED_HELD_BACK` (`scripts/check-coverage.ts`): utilities without `defineFeature` and scaffold-only features. They cannot be added to `APP_FEATURES` by design.
|
|
43
37
|
|
|
44
38
|
## Maintenance
|
|
45
39
|
|
|
46
40
|
When you add a new feature-export to `@cosmicdrift/kumiko-bundled-features` you MUST either:
|
|
47
41
|
|
|
48
42
|
1. Mount it in `src/run-config.ts`. `kumiko/schema.ts` picks up entities via
|
|
49
|
-
`composeFeatures(APP_FEATURES)
|
|
50
|
-
```sh
|
|
51
|
-
cd samples/apps/use-all-bundled
|
|
52
|
-
bun ../../bin/kumiko.ts schema generate feature-coverage
|
|
53
|
-
```
|
|
43
|
+
`composeFeatures(APP_FEATURES)`; run `kumiko-schema generate` if the schema changed.
|
|
54
44
|
2. Or, if the feature is not yet smoke-ready, add it to `EXPECTED_HELD_BACK` in `scripts/check-coverage.ts` with a one-line reason.
|
|
55
45
|
|
|
56
|
-
CI fails otherwise via `Coverage lint
|
|
46
|
+
CI fails otherwise via `Coverage lint`.
|
|
@@ -1423,7 +1423,7 @@
|
|
|
1423
1423
|
},
|
|
1424
1424
|
{
|
|
1425
1425
|
"name": "jobs",
|
|
1426
|
-
"description": "Persistence and operator tooling for background jobs registered via `r.job(...)`. Every job execution writes directly into `store_job_runs` (current status + duration) and `store_job_run_logs` (per-line log rows) from the BullMQ callbacks — no event stream in between (#2243). A daily `retention-cleanup` job deletes runs (and their logs) older than `retentionDays`; an hourly `stale-run-sweep` job marks runs stuck at status `running` past `staleRunTimeoutHours` as `failed` (#2246 — a crashed worker never fires the completion callback, so nothing else ever revisits the row). Exposes `jobs:write:trigger` (manual run) and `jobs:write:retry` (operator retry of a failed run), plus `jobs:query:list`, `jobs:query:details`, and `jobs:query:catalog` (manual jobs) for the operator UI. A job that declares `tenantVisibleFailure` also records its last failed attempt per tenant and subject in `store_tenant_job_failures`, which the tenant itself reads through `jobs:query:failures` — a translation key only, never the provider's message (fw#3079). A job that declares `tenantVisibleRun` also keeps its run state per tenant and subject in `store_tenant_job_runs` (queued, running, plus the latest completed or failed run, no payload, error text or logs), which the tenant reads through `jobs:query:tenant-runs` (fw#3616); the stale-run sweep fails its stuck `running` rows and drops
|
|
1426
|
+
"description": "Persistence and operator tooling for background jobs registered via `r.job(...)`. Every job execution writes directly into `store_job_runs` (current status + duration) and `store_job_run_logs` (per-line log rows) from the BullMQ callbacks — no event stream in between (#2243). A daily `retention-cleanup` job deletes runs (and their logs) older than `retentionDays`; an hourly `stale-run-sweep` job marks runs stuck at status `running` past `staleRunTimeoutHours` as `failed` (#2246 — a crashed worker never fires the completion callback, so nothing else ever revisits the row). Exposes `jobs:write:trigger` (manual run) and `jobs:write:retry` (operator retry of a failed run), plus `jobs:query:list`, `jobs:query:details`, and `jobs:query:catalog` (manual jobs) for the operator UI. A job that declares `tenantVisibleFailure` also records its last failed attempt per tenant and subject in `store_tenant_job_failures`, which the tenant itself reads through `jobs:query:failures` — a translation key only, never the provider's message (fw#3079). A job that declares `tenantVisibleRun` also keeps its run state per tenant and subject in `store_tenant_job_runs` (queued, running, plus the latest completed or failed run, no payload, error text or logs), which the tenant reads through `jobs:query:tenant-runs` (fw#3616); the stale-run sweep fails its stuck `running` rows and drops `queued` rows older than `retentionDays`.",
|
|
1427
1427
|
"toggleableDefault": null,
|
|
1428
1428
|
"requires": [],
|
|
1429
1429
|
"optionalRequires": [],
|
|
@@ -2122,7 +2122,7 @@
|
|
|
2122
2122
|
},
|
|
2123
2123
|
{
|
|
2124
2124
|
"name": "renderer-mail-html",
|
|
2125
|
-
"description": "A `renderer-foundation` plugin that handles `RenderRequest { kind: \"mail\" }` and returns `{ subject, html, text, previewText? }` suitable for sending via `channel-email`. The mail template (`kind: \"mail\"`, document HTML, subject in `meta`) resolves through `template-resolver`'s DB fallback, its body is compiled to Handlebars by template-resolver and rendered with escaping, then placed into an MJML layout (`kind: \"mail-layout\"`, slug `default` unless `meta.layoutSlug` says otherwise, in-code
|
|
2125
|
+
"description": "A `renderer-foundation` plugin that handles `RenderRequest { kind: \"mail\" }` and returns `{ subject, html, text, previewText? }` suitable for sending via `channel-email`. The mail template (`kind: \"mail\"`, document HTML, subject in `meta`) resolves through `template-resolver`'s DB fallback, its body is compiled to Handlebars by template-resolver and rendered with escaping, then placed into an MJML layout (`kind: \"mail-layout\"`, slug `default` unless `meta.layoutSlug` says otherwise, an in-code structured layout with the tenant branding logo and accent color as last resort) that `mjml()` compiles to email-safe table HTML, with `html-to-text` producing the plain-text alternative. An optional `footer` resolver adds the platform imprint, privacy link and footer text below every structured layout, and an optional `brandSuffix` resolver adds a word after the tenant name in the brand row of the in-code layout. Mail meta `headerChip`, `tone` and `footerNote` add a chip beside the brand, the tone of the top line and chip, and a note above the site and unsubscribe links of the card. Seed an editable system layout with `seedSystemMailLayout(db)`. Requires `renderer-foundation` + `template-resolver`.",
|
|
2126
2126
|
"toggleableDefault": null,
|
|
2127
2127
|
"requires": [
|
|
2128
2128
|
"renderer-foundation",
|
|
@@ -2170,7 +2170,7 @@
|
|
|
2170
2170
|
},
|
|
2171
2171
|
{
|
|
2172
2172
|
"name": "renderer-puppeteer-client",
|
|
2173
|
-
"description": "A `renderer-foundation` plugin that prints documents to PDF through a separately deployed `puppeteer-service`. A `document-pdf` request names a stored template (`{ slug, locale, variables }`): the plugin resolves the template and its `pdf-layout` (header, signature and footer regions), embeds every image as a data URI, turns repeating table header rows into a real table head and sends the finished HTML to the service, which performs no network fetches itself. The page format, margins, file name pattern and title come from the template meta. `
|
|
2173
|
+
"description": "A `renderer-foundation` plugin that prints documents to PDF through a separately deployed `puppeteer-service`. A `document-pdf` request names a stored template (`{ slug, locale, variables }`): the plugin resolves the template and its `pdf-layout` (header, signature and footer regions), embeds every image as a data URI, turns repeating table header rows into a real table head and sends the finished HTML to the service, which performs no network fetches itself. The page format, margins, file name pattern and title come from the template meta. A write handler renders with `renderPdfBeforeTransaction(ctx, { slug, locale, variables })` as its `beforeTransaction` phase, so the print service call holds no database connection, and stores the result with `storeRenderedPdf(ctx, event.prepared)` in the handler: a file plus a `rendered-pdf` record that keeps the template and layout version, so a stored document can always be traced to the text it was created from. The `puppeteer-status` query tells a TenantAdmin whether the service answers; while it does not, rendering fails with a `puppeteer_unavailable` error (HTTP 503) and no PDF is created. `image-snapshot` requests still take ready HTML in `payload.content`, which must embed all resources as data: URIs because the service fetches no http(s) resources. Configure the endpoint and token with `PUPPETEER_SERVICE_URL` and `PUPPETEER_AUTH_TOKEN`; the client retries 5xx and network errors with exponential backoff (default 3 retries), 4xx fails at once. The feature adds the `rendered_pdfs` table, so apps that mount it need a schema migration.",
|
|
2174
2174
|
"toggleableDefault": null,
|
|
2175
2175
|
"requires": [
|
|
2176
2176
|
"renderer-foundation"
|
|
@@ -2563,7 +2563,7 @@
|
|
|
2563
2563
|
},
|
|
2564
2564
|
{
|
|
2565
2565
|
"name": "template-resolver",
|
|
2566
|
-
"description": "Every piece of editable text lives here, in one entity: mail bodies, notification texts, PDF document templates, AI prompts and plain text blocks. What a record is used for is the `kind` (`notification`, `mail`, `mail-layout`, `document-pdf`, `pdf-layout`, `ai-prompt`, `text-block`, `snippet`, `image-snapshot`).\n\nReading is one call — `ctx.templateResolver.resolveTemplate({ tenantId, slug, kind, locale })`. It walks four levels: tenant+locale, system+locale, tenant+fallback-locale, system+fallback-locale. A tenant overrides a system default by simply having its own record; no application code changes. Writing programmatically goes through the `upsertSystem`, `upsertTenant`, `publish` and `archive` write handlers; a SystemAdmin retires or restores a system default with `publishSystem` and `archiveSystem`, and a tenant drops its override with `resetToSystem
|
|
2566
|
+
"description": "Every piece of editable text lives here, in one entity: mail bodies, notification texts, PDF document templates, AI prompts and plain text blocks. What a record is used for is the `kind` (`notification`, `mail`, `mail-layout`, `document-pdf`, `pdf-layout`, `ai-prompt`, `text-block`, `snippet`, `image-snapshot`).\n\nReading is one call — `ctx.templateResolver.resolveTemplate({ tenantId, slug, kind, locale })`. It walks four levels: tenant+locale, system+locale, tenant+fallback-locale, system+fallback-locale. A tenant overrides a system default by simply having its own record; no application code changes. Writing programmatically goes through the `upsertSystem`, `upsertTenant`, `publish` and `archive` write handlers; a SystemAdmin retires or restores a system default with `publishSystem` and `archiveSystem`, and a tenant drops its override with `resetToSystem`. Saving a draft over an active template keeps the active version served and stores the edit beside it (column `draft`); `publish` makes it the served version, `discardDraft` / `discardDraftSystem` throws it away.\n\nText an editor should be able to change belongs in a collection, declared at mount: `createTemplateResolverFeature({ collections: [{ id, kind, access: { roles }, nav }] })`. It appears in the navigation, and `access` is part of the mount because a bundled feature cannot know the host's roles. Each collection gets its own `<id>-list` / `<id>-item` / `<id>-set` handlers, so the dispatcher enforces the separation.\n\nHow a collection is edited follows from `contentFormat`: `plain` gives a text area, `rich` a small WYSIWYG (bold, italic, headings, lists, links), `markdown` a text area that stores markdown text (same insertable chips as `plain`). All three offer the collection's `variableSchema` as insertable chips and a preview rendered with sample data — an editor sees what `{{firstName}}` becomes without sending a mail. An app can register its own editor for a format; the last `clientFeature` that registers a format wins (a conflict logs a warning).\n\nA collection is tenant-wide by default. With `ownership: \"user\"` every user keeps their own entries — mail signatures being the obvious case. Those rows live in the separate `user-content-entry` entity and count as user data, so mounting one also requires the `template-resolver-user-data` feature and a migration on the app side.\n\n`validate` dry-runs the write pipeline for a draft (sanitizer, layout rules, placeholders) and reports what the template uses and misses. `export-bundle` / `import-bundle` (and `import-bundle-system`) move one template with all locales and resource files between tenants or environments; `copy-locale` starts a translation from an existing locale. Layouts are structured: a mail or PDF layout is `data-header` / `data-content` / `data-signature` / `data-footer` regions, and mjml layout source is system-only.\n\n`preview` renders a draft (or the stored template) with sample values taken from the `example` of each declared variable, through the same gates and the same mail renderer as a real send, and returns subject, html, `displayHtml` (the html with cid images as data URIs for an editor), text, attachments (inline images and files) and `checks` (images resolved, plain text present, size under 10 MiB, required placeholders used); nothing is saved or sent. `send-test` mails that rendering to the calling user's own address only, subject prefixed with `[TEST]`, at most 10 per user and hour; it needs `delivery`, `channel-email` and `renderer-mail-html` to be mounted.\n\nImages used by a template (`<img data-resource=\"logo\">`) are uploaded with `resourceUpload`, which re-encodes them, stores a file and links it under the key; `resourceRemove` unlinks it. `linkedResources` is written only through these two handlers. With `purpose: \"attachment\"` and a `fileName`, `resourceUpload` stores a PDF, PNG or JPEG as a file attachment of a mail template (original bytes, at most 10) that the mail renderer attaches after the inline images. `resourceLinkPdf` instead links a `document-pdf` template under a key: every mail sent from the template then carries that PDF, rendered at send time with the mail's own variables and locale, under a file name pattern such as `invoice-{{number}}.pdf`; files and linked PDFs share the limit of 10.\n\nReplaces the former `text-content` feature; its blocks now live here as kind `text-block`.",
|
|
2567
2567
|
"toggleableDefault": null,
|
|
2568
2568
|
"requires": [],
|
|
2569
2569
|
"optionalRequires": [],
|
|
@@ -2601,6 +2601,8 @@
|
|
|
2601
2601
|
"template-resolver:write:archive",
|
|
2602
2602
|
"template-resolver:write:archive-system",
|
|
2603
2603
|
"template-resolver:write:copy-locale",
|
|
2604
|
+
"template-resolver:write:discard-draft",
|
|
2605
|
+
"template-resolver:write:discard-draft-system",
|
|
2604
2606
|
"template-resolver:write:import-bundle",
|
|
2605
2607
|
"template-resolver:write:import-bundle-system",
|
|
2606
2608
|
"template-resolver:write:publish",
|
|
@@ -3207,7 +3209,7 @@
|
|
|
3207
3209
|
},
|
|
3208
3210
|
{
|
|
3209
3211
|
"name": "waitlist",
|
|
3210
|
-
"description": "Public waitlist: an anonymous, rate-limited `submit` handler collects name, email and optional company, portfolio and message (a honeypot field and per-email dedupe keep bots and repeat submits out, and the response
|
|
3212
|
+
"description": "Public waitlist: an anonymous, rate-limited `submit` handler collects name, email and optional company, portfolio and message (a honeypot field and per-email dedupe keep bots and repeat submits out, and the response body is identical whether or not an address was already listed), and mails the submitter a confirmation. SystemAdmins review entries in a list screen and invite or reject them; an invite creates the entrant's own tenant (or joins one shared tenant) and mails an accept link with the configured membership role, never a global role. Entries are platform-wide, their personal data is crypto-shredding-ready, and the EXT_USER_DATA hooks export and erase entries matching a user's email.",
|
|
3211
3213
|
"toggleableDefault": null,
|
|
3212
3214
|
"requires": [
|
|
3213
3215
|
"tenant",
|
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// biome-ignore-all lint/suspicious/noConsole: CLI-Script, console ist Feature.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// Coverage lint. Diffs die Top-Level-Exports von bundled-features gegen die Features, die
|
|
5
|
+
// use-all-bundled mountet (FEATURES aus kumiko/schema.ts: APP_FEATURES plus die von
|
|
6
|
+
// composeFeatures ergänzten Auth-/Signup-Features). Fängt drei Drifts:
|
|
7
7
|
//
|
|
8
|
-
// 1. Ein
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// → stale registry
|
|
12
|
-
//
|
|
13
|
-
// Held-back features für M0.1 (subscription-stripe, channel-email, …)
|
|
14
|
-
// stehen in EXPECTED_HELD_BACK. Sobald sie mit stub-options gemountet
|
|
15
|
-
// werden, müssen sie aus dieser Liste raus.
|
|
8
|
+
// 1. Ein Export, der weder gemountet ist noch in EXPECTED_HELD_BACK steht
|
|
9
|
+
// 2. Ein EXPECTED_HELD_BACK-Eintrag, der kein Export mehr ist
|
|
10
|
+
// 3. Ein EXPECTED_HELD_BACK-Eintrag, der inzwischen gemountet ist (stale)
|
|
16
11
|
//
|
|
17
12
|
// Usage:
|
|
18
13
|
// bun samples/apps/use-all-bundled/scripts/check-coverage.ts
|
|
@@ -20,6 +15,7 @@
|
|
|
20
15
|
|
|
21
16
|
import { readFileSync } from "node:fs";
|
|
22
17
|
import { resolve } from "node:path";
|
|
18
|
+
import { FEATURES } from "../kumiko/schema";
|
|
23
19
|
|
|
24
20
|
const ROOT = resolve(import.meta.dir, "..");
|
|
25
21
|
const FRAMEWORK_ROOT = resolve(ROOT, "..", "..", "..");
|
|
@@ -27,33 +23,13 @@ const FRAMEWORK_ROOT = resolve(ROOT, "..", "..", "..");
|
|
|
27
23
|
// Held-back exports: existieren in bundled-features, sind aber nicht
|
|
28
24
|
// als runtime-feature in use-all-bundled mountbar.
|
|
29
25
|
//
|
|
30
|
-
//
|
|
31
|
-
// -
|
|
32
|
-
// sind pure helpers).
|
|
33
|
-
// - auto-mounted: kommt via composeFeatures(includeBundled:true) automatisch
|
|
34
|
-
// (auth-email-password) — wäre doppelt-mount wenn explizit gelistet.
|
|
26
|
+
// Nur Utilities ohne defineFeature (foundation-shared, files-provider-s3 sind pure helpers)
|
|
27
|
+
// und scaffold-only Features. Gemountete Features gehören nicht hierher.
|
|
35
28
|
const EXPECTED_HELD_BACK = new Set([
|
|
36
|
-
"auth-email-password", // auto-mounted via composeFeatures(authOptions)
|
|
37
|
-
"agent-tools", // utility (buildToolCatalog: Registry -> ToolDefinition[]), kein defineFeature
|
|
38
29
|
"files-provider-s3", // utility (createS3Provider helpers), kein defineFeature
|
|
39
30
|
"foundation-shared", // utilities (requireDefined/requireNonEmpty), kein feature
|
|
40
31
|
"page-render", // shared HTML render/cache helpers (legal/managed-pages), kein defineFeature
|
|
41
32
|
"presets", // feature-bundle helpers (dsgvoSelfServiceFeatures, securityBaselineFeatures), kein defineFeature
|
|
42
|
-
"auth-foundation", // scaffold-only (#1368): bootCheck wirft ohne gemountete auth-provider-*, die es noch nicht gibt
|
|
43
|
-
]);
|
|
44
|
-
|
|
45
|
-
// Registry-only: mounted features that are NOT top-level package exports
|
|
46
|
-
// (companion features living under another export path). schema-check still
|
|
47
|
-
// needs them in FEATURE_IMPORT_REGISTRY; coverage must not flag them stale.
|
|
48
|
-
const EXPECTED_REGISTRY_ONLY = new Set([
|
|
49
|
-
"auth-self-registration", // from ./auth-email-password, auto-mounted with signup (#1521)
|
|
50
|
-
"collection-labels", // app-local (src/app/), nav labels for this app's content collections
|
|
51
|
-
"cap-overview-labels", // app-local (src/app/), labels for this app's example cap-overview caps
|
|
52
|
-
"metrics-system", // from ./metrics, companion of createMetricsFeature (platform-wide scope)
|
|
53
|
-
"locale-de", // @cosmicdrift/kumiko-locale-de, not a bundled-features export
|
|
54
|
-
"renderer-mail-html", // @cosmicdrift/kumiko-renderer-mail-html, auto-mounted with auth by composeFeatures
|
|
55
|
-
"renderer-puppeteer-client", // @cosmicdrift/kumiko-renderer-puppeteer-client, not a bundled-features export
|
|
56
|
-
"document-designer", // @cosmicdrift/kumiko-document-designer, not a bundled-features export
|
|
57
33
|
]);
|
|
58
34
|
|
|
59
35
|
// Sub-paths in bundled-features's package.json exports (./tenant/seeding,
|
|
@@ -87,120 +63,60 @@ function readBundledExports(): Set<string> {
|
|
|
87
63
|
return out;
|
|
88
64
|
}
|
|
89
65
|
|
|
90
|
-
function extractRegistryBlock(src: string): string {
|
|
91
|
-
// Anchor auf den FEATURE_IMPORT_REGISTRY-Object-Body. Ohne diesen Anchor
|
|
92
|
-
// würde die kind-Regex jedes `<ident>: { kind: "factory"|"named" }` im File
|
|
93
|
-
// matchen (z.B. eine künftige Hilfsvariable) → false positive/negative.
|
|
94
|
-
const startMatch = /FEATURE_IMPORT_REGISTRY[^=]*=\s*\{/.exec(src);
|
|
95
|
-
if (!startMatch) {
|
|
96
|
-
throw new Error("check-coverage: FEATURE_IMPORT_REGISTRY block not found in generate.ts");
|
|
97
|
-
}
|
|
98
|
-
const bodyStart = startMatch.index + startMatch[0].length;
|
|
99
|
-
// Top-level-Close ist das erste `};` am Zeilenanfang nach bodyStart.
|
|
100
|
-
const closeMatch = /\n};/.exec(src.slice(bodyStart));
|
|
101
|
-
if (!closeMatch) {
|
|
102
|
-
throw new Error("check-coverage: FEATURE_IMPORT_REGISTRY closing `};` not found");
|
|
103
|
-
}
|
|
104
|
-
return src.slice(bodyStart, bodyStart + closeMatch.index);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function readRegistryFeatures(): Set<string> {
|
|
108
|
-
const generateSrc = readFileSync(resolve(ROOT, "schema", "generate.ts"), "utf-8");
|
|
109
|
-
const registryBlock = extractRegistryBlock(generateSrc);
|
|
110
|
-
// Match object-keys mit Discriminator `kind: "factory" | "named"`.
|
|
111
|
-
// Property-Keys können quoted ("billing-foundation") oder unquoted
|
|
112
|
-
// (config, user) sein — beides ist gültig wenn kebab-segment-free.
|
|
113
|
-
const re = /(?:"([a-z][a-z0-9-]*)"|([a-z][a-z0-9]*)):\s*\{\s*kind:\s*"(factory|named)"/g;
|
|
114
|
-
const out = new Set<string>();
|
|
115
|
-
for (const m of registryBlock.matchAll(re)) {
|
|
116
|
-
const name = m[1] ?? m[2];
|
|
117
|
-
if (name) out.add(name);
|
|
118
|
-
}
|
|
119
|
-
return out;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
66
|
const exports = readBundledExports();
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
const unreferenced: string[] = [];
|
|
126
|
-
const stale: string[] = [];
|
|
127
|
-
// Held-back-Einträge, die kein bundled-Export mehr sind → stale Konfig.
|
|
128
|
-
// Ohne diese Prüfung hätte der Drift-Guard in seiner eigenen Konfig einen
|
|
129
|
-
// Blind-Spot: ein umbenanntes/entferntes Feature bliebe still in der Liste.
|
|
130
|
-
const staleHeldBack: string[] = [];
|
|
131
|
-
|
|
132
|
-
for (const name of exports) {
|
|
133
|
-
if (registry.has(name)) continue;
|
|
134
|
-
if (EXPECTED_HELD_BACK.has(name)) continue;
|
|
135
|
-
unreferenced.push(name);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
for (const name of registry) {
|
|
139
|
-
if (!exports.has(name) && !EXPECTED_REGISTRY_ONLY.has(name)) stale.push(name);
|
|
140
|
-
}
|
|
67
|
+
const mounted = new Set<string>(FEATURES.map((feature) => feature.name));
|
|
141
68
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
69
|
+
const unreferenced = [...exports].filter(
|
|
70
|
+
(name) => !mounted.has(name) && !EXPECTED_HELD_BACK.has(name),
|
|
71
|
+
);
|
|
72
|
+
// Ohne diese Prüfungen hätte der Guard in seiner eigenen Konfig einen Blind-Spot: ein
|
|
73
|
+
// umbenanntes oder entferntes Feature bliebe still in der Liste, ein gemountetes ebenso.
|
|
74
|
+
const staleHeldBack = [...EXPECTED_HELD_BACK].filter((name) => !exports.has(name));
|
|
75
|
+
const mountedHeldBack = [...EXPECTED_HELD_BACK].filter((name) => mounted.has(name));
|
|
145
76
|
|
|
146
77
|
let ok = true;
|
|
147
78
|
|
|
148
79
|
if (unreferenced.length > 0) {
|
|
149
80
|
console.error(
|
|
150
|
-
`\n❌ ${unreferenced.length} bundled-feature export(s)
|
|
81
|
+
`\n❌ ${unreferenced.length} bundled-feature export(s) nicht in use-all-bundled gemountet:`,
|
|
151
82
|
);
|
|
152
83
|
for (const name of unreferenced.sort()) {
|
|
153
84
|
console.error(` - ${name}`);
|
|
154
85
|
}
|
|
155
86
|
console.error(
|
|
156
|
-
"\n Action: in src/run-config.ts
|
|
157
|
-
);
|
|
158
|
-
console.error(
|
|
159
|
-
" oder (wenn mount-options noch fehlen) zu EXPECTED_HELD_BACK in dieser Datei hinzufügen.",
|
|
87
|
+
"\n Action: in src/run-config.ts mounten, oder (wenn mount-options noch fehlen) zu EXPECTED_HELD_BACK in dieser Datei hinzufügen.",
|
|
160
88
|
);
|
|
161
89
|
ok = false;
|
|
162
90
|
}
|
|
163
91
|
|
|
164
|
-
if (
|
|
92
|
+
if (staleHeldBack.length > 0) {
|
|
165
93
|
console.error(
|
|
166
|
-
`\n❌ ${
|
|
94
|
+
`\n❌ ${staleHeldBack.length} EXPECTED_HELD_BACK entry/entries ohne Match in bundled-features exports:`,
|
|
167
95
|
);
|
|
168
|
-
for (const name of
|
|
96
|
+
for (const name of staleHeldBack.sort()) {
|
|
169
97
|
console.error(` - ${name}`);
|
|
170
98
|
}
|
|
171
99
|
console.error(
|
|
172
|
-
"\n Action: aus
|
|
100
|
+
"\n Action: aus EXPECTED_HELD_BACK in dieser Datei entfernen (Feature wurde umbenannt/entfernt).",
|
|
173
101
|
);
|
|
174
102
|
ok = false;
|
|
175
103
|
}
|
|
176
104
|
|
|
177
|
-
if (
|
|
105
|
+
if (mountedHeldBack.length > 0) {
|
|
178
106
|
console.error(
|
|
179
|
-
`\n❌ ${
|
|
107
|
+
`\n❌ ${mountedHeldBack.length} EXPECTED_HELD_BACK entry/entries sind gemountet (stale):`,
|
|
180
108
|
);
|
|
181
|
-
for (const name of
|
|
109
|
+
for (const name of mountedHeldBack.sort()) {
|
|
182
110
|
console.error(` - ${name}`);
|
|
183
111
|
}
|
|
184
|
-
console.error(
|
|
185
|
-
"\n Action: aus EXPECTED_HELD_BACK in dieser Datei entfernen (Feature wurde umbenannt/entfernt).",
|
|
186
|
-
);
|
|
112
|
+
console.error("\n Action: aus EXPECTED_HELD_BACK in dieser Datei entfernen.");
|
|
187
113
|
ok = false;
|
|
188
114
|
}
|
|
189
115
|
|
|
190
|
-
if (ok)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
for (const name of registry) {
|
|
198
|
-
if (exports.has(name) && !EXPECTED_HELD_BACK.has(name)) coveredCount++;
|
|
199
|
-
}
|
|
200
|
-
console.log(
|
|
201
|
-
`✓ Coverage: ${coveredCount}/${mountable} mountable bundled-features im Registry, ${EXPECTED_HELD_BACK.size} held-back (M0.1).`,
|
|
202
|
-
);
|
|
203
|
-
process.exit(0);
|
|
204
|
-
} else {
|
|
205
|
-
process.exit(1);
|
|
206
|
-
}
|
|
116
|
+
if (!ok) process.exit(1);
|
|
117
|
+
|
|
118
|
+
const mountable = exports.size - EXPECTED_HELD_BACK.size;
|
|
119
|
+
const mountedExports = [...exports].filter((name) => mounted.has(name)).length;
|
|
120
|
+
console.log(
|
|
121
|
+
`✓ Coverage: ${mountedExports}/${mountable} mountable bundled-features gemountet, ${EXPECTED_HELD_BACK.size} held-back.`,
|
|
122
|
+
);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// proving the "extend any object" promise on real, bootable screens.
|
|
7
7
|
//
|
|
8
8
|
// Deliberately NOT in src/run-config.ts APP_FEATURES: that set is bundled-only
|
|
9
|
-
// (
|
|
9
|
+
// (check-coverage compares it against the bundled-features exports). Like
|
|
10
10
|
// appScreensFeature, this app-owned feature is mounted only in the dev server
|
|
11
11
|
// (src/app/dev-app-options.ts), where runDevApp dev-pushes its table into the ephemeral
|
|
12
12
|
// DB. Nothing here ships to prod / the schema pipeline.
|
|
@@ -10,7 +10,7 @@ The production wiring shape: mount `createAuthMfaFeature()` next to `createAuthE
|
|
|
10
10
|
|
|
11
11
|
1. `createAuthMfaFeature({ setupTokenSecret, challengeTokenSecret, issuer })` builds the feature; `mfaStatusCheckerFromFeature(mfaFeature)` reads its login-status callback back off.
|
|
12
12
|
2. That callback goes into `createAuthEmailPasswordFeature({ mfaStatusChecker: ... })` — `login.write.ts` calls it after the password check to decide whether to mint a JWT or a challenge.
|
|
13
|
-
3. `buildServer({ auth: { mfaVerifyHandler: AuthMfaHandlers.verify } })` mounts `POST /auth/mfa/verify`, a framework route (not a dispatcher write-handler) with its own rate limiter, since it runs before a JWT exists.
|
|
13
|
+
3. `buildServer({ auth: { mfaVerifyHandler: AuthMfaHandlers.verify, switchTenantMfaGateHandler: AuthHandlers.switchTenantMfaGate } })` (the gate is required with `mfaVerifyHandler`, so a tenant switch asks for the second factor too) mounts `POST /auth/mfa/verify`, a framework route (not a dispatcher write-handler) with its own rate limiter, since it runs before a JWT exists.
|
|
14
14
|
4. If `sessions` is mounted too, `bindMfaRevokeAllOtherSessionsFromFeature(mfaFeature)?.(sessionCallbacks.sessionRevokeAllOthers)` wires "enable/disable/regenerate signs out every other session."
|
|
15
15
|
|
|
16
16
|
## What to copy into your app
|
|
@@ -43,6 +43,7 @@ const server = buildServer({
|
|
|
43
43
|
membershipQuery: "tenant:query:memberships",
|
|
44
44
|
loginHandler: "auth-email-password:[REDACTED:API key param]",
|
|
45
45
|
mfaVerifyHandler: AuthMfaHandlers.verify,
|
|
46
|
+
switchTenantMfaGateHandler: AuthHandlers.switchTenantMfaGate,
|
|
46
47
|
sessionCreator: sessionCallbacks.sessionCreator,
|
|
47
48
|
sessionRevoker: sessionCallbacks.sessionRevoker,
|
|
48
49
|
sessionChecker: sessionCallbacks.sessionChecker,
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
// membershipQuery: "tenant:query:memberships",
|
|
38
38
|
// loginHandler: "auth-email-password:[REDACTED:API key param]",
|
|
39
39
|
// mfaVerifyHandler: AuthMfaHandlers.verify,
|
|
40
|
+
// switchTenantMfaGateHandler: AuthHandlers.switchTenantMfaGate,
|
|
40
41
|
// sessionCreator: sessionCallbacks.sessionCreator,
|
|
41
42
|
// sessionRevoker: sessionCallbacks.sessionRevoker,
|
|
42
43
|
// sessionChecker: sessionCallbacks.sessionChecker,
|
|
@@ -22,9 +22,10 @@ logic is yours.
|
|
|
22
22
|
- **`skipOptimisticLock` for last-writer-wins** — `reset` deliberately
|
|
23
23
|
bypasses the version check because the operation is admin-driven and
|
|
24
24
|
resetting twice is semantically the same as resetting once.
|
|
25
|
-
- **Custom queries** — `counter:active`
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
- **Custom queries** — `counter:active` passes a server-side `filter`
|
|
26
|
+
(`eq`/`ne`/`lt`/`gt`/`lte`/`gte`/`in`) and `totalCount` to the executor's
|
|
27
|
+
list. Never filter a page in TypeScript: list is paginated, so rows after
|
|
28
|
+
the first page would be missed.
|
|
28
29
|
- **`failNotFound` helper** — typed error response for the 404 case
|
|
29
30
|
without a hand-built `KumikoError` subclass.
|
|
30
31
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Custom Handlers Sample
|
|
2
|
-
// Shows: writeHandler with business logic, queryHandler with
|
|
2
|
+
// Shows: writeHandler with business logic, queryHandler with a server-side filter,
|
|
3
3
|
// handler that modifies payload before DB write.
|
|
4
4
|
|
|
5
5
|
import {
|
|
@@ -12,6 +12,9 @@ import {
|
|
|
12
12
|
import { failNotFound } from "@cosmicdrift/kumiko-framework/errors";
|
|
13
13
|
import { z } from "zod";
|
|
14
14
|
|
|
15
|
+
// detail() returns an untyped row; parse the two fields the increment needs once.
|
|
16
|
+
const counterVersionRowSchema = z.object({ count: z.number(), version: z.number() });
|
|
17
|
+
|
|
15
18
|
export const counterEntity = createEntity({
|
|
16
19
|
table: "read_sample_counters",
|
|
17
20
|
fields: {
|
|
@@ -48,13 +51,14 @@ export const counterFeature = defineFeature("counters", (r) => {
|
|
|
48
51
|
return failNotFound("counter", event.payload.id);
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
const
|
|
54
|
+
const { count, version } = counterVersionRowSchema.parse(current);
|
|
55
|
+
const newCount = count + event.payload.amount;
|
|
52
56
|
// Read-modify-write on counter: use the version we just read so two
|
|
53
57
|
// concurrent increments don't clobber each other's changes.
|
|
54
58
|
return counterExecutor.update(
|
|
55
59
|
{
|
|
56
60
|
id: event.payload.id,
|
|
57
|
-
version
|
|
61
|
+
version,
|
|
58
62
|
changes: { count: newCount, lastIncrementedBy: `user:${event.user.id}` },
|
|
59
63
|
},
|
|
60
64
|
event.user,
|
|
@@ -80,17 +84,22 @@ export const counterFeature = defineFeature("counters", (r) => {
|
|
|
80
84
|
{ access: { roles: ["Admin"] } },
|
|
81
85
|
);
|
|
82
86
|
|
|
83
|
-
// Custom query: only counters above a threshold
|
|
87
|
+
// Custom query: only counters above a threshold. list() returns one page
|
|
88
|
+
// (default 50, max 200, MAX_LIST_LIMIT), so a JS filter would miss rows after
|
|
89
|
+
// page 1; filter in the query instead. totalCount counts over the whole WHERE.
|
|
84
90
|
r.queryHandler(
|
|
85
91
|
"counter:active",
|
|
86
|
-
z.object({ minCount: z.number().default(1) }),
|
|
87
|
-
async (query, ctx) =>
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
z.object({ minCount: z.number().int().default(1), cursor: z.string().optional() }),
|
|
93
|
+
async (query, ctx) =>
|
|
94
|
+
counterExecutor.list(
|
|
95
|
+
{
|
|
96
|
+
filter: { field: "count", op: "gte", value: query.payload.minCount },
|
|
97
|
+
totalCount: true,
|
|
98
|
+
cursor: query.payload.cursor,
|
|
99
|
+
},
|
|
100
|
+
query.user,
|
|
101
|
+
ctx.db,
|
|
102
|
+
),
|
|
94
103
|
{
|
|
95
104
|
access: {
|
|
96
105
|
openToAll: {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Dashboard stats
|
|
2
|
+
|
|
3
|
+
Key figures on a dashboard. This recipe ships a `ticket` entity, one query
|
|
4
|
+
handler that returns the figures as a flat object, and a `dashboard` screen
|
|
5
|
+
with a stat group (open, closed) and a single stat (total).
|
|
6
|
+
|
|
7
|
+
The point is to show how the three pieces connect: the screen only names a
|
|
8
|
+
query and the fields to read, the query does the counting.
|
|
9
|
+
|
|
10
|
+
## What it shows
|
|
11
|
+
|
|
12
|
+
- **Panels reference a query by qualified name**: each `stat` carries
|
|
13
|
+
`query: "tickets:query:ticket:stats"`. The dashboard calls that handler and
|
|
14
|
+
renders the result.
|
|
15
|
+
- **`valueField` picks a field from a flat result**: the handler returns
|
|
16
|
+
`{ open, closed, total }`, and each stat names one of those keys.
|
|
17
|
+
- **`stat-group` and `stat`**: a group shows several stats side by side under
|
|
18
|
+
one title, a top-level `stat` stands alone.
|
|
19
|
+
- **Counting happens in the database**: the handler calls the executor's
|
|
20
|
+
`list` with `filter`, `limit: 1` and `totalCount: true`. The total is counted
|
|
21
|
+
over the whole WHERE clause, so no rows are loaded just to be counted.
|
|
22
|
+
- **Labels are translation keys**: declared with `r.translations`.
|
|
23
|
+
|
|
24
|
+
## When to reach for it
|
|
25
|
+
|
|
26
|
+
You want a landing screen with a few numbers: open items, overdue invoices,
|
|
27
|
+
this month's revenue. Write one query per screen that returns every figure,
|
|
28
|
+
and let the stats pick their field.
|
|
29
|
+
|
|
30
|
+
## Source
|
|
31
|
+
|
|
32
|
+
The whole feature lives in `src/feature.ts` (~100 lines). The integration test
|
|
33
|
+
checks that every stat reads a field the query returns, and that the query
|
|
34
|
+
counts open and closed tickets correctly.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
bun kumiko test integration samples/dashboard-stats
|
|
38
|
+
```
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Dashboard Stats Sample
|
|
2
|
+
// Shows: a dashboard screen with a stat-group and a single stat, backed by one
|
|
3
|
+
// query handler that counts in the database.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createEntity,
|
|
7
|
+
createEntityExecutor,
|
|
8
|
+
createSelectField,
|
|
9
|
+
createTextField,
|
|
10
|
+
defineFeature,
|
|
11
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
|
|
14
|
+
export const TICKET_STATUSES = ["open", "closed"] as const;
|
|
15
|
+
|
|
16
|
+
export const ticketEntity = createEntity({
|
|
17
|
+
table: "read_sample_tickets",
|
|
18
|
+
fields: {
|
|
19
|
+
title: createTextField({ personal: false, reason: "is_business_data", required: true }),
|
|
20
|
+
status: createSelectField({ options: TICKET_STATUSES, default: "open", required: true }),
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const TICKET_STATS_QUERY = "tickets:query:ticket:stats";
|
|
25
|
+
|
|
26
|
+
const readerRoles = { roles: ["Admin", "User"] } as const;
|
|
27
|
+
|
|
28
|
+
export const ticketFeature = defineFeature("tickets", (r) => {
|
|
29
|
+
const { executor: ticketExecutor } = createEntityExecutor("ticket", ticketEntity);
|
|
30
|
+
|
|
31
|
+
r.crud("ticket", ticketEntity, {
|
|
32
|
+
write: { access: { roles: ["Admin"] } },
|
|
33
|
+
read: { access: readerRoles },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Flat result: each dashboard stat reads one field via its valueField.
|
|
37
|
+
// limit: 1 keeps the page tiny; totalCount counts over the whole WHERE in
|
|
38
|
+
// the database, so no rows are loaded just to be counted.
|
|
39
|
+
r.queryHandler(
|
|
40
|
+
"ticket:stats",
|
|
41
|
+
z.object({}),
|
|
42
|
+
async (query, ctx) => {
|
|
43
|
+
const countWithStatus = async (status: (typeof TICKET_STATUSES)[number]): Promise<number> => {
|
|
44
|
+
const page = await ticketExecutor.list(
|
|
45
|
+
{ filter: { field: "status", op: "eq", value: status }, limit: 1, totalCount: true },
|
|
46
|
+
query.user,
|
|
47
|
+
ctx.db,
|
|
48
|
+
);
|
|
49
|
+
return page.total ?? 0;
|
|
50
|
+
};
|
|
51
|
+
const [open, closed] = await Promise.all([
|
|
52
|
+
countWithStatus("open"),
|
|
53
|
+
countWithStatus("closed"),
|
|
54
|
+
]);
|
|
55
|
+
return { open, closed, total: open + closed };
|
|
56
|
+
},
|
|
57
|
+
{ access: readerRoles },
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
r.translations({
|
|
61
|
+
keys: {
|
|
62
|
+
"tickets.dashboard.group": { en: "Tickets by status" },
|
|
63
|
+
"tickets.dashboard.open": { en: "Open" },
|
|
64
|
+
"tickets.dashboard.closed": { en: "Closed" },
|
|
65
|
+
"tickets.dashboard.total": { en: "All tickets" },
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
r.screen({
|
|
70
|
+
id: "ticket-dashboard",
|
|
71
|
+
type: "dashboard",
|
|
72
|
+
panels: [
|
|
73
|
+
{
|
|
74
|
+
kind: "stat-group",
|
|
75
|
+
id: "by-status",
|
|
76
|
+
label: "tickets.dashboard.group",
|
|
77
|
+
stats: [
|
|
78
|
+
{
|
|
79
|
+
kind: "stat",
|
|
80
|
+
id: "open",
|
|
81
|
+
label: "tickets.dashboard.open",
|
|
82
|
+
query: TICKET_STATS_QUERY,
|
|
83
|
+
valueField: "open",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
kind: "stat",
|
|
87
|
+
id: "closed",
|
|
88
|
+
label: "tickets.dashboard.closed",
|
|
89
|
+
query: TICKET_STATS_QUERY,
|
|
90
|
+
valueField: "closed",
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
kind: "stat",
|
|
96
|
+
id: "total",
|
|
97
|
+
label: "tickets.dashboard.total",
|
|
98
|
+
query: TICKET_STATS_QUERY,
|
|
99
|
+
valueField: "total",
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
access: readerRoles,
|
|
103
|
+
});
|
|
104
|
+
});
|