diff --git a/src/services/api/handlers.ts b/src/services/api/handlers.ts index 8c7396c..b64f2aa 100644 --- a/src/services/api/handlers.ts +++ b/src/services/api/handlers.ts @@ -42,7 +42,7 @@ import { readJsonBody, sendJson, sendNoContent, sendValidationError } from "./ht import type { RouteContext } from "./router.js"; import { globalEmitter, getPriorityBucket, normaliseTarget } from "@/shared/index.js"; import { metrics } from "@/metrics/index.js"; -import { eq, desc, and, lt, lte, gte, sql, like, or, isNotNull } from "drizzle-orm"; +import { eq, desc, and, lt, lte, gte, sql, like, or, isNotNull, inArray } from "drizzle-orm"; import { messageLogs, workflowDefinitions, @@ -50,6 +50,7 @@ import { suppressions, users as dbUsers, userTopicPreferences, + deliveryOutbox, } from "@/db/schema.js"; import { readBaseConfig } from "@/config/index.js"; import { verifyUnsubscribeToken } from "@/unsubscribe/index.js"; @@ -1360,12 +1361,25 @@ export function createHandlers(deps: Deps) { if (!user) return sendJson(res, 404, { error: "user_not_found", id: userId }); const contacts = await deps.contactRepo.findByUserId(ctx.projectId!, userId); - const logs = await deps.db - .select() - .from(messageLogs) - .where(eq(messageLogs.projectId, ctx.projectId!)) - .orderBy(desc(messageLogs.timestamp)) - .limit(50); + const contactTargets = contacts.map((c) => c.target); + let logs: any[] = []; + if (contactTargets.length > 0) { + const outboxRows = await deps.db + .select({ taskId: deliveryOutbox.taskId }) + .from(deliveryOutbox) + .where(inArray(deliveryOutbox.destination, contactTargets)); + const taskIds = Array.from(new Set(outboxRows.map((r: any) => r.taskId))); + if (taskIds.length > 0) { + logs = await deps.db + .select() + .from(messageLogs) + .where( + and(eq(messageLogs.projectId, ctx.projectId!), inArray(messageLogs.taskId, taskIds)), + ) + .orderBy(desc(messageLogs.timestamp)) + .limit(50); + } + } sendJson(res, 200, { ...user, contacts, logs }); } diff --git a/tests/api-admin.test.ts b/tests/api-admin.test.ts index c45caab..959abcc 100644 --- a/tests/api-admin.test.ts +++ b/tests/api-admin.test.ts @@ -503,17 +503,32 @@ describe("API operational handlers", () => { describe("getUserDetails", () => { it("merges the profile with its contacts and recent logs", async () => { - selectRows = [[{ taskId: "t1" }]]; + deps.contactRepo.findByUserId.mockResolvedValue([ + { channel: "email", target: "alice@example.com" }, + ]); + selectRows = [[{ taskId: "t1" }], [{ taskId: "t1" }]]; const res = createMockRes(); await handlers.getUserDetails(createMockReq(), res, ctx({ params: { id: "usr_1" } })); expect(parse(res)).toMatchObject({ id: "usr_1", - contacts: [{ channel: "email" }], + contacts: [{ channel: "email", target: "alice@example.com" }], logs: [{ taskId: "t1" }], }); }); + it("returns empty logs when user has no contacts", async () => { + deps.contactRepo.findByUserId.mockResolvedValue([]); + const res = createMockRes(); + await handlers.getUserDetails(createMockReq(), res, ctx({ params: { id: "usr_1" } })); + + expect(parse(res)).toMatchObject({ + id: "usr_1", + contacts: [], + logs: [], + }); + }); + it("404s for an unknown user", async () => { deps.userRepo.findRecordById.mockResolvedValue(null); const res = createMockRes();