Skip to content

Commit 7053925

Browse files
authored
Merge pull request #1502 from salesforcecli/er/cascadeDeleteWarning
W-21197350: add warning message for AAB cascade delete
2 parents 67585e0 + 8e58483 commit 7053925

3 files changed

Lines changed: 67 additions & 1 deletion

File tree

messages/delete.source.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ This operation will delete the following metadata in your org:
125125
This operation will deploy the following:
126126
%s
127127

128+
# cascadeDeleteWarning
129+
130+
When you delete components of type "%s", the org also deletes the following related metadata (known as a "cascade delete"): %s.
131+
128132
# areYouSure
129133

130134
Are you sure you want to proceed?

src/commands/project/delete/source.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,27 @@ Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
5959
const messages = Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'delete.source');
6060
const xorFlags = ['metadata', 'source-dir'];
6161

62+
/**
63+
* Metadata types that trigger cascade deletion in the org.
64+
*/
65+
const CASCADE_DELETE_TYPES: Record<string, string[]> = {
66+
AiAuthoringBundle: ['Bot, BotVersion, GenAiPlannerBundle'],
67+
};
68+
69+
/**
70+
* Returns warning messages for components that trigger cascade deletion in the org.
71+
*/
72+
const getCascadeDeleteWarnings = (typesBeingDeleted: Set<string>): string[] => {
73+
const warnings: string[] = [];
74+
for (const typeName of typesBeingDeleted) {
75+
const cascadeTypes = CASCADE_DELETE_TYPES[typeName];
76+
if (cascadeTypes?.length) {
77+
warnings.push(messages.getMessage('cascadeDeleteWarning', [typeName, cascadeTypes.join(', ')]));
78+
}
79+
}
80+
return warnings;
81+
};
82+
6283
type MixedDeployDelete = { deploy: string[]; delete: FileResponseSuccess[] };
6384
export class Source extends SfCommand<DeleteSourceJson> {
6485
public static readonly summary = messages.getMessage('summary');
@@ -459,6 +480,9 @@ Update the .forceignore file and try again.`);
459480
)
460481
.concat(this.mixedDeployDelete.delete.map((fr) => `${fr.fullName} (${fr.filePath})`));
461482

483+
const typesBeingDeleted = new Set((this.components ?? []).map((comp) => comp.type.name));
484+
const cascadeWarnings = getCascadeDeleteWarnings(typesBeingDeleted);
485+
462486
const message: string[] = [
463487
...(this.mixedDeployDelete.deploy.length
464488
? [messages.getMessage('deployPrompt', [[...new Set(this.mixedDeployDelete.deploy)].join('\n')])]
@@ -470,6 +494,8 @@ Update the .forceignore file and try again.`);
470494
...(local.length && (this.mixedDeployDelete.deploy.length || remote.length) ? ['\n'] : []),
471495
...(local.length ? [messages.getMessage('localPrompt', [[...new Set(local)].join('\n')])] : []),
472496

497+
...(cascadeWarnings.length ? [...cascadeWarnings] : []),
498+
473499
this.flags['check-only'] ?? false
474500
? messages.getMessage('areYouSureCheckOnly')
475501
: messages.getMessage('areYouSure'),

test/commands/delete/source.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ const agentComponents: SourceComponent[] = [
7474
}),
7575
];
7676

77+
// Component with type that triggers cascade delete warning (AiAuthoringBundle)
78+
const aiAuthoringBundleComponent = new SourceComponent({
79+
name: 'MyAiBundle',
80+
type: registry.getTypeByName('AiAuthoringBundle'),
81+
xml: '/dreamhouse-lwc/force-app/main/default/aiAuthoringBundles/MyAiBundle.agent-meta.xml',
82+
});
83+
7784
export const exampleDeleteResponse = {
7885
// required but ignored by the delete UT
7986
getFileResponses: (): void => {},
@@ -168,6 +175,7 @@ describe('project delete source', () => {
168175
options?: {
169176
sourceApiVersion?: string;
170177
inquirerMock?: { checkbox: sinon.SinonStub };
178+
captureConfirmMessage?: { ref: { message: string } };
171179
}
172180
) => {
173181
const cmd = new TestDelete(params, oclifConfigStub);
@@ -192,7 +200,15 @@ describe('project delete source', () => {
192200
onCancel: () => {},
193201
onError: () => {},
194202
});
195-
handlePromptStub = stubMethod($$.SANDBOX, cmd, 'handlePrompt').returns(confirm);
203+
if (options?.captureConfirmMessage) {
204+
const messageRef = options.captureConfirmMessage.ref;
205+
stubMethod($$.SANDBOX, SfCommand.prototype, 'confirm').callsFake(async (opts: { message: string }) => {
206+
messageRef.message = opts.message;
207+
return true;
208+
});
209+
} else {
210+
handlePromptStub = stubMethod($$.SANDBOX, cmd, 'handlePrompt').returns(confirm);
211+
}
196212
if (options?.inquirerMock) {
197213
// @ts-expect-error stubbing private member of the command
198214
cmd.inquirer = options.inquirerMock;
@@ -371,4 +387,24 @@ describe('project delete source', () => {
371387
});
372388
ensureHookArgs();
373389
});
390+
391+
it('should include cascade delete warning in prompt when deleting AiAuthoringBundle', async () => {
392+
buildComponentSetStub.restore();
393+
buildComponentSetStub = stubMethod($$.SANDBOX, ComponentSetBuilder, 'build').resolves({
394+
toArray: () => [aiAuthoringBundleComponent],
395+
forceIgnoredPaths: undefined,
396+
apiVersion: '65.0',
397+
sourceApiVersion: '65.0',
398+
});
399+
400+
const captured = { message: '' };
401+
await runDeleteCmd(['--metadata', 'AiAuthoringBundle:MyAiBundle', '--json'], {
402+
captureConfirmMessage: { ref: captured },
403+
});
404+
405+
expect(captured.message).to.include('AiAuthoringBundle');
406+
expect(captured.message).to.include('cascade');
407+
expect(captured.message).to.include('Bot, BotVersion, GenAiPlannerBundle');
408+
ensureHookArgs();
409+
});
374410
});

0 commit comments

Comments
 (0)