import { eq, count } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; import { companies, agents, agentApiKeys, agentRuntimeState, agentTaskSessions, agentWakeupRequests, issues, issueComments, projects, goals, heartbeatRuns, heartbeatRunEvents, costEvents, approvalComments, approvals, activityLog, companySecrets, joinRequests, invites, principalPermissionGrants, companyMemberships, } from "@paperclipai/db"; export function companyService(db: Db) { const ISSUE_PREFIX_FALLBACK = "CMP"; function deriveIssuePrefixBase(name: string) { const normalized = name.toUpperCase().replace(/[^A-Z]/g, ""); return normalized.slice(0, 3) || ISSUE_PREFIX_FALLBACK; } function suffixForAttempt(attempt: number) { if (attempt <= 1) return ""; return "A".repeat(attempt - 1); } function isIssuePrefixConflict(error: unknown) { const constraint = typeof error === "object" && error !== null && "constraint" in error ? (error as { constraint?: string }).constraint : typeof error === "object" && error !== null && "constraint_name" in error ? (error as { constraint_name?: string }).constraint_name : undefined; return typeof error === "object" && error !== null && "code" in error && (error as { code?: string }).code === "23505" && constraint === "companies_issue_prefix_idx"; } async function createCompanyWithUniquePrefix(data: typeof companies.$inferInsert) { const base = deriveIssuePrefixBase(data.name); let suffix = 1; while (suffix < 10000) { const candidate = `${base}${suffixForAttempt(suffix)}`; try { const rows = await db .insert(companies) .values({ ...data, issuePrefix: candidate }) .returning(); return rows[0]; } catch (error) { if (!isIssuePrefixConflict(error)) throw error; } suffix += 1; } throw new Error("Unable to allocate unique issue prefix"); } return { list: () => db.select().from(companies), getById: (id: string) => db .select() .from(companies) .where(eq(companies.id, id)) .then((rows) => rows[0] ?? null), create: async (data: typeof companies.$inferInsert) => createCompanyWithUniquePrefix(data), update: (id: string, data: Partial) => db .update(companies) .set({ ...data, updatedAt: new Date() }) .where(eq(companies.id, id)) .returning() .then((rows) => rows[0] ?? null), archive: (id: string) => db .update(companies) .set({ status: "archived", updatedAt: new Date() }) .where(eq(companies.id, id)) .returning() .then((rows) => rows[0] ?? null), remove: (id: string) => db.transaction(async (tx) => { // Delete from child tables in dependency order await tx.delete(heartbeatRunEvents).where(eq(heartbeatRunEvents.companyId, id)); await tx.delete(agentTaskSessions).where(eq(agentTaskSessions.companyId, id)); await tx.delete(heartbeatRuns).where(eq(heartbeatRuns.companyId, id)); await tx.delete(agentWakeupRequests).where(eq(agentWakeupRequests.companyId, id)); await tx.delete(agentApiKeys).where(eq(agentApiKeys.companyId, id)); await tx.delete(agentRuntimeState).where(eq(agentRuntimeState.companyId, id)); await tx.delete(issueComments).where(eq(issueComments.companyId, id)); await tx.delete(costEvents).where(eq(costEvents.companyId, id)); await tx.delete(approvalComments).where(eq(approvalComments.companyId, id)); await tx.delete(approvals).where(eq(approvals.companyId, id)); await tx.delete(companySecrets).where(eq(companySecrets.companyId, id)); await tx.delete(joinRequests).where(eq(joinRequests.companyId, id)); await tx.delete(invites).where(eq(invites.companyId, id)); await tx.delete(principalPermissionGrants).where(eq(principalPermissionGrants.companyId, id)); await tx.delete(companyMemberships).where(eq(companyMemberships.companyId, id)); await tx.delete(issues).where(eq(issues.companyId, id)); await tx.delete(goals).where(eq(goals.companyId, id)); await tx.delete(projects).where(eq(projects.companyId, id)); await tx.delete(agents).where(eq(agents.companyId, id)); await tx.delete(activityLog).where(eq(activityLog.companyId, id)); const rows = await tx .delete(companies) .where(eq(companies.id, id)) .returning(); return rows[0] ?? null; }), stats: () => Promise.all([ db .select({ companyId: agents.companyId, count: count() }) .from(agents) .groupBy(agents.companyId), db .select({ companyId: issues.companyId, count: count() }) .from(issues) .groupBy(issues.companyId), ]).then(([agentRows, issueRows]) => { const result: Record = {}; for (const row of agentRows) { result[row.companyId] = { agentCount: row.count, issueCount: 0 }; } for (const row of issueRows) { if (result[row.companyId]) { result[row.companyId].issueCount = row.count; } else { result[row.companyId] = { agentCount: 0, issueCount: row.count }; } } return result; }), }; }