Skip to content

Commit

Permalink
feat: add support for refreshing sandboxes
Browse files Browse the repository at this point in the history
  • Loading branch information
shetzel committed Feb 21, 2024
1 parent 5bd3456 commit 1071303
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 9 deletions.
4 changes: 4 additions & 0 deletions messages/org.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ We can't find a SandboxProcess for the sandbox %s.

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

# sandboxInfoRefreshFailed

The sandbox org refresh failed with a result of %s.

# missingAuthUsername

The sandbox %s does not have an authorized username.
Expand Down
3 changes: 2 additions & 1 deletion src/config/sandboxProcessCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import { TTLConfig } from './ttlConfig';

export type SandboxRequestCacheEntry = {
alias?: string;
setDefault: boolean;
setDefault?: boolean;
prodOrgUsername: string;
action: 'Create' | 'Refresh'; // Sandbox create and refresh requests can be cached
sandboxProcessObject: Partial<SandboxProcessObject>;
sandboxRequest: Partial<SandboxRequest>;
tracksSource?: boolean;
Expand Down
1 change: 1 addition & 0 deletions src/exported.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export {
Org,
SandboxProcessObject,
StatusEvent,
SandboxInfo,
SandboxEvents,
SandboxUserAuthResponse,
SandboxUserAuthRequest,
Expand Down
107 changes: 101 additions & 6 deletions src/org/org.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,19 @@ export type SandboxProcessObject = {
ApexClassId?: string;
EndDate?: string;
};
const sandboxProcessFields = [
'Id',
'Status',
'SandboxName',
'SandboxInfoId',
'LicenseType',
'CreatedDate',
'CopyProgress',
'SandboxOrganization',
'SourceId',
'Description',
'EndDate',
];

export type SandboxRequest = {
SandboxName: string;
Expand All @@ -124,6 +137,27 @@ export type ResumeSandboxRequest = {
SandboxProcessObjId?: string;
};

// https://developer.salesforce.com/docs/atlas.en-us.api_tooling.meta/api_tooling/tooling_api_objects_sandboxinfo.htm
export type SandboxInfo = {
Id: string; // 0GQB0000000TVobOAG
IsDeleted: boolean;
CreatedDate: string; // 2023-06-16T18:35:47.000+0000
CreatedById: string; // 005B0000004TiUpIAK
LastModifiedDate: string; // 2023-09-27T20:50:26.000+0000
LastModifiedById: string; // 005B0000004TiUpIAK
SandboxName: string; // must be 10 or less alphanumeric chars
LicenseType: 'DEVELOPER' | 'DEVELOPER PRO' | 'PARTIAL' | 'FULL';
TemplateId?: string; // reference to PartitionLevelScheme
HistoryDays: -1 | 0 | 10 | 20 | 30 | 60 | 90 | 120 | 150 | 180; // full sandboxes only
CopyChatter: boolean;
AutoActivate: boolean; // only editable for an update/refresh
ApexClassId?: string; // apex class ID. Only editable on create.
Description?: string;
SourceId?: string; // SandboxInfoId as the source org used for a clone
// 'ActivationUserGroupId', // Support might be added back in API v61.0 (Summer '24)
CopyArchivedActivities?: boolean; // only for full sandboxes; depends if a license was purchased
};

export type ScratchOrgRequest = Omit<ScratchOrgCreateOptions, 'hubOrg'>;

export type SandboxFields = {
Expand Down Expand Up @@ -227,6 +261,62 @@ export class Org extends AsyncOptionalCreatable<Org.Options> {
});
}

/**
* Refresh (update) a sandbox from a production org.
* 'this' needs to be a production org with sandbox licenses available
*
* @param sandboxInfo SandboxInfo to update the sandbox with
* @param options Wait: The amount of time to wait before timing out, Interval: The time interval between polling
*/
public async refreshSandbox(
sandboxInfo: SandboxInfo,
options: { wait?: Duration; interval?: Duration; async?: boolean } = {
wait: Duration.minutes(6),
async: false,
interval: Duration.seconds(30),
}
): Promise<SandboxProcessObject> {
this.logger.debug(sandboxInfo, 'RefreshSandbox called with SandboxInfo');
const refreshResult = await this.connection.tooling.update('SandboxInfo', sandboxInfo);
this.logger.debug(refreshResult, 'Return from calling tooling.update');

if (!refreshResult.success) {
throw messages.createError('sandboxInfoRefreshFailed', [JSON.stringify(refreshResult)]);
}

const soql = `SELECT ${sandboxProcessFields.join(',')} FROM SandboxProcess WHERE SandboxName='${
sandboxInfo.SandboxName
}' ORDER BY CreatedDate DESC`;
const sbxProcessObjects = (await this.connection.tooling.query<SandboxProcessObject>(soql)).records.filter(
(item) => !item.Status.startsWith('Del')
);
this.logger.debug(sbxProcessObjects, `SandboxProcesses for ${sandboxInfo.SandboxName}`);

// throw if none found
if (sbxProcessObjects?.length === 0) {
throw new Error(`No SandboxProcesses found for: ${sandboxInfo.SandboxName}`);
}
const sandboxRefreshProgress = sbxProcessObjects[0];

const isAsync = !!options.async;

if (isAsync) {
// The user didn't want us to poll, so simply return the status
await Lifecycle.getInstance().emit(SandboxEvents.EVENT_ASYNC_RESULT, sandboxRefreshProgress);
return sandboxRefreshProgress;
}
const [wait, pollInterval] = this.validateWaitOptions(options);
this.logger.debug(
sandboxRefreshProgress,
`refresh - pollStatusAndAuth sandboxProcessObj, max wait time of ${wait.minutes} minutes`
);
return this.pollStatusAndAuth({
sandboxProcessObj: sandboxRefreshProgress,
wait,
pollInterval,
});
}

/**
*
* @param sandboxReq SandboxRequest options to create the sandbox with
Expand All @@ -245,10 +335,10 @@ export class Org extends AsyncOptionalCreatable<Org.Options> {
}

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

const authInfo = await AuthInfo.create({
username: sandboxRes.authUserName,
oauth2Options,
parentUsername: productionAuthFields.username,
});

Expand All @@ -1305,8 +1394,12 @@ export class Org extends AsyncOptionalCreatable<Org.Options> {
},
'Creating AuthInfo for sandbox'
);
// save auth info for new sandbox
await authInfo.save();
// save auth info for sandbox
await authInfo.save({
...oauth2Options,
isScratch: false,
isSandbox: true,
});

const sandboxOrgId = authInfo.getFields().orgId;

Expand Down Expand Up @@ -1390,7 +1483,9 @@ export class Org extends AsyncOptionalCreatable<Org.Options> {
* @private
*/
private async querySandboxProcess(where: string): Promise<SandboxProcessObject> {
const soql = `SELECT Id, Status, SandboxName, SandboxInfoId, LicenseType, CreatedDate, CopyProgress, SandboxOrganization, SourceId, Description, EndDate FROM SandboxProcess WHERE ${where} ORDER BY CreatedDate DESC`;
const soql = `SELECT ${sandboxProcessFields.join(
','
)} FROM SandboxProcess WHERE ${where} ORDER BY CreatedDate DESC`;
const result = (await this.connection.tooling.query<SandboxProcessObject>(soql)).records.filter(
(item) => !item.Status.startsWith('Del')
);
Expand Down
4 changes: 2 additions & 2 deletions test/unit/org/orgTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1059,7 +1059,7 @@ describe('Org Tests', () => {

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

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

3 comments on commit 1071303

@svc-cli-bot
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logger Benchmarks - ubuntu-latest

Benchmark suite Current: 1071303 Previous: 33cac99 Ratio
Child logger creation 488730 ops/sec (±0.92%) 491427 ops/sec (±0.51%) 1.01
Logging a string on root logger 820383 ops/sec (±8.80%) 837404 ops/sec (±9.79%) 1.02
Logging an object on root logger 654174 ops/sec (±7.97%) 649825 ops/sec (±9.85%) 0.99
Logging an object with a message on root logger 5736 ops/sec (±211.22%) 26352 ops/sec (±184.20%) 4.59
Logging an object with a redacted prop on root logger 461732 ops/sec (±8.97%) 515283 ops/sec (±7.36%) 1.12
Logging a nested 3-level object on root logger 376128 ops/sec (±7.91%) 24867 ops/sec (±183.63%) 0.06611313169984687

This comment was automatically generated by workflow using github-action-benchmark.

@svc-cli-bot
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark 'Logger Benchmarks - ubuntu-latest'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 2.

Benchmark suite Current: 1071303 Previous: 33cac99 Ratio
Logging an object with a message on root logger 5736 ops/sec (±211.22%) 26352 ops/sec (±184.20%) 4.59

This comment was automatically generated by workflow using github-action-benchmark.

@svc-cli-bot
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logger Benchmarks - windows-latest

Benchmark suite Current: 1071303 Previous: 33cac99 Ratio
Child logger creation 328142 ops/sec (±0.72%) 331956 ops/sec (±1.67%) 1.01
Logging a string on root logger 758472 ops/sec (±8.33%) 781846 ops/sec (±9.08%) 1.03
Logging an object on root logger 567257 ops/sec (±7.70%) 605912 ops/sec (±6.94%) 1.07
Logging an object with a message on root logger 13352 ops/sec (±188.32%) 7638 ops/sec (±201.72%) 0.57
Logging an object with a redacted prop on root logger 452237 ops/sec (±7.07%) 405076 ops/sec (±12.76%) 0.90
Logging a nested 3-level object on root logger 334072 ops/sec (±4.95%) 322975 ops/sec (±4.36%) 0.97

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.