Skip to content

Commit 1071303

Browse files
committed
feat: add support for refreshing sandboxes
1 parent 5bd3456 commit 1071303

File tree

5 files changed

+110
-9
lines changed

5 files changed

+110
-9
lines changed

messages/org.md

+4
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ We can't find a SandboxProcess for the sandbox %s.
3434

3535
The sandbox org creation failed with a result of %s.
3636

37+
# sandboxInfoRefreshFailed
38+
39+
The sandbox org refresh failed with a result of %s.
40+
3741
# missingAuthUsername
3842

3943
The sandbox %s does not have an authorized username.

src/config/sandboxProcessCache.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ import { TTLConfig } from './ttlConfig';
1111

1212
export type SandboxRequestCacheEntry = {
1313
alias?: string;
14-
setDefault: boolean;
14+
setDefault?: boolean;
1515
prodOrgUsername: string;
16+
action: 'Create' | 'Refresh'; // Sandbox create and refresh requests can be cached
1617
sandboxProcessObject: Partial<SandboxProcessObject>;
1718
sandboxRequest: Partial<SandboxRequest>;
1819
tracksSource?: boolean;

src/exported.ts

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export {
6262
Org,
6363
SandboxProcessObject,
6464
StatusEvent,
65+
SandboxInfo,
6566
SandboxEvents,
6667
SandboxUserAuthResponse,
6768
SandboxUserAuthRequest,

src/org/org.ts

+101-6
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,19 @@ export type SandboxProcessObject = {
111111
ApexClassId?: string;
112112
EndDate?: string;
113113
};
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+
];
114127

115128
export type SandboxRequest = {
116129
SandboxName: string;
@@ -124,6 +137,27 @@ export type ResumeSandboxRequest = {
124137
SandboxProcessObjId?: string;
125138
};
126139

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+
127161
export type ScratchOrgRequest = Omit<ScratchOrgCreateOptions, 'hubOrg'>;
128162

129163
export type SandboxFields = {
@@ -227,6 +261,62 @@ export class Org extends AsyncOptionalCreatable<Org.Options> {
227261
});
228262
}
229263

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+
230320
/**
231321
*
232322
* @param sandboxReq SandboxRequest options to create the sandbox with
@@ -245,10 +335,10 @@ export class Org extends AsyncOptionalCreatable<Org.Options> {
245335
}
246336

247337
/**
248-
* Resume a sandbox creation from a production org.
338+
* Resume a sandbox create or refresh from a production org.
249339
* `this` needs to be a production org with sandbox licenses available.
250340
*
251-
* @param resumeSandboxRequest SandboxRequest options to create the sandbox with
341+
* @param resumeSandboxRequest SandboxRequest options to create/refresh the sandbox with
252342
* @param options Wait: The amount of time to wait (default: 0 minutes) before timing out,
253343
* Interval: The time interval (default: 30 seconds) between polling
254344
*/
@@ -1293,7 +1383,6 @@ export class Org extends AsyncOptionalCreatable<Org.Options> {
12931383

12941384
const authInfo = await AuthInfo.create({
12951385
username: sandboxRes.authUserName,
1296-
oauth2Options,
12971386
parentUsername: productionAuthFields.username,
12981387
});
12991388

@@ -1305,8 +1394,12 @@ export class Org extends AsyncOptionalCreatable<Org.Options> {
13051394
},
13061395
'Creating AuthInfo for sandbox'
13071396
);
1308-
// save auth info for new sandbox
1309-
await authInfo.save();
1397+
// save auth info for sandbox
1398+
await authInfo.save({
1399+
...oauth2Options,
1400+
isScratch: false,
1401+
isSandbox: true,
1402+
});
13101403

13111404
const sandboxOrgId = authInfo.getFields().orgId;
13121405

@@ -1390,7 +1483,9 @@ export class Org extends AsyncOptionalCreatable<Org.Options> {
13901483
* @private
13911484
*/
13921485
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`;
1486+
const soql = `SELECT ${sandboxProcessFields.join(
1487+
','
1488+
)} FROM SandboxProcess WHERE ${where} ORDER BY CreatedDate DESC`;
13941489
const result = (await this.connection.tooling.query<SandboxProcessObject>(soql)).records.filter(
13951490
(item) => !item.Status.startsWith('Del')
13961491
);

test/unit/org/orgTest.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1059,7 +1059,7 @@ describe('Org Tests', () => {
10591059

10601060
describe('resumeSandbox', () => {
10611061
const expectedSoql =
1062-
'SELECT Id, Status, SandboxName, SandboxInfoId, LicenseType, CreatedDate, CopyProgress, SandboxOrganization, SourceId, Description, EndDate FROM SandboxProcess WHERE %s ORDER BY CreatedDate DESC';
1062+
'SELECT Id,Status,SandboxName,SandboxInfoId,LicenseType,CreatedDate,CopyProgress,SandboxOrganization,SourceId,Description,EndDate FROM SandboxProcess WHERE %s ORDER BY CreatedDate DESC';
10631063
let lifecycleSpy: SinonSpy;
10641064
let queryStub: SinonStub;
10651065
let pollStatusAndAuthSpy: SinonSpy;
@@ -1250,7 +1250,7 @@ describe('Org Tests', () => {
12501250
const deletedSbxProcess = Object.assign({}, statusResult.records[0], { Status: 'Deleted' });
12511251
queryStub.resolves({ records: [deletingSbxProcess, statusResult.records[0], deletedSbxProcess] });
12521252
const where = 'name="foo"';
1253-
const expectedSoql = `SELECT Id, Status, SandboxName, SandboxInfoId, LicenseType, CreatedDate, CopyProgress, SandboxOrganization, SourceId, Description, EndDate FROM SandboxProcess WHERE ${where} ORDER BY CreatedDate DESC`;
1253+
const expectedSoql = `SELECT Id,Status,SandboxName,SandboxInfoId,LicenseType,CreatedDate,CopyProgress,SandboxOrganization,SourceId,Description,EndDate FROM SandboxProcess WHERE ${where} ORDER BY CreatedDate DESC`;
12541254

12551255
// @ts-ignore Testing a private method
12561256
const sbxProcess = await prod.querySandboxProcess(where);

0 commit comments

Comments
 (0)