Spaces:
Paused
Paused
| import { and, desc, eq, inArray } from "drizzle-orm"; | |
| import type { Db } from "@paperclipai/db"; | |
| import { approvals, issueApprovals, issues } from "@paperclipai/db"; | |
| import { notFound, unprocessable } from "../errors.js"; | |
| import { redactEventPayload } from "../redaction.js"; | |
| interface LinkActor { | |
| agentId?: string | null; | |
| userId?: string | null; | |
| } | |
| export function issueApprovalService(db: Db) { | |
| async function getIssue(issueId: string) { | |
| return db | |
| .select() | |
| .from(issues) | |
| .where(eq(issues.id, issueId)) | |
| .then((rows) => rows[0] ?? null); | |
| } | |
| async function getApproval(approvalId: string) { | |
| return db | |
| .select() | |
| .from(approvals) | |
| .where(eq(approvals.id, approvalId)) | |
| .then((rows) => rows[0] ?? null); | |
| } | |
| async function assertIssueAndApprovalSameCompany(issueId: string, approvalId: string) { | |
| const issue = await getIssue(issueId); | |
| if (!issue) throw notFound("Issue not found"); | |
| const approval = await getApproval(approvalId); | |
| if (!approval) throw notFound("Approval not found"); | |
| if (issue.companyId !== approval.companyId) { | |
| throw unprocessable("Issue and approval must belong to the same company"); | |
| } | |
| return { issue, approval }; | |
| } | |
| return { | |
| listApprovalsForIssue: async (issueId: string) => { | |
| const issue = await getIssue(issueId); | |
| if (!issue) throw notFound("Issue not found"); | |
| const result = await db | |
| .select({ | |
| id: approvals.id, | |
| companyId: approvals.companyId, | |
| type: approvals.type, | |
| requestedByAgentId: approvals.requestedByAgentId, | |
| requestedByUserId: approvals.requestedByUserId, | |
| status: approvals.status, | |
| payload: approvals.payload, | |
| decisionNote: approvals.decisionNote, | |
| decidedByUserId: approvals.decidedByUserId, | |
| decidedAt: approvals.decidedAt, | |
| createdAt: approvals.createdAt, | |
| updatedAt: approvals.updatedAt, | |
| }) | |
| .from(issueApprovals) | |
| .innerJoin(approvals, eq(issueApprovals.approvalId, approvals.id)) | |
| .where(eq(issueApprovals.issueId, issueId)) | |
| .orderBy(desc(issueApprovals.createdAt)); | |
| return result.map((approval) => ({ | |
| ...approval, | |
| payload: redactEventPayload(approval.payload) ?? {}, | |
| })); | |
| }, | |
| listIssuesForApproval: async (approvalId: string) => { | |
| const approval = await getApproval(approvalId); | |
| if (!approval) throw notFound("Approval not found"); | |
| return db | |
| .select({ | |
| id: issues.id, | |
| companyId: issues.companyId, | |
| projectId: issues.projectId, | |
| goalId: issues.goalId, | |
| parentId: issues.parentId, | |
| title: issues.title, | |
| description: issues.description, | |
| status: issues.status, | |
| priority: issues.priority, | |
| assigneeAgentId: issues.assigneeAgentId, | |
| createdByAgentId: issues.createdByAgentId, | |
| createdByUserId: issues.createdByUserId, | |
| issueNumber: issues.issueNumber, | |
| identifier: issues.identifier, | |
| requestDepth: issues.requestDepth, | |
| billingCode: issues.billingCode, | |
| startedAt: issues.startedAt, | |
| completedAt: issues.completedAt, | |
| cancelledAt: issues.cancelledAt, | |
| createdAt: issues.createdAt, | |
| updatedAt: issues.updatedAt, | |
| }) | |
| .from(issueApprovals) | |
| .innerJoin(issues, eq(issueApprovals.issueId, issues.id)) | |
| .where(eq(issueApprovals.approvalId, approvalId)) | |
| .orderBy(desc(issueApprovals.createdAt)); | |
| }, | |
| link: async (issueId: string, approvalId: string, actor?: LinkActor) => { | |
| const { issue } = await assertIssueAndApprovalSameCompany(issueId, approvalId); | |
| await db | |
| .insert(issueApprovals) | |
| .values({ | |
| companyId: issue.companyId, | |
| issueId, | |
| approvalId, | |
| linkedByAgentId: actor?.agentId ?? null, | |
| linkedByUserId: actor?.userId ?? null, | |
| }) | |
| .onConflictDoNothing(); | |
| return db | |
| .select() | |
| .from(issueApprovals) | |
| .where(and(eq(issueApprovals.issueId, issueId), eq(issueApprovals.approvalId, approvalId))) | |
| .then((rows) => rows[0] ?? null); | |
| }, | |
| unlink: async (issueId: string, approvalId: string) => { | |
| await assertIssueAndApprovalSameCompany(issueId, approvalId); | |
| await db | |
| .delete(issueApprovals) | |
| .where(and(eq(issueApprovals.issueId, issueId), eq(issueApprovals.approvalId, approvalId))); | |
| }, | |
| linkManyForApproval: async (approvalId: string, issueIds: string[], actor?: LinkActor) => { | |
| if (issueIds.length === 0) return; | |
| const approval = await getApproval(approvalId); | |
| if (!approval) throw notFound("Approval not found"); | |
| const uniqueIssueIds = Array.from(new Set(issueIds)); | |
| const rows = await db | |
| .select({ | |
| id: issues.id, | |
| companyId: issues.companyId, | |
| }) | |
| .from(issues) | |
| .where(inArray(issues.id, uniqueIssueIds)); | |
| if (rows.length !== uniqueIssueIds.length) { | |
| throw notFound("One or more issues not found"); | |
| } | |
| for (const row of rows) { | |
| if (row.companyId !== approval.companyId) { | |
| throw unprocessable("Issue and approval must belong to the same company"); | |
| } | |
| } | |
| await db | |
| .insert(issueApprovals) | |
| .values( | |
| uniqueIssueIds.map((issueId) => ({ | |
| companyId: approval.companyId, | |
| issueId, | |
| approvalId, | |
| linkedByAgentId: actor?.agentId ?? null, | |
| linkedByUserId: actor?.userId ?? null, | |
| })), | |
| ) | |
| .onConflictDoNothing(); | |
| }, | |
| }; | |
| } | |