Skip to content

Commit 51ce237

Browse files
committed
feat: add bundle uninstall command
1 parent 9c8fa8a commit 51ce237

7 files changed

Lines changed: 456 additions & 0 deletions

File tree

messages/bundle_uninstall.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# failedToGetPackageBundleUninstallStatus
2+
3+
Failed to get package bundle uninstall status
4+
5+
# failedToUninstallPackageBundle
6+
7+
Failed to uninstall package bundle
8+
9+
# uninstallTimedOut
10+
11+
Uninstall request %s timed out while waiting for completion.
12+

src/exported.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ export * from './package/packageBundle';
2121
export * from './package/packageBundleVersionCreate';
2222
export * from './package/packageBundleInstall';
2323
export * from './package/packageBundleInstalledList';
24+
export * from './package/packageBundleUninstall';

src/interfaces/bundleInterfacesAndType.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,14 @@ export type BundleInstallOptions = {
5050
};
5151
};
5252

53+
export type BundleUninstallOptions = {
54+
connection: Connection;
55+
project: SfProject;
56+
PackageBundleVersion: string;
57+
polling?: {
58+
timeout: Duration;
59+
frequency: Duration;
60+
};
61+
};
62+
5363
export type BundleSaveResult = SaveResult;

src/interfaces/bundleSObjects.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,20 @@ export namespace BundleSObjects {
135135
error = 'Error',
136136
}
137137

