@cosmicdrift/kumiko-bundled-features 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/__tests__/env-schemas.test.ts +1 -1
- package/src/__tests__/es-ops-e2e.integration.ts +10 -9
- package/src/audit/__tests__/audit.integration.ts +3 -3
- package/src/audit/handlers/list.query.ts +39 -51
- package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.ts +4 -3
- package/src/auth-email-password/__tests__/account-lockout.integration.ts +4 -3
- package/src/auth-email-password/__tests__/auth-claims.integration.ts +5 -4
- package/src/auth-email-password/__tests__/auth.integration.ts +4 -3
- package/src/auth-email-password/__tests__/confirm-token-flow.test.ts +1 -1
- package/src/auth-email-password/__tests__/email-templates.test.ts +1 -1
- package/src/auth-email-password/__tests__/email-verification.integration.ts +7 -10
- package/src/auth-email-password/__tests__/identity-v3-hash.test.ts +1 -1
- package/src/auth-email-password/__tests__/identity-v3-login.integration.ts +4 -3
- package/src/auth-email-password/__tests__/invite-flow.integration.ts +16 -43
- package/src/auth-email-password/__tests__/multi-roles.integration.ts +6 -9
- package/src/auth-email-password/__tests__/password-reset.integration.ts +8 -7
- package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.ts +4 -3
- package/src/auth-email-password/__tests__/seed-admin.integration.ts +19 -32
- package/src/auth-email-password/__tests__/session-callbacks.integration.ts +6 -5
- package/src/auth-email-password/__tests__/session-strict-mode.integration.ts +1 -1
- package/src/auth-email-password/__tests__/signed-token.test.ts +1 -1
- package/src/auth-email-password/__tests__/signup-flow.integration.ts +11 -15
- package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +26 -26
- package/src/auth-email-password/handlers/invite-accept.write.ts +24 -21
- package/src/auth-email-password/handlers/invite-create.write.ts +3 -8
- package/src/auth-email-password/handlers/invite-signup-complete.write.ts +20 -17
- package/src/auth-email-password/handlers/signup-confirm.write.ts +3 -7
- package/src/auth-email-password/seeding.ts +1 -1
- package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +1 -2
- package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +10 -19
- package/src/auth-email-password/web/__tests__/login-screen.test.tsx +12 -18
- package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +12 -17
- package/src/auth-email-password/web/__tests__/session-roles.test.ts +1 -1
- package/src/auth-email-password/web/__tests__/tenant-switcher.test.tsx +1 -8
- package/src/auth-email-password/web/__tests__/test-utils.tsx +4 -8
- package/src/auth-email-password/web/__tests__/user-menu.test.tsx +2 -8
- package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +10 -15
- package/src/billing-foundation/__tests__/billing-foundation.integration.ts +1 -1
- package/src/billing-foundation/__tests__/feature.test.ts +1 -1
- package/src/billing-foundation/__tests__/webhook-handler.test.ts +6 -5
- package/src/billing-foundation/db/queries/subscription-projection.ts +15 -0
- package/src/billing-foundation/get-subscription-for-tenant.ts +2 -6
- package/src/billing-foundation/handlers/create-portal-session.write.ts +2 -2
- package/src/billing-foundation/handlers/list-subscriptions.query.ts +4 -1
- package/src/billing-foundation/projection.ts +32 -13
- package/src/cap-counter/__tests__/cap-counter.integration.ts +1 -1
- package/src/cap-counter/__tests__/enforce-cap.test.ts +37 -32
- package/src/cap-counter/__tests__/with-cap-enforcement.integration.ts +1 -1
- package/src/cap-counter/enforce-cap.ts +14 -20
- package/src/cap-counter/handlers/get-counter.query.ts +7 -13
- package/src/cap-counter/handlers/increment.write.ts +2 -2
- package/src/cap-counter/handlers/mark-soft-warned.write.ts +2 -2
- package/src/channel-in-app/handlers/inbox.query.ts +7 -13
- package/src/channel-in-app/handlers/mark-all-read.write.ts +7 -9
- package/src/channel-in-app/handlers/mark-read.write.ts +8 -14
- package/src/channel-in-app/handlers/unread-count.query.ts +10 -9
- package/src/channel-in-app/in-app-channel.ts +10 -12
- package/src/channel-in-app/tables.ts +1 -1
- package/src/compliance-profiles/__tests__/compliance-profiles.integration.ts +1 -1
- package/src/compliance-profiles/__tests__/seeding.integration.ts +1 -1
- package/src/compliance-profiles/_internal/parse-override.ts +19 -0
- package/src/compliance-profiles/handlers/for-tenant.query.ts +10 -32
- package/src/compliance-profiles/handlers/needs-profile.query.ts +4 -7
- package/src/compliance-profiles/handlers/set-profile.write.ts +5 -7
- package/src/compliance-profiles/resolve-for-tenant.ts +11 -27
- package/src/compliance-profiles/schema/profile-selection.ts +2 -2
- package/src/compliance-profiles/seeding.ts +4 -7
- package/src/config/__tests__/app-overrides.test.ts +1 -1
- package/src/config/__tests__/cascade.integration.ts +1 -1
- package/src/config/__tests__/config.integration.ts +8 -27
- package/src/config/db/queries/resolver.ts +47 -0
- package/src/config/handlers/__tests__/prepare-config-write.test.ts +1 -1
- package/src/config/resolver.ts +14 -62
- package/src/config/table.ts +4 -4
- package/src/config/write-helpers.ts +7 -11
- package/src/custom-fields/__tests__/audit-integration.integration.ts +6 -6
- package/src/custom-fields/__tests__/custom-fields.integration.ts +7 -7
- package/src/custom-fields/__tests__/feature.test.ts +1 -1
- package/src/custom-fields/__tests__/field-access.integration.ts +6 -6
- package/src/custom-fields/__tests__/quota.integration.ts +6 -6
- package/src/custom-fields/__tests__/retention.integration.ts +12 -10
- package/src/custom-fields/__tests__/user-data-rights.integration.ts +27 -17
- package/src/custom-fields/__tests__/wire-for-entity.test.ts +5 -5
- package/src/custom-fields/db/queries/field-access.ts +16 -0
- package/src/custom-fields/db/queries/projection.ts +43 -0
- package/src/custom-fields/db/queries/quota.ts +14 -0
- package/src/custom-fields/db/queries/retention.ts +39 -0
- package/src/custom-fields/db/queries/user-data-rights.ts +54 -0
- package/src/custom-fields/lib/field-access.ts +2 -41
- package/src/custom-fields/lib/quota.ts +2 -25
- package/src/custom-fields/run-retention.ts +19 -21
- package/src/custom-fields/wire-for-entity.ts +30 -23
- package/src/custom-fields/wire-user-data-rights.ts +33 -85
- package/src/data-retention/__tests__/data-retention.integration.ts +1 -1
- package/src/data-retention/__tests__/keep-for.test.ts +1 -1
- package/src/data-retention/__tests__/override-schema.test.ts +1 -1
- package/src/data-retention/__tests__/policy-for.integration.ts +1 -1
- package/src/data-retention/__tests__/resolver.test.ts +1 -1
- package/src/data-retention/handlers/policy-for.query.ts +5 -8
- package/src/data-retention/resolve-for-tenant.ts +6 -8
- package/src/data-retention/schema/tenant-retention-override.ts +2 -2
- package/src/delivery/__tests__/delivery-events.integration.ts +8 -21
- package/src/delivery/__tests__/delivery.integration.ts +100 -190
- package/src/delivery/db/queries/preferences.ts +30 -0
- package/src/delivery/delivery-service.ts +8 -36
- package/src/delivery/feature.ts +10 -2
- package/src/delivery/handlers/log.query.ts +5 -7
- package/src/delivery/handlers/preferences.query.ts +2 -5
- package/src/delivery/tables.ts +26 -1
- package/src/delivery/upsert-preference.ts +8 -14
- package/src/feature-toggles/__tests__/feature-toggles.integration.ts +30 -30
- package/src/feature-toggles/__tests__/registered-system-tenant.test.ts +7 -6
- package/src/feature-toggles/db/queries/toggle-state.ts +25 -0
- package/src/feature-toggles/feature.ts +16 -2
- package/src/feature-toggles/global-feature-state-table.ts +1 -1
- package/src/feature-toggles/handlers/list.query.ts +9 -2
- package/src/feature-toggles/handlers/registered.query.ts +3 -7
- package/src/feature-toggles/handlers/set.write.ts +37 -25
- package/src/feature-toggles/toggle-runtime.ts +3 -6
- package/src/file-foundation/__tests__/feature.test.ts +1 -1
- package/src/file-foundation/__tests__/file-foundation.integration.ts +1 -1
- package/src/file-provider-inmemory/__tests__/feature.test.ts +1 -1
- package/src/file-provider-s3/__tests__/feature.test.ts +1 -1
- package/src/files/__tests__/files.integration.ts +18 -7
- package/src/files/schema/file-ref.ts +1 -1
- package/src/files-provider-s3/__tests__/env-helper.test.ts +1 -1
- package/src/files-provider-s3/__tests__/s3-provider.integration.ts +1 -1
- package/src/files-provider-s3/__tests__/s3-provider.test.ts +1 -1
- package/src/jobs/__tests__/job-system-user.integration.ts +1 -1
- package/src/jobs/__tests__/jobs-events.integration.ts +8 -21
- package/src/jobs/__tests__/jobs-feature.integration.ts +1 -1
- package/src/jobs/feature.ts +26 -15
- package/src/jobs/handlers/detail.query.ts +10 -8
- package/src/jobs/handlers/list.query.ts +9 -21
- package/src/jobs/handlers/retry.write.ts +2 -7
- package/src/jobs/job-run-logger.ts +3 -9
- package/src/jobs/job-run-table.ts +49 -17
- package/src/legal-pages/__tests__/legal-pages.integration.ts +1 -1
- package/src/mail-foundation/__tests__/feature.test.ts +1 -1
- package/src/mail-foundation/__tests__/mail-foundation.integration.ts +1 -1
- package/src/mail-transport-inmemory/__tests__/feature.test.ts +1 -1
- package/src/mail-transport-smtp/__tests__/feature.test.ts +1 -1
- package/src/rate-limiting/__tests__/rate-limiting.integration.ts +1 -1
- package/src/renderer-foundation/__tests__/api.test.ts +2 -2
- package/src/renderer-foundation/__tests__/collect-plugins.integration.ts +1 -1
- package/src/renderer-simple/__tests__/adapter.test.ts +2 -2
- package/src/renderer-simple/__tests__/simple-renderer.test.ts +1 -1
- package/src/secrets/__tests__/require-secrets-context.test.ts +6 -5
- package/src/secrets/__tests__/rotate.integration.ts +6 -9
- package/src/secrets/__tests__/secrets-events.integration.ts +6 -12
- package/src/secrets/__tests__/secrets.integration.ts +6 -11
- package/src/secrets/db/queries/read.ts +16 -0
- package/src/secrets/handlers/list.query.ts +16 -17
- package/src/secrets/handlers/rotate.job.ts +8 -12
- package/src/secrets/secrets-context.ts +9 -21
- package/src/secrets/table.ts +1 -1
- package/src/sessions/__tests__/cleanup.integration.ts +8 -6
- package/src/sessions/__tests__/password-auto-revoke.integration.ts +7 -6
- package/src/sessions/__tests__/sessions.integration.ts +23 -38
- package/src/sessions/__tests__/test-helpers.ts +1 -1
- package/src/sessions/db/queries/cleanup.ts +21 -0
- package/src/sessions/handlers/cleanup.job.ts +6 -29
- package/src/sessions/handlers/list.query.ts +24 -24
- package/src/sessions/handlers/mine.query.ts +24 -23
- package/src/sessions/handlers/revoke-all-for-user.write.ts +7 -11
- package/src/sessions/handlers/revoke-all-others.write.ts +7 -12
- package/src/sessions/handlers/revoke.write.ts +11 -18
- package/src/sessions/schema/user-session.ts +2 -2
- package/src/sessions/session-callbacks.ts +19 -21
- package/src/subscription-mollie/__tests__/feature.test.ts +1 -1
- package/src/subscription-mollie/__tests__/mollie-foundation.integration.ts +1 -1
- package/src/subscription-mollie/__tests__/verify-webhook.test.ts +8 -7
- package/src/subscription-stripe/__tests__/feature.test.ts +1 -1
- package/src/subscription-stripe/__tests__/plugin-methods.test.ts +14 -15
- package/src/subscription-stripe/__tests__/stripe-foundation.integration.ts +1 -1
- package/src/subscription-stripe/__tests__/verify-webhook.test.ts +14 -14
- package/src/subscription-stripe/verify-webhook.ts +1 -1
- package/src/template-resolver/__tests__/handlers.integration.ts +1 -1
- package/src/template-resolver/__tests__/template-resolver.integration.ts +3 -2
- package/src/template-resolver/api.ts +7 -13
- package/src/template-resolver/handlers/archive.write.ts +4 -7
- package/src/template-resolver/handlers/find-by-id.query.ts +4 -7
- package/src/template-resolver/handlers/list.query.ts +13 -21
- package/src/template-resolver/handlers/publish.write.ts +4 -7
- package/src/template-resolver/handlers/upsert-system.write.ts +7 -10
- package/src/template-resolver/handlers/upsert-tenant.write.ts +7 -10
- package/src/template-resolver/table.ts +2 -5
- package/src/tenant/__tests__/multi-tenant.integration.ts +1 -1
- package/src/tenant/__tests__/seed-testing.integration.ts +19 -45
- package/src/tenant/__tests__/tenant.integration.ts +1 -1
- package/src/tenant/handlers/active-tenant-ids.query.ts +3 -8
- package/src/tenant/handlers/add-member.write.ts +6 -8
- package/src/tenant/handlers/cancel-invitation.write.ts +5 -7
- package/src/tenant/handlers/invitations.query.ts +5 -10
- package/src/tenant/handlers/me.query.ts +2 -3
- package/src/tenant/handlers/members.query.ts +4 -5
- package/src/tenant/handlers/memberships.query.ts +2 -5
- package/src/tenant/handlers/remove-member.write.ts +6 -8
- package/src/tenant/handlers/resolve-user-ids.query.ts +6 -16
- package/src/tenant/handlers/update-member-roles.write.ts +6 -8
- package/src/tenant/invitation-table.ts +2 -5
- package/src/tenant/membership-table.ts +3 -6
- package/src/tenant/schema/tenant.ts +2 -2
- package/src/tenant/seeding.ts +12 -18
- package/src/text-content/README.md +1 -1
- package/src/text-content/__tests__/text-content.integration.ts +2 -2
- package/src/text-content/api.ts +2 -9
- package/src/text-content/handlers/by-slug.query.ts +6 -9
- package/src/text-content/handlers/by-tenant.query.ts +2 -2
- package/src/text-content/handlers/set.write.ts +7 -9
- package/src/text-content/seeding.ts +6 -9
- package/src/text-content/table.ts +2 -2
- package/src/text-content/web/__tests__/editor-read-only.test.tsx +31 -45
- package/src/text-content/web/__tests__/group-blocks.test.ts +1 -18
- package/src/text-content/web/client-plugin.tsx +11 -23
- package/src/tier-engine/__tests__/auto-default-tier.integration.ts +10 -16
- package/src/tier-engine/__tests__/compose-app.test.ts +1 -1
- package/src/tier-engine/__tests__/drift.test.ts +1 -1
- package/src/tier-engine/__tests__/resolver.integration.ts +6 -6
- package/src/tier-engine/__tests__/tier-engine.integration.ts +1 -1
- package/src/tier-engine/feature.ts +9 -16
- package/src/user/__tests__/seed-testing.integration.ts +10 -22
- package/src/user/__tests__/user-status.test.ts +1 -1
- package/src/user/__tests__/user.integration.ts +6 -5
- package/src/user/handlers/create.write.ts +5 -7
- package/src/user/handlers/find-for-auth.query.ts +5 -7
- package/src/user/schema/user.ts +2 -2
- package/src/user/seeding.ts +2 -3
- package/src/user-data-rights/__tests__/audit-log.integration.ts +24 -12
- package/src/user-data-rights/__tests__/cross-data-matrix.integration.ts +64 -37
- package/src/user-data-rights/__tests__/download.integration.ts +29 -46
- package/src/user-data-rights/__tests__/export-job-idempotency.integration.ts +35 -28
- package/src/user-data-rights/__tests__/export-job-schema.test.ts +2 -2
- package/src/user-data-rights/__tests__/policy-to-strategy.test.ts +1 -1
- package/src/user-data-rights/__tests__/request-cancel-deletion.integration.ts +11 -15
- package/src/user-data-rights/__tests__/request-deletion-callback.integration.ts +10 -12
- package/src/user-data-rights/__tests__/request-export.integration.ts +23 -16
- package/src/user-data-rights/__tests__/restriction-flow.integration.ts +24 -32
- package/src/user-data-rights/__tests__/run-export-jobs.integration.ts +142 -137
- package/src/user-data-rights/__tests__/run-forget-cleanup.integration.ts +46 -28
- package/src/user-data-rights/__tests__/run-user-export.integration.ts +20 -14
- package/src/user-data-rights/__tests__/token-helpers.test.ts +1 -1
- package/src/user-data-rights/__tests__/user-data-rights.integration.ts +1 -1
- package/src/user-data-rights/__tests__/zip-path.test.ts +1 -1
- package/src/user-data-rights/audit-download.ts +3 -3
- package/src/user-data-rights/db/queries/export-jobs.ts +23 -0
- package/src/user-data-rights/db/queries/forget-cleanup.ts +13 -0
- package/src/user-data-rights/handlers/cancel-deletion.write.ts +28 -22
- package/src/user-data-rights/handlers/download-by-job.query.ts +11 -21
- package/src/user-data-rights/handlers/download-by-token.query.ts +20 -35
- package/src/user-data-rights/handlers/export-status.query.ts +19 -33
- package/src/user-data-rights/handlers/lift-restriction.write.ts +7 -12
- package/src/user-data-rights/handlers/list-download-attempts.query.ts +14 -23
- package/src/user-data-rights/handlers/my-audit-log.query.ts +33 -23
- package/src/user-data-rights/handlers/request-deletion.write.ts +15 -15
- package/src/user-data-rights/handlers/request-export.write.ts +7 -11
- package/src/user-data-rights/handlers/restrict-account.write.ts +12 -12
- package/src/user-data-rights/run-export-jobs.ts +20 -60
- package/src/user-data-rights/run-forget-cleanup.ts +19 -33
- package/src/user-data-rights/run-user-export.ts +4 -6
- package/src/user-data-rights/schema/download-attempt.ts +2 -2
- package/src/user-data-rights/schema/download-token.ts +2 -2
- package/src/user-data-rights/schema/export-job.ts +2 -3
- package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.ts +37 -30
- package/src/user-data-rights-defaults/db/queries/user-hook.ts +17 -0
- package/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +12 -27
- package/src/user-data-rights-defaults/hooks/user.userdata-hook.ts +16 -18
- package/CHANGELOG.md +0 -689
|
@@ -14,15 +14,16 @@
|
|
|
14
14
|
// `drizzle/generate.ts` ergänzen (= via subscriptionsProjectionTable-
|
|
15
15
|
// import). setupTestStack pusht sie automatisch via r.projection.table.
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
|
|
18
18
|
import { defineApply } from "@cosmicdrift/kumiko-framework/engine";
|
|
19
|
+
import { upsertSubscriptionProjectionRow } from "./db/queries/subscription-projection";
|
|
19
20
|
import { subscriptionEntity } from "./entities";
|
|
20
21
|
import type { SubscriptionEventPayload } from "./events";
|
|
21
22
|
|
|
22
23
|
// Drizzle-table-instance aus dem entity-shape. Wird sowohl von der
|
|
23
24
|
// projection-apply als auch von list-query / get-helper genutzt damit
|
|
24
25
|
// alle drei Stellen denselben column-namespace teilen.
|
|
25
|
-
export const subscriptionsProjectionTable =
|
|
26
|
+
export const subscriptionsProjectionTable = buildEntityTable("subscription", subscriptionEntity);
|
|
26
27
|
|
|
27
28
|
// =============================================================================
|
|
28
29
|
// Shared helpers
|
|
@@ -62,17 +63,35 @@ async function upsert(
|
|
|
62
63
|
// jemand nur teil-felder updated (z.B. invoice-payment-failed nur
|
|
63
64
|
// status+tier), nutzen wir trotzdem den vollen payload für den
|
|
64
65
|
// INSERT-Pfad und nur den teil-`set` für ON CONFLICT.
|
|
65
|
-
|
|
66
|
-
.
|
|
67
|
-
.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
66
|
+
const insertCols = {
|
|
67
|
+
id: event.aggregateId,
|
|
68
|
+
tenant_id: event.tenantId,
|
|
69
|
+
provider_name: fullPayload.providerName,
|
|
70
|
+
provider_customer_id: fullPayload.providerCustomerId,
|
|
71
|
+
provider_subscription_id: fullPayload.providerSubscriptionId,
|
|
72
|
+
status: fullPayload.status,
|
|
73
|
+
tier: fullPayload.tier,
|
|
74
|
+
current_period_end: fullPayload.currentPeriodEndIso,
|
|
75
|
+
};
|
|
76
|
+
// Map camelCase set-keys to snake_case DB columns.
|
|
77
|
+
const setMap: Record<keyof typeof set, string> = {
|
|
78
|
+
providerName: "provider_name",
|
|
79
|
+
providerCustomerId: "provider_customer_id",
|
|
80
|
+
providerSubscriptionId: "provider_subscription_id",
|
|
81
|
+
status: "status",
|
|
82
|
+
tier: "tier",
|
|
83
|
+
currentPeriodEnd: "current_period_end",
|
|
84
|
+
};
|
|
85
|
+
const insertParams = Object.values(insertCols);
|
|
86
|
+
const setEntries = Object.entries(set).filter(([, v]) => v !== undefined);
|
|
87
|
+
const setClauses: string[] = [];
|
|
88
|
+
const allParams: unknown[] = [...insertParams];
|
|
89
|
+
for (const [k, v] of setEntries) {
|
|
90
|
+
allParams.push(v);
|
|
91
|
+
setClauses.push(`"${setMap[k as keyof typeof set]}" = $${allParams.length}`);
|
|
92
|
+
}
|
|
93
|
+
const tableName = (subscriptionsProjectionTable as { tableName: string }).tableName;
|
|
94
|
+
await upsertSubscriptionProjectionRow(tx, tableName, insertCols, setClauses, allParams);
|
|
76
95
|
}
|
|
77
96
|
|
|
78
97
|
// =============================================================================
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// assert. Mirrors the mail-foundation / file-foundation integration
|
|
7
7
|
// test pattern.
|
|
8
8
|
|
|
9
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
9
10
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
10
11
|
import { defineFeature, type WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
11
12
|
import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
@@ -17,7 +18,6 @@ import {
|
|
|
17
18
|
testTenantId,
|
|
18
19
|
unsafeCreateEntityTable,
|
|
19
20
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
20
|
-
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
21
21
|
import { z } from "zod";
|
|
22
22
|
import { CapCounterHandlers, CapCounterQueries } from "../constants";
|
|
23
23
|
import {
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
// without spinning up the test-stack — the real event-store +
|
|
4
4
|
// dispatcher integration is exercised in cap-counter.integration.ts.
|
|
5
5
|
|
|
6
|
-
import { describe, expect,
|
|
7
|
-
|
|
6
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
8
7
|
// Temporal: rely on the global ambient declaration from temporal-spec.
|
|
9
8
|
// The framework polyfill is loaded by setupTestStack, but pure unit
|
|
10
9
|
// tests (no stack) need a manual polyfill — vitest.setup.ts does that.
|
|
@@ -18,35 +17,38 @@ import {
|
|
|
18
17
|
enforceRollingCapAndMaybeNotify,
|
|
19
18
|
} from "../enforce-cap";
|
|
20
19
|
|
|
21
|
-
//
|
|
20
|
+
// Test-mock: ctx.db unterstützt sowohl bun-db's .unsafe() (selectMany ruft das)
|
|
21
|
+
// als auch drizzle's .select().from().where() chain (rolling-path nutzt das
|
|
22
|
+
// direkt). Beide pfade returnen denselben rows-set unabhängig von filtern.
|
|
23
|
+
|
|
24
|
+
function makeMockDb(rows: unknown[]) {
|
|
25
|
+
return {
|
|
26
|
+
unsafe: async () => rows,
|
|
27
|
+
begin: async <T>(fn: (tx: unknown) => Promise<T>) =>
|
|
28
|
+
fn({ unsafe: async () => rows, begin: async () => undefined }),
|
|
29
|
+
select: () => ({
|
|
30
|
+
from: () => ({
|
|
31
|
+
where: Object.assign(async () => rows, {
|
|
32
|
+
// calendar path also chains .limit(1) after .where()
|
|
33
|
+
limit: async () => rows,
|
|
34
|
+
}),
|
|
35
|
+
}),
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
22
39
|
|
|
23
40
|
function stubCalendarCtx(rows: { value: number; lastSoftWarnedAt: unknown }[]) {
|
|
24
41
|
const ctx = {
|
|
25
|
-
db:
|
|
26
|
-
select: () => ({
|
|
27
|
-
from: () => ({
|
|
28
|
-
where: () => ({
|
|
29
|
-
limit: async () => rows,
|
|
30
|
-
}),
|
|
31
|
-
}),
|
|
32
|
-
}),
|
|
33
|
-
},
|
|
42
|
+
db: makeMockDb(rows),
|
|
34
43
|
user: { tenantId: "tenant-test" },
|
|
35
44
|
};
|
|
36
45
|
return ctx as unknown as Parameters<typeof enforceCap>[0];
|
|
37
46
|
}
|
|
38
47
|
|
|
39
|
-
// --- Rolling-Window stub: db.select(...).from(...).where(...) returns rows ---
|
|
40
|
-
|
|
41
48
|
function stubRollingCtx(eventPayloads: { amount: number }[]) {
|
|
49
|
+
const rows = eventPayloads.map((p) => ({ payload: p }));
|
|
42
50
|
const ctx = {
|
|
43
|
-
db:
|
|
44
|
-
select: () => ({
|
|
45
|
-
from: () => ({
|
|
46
|
-
where: async () => eventPayloads.map((p) => ({ payload: p })),
|
|
47
|
-
}),
|
|
48
|
-
}),
|
|
49
|
-
},
|
|
51
|
+
db: makeMockDb(rows),
|
|
50
52
|
user: { tenantId: "tenant-test" },
|
|
51
53
|
};
|
|
52
54
|
return ctx as unknown as Parameters<typeof enforceRollingCap>[0];
|
|
@@ -284,7 +286,7 @@ describe("enforceCapAndMaybeNotify — calendar", () => {
|
|
|
284
286
|
|
|
285
287
|
test("ok → notifier NICHT aufgerufen", async () => {
|
|
286
288
|
const ctx = stubCalendarCtx([{ value: 100, lastSoftWarnedAt: null }]);
|
|
287
|
-
const notify =
|
|
289
|
+
const notify = mock();
|
|
288
290
|
const result = await enforceCapAndMaybeNotify(ctx, { ...baseOpts, notify });
|
|
289
291
|
expect(result.state).toBe("ok");
|
|
290
292
|
expect(notify).not.toHaveBeenCalled();
|
|
@@ -292,19 +294,21 @@ describe("enforceCapAndMaybeNotify — calendar", () => {
|
|
|
292
294
|
|
|
293
295
|
test("soft-hit, crossed=true → notifier mit info-payload + ctx.write markSoftWarned", async () => {
|
|
294
296
|
const ctx = stubCalendarCtx([{ value: 1100, lastSoftWarnedAt: null }]);
|
|
295
|
-
const write =
|
|
297
|
+
const write = mock(async () => ({ isSuccess: true, data: {} }));
|
|
296
298
|
(ctx as unknown as { write: typeof write }).write = write;
|
|
297
|
-
const notify =
|
|
299
|
+
const notify = mock();
|
|
298
300
|
|
|
299
301
|
const result = await enforceCapAndMaybeNotify(ctx, { ...baseOpts, notify });
|
|
300
302
|
expect(result.state).toBe("soft-hit");
|
|
301
|
-
expect(notify).
|
|
303
|
+
expect(notify).toHaveBeenCalledTimes(1);
|
|
304
|
+
expect(notify).toHaveBeenCalledWith({
|
|
302
305
|
capName: "mails-per-month",
|
|
303
306
|
value: 1100,
|
|
304
307
|
limit: 1000,
|
|
305
308
|
tenantId: "tenant-test",
|
|
306
309
|
});
|
|
307
|
-
expect(write).
|
|
310
|
+
expect(write).toHaveBeenCalledTimes(1);
|
|
311
|
+
expect(write).toHaveBeenCalledWith("cap-counter:write:mark-soft-warned", {
|
|
308
312
|
capName: "mails-per-month",
|
|
309
313
|
periodStartIso: PERIOD,
|
|
310
314
|
});
|
|
@@ -312,7 +316,7 @@ describe("enforceCapAndMaybeNotify — calendar", () => {
|
|
|
312
316
|
|
|
313
317
|
test("soft-hit, crossed=false (already warned) → notifier NICHT erneut aufgerufen", async () => {
|
|
314
318
|
const ctx = stubCalendarCtx([{ value: 1150, lastSoftWarnedAt: "2026-05-15T12:00:00Z" }]);
|
|
315
|
-
const notify =
|
|
319
|
+
const notify = mock();
|
|
316
320
|
const result = await enforceCapAndMaybeNotify(ctx, { ...baseOpts, notify });
|
|
317
321
|
expect(result.state).toBe("soft-hit");
|
|
318
322
|
expect(notify).not.toHaveBeenCalled();
|
|
@@ -320,7 +324,7 @@ describe("enforceCapAndMaybeNotify — calendar", () => {
|
|
|
320
324
|
|
|
321
325
|
test("hard-hit → throws CapExceededError BEVOR notifier feuert", async () => {
|
|
322
326
|
const ctx = stubCalendarCtx([{ value: 1500, lastSoftWarnedAt: null }]);
|
|
323
|
-
const notify =
|
|
327
|
+
const notify = mock();
|
|
324
328
|
await expect(enforceCapAndMaybeNotify(ctx, { ...baseOpts, notify })).rejects.toThrow(
|
|
325
329
|
CapExceededError,
|
|
326
330
|
);
|
|
@@ -342,7 +346,7 @@ describe("enforceRollingCapAndMaybeNotify — rolling", () => {
|
|
|
342
346
|
|
|
343
347
|
test("ok → notifier NICHT aufgerufen", async () => {
|
|
344
348
|
const ctx = stubRollingCtx([{ amount: 100 }]);
|
|
345
|
-
const notify =
|
|
349
|
+
const notify = mock();
|
|
346
350
|
const result = await enforceRollingCapAndMaybeNotify(ctx, { ...baseOpts, notify });
|
|
347
351
|
expect(result.state).toBe("ok");
|
|
348
352
|
expect(notify).not.toHaveBeenCalled();
|
|
@@ -350,10 +354,11 @@ describe("enforceRollingCapAndMaybeNotify — rolling", () => {
|
|
|
350
354
|
|
|
351
355
|
test("soft-hit → notifier feuert (ohne dedup, Caller-Verantwortung)", async () => {
|
|
352
356
|
const ctx = stubRollingCtx([{ amount: 6000 }, { amount: 5000 }]);
|
|
353
|
-
const notify =
|
|
357
|
+
const notify = mock();
|
|
354
358
|
const result = await enforceRollingCapAndMaybeNotify(ctx, { ...baseOpts, notify });
|
|
355
359
|
expect(result.state).toBe("soft-hit");
|
|
356
|
-
expect(notify).
|
|
360
|
+
expect(notify).toHaveBeenCalledTimes(1);
|
|
361
|
+
expect(notify).toHaveBeenCalledWith({
|
|
357
362
|
capName: "ai-tokens-7d",
|
|
358
363
|
value: 11000,
|
|
359
364
|
limit: 10000,
|
|
@@ -367,7 +372,7 @@ describe("enforceRollingCapAndMaybeNotify — rolling", () => {
|
|
|
367
372
|
// ein Refactor heimlich Dedup einbaut ohne projection-row, fällt
|
|
368
373
|
// das hier auf.
|
|
369
374
|
const ctx = stubRollingCtx([{ amount: 11000 }]);
|
|
370
|
-
const notify =
|
|
375
|
+
const notify = mock();
|
|
371
376
|
await enforceRollingCapAndMaybeNotify(ctx, { ...baseOpts, notify });
|
|
372
377
|
await enforceRollingCapAndMaybeNotify(ctx, { ...baseOpts, notify });
|
|
373
378
|
expect(notify).toHaveBeenCalledTimes(2);
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// 5. Failed handler: counter NICHT inkrementiert (cap-quota nicht
|
|
9
9
|
// verbrannt für gescheiterte writes)
|
|
10
10
|
|
|
11
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
11
12
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
12
13
|
import { defineFeature, type WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
13
14
|
import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
@@ -18,7 +19,6 @@ import {
|
|
|
18
19
|
testTenantId,
|
|
19
20
|
unsafeCreateEntityTable,
|
|
20
21
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
21
|
-
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
22
22
|
import { z } from "zod";
|
|
23
23
|
import { CapCounterQueries } from "../constants";
|
|
24
24
|
import type { SoftHitNotifier } from "../enforce-cap";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { createEntityExecutor, type HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
3
|
import { eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
3
|
-
import { and, eq, gte } from "drizzle-orm";
|
|
4
4
|
import { rollingCapAggregateId } from "./aggregate-id";
|
|
5
5
|
import {
|
|
6
6
|
CAP_COUNTER_ROLLING_AGGREGATE_TYPE,
|
|
@@ -109,13 +109,12 @@ export async function enforceCap(
|
|
|
109
109
|
const softThreshold = options.limit * tolerance.soft;
|
|
110
110
|
const hardThreshold = options.limit * tolerance.hard;
|
|
111
111
|
|
|
112
|
-
const rows = await
|
|
113
|
-
.
|
|
114
|
-
|
|
115
|
-
.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
.limit(1);
|
|
112
|
+
const rows = await selectMany(
|
|
113
|
+
ctx.db,
|
|
114
|
+
table,
|
|
115
|
+
{ capName: options.capName, periodStart: options.periodStartIso },
|
|
116
|
+
{ limit: 1 },
|
|
117
|
+
);
|
|
119
118
|
|
|
120
119
|
const row = rows[0];
|
|
121
120
|
const value = row ? (row["value"] as number) : 0; // @cast-boundary db-row
|
|
@@ -189,18 +188,13 @@ export async function enforceRollingCap(
|
|
|
189
188
|
// covers the prefix; the additional aggregate_id eq narrows to the
|
|
190
189
|
// single rolling-stream. Postgres can use the index even with the
|
|
191
190
|
// aggregate_id filter applied as a residual.
|
|
192
|
-
const rows = await ctx.db
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
eq(eventsTable.aggregateId, aggregateId),
|
|
200
|
-
eq(eventsTable.type, ROLLING_INCREMENTED_EVENT_QN),
|
|
201
|
-
gte(eventsTable.createdAt, cutoff),
|
|
202
|
-
),
|
|
203
|
-
);
|
|
191
|
+
const rows = await selectMany<{ payload: { amount?: number } }>(ctx.db, eventsTable, {
|
|
192
|
+
tenantId: ctx.user.tenantId,
|
|
193
|
+
aggregateType: CAP_COUNTER_ROLLING_AGGREGATE_TYPE,
|
|
194
|
+
aggregateId,
|
|
195
|
+
type: ROLLING_INCREMENTED_EVENT_QN,
|
|
196
|
+
createdAt: { gte: cutoff },
|
|
197
|
+
});
|
|
204
198
|
|
|
205
199
|
let value = 0;
|
|
206
200
|
for (const row of rows) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { createEntityExecutor, type QueryHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import { and, eq } from "drizzle-orm";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { capCounterEntity } from "../entity";
|
|
5
5
|
|
|
@@ -25,18 +25,12 @@ export const getCounterQuery: QueryHandlerDef = {
|
|
|
25
25
|
const { capName, periodStartIso } = query.payload as z.infer<typeof getCounterSchema>; // @cast-boundary engine-payload
|
|
26
26
|
|
|
27
27
|
// ctx.db is tenant-scoped; filter by capName + periodStart explicitly.
|
|
28
|
-
const rows = await
|
|
29
|
-
.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// periodStart is stored as Temporal.Instant; compare against
|
|
35
|
-
// the iso string directly (timestamptz-column round-trips).
|
|
36
|
-
eq(table["periodStart"], periodStartIso),
|
|
37
|
-
),
|
|
38
|
-
)
|
|
39
|
-
.limit(1);
|
|
28
|
+
const rows = await selectMany(
|
|
29
|
+
ctx.db,
|
|
30
|
+
table,
|
|
31
|
+
{ capName, periodStart: periodStartIso },
|
|
32
|
+
{ limit: 1 },
|
|
33
|
+
);
|
|
40
34
|
|
|
41
35
|
return rows[0] ?? null;
|
|
42
36
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { createEntityExecutor, type WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import { eq } from "drizzle-orm";
|
|
3
3
|
import { Temporal } from "temporal-polyfill";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { capCounterAggregateId } from "../aggregate-id";
|
|
@@ -55,7 +55,7 @@ export const incrementCapHandler: WriteHandlerDef = {
|
|
|
55
55
|
|
|
56
56
|
// Read existing aggregate's projection-row to decide create vs update.
|
|
57
57
|
// ctx.db is auto-tenant-scoped — id-lookup is unique per tenant.
|
|
58
|
-
const existing = await ctx.db
|
|
58
|
+
const existing = await selectMany(ctx.db, table, { id: aggregateId }, { limit: 1 });
|
|
59
59
|
|
|
60
60
|
if (existing.length === 0) {
|
|
61
61
|
return executor.create(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { createEntityExecutor, type WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import { eq } from "drizzle-orm";
|
|
3
3
|
import { Temporal } from "temporal-polyfill";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { capCounterAggregateId } from "../aggregate-id";
|
|
@@ -32,7 +32,7 @@ export const markSoftWarnedHandler: WriteHandlerDef = {
|
|
|
32
32
|
payload.periodStartIso,
|
|
33
33
|
);
|
|
34
34
|
|
|
35
|
-
const existing = await ctx.db
|
|
35
|
+
const existing = await selectMany(ctx.db, table, { id: aggregateId }, { limit: 1 });
|
|
36
36
|
if (existing.length === 0) {
|
|
37
37
|
throw new Error(
|
|
38
38
|
`cap-counter: cannot mark-soft-warned, no counter found for tenant=${event.user.tenantId} cap=${payload.capName} period=${payload.periodStartIso}`,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import { and, desc, eq } from "drizzle-orm";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { inAppMessagesTable } from "../tables";
|
|
5
5
|
|
|
@@ -11,18 +11,12 @@ export const inboxQuery = defineQueryHandler({
|
|
|
11
11
|
}),
|
|
12
12
|
access: { openToAll: true },
|
|
13
13
|
handler: async (query, ctx) => {
|
|
14
|
-
const
|
|
15
|
-
if (query.payload.unreadOnly)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
.select()
|
|
21
|
-
.from(inAppMessagesTable)
|
|
22
|
-
.where(and(...conditions))
|
|
23
|
-
.orderBy(desc(inAppMessagesTable.createdAt))
|
|
24
|
-
.limit(query.payload.limit);
|
|
25
|
-
|
|
14
|
+
const where: Record<string, unknown> = { userId: query.user.id };
|
|
15
|
+
if (query.payload.unreadOnly) where["isRead"] = false;
|
|
16
|
+
const rows = await selectMany(ctx.db, inAppMessagesTable, where, {
|
|
17
|
+
limit: query.payload.limit,
|
|
18
|
+
orderBy: { col: "createdAt", direction: "desc" },
|
|
19
|
+
});
|
|
26
20
|
return { rows };
|
|
27
21
|
},
|
|
28
22
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import { and, eq } from "drizzle-orm";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { inAppMessagesTable } from "../tables";
|
|
5
5
|
|
|
@@ -8,14 +8,12 @@ export const markAllReadWrite = defineWriteHandler({
|
|
|
8
8
|
schema: z.object({}),
|
|
9
9
|
access: { openToAll: true },
|
|
10
10
|
handler: async (event, ctx) => {
|
|
11
|
-
const rows = await
|
|
12
|
-
.
|
|
13
|
-
|
|
14
|
-
.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
.returning();
|
|
18
|
-
|
|
11
|
+
const rows = await updateMany(
|
|
12
|
+
ctx.db,
|
|
13
|
+
inAppMessagesTable,
|
|
14
|
+
{ isRead: true, readAt: Temporal.Now.instant() },
|
|
15
|
+
{ userId: event.user.id, isRead: false },
|
|
16
|
+
);
|
|
19
17
|
return { isSuccess: true, data: { marked: rows.length } };
|
|
20
18
|
},
|
|
21
19
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
3
|
import { NotFoundError, writeFailure } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
|
-
import { and, eq } from "drizzle-orm";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { inAppMessagesTable } from "../tables";
|
|
6
6
|
|
|
@@ -12,21 +12,15 @@ export const markReadWrite = defineWriteHandler({
|
|
|
12
12
|
}),
|
|
13
13
|
access: { openToAll: true },
|
|
14
14
|
handler: async (event, ctx) => {
|
|
15
|
-
const rows = await
|
|
16
|
-
.
|
|
17
|
-
|
|
18
|
-
.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
eq(inAppMessagesTable.userId, event.user.id),
|
|
22
|
-
),
|
|
23
|
-
)
|
|
24
|
-
.returning();
|
|
25
|
-
|
|
15
|
+
const rows = await updateMany(
|
|
16
|
+
ctx.db,
|
|
17
|
+
inAppMessagesTable,
|
|
18
|
+
{ isRead: true, readAt: Temporal.Now.instant() },
|
|
19
|
+
{ id: event.payload.id, userId: event.user.id },
|
|
20
|
+
);
|
|
26
21
|
if (rows.length === 0) {
|
|
27
22
|
return writeFailure(new NotFoundError("inAppMessage", event.payload.id));
|
|
28
23
|
}
|
|
29
|
-
|
|
30
|
-
return { isSuccess: true, data: { id: rows[0]?.["id"] } };
|
|
24
|
+
return { isSuccess: true, data: { id: (rows[0] as { id: number } | undefined)?.id } };
|
|
31
25
|
},
|
|
32
26
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import { and, count, eq } from "drizzle-orm";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { inAppMessagesTable } from "../tables";
|
|
5
5
|
|
|
@@ -8,13 +8,14 @@ export const unreadCountQuery = defineQueryHandler({
|
|
|
8
8
|
schema: z.object({}),
|
|
9
9
|
access: { openToAll: true },
|
|
10
10
|
handler: async (query, ctx) => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
// bun-db hat keinen aggregate-helper — selectMany alle matching rows,
|
|
12
|
+
// count() in JS. Pragma: unread-counts sind low-cardinality (user-
|
|
13
|
+
// scoped, max ~hunderte rows). Wenn das wachsen sollte: raw .unsafe()
|
|
14
|
+
// mit COUNT(*).
|
|
15
|
+
const rows = await selectMany(ctx.db, inAppMessagesTable, {
|
|
16
|
+
userId: query.user.id,
|
|
17
|
+
isRead: false,
|
|
18
|
+
});
|
|
19
|
+
return { count: rows.length };
|
|
19
20
|
},
|
|
20
21
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { insertOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { tenantChannel } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
3
|
import type { DeliveryChannel } from "../delivery";
|
|
3
4
|
import { inAppMessagesTable } from "./tables";
|
|
@@ -14,23 +15,20 @@ export const inAppChannel: DeliveryChannel = {
|
|
|
14
15
|
// address is the user-id string after the ES migration — keep it as-is.
|
|
15
16
|
const userId = address;
|
|
16
17
|
|
|
17
|
-
const
|
|
18
|
-
.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
data: message.data ? JSON.stringify(message.data) : null,
|
|
26
|
-
})
|
|
27
|
-
.returning();
|
|
18
|
+
const row = await insertOne<{ id: string }>(ctx.db, inAppMessagesTable, {
|
|
19
|
+
tenantId: ctx.tenantId,
|
|
20
|
+
userId,
|
|
21
|
+
notificationType: message.notificationType,
|
|
22
|
+
title: message.title,
|
|
23
|
+
body: message.body ?? null,
|
|
24
|
+
data: message.data ? JSON.stringify(message.data) : null,
|
|
25
|
+
});
|
|
28
26
|
|
|
29
27
|
if (ctx.sseBroker) {
|
|
30
28
|
ctx.sseBroker.pushToChannel(tenantChannel(ctx.tenantId), {
|
|
31
29
|
type: "channel-in-app:event:delivered",
|
|
32
30
|
data: {
|
|
33
|
-
id:
|
|
31
|
+
id: row?.id,
|
|
34
32
|
userId,
|
|
35
33
|
notificationType: message.notificationType,
|
|
36
34
|
title: message.title,
|
|
@@ -3,10 +3,10 @@ import {
|
|
|
3
3
|
instant,
|
|
4
4
|
table as pgTable,
|
|
5
5
|
serial,
|
|
6
|
+
sql,
|
|
6
7
|
text,
|
|
7
8
|
uuid,
|
|
8
9
|
} from "@cosmicdrift/kumiko-framework/db";
|
|
9
|
-
import { sql } from "drizzle-orm";
|
|
10
10
|
|
|
11
11
|
export const inAppMessagesTable = pgTable("in_app_messages", {
|
|
12
12
|
id: serial("id").primaryKey(),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
1
2
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
2
3
|
import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
3
4
|
import {
|
|
@@ -7,7 +8,6 @@ import {
|
|
|
7
8
|
testTenantId,
|
|
8
9
|
unsafeCreateEntityTable,
|
|
9
10
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
10
|
-
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
11
11
|
import { createComplianceProfilesFeature, tenantComplianceProfileEntity } from "../feature";
|
|
12
12
|
|
|
13
13
|
const SET_PROFILE = "compliance-profiles:write:set-profile";
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// 3. Override wird als JSON-String persistiert + via for-tenant
|
|
9
9
|
// korrekt zurueckgelesen
|
|
10
10
|
|
|
11
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
11
12
|
import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
12
13
|
import {
|
|
13
14
|
createTestUser,
|
|
@@ -16,7 +17,6 @@ import {
|
|
|
16
17
|
testTenantId,
|
|
17
18
|
unsafeCreateEntityTable,
|
|
18
19
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
19
|
-
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
20
20
|
import { createComplianceProfilesFeature, tenantComplianceProfileEntity } from "../feature";
|
|
21
21
|
import { seedComplianceProfile } from "../seeding";
|
|
22
22
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ComplianceProfileOverride } from "@cosmicdrift/kumiko-framework/compliance";
|
|
2
|
+
import { parseJsonSafe } from "@cosmicdrift/kumiko-framework/utils";
|
|
3
|
+
|
|
4
|
+
export function parseComplianceProfileOverride(
|
|
5
|
+
raw: string | null,
|
|
6
|
+
tenantId: string,
|
|
7
|
+
callerLabel: string,
|
|
8
|
+
): ComplianceProfileOverride | undefined {
|
|
9
|
+
if (!raw || raw.trim() === "") return undefined;
|
|
10
|
+
const parsed = parseJsonSafe<ComplianceProfileOverride | null>(raw, null);
|
|
11
|
+
if (parsed === null) {
|
|
12
|
+
// biome-ignore lint/suspicious/noConsole: operator visibility for DB-corruption edge-case
|
|
13
|
+
console.warn(
|
|
14
|
+
`[${callerLabel}] tenant ${tenantId}: stored override is not valid JSON, ignoring.`,
|
|
15
|
+
);
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
return parsed;
|
|
19
|
+
}
|