@@ -111,6 +111,19 @@ export type SandboxProcessObject = {
111
111
ApexClassId ?: string ;
112
112
EndDate ?: string ;
113
113
} ;
114
+ const sandboxProcessFields = [
115
+ 'Id' ,
116
+ 'Status' ,
117
+ 'SandboxName' ,
118
+ 'SandboxInfoId' ,
119
+ 'LicenseType' ,
120
+ 'CreatedDate' ,
121
+ 'CopyProgress' ,
122
+ 'SandboxOrganization' ,
123
+ 'SourceId' ,
124
+ 'Description' ,
125
+ 'EndDate' ,
126
+ ] ;
114
127
115
128
export type SandboxRequest = {
116
129
SandboxName : string ;
@@ -124,6 +137,27 @@ export type ResumeSandboxRequest = {
124
137
SandboxProcessObjId ?: string ;
125
138
} ;
126
139
140
+ // https://developer.salesforce.com/docs/atlas.en-us.api_tooling.meta/api_tooling/tooling_api_objects_sandboxinfo.htm
141
+ export type SandboxInfo = {
142
+ Id : string ; // 0GQB0000000TVobOAG
143
+ IsDeleted : boolean ;
144
+ CreatedDate : string ; // 2023-06-16T18:35:47.000+0000
145
+ CreatedById : string ; // 005B0000004TiUpIAK
146
+ LastModifiedDate : string ; // 2023-09-27T20:50:26.000+0000
147
+ LastModifiedById : string ; // 005B0000004TiUpIAK
148
+ SandboxName : string ; // must be 10 or less alphanumeric chars
149
+ LicenseType : 'DEVELOPER' | 'DEVELOPER PRO' | 'PARTIAL' | 'FULL' ;
150
+ TemplateId ?: string ; // reference to PartitionLevelScheme
151
+ HistoryDays : - 1 | 0 | 10 | 20 | 30 | 60 | 90 | 120 | 150 | 180 ; // full sandboxes only
152
+ CopyChatter : boolean ;
153
+ AutoActivate : boolean ; // only editable for an update/refresh
154
+ ApexClassId ?: string ; // apex class ID. Only editable on create.
155
+ Description ?: string ;
156
+ SourceId ?: string ; // SandboxInfoId as the source org used for a clone
157
+ // 'ActivationUserGroupId', // Support might be added back in API v61.0 (Summer '24)
158
+ CopyArchivedActivities ?: boolean ; // only for full sandboxes; depends if a license was purchased
159
+ } ;
160
+
127
161
export type ScratchOrgRequest = Omit < ScratchOrgCreateOptions , 'hubOrg' > ;
128
162
129
163
export type SandboxFields = {
@@ -227,6 +261,62 @@ export class Org extends AsyncOptionalCreatable<Org.Options> {
227
261
} ) ;
228
262
}
229
263
264
+ /**
265
+ * Refresh (update) a sandbox from a production org.
266
+ * 'this' needs to be a production org with sandbox licenses available
267
+ *
268
+ * @param sandboxInfo SandboxInfo to update the sandbox with
269
+ * @param options Wait: The amount of time to wait before timing out, Interval: The time interval between polling
270
+ */
271
+ public async refreshSandbox (
272
+ sandboxInfo : SandboxInfo ,
273
+ options : { wait ?: Duration ; interval ?: Duration ; async ?: boolean } = {
274
+ wait : Duration . minutes ( 6 ) ,
275
+ async : false ,
276
+ interval : Duration . seconds ( 30 ) ,
277
+ }
278
+ ) : Promise < SandboxProcessObject > {
279
+ this . logger . debug ( sandboxInfo , 'RefreshSandbox called with SandboxInfo' ) ;
280
+ const refreshResult = await this . connection . tooling . update ( 'SandboxInfo' , sandboxInfo ) ;
281
+ this . logger . debug ( refreshResult , 'Return from calling tooling.update' ) ;
282
+
283
+ if ( ! refreshResult . success ) {
284
+ throw messages . createError ( 'sandboxInfoRefreshFailed' , [ JSON . stringify ( refreshResult ) ] ) ;
285
+ }
286
+
287
+ const soql = `SELECT ${ sandboxProcessFields . join ( ',' ) } FROM SandboxProcess WHERE SandboxName='${
288
+ sandboxInfo . SandboxName
289
+ } ' ORDER BY CreatedDate DESC`;
290
+ const sbxProcessObjects = ( await this . connection . tooling . query < SandboxProcessObject > ( soql ) ) . records . filter (
291
+ ( item ) => ! item . Status . startsWith ( 'Del' )
292
+ ) ;
293
+ this . logger . debug ( sbxProcessObjects , `SandboxProcesses for ${ sandboxInfo . SandboxName } ` ) ;
294
+
295
+ // throw if none found
296
+ if ( sbxProcessObjects ?. length === 0 ) {
297
+ throw new Error ( `No SandboxProcesses found for: ${ sandboxInfo . SandboxName } ` ) ;
298
+ }
299
+ const sandboxRefreshProgress = sbxProcessObjects [ 0 ] ;
300
+
301
+ const isAsync = ! ! options . async ;
302
+
303
+ if ( isAsync ) {
304
+ // The user didn't want us to poll, so simply return the status
305
+ await Lifecycle . getInstance ( ) . emit ( SandboxEvents . EVENT_ASYNC_RESULT , sandboxRefreshProgress ) ;
306
+ return sandboxRefreshProgress ;
307
+ }
308
+ const [ wait , pollInterval ] = this . validateWaitOptions ( options ) ;
309
+ this . logger . debug (
310
+ sandboxRefreshProgress ,
311
+ `refresh - pollStatusAndAuth sandboxProcessObj, max wait time of ${ wait . minutes } minutes`
312
+ ) ;
313
+ return this . pollStatusAndAuth ( {
314
+ sandboxProcessObj : sandboxRefreshProgress ,
315
+ wait,
316
+ pollInterval,
317
+ } ) ;
318
+ }
319
+
230
320
/**
231
321
*
232
322
* @param sandboxReq SandboxRequest options to create the sandbox with
@@ -245,10 +335,10 @@ export class Org extends AsyncOptionalCreatable<Org.Options> {
245
335
}
246
336
247
337
/**
248
- * Resume a sandbox creation from a production org.
338
+ * Resume a sandbox create or refresh from a production org.
249
339
* `this` needs to be a production org with sandbox licenses available.
250
340
*
251
- * @param resumeSandboxRequest SandboxRequest options to create the sandbox with
341
+ * @param resumeSandboxRequest SandboxRequest options to create/refresh the sandbox with
252
342
* @param options Wait: The amount of time to wait (default: 0 minutes) before timing out,
253
343
* Interval: The time interval (default: 30 seconds) between polling
254
344
*/
@@ -1046,7 +1136,9 @@ export class Org extends AsyncOptionalCreatable<Org.Options> {
1046
1136
private async queryLatestSandboxProcessBySandboxName ( sandboxNameIn : string ) : Promise < SandboxProcessObject > {
1047
1137
const { tooling } = this . getConnection ( ) ;
1048
1138
this . logger . debug ( `QueryLatestSandboxProcessBySandboxName called with SandboxName: ${ sandboxNameIn } ` ) ;
1049
- const queryStr = `SELECT Id, Status, SandboxName, SandboxInfoId, LicenseType, CreatedDate, CopyProgress, SandboxOrganization, SourceId, Description, EndDate FROM SandboxProcess WHERE SandboxName='${ sandboxNameIn } ' AND Status != 'D' ORDER BY CreatedDate DESC LIMIT 1` ;
1139
+ const queryStr = `SELECT ${ sandboxProcessFields . join (
1140
+ ','
1141
+ ) } FROM SandboxProcess WHERE SandboxName='${ sandboxNameIn } ' AND Status != 'D' ORDER BY CreatedDate DESC LIMIT 1`;
1050
1142
1051
1143
const queryResult = await tooling . query ( queryStr ) ;
1052
1144
this . logger . debug ( queryResult , 'Return from calling queryToolingApi' ) ;
@@ -1291,6 +1383,17 @@ export class Org extends AsyncOptionalCreatable<Org.Options> {
1291
1383
oauth2Options . clientId = productionAuthFields . clientId ;
1292
1384
}
1293
1385
1386
+ // Before creating the AuthInfo, delete any existing auth files for the sandbox.
1387
+ // This is common when refreshing sandboxes, and will cause AuthInfo to throw
1388
+ // because it doesn't want to overwrite existing auth files.
1389
+ const stateAggregator = await StateAggregator . getInstance ( ) ;
1390
+ try {
1391
+ await stateAggregator . orgs . read ( sandboxRes . authUserName ) ;
1392
+ await stateAggregator . orgs . remove ( sandboxRes . authUserName ) ;
1393
+ } catch ( e ) {
1394
+ // ignore since this is only for deleting existing auth files.
1395
+ }
1396
+
1294
1397
const authInfo = await AuthInfo . create ( {
1295
1398
username : sandboxRes . authUserName ,
1296
1399
oauth2Options,
@@ -1305,8 +1408,11 @@ export class Org extends AsyncOptionalCreatable<Org.Options> {
1305
1408
} ,
1306
1409
'Creating AuthInfo for sandbox'
1307
1410
) ;
1308
- // save auth info for new sandbox
1309
- await authInfo . save ( ) ;
1411
+ // save auth info for sandbox
1412
+ await authInfo . save ( {
1413
+ isScratch : false ,
1414
+ isSandbox : true ,
1415
+ } ) ;
1310
1416
1311
1417
const sandboxOrgId = authInfo . getFields ( ) . orgId ;
1312
1418
@@ -1390,7 +1496,9 @@ export class Org extends AsyncOptionalCreatable<Org.Options> {
1390
1496
* @private
1391
1497
*/
1392
1498
private async querySandboxProcess ( where : string ) : Promise < SandboxProcessObject > {
1393
- const soql = `SELECT Id, Status, SandboxName, SandboxInfoId, LicenseType, CreatedDate, CopyProgress, SandboxOrganization, SourceId, Description, EndDate FROM SandboxProcess WHERE ${ where } ORDER BY CreatedDate DESC` ;
1499
+ const soql = `SELECT ${ sandboxProcessFields . join (
1500
+ ','
1501
+ ) } FROM SandboxProcess WHERE ${ where } ORDER BY CreatedDate DESC`;
1394
1502
const result = ( await this . connection . tooling . query < SandboxProcessObject > ( soql ) ) . records . filter (
1395
1503
( item ) => ! item . Status . startsWith ( 'Del' )
1396
1504
) ;
@@ -1445,15 +1553,15 @@ export class Org extends AsyncOptionalCreatable<Org.Options> {
1445
1553
1446
1554
this . logger . debug ( result , 'Result of calling sandboxAuth' ) ;
1447
1555
return result ;
1448
- } catch ( err ) {
1449
- const error = err as Error ;
1556
+ } catch ( err : unknown ) {
1557
+ const error = err instanceof Error ? err : SfError . wrap ( isString ( err ) ? err : 'unknown' ) ;
1450
1558
// There are cases where the endDate is set before the sandbox has actually completed.
1451
1559
// In that case, the sandboxAuth call will throw a specific exception.
1452
1560
if ( error ?. name === 'INVALID_STATUS' ) {
1453
- this . logger . debug ( 'Error while authenticating the user' , error ?. toString ( ) ) ;
1561
+ this . logger . debug ( 'Error while authenticating the user: ' , error . message ) ;
1454
1562
} else {
1455
- // If it fails for any unexpected reason, just pass that through
1456
- throw SfError . wrap ( error ) ;
1563
+ // If it fails for any unexpected reason, rethrow
1564
+ throw error ;
1457
1565
}
1458
1566
}
1459
1567
}
0 commit comments