138+
export enum PkgBundleVersionUninstallReqStatus {
139+
queued = 'Queued',
140+
inProgress = 'InProgress',
141+
success = 'Success',
142+
error = 'Error',
143+
}
144+
145+
export enum PkgBundleVerCpntUnistlReqStatus {
146+
queued = 'Queued',
147+
inProgress = 'InProgress',
148+
success = 'Success',
149+
error = 'Error',
150+
}
151+
138152
export type PkgBundleVersionQueryRecord = {
139153
Id: string;
140154
RequestStatus: BundleSObjects.PkgBundleVersionCreateReqStatus;
@@ -175,6 +189,41 @@ export namespace BundleSObjects {
175189
Error?: string[];
176190
} & Schema;
177191

192+
export type PkgBundleVerUninstallReq = {
193+
PackageBundleVersionId: string;
194+
InstalledPkgBundleVersionId?: string;
195+
ValidationError?: string;
196+
};
197+
198+
export type PkgBundleVerUninstallReqResult = PkgBundleVerUninstallReq & {
199+
Id: string;
200+
UninstallStatus: PkgBundleVersionUninstallReqStatus;
201+
CreatedDate: string;
202+
CreatedById: string;
203+
Error?: string[];
204+
};
205+
206+
export type PkgBundleVerUninstallReqQueryRecord = {
207+
Id: string;
208+
UninstallStatus: PkgBundleVersionUninstallReqStatus;
209+
PackageBundleVersionId: string;
210+
InstalledPkgBundleVersionId?: string;
211+
ValidationError?: string;
212+
CreatedDate: string;
213+
CreatedById: string;
214+
Error?: string[];
215+
} & Schema;
216+
217+
export type PkgBundleVerCpntUnistlReqRecord = {
218+
Id: string;
219+
SequenceOrder: number;
220+
UninstallStatus: PkgBundleVerCpntUnistlReqStatus;
221+
PkgBundleVersionComponent?: {
222+
Id: string;
223+
};
224+
Error?: string;
225+
} & Schema;
226+
178227
export type InstalledPackageBundleVersionComponent = {
179228
ExpectedPackageName: string;
180229
ExpectedPackageVersionNumber: string;

src/package/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ export { VersionNumber } from './versionNumber';
2121
export * from './packageBundle';
2222
export * from './packageBundleVersion';
2323
export * from './packageBundleInstall';
24+
export * from './packageBundleUninstall';
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/*
2+
* Copyright 2025, Salesforce, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import { Connection, Lifecycle, Messages, PollingClient, SfError, SfProject, StatusResult } from '@salesforce/core';
17+
import { Duration } from '@salesforce/kit';
18+
import { BundleSObjects, BundleUninstallOptions } from '../interfaces';
19+
import { massageErrorMessage } from '../utils/bundleUtils';
20+
21+
Messages.importMessagesDirectory(__dirname);
22+
const messages = Messages.loadMessages('@salesforce/packaging', 'bundle_uninstall');
23+
const installMessages = Messages.loadMessages('@salesforce/packaging', 'bundle_install');
24+
25+
export class PackageBundleUninstall {
26+
public static async getUninstallStatus(
27+
uninstallRequestId: string,
28+
connection: Connection
29+
): Promise<BundleSObjects.PkgBundleVerUninstallReqResult> {
30+
try {
31+
const query =
32+
'SELECT Id, UninstallStatus, PackageBundleVersionId, InstalledPkgBundleVersionId, ValidationError, ' +
33+
'CreatedDate, CreatedById ' +
34+
`FROM PkgBundleVerUninstallReq WHERE Id = '${uninstallRequestId}'`;
35+
36+
const queryResult = await connection.autoFetchQuery<BundleSObjects.PkgBundleVerUninstallReqQueryRecord>(query, {
37+
tooling: true,
38+
});
39+
40+
if (!queryResult.records || queryResult.records.length === 0) {
41+
throw new Error(messages.getMessage('failedToGetPackageBundleUninstallStatus'));
42+
}
43+
44+
const record = queryResult.records[0];
45+
46+
return {
47+
Id: record.Id,
48+
UninstallStatus: record.UninstallStatus,
49+
PackageBundleVersionId: record.PackageBundleVersionId ?? '',
50+
InstalledPkgBundleVersionId: record.InstalledPkgBundleVersionId ?? '',
51+
ValidationError: record.ValidationError ?? '',
52+
CreatedDate: record.CreatedDate ?? '',
53+
CreatedById: record.CreatedById ?? '',
54+
Error: record.Error,
55+
};
56+
} catch (err) {
57+
const error =
58+
err instanceof Error ? err : new Error(messages.getMessage('failedToGetPackageBundleUninstallStatus'));
59+
throw SfError.wrap(massageErrorMessage(error));
60+
}
61+
}
62+
63+
public static async getUninstallStatuses(
64+
connection: Connection,
65+
status?: BundleSObjects.PkgBundleVersionUninstallReqStatus,
66+
createdLastDays?: number
67+
): Promise<BundleSObjects.PkgBundleVerUninstallReqResult[]> {
68+
let query =
69+
'SELECT Id, UninstallStatus, PackageBundleVersionId, InstalledPkgBundleVersionId, ValidationError, ' +
70+
'CreatedDate, CreatedById ' +
71+
'FROM PkgBundleVerUninstallReq';
72+
73+
if (status && createdLastDays) {
74+
query += ` WHERE UninstallStatus = '${status}' AND CreatedDate = LAST_N_DAYS: ${createdLastDays}`;
75+
} else if (status) {
76+
query += ` WHERE UninstallStatus = '${status}'`;
77+
} else if (createdLastDays) {
78+
query += ` WHERE CreatedDate = LAST_N_DAYS: ${createdLastDays}`;
79+
}
80+
81+
const queryResult = await connection.autoFetchQuery<BundleSObjects.PkgBundleVerUninstallReqQueryRecord>(query, {
82+
tooling: true,
83+
});
84+
85+
return queryResult.records.map((record) => ({
86+
Id: record.Id,
87+
UninstallStatus: record.UninstallStatus,
88+
PackageBundleVersionId: record.PackageBundleVersionId ?? '',
89+
InstalledPkgBundleVersionId: record.InstalledPkgBundleVersionId ?? '',
90+
ValidationError: record.ValidationError ?? '',
91+
CreatedDate: record.CreatedDate ?? '',
92+
CreatedById: record.CreatedById ?? '',
93+
Error: record.Error,
94+
}));
95+
}
96+
97+
public static async uninstallBundle(
98+
connection: Connection,
99+
project: SfProject,
100+
options: BundleUninstallOptions
101+
): Promise<BundleSObjects.PkgBundleVerUninstallReqResult> {
102+
const packageBundleVersionId = PackageBundleUninstall.parsePackageBundleVersionId(
103+
options.PackageBundleVersion,
104+
project
105+
);
106+
107+
const request: BundleSObjects.PkgBundleVerUninstallReq = {
108+
PackageBundleVersionId: packageBundleVersionId,
109+
};
110+
111+
let uninstallResult;
112+
try {
113+
uninstallResult = await connection.tooling.sobject('PkgBundleVerUninstallReq').create(request);
114+
} catch (err) {
115+
const error =
116+
err instanceof Error
117+
? err
118+
: new Error(typeof err === 'string' ? err : messages.getMessage('failedToUninstallPackageBundle'));
119+
throw SfError.wrap(massageErrorMessage(error));
120+
}
121+
122+
if (!uninstallResult?.success) {
123+
throw SfError.wrap(massageErrorMessage(new Error(messages.getMessage('failedToUninstallPackageBundle'))));
124+
}
125+
126+
if (options.polling) {
127+
return PackageBundleUninstall.pollUninstallStatus(uninstallResult.id, connection, options.polling);
128+
}
129+
130+
// When not polling, query the actual status from the server to get accurate information
131+
return PackageBundleUninstall.getUninstallStatus(uninstallResult.id, connection);
132+
}
133+
134+
private static parsePackageBundleVersionId(packageBundleVersion: string, project: SfProject): string {
135+
// Check if it's already an ID (starts with appropriate prefix)
136+
if (/^1Q8.{15}$/.test(packageBundleVersion)) {
137+
return packageBundleVersion;
138+
}
139+
140+
// Otherwise, treat it as an alias and resolve it from sfdx-project.json
141+
const packageBundleVersionId = project.getPackageBundleIdFromAlias(packageBundleVersion);
142+
if (!packageBundleVersionId) {
143+
throw new SfError(installMessages.getMessage('noPackageBundleVersionFoundWithAlias', [packageBundleVersion]));
144+
}
145+
return packageBundleVersionId;
146+
}
147+
148+
private static async pollUninstallStatus(
149+
uninstallRequestId: string,
150+
connection: Connection,
151+
polling: { timeout: Duration; frequency: Duration }
152+
): Promise<BundleSObjects.PkgBundleVerUninstallReqResult> {
153+
if (polling.timeout?.milliseconds <= 0) {
154+
return PackageBundleUninstall.getUninstallStatus(uninstallRequestId, connection);
155+
}
156+
157+
let remainingWaitTime: Duration = polling.timeout;
158+
const pollingClient = await PollingClient.create({
159+
poll: async (): Promise<StatusResult> => {
160+
const report = await PackageBundleUninstall.getUninstallStatus(uninstallRequestId, connection);
161+
switch (report.UninstallStatus) {
162+
case BundleSObjects.PkgBundleVersionUninstallReqStatus.queued:
163+
case BundleSObjects.PkgBundleVersionUninstallReqStatus.inProgress:
164+
// Emit progress event for UI updates
165+
await Lifecycle.getInstance().emit('bundle-uninstall-progress', { ...report, remainingWaitTime });
166+
remainingWaitTime = Duration.seconds(remainingWaitTime.seconds - polling.frequency.seconds);
167+
return {
168+
completed: false,
169+
payload: report,
170+
};
171+
case BundleSObjects.PkgBundleVersionUninstallReqStatus.success:
172+
return { completed: true, payload: report };
173+
case BundleSObjects.PkgBundleVersionUninstallReqStatus.error:
174+
return { completed: true, payload: report };
175+
default:
176+
await Lifecycle.getInstance().emit('bundle-uninstall-progress', { ...report, remainingWaitTime });
177+
remainingWaitTime = Duration.seconds(remainingWaitTime.seconds - polling.frequency.seconds);
178+
return {
179+
completed: false,
180+
payload: report,
181+
};
182+
}
183+
},
184+
frequency: polling.frequency,
185+
timeout: polling.timeout,
186+
});
187+
188+
try {
189+
return await pollingClient.subscribe<BundleSObjects.PkgBundleVerUninstallReqResult>();
190+
} catch (err) {
191+
const report = await PackageBundleUninstall.getUninstallStatus(uninstallRequestId, connection);
192+
if (err instanceof Error) {
193+
const timeoutError = new SfError(
194+
messages.getMessage('uninstallTimedOut', [uninstallRequestId]),
195+
'BundleUninstallTimeout'
196+
);
197+
timeoutError.setData({ UninstallRequestId: uninstallRequestId, ...report });
198+
throw timeoutError;
199+
}
200+
throw err;
201+
}
202+
}
203+
}
204+

0 commit comments

Comments
 (0)