@cosmicdrift/kumiko-bundled-features 0.367.0 → 0.369.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/dist/audit/handlers/details.query.js +1 -1
- package/dist/audit/handlers/list.query.js +1 -1
- package/dist/audit/run-escape-hatch-retention.js +1 -1
- package/dist/auth-email-password/bootstrap.js +1 -1
- package/dist/auth-email-password/handlers/invite-accept-with-login.write.js +6 -3
- package/dist/auth-email-password/handlers/invite-accept.write.js +6 -3
- package/dist/auth-email-password/handlers/invite-info.query.js +1 -1
- package/dist/auth-email-password/handlers/invite-signup-complete.write.js +6 -3
- package/dist/auth-email-password/handlers/signup-confirm.write.js +1 -1
- package/dist/auth-email-password/seeding.js +2 -2
- package/dist/auth-foundation/feature.d.ts +4 -0
- package/dist/auth-foundation/feature.js +37 -33
- package/dist/auth-foundation/index.d.ts +1 -1
- package/dist/auth-foundation/index.js +1 -1
- package/dist/auth-mfa/db/queries.js +1 -2
- package/dist/auth-mfa/handlers/enable-confirm-preauth.write.js +1 -1
- package/dist/auth-mfa/handlers/enable-start-preauth.write.js +1 -1
- package/dist/auth-mfa/handlers/reencrypt.job.js +2 -3
- package/dist/auth-mfa/handlers/status.query.js +1 -1
- package/dist/auth-mfa/handlers/verify.write.js +1 -1
- package/dist/auth-mfa/mfa-code-verifier.js +1 -1
- package/dist/auth-mfa/mfa-status-checker.js +1 -1
- package/dist/auth-mfa-user-data/index.d.ts +4 -0
- package/dist/auth-mfa-user-data/index.js +13 -9
- package/dist/billing-foundation/billing-info-query.js +1 -1
- package/dist/billing-foundation/feature.d.ts +1 -0
- package/dist/billing-foundation/feature.js +5 -6
- package/dist/billing-foundation/handlers/create-portal-session.write.js +1 -1
- package/dist/billing-foundation/projection.js +1 -2
- package/dist/cap-counter/calendar-cap-usage-reservation.d.ts +7 -0
- package/dist/cap-counter/calendar-cap-usage-reservation.js +28 -2
- package/dist/cap-counter/cap-reservation.d.ts +16 -0
- package/dist/cap-counter/cap-reservation.js +144 -14
- package/dist/cap-counter/feature.d.ts +4 -0
- package/dist/cap-counter/feature.js +74 -68
- package/dist/cap-counter/handlers/increment-rolling.write.js +3 -4
- package/dist/cap-counter/index.d.ts +3 -2
- package/dist/cap-counter/index.js +3 -2
- package/dist/cap-counter/tables.js +10 -3
- package/dist/cap-overview/handlers/caps-usage.query.js +1 -2
- package/dist/cap-overview/handlers/tenant-caps-list.query.js +1 -2
- package/dist/cap-overview/handlers/tenant-options.query.js +1 -1
- package/dist/channel-email/email-channel.d.ts +13 -5
- package/dist/channel-email/email-channel.js +17 -3
- package/dist/channel-email/feature.js +1 -1
- package/dist/channel-email/index.d.ts +1 -1
- package/dist/channel-email/index.js +1 -1
- package/dist/channel-in-app/handlers/inbox.query.js +1 -1
- package/dist/channel-in-app/handlers/mark-all-read.write.js +1 -1
- package/dist/channel-in-app/handlers/mark-read.write.js +1 -1
- package/dist/channel-in-app/handlers/unread-count.query.js +1 -1
- package/dist/channel-in-app/tables.js +1 -2
- package/dist/compliance-profiles/handlers/for-tenant.query.js +1 -1
- package/dist/compliance-profiles/handlers/needs-profile.query.js +1 -1
- package/dist/compliance-profiles/handlers/set-profile.write.js +1 -2
- package/dist/compliance-profiles/resolve-for-tenant.js +1 -1
- package/dist/compliance-profiles/seeding.js +3 -5
- package/dist/compliance-profiles-ops/index.d.ts +8 -0
- package/dist/compliance-profiles-ops/index.js +19 -15
- package/dist/config/db/queries/resolver.js +1 -1
- package/dist/config/handlers/reencrypt.job.js +2 -2
- package/dist/config/resolver.js +1 -1
- package/dist/config/web/i18n.js +1 -2
- package/dist/config/write-helpers.js +1 -1
- package/dist/crypto-shredding/handlers/forget-subject.write.js +6 -3
- package/dist/custom-fields/db/queries/field-access.js +1 -1
- package/dist/custom-fields/db/queries/projection.js +1 -1
- package/dist/custom-fields/db/queries/quota.js +1 -1
- package/dist/custom-fields/db/queries/retention.js +1 -2
- package/dist/custom-fields/db/queries/user-data-rights.js +1 -1
- package/dist/custom-fields/feature.d.ts +20 -2
- package/dist/custom-fields/feature.js +9 -13
- package/dist/custom-fields/handlers/clear-custom-field.write.js +2 -2
- package/dist/custom-fields/handlers/delete-system-field.write.js +2 -2
- package/dist/custom-fields/handlers/delete-tenant-field.write.js +2 -2
- package/dist/custom-fields/handlers/set-custom-field.write.js +2 -2
- package/dist/custom-fields/wire-for-entity.js +4 -4
- package/dist/data-retention/resolve-for-tenant.js +1 -1
- package/dist/data-retention/run-retention-cleanup.js +17 -13
- package/dist/delivery/address-opt-out.js +1 -2
- package/dist/delivery/attempt-log-retention.js +1 -1
- package/dist/delivery/channel-context.js +1 -1
- package/dist/delivery/db/queries/address-opt-outs.js +1 -1
- package/dist/delivery/db/queries/preferences.js +1 -1
- package/dist/delivery/delivery-service.js +16 -7
- package/dist/delivery/feature.js +2 -2
- package/dist/delivery/handlers/log.query.js +2 -3
- package/dist/delivery/handlers/preferences.query.js +1 -1
- package/dist/delivery/index.d.ts +3 -2
- package/dist/delivery/index.js +3 -2
- package/dist/delivery/unsubscribe.d.ts +13 -0
- package/dist/delivery/unsubscribe.js +32 -27
- package/dist/delivery/upsert-preference.js +1 -2
- package/dist/derivatives-sharp/feature.d.ts +4 -0
- package/dist/derivatives-sharp/feature.js +15 -11
- package/dist/derivatives-sharp/index.d.ts +1 -1
- package/dist/derivatives-sharp/index.js +1 -1
- package/dist/document-ingest-foundation/feature.d.ts +7 -0
- package/dist/document-ingest-foundation/feature.js +94 -90
- package/dist/document-ingest-foundation/forget-extract-with-file-ref.js +3 -3
- package/dist/document-ingest-foundation/index.d.ts +1 -1
- package/dist/document-ingest-foundation/index.js +1 -1
- package/dist/feature-toggles/global-feature-state-table.js +1 -2
- package/dist/feature-toggles/handlers/set.write.js +3 -5
- package/dist/feature-toggles/toggle-runtime.js +1 -1
- package/dist/file-derivatives/feature.d.ts +1 -0
- package/dist/file-derivatives/feature.js +2 -1
- package/dist/file-derivatives/handlers/public-variant-by-file-ref.query.js +1 -1
- package/dist/file-derivatives/handlers/public-variant.query.js +1 -1
- package/dist/file-foundation/feature.d.ts +6 -0
- package/dist/file-foundation/feature.js +29 -25
- package/dist/file-foundation/index.d.ts +1 -1
- package/dist/file-foundation/index.js +1 -1
- package/dist/file-foundation/read-file-ref-content.js +1 -1
- package/dist/file-provider-inmemory/feature.d.ts +4 -0
- package/dist/file-provider-inmemory/feature.js +24 -20
- package/dist/file-provider-inmemory/index.d.ts +1 -1
- package/dist/file-provider-inmemory/index.js +1 -1
- package/dist/file-provider-s3/env-feature.d.ts +3 -0
- package/dist/file-provider-s3/env-feature.js +46 -0
- package/dist/file-provider-s3/feature.d.ts +26 -0
- package/dist/file-provider-s3/feature.js +84 -72
- package/dist/file-provider-s3/index.d.ts +1 -1
- package/dist/file-provider-s3/index.js +1 -1
- package/dist/file-provider-s3/lib/index.d.ts +3 -0
- package/dist/file-provider-s3/lib/index.js +2 -0
- package/dist/{files-provider-s3 → file-provider-s3/lib}/s3-provider.js +19 -19
- package/dist/file-provider-s3-env/index.d.ts +8 -1
- package/dist/file-provider-s3-env/index.js +7 -2
- package/dist/files-provider-s3/index.d.ts +5 -3
- package/dist/files-provider-s3/index.js +5 -2
- package/dist/files-tenant-data/handlers/sweep-orphaned-derivatives.job.js +1 -1
- package/dist/files-tenant-data/hooks.js +1 -2
- package/dist/folders/feature.d.ts +6 -2
- package/dist/folders/feature.js +4 -2
- package/dist/folders-user-data/index.d.ts +4 -0
- package/dist/folders-user-data/index.js +21 -17
- package/dist/form-draft/db/queries/cleanup.js +1 -1
- package/dist/form-draft/db/queries/draft-count.js +1 -1
- package/dist/form-draft/db/queries/owned-file-refs.js +1 -2
- package/dist/form-draft/feature.d.ts +4 -0
- package/dist/form-draft/feature.js +5 -1
- package/dist/form-draft/handlers/cleanup.job.js +2 -2
- package/dist/form-draft/index.d.ts +1 -1
- package/dist/form-draft/index.js +1 -1
- package/dist/form-draft/lookup.js +1 -1
- package/dist/form-draft-user-data/index.d.ts +4 -0
- package/dist/form-draft-user-data/index.js +13 -9
- package/dist/inbound-mail-foundation/feature.d.ts +4 -0
- package/dist/inbound-mail-foundation/feature.js +151 -147
- package/dist/inbound-mail-foundation/handlers/ingest-message.write.js +1 -2
- package/dist/inbound-mail-foundation/index.d.ts +1 -1
- package/dist/inbound-mail-foundation/index.js +1 -1
- package/dist/inbound-mail-foundation/oauth-access-token.js +1 -2
- package/dist/inbound-mail-foundation/projection.js +1 -2
- package/dist/inbound-mail-foundation/watch-supervisor.js +1 -1
- package/dist/inbound-provider-imap/feature.d.ts +4 -0
- package/dist/inbound-provider-imap/feature.js +16 -12
- package/dist/inbound-provider-imap/index.d.ts +1 -1
- package/dist/inbound-provider-imap/index.js +1 -1
- package/dist/inbound-provider-inmemory/feature.d.ts +4 -0
- package/dist/inbound-provider-inmemory/feature.js +14 -10
- package/dist/inbound-provider-inmemory/index.d.ts +1 -1
- package/dist/inbound-provider-inmemory/index.js +1 -1
- package/dist/jobs/db/queries/retention.js +1 -1
- package/dist/jobs/db/queries/stale-run-sweep.js +1 -1
- package/dist/jobs/handlers/detail.query.js +1 -1
- package/dist/jobs/handlers/list.query.js +1 -2
- package/dist/jobs/handlers/retry.write.js +1 -1
- package/dist/jobs/handlers/tenant-failures.query.js +1 -1
- package/dist/jobs/handlers/tenant-runs.query.js +1 -1
- package/dist/jobs/job-run-logger.js +1 -2
- package/dist/jobs/tenant-job-failure-table.js +1 -2
- package/dist/jobs/tenant-job-run-table.js +1 -2
- package/dist/ledger/feature.d.ts +6 -2
- package/dist/ledger/feature.js +4 -2
- package/dist/mail-foundation/feature.d.ts +11 -0
- package/dist/mail-foundation/feature.js +52 -42
- package/dist/mail-foundation/i18n.d.ts +14 -0
- package/dist/mail-foundation/i18n.js +8 -0
- package/dist/mail-foundation/index.d.ts +1 -1
- package/dist/mail-foundation/index.js +1 -1
- package/dist/mail-transport-inmemory/feature.d.ts +4 -0
- package/dist/mail-transport-inmemory/feature.js +26 -16
- package/dist/mail-transport-inmemory/index.d.ts +1 -1
- package/dist/mail-transport-inmemory/index.js +1 -1
- package/dist/mail-transport-smtp/config-keys.d.ts +7 -0
- package/dist/mail-transport-smtp/config-keys.js +65 -0
- package/dist/mail-transport-smtp/feature.d.ts +17 -1
- package/dist/mail-transport-smtp/feature.js +58 -80
- package/dist/mail-transport-smtp/i18n.d.ts +41 -0
- package/dist/mail-transport-smtp/i18n.js +28 -0
- package/dist/mail-transport-smtp/index.d.ts +1 -1
- package/dist/mail-transport-smtp/index.js +1 -1
- package/dist/managed-pages/handlers/by-slug.query.js +1 -1
- package/dist/managed-pages/handlers/by-tenant-published.query.js +1 -2
- package/dist/managed-pages/handlers/set.write.js +1 -2
- package/dist/managed-pages/seeding.js +3 -5
- package/dist/metrics/compute.d.ts +1 -1
- package/dist/metrics/filters.d.ts +1 -1
- package/dist/metrics/filters.js +1 -1
- package/dist/metrics/validate.js +1 -1
- package/dist/notes-history/feature.d.ts +7 -2
- package/dist/notes-history/feature.js +4 -2
- package/dist/notes-history/handlers/add-note.write.js +1 -1
- package/dist/notes-history-user-data/hooks.js +1 -1
- package/dist/notes-history-user-data/index.d.ts +4 -0
- package/dist/notes-history-user-data/index.js +21 -17
- package/dist/personal-access-tokens/feature.d.ts +4 -1
- package/dist/personal-access-tokens/feature.js +10 -2
- package/dist/personal-access-tokens/handlers/create.write.js +1 -1
- package/dist/personal-access-tokens/handlers/list.query.js +2 -2
- package/dist/personal-access-tokens/handlers/revoke.write.js +1 -1
- package/dist/personal-access-tokens/resolver.js +1 -1
- package/dist/personal-access-tokens/revoke-for-user.js +1 -1
- package/dist/personal-access-tokens/roles.js +3 -5
- package/dist/personal-access-tokens/screens.js +2 -0
- package/dist/readiness/feature.d.ts +8 -0
- package/dist/readiness/feature.js +18 -14
- package/dist/readiness/index.d.ts +1 -1
- package/dist/readiness/index.js +1 -1
- package/dist/secrets/db/queries/read.js +1 -1
- package/dist/secrets/feature.d.ts +4 -0
- package/dist/secrets/feature.js +19 -6
- package/dist/secrets/handlers/delete.write.js +3 -2
- package/dist/secrets/handlers/rotate.job.js +2 -3
- package/dist/secrets/handlers/set.write.js +3 -2
- package/dist/secrets/secrets-context.js +3 -4
- package/dist/sessions/db/queries/cleanup.js +1 -1
- package/dist/sessions/handlers/detail.query.js +1 -1
- package/dist/sessions/handlers/list.query.js +2 -2
- package/dist/sessions/handlers/mine.query.js +1 -1
- package/dist/sessions/handlers/revoke-all-for-user.write.js +1 -1
- package/dist/sessions/handlers/revoke-all-others.write.js +1 -1
- package/dist/sessions/handlers/revoke.write.js +1 -1
- package/dist/sessions/session-callbacks.js +4 -6
- package/dist/sessions/testing.d.ts +1 -1
- package/dist/tags/feature.d.ts +6 -2
- package/dist/tags/feature.js +4 -2
- package/dist/template-resolver/api.js +1 -1
- package/dist/template-resolver/constants.d.ts +0 -1
- package/dist/template-resolver/constants.js +0 -1
- package/dist/template-resolver/handlers/bundle-import.js +1 -1
- package/dist/template-resolver/handlers/by-slug.query.js +1 -1
- package/dist/template-resolver/handlers/by-tenant.query.js +1 -2
- package/dist/template-resolver/handlers/collection-item.query.js +1 -1
- package/dist/template-resolver/handlers/collection-list.query.js +1 -2
- package/dist/template-resolver/handlers/collection-set.write.js +1 -1
- package/dist/template-resolver/handlers/copy-locale.write.js +1 -1
- package/dist/template-resolver/handlers/diff-to-system.query.js +1 -1
- package/dist/template-resolver/handlers/export-bundle.query.js +1 -1
- package/dist/template-resolver/handlers/find-by-id.query.js +1 -1
- package/dist/template-resolver/handlers/layout-source.query.js +1 -1
- package/dist/template-resolver/handlers/list.query.js +1 -1
- package/dist/template-resolver/handlers/overview.query.js +1 -1
- package/dist/template-resolver/handlers/reset-to-system.write.js +1 -1
- package/dist/template-resolver/handlers/resource-remove.write.js +1 -1
- package/dist/template-resolver/handlers/send-test.write.js +4 -2
- package/dist/template-resolver/handlers/set.write.js +1 -1
- package/dist/template-resolver/handlers/template-write.js +1 -1
- package/dist/template-resolver/handlers/tenant-overrides.query.js +1 -1
- package/dist/template-resolver/handlers/toggle-status.write.js +1 -1
- package/dist/template-resolver/handlers/usage.query.js +1 -1
- package/dist/template-resolver/handlers/validate.query.js +1 -1
- package/dist/template-resolver/seeding.js +3 -4
- package/dist/template-resolver/store-template-resource.js +1 -1
- package/dist/template-resolver/template-preview.js +1 -1
- package/dist/template-resolver/template-resources.js +1 -1
- package/dist/template-resolver/testing.js +1 -1
- package/dist/template-resolver-user-data/index.d.ts +4 -0
- package/dist/template-resolver-user-data/index.js +17 -13
- package/dist/tenant/handlers/active-tenant-ids.query.js +1 -1
- package/dist/tenant/handlers/add-member.write.js +1 -2
- package/dist/tenant/handlers/cancel-invitation.write.js +1 -1
- package/dist/tenant/handlers/invitations.query.js +1 -1
- package/dist/tenant/handlers/me.query.js +1 -1
- package/dist/tenant/handlers/member-directory.query.js +1 -1
- package/dist/tenant/handlers/members.query.js +1 -1
- package/dist/tenant/handlers/memberships.query.js +1 -1
- package/dist/tenant/handlers/remove-member.write.js +1 -2
- package/dist/tenant/handlers/resolve-user-ids.query.js +1 -1
- package/dist/tenant/handlers/team-list.query.js +1 -1
- package/dist/tenant/handlers/tenant-directory.query.js +1 -1
- package/dist/tenant/handlers/update-member-roles.write.js +1 -2
- package/dist/tenant/invited-membership.js +2 -3
- package/dist/tenant/last-tenant-admin.js +1 -2
- package/dist/tenant/seeding.js +12 -6
- package/dist/tenant-lifecycle/handlers/cancel-destruction.write.js +1 -2
- package/dist/tenant-lifecycle/handlers/request-destruction.write.js +1 -2
- package/dist/tenant-lifecycle/lib/revoke-tenant-sessions.js +1 -1
- package/dist/tenant-lifecycle/lifecycle-gate.js +1 -1
- package/dist/tenant-lifecycle/run-tenant-destroy.js +3 -5
- package/dist/tenant-lifecycle/stages.js +10 -6
- package/dist/tenant-lifecycle/testing.d.ts +1 -1
- package/dist/tenant-lifecycle/testing.js +2 -2
- package/dist/tenant-settings/feature.d.ts +1 -0
- package/dist/tenant-settings/feature.js +2 -1
- package/dist/tier-engine/feature.d.ts +1 -8
- package/dist/tier-engine/feature.js +71 -78
- package/dist/tier-engine/handlers/get-tenant-tier.query.js +2 -3
- package/dist/tier-engine/handlers/set-tenant-tier.write.js +1 -2
- package/dist/user/db/queries/stream-tenant-backfill.js +1 -1
- package/dist/user/handlers/create.write.js +1 -2
- package/dist/user/handlers/find-for-auth.query.js +1 -1
- package/dist/user/handlers/list.query.js +1 -1
- package/dist/user/handlers/update-roles.js +1 -2
- package/dist/user/index.d.ts +1 -1
- package/dist/user/index.js +1 -1
- package/dist/user/invited-global-roles.js +2 -3
- package/dist/user/principal-status.d.ts +4 -0
- package/dist/user/principal-status.js +9 -3
- package/dist/user/resolve-display-names.js +1 -1
- package/dist/user/seeding.js +5 -6
- package/dist/user-data-rights/audit-download.js +2 -2
- package/dist/user-data-rights/db/queries/export-jobs.js +1 -1
- package/dist/user-data-rights/db/queries/forget-cleanup.js +1 -1
- package/dist/user-data-rights/feature.js +7 -4
- package/dist/user-data-rights/handlers/download-by-job.query.js +1 -1
- package/dist/user-data-rights/handlers/download-by-token.query.js +1 -1
- package/dist/user-data-rights/handlers/export-status.query.js +1 -1
- package/dist/user-data-rights/handlers/list-download-attempts.query.js +1 -1
- package/dist/user-data-rights/handlers/my-audit-log.query.js +1 -1
- package/dist/user-data-rights/handlers/request-export.write.js +1 -2
- package/dist/user-data-rights/handlers/run-forget-cleanup.write.js +3 -3
- package/dist/user-data-rights/lib/default-mailers.d.ts +3 -0
- package/dist/user-data-rights/lib/default-mailers.js +25 -3
- package/dist/user-data-rights/lib/deny-if-target-outside-admin-tenant.d.ts +1 -1
- package/dist/user-data-rights/lib/deny-if-target-outside-admin-tenant.js +1 -1
- package/dist/user-data-rights/lib/update-user-lifecycle.d.ts +1 -0
- package/dist/user-data-rights/lib/update-user-lifecycle.js +3 -3
- package/dist/user-data-rights/run-export-jobs.js +2 -3
- package/dist/user-data-rights/run-forget-cleanup.js +9 -5
- package/dist/user-data-rights/run-user-export.js +9 -5
- package/dist/waitlist/handlers/invite.write.js +1 -2
- package/dist/waitlist/handlers/reject.write.js +1 -1
- package/dist/waitlist/lib.js +1 -2
- package/dist/workflow-runner/event-subscriber.js +1 -1
- package/dist/workflow-runner/feature.d.ts +4 -0
- package/dist/workflow-runner/feature.js +44 -40
- package/dist/workflow-runner/handlers/resume-run.write.js +1 -1
- package/dist/workflow-runner/index.d.ts +1 -1
- package/dist/workflow-runner/index.js +1 -1
- package/dist/workflow-runner/pending-projection.js +1 -1
- package/package.json +28 -11
- package/src/cap-counter/changes.json +13 -0
- package/src/channel-email/changes.json +7 -0
- package/src/crypto-shredding/changes.json +6 -0
- package/src/delivery/changes.json +6 -0
- package/src/file-provider-s3/changes.json +10 -1
- package/src/mail-transport-smtp/changes.json +7 -0
- package/src/personal-access-tokens/changes.json +6 -0
- package/src/secrets/changes.json +6 -0
- package/src/user/changes.json +6 -0
- package/src/user-data-rights/changes.json +6 -0
- package/dist/file-provider-s3-env/feature.d.ts +0 -3
- package/dist/file-provider-s3-env/feature.js +0 -46
- /package/dist/{files-provider-s3 → file-provider-s3/lib}/env-helper.d.ts +0 -0
- /package/dist/{files-provider-s3 → file-provider-s3/lib}/env-helper.js +0 -0
- /package/dist/{files-provider-s3 → file-provider-s3/lib}/s3-provider.d.ts +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { deleteMany, isUniqueViolation, runInOwnTransaction, } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
-
import { reraiseAsKumikoError } from "@cosmicdrift/kumiko-framework/errors";
|
|
2
|
+
import { reraiseAsKumikoError, UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
3
|
import { appendEventInTenantDb, getStreamVersionInTenantDb, } from "@cosmicdrift/kumiko-framework/event-store";
|
|
4
4
|
import { generateId } from "@cosmicdrift/kumiko-framework/utils";
|
|
5
5
|
import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
@@ -20,6 +20,8 @@ async function insertReservationRow(txDb, user, id, draft) {
|
|
|
20
20
|
expiresAt: now.add({ minutes: CAP_RESERVATION_TTL_MINUTES }),
|
|
21
21
|
createdAt: now,
|
|
22
22
|
settledAt: null,
|
|
23
|
+
consumedAmount: 0,
|
|
24
|
+
releasedAt: null,
|
|
23
25
|
});
|
|
24
26
|
}
|
|
25
27
|
function toHandle(ctx, id, outsideTransaction) {
|
|
@@ -99,12 +101,16 @@ async function confirmCapReservation(confirm, id) {
|
|
|
99
101
|
id,
|
|
100
102
|
tenantId: confirm.tenantId,
|
|
101
103
|
settledAt: null,
|
|
104
|
+
releasedAt: null,
|
|
102
105
|
});
|
|
103
106
|
}
|
|
104
107
|
async function giveBackCalendar(txDb, user, row) {
|
|
105
108
|
if (row.periodStartIso === null) {
|
|
106
109
|
throw new Error(`cap-counter: calendar reservation ${row.id} has no period to release against`);
|
|
107
110
|
}
|
|
111
|
+
// Fully consumed by consumeCalendarCapReservation: nothing left to give back.
|
|
112
|
+
if (row.amount === 0)
|
|
113
|
+
return NOTHING_TO_RELEASE;
|
|
108
114
|
return applyCapDeltaOn(txDb, user, { capName: row.capName, periodStartIso: row.periodStartIso, amount: row.amount }, "subtract");
|
|
109
115
|
}
|
|
110
116
|
async function giveBackRolling(txDb, user, row) {
|
|
@@ -125,21 +131,33 @@ async function giveBackReservation(txDb, user, id) {
|
|
|
125
131
|
if (!row)
|
|
126
132
|
return NOTHING_TO_RELEASE;
|
|
127
133
|
const claimed = await claimReservationRow(txDb, user, row);
|
|
128
|
-
if (!claimed)
|
|
134
|
+
if (!claimed || claimed.releasedAt !== null)
|
|
129
135
|
return NOTHING_TO_RELEASE;
|
|
130
|
-
const given =
|
|
131
|
-
? await giveBackRolling(txDb, user,
|
|
132
|
-
: await giveBackCalendar(txDb, user,
|
|
136
|
+
const given = claimed.kind === "rolling"
|
|
137
|
+
? await giveBackRolling(txDb, user, claimed)
|
|
138
|
+
: await giveBackCalendar(txDb, user, claimed);
|
|
133
139
|
if (!given.isSuccess)
|
|
134
140
|
return given;
|
|
135
|
-
|
|
141
|
+
// Only consumed units need remembering: a later settle or consume must not count them twice.
|
|
142
|
+
if (claimed.kind === "rolling" || claimed.consumedAmount === 0) {
|
|
143
|
+
await txDb.deleteMany(capReservationsTable, ownRow);
|
|
144
|
+
return given;
|
|
145
|
+
}
|
|
146
|
+
const now = Temporal.Now.instant();
|
|
147
|
+
await txDb.updateMany(capReservationsTable, {
|
|
148
|
+
amount: 0,
|
|
149
|
+
releasedAt: now,
|
|
150
|
+
expiresAt: now.add({ minutes: CAP_SETTLED_TOMBSTONE_TTL_MINUTES }),
|
|
151
|
+
}, ownRow);
|
|
136
152
|
return given;
|
|
137
153
|
}
|
|
138
|
-
// Waits for a confirm still holding the row;
|
|
139
|
-
//
|
|
154
|
+
// Waits for a confirm still holding the row; undefined means that confirm or a settle won (Postgres
|
|
155
|
+
// re-checks settled_at after the lock wait). A released row is claimed too: callers branch on
|
|
156
|
+
// releasedAt. The returned row is read after the lock wait, so it already includes a concurrent
|
|
157
|
+
// consume.
|
|
140
158
|
async function claimReservationRow(txDb, user, row) {
|
|
141
|
-
const claimed = await txDb.updateMany(capReservationsTable, { expiresAt: row.expiresAt }, { id: row.id, tenantId: user.tenantId, settledAt: null });
|
|
142
|
-
return claimed
|
|
159
|
+
const [claimed] = await txDb.updateMany(capReservationsTable, { expiresAt: row.expiresAt }, { id: row.id, tenantId: user.tenantId, settledAt: null });
|
|
160
|
+
return claimed;
|
|
143
161
|
}
|
|
144
162
|
export async function releaseCapReservation(ctx, id, outsideTransaction = true) {
|
|
145
163
|
const { user, writeDb } = resolveCapCounterScope(ctx, outsideTransaction);
|
|
@@ -147,6 +165,22 @@ export async function releaseCapReservation(ctx, id, outsideTransaction = true)
|
|
|
147
165
|
if (!released.isSuccess)
|
|
148
166
|
throw reraiseAsKumikoError(released.error);
|
|
149
167
|
}
|
|
168
|
+
export class CapSettleBelowConsumedError extends UnprocessableError {
|
|
169
|
+
constructor(capName, consumed, actualAmount) {
|
|
170
|
+
super("cap_settle_below_consumed", {
|
|
171
|
+
message: `Cap "${capName}": cannot settle ${actualAmount}, ${consumed} units were already consumed.`,
|
|
172
|
+
details: { capName, consumed, actualAmount },
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
export class CapReservationSettledError extends UnprocessableError {
|
|
177
|
+
constructor(capName) {
|
|
178
|
+
super("cap_reservation_settled", {
|
|
179
|
+
message: `Cap "${capName}": the reservation is already settled, consume is no longer possible.`,
|
|
180
|
+
details: { capName },
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
150
184
|
function bookCalendarDelta(txDb, user, period, delta) {
|
|
151
185
|
if (delta === 0)
|
|
152
186
|
return Promise.resolve({ isSuccess: true, data: {} });
|
|
@@ -158,8 +192,13 @@ async function settleInTransaction(txDb, user, id, options) {
|
|
|
158
192
|
...ownRow,
|
|
159
193
|
settledAt: null,
|
|
160
194
|
});
|
|
161
|
-
|
|
162
|
-
|
|
195
|
+
const claimed = open && (await claimReservationRow(txDb, user, open));
|
|
196
|
+
if (claimed) {
|
|
197
|
+
if (options.actualAmount < claimed.consumedAmount) {
|
|
198
|
+
throw new CapSettleBelowConsumedError(options.capName, claimed.consumedAmount, options.actualAmount);
|
|
199
|
+
}
|
|
200
|
+
const alreadyBooked = claimed.amount + claimed.consumedAmount;
|
|
201
|
+
const booked = await bookCalendarDelta(txDb, user, options, options.actualAmount - alreadyBooked);
|
|
163
202
|
if (!booked.isSuccess)
|
|
164
203
|
return booked;
|
|
165
204
|
const now = Temporal.Now.instant();
|
|
@@ -172,6 +211,11 @@ async function settleInTransaction(txDb, user, id, options) {
|
|
|
172
211
|
}
|
|
173
212
|
// Row swept or released: its amount was given back, so the full actual amount is booked and a
|
|
174
213
|
// tombstone makes a repeated settle a no-op. A parallel settle inserting first fails this insert.
|
|
214
|
+
// Booking first: a lost counter race returns a result instead of throwing, so the transaction
|
|
215
|
+
// commits and a tombstone written before it would turn the retry into a no-op.
|
|
216
|
+
const booked = await bookCalendarDelta(txDb, user, options, options.actualAmount);
|
|
217
|
+
if (!booked.isSuccess)
|
|
218
|
+
return booked;
|
|
175
219
|
const now = Temporal.Now.instant();
|
|
176
220
|
await txDb.insertOne(capReservationsTable, {
|
|
177
221
|
...ownRow,
|
|
@@ -182,8 +226,10 @@ async function settleInTransaction(txDb, user, id, options) {
|
|
|
182
226
|
expiresAt: now.add({ minutes: CAP_SETTLED_TOMBSTONE_TTL_MINUTES }),
|
|
183
227
|
createdAt: now,
|
|
184
228
|
settledAt: now,
|
|
229
|
+
consumedAmount: 0,
|
|
230
|
+
releasedAt: null,
|
|
185
231
|
});
|
|
186
|
-
return
|
|
232
|
+
return booked;
|
|
187
233
|
}
|
|
188
234
|
// The failed tombstone insert aborts the whole attempt, so nothing was booked; the repeat sees the
|
|
189
235
|
// winner's tombstone.
|
|
@@ -208,8 +254,87 @@ export async function settleCalendarCapReservation(ctx, id, options, outsideTran
|
|
|
208
254
|
if (!settled.isSuccess)
|
|
209
255
|
throw reraiseAsKumikoError(settled.error);
|
|
210
256
|
}
|
|
257
|
+
export class CapReservationOverConsumedError extends UnprocessableError {
|
|
258
|
+
constructor(capName, remaining, requested) {
|
|
259
|
+
super("cap_reservation_over_consumed", {
|
|
260
|
+
message: `Cap "${capName}": cannot consume ${requested} units, only ${remaining} still reserved.`,
|
|
261
|
+
details: { capName, remaining, requested },
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async function consumeClaimedRow(txDb, user, claimed, options) {
|
|
266
|
+
const ownRow = { id: claimed.id, tenantId: user.tenantId };
|
|
267
|
+
if (claimed.releasedAt !== null) {
|
|
268
|
+
// The reserved rest was given back, so each unit is booked on top of what is still counted.
|
|
269
|
+
const booked = await bookCalendarDelta(txDb, user, options, options.units);
|
|
270
|
+
if (!booked.isSuccess)
|
|
271
|
+
return booked;
|
|
272
|
+
await txDb.updateMany(capReservationsTable, { consumedAmount: claimed.consumedAmount + options.units }, ownRow);
|
|
273
|
+
return booked;
|
|
274
|
+
}
|
|
275
|
+
if (options.units > claimed.amount) {
|
|
276
|
+
throw new CapReservationOverConsumedError(options.capName, claimed.amount, options.units);
|
|
277
|
+
}
|
|
278
|
+
await txDb.updateMany(capReservationsTable, {
|
|
279
|
+
amount: claimed.amount - options.units,
|
|
280
|
+
consumedAmount: claimed.consumedAmount + options.units,
|
|
281
|
+
}, ownRow);
|
|
282
|
+
return NOTHING_TO_RELEASE;
|
|
283
|
+
}
|
|
284
|
+
// Row already expired away: the units are booked directly and a released tombstone remembers them
|
|
285
|
+
// so a later settle subtracts them. A parallel consume inserting first fails this insert.
|
|
286
|
+
async function consumeWithoutRow(txDb, user, id, options) {
|
|
287
|
+
// Booking first, for the same reason as settle's missing-row path.
|
|
288
|
+
const booked = await bookCalendarDelta(txDb, user, options, options.units);
|
|
289
|
+
if (!booked.isSuccess)
|
|
290
|
+
return booked;
|
|
291
|
+
const now = Temporal.Now.instant();
|
|
292
|
+
await txDb.insertOne(capReservationsTable, {
|
|
293
|
+
id,
|
|
294
|
+
tenantId: user.tenantId,
|
|
295
|
+
capName: options.capName,
|
|
296
|
+
kind: "calendar",
|
|
297
|
+
periodStartIso: options.periodStartIso,
|
|
298
|
+
amount: 0,
|
|
299
|
+
expiresAt: now.add({ minutes: CAP_SETTLED_TOMBSTONE_TTL_MINUTES }),
|
|
300
|
+
createdAt: now,
|
|
301
|
+
settledAt: null,
|
|
302
|
+
consumedAmount: options.units,
|
|
303
|
+
releasedAt: now,
|
|
304
|
+
});
|
|
305
|
+
return booked;
|
|
306
|
+
}
|
|
307
|
+
async function consumeInTransaction(txDb, user, id, options) {
|
|
308
|
+
const ownRow = { id, tenantId: user.tenantId };
|
|
309
|
+
const open = await txDb.fetchOne(capReservationsTable, {
|
|
310
|
+
...ownRow,
|
|
311
|
+
settledAt: null,
|
|
312
|
+
});
|
|
313
|
+
if (open && open.capName !== options.capName) {
|
|
314
|
+
throw new Error(`cap-counter: reservation ${id} belongs to cap "${open.capName}", not "${options.capName}"`);
|
|
315
|
+
}
|
|
316
|
+
const claimed = open && (await claimReservationRow(txDb, user, open));
|
|
317
|
+
if (claimed)
|
|
318
|
+
return consumeClaimedRow(txDb, user, claimed, options);
|
|
319
|
+
if (await txDb.fetchOne(capReservationsTable, ownRow)) {
|
|
320
|
+
throw new CapReservationSettledError(options.capName);
|
|
321
|
+
}
|
|
322
|
+
return consumeWithoutRow(txDb, user, id, options);
|
|
323
|
+
}
|
|
324
|
+
// Turns reserved units into booked ones without touching the counter; the row keeps the unconsumed
|
|
325
|
+
// rest so a release gives back exactly that. After a release or sweep the units are booked directly.
|
|
326
|
+
export async function consumeCalendarCapReservation(ctx, id, options, outsideTransaction = true) {
|
|
327
|
+
if (!Number.isInteger(options.units) || options.units < 1) {
|
|
328
|
+
throw new Error(`cap-counter: consume units must be a positive integer, got ${options.units}`);
|
|
329
|
+
}
|
|
330
|
+
const { user, writeDb } = resolveCapCounterScope(ctx, outsideTransaction);
|
|
331
|
+
const consumed = await retryCounterWriteOnVersionConflict(() => retryOnceOnTombstoneRace(() => runInOwnTransaction(writeDb, (txDb) => consumeInTransaction(txDb, user, id, options))));
|
|
332
|
+
if (!consumed.isSuccess)
|
|
333
|
+
throw reraiseAsKumikoError(consumed.error);
|
|
334
|
+
}
|
|
211
335
|
// A failed release leaves its row behind; giving it back once the TTL passed bounds how long that
|
|
212
|
-
// capacity stays counted. Expired settled tombstones are only deleted:
|
|
336
|
+
// capacity stays counted. Expired settled and released tombstones are only deleted: what they
|
|
337
|
+
// stand for stays booked.
|
|
213
338
|
export async function releaseExpiredCapReservations(ctx, capName, outsideTransaction = true) {
|
|
214
339
|
const { user, writeDb } = resolveCapCounterScope(ctx, outsideTransaction);
|
|
215
340
|
const expiredWhere = {
|
|
@@ -221,9 +346,14 @@ export async function releaseExpiredCapReservations(ctx, capName, outsideTransac
|
|
|
221
346
|
...expiredWhere,
|
|
222
347
|
settledAt: { ne: null },
|
|
223
348
|
});
|
|
349
|
+
await writeDb.deleteMany(capReservationsTable, {
|
|
350
|
+
...expiredWhere,
|
|
351
|
+
releasedAt: { ne: null },
|
|
352
|
+
});
|
|
224
353
|
const expired = await writeDb.selectMany(capReservationsTable, {
|
|
225
354
|
...expiredWhere,
|
|
226
355
|
settledAt: null,
|
|
356
|
+
releasedAt: null,
|
|
227
357
|
});
|
|
228
358
|
for (const { id } of expired) {
|
|
229
359
|
try {
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
export declare function createCapCounterFeature(): import("@cosmicdrift/kumiko-types/feature").FeatureDefinition & {
|
|
2
|
+
readonly exports: void;
|
|
3
|
+
};
|
|
4
|
+
/** @deprecated Use createCapCounterFeature(). Run `bun kumiko-upgrade --apply`. Removed in the first release after 2026-10-29. */
|
|
1
5
|
export declare const capCounterFeature: import("@cosmicdrift/kumiko-types/feature").FeatureDefinition & {
|
|
2
6
|
readonly exports: void;
|
|
3
7
|
};
|
|
@@ -53,71 +53,77 @@ import { incrementRollingCapHandler, rollingIncrementedSchema, rollingReleasedSc
|
|
|
53
53
|
import { markSoftWarnedHandler } from "./handlers/mark-soft-warned.write.js";
|
|
54
54
|
import { CAP_COUNTER_I18N } from "./i18n.js";
|
|
55
55
|
import { capReservationsTableMeta } from "./tables.js";
|
|
56
|
-
export
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
56
|
+
export function createCapCounterFeature() {
|
|
57
|
+
return defineFeature(CAP_COUNTER_FEATURE, (r) => {
|
|
58
|
+
r.describe("Tracks per-tenant usage against configurable limits using two complementary storage models: calendar-period counters (one projection row per tenant/capName/period, reset implicitly by period rollover) and rolling-window counters (append-only event stream, no projection). Use `enforceCap` / `enforceRollingCap` (or the `withCapEnforcement` / `withRollingCapEnforcement` handler wrappers) in your write-handlers to check limits with soft-warn and hard-block tolerances; call `enforceCapAndMaybeNotify` when you also want to trigger a delivery notification on soft-threshold hits.");
|
|
59
|
+
r.uiHints({
|
|
60
|
+
displayLabel: "Cap Counter · Usage Limits",
|
|
61
|
+
category: "operations",
|
|
62
|
+
recommended: false,
|
|
63
|
+
});
|
|
64
|
+
r.entity({ name: "cap-counter", ...capCounterEntity });
|
|
65
|
+
r.storeTable(capReservationsTableMeta, {
|
|
66
|
+
reason: "coordination.cap_reservation_ledger",
|
|
67
|
+
});
|
|
68
|
+
// Custom domain event for the rolling counter. r.defineEvent registers the
|
|
69
|
+
// schema with the registry; ctx.unsafeAppendEvent in the handler reuses the
|
|
70
|
+
// same schema for append-time validation. QN after prefixing:
|
|
71
|
+
// "cap-counter:event:rolling-incremented" (see
|
|
72
|
+
// ROLLING_INCREMENTED_EVENT_QN).
|
|
73
|
+
r.defineEvent(ROLLING_INCREMENTED_EVENT_SHORT, rollingIncrementedSchema, {
|
|
74
|
+
piiFields: "none",
|
|
75
|
+
});
|
|
76
|
+
r.defineEvent(ROLLING_RELEASED_EVENT_SHORT, rollingReleasedSchema, { piiFields: "none" });
|
|
77
|
+
// Custom write-handlers.
|
|
78
|
+
// - increment: calendar period (CRUD via projection row).
|
|
79
|
+
// - increment-rolling: rolling window (custom event, no projection).
|
|
80
|
+
// - mark-soft-warned: anti-notification-storm flag (calendar only).
|
|
81
|
+
r.writeHandler(incrementCapHandler);
|
|
82
|
+
r.writeHandler(incrementRollingCapHandler);
|
|
83
|
+
r.writeHandler(markSoftWarnedHandler);
|
|
84
|
+
// Custom + standard reads. Sysadmin cross-tenant via list, per-tenant
|
|
85
|
+
// single row via get-counter. Detail-by-id handler left out on purpose
|
|
86
|
+
// (no use case; the natural lookup is capName + period, not the
|
|
87
|
+
// aggregate id).
|
|
88
|
+
r.queryHandler(defineEntityListHandler("cap-counter", capCounterEntity, {
|
|
89
|
+
access: { roles: access.systemAdmin },
|
|
90
|
+
escapeHatch: {
|
|
91
|
+
reason: "SystemAdmin quota operator view lists usage counters of every tenant to see who consumes which cap",
|
|
92
|
+
},
|
|
93
|
+
description: "Lists usage-counter rows across all tenants with cap name, current value, period start and soft-warn timestamp; use it as the operator view of who is consuming which quota.",
|
|
94
|
+
}));
|
|
95
|
+
r.queryHandler(getCounterQuery);
|
|
96
|
+
r.screen({
|
|
97
|
+
id: CAP_COUNTER_LIST_SCREEN_ID,
|
|
98
|
+
type: "entityList",
|
|
99
|
+
entity: "cap-counter",
|
|
100
|
+
columns: [
|
|
101
|
+
"tenantId",
|
|
102
|
+
"capName",
|
|
103
|
+
"value",
|
|
104
|
+
"periodStart",
|
|
105
|
+
{ field: "lastSoftWarnedAt", hideOnNarrow: true },
|
|
106
|
+
],
|
|
107
|
+
defaultSort: { field: "capName", dir: "asc" },
|
|
108
|
+
// Search resolves against the caller's own tenant stream, so on this
|
|
109
|
+
// cross-tenant operator list it would only ever hit the SystemAdmin's tenant.
|
|
110
|
+
searchable: false,
|
|
111
|
+
access: { roles: ["SystemAdmin"] },
|
|
112
|
+
});
|
|
113
|
+
r.nav({
|
|
114
|
+
id: "cap-list",
|
|
115
|
+
label: "cap-counter:nav.cap-list",
|
|
116
|
+
screen: "cap-counter:screen:cap-list",
|
|
117
|
+
icon: "gauge",
|
|
118
|
+
order: 60,
|
|
119
|
+
});
|
|
120
|
+
r.translations({
|
|
121
|
+
keys: {
|
|
122
|
+
...CAP_COUNTER_I18N,
|
|
123
|
+
"cap-counter:nav.cap-list": { de: "Limits", en: "Caps" },
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}, { dedupeOptions: {} });
|
|
127
|
+
}
|
|
128
|
+
/** @deprecated Use createCapCounterFeature(). Run `bun kumiko-upgrade --apply`. Removed in the first release after 2026-10-29. */
|
|
129
|
+
export const capCounterFeature = createCapCounterFeature();
|
|
@@ -60,10 +60,9 @@ export const incrementRollingCapHandler = {
|
|
|
60
60
|
// existing increment.write.ts-Cast — gleiche dispatcher-boundary.
|
|
61
61
|
const payload = event.payload;
|
|
62
62
|
const aggregateId = rollingCapAggregateId(event.user.tenantId, payload.capName);
|
|
63
|
-
// unsafeAppendEvent
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// registriert hat.
|
|
63
|
+
// unsafeAppendEvent: the strict appendEvent is bound to an app's own event map, which
|
|
64
|
+
// bundled-features cannot see. Schema validation still runs because r.defineEvent
|
|
65
|
+
// registered the schema.
|
|
67
66
|
await ctx.unsafeAppendEvent({
|
|
68
67
|
aggregateId,
|
|
69
68
|
aggregateType: CAP_COUNTER_ROLLING_AGGREGATE_TYPE,
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
export { capCounterAggregateId, rollingCapAggregateId } from "./aggregate-id.js";
|
|
2
2
|
export { type BookCapUsageOptions, bookCapUsage, type MarkCapSoftWarnedOptions, markCapSoftWarned, type ReadRollingCapUsageOptions, readRollingCapUsage, } from "./book-cap-usage.js";
|
|
3
|
-
export { type CalendarCapUsageReservation, type CalendarCapUsageReservationSpec, reserveCalendarCapUsage, } from "./calendar-cap-usage-reservation.js";
|
|
3
|
+
export { type CalendarCapReservationHandle, type CalendarCapReservationHandleSpec, type CalendarCapUsageReservation, type CalendarCapUsageReservationSpec, calendarCapReservationHandle, reserveCalendarCapUsage, } from "./calendar-cap-usage-reservation.js";
|
|
4
|
+
export { CapReservationOverConsumedError, CapReservationSettledError, CapSettleBelowConsumedError, } from "./cap-reservation.js";
|
|
4
5
|
export { CAP_COUNTER_FEATURE, CAP_COUNTER_ROLLING_AGGREGATE_TYPE, CapCounterHandlers, CapCounterQueries, ROLLING_INCREMENTED_EVENT_QN, ROLLING_INCREMENTED_EVENT_SHORT, ROLLING_RELEASED_EVENT_QN, ROLLING_RELEASED_EVENT_SHORT, } from "./constants.js";
|
|
5
6
|
export { CAP_TOLERANCES, CapExceededError, type CapToleranceProfile, type CapToleranceProfileName, calendarMonthPeriodEndIso, currentCalendarMonthStartIso, type EnforceCapResult, enforceCap, enforceCapAndMaybeNotify, enforceRollingCap, enforceRollingCapAndMaybeNotify, enforceStockCap, type SoftHitNotifier, type StockCapResult, } from "./enforce-cap.js";
|
|
6
7
|
export { capCounterEntity } from "./entity.js";
|
|
7
|
-
export { capCounterFeature } from "./feature.js";
|
|
8
|
+
export { capCounterFeature, createCapCounterFeature } from "./feature.js";
|
|
8
9
|
export { readPayloadField } from "./payload-field.js";
|
|
9
10
|
export { type CapGuardContext, type CapLimitContext, createStockCapGuard, type StockCapGuard, type StockCapSpec, } from "./stock-cap-guard.js";
|
|
10
11
|
export { capReservationsTable, capReservationsTableMeta } from "./tables.js";
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// Public API of the cap-counter bundled-feature.
|
|
2
2
|
export { capCounterAggregateId, rollingCapAggregateId } from "./aggregate-id.js";
|
|
3
3
|
export { bookCapUsage, markCapSoftWarned, readRollingCapUsage, } from "./book-cap-usage.js";
|
|
4
|
-
export { reserveCalendarCapUsage, } from "./calendar-cap-usage-reservation.js";
|
|
4
|
+
export { calendarCapReservationHandle, reserveCalendarCapUsage, } from "./calendar-cap-usage-reservation.js";
|
|
5
|
+
export { CapReservationOverConsumedError, CapReservationSettledError, CapSettleBelowConsumedError, } from "./cap-reservation.js";
|
|
5
6
|
export { CAP_COUNTER_FEATURE, CAP_COUNTER_ROLLING_AGGREGATE_TYPE, CapCounterHandlers, CapCounterQueries, ROLLING_INCREMENTED_EVENT_QN, ROLLING_INCREMENTED_EVENT_SHORT, ROLLING_RELEASED_EVENT_QN, ROLLING_RELEASED_EVENT_SHORT, } from "./constants.js";
|
|
6
7
|
export { CAP_TOLERANCES, CapExceededError, calendarMonthPeriodEndIso, currentCalendarMonthStartIso, enforceCap, enforceCapAndMaybeNotify, enforceRollingCap, enforceRollingCapAndMaybeNotify, enforceStockCap, } from "./enforce-cap.js";
|
|
7
8
|
export { capCounterEntity } from "./entity.js";
|
|
8
|
-
export { capCounterFeature } from "./feature.js";
|
|
9
|
+
export { capCounterFeature, createCapCounterFeature } from "./feature.js";
|
|
9
10
|
export { readPayloadField } from "./payload-field.js";
|
|
10
11
|
export { createStockCapGuard, } from "./stock-cap-guard.js";
|
|
11
12
|
export { capReservationsTable, capReservationsTableMeta } from "./tables.js";
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { defineUnmanagedTable, index, instant, integer, table as pgTable, text, uuid, } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
const CAP_RESERVATIONS_TENANT_CAP_EXPIRES_INDEX = "store_cap_reservations_tenant_id_cap_name_expires_at_idx";
|
|
3
3
|
// One row per capacity that was booked before a handler transaction and is not settled yet. The
|
|
4
|
-
// handler transaction deletes it on its own commit (confirm)
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// handler transaction deletes it on its own commit (confirm). While a calendar row is open the
|
|
5
|
+
// counter holds amount + consumed_amount for it. A release or the TTL sweep gives back amount and
|
|
6
|
+
// keeps the row as a released tombstone (released_at set, amount 0, consumed_amount still booked)
|
|
7
|
+
// until its shortened TTL passes, so a later consume or settle accounts for the consumed units.
|
|
8
|
+
// A settle keeps the row as a settled tombstone (settled_at set) the same way, so a repeated settle
|
|
9
|
+
// or a late release finds it and does nothing.
|
|
7
10
|
// Unmanaged: the rows are a coordination aid, not state with an event history.
|
|
8
11
|
export const capReservationsTable = pgTable("store_cap_reservations", {
|
|
9
12
|
id: uuid("id").primaryKey(),
|
|
@@ -16,6 +19,8 @@ export const capReservationsTable = pgTable("store_cap_reservations", {
|
|
|
16
19
|
expiresAt: instant("expires_at").notNull(),
|
|
17
20
|
createdAt: instant("created_at").notNull(),
|
|
18
21
|
settledAt: instant("settled_at"),
|
|
22
|
+
consumedAmount: integer("consumed_amount").notNull().default(0),
|
|
23
|
+
releasedAt: instant("released_at"),
|
|
19
24
|
}, (t) => [index(CAP_RESERVATIONS_TENANT_CAP_EXPIRES_INDEX).on(t.tenantId, t.capName, t.expiresAt)]);
|
|
20
25
|
export const capReservationsTableMeta = defineUnmanagedTable({
|
|
21
26
|
tableName: "store_cap_reservations",
|
|
@@ -35,5 +40,7 @@ export const capReservationsTableMeta = defineUnmanagedTable({
|
|
|
35
40
|
{ name: "expires_at", pgType: "timestamptz", notNull: true },
|
|
36
41
|
{ name: "created_at", pgType: "timestamptz", notNull: true },
|
|
37
42
|
{ name: "settled_at", pgType: "timestamptz", notNull: false },
|
|
43
|
+
{ name: "consumed_amount", pgType: "integer", notNull: true, defaultSql: "0" },
|
|
44
|
+
{ name: "released_at", pgType: "timestamptz", notNull: false },
|
|
38
45
|
],
|
|
39
46
|
});
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { selectMany } from "@cosmicdrift/kumiko-framework/
|
|
2
|
-
import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
|
|
1
|
+
import { buildEntityTable, selectMany } from "@cosmicdrift/kumiko-framework/db";
|
|
3
2
|
import { defineQueryHandler, mayOverrideTenant, } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
3
|
import { AccessDeniedError, FrameworkReasons, InternalError, } from "@cosmicdrift/kumiko-framework/errors";
|
|
5
4
|
import * as z from "zod";
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { selectMany } from "@cosmicdrift/kumiko-framework/
|
|
2
|
-
import { buildEntityTable, decodeCursor, encodeCursor } from "@cosmicdrift/kumiko-framework/db";
|
|
1
|
+
import { buildEntityTable, decodeCursor, encodeCursor, selectMany, } from "@cosmicdrift/kumiko-framework/db";
|
|
3
2
|
import { definePagedQueryHandler, MAX_LIST_LIMIT, } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
3
|
import { InternalError, ValidationError } from "@cosmicdrift/kumiko-framework/errors";
|
|
5
4
|
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { selectMany } from "@cosmicdrift/kumiko-framework/
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
4
4
|
import * as z from "zod";
|
|
@@ -2,15 +2,23 @@ import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
|
2
2
|
import { type ChannelContext, type DeliveryChannel, type EmailAttachmentRef } from "../delivery/index.js";
|
|
3
3
|
import type { EmailTransport } from "./types.js";
|
|
4
4
|
export declare function fileAttachmentsFrom(data: Readonly<Record<string, unknown>> | undefined, messageLocale?: string): readonly EmailAttachmentRef[];
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/** Resolves the sending tenant's own transport, null when it has none. Built per send and
|
|
6
|
+
* never cached, so a provider change applies to the next mail; a throw fails the send. */
|
|
7
|
+
export type TenantTransportResolver = (ctx: ChannelContext) => Promise<EmailTransport | null>;
|
|
8
|
+
type EmailChannelTransports = {
|
|
9
|
+
/** The app-wide transport for every mail that does not go through a tenant transport. */
|
|
7
10
|
readonly transport: EmailTransport;
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
/** Used only for notifications declared with `tenantTransport: true`. */
|
|
12
|
+
readonly tenantTransport?: TenantTransportResolver;
|
|
13
|
+
} | {
|
|
14
|
+
readonly transport?: undefined;
|
|
15
|
+
readonly tenantTransport: TenantTransportResolver;
|
|
16
|
+
};
|
|
17
|
+
export type EmailChannelOptions = EmailChannelTransports & {
|
|
11
18
|
readonly resolveEmail: (userId: string, ctx: {
|
|
12
19
|
db: unknown;
|
|
13
20
|
tenantId: TenantId;
|
|
14
21
|
}) => Promise<string | null>;
|
|
15
22
|
};
|
|
16
23
|
export declare function createEmailChannel(options: EmailChannelOptions): DeliveryChannel;
|
|
24
|
+
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PII_ERASED_SENTINEL } from "@cosmicdrift/kumiko-framework/crypto";
|
|
2
2
|
import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
|
|
3
3
|
import * as z from "zod";
|
|
4
|
-
import { DELIVERY_UNSUBSCRIBE_ONE_CLICK_HEADER_VALUE, DELIVERY_UNSUBSCRIBE_PATH, EMAIL_CHANNEL_NAME, fileAttachmentRefSchema, MAX_EMAIL_ATTACHMENTS, renderedPdfAttachmentRefSchema, } from "../delivery/index.js";
|
|
4
|
+
import { currentNotificationType, DELIVERY_UNSUBSCRIBE_ONE_CLICK_HEADER_VALUE, DELIVERY_UNSUBSCRIBE_PATH, EMAIL_CHANNEL_NAME, fileAttachmentRefSchema, MAX_EMAIL_ATTACHMENTS, renderedPdfAttachmentRefSchema, } from "../delivery/index.js";
|
|
5
5
|
import { decryptStoredPii, extensionForResourceContentType } from "../shared/index.js";
|
|
6
6
|
import { guardEmailMessage } from "./pii-guard.js";
|
|
7
7
|
// Envelope (From / Reply-To / threading headers) rides on the channel data —
|
|
@@ -121,6 +121,10 @@ async function resolveAttachments(sources, ctx) {
|
|
|
121
121
|
}
|
|
122
122
|
return { kind: "resolved", attachments: resolved };
|
|
123
123
|
}
|
|
124
|
+
function isTenantTransportNotification(ctx, notificationType) {
|
|
125
|
+
const current = currentNotificationType(ctx.registry, notificationType);
|
|
126
|
+
return ctx.registry.getAllNotifications().get(current)?.tenantTransport === true;
|
|
127
|
+
}
|
|
124
128
|
export function createEmailChannel(options) {
|
|
125
129
|
const { transport, resolveEmail, tenantTransport } = options;
|
|
126
130
|
// Render runs in the delivery.render job, decoupled from the SMTP send so
|
|
@@ -150,9 +154,11 @@ export function createEmailChannel(options) {
|
|
|
150
154
|
...(attachments !== undefined && { attachments }),
|
|
151
155
|
};
|
|
152
156
|
}
|
|
153
|
-
async function transportFor(ctx) {
|
|
157
|
+
async function transportFor(message, ctx) {
|
|
154
158
|
if (tenantTransport === undefined)
|
|
155
159
|
return transport;
|
|
160
|
+
if (!isTenantTransportNotification(ctx, message.notificationType))
|
|
161
|
+
return transport;
|
|
156
162
|
return (await tenantTransport(ctx)) ?? transport;
|
|
157
163
|
}
|
|
158
164
|
return {
|
|
@@ -189,7 +195,15 @@ export function createEmailChannel(options) {
|
|
|
189
195
|
return { status: "failed", error: "send_failed", address };
|
|
190
196
|
}
|
|
191
197
|
const resolvedAttachments = resolution.attachments;
|
|
192
|
-
const sendTransport = await transportFor(ctx);
|
|
198
|
+
const sendTransport = await transportFor(message, ctx);
|
|
199
|
+
if (sendTransport === undefined) {
|
|
200
|
+
// Configuration, not a transient error: a retry cannot conjure a transport.
|
|
201
|
+
createFallbackLogger("delivery").warn("no mail transport for this mail, send failed", {
|
|
202
|
+
tenantId: ctx.tenantId,
|
|
203
|
+
notificationType: message.notificationType,
|
|
204
|
+
});
|
|
205
|
+
return { status: "failed", error: "missing_credentials", address };
|
|
206
|
+
}
|
|
193
207
|
await sendTransport.send(guardEmailMessage({
|
|
194
208
|
to: address,
|
|
195
209
|
subject,
|
|
@@ -4,7 +4,7 @@ import { createEmailChannel } from "./email-channel.js";
|
|
|
4
4
|
export function createChannelEmailFeature(options) {
|
|
5
5
|
const channel = createEmailChannel(options);
|
|
6
6
|
return defineFeature("channel-email", (r) => {
|
|
7
|
-
r.describe("Wires an `EmailTransport` (typically `mail-transport-smtp` in production, `createInMemoryTransport()` in tests) into the delivery system as the `email` channel. Requires `delivery`; pass an `EmailChannelOptions` with a `transport` and a `resolveEmail` function that maps a user ID to their email address. Mails render from stored templates (`renderer-mail-html`): a notification routed to this channel needs `template` + `meta` on `r.notification`, otherwise its delivery fails. Pass `tenantTransport: tenantMailTransport` (from `mail-foundation`) to send
|
|
7
|
+
r.describe("Wires an `EmailTransport` (typically `mail-transport-smtp` in production, `createInMemoryTransport()` in tests) into the delivery system as the `email` channel. Requires `delivery`; pass an `EmailChannelOptions` with a `transport` and a `resolveEmail` function that maps a user ID to their email address. Mails render from stored templates (`renderer-mail-html`): a notification routed to this channel needs `template` + `meta` on `r.notification`, otherwise its delivery fails. Pass `tenantTransport: tenantMailTransport` (from `mail-foundation`) to send the notifications declared with `tenantTransport: true` through the sending tenant's own configured provider: a broken provider fails the send, a tenant without one uses the app `transport`; every other notification (password reset, sign-in) always goes through the app `transport`. `transport` may be left out when `tenantTransport` is set; a mail that then has no transport fails with `missing_credentials`. Notifications declared with `unsubscribe: true` get a signed `data.unsubscribeUrl` from delivery, which turns into the `List-Unsubscribe` headers.");
|
|
8
8
|
r.uiHints({
|
|
9
9
|
displayLabel: "Email Channel",
|
|
10
10
|
category: "notifications",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createEmailChannel, type EmailChannelOptions } from "./email-channel.js";
|
|
1
|
+
export { createEmailChannel, type EmailChannelOptions, type TenantTransportResolver, } from "./email-channel.js";
|
|
2
2
|
export { createChannelEmailFeature } from "./feature.js";
|
|
3
3
|
export { guardEmailMessage, withPiiCiphertextGuard } from "./pii-guard.js";
|
|
4
4
|
export { createSmtpTransport, createSmtpTransportFromEnv, type SmtpEnv, type SmtpTransportOptions, } from "./smtp-transport.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createEmailChannel } from "./email-channel.js";
|
|
1
|
+
export { createEmailChannel, } from "./email-channel.js";
|
|
2
2
|
export { createChannelEmailFeature } from "./feature.js";
|
|
3
3
|
export { guardEmailMessage, withPiiCiphertextGuard } from "./pii-guard.js";
|
|
4
4
|
export { createSmtpTransport, createSmtpTransportFromEnv, } from "./smtp-transport.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { selectMany } from "@cosmicdrift/kumiko-framework/
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import * as z from "zod";
|
|
4
4
|
import { inAppMessagesTable } from "../tables.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { updateMany } from "@cosmicdrift/kumiko-framework/
|
|
1
|
+
import { updateMany } from "@cosmicdrift/kumiko-framework/db";
|
|
2
2
|
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
3
3
|
import * as z from "zod";
|
|
4
4
|
import { inAppMessagesTable } from "../tables.js";
|