Skip to content

Commit a23ce06

Browse files
feat: add source flags for sbx create (#1237)
* feat: name fields for definitionfile * fix: update the changes * fix: definition-file refresh payload * testing * feat: create flag and field for sandbox * fix: update the name and id for definition file * fix: sandbox name * fix: update sourceSandboxName and Id * fix: update the fields and error messages * fix: update the error message * fix: update the error messages * fix: update the snapshot of alias clone * fix: error message * fix: update clone message * fix: validate error messages for cloning * chore: ci-rerun * fix: add validation and messages * fix: message description --------- Co-authored-by: Cristian Dominguez <[email protected]>
1 parent 289ac09 commit a23ce06

File tree

6 files changed

+166
-64
lines changed

6 files changed

+166
-64
lines changed

command-snapshot.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22
{
33
"alias": ["env:create:sandbox"],
44
"command": "org:create:sandbox",
5-
"flagAliases": [],
6-
"flagChars": ["a", "c", "f", "i", "l", "n", "o", "s", "w"],
5+
"flagAliases": ["c", "clone"],
6+
"flagChars": ["a", "f", "i", "l", "n", "o", "s", "w"],
77
"flags": [
88
"alias",
99
"async",
10-
"clone",
1110
"definition-file",
1211
"flags-dir",
1312
"json",
@@ -17,6 +16,8 @@
1716
"no-track-source",
1817
"poll-interval",
1918
"set-default",
19+
"source-id",
20+
"source-sandbox-name",
2021
"target-org",
2122
"wait"
2223
],

messages/clone.md

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
# missingSourceSandboxName
1+
# missingSourceSandboxNameORSourceId
22

3-
Specify a value for %s in a definition file or on the command line.
3+
Specify a value for name or ID in a definition file or on the command line.
44

5-
# missingSourceSandboxNameAction
5+
# missingSourceSandboxNameORSourceIdAction
66

7-
To indicate which sandbox org you want to clone, specify %s in a definition file or as a command line argument.
7+
To indicate which sandbox org you want to clone, specify name or ID in a definition file or as a command line argument.
88

99
# error.bothApexClassIdAndNameProvided
1010

@@ -21,3 +21,19 @@ Unable to find the ID of the activation user group "%s" that's defined in the de
2121
# error.apexClassQueryFailed
2222

2323
Unable to find the ID of the Apex class "%s" that's defined in the definition file.
24+
25+
# error.bothSourceIdAndNameProvided
26+
27+
You can't specify both `SourceId` and `SourceSandboxName` in the definition file at the same time.
28+
29+
# error.sandboxNameQueryFailed
30+
31+
Unable to find the ID of the sandbox "%s" that's defined in the definition file or as a command line argument.
32+
33+
# error.bothSourceIdAndLicenseTypeProvided
34+
35+
You can't specify both `SourceId` and `LicenseType` in the definition file at the same time.
36+
37+
# error.bothSourceSandboxNameAndLicenseTypeProvided
38+
39+
You can't specify both `SourceSandboxName` and `LicenseType` in the definition file at the same time.

messages/create.sandbox.md

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Create a sandbox org.
66

77
There are two ways to create a sandbox org: specify a definition file that contains the sandbox options or use the --name and --license-type flags to specify the two required options. If you want to set an option other than name or license type, such as apexClassId, you must use a definition file.
88

9-
You can also use this command to clone an existing sandbox. Use the --clone flag to specify the existing sandbox name and the --name flag to the name of the new sandbox.
9+
You can also use this command to clone an existing sandbox. Use the --source-sandbox-name flag to specify the existing sandbox name and the --name flag to the name of the new sandbox.
1010

1111
# examples
1212

@@ -20,7 +20,7 @@ You can also use this command to clone an existing sandbox. Use the --clone flag
2020

2121
- Clone the existing sandbox with name "ExistingSandbox" and name the new sandbox "NewClonedSandbox". Set the new sandbox as your default org. Wait for 30 minutes for the sandbox creation to complete.
2222

23-
<%= config.bin %> <%= command.id %> --clone ExistingSandbox --name NewClonedSandbox --target-org prodOrg --alias MyDevSandbox --set-default --wait 30
23+
<%= config.bin %> <%= command.id %> --source-sandbox-name ExistingSandbox --name NewClonedSandbox --target-org prodOrg --alias MyDevSandbox --set-default --wait 30
2424

2525
# flags.setDefault.summary
2626

@@ -58,13 +58,25 @@ Name of the sandbox org.
5858

5959
The name must be a unique alphanumeric string (10 or fewer characters) to identify the sandbox. You can’t reuse a name while a sandbox is in the process of being deleted.
6060

61-
# flags.clone.summary
61+
# flags.source-sandbox-name.summary
6262

6363
Name of the sandbox org to clone.
6464

65-
# flags.clone.description
65+
# flags.source-sandbox-name.description
6666

67-
The value of --clone must be an existing sandbox. The existing sandbox, and the new sandbox specified with the --name flag, must both be associated with the production org (--target-org) that contains the sandbox licenses.
67+
The value of --source-sandbox-name must be an existing sandbox. The existing sandbox, and the new sandbox specified with the --name flag, must both be associated with the production org (--target-org) that contains the sandbox licenses.
68+
69+
You can specify either --source-sandbox-name or --source-id when cloning an existing sandbox, but not both.
70+
71+
# flags.source-id.summary
72+
73+
ID of the sandbox org to clone.
74+
75+
# flags.source-id.description
76+
77+
The value of --source-id must be an existing sandbox. The existing sandbox, and the new sandbox specified with the --name flag, must both be associated with the production org (--target-org) that contains the sandbox licenses.
78+
79+
You can specify either --source-sandbox-name or --source-id when cloning an existing sandbox, but not both.
6880

6981
# flags.licenseType.summary
7082

@@ -120,6 +132,14 @@ The sandbox request configuration isn't acceptable.
120132

121133
The poll interval (%d seconds) can't be larger that wait (%d in seconds)
122134

123-
# error.noCloneSource
135+
# error.bothIdFlagAndDefFilePropertyAreProvided
136+
137+
You can't specify both the --source-id and --definition-file flags, and also include the "SourceId" option in the definition file. Pick one method of cloning a sandbox and try again.
138+
139+
# error.bothNameFlagAndDefFilePropertyAreProvided
140+
141+
You can't specify both the --source-sandbox-name and --definition-file flags, and also include the "SourceSandboxName" option in the definition file. Pick one method of cloning a sandbox and try again.
142+
143+
# error.bothIdFlagAndNameDefFileAreNotAllowed
124144

125-
Could not find the clone sandbox name "%s" in the target org.
145+
You can't specify both the --source-sandbox-name and --definition-file flags, and also include the "SourceId" option in the definition file. Same with the --source-id flag and "SourceSandboxName" option. Pick one method of cloning a sandbox and try again.

src/commands/org/create/sandbox.ts

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Duration } from '@salesforce/kit';
99
import { Flags } from '@salesforce/sf-plugins-core';
1010
import { Lifecycle, Messages, SandboxEvents, SandboxRequest, SfError } from '@salesforce/core';
1111
import { Interfaces } from '@oclif/core';
12-
import requestFunctions from '../../../shared/sandboxRequest.js';
12+
import requestFunctions, { readSandboxDefFile } from '../../../shared/sandboxRequest.js';
1313
import { SandboxCommandBase, SandboxCommandResponse } from '../../../shared/sandboxCommandBase.js';
1414
import { SandboxLicenseType } from '../../../shared/orgTypes.js';
1515

@@ -27,6 +27,7 @@ export default class CreateSandbox extends SandboxCommandBase<SandboxCommandResp
2727
public static readonly aliases = ['env:create:sandbox'];
2828
public static readonly deprecateAliases = true;
2929

30+
// eslint-disable-next-line sf-plugin/spread-base-flags
3031
public static flags = {
3132
// needs to change when new flags are available
3233
'definition-file': Flags.file({
@@ -79,18 +80,26 @@ export default class CreateSandbox extends SandboxCommandBase<SandboxCommandResp
7980
return Promise.resolve(name);
8081
},
8182
}),
82-
clone: Flags.string({
83-
char: 'c',
84-
summary: messages.getMessage('flags.clone.summary'),
85-
description: messages.getMessage('flags.clone.description'),
83+
'source-sandbox-name': Flags.string({
84+
summary: messages.getMessage('flags.source-sandbox-name.summary'),
85+
description: messages.getMessage('flags.source-sandbox-name.description'),
86+
exclusive: ['license-type', 'source-id'],
87+
deprecateAliases: true,
88+
aliases: ['clone', 'c'],
89+
}),
90+
'source-id': Flags.salesforceId({
91+
summary: messages.getMessage('flags.source-id.summary'),
92+
description: messages.getMessage('flags.source-id.description'),
8693
exclusive: ['license-type'],
94+
length: 'both',
95+
char: undefined,
8796
}),
8897
'license-type': Flags.custom<SandboxLicenseType>({
8998
options: getLicenseTypes(),
9099
})({
91100
char: 'l',
92101
summary: messages.getMessage('flags.licenseType.summary'),
93-
exclusive: ['clone'],
102+
exclusive: ['source-sandbox-name', 'source-id'],
94103
}),
95104
'target-org': Flags.requiredOrg({
96105
char: 'o',
@@ -112,6 +121,7 @@ export default class CreateSandbox extends SandboxCommandBase<SandboxCommandResp
112121
public async run(): Promise<SandboxCommandResponse> {
113122
this.sandboxRequestConfig = await this.getSandboxRequestConfig();
114123
this.flags = (await this.parse(CreateSandbox)).flags;
124+
115125
this.debug('Create started with args %s ', this.flags);
116126
this.validateFlags();
117127
return this.createSandbox();
@@ -129,17 +139,25 @@ export default class CreateSandbox extends SandboxCommandBase<SandboxCommandResp
129139
// reuse the existing sandbox request generator, with this command's flags as the varargs
130140
const requestOptions = {
131141
...(this.flags.name ? { SandboxName: this.flags.name } : {}),
132-
...(this.flags.clone ? { SourceSandboxName: this.flags.clone } : {}),
133-
...(!this.flags.clone && this.flags['license-type'] ? { LicenseType: this.flags['license-type'] } : {}),
142+
...(this.flags['source-sandbox-name']
143+
? { SourceSandboxName: this.flags['source-sandbox-name'] }
144+
: this.flags['source-id']
145+
? { SourceId: this.flags['source-id'] }
146+
: {}),
147+
...(!this.flags['source-sandbox-name'] && !this.flags['source-id'] && this.flags['license-type']
148+
? { LicenseType: this.flags['license-type'] }
149+
: {}),
134150
};
135-
const { sandboxReq } = !this.flags.clone
136-
? await requestFunctions.createSandboxRequest(false, this.flags['definition-file'], undefined, requestOptions)
137-
: await requestFunctions.createSandboxRequest(true, this.flags['definition-file'], undefined, requestOptions);
151+
152+
const { sandboxReq, srcSandboxName, srcId } = await requestFunctions.createSandboxRequest(
153+
this.flags['definition-file'],
154+
undefined,
155+
requestOptions
156+
);
138157

139158
let apexId: string | undefined;
140159
let groupId: string | undefined;
141160

142-
// Determine which value to use
143161
if (sandboxReq.ApexClassName) {
144162
apexId = await requestFunctions.getApexClassIdByName(
145163
this.flags['target-org'].getConnection(),
@@ -155,9 +173,13 @@ export default class CreateSandbox extends SandboxCommandBase<SandboxCommandResp
155173
);
156174
delete sandboxReq.ActivationUserGroupName;
157175
}
176+
158177
return {
159178
...sandboxReq,
160-
...(this.flags.clone ? { SourceId: await this.getSourceId() } : {}),
179+
...(srcSandboxName
180+
? { SourceId: await requestFunctions.getSrcIdByName(this.flags['target-org'].getConnection(), srcSandboxName) }
181+
: {}),
182+
...(srcId ? { SourceId: srcId } : {}),
161183
...(apexId ? { ApexClassId: apexId } : {}),
162184
...(groupId ? { ActivationUserGroupId: groupId } : {}),
163185
};
@@ -176,7 +198,9 @@ export default class CreateSandbox extends SandboxCommandBase<SandboxCommandResp
176198
tracksSource: this.flags['no-track-source'] === true ? false : undefined,
177199
});
178200
const sandboxReq = await this.createSandboxRequest();
179-
await this.confirmSandboxReq({ ...sandboxReq, ...(this.flags.clone ? { CloneSource: this.flags.clone } : {}) });
201+
await this.confirmSandboxReq({
202+
...sandboxReq,
203+
});
180204
this.initSandboxProcessData(sandboxReq);
181205

182206
if (!this.flags.async) {
@@ -265,17 +289,21 @@ export default class CreateSandbox extends SandboxCommandBase<SandboxCommandResp
265289
this.flags.wait.seconds,
266290
]);
267291
}
268-
}
269-
270-
private async getSourceId(): Promise<string | undefined> {
271-
if (!this.flags.clone) {
292+
if (!this.flags['definition-file']) {
272293
return undefined;
273294
}
274-
try {
275-
const sourceOrg = await this.flags['target-org'].querySandboxProcessBySandboxName(this.flags.clone);
276-
return sourceOrg.SandboxInfoId;
277-
} catch (err) {
278-
throw messages.createError('error.noCloneSource', [this.flags.clone], [], err as Error);
295+
const parsedDef = readSandboxDefFile(this.flags['definition-file']);
296+
if (this.flags['source-id'] && parsedDef.SourceId) {
297+
throw messages.createError('error.bothIdFlagAndDefFilePropertyAreProvided');
298+
}
299+
if (this.flags['source-sandbox-name'] && parsedDef.SourceSandboxName) {
300+
throw messages.createError('error.bothNameFlagAndDefFilePropertyAreProvided');
301+
}
302+
if (this.flags['source-id'] && parsedDef.SourceSandboxName) {
303+
throw messages.createError('error.bothIdFlagAndNameDefFileAreNotAllowed');
304+
}
305+
if (this.flags['source-sandbox-name'] && parsedDef.SourceId) {
306+
throw messages.createError('error.bothIdFlagAndNameDefFileAreNotAllowed');
279307
}
280308
}
281309
}

src/shared/sandboxRequest.ts

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const generateSboxName = async (): Promise<string> => {
2424
// Reads the sandbox definition file and converts properties to CapCase.
2525
export function readSandboxDefFile(
2626
defFile: string
27-
): Partial<SandboxInfo & { ApexClassName?: string; ActivationUserGroupName?: string }> {
27+
): Partial<SandboxInfo & { ApexClassName?: string; ActivationUserGroupName?: string; SourceSandboxName?: string }> {
2828
const fileContent = fs.readFileSync(defFile, 'utf-8');
2929
const parsedContent = lowerToUpper(JSON.parse(fileContent) as Record<string, unknown>);
3030

@@ -36,11 +36,23 @@ export function readSandboxDefFile(
3636
if (parsedContent.ActivationUserGroupId && parsedContent.ActivationUserGroupName) {
3737
throw cloneMessages.createError('error.bothUserGroupIdAndNameProvided');
3838
}
39+
40+
if (parsedContent.SourceId && parsedContent.SourceSandboxName) {
41+
throw cloneMessages.createError('error.bothSourceIdAndNameProvided');
42+
}
43+
44+
if (parsedContent.SourceId && parsedContent.LicenseType) {
45+
throw cloneMessages.createError('error.bothSourceIdAndLicenseTypeProvided');
46+
}
47+
48+
if (parsedContent.LicenseType && parsedContent.SourceSandboxName) {
49+
throw cloneMessages.createError('error.bothSourceSandboxNameAndLicenseTypeProvided');
50+
}
51+
3952
return parsedContent as Partial<SandboxInfo>;
4053
}
4154

4255
export async function createSandboxRequest(
43-
isClone: true,
4456
definitionFile: string | undefined,
4557
logger?: Logger | undefined,
4658
properties?: Record<string, string | undefined>
@@ -50,9 +62,9 @@ export async function createSandboxRequest(
5062
ActivationUserGroupName: string | undefined;
5163
};
5264
srcSandboxName: string;
65+
srcId: string;
5366
}>;
5467
export async function createSandboxRequest(
55-
isClone: false,
5668
definitionFile: string | undefined,
5769
logger?: Logger | undefined,
5870
properties?: Record<string, string | undefined>
@@ -63,11 +75,10 @@ export async function createSandboxRequest(
6375
};
6476
}>;
6577
export async function createSandboxRequest(
66-
isClone = false,
6778
definitionFile: string | undefined,
6879
logger?: Logger | undefined,
6980
properties?: Record<string, string | undefined>
70-
): Promise<{ sandboxReq: SandboxRequest; srcSandboxName?: string }> {
81+
): Promise<{ sandboxReq: SandboxRequest; srcSandboxName?: string; srcId?: string }> {
7182
if (!logger) {
7283
logger = await Logger.child('createSandboxRequest');
7384
}
@@ -76,29 +87,28 @@ export async function createSandboxRequest(
7687
const sandboxDefFileContents = definitionFile ? readSandboxDefFile(definitionFile) : {};
7788

7889
const capitalizedVarArgs = properties ? lowerToUpper(properties) : {};
79-
8090
// varargs override file input
81-
const sandboxReqWithName: SandboxRequest & { SourceSandboxName?: string } = {
91+
const sandboxReqWithName: SandboxRequest & { SourceSandboxName?: string; SourceId?: string } = {
8292
...(sandboxDefFileContents as Record<string, unknown>),
8393
...capitalizedVarArgs,
8494
SandboxName:
8595
(capitalizedVarArgs.SandboxName as string) ??
8696
(sandboxDefFileContents.SandboxName as string) ??
8797
(await generateSboxName()),
8898
};
89-
90-
const { SourceSandboxName, ...sandboxReq } = sandboxReqWithName;
99+
const isClone = sandboxReqWithName.SourceSandboxName ?? sandboxReqWithName.SourceId;
100+
const { SourceSandboxName, SourceId, ...sandboxReq } = sandboxReqWithName;
91101
logger.debug('SandboxRequest after merging DefFile and Varargs: %s ', sandboxReq);
92102

93103
if (isClone) {
94-
if (!SourceSandboxName) {
95-
// error - we need SourceSandboxName to know which sandbox to clone from
104+
if (!sandboxReqWithName.SourceSandboxName && !sandboxReqWithName.SourceId) {
105+
// error - we need SourceSandboxName or SourceID to know which sandbox to clone from
96106
throw new SfError(
97-
cloneMessages.getMessage('missingSourceSandboxName', ['SourceSandboxName']),
98-
cloneMessages.getMessage('missingSourceSandboxNameAction', ['SourceSandboxName'])
107+
cloneMessages.getMessage('missingSourceSandboxNameORSourceId'),
108+
cloneMessages.getMessage('missingSourceSandboxNameORSourceIdAction')
99109
);
100110
}
101-
return { sandboxReq, srcSandboxName: SourceSandboxName };
111+
return { sandboxReq, srcSandboxName: SourceSandboxName, srcId: SourceId };
102112
} else {
103113
if (!sandboxReq.LicenseType) {
104114
return { sandboxReq: { ...sandboxReq, LicenseType: SandboxLicenseType.developer } };
@@ -122,10 +132,22 @@ export async function getUserGroupIdByName(conn: Connection, groupName: string):
122132
throw cloneMessages.createError('error.userGroupQueryFailed', [groupName], [], err as Error);
123133
}
124134
}
135+
export async function getSrcIdByName(conn: Connection, sandboxName: string): Promise<string | undefined> {
136+
try {
137+
const result = (
138+
await conn.singleRecordQuery(`SELECT id FROM SandboxInfo WHERE SandboxName = '${sandboxName}'`, { tooling: true })
139+
).Id;
140+
return result;
141+
} catch (err) {
142+
throw cloneMessages.createError('error.sandboxNameQueryFailed', [sandboxName], [], err as Error);
143+
}
144+
}
145+
125146
export default {
126147
createSandboxRequest,
127148
generateSboxName,
128149
readSandboxDefFile,
129150
getApexClassIdByName,
130151
getUserGroupIdByName,
152+
getSrcIdByName,
131153
};

0 commit comments

Comments
 (0)