@salesforce/plugin-org
Version:
Commands to interact with Salesforce orgs
249 lines • 10.8 kB
JavaScript
/*
* Copyright (c) 2022, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { Duration, omit } from '@salesforce/kit';
import { Flags } from '@salesforce/sf-plugins-core';
import { Lifecycle, Messages, SandboxEvents, SfError } from '@salesforce/core';
import requestFunctions from '../../../shared/sandboxRequest.js';
import { SandboxCommandBase } from '../../../shared/sandboxCommandBase.js';
// Fields of SandboxInfo
const uneditableFields = ['IsDeleted', 'CreatedDate', 'CreatedById', 'LastModifiedDate', 'LastModifiedById'];
const fields = [
'Id',
'SandboxName', // (string)
'LicenseType', // (string) DEVELOPER | DEVELOPER PRO | PARTIAL | FULL
'TemplateId', // (string) reference to PartitionLevelScheme
'HistoryDays', // (int)
'CopyChatter', // (boolean)
'AutoActivate', // (boolean)
'ApexClassId', // (string) apex class ID
'Description', // (string)
'SourceId', // (string) SandboxInfoId as the source org used for a clone
// 'ActivationUserGroupId', // Currently not supported but might be added in API v61.0
// 'CopyArchivedActivities', -- only for full sandboxes; depends if a license was purchased
...uneditableFields,
];
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-org', 'refresh.sandbox');
export default class RefreshSandbox extends SandboxCommandBase {
static summary = messages.getMessage('summary');
static description = messages.getMessage('description');
static examples = messages.getMessages('examples');
static flags = {
'no-auto-activate': Flags.boolean({
summary: messages.getMessage('flags.no-auto-activate.summary'),
description: messages.getMessage('flags.no-auto-activate.description'),
}),
wait: Flags.duration({
char: 'w',
summary: messages.getMessage('flags.wait.summary'),
description: messages.getMessage('flags.wait.description'),
min: 1,
unit: 'minutes',
default: Duration.minutes(30),
helpValue: '<minutes>',
exclusive: ['async'],
}),
'poll-interval': Flags.duration({
char: 'i',
summary: messages.getMessage('flags.poll-interval.summary'),
min: 15,
unit: 'seconds',
default: Duration.seconds(30),
helpValue: '<seconds>',
exclusive: ['async'],
}),
async: Flags.boolean({
summary: messages.getMessage('flags.async.summary'),
description: messages.getMessage('flags.async.description'),
exclusive: ['wait', 'poll-interval'],
}),
name: Flags.string({
char: 'n',
summary: messages.getMessage('flags.name.summary'),
parse: (name) => {
if (name.length > 10) {
throw messages.createError('error.SandboxNameLength', [name]);
}
return Promise.resolve(name);
},
}),
'definition-file': Flags.file({
exists: true,
char: 'f',
summary: messages.getMessage('flags.definitionFile.summary'),
description: messages.getMessage('flags.definitionFile.description'),
}),
'target-org': Flags.requiredOrg({
char: 'o',
summary: messages.getMessage('flags.targetOrg.summary'),
required: true,
}),
'no-prompt': Flags.boolean({
summary: messages.getMessage('flags.noPrompt.summary'),
}),
};
flags;
sbxConfig;
async run() {
this.sandboxRequestConfig = await this.getSandboxRequestConfig();
this.flags = (await this.parse(RefreshSandbox)).flags;
this.validateFlags();
this.sbxConfig = await this.resolveConfig();
this.debug('Refresh started with args %s ', this.flags);
return this.refreshSandbox();
}
getCheckSandboxStatusParams() {
return [
this.config.bin,
...(this.latestSandboxProgressObj ? [this.latestSandboxProgressObj.Id] : []),
this.flags['target-org'].getUsername(),
];
}
async refreshSandbox() {
this.prodOrg = this.flags['target-org'];
await this.confirmSandboxRefresh(this.sbxConfig);
const lifecycle = Lifecycle.getInstance();
this.registerLifecycleListenersAndMSO(lifecycle, {
mso: {
refresh: true,
title: 'Sandbox Refresh',
},
isAsync: this.flags['async'],
prodOrg: this.prodOrg,
});
// remove uneditable fields before refresh
const updateableSandboxInfo = omit(this.sbxConfig, uneditableFields);
this.debug('Calling refresh with SandboxInfo: %s ', updateableSandboxInfo);
this.initSandboxProcessData(this.sbxConfig);
try {
const sandboxProcessObject = await this.prodOrg.refreshSandbox(updateableSandboxInfo, {
wait: this.flags['wait'],
interval: this.flags['poll-interval'],
async: this.flags['async'],
});
this.latestSandboxProgressObj = sandboxProcessObject;
// persist sandbox refresh request in cache for resume
this.sandboxRequestData = {
prodOrgUsername: this.flags['target-org'].getUsername(),
action: 'Refresh',
sandboxProcessObject,
sandboxRequest: this.sbxConfig,
};
this.saveSandboxProgressConfig();
if (this.flags.async) {
process.exitCode = 68;
}
return this.getSandboxCommandResponse();
}
catch (err) {
if (this.pollingTimeOut && this.latestSandboxProgressObj) {
void lifecycle.emit(SandboxEvents.EVENT_ASYNC_RESULT, undefined);
process.exitCode = 68;
return this.getSandboxCommandResponse();
}
else if (err instanceof SfError &&
err.name === 'SandboxRefreshNotCompleteError' &&
this.latestSandboxProgressObj) {
void lifecycle.emit(SandboxEvents.EVENT_ASYNC_RESULT, undefined);
process.exitCode = 68;
return this.latestSandboxProgressObj;
}
throw err;
}
}
async resolveConfig() {
const defFile = this.flags['definition-file'];
let sbxName = this.flags['name'];
const defFileContent = defFile ? requestFunctions.readSandboxDefFile(defFile) : {};
// Ensure we have a sandbox name.
if (!defFileContent?.SandboxName && !sbxName) {
throw messages.createError('error.NoSandboxName');
}
let apexId;
let groupId;
if (defFileContent.ApexClassName) {
apexId = await requestFunctions.getApexClassIdByName(this.flags['target-org'].getConnection(), defFileContent.ApexClassName); // convert name to ID
delete defFileContent.ApexClassName;
}
if (defFileContent.ActivationUserGroupName) {
groupId = await requestFunctions.getUserGroupIdByName(this.flags['target-org'].getConnection(), defFileContent.ActivationUserGroupName);
delete defFileContent.ActivationUserGroupName;
}
// Warn if sandbox name is in `--name` and `--definition-file` flags and they differ.
if (defFileContent?.SandboxName && sbxName && sbxName !== defFileContent?.SandboxName) {
this.warn(messages.createWarning('warning.ConflictingSandboxNames', [sbxName, defFileContent?.SandboxName]));
}
sbxName ??= defFileContent.SandboxName; // The code above ensures a value for sbxName
const prodOrg = this.flags['target-org'];
const prodOrgConnection = prodOrg.getConnection();
let sandboxInfo;
try {
const soql = `SELECT ${fields.join(',')} FROM SandboxInfo WHERE SandboxName='${sbxName}'`;
const sandboxInfoRecord = await prodOrgConnection.singleRecordQuery(soql, { tooling: true });
sandboxInfo = omit(sandboxInfoRecord, 'attributes');
}
catch (error) {
const err = error instanceof Error ? error : typeof error === 'string' ? SfError.wrap(error) : new SfError('unknown');
if (err.name === 'SingleRecordQuery_NoRecords') {
throw messages.createError('error.SandboxNotFound', [sbxName, prodOrg.getUsername()]);
}
throw err;
}
// assign overrides
sandboxInfo = Object.assign(sandboxInfo, defFileContent, {
SandboxName: sbxName,
AutoActivate: !this.flags['no-auto-activate'],
...(apexId ? { ApexClassId: apexId } : {}),
...(groupId ? { ActivationUserGroupId: groupId } : {}),
});
return sandboxInfo;
}
// Ensure polling flags make sense
validateFlags() {
if (!this.flags['poll-interval'] || !this.flags.wait) {
return;
}
if (this.flags['poll-interval'].seconds > this.flags.wait.seconds) {
throw messages.createError('error.pollIntervalGreaterThanWait', [
this.flags['poll-interval'].seconds,
this.flags.wait.seconds,
]);
}
}
initSandboxProcessData(sandboxInfo) {
this.sandboxRequestData = {
...this.sandboxRequestData,
prodOrgUsername: this.flags['target-org'].getUsername(),
action: 'Refresh',
sandboxProcessObject: {
SandboxName: sandboxInfo.SandboxName,
},
sandboxRequest: sandboxInfo,
};
this.saveSandboxProgressConfig();
}
async confirmSandboxRefresh(sandboxInfo) {
if (this.flags['no-prompt'] || this.jsonEnabled())
return;
const data = Object.entries(sandboxInfo).map(([key, value]) => ({ key, value: value ?? 'null' }));
this.table({
data,
columns: [
{ key: 'key', name: 'Field' },
{ key: 'value', name: 'Value' },
],
title: 'Config Sandbox Refresh',
});
if (!(await this.confirm({
message: messages.getMessage('isConfigurationOk'),
ms: 30_000,
}))) {
throw messages.createError('error.UserNotSatisfiedWithSandboxConfig');
}
}
}
//# sourceMappingURL=sandbox.js.map