import { type PrismaClient } from "@trigger.dev/database"; import { prisma } from "~/db.server"; import { DeleteProjectService } from "~/services/deleteProject.server"; import { BranchTrackingConfigSchema, type BranchTrackingConfig } from "~/v3/github"; import { checkGitHubBranchExists } from "~/services/gitHub.server"; import { errAsync, fromPromise, okAsync, ResultAsync } from "neverthrow"; import { type BuildSettings } from "~/v3/buildSettings"; export class ProjectSettingsService { #prismaClient: PrismaClient; constructor(prismaClient: PrismaClient = prisma) { this.#prismaClient = prismaClient; } renameProject(projectId: string, newName: string) { return fromPromise( this.#prismaClient.project.update({ where: { id: projectId, }, data: { name: newName, }, }), (error) => ({ type: "other" as const, cause: error, }) ); } deleteProject(projectId: string, userId: string) { const deleteProjectService = new DeleteProjectService(this.#prismaClient); return fromPromise(deleteProjectService.call({ projectId, userId }), (error) => ({ type: "other" as const, cause: error, })); } connectGitHubRepo( projectId: string, organizationId: string, repositoryId: string, installationId: string ) { const getRepository = () => fromPromise( this.#prismaClient.githubRepository.findFirst({ where: { id: repositoryId, installationId, installation: { organizationId: organizationId, }, }, select: { id: true, name: true, defaultBranch: true, }, }), (error) => ({ type: "other" as const, cause: error, }) ).andThen((repository) => { if (!repository) { return errAsync({ type: "gh_repository_not_found" as const }); } return okAsync(repository); }); const findExistingConnection = () => fromPromise( this.#prismaClient.connectedGithubRepository.findFirst({ where: { projectId: projectId, }, }), (error) => ({ type: "other" as const, cause: error }) ); const createConnectedRepo = (defaultBranch: string, previewDeploymentsEnabled: boolean) => fromPromise( this.#prismaClient.connectedGithubRepository.create({ data: { projectId: projectId, repositoryId: repositoryId, branchTracking: { prod: { branch: defaultBranch }, staging: {}, } satisfies BranchTrackingConfig, previewDeploymentsEnabled, }, }), (error) => ({ type: "other" as const, cause: error }) ); return ResultAsync.combine([ getRepository(), findExistingConnection(), this.isPreviewEnvironmentEnabled(projectId), ]).andThen(([repository, existingConnection, previewEnvironmentEnabled]) => { if (existingConnection) { return errAsync({ type: "project_already_has_connected_repository" as const }); } return createConnectedRepo(repository.defaultBranch, previewEnvironmentEnabled); }); } disconnectGitHubRepo(projectId: string) { return fromPromise( this.#prismaClient.connectedGithubRepository.delete({ where: { projectId: projectId, }, }), (error) => ({ type: "other" as const, cause: error }) ); } updateGitSettings( projectId: string, productionBranch?: string, stagingBranch?: string, previewDeploymentsEnabled?: boolean ) { const getExistingConnectedRepo = () => fromPromise( this.#prismaClient.connectedGithubRepository.findFirst({ where: { projectId: projectId, }, include: { repository: { include: { installation: true, }, }, }, }), (error) => ({ type: "other" as const, cause: error }) ) .andThen((connectedRepo) => { if (!connectedRepo) { return errAsync({ type: "connected_gh_repository_not_found" as const }); } return okAsync(connectedRepo); }) .map((connectedRepo) => { const branchTrackingOrFailure = BranchTrackingConfigSchema.safeParse( connectedRepo.branchTracking ); const branchTracking = branchTrackingOrFailure.success ? branchTrackingOrFailure.data : undefined; return { ...connectedRepo, branchTracking, }; }); const validateProductionBranch = ({ installationId, fullRepoName, oldProductionBranch, }: { installationId: number; fullRepoName: string; oldProductionBranch?: string; }) => { if (productionBranch && oldProductionBranch !== productionBranch) { return checkGitHubBranchExists(installationId, fullRepoName, productionBranch).andThen( (exists) => { if (!exists) { return errAsync({ type: "production_tracking_branch_not_found" as const }); } return okAsync(productionBranch); } ); } return okAsync(productionBranch); }; const validateStagingBranch = ({ installationId, fullRepoName, oldStagingBranch, }: { installationId: number; fullRepoName: string; oldStagingBranch?: string; }) => { if (stagingBranch && oldStagingBranch !== stagingBranch) { return checkGitHubBranchExists(installationId, fullRepoName, stagingBranch).andThen( (exists) => { if (!exists) { return errAsync({ type: "staging_tracking_branch_not_found" as const }); } return okAsync(stagingBranch); } ); } return okAsync(stagingBranch); }; const updateConnectedRepo = (data: { productionBranch: string | undefined; stagingBranch: string | undefined; previewDeploymentsEnabled: boolean | undefined; }) => fromPromise( this.#prismaClient.connectedGithubRepository.update({ where: { projectId: projectId, }, data: { branchTracking: { prod: data.productionBranch ? { branch: data.productionBranch } : {}, staging: data.stagingBranch ? { branch: data.stagingBranch } : {}, } satisfies BranchTrackingConfig, previewDeploymentsEnabled: data.previewDeploymentsEnabled, }, }), (error) => ({ type: "other" as const, cause: error }) ); return getExistingConnectedRepo() .andThen((connectedRepo) => { const installationId = Number(connectedRepo.repository.installation.appInstallationId); return ResultAsync.combine([ validateProductionBranch({ installationId, fullRepoName: connectedRepo.repository.fullName, oldProductionBranch: connectedRepo.branchTracking?.prod?.branch, }), validateStagingBranch({ installationId, fullRepoName: connectedRepo.repository.fullName, oldStagingBranch: connectedRepo.branchTracking?.staging?.branch, }), this.isPreviewEnvironmentEnabled(projectId), ]); }) .map(([productionBranch, stagingBranch, previewEnvironmentEnabled]) => ({ productionBranch, stagingBranch, previewDeploymentsEnabled: previewDeploymentsEnabled && previewEnvironmentEnabled, })) .andThen(updateConnectedRepo); } updateBuildSettings(projectId: string, buildSettings: BuildSettings) { return fromPromise( this.#prismaClient.project.update({ where: { id: projectId, }, data: { buildSettings: buildSettings, }, }), (error) => ({ type: "other" as const, cause: error, }) ); } verifyProjectMembership(organizationSlug: string, projectSlug: string, userId: string) { const findProject = () => fromPromise( this.#prismaClient.project.findFirst({ where: { slug: projectSlug, organization: { slug: organizationSlug, members: { some: { userId, }, }, }, }, select: { id: true, organizationId: true, }, }), (error) => ({ type: "other" as const, cause: error }) ); return findProject().andThen((project) => { if (!project) { return errAsync({ type: "user_not_in_project" as const }); } return okAsync({ projectId: project.id, organizationId: project.organizationId, }); }); } private isPreviewEnvironmentEnabled(projectId: string) { return fromPromise( this.#prismaClient.runtimeEnvironment.findFirst({ select: { id: true, }, where: { projectId: projectId, slug: "preview", }, }), (error) => ({ type: "other" as const, cause: error, }) ).map((previewEnvironment) => previewEnvironment !== null); } }