@@ -5,7 +5,7 @@ import type {
55 SecretReference ,
66} from "@trigger.dev/database" ;
77import { ResultAsync } from "neverthrow" ;
8- import { prisma } from "~/db.server" ;
8+ import { prisma , $transaction } from "~/db.server" ;
99import { logger } from "~/services/logger.server" ;
1010import { VercelIntegrationRepository } from "~/models/vercelIntegration.server" ;
1111import { findCurrentWorkerDeployment } from "~/v3/models/workerDeployment.server" ;
@@ -178,82 +178,124 @@ export class VercelIntegrationService {
178178 ( ) => undefined
179179 ) ;
180180
181- const existing = await this . getVercelProjectIntegration ( params . projectId ) ;
182- if ( existing ) {
183- const updated = await this . #prismaClient. organizationProjectIntegration . update ( {
184- where : { id : existing . id } ,
185- data : {
186- externalEntityId : params . vercelProjectId ,
187- integrationData : {
188- ...existing . parsedIntegrationData ,
189- vercelProjectId : params . vercelProjectId ,
190- vercelProjectName : params . vercelProjectName ,
191- vercelTeamId : teamId ,
192- vercelTeamSlug,
181+ // Use a serializable transaction to prevent duplicate project integrations
182+ // from concurrent selectVercelProject calls (read-then-write race condition).
183+ const txResult = await $transaction (
184+ this . #prismaClient,
185+ "selectVercelProject" ,
186+ async ( tx ) => {
187+ const existing = await tx . organizationProjectIntegration . findFirst ( {
188+ where : {
189+ projectId : params . projectId ,
190+ deletedAt : null ,
191+ organizationIntegration : {
192+ service : "VERCEL" ,
193+ deletedAt : null ,
194+ } ,
193195 } ,
194- } ,
195- } ) ;
196+ include : {
197+ organizationIntegration : true ,
198+ } ,
199+ } ) ;
196200
197- const syncResultAsync = await VercelIntegrationRepository . syncApiKeysToVercel ( {
198- projectId : params . projectId ,
199- vercelProjectId : params . vercelProjectId ,
200- teamId,
201- vercelStagingEnvironment : existing . parsedIntegrationData . config . vercelStagingEnvironment ,
202- orgIntegration,
203- } ) ;
204- const syncResult = syncResultAsync . isOk ( )
205- ? { success : syncResultAsync . value . errors . length === 0 , errors : syncResultAsync . value . errors }
206- : { success : false , errors : [ syncResultAsync . error . message ] } ;
201+ if ( existing ) {
202+ const parsedData = VercelProjectIntegrationDataSchema . safeParse (
203+ existing . integrationData
204+ ) ;
205+
206+ const updated = await tx . organizationProjectIntegration . update ( {
207+ where : { id : existing . id } ,
208+ data : {
209+ externalEntityId : params . vercelProjectId ,
210+ integrationData : {
211+ ...( parsedData . success ? parsedData . data : { } ) ,
212+ vercelProjectId : params . vercelProjectId ,
213+ vercelProjectName : params . vercelProjectName ,
214+ vercelTeamId : teamId ,
215+ vercelTeamSlug,
216+ } ,
217+ } ,
218+ } ) ;
219+
220+ return {
221+ integration : updated ,
222+ wasCreated : false ,
223+ vercelStagingEnvironment : parsedData . success
224+ ? parsedData . data . config . vercelStagingEnvironment
225+ : null ,
226+ } ;
227+ }
228+
229+ const integrationData = createDefaultVercelIntegrationData (
230+ params . vercelProjectId ,
231+ params . vercelProjectName ,
232+ teamId ,
233+ vercelTeamSlug
234+ ) ;
235+
236+ const created = await tx . organizationProjectIntegration . create ( {
237+ data : {
238+ organizationIntegrationId : orgIntegration . id ,
239+ projectId : params . projectId ,
240+ externalEntityId : params . vercelProjectId ,
241+ integrationData : integrationData ,
242+ installedBy : params . userId ,
243+ } ,
244+ } ) ;
207245
208- return { integration : updated , syncResult } ;
246+ return {
247+ integration : created ,
248+ wasCreated : true ,
249+ vercelStagingEnvironment : null ,
250+ } ;
251+ } ,
252+ { isolationLevel : "Serializable" }
253+ ) ;
254+
255+ if ( ! txResult ) {
256+ throw new Error ( "Failed to select Vercel project: transaction returned undefined" ) ;
209257 }
210258
211- const integration = await this . createVercelProjectIntegration ( {
212- organizationIntegrationId : orgIntegration . id ,
213- projectId : params . projectId ,
214- vercelProjectId : params . vercelProjectId ,
215- vercelProjectName : params . vercelProjectName ,
216- vercelTeamId : teamId ,
217- vercelTeamSlug,
218- installedByUserId : params . userId ,
219- } ) ;
259+ const { integration, wasCreated, vercelStagingEnvironment } = txResult ;
220260
221261 const syncResultAsync = await VercelIntegrationRepository . syncApiKeysToVercel ( {
222262 projectId : params . projectId ,
223263 vercelProjectId : params . vercelProjectId ,
224264 teamId,
225- vercelStagingEnvironment : null ,
265+ vercelStagingEnvironment,
226266 orgIntegration,
227267 } ) ;
228268 const syncResult = syncResultAsync . isOk ( )
229269 ? { success : syncResultAsync . value . errors . length === 0 , errors : syncResultAsync . value . errors }
230270 : { success : false , errors : [ syncResultAsync . error . message ] } ;
231271
232- const disableResult = await VercelIntegrationRepository . getVercelClient ( orgIntegration )
233- . andThen ( ( client ) =>
234- VercelIntegrationRepository . disableAutoAssignCustomDomains (
235- client ,
236- params . vercelProjectId ,
237- teamId
238- )
239- ) ;
272+ if ( wasCreated ) {
273+ const disableResult = await VercelIntegrationRepository . getVercelClient ( orgIntegration )
274+ . andThen ( ( client ) =>
275+ VercelIntegrationRepository . disableAutoAssignCustomDomains (
276+ client ,
277+ params . vercelProjectId ,
278+ teamId
279+ )
280+ ) ;
281+
282+ if ( disableResult . isErr ( ) ) {
283+ logger . warn ( "Failed to disable autoAssignCustomDomains during project selection" , {
284+ projectId : params . projectId ,
285+ vercelProjectId : params . vercelProjectId ,
286+ error : disableResult . error . message ,
287+ } ) ;
288+ }
240289
241- if ( disableResult . isErr ( ) ) {
242- logger . warn ( "Failed to disable autoAssignCustomDomains during project selection" , {
290+ logger . info ( "Vercel project selected and API keys synced" , {
243291 projectId : params . projectId ,
244292 vercelProjectId : params . vercelProjectId ,
245- error : disableResult . error . message ,
293+ vercelProjectName : params . vercelProjectName ,
294+ syncSuccess : syncResult . success ,
295+ syncErrors : syncResult . errors ,
246296 } ) ;
247297 }
248298
249- logger . info ( "Vercel project selected and API keys synced" , {
250- projectId : params . projectId ,
251- vercelProjectId : params . vercelProjectId ,
252- vercelProjectName : params . vercelProjectName ,
253- syncSuccess : syncResult . success ,
254- syncErrors : syncResult . errors ,
255- } ) ;
256-
257299 return { integration, syncResult } ;
258300 }
259301
0 commit comments