import { Prisma, type WorkerDeploymentStatus, type WorkerInstanceGroupType, } from "@trigger.dev/database"; import { sqlDatabaseSchema, type PrismaClient, prisma } from "~/db.server"; import { type Organization } from "~/models/organization.server"; import { type Project } from "~/models/project.server"; import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; import { type User } from "~/models/user.server"; import { processGitMetadata } from "./BranchesPresenter.server"; import { BranchTrackingConfigSchema, getTrackedBranchForEnvironment } from "~/v3/github"; import { VercelProjectIntegrationDataSchema, buildVercelDeploymentUrl, } from "~/v3/vercel/vercelProjectIntegrationSchema"; const pageSize = 20; export type DeploymentList = Awaited>; export type DeploymentListItem = DeploymentList["deployments"][0]; export class DeploymentListPresenter { #prismaClient: PrismaClient; constructor(prismaClient: PrismaClient = prisma) { this.#prismaClient = prismaClient; } public async call({ userId, projectSlug, organizationSlug, environmentSlug, page = 1, }: { userId: User["id"]; projectSlug: Project["slug"]; organizationSlug: Organization["slug"]; environmentSlug: string; page?: number; }) { const project = await this.#prismaClient.project.findFirstOrThrow({ select: { id: true, environments: { select: { id: true, type: true, slug: true, orgMember: { select: { user: { select: { id: true, name: true, displayName: true, }, }, }, }, }, }, connectedGithubRepository: { select: { branchTracking: true, previewDeploymentsEnabled: true, repository: { select: { htmlUrl: true, fullName: true, }, }, }, }, }, where: { slug: projectSlug, organization: { slug: organizationSlug, members: { some: { userId, }, }, }, }, }); const environment = await findEnvironmentBySlug(project.id, environmentSlug, userId); if (!environment) { throw new Error(`Environment not found`); } const totalCount = await this.#prismaClient.workerDeployment.count({ where: { projectId: project.id, environmentId: environment.id, }, }); const labeledDeployments = await this.#prismaClient.workerDeploymentPromotion.findMany({ where: { environmentId: environment.id, }, select: { deploymentId: true, label: true, }, }); // Check for Vercel integration before the main query so we can conditionally LEFT JOIN let hasVercelIntegration = false; let vercelTeamSlug: string | undefined; let vercelProjectName: string | undefined; const vercelProjectIntegration = await this.#prismaClient.organizationProjectIntegration.findFirst({ where: { projectId: project.id, deletedAt: null, organizationIntegration: { service: "VERCEL", deletedAt: null, }, }, select: { integrationData: true, }, }); if (vercelProjectIntegration) { const parsed = VercelProjectIntegrationDataSchema.safeParse( vercelProjectIntegration.integrationData ); if (parsed.success && parsed.data.vercelTeamSlug) { hasVercelIntegration = true; vercelTeamSlug = parsed.data.vercelTeamSlug; vercelProjectName = parsed.data.vercelProjectName; } } const vercelSelect = hasVercelIntegration ? Prisma.sql`, id_dep."integrationDeploymentId"` : Prisma.sql``; const vercelJoin = hasVercelIntegration ? Prisma.sql`LEFT JOIN LATERAL ( SELECT id_inner."integrationDeploymentId" FROM ${sqlDatabaseSchema}."IntegrationDeployment" as id_inner WHERE id_inner."deploymentId" = wd."id" AND id_inner."integrationName" = 'vercel' ORDER BY id_inner."createdAt" DESC LIMIT 1 ) id_dep ON true` : Prisma.sql``; const deployments = await this.#prismaClient.$queryRaw< { id: string; shortCode: string; version: string; runtime: string | null; runtimeVersion: string | null; status: WorkerDeploymentStatus; environmentId: string; builtAt: Date | null; deployedAt: Date | null; tasksCount: bigint | null; userId: string | null; userName: string | null; userDisplayName: string | null; userAvatarUrl: string | null; type: WorkerInstanceGroupType; git: Prisma.JsonValue | null; integrationDeploymentId: string | null; }[] >` SELECT wd."id", wd."shortCode", wd."version", wd."runtime", wd."runtimeVersion", (SELECT COUNT(*) FROM ${sqlDatabaseSchema}."BackgroundWorkerTask" WHERE "BackgroundWorkerTask"."workerId" = wd."workerId") AS "tasksCount", wd."environmentId", wd."status", u."id" AS "userId", u."name" AS "userName", u."displayName" AS "userDisplayName", u."avatarUrl" AS "userAvatarUrl", wd."builtAt", wd."deployedAt", wd."type", wd."git" ${vercelSelect} FROM ${sqlDatabaseSchema}."WorkerDeployment" as wd LEFT JOIN ${sqlDatabaseSchema}."User" as u ON wd."triggeredById" = u."id" ${vercelJoin} WHERE wd."projectId" = ${project.id} AND wd."environmentId" = ${environment.id} ORDER BY string_to_array(wd."version", '.')::int[] DESC LIMIT ${pageSize} OFFSET ${pageSize * (page - 1)};`; const { connectedGithubRepository } = project; const branchTrackingOrError = connectedGithubRepository && BranchTrackingConfigSchema.safeParse(connectedGithubRepository.branchTracking); const environmentGitHubBranch = branchTrackingOrError && branchTrackingOrError.success ? getTrackedBranchForEnvironment( branchTrackingOrError.data, connectedGithubRepository.previewDeploymentsEnabled, { type: environment.type, branchName: environment.branchName ?? undefined, } ) : undefined; return { currentPage: page, totalPages: Math.ceil(totalCount / pageSize), hasVercelIntegration, connectedGithubRepository: project.connectedGithubRepository ?? undefined, environmentGitHubBranch, deployments: deployments.map((deployment, index) => { const label = labeledDeployments.find( (labeledDeployment) => labeledDeployment.deploymentId === deployment.id ); let vercelDeploymentUrl: string | null = null; if ( hasVercelIntegration && deployment.integrationDeploymentId && vercelTeamSlug && vercelProjectName ) { vercelDeploymentUrl = buildVercelDeploymentUrl( vercelTeamSlug, vercelProjectName, deployment.integrationDeploymentId ); } return { id: deployment.id, shortCode: deployment.shortCode, version: deployment.version, runtime: deployment.runtime, runtimeVersion: deployment.runtimeVersion, status: deployment.status, builtAt: deployment.builtAt, deployedAt: deployment.deployedAt, tasksCount: deployment.tasksCount ? Number(deployment.tasksCount) : null, label: label?.label, isBuilt: !!deployment.builtAt, isCurrent: label?.label === "current", isDeployed: deployment.status === "DEPLOYED", isLatest: page === 1 && index === 0, type: deployment.type, environment: { id: environment.id, type: environment.type, slug: environment.slug, }, deployedBy: deployment.userId ? { id: deployment.userId, name: deployment.userName, displayName: deployment.userDisplayName, avatarUrl: deployment.userAvatarUrl, } : undefined, git: processGitMetadata(deployment.git), vercelDeploymentUrl, }; }), }; } public async findPageForVersion({ userId, projectSlug, organizationSlug, environmentSlug, version, }: { userId: User["id"]; projectSlug: Project["slug"]; organizationSlug: Organization["slug"]; environmentSlug: string; version: string; }) { const project = await this.#prismaClient.project.findFirstOrThrow({ select: { id: true, }, where: { slug: projectSlug, organization: { slug: organizationSlug, members: { some: { userId, }, }, }, }, }); const environment = await findEnvironmentBySlug(project.id, environmentSlug, userId); if (!environment) { throw new Error(`Environment not found`); } // Find how many deployments have been made since this version const deploymentsSinceVersion = await this.#prismaClient.$queryRaw<{ count: bigint }[]>` SELECT COUNT(*) as count FROM ${sqlDatabaseSchema}."WorkerDeployment" WHERE "projectId" = ${project.id} AND "environmentId" = ${environment.id} AND string_to_array(version, '.')::int[] > string_to_array(${version}, '.')::int[] `; const count = Number(deploymentsSinceVersion[0].count); return Math.floor(count / pageSize) + 1; } }