| 570 | * Get usage logs for a user with optional filtering and pagination |
| 571 | */ |
| 572 | export async function getUserUsageLogs( |
| 573 | userId: string, |
| 574 | options: GetUsageLogsOptions = {} |
| 575 | ): Promise<UsageLogsResult> { |
| 576 | const { source, workspaceId, startDate, endDate, limit = 50, cursor } = options |
| 577 | |
| 578 | try { |
| 579 | const conditions = [eq(usageLog.userId, userId)] |
| 580 | |
| 581 | if (source) { |
| 582 | conditions.push(eq(usageLog.source, source)) |
| 583 | } |
| 584 | |
| 585 | if (workspaceId) { |
| 586 | conditions.push(eq(usageLog.workspaceId, workspaceId)) |
| 587 | } |
| 588 | |
| 589 | if (startDate) { |
| 590 | conditions.push(gte(usageLog.createdAt, startDate)) |
| 591 | } |
| 592 | |
| 593 | if (endDate) { |
| 594 | conditions.push(lte(usageLog.createdAt, endDate)) |
| 595 | } |
| 596 | |
| 597 | if (cursor) { |
| 598 | // Cursor resolution stays on the primary: the page itself reads a |
| 599 | // load-balanced replica, and a laggier sibling replica missing the cursor |
| 600 | // row would silently restart pagination from page 1. |
| 601 | const cursorLog = await db |
| 602 | .select({ createdAt: usageLog.createdAt }) |
| 603 | .from(usageLog) |
| 604 | .where(eq(usageLog.id, cursor)) |
| 605 | .limit(1) |
| 606 | |
| 607 | if (cursorLog.length > 0) { |
| 608 | conditions.push( |
| 609 | sql`(${usageLog.createdAt} < ${cursorLog[0].createdAt} OR (${usageLog.createdAt} = ${cursorLog[0].createdAt} AND ${usageLog.id} < ${cursor}))` |
| 610 | ) |
| 611 | } |
| 612 | } |
| 613 | |
| 614 | const logs = await dbReplica |
| 615 | .select() |
| 616 | .from(usageLog) |
| 617 | .where(and(...conditions)) |
| 618 | .orderBy(desc(usageLog.createdAt), desc(usageLog.id)) |
| 619 | .limit(limit + 1) |
| 620 | |
| 621 | const hasMore = logs.length > limit |
| 622 | const resultLogs = hasMore ? logs.slice(0, limit) : logs |
| 623 | |
| 624 | const transformedLogs: UsageLogEntry[] = resultLogs.map((log) => ({ |
| 625 | id: log.id, |
| 626 | createdAt: log.createdAt.toISOString(), |
| 627 | category: log.category as UsageLogCategory, |
| 628 | source: log.source as UsageLogSource, |
| 629 | description: log.description, |