import { and, desc, eq, isNotNull, isNull, or, sql } from "drizzle-orm"; import type { Db } from "@paperclip/db"; import { activityLog, heartbeatRuns, issues } from "@paperclip/db"; export interface ActivityFilters { companyId: string; agentId?: string; entityType?: string; entityId?: string; } export function activityService(db: Db) { const issueIdAsText = sql`${issues.id}::text`; return { list: (filters: ActivityFilters) => { const conditions = [eq(activityLog.companyId, filters.companyId)]; if (filters.agentId) { conditions.push(eq(activityLog.agentId, filters.agentId)); } if (filters.entityType) { conditions.push(eq(activityLog.entityType, filters.entityType)); } if (filters.entityId) { conditions.push(eq(activityLog.entityId, filters.entityId)); } return db .select({ activityLog }) .from(activityLog) .leftJoin( issues, and( eq(activityLog.entityType, sql`'issue'`), eq(activityLog.entityId, issueIdAsText), ), ) .where( and( ...conditions, or( sql`${activityLog.entityType} != 'issue'`, isNull(issues.hiddenAt), ), ), ) .orderBy(desc(activityLog.createdAt)) .then((rows) => rows.map((r) => r.activityLog)); }, forIssue: (issueId: string) => db .select() .from(activityLog) .where( and( eq(activityLog.entityType, "issue"), eq(activityLog.entityId, issueId), ), ) .orderBy(desc(activityLog.createdAt)), runsForIssue: (issueId: string) => db .selectDistinctOn([activityLog.runId], { runId: activityLog.runId, status: heartbeatRuns.status, agentId: heartbeatRuns.agentId, startedAt: heartbeatRuns.startedAt, finishedAt: heartbeatRuns.finishedAt, createdAt: heartbeatRuns.createdAt, invocationSource: heartbeatRuns.invocationSource, usageJson: heartbeatRuns.usageJson, resultJson: heartbeatRuns.resultJson, }) .from(activityLog) .innerJoin(heartbeatRuns, eq(activityLog.runId, heartbeatRuns.id)) .where( and( eq(activityLog.entityType, "issue"), eq(activityLog.entityId, issueId), isNotNull(activityLog.runId), ), ) .orderBy(activityLog.runId, desc(heartbeatRuns.createdAt)), issuesForRun: (runId: string) => db .selectDistinctOn([issueIdAsText], { issueId: issues.id, identifier: issues.identifier, title: issues.title, status: issues.status, priority: issues.priority, }) .from(activityLog) .innerJoin(issues, eq(activityLog.entityId, issueIdAsText)) .where( and( eq(activityLog.runId, runId), eq(activityLog.entityType, "issue"), isNull(issues.hiddenAt), ), ) .orderBy(issueIdAsText), create: (data: typeof activityLog.$inferInsert) => db .insert(activityLog) .values(data) .returning() .then((rows) => rows[0]), }; }