77 boundedIn ,
88 Prisma ,
99 type PrismaClient ,
10+ type PrismaClientOrTransaction ,
1011 type RuntimeEnvironmentType ,
1112} from "@trigger.dev/database" ;
1213import { z } from "zod" ;
@@ -24,9 +25,13 @@ import {
2425 type CreateResult ,
2526 type DeleteEnvironmentVariable ,
2627 type DeleteEnvironmentVariableValue ,
28+ type DeleteEnvironmentVariableValues ,
29+ type DeleteEnvironmentVariableValuesResult ,
2730 type EditEnvironmentVariable ,
2831 type EditEnvironmentVariableValue ,
2932 type EnvironmentVariable ,
33+ type EnvironmentVariableUpdater ,
34+ EnvironmentVariableUpdaterSchema ,
3035 type EnvironmentVariableWithSecret ,
3136 type ProjectEnvironmentVariable ,
3237 type Repository ,
@@ -56,6 +61,97 @@ function parseSecretKey(key: string) {
5661
5762const SecretValue = z . object ( { secret : z . string ( ) } ) ;
5863
64+ function isSameUpdater ( stored : unknown , source : EnvironmentVariableUpdater ) : boolean {
65+ const parsed = EnvironmentVariableUpdaterSchema . safeParse ( stored ) ;
66+ if ( ! parsed . success ) {
67+ return false ;
68+ }
69+ if ( parsed . data . type === "user" && source . type === "user" ) {
70+ return parsed . data . userId === source . userId ;
71+ }
72+ if ( parsed . data . type === "integration" && source . type === "integration" ) {
73+ return parsed . data . integration === source . integration ;
74+ }
75+ return false ;
76+ }
77+
78+ export type EnvironmentVariableValueRow = {
79+ id : string ;
80+ /** When set, the row is only removed if it still has this version. */
81+ version ?: number ;
82+ variableId : string ;
83+ key : string ;
84+ secretReferenceKey ?: string ;
85+ } ;
86+
87+ /**
88+ * The single code path that removes value rows of one environment together with their secret
89+ * store entries and secret references, using a fixed number of statements for any number of
90+ * rows. A variable left with no values afterwards is removed as well.
91+ */
92+ export async function deleteEnvironmentVariableValueRows (
93+ tx : PrismaClientOrTransaction ,
94+ projectId : string ,
95+ environmentId : string ,
96+ rows : EnvironmentVariableValueRow [ ]
97+ ) : Promise < { deleted : EnvironmentVariableValueRow [ ] ; skipped : EnvironmentVariableValueRow [ ] } > {
98+ if ( rows . length === 0 ) {
99+ return { deleted : [ ] , skipped : [ ] } ;
100+ }
101+
102+ const removed = await tx . environmentVariableValue . deleteMany ( {
103+ where : {
104+ OR : rows . map ( ( row ) =>
105+ row . version === undefined ? { id : row . id } : { id : row . id , version : row . version }
106+ ) ,
107+ } ,
108+ } ) ;
109+
110+ let deleted = rows ;
111+ let skipped : EnvironmentVariableValueRow [ ] = [ ] ;
112+ if ( removed . count < rows . length ) {
113+ const survivors = await tx . environmentVariableValue . findMany ( {
114+ where : { id : { in : boundedIn ( rows . map ( ( row ) => row . id ) ) } } ,
115+ select : { id : true } ,
116+ } ) ;
117+ const survivorIds = new Set ( survivors . map ( ( value ) => value . id ) ) ;
118+ deleted = rows . filter ( ( row ) => ! survivorIds . has ( row . id ) ) ;
119+ skipped = rows . filter ( ( row ) => survivorIds . has ( row . id ) ) ;
120+ }
121+
122+ if ( deleted . length === 0 ) {
123+ return { deleted, skipped } ;
124+ }
125+
126+ const referenceKeys = deleted . flatMap ( ( row ) =>
127+ row . secretReferenceKey ? [ row . secretReferenceKey ] : [ ]
128+ ) ;
129+ if ( referenceKeys . length > 0 ) {
130+ await tx . secretReference . deleteMany ( { where : { key : { in : boundedIn ( referenceKeys ) } } } ) ;
131+ }
132+
133+ await tx . secretStore . deleteMany ( {
134+ where : {
135+ key : { in : boundedIn ( deleted . map ( ( row ) => secretKey ( projectId , environmentId , row . key ) ) ) } ,
136+ } ,
137+ } ) ;
138+
139+ const emptied = await tx . environmentVariable . findMany ( {
140+ where : {
141+ id : { in : boundedIn ( deleted . map ( ( row ) => row . variableId ) ) } ,
142+ values : { none : { } } ,
143+ } ,
144+ select : { id : true } ,
145+ } ) ;
146+ if ( emptied . length > 0 ) {
147+ await tx . environmentVariable . deleteMany ( {
148+ where : { id : { in : boundedIn ( emptied . map ( ( variable ) => variable . id ) ) } } ,
149+ } ) ;
150+ }
151+
152+ return { deleted, skipped } ;
153+ }
154+
59155export class EnvironmentVariablesRepository implements Repository {
60156 constructor (
61157 private prismaClient : PrismaClient = prisma ,
@@ -870,11 +966,7 @@ export class EnvironmentVariablesRepository implements Repository {
870966 deletedAt : null ,
871967 } ,
872968 select : {
873- environments : {
874- select : {
875- id : true ,
876- } ,
877- } ,
969+ id : true ,
878970 } ,
879971 } ) ;
880972
@@ -887,9 +979,11 @@ export class EnvironmentVariablesRepository implements Repository {
887979 id : true ,
888980 key : true ,
889981 values : {
982+ where : {
983+ environmentId : options . environmentId ,
984+ } ,
890985 select : {
891986 id : true ,
892- environmentId : true ,
893987 valueReference : {
894988 select : {
895989 key : true ,
@@ -900,46 +994,30 @@ export class EnvironmentVariablesRepository implements Repository {
900994 } ,
901995 where : {
902996 id : options . id ,
997+ projectId,
903998 } ,
904999 } ) ;
9051000
9061001 if ( ! environmentVariable ) {
9071002 return { success : false as const , error : "Environment variable not found" } ;
9081003 }
9091004
910- const value = environmentVariable . values . find ( ( v ) => v . environmentId === options . environmentId ) ;
1005+ const value = environmentVariable . values [ 0 ] ;
9111006
9121007 if ( ! value ) {
9131008 return { success : false as const , error : "Environment variable value not found" } ;
9141009 }
9151010
916- // If this is the last value, delete the whole variable
917- if ( environmentVariable . values . length === 1 ) {
918- return this . delete ( projectId , { id : options . id } ) ;
919- }
920-
9211011 try {
9221012 await $transaction ( this . prismaClient , "delete env var value" , async ( tx ) => {
923- const secretStore = getSecretStore ( "DATABASE" , {
924- prismaClient : tx ,
925- } ) ;
926-
927- const key = secretKey ( projectId , options . environmentId , environmentVariable . key ) ;
928- await secretStore . deleteSecret ( key ) ;
929-
930- if ( value . valueReference ) {
931- await tx . secretReference . delete ( {
932- where : {
933- key : value . valueReference . key ,
934- } ,
935- } ) ;
936- }
937-
938- await tx . environmentVariableValue . delete ( {
939- where : {
1013+ await deleteEnvironmentVariableValueRows ( tx , projectId , options . environmentId , [
1014+ {
9401015 id : value . id ,
1016+ variableId : environmentVariable . id ,
1017+ key : environmentVariable . key ,
1018+ secretReferenceKey : value . valueReference ?. key ,
9411019 } ,
942- } ) ;
1020+ ] ) ;
9431021 } ) ;
9441022
9451023 return {
@@ -952,6 +1030,98 @@ export class EnvironmentVariablesRepository implements Repository {
9521030 } ;
9531031 }
9541032 }
1033+
1034+ async deleteValues (
1035+ projectId : string ,
1036+ options : DeleteEnvironmentVariableValues
1037+ ) : Promise < DeleteEnvironmentVariableValuesResult > {
1038+ const keys = Array . from ( new Set ( options . keys ) ) ;
1039+ if ( keys . length === 0 ) {
1040+ return { deleted : [ ] , skipped : [ ] } ;
1041+ }
1042+
1043+ const deletedKeys = await $transaction (
1044+ this . prismaClient ,
1045+ "delete env var values" ,
1046+ async ( tx ) => {
1047+ let parentEnvironmentId : string | undefined ;
1048+ if ( options . onlyShadowingParent ) {
1049+ const environment = await tx . runtimeEnvironment . findFirst ( {
1050+ where : { id : options . environmentId , projectId } ,
1051+ select : { parentEnvironmentId : true } ,
1052+ } ) ;
1053+ parentEnvironmentId = environment ?. parentEnvironmentId ?? undefined ;
1054+ if ( ! parentEnvironmentId ) {
1055+ return [ ] ;
1056+ }
1057+ }
1058+
1059+ const environmentIds = parentEnvironmentId
1060+ ? [ options . environmentId , parentEnvironmentId ]
1061+ : [ options . environmentId ] ;
1062+
1063+ const variables = await tx . environmentVariable . findMany ( {
1064+ where : {
1065+ projectId,
1066+ key : { in : boundedIn ( keys ) } ,
1067+ project : { deletedAt : null } ,
1068+ } ,
1069+ select : {
1070+ id : true ,
1071+ key : true ,
1072+ values : {
1073+ where : { environmentId : { in : boundedIn ( environmentIds ) } } ,
1074+ select : {
1075+ id : true ,
1076+ version : true ,
1077+ environmentId : true ,
1078+ lastUpdatedBy : true ,
1079+ valueReference : { select : { key : true } } ,
1080+ } ,
1081+ } ,
1082+ } ,
1083+ } ) ;
1084+
1085+ const rows : EnvironmentVariableValueRow [ ] = [ ] ;
1086+ for ( const variable of variables ) {
1087+ const own = variable . values . find ( ( v ) => v . environmentId === options . environmentId ) ;
1088+ if ( ! own ) {
1089+ continue ;
1090+ }
1091+ if ( options . onlyWrittenBy && ! isSameUpdater ( own . lastUpdatedBy , options . onlyWrittenBy ) ) {
1092+ continue ;
1093+ }
1094+ if (
1095+ parentEnvironmentId &&
1096+ ! variable . values . some ( ( v ) => v . environmentId === parentEnvironmentId )
1097+ ) {
1098+ continue ;
1099+ }
1100+ rows . push ( {
1101+ id : own . id ,
1102+ version : own . version ,
1103+ variableId : variable . id ,
1104+ key : variable . key ,
1105+ secretReferenceKey : own . valueReference ?. key ,
1106+ } ) ;
1107+ }
1108+
1109+ const { deleted } = await deleteEnvironmentVariableValueRows (
1110+ tx ,
1111+ projectId ,
1112+ options . environmentId ,
1113+ rows
1114+ ) ;
1115+ return deleted . map ( ( row ) => row . key ) ;
1116+ }
1117+ ) ;
1118+
1119+ const deleted = new Set ( deletedKeys ?? [ ] ) ;
1120+ return {
1121+ deleted : keys . filter ( ( key ) => deleted . has ( key ) ) ,
1122+ skipped : keys . filter ( ( key ) => ! deleted . has ( key ) ) ,
1123+ } ;
1124+ }
9551125}
9561126
9571127// Derived from the slim AuthenticatedEnvironment so a full AE satisfies
0 commit comments