@cosmicdrift/kumiko-bundled-features 0.14.0 → 0.15.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/handlers/for-tenant.query.ts +4 -7
- 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 +5 -7
- 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 +2 -1
- 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 +22 -14
- 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
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { deleteMany, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
3
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
2
|
-
import {
|
|
4
|
+
import { buildEntityTable, createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
|
|
3
5
|
import {
|
|
4
6
|
createEntity,
|
|
5
7
|
createTextField,
|
|
@@ -15,8 +17,6 @@ import {
|
|
|
15
17
|
TestUsers,
|
|
16
18
|
unsafePushTables,
|
|
17
19
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
18
|
-
import { and, eq } from "drizzle-orm";
|
|
19
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
20
20
|
import { z } from "zod";
|
|
21
21
|
import { createChannelEmailFeature } from "../../channel-email/feature";
|
|
22
22
|
import { createInMemoryTransport, type EmailMessage } from "../../channel-email/types";
|
|
@@ -176,7 +176,7 @@ const ticketEntity = createEntity({
|
|
|
176
176
|
status: createTextField({ required: true }),
|
|
177
177
|
},
|
|
178
178
|
});
|
|
179
|
-
const ticketTable =
|
|
179
|
+
const ticketTable = buildEntityTable("ticket", ticketEntity);
|
|
180
180
|
|
|
181
181
|
function ticketExecutor() {
|
|
182
182
|
return createEventStoreExecutor(ticketTable, ticketEntity, { entityName: "ticket" });
|
|
@@ -346,10 +346,7 @@ describe("flow 1: handler sends notification via ctx.notify()", () => {
|
|
|
346
346
|
expect(result).toEqual({ assigned: true });
|
|
347
347
|
|
|
348
348
|
// InApp message in DB
|
|
349
|
-
const messages = await db
|
|
350
|
-
.select()
|
|
351
|
-
.from(inAppMessagesTable)
|
|
352
|
-
.where(eq(inAppMessagesTable.userId, user1.id));
|
|
349
|
+
const messages = await selectMany(db, inAppMessagesTable, { userId: user1.id });
|
|
353
350
|
expect(messages).toHaveLength(1);
|
|
354
351
|
expect(messages[0]?.["title"]).toBe("Neuer Auftrag");
|
|
355
352
|
expect(messages[0]?.["body"]).toBe("Auftrag #42 wurde dir zugewiesen");
|
|
@@ -361,10 +358,9 @@ describe("flow 1: handler sends notification via ctx.notify()", () => {
|
|
|
361
358
|
expect(sseEvents[0]?.data["userId"]).toBe(user1.id);
|
|
362
359
|
|
|
363
360
|
// DeliveryLog entries for all 3 channels
|
|
364
|
-
const logs = await db
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
.where(eq(deliveryAttemptsTable.notificationType, "app:notify:order-assigned"));
|
|
361
|
+
const logs = await selectMany(db, deliveryAttemptsTable, {
|
|
362
|
+
notificationType: "app:notify:order-assigned",
|
|
363
|
+
});
|
|
368
364
|
expect(logs).toHaveLength(3);
|
|
369
365
|
const channels = logs.map((l) => l["channel"]);
|
|
370
366
|
expect(channels).toContain("inApp");
|
|
@@ -452,15 +448,10 @@ describe("flow 3: broadcast to multiple users + markAllRead", () => {
|
|
|
452
448
|
|
|
453
449
|
// Both users have messages in DB
|
|
454
450
|
for (const user of [user1, user2]) {
|
|
455
|
-
const messages = await db
|
|
456
|
-
.
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
and(
|
|
460
|
-
eq(inAppMessagesTable.userId, user.id),
|
|
461
|
-
eq(inAppMessagesTable.notificationType, "app:notify:announcement"),
|
|
462
|
-
),
|
|
463
|
-
);
|
|
451
|
+
const messages = await selectMany(db, inAppMessagesTable, {
|
|
452
|
+
userId: user.id,
|
|
453
|
+
notificationType: "app:notify:announcement",
|
|
454
|
+
});
|
|
464
455
|
expect(messages).toHaveLength(1);
|
|
465
456
|
}
|
|
466
457
|
|
|
@@ -473,10 +464,9 @@ describe("flow 3: broadcast to multiple users + markAllRead", () => {
|
|
|
473
464
|
});
|
|
474
465
|
|
|
475
466
|
test("delivery log has entries for all recipients and channels", async () => {
|
|
476
|
-
const logs = await db
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
.where(eq(deliveryAttemptsTable.notificationType, "app:notify:announcement"));
|
|
467
|
+
const logs = await selectMany(db, deliveryAttemptsTable, {
|
|
468
|
+
notificationType: "app:notify:announcement",
|
|
469
|
+
});
|
|
480
470
|
|
|
481
471
|
// 2 users × 3 channels (inApp + email + push) = 6 entries
|
|
482
472
|
expect(logs).toHaveLength(6);
|
|
@@ -530,28 +520,18 @@ describe("flow 4: declarative notification via r.notification()", () => {
|
|
|
530
520
|
);
|
|
531
521
|
|
|
532
522
|
// user1 gets ticketAssigned, admin gets ticketCreatedAdmin
|
|
533
|
-
const user1Messages = await db
|
|
534
|
-
.
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
and(
|
|
538
|
-
eq(inAppMessagesTable.userId, user1.id),
|
|
539
|
-
eq(inAppMessagesTable.notificationType, "tickets:notify:ticket-assigned"),
|
|
540
|
-
),
|
|
541
|
-
);
|
|
523
|
+
const user1Messages = await selectMany(db, inAppMessagesTable, {
|
|
524
|
+
userId: user1.id,
|
|
525
|
+
notificationType: "tickets:notify:ticket-assigned",
|
|
526
|
+
});
|
|
542
527
|
expect(user1Messages).toHaveLength(1);
|
|
543
528
|
expect(user1Messages[0]?.["title"]).toBe("Neues Ticket");
|
|
544
529
|
expect(user1Messages[0]?.["body"]).toContain("Server down");
|
|
545
530
|
|
|
546
|
-
const adminMessages = await db
|
|
547
|
-
.
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
and(
|
|
551
|
-
eq(inAppMessagesTable.userId, admin.id),
|
|
552
|
-
eq(inAppMessagesTable.notificationType, "tickets:notify:ticket-created-admin"),
|
|
553
|
-
),
|
|
554
|
-
);
|
|
531
|
+
const adminMessages = await selectMany(db, inAppMessagesTable, {
|
|
532
|
+
userId: admin.id,
|
|
533
|
+
notificationType: "tickets:notify:ticket-created-admin",
|
|
534
|
+
});
|
|
555
535
|
expect(adminMessages).toHaveLength(1);
|
|
556
536
|
expect(adminMessages[0]?.["title"]).toBe("Ticket erstellt");
|
|
557
537
|
|
|
@@ -564,16 +544,11 @@ describe("flow 4: declarative notification via r.notification()", () => {
|
|
|
564
544
|
});
|
|
565
545
|
|
|
566
546
|
test("delivery log entries for both notifications", async () => {
|
|
567
|
-
const logs = await db
|
|
568
|
-
|
|
569
|
-
.
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
eq(deliveryAttemptsTable.channel, "inApp"),
|
|
573
|
-
eq(deliveryAttemptsTable.recipientId, user1.id),
|
|
574
|
-
eq(deliveryAttemptsTable.notificationType, "tickets:notify:ticket-assigned"),
|
|
575
|
-
),
|
|
576
|
-
);
|
|
547
|
+
const logs = await selectMany(db, deliveryAttemptsTable, {
|
|
548
|
+
channel: "inApp",
|
|
549
|
+
recipientId: user1.id,
|
|
550
|
+
notificationType: "tickets:notify:ticket-assigned",
|
|
551
|
+
});
|
|
577
552
|
expect(logs).toHaveLength(1);
|
|
578
553
|
expect(logs[0]?.["status"]).toBe("sent");
|
|
579
554
|
});
|
|
@@ -592,18 +567,16 @@ describe("flow 5: notification skipped when recipient is null", () => {
|
|
|
592
567
|
);
|
|
593
568
|
|
|
594
569
|
// No ticketAssigned notification (no assignee)
|
|
595
|
-
const assigneeNotifs = await db
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
.where(eq(inAppMessagesTable.notificationType, "tickets:notify:ticket-assigned"));
|
|
570
|
+
const assigneeNotifs = await selectMany(db, inAppMessagesTable, {
|
|
571
|
+
notificationType: "tickets:notify:ticket-assigned",
|
|
572
|
+
});
|
|
599
573
|
// Only the one from flow 4 should exist
|
|
600
574
|
expect(assigneeNotifs).toHaveLength(1);
|
|
601
575
|
|
|
602
576
|
// But admin still gets ticketCreatedAdmin
|
|
603
|
-
const adminMessages = await db
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
.where(eq(inAppMessagesTable.notificationType, "tickets:notify:ticket-created-admin"));
|
|
577
|
+
const adminMessages = await selectMany(db, inAppMessagesTable, {
|
|
578
|
+
notificationType: "tickets:notify:ticket-created-admin",
|
|
579
|
+
});
|
|
607
580
|
expect(adminMessages).toHaveLength(2); // flow 4 + flow 5
|
|
608
581
|
|
|
609
582
|
// SSE: only 1 event (admin only, no assignee)
|
|
@@ -639,20 +612,14 @@ describe("flow 6: user preferences", () => {
|
|
|
639
612
|
stack.events.reset();
|
|
640
613
|
|
|
641
614
|
// Count messages before
|
|
642
|
-
const before = await db
|
|
643
|
-
.select()
|
|
644
|
-
.from(inAppMessagesTable)
|
|
645
|
-
.where(eq(inAppMessagesTable.userId, user1.id));
|
|
615
|
+
const before = await selectMany(db, inAppMessagesTable, { userId: user1.id });
|
|
646
616
|
const beforeCount = before.length;
|
|
647
617
|
|
|
648
618
|
// Send notification to user1 who has disabled inApp for orderAssigned
|
|
649
619
|
await stack.http.writeOk("app:write:assign-order", { orderId: 99, driverId: user1.id }, admin);
|
|
650
620
|
|
|
651
621
|
// No new InApp message for user1
|
|
652
|
-
const after = await db
|
|
653
|
-
.select()
|
|
654
|
-
.from(inAppMessagesTable)
|
|
655
|
-
.where(eq(inAppMessagesTable.userId, user1.id));
|
|
622
|
+
const after = await selectMany(db, inAppMessagesTable, { userId: user1.id });
|
|
656
623
|
expect(after.length).toBe(beforeCount);
|
|
657
624
|
|
|
658
625
|
// No SSE event
|
|
@@ -660,17 +627,12 @@ describe("flow 6: user preferences", () => {
|
|
|
660
627
|
expect(notifs).toHaveLength(0);
|
|
661
628
|
|
|
662
629
|
// DeliveryLog shows skipped with preference_disabled
|
|
663
|
-
const logs = await db
|
|
664
|
-
|
|
665
|
-
.
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
eq(deliveryAttemptsTable.recipientId, user1.id),
|
|
670
|
-
eq(deliveryAttemptsTable.status, "skipped"),
|
|
671
|
-
eq(deliveryAttemptsTable.error, "preference_disabled"),
|
|
672
|
-
),
|
|
673
|
-
);
|
|
630
|
+
const logs = await selectMany(db, deliveryAttemptsTable, {
|
|
631
|
+
notificationType: "app:notify:order-assigned",
|
|
632
|
+
recipientId: user1.id,
|
|
633
|
+
status: "skipped",
|
|
634
|
+
error: "preference_disabled",
|
|
635
|
+
});
|
|
674
636
|
expect(logs.length).toBeGreaterThanOrEqual(1);
|
|
675
637
|
});
|
|
676
638
|
|
|
@@ -816,10 +778,9 @@ describe("flow 9: email channel with renderer", () => {
|
|
|
816
778
|
});
|
|
817
779
|
|
|
818
780
|
test("delivery log has entries for both channels", async () => {
|
|
819
|
-
const logs = await db
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
.where(eq(deliveryAttemptsTable.notificationType, "tickets:notify:ticket-assigned"));
|
|
781
|
+
const logs = await selectMany(db, deliveryAttemptsTable, {
|
|
782
|
+
notificationType: "tickets:notify:ticket-assigned",
|
|
783
|
+
});
|
|
823
784
|
|
|
824
785
|
const channels = logs.map((l) => l["channel"]);
|
|
825
786
|
expect(channels).toContain("inApp");
|
|
@@ -862,15 +823,10 @@ describe("flow 10: complete end-to-end", () => {
|
|
|
862
823
|
// --- InApp Channel ---
|
|
863
824
|
|
|
864
825
|
// InApp message in DB with template-transformed data
|
|
865
|
-
const inAppMessages = await db
|
|
866
|
-
.
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
and(
|
|
870
|
-
eq(inAppMessagesTable.userId, user2.id),
|
|
871
|
-
eq(inAppMessagesTable.notificationType, "tickets:notify:ticket-assigned"),
|
|
872
|
-
),
|
|
873
|
-
);
|
|
826
|
+
const inAppMessages = await selectMany(db, inAppMessagesTable, {
|
|
827
|
+
userId: user2.id,
|
|
828
|
+
notificationType: "tickets:notify:ticket-assigned",
|
|
829
|
+
});
|
|
874
830
|
// Filter to this specific ticket by checking title
|
|
875
831
|
const thisMessage = inAppMessages.find((m) =>
|
|
876
832
|
(m["body"] as string)?.includes("Datenbank Backup"),
|
|
@@ -906,15 +862,10 @@ describe("flow 10: complete end-to-end", () => {
|
|
|
906
862
|
|
|
907
863
|
// --- DeliveryLog ---
|
|
908
864
|
|
|
909
|
-
const logs = await db
|
|
910
|
-
|
|
911
|
-
.
|
|
912
|
-
|
|
913
|
-
and(
|
|
914
|
-
eq(deliveryAttemptsTable.notificationType, "tickets:notify:ticket-assigned"),
|
|
915
|
-
eq(deliveryAttemptsTable.recipientId, user2.id),
|
|
916
|
-
),
|
|
917
|
-
);
|
|
865
|
+
const logs = await selectMany(db, deliveryAttemptsTable, {
|
|
866
|
+
notificationType: "tickets:notify:ticket-assigned",
|
|
867
|
+
recipientId: user2.id,
|
|
868
|
+
});
|
|
918
869
|
// Filter to logs from this test (there may be prior entries)
|
|
919
870
|
const inAppLog = logs.find((l) => l["channel"] === "inApp");
|
|
920
871
|
const emailLog = logs.find((l) => l["channel"] === "email");
|
|
@@ -937,10 +888,9 @@ describe("flow 8: tenant broadcast via to: { tenant }", () => {
|
|
|
937
888
|
);
|
|
938
889
|
|
|
939
890
|
// All 3 tenant users get a message
|
|
940
|
-
const messages = await db
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
.where(eq(inAppMessagesTable.notificationType, "app:notify:tenant-alert"));
|
|
891
|
+
const messages = await selectMany(db, inAppMessagesTable, {
|
|
892
|
+
notificationType: "app:notify:tenant-alert",
|
|
893
|
+
});
|
|
944
894
|
const recipientIds = messages.map((m) => m["userId"]);
|
|
945
895
|
expect(recipientIds).toContain(admin.id);
|
|
946
896
|
expect(recipientIds).toContain(user1.id);
|
|
@@ -960,10 +910,9 @@ describe("flow 8: tenant broadcast via to: { tenant }", () => {
|
|
|
960
910
|
});
|
|
961
911
|
|
|
962
912
|
test("delivery log has entries for all recipients and channels", async () => {
|
|
963
|
-
const logs = await db
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
.where(eq(deliveryAttemptsTable.notificationType, "app:notify:tenant-alert"));
|
|
913
|
+
const logs = await selectMany(db, deliveryAttemptsTable, {
|
|
914
|
+
notificationType: "app:notify:tenant-alert",
|
|
915
|
+
});
|
|
967
916
|
|
|
968
917
|
// 3 users × 3 channels (inApp + email + push) = 9
|
|
969
918
|
expect(logs).toHaveLength(9);
|
|
@@ -1001,17 +950,12 @@ describe("flow 12: rate limiting", () => {
|
|
|
1001
950
|
await stack.http.writeOk("app:write:assign-order", { orderId: 501, driverId: user1.id }, admin);
|
|
1002
951
|
|
|
1003
952
|
// Email should be skipped (rate limited), but inApp + push should work
|
|
1004
|
-
const emailLogs = await db
|
|
1005
|
-
|
|
1006
|
-
.
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
eq(deliveryAttemptsTable.recipientId, user1.id),
|
|
1011
|
-
eq(deliveryAttemptsTable.channel, "email"),
|
|
1012
|
-
eq(deliveryAttemptsTable.error, "rate_limited"),
|
|
1013
|
-
),
|
|
1014
|
-
);
|
|
953
|
+
const emailLogs = await selectMany(db, deliveryAttemptsTable, {
|
|
954
|
+
notificationType: "app:notify:order-assigned",
|
|
955
|
+
recipientId: user1.id,
|
|
956
|
+
channel: "email",
|
|
957
|
+
error: "rate_limited",
|
|
958
|
+
});
|
|
1015
959
|
expect(emailLogs.length).toBeGreaterThanOrEqual(1);
|
|
1016
960
|
|
|
1017
961
|
// InApp still works
|
|
@@ -1107,15 +1051,10 @@ describe("flow 12c: idempotency key dedup", () => {
|
|
|
1107
1051
|
expect(emails.length).toBe(1);
|
|
1108
1052
|
|
|
1109
1053
|
// Dup attempt is recorded in the log for audit
|
|
1110
|
-
const dupLogs = await db
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
and(
|
|
1115
|
-
eq(deliveryAttemptsTable.notificationType, "app:notify:idem-test"),
|
|
1116
|
-
eq(deliveryAttemptsTable.error, "duplicate_idempotency_key"),
|
|
1117
|
-
),
|
|
1118
|
-
);
|
|
1054
|
+
const dupLogs = await selectMany(db, deliveryAttemptsTable, {
|
|
1055
|
+
notificationType: "app:notify:idem-test",
|
|
1056
|
+
error: "duplicate_idempotency_key",
|
|
1057
|
+
});
|
|
1119
1058
|
expect(dupLogs.length).toBe(1);
|
|
1120
1059
|
});
|
|
1121
1060
|
|
|
@@ -1162,17 +1101,12 @@ describe("flow 12d: channel error paths", () => {
|
|
|
1162
1101
|
expect(emails.length).toBe(0);
|
|
1163
1102
|
|
|
1164
1103
|
// Log shows the failure with the original error string
|
|
1165
|
-
const failedLogs = await db
|
|
1166
|
-
|
|
1167
|
-
.
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
eq(deliveryAttemptsTable.recipientId, user1.id),
|
|
1172
|
-
eq(deliveryAttemptsTable.channel, "email"),
|
|
1173
|
-
eq(deliveryAttemptsTable.status, "failed"),
|
|
1174
|
-
),
|
|
1175
|
-
);
|
|
1104
|
+
const failedLogs = await selectMany(db, deliveryAttemptsTable, {
|
|
1105
|
+
notificationType: "app:notify:order-assigned",
|
|
1106
|
+
recipientId: user1.id,
|
|
1107
|
+
channel: "email",
|
|
1108
|
+
status: "failed",
|
|
1109
|
+
});
|
|
1176
1110
|
expect(failedLogs.length).toBeGreaterThanOrEqual(1);
|
|
1177
1111
|
expect(failedLogs.at(-1)?.["error"]).toContain("smtp_timeout_simulated");
|
|
1178
1112
|
|
|
@@ -1200,17 +1134,12 @@ describe("flow 12d: channel error paths", () => {
|
|
|
1200
1134
|
);
|
|
1201
1135
|
expect(broadcastEmails.length).toBe(1);
|
|
1202
1136
|
|
|
1203
|
-
const failedLogs = await db
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
eq(deliveryAttemptsTable.channel, "email"),
|
|
1210
|
-
eq(deliveryAttemptsTable.status, "failed"),
|
|
1211
|
-
eq(deliveryAttemptsTable.error, "smtp_transient"),
|
|
1212
|
-
),
|
|
1213
|
-
);
|
|
1137
|
+
const failedLogs = await selectMany(db, deliveryAttemptsTable, {
|
|
1138
|
+
notificationType: "app:notify:announcement",
|
|
1139
|
+
channel: "email",
|
|
1140
|
+
status: "failed",
|
|
1141
|
+
error: "smtp_transient",
|
|
1142
|
+
});
|
|
1214
1143
|
expect(failedLogs.length).toBe(1);
|
|
1215
1144
|
});
|
|
1216
1145
|
});
|
|
@@ -1232,17 +1161,12 @@ describe("flow 13: tenant kill switch", () => {
|
|
|
1232
1161
|
expect(pushes).toHaveLength(0);
|
|
1233
1162
|
|
|
1234
1163
|
// DeliveryLog shows channel_disabled
|
|
1235
|
-
const pushLogs = await db
|
|
1236
|
-
|
|
1237
|
-
.
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
eq(deliveryAttemptsTable.recipientId, user1.id),
|
|
1242
|
-
eq(deliveryAttemptsTable.channel, "push"),
|
|
1243
|
-
eq(deliveryAttemptsTable.error, "channel_disabled"),
|
|
1244
|
-
),
|
|
1245
|
-
);
|
|
1164
|
+
const pushLogs = await selectMany(db, deliveryAttemptsTable, {
|
|
1165
|
+
notificationType: "app:notify:order-assigned",
|
|
1166
|
+
recipientId: user1.id,
|
|
1167
|
+
channel: "push",
|
|
1168
|
+
error: "channel_disabled",
|
|
1169
|
+
});
|
|
1246
1170
|
expect(pushLogs.length).toBeGreaterThanOrEqual(1);
|
|
1247
1171
|
|
|
1248
1172
|
// InApp + Email still work
|
|
@@ -1259,9 +1183,7 @@ describe("flow 13: tenant kill switch", () => {
|
|
|
1259
1183
|
describe("flow 14: wildcard-only preference conflicts resolve deterministically", () => {
|
|
1260
1184
|
test("conflicting wildcards (type=*, false vs channel=*, true) → disabled wins", async () => {
|
|
1261
1185
|
// Clean slate for user2 on this type/channel
|
|
1262
|
-
await db
|
|
1263
|
-
.delete(notificationPreferencesTable)
|
|
1264
|
-
.where(eq(notificationPreferencesTable.userId, user2.id));
|
|
1186
|
+
await deleteMany(db, notificationPreferencesTable, { userId: user2.id });
|
|
1265
1187
|
|
|
1266
1188
|
// Wildcard A: disable inApp globally
|
|
1267
1189
|
await stack.http.writeOk(
|
|
@@ -1292,17 +1214,12 @@ describe("flow 14: wildcard-only preference conflicts resolve deterministically"
|
|
|
1292
1214
|
const inAppEvents = stack.events.sse.filter((e) => e.type === "channel-in-app:event:delivered");
|
|
1293
1215
|
expect(inAppEvents.filter((e) => e.data["userId"] === user2.id)).toHaveLength(0);
|
|
1294
1216
|
|
|
1295
|
-
const skipped = await db
|
|
1296
|
-
|
|
1297
|
-
.
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
eq(deliveryAttemptsTable.recipientId, user2.id),
|
|
1302
|
-
eq(deliveryAttemptsTable.channel, "inApp"),
|
|
1303
|
-
eq(deliveryAttemptsTable.error, "preference_disabled"),
|
|
1304
|
-
),
|
|
1305
|
-
);
|
|
1217
|
+
const skipped = await selectMany(db, deliveryAttemptsTable, {
|
|
1218
|
+
notificationType: "app:notify:wildcard-conflict",
|
|
1219
|
+
recipientId: user2.id,
|
|
1220
|
+
channel: "inApp",
|
|
1221
|
+
error: "preference_disabled",
|
|
1222
|
+
});
|
|
1306
1223
|
expect(skipped.length).toBeGreaterThanOrEqual(1);
|
|
1307
1224
|
});
|
|
1308
1225
|
|
|
@@ -1334,9 +1251,7 @@ describe("flow 14: wildcard-only preference conflicts resolve deterministically"
|
|
|
1334
1251
|
expect(inAppEvents.filter((e) => e.data["userId"] === user2.id)).toHaveLength(1);
|
|
1335
1252
|
|
|
1336
1253
|
// Clean up for later tests
|
|
1337
|
-
await db
|
|
1338
|
-
.delete(notificationPreferencesTable)
|
|
1339
|
-
.where(eq(notificationPreferencesTable.userId, user2.id));
|
|
1254
|
+
await deleteMany(db, notificationPreferencesTable, { userId: user2.id });
|
|
1340
1255
|
});
|
|
1341
1256
|
});
|
|
1342
1257
|
|
|
@@ -1395,16 +1310,11 @@ describe("flow 16: repeated unsubscribe clicks are idempotent", () => {
|
|
|
1395
1310
|
}
|
|
1396
1311
|
|
|
1397
1312
|
// Exactly one row exists, marked disabled
|
|
1398
|
-
const rows = await db
|
|
1399
|
-
.
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
eq(notificationPreferencesTable.userId, user1.id),
|
|
1404
|
-
eq(notificationPreferencesTable.notificationType, "app:notify:concurrent-unsub"),
|
|
1405
|
-
eq(notificationPreferencesTable.channel, "email"),
|
|
1406
|
-
),
|
|
1407
|
-
);
|
|
1313
|
+
const rows = await selectMany(db, notificationPreferencesTable, {
|
|
1314
|
+
userId: user1.id,
|
|
1315
|
+
notificationType: "app:notify:concurrent-unsub",
|
|
1316
|
+
channel: "email",
|
|
1317
|
+
});
|
|
1408
1318
|
expect(rows).toHaveLength(1);
|
|
1409
1319
|
expect(rows[0]?.["enabled"]).toBe(false);
|
|
1410
1320
|
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
4
|
+
|
|
5
|
+
export type NotificationPreferenceRow = {
|
|
6
|
+
readonly notificationType: string;
|
|
7
|
+
readonly channel: string;
|
|
8
|
+
readonly enabled: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export async function selectNotificationPreferences(
|
|
12
|
+
db: DbConnection,
|
|
13
|
+
tenantId: TenantId,
|
|
14
|
+
userId: string,
|
|
15
|
+
notificationType: string,
|
|
16
|
+
channelName: string,
|
|
17
|
+
): Promise<readonly NotificationPreferenceRow[]> {
|
|
18
|
+
return asRawClient(db).unsafe<NotificationPreferenceRow>(
|
|
19
|
+
`SELECT notification_type AS "notificationType", channel, enabled
|
|
20
|
+
FROM read_notification_preferences
|
|
21
|
+
WHERE tenant_id = $1
|
|
22
|
+
AND user_id = $2
|
|
23
|
+
AND (
|
|
24
|
+
(notification_type = $3 AND channel = $4)
|
|
25
|
+
OR (notification_type = '*' AND channel = $4)
|
|
26
|
+
OR (notification_type = $3 AND channel = '*')
|
|
27
|
+
)`,
|
|
28
|
+
[tenantId, userId, notificationType, channelName],
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -7,11 +7,10 @@ import { append } from "@cosmicdrift/kumiko-framework/event-store";
|
|
|
7
7
|
import { runProjectionsForEvent } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
8
8
|
import { bridgeStub } from "@cosmicdrift/kumiko-framework/testing/handler-context";
|
|
9
9
|
import { generateId } from "@cosmicdrift/kumiko-framework/utils";
|
|
10
|
-
import { and, eq, or } from "drizzle-orm";
|
|
11
10
|
import type { Redis } from "ioredis";
|
|
12
11
|
import { DELIVERY_ATTEMPT_EVENT } from "./constants";
|
|
12
|
+
import { selectNotificationPreferences } from "./db/queries/preferences";
|
|
13
13
|
import { deliveryAttemptSchema } from "./events";
|
|
14
|
-
import { notificationPreferencesTable } from "./tables";
|
|
15
14
|
import type {
|
|
16
15
|
ChannelContext,
|
|
17
16
|
ChannelMessage,
|
|
@@ -227,40 +226,13 @@ export function createDeliveryService(options: DeliveryServiceOptions): Delivery
|
|
|
227
226
|
notificationType: string,
|
|
228
227
|
channelName: string,
|
|
229
228
|
): Promise<boolean> {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const prefs = (await db
|
|
238
|
-
.select({
|
|
239
|
-
notificationType: notificationPreferencesTable.notificationType,
|
|
240
|
-
channel: notificationPreferencesTable.channel,
|
|
241
|
-
enabled: notificationPreferencesTable.enabled,
|
|
242
|
-
})
|
|
243
|
-
.from(notificationPreferencesTable)
|
|
244
|
-
.where(
|
|
245
|
-
and(
|
|
246
|
-
eq(notificationPreferencesTable.tenantId, tenantId),
|
|
247
|
-
eq(notificationPreferencesTable.userId, userId),
|
|
248
|
-
or(
|
|
249
|
-
and(
|
|
250
|
-
eq(notificationPreferencesTable.notificationType, notificationType),
|
|
251
|
-
eq(notificationPreferencesTable.channel, channelName),
|
|
252
|
-
),
|
|
253
|
-
and(
|
|
254
|
-
eq(notificationPreferencesTable.notificationType, "*"),
|
|
255
|
-
eq(notificationPreferencesTable.channel, channelName),
|
|
256
|
-
),
|
|
257
|
-
and(
|
|
258
|
-
eq(notificationPreferencesTable.notificationType, notificationType),
|
|
259
|
-
eq(notificationPreferencesTable.channel, "*"),
|
|
260
|
-
),
|
|
261
|
-
),
|
|
262
|
-
),
|
|
263
|
-
)) as readonly PrefRow[]; // @cast-boundary db-row
|
|
229
|
+
const prefs = await selectNotificationPreferences(
|
|
230
|
+
db,
|
|
231
|
+
tenantId,
|
|
232
|
+
userId,
|
|
233
|
+
notificationType,
|
|
234
|
+
channelName,
|
|
235
|
+
);
|
|
264
236
|
|
|
265
237
|
if (prefs.length === 0) return true;
|
|
266
238
|
|
package/src/delivery/feature.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { insertOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { defineFeature, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
3
|
import type { z } from "zod";
|
|
3
4
|
import { DELIVERY_ATTEMPT_EVENT } from "./constants";
|
|
@@ -34,7 +35,7 @@ export function createDeliveryFeature(): FeatureDefinition {
|
|
|
34
35
|
const p = event.payload as z.infer<typeof deliveryAttemptSchema>; // @cast-boundary engine-payload
|
|
35
36
|
// PK = aggregateId — replaying the same event twice conflicts on
|
|
36
37
|
// the PK rather than silently duplicating the log row.
|
|
37
|
-
await tx
|
|
38
|
+
await insertOne(tx, deliveryAttemptsTable, {
|
|
38
39
|
id: event.aggregateId,
|
|
39
40
|
tenantId: event.tenantId,
|
|
40
41
|
notificationType: p.notificationType,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import { desc } from "drizzle-orm";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { deliveryAttemptsTable } from "../tables";
|
|
5
5
|
|
|
@@ -10,12 +10,10 @@ export const logQuery = defineQueryHandler({
|
|
|
10
10
|
}),
|
|
11
11
|
access: { roles: ["Admin", "SystemAdmin"] },
|
|
12
12
|
handler: async (query, ctx) => {
|
|
13
|
-
const rows = await ctx.db
|
|
14
|
-
|
|
15
|
-
.
|
|
16
|
-
|
|
17
|
-
.limit(query.payload.limit);
|
|
18
|
-
|
|
13
|
+
const rows = await selectMany(ctx.db, deliveryAttemptsTable, undefined, {
|
|
14
|
+
orderBy: { col: "createdAt", direction: "desc" },
|
|
15
|
+
limit: query.payload.limit,
|
|
16
|
+
});
|
|
19
17
|
return { rows };
|
|
20
18
|
},
|
|
21
19
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
1
2
|
import { defineQueryHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
-
import { eq } from "drizzle-orm";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { notificationPreferencesTable } from "../tables";
|
|
5
5
|
|
|
@@ -8,10 +8,7 @@ export const preferencesQuery = defineQueryHandler({
|
|
|
8
8
|
schema: z.object({}),
|
|
9
9
|
access: { openToAll: true },
|
|
10
10
|
handler: async (query, ctx) => {
|
|
11
|
-
const rows = await ctx.db
|
|
12
|
-
.select()
|
|
13
|
-
.from(notificationPreferencesTable)
|
|
14
|
-
.where(eq(notificationPreferencesTable.userId, query.user.id));
|
|
11
|
+
const rows = await selectMany(ctx.db, notificationPreferencesTable, { userId: query.user.id });
|
|
15
12
|
|
|
16
13
|
return { rows };
|
|
17
14
|
},
|