import { ok, err, type ReadonlyDB, UnauthenticatedError, type CallerContext, type PaginationInput, buildPaginatedResult, DEFAULT_PAGE_SIZE, } from "@tailor-platform/erp-kit/core"; import type { DB } from "../generated/kysely-tailordb"; import { ForbiddenError } from "../lib/errors.generated"; type AuditEventType = | "QUEUED" | "SENT" | "DELIVERED" | "SEEN" | "READ" | "ARCHIVED" | "FAILED" | "BOUNCED" | "OPENED"; type AuditOrderByField = "occurredAt" | "createdAt"; export interface SearchNotificationDeliveryAuditInput extends PaginationInput { eventType?: AuditEventType; dateRange?: { from?: string; to?: string; }; recipientUserId?: string; sourceType?: string; sourceId?: string; from?: string; to?: string; } /** * Searches the delivery-audit trail with optional filters ordered by occurredAt desc; callers are scoped to their own rows. */ export async function run( db: ReadonlyDB, input: SearchNotificationDeliveryAuditInput, ctx: CallerContext, ) { // Every result here is scoped to the caller, so there is nothing to return when // there is no caller. Queries carry no permission gate of their own. const { actorId } = ctx; if (actorId === null) return err(new UnauthenticatedError()); const limit = input.limit ?? DEFAULT_PAGE_SIZE; const offset = input.offset ?? 0; const orderBy = input.orderBy ?? "occurredAt"; const orderDirection = input.orderDirection ?? "desc"; // Self-scope: an explicit recipientUserId filter must match the caller; // results are always restricted to the caller's own audit rows. if (input.recipientUserId !== undefined && input.recipientUserId !== actorId) { return err(new ForbiddenError(input.recipientUserId)); } // Audit rows are anchored to a parent Notification. Resolve the caller's // matching notificationIds first, applying the optional source filters. let parentQuery = db .selectFrom("Notification") .select(["id"]) .where("recipientUserId", "=", actorId); if (input.sourceType !== undefined) { parentQuery = parentQuery.where("sourceType", "=", input.sourceType); } if (input.sourceId !== undefined) { parentQuery = parentQuery.where("sourceId", "=", input.sourceId); } const parents = await parentQuery.execute(); const notificationIdFilter = parents.map((p) => p.id); if (notificationIdFilter.length === 0) { return ok(buildPaginatedResult([], limit)); } let query = db.selectFrom("NotificationDeliveryAudit").selectAll(); if (input.eventType !== undefined) { query = query.where("eventType", "=", input.eventType); } const from = input.dateRange?.from ?? input.from; const to = input.dateRange?.to ?? input.to; if (from !== undefined) { query = query.where("occurredAt", ">=", new Date(from)); } if (to !== undefined) { query = query.where("occurredAt", "<=", new Date(to)); } query = query.where("notificationId", "in", notificationIdFilter); const rows = await query .orderBy(orderBy, orderDirection) .limit(limit + 1) .offset(offset) .execute(); return ok(buildPaginatedResult(rows, limit)); }