334 lines
9.7 KiB
TypeScript
334 lines
9.7 KiB
TypeScript
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<ReturnType<DeploymentListPresenter["call"]>>;
|
|
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;
|
|
}
|
|
}
|