Skip to content

Commit 71a2321

Browse files
committed
feat: add bundle version installed list command
1 parent c31e695 commit 71a2321

5 files changed

Lines changed: 506 additions & 254 deletions

File tree

messages/bundle_installed_list.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# failedToGetInstalledBundles
2+
3+
Failed to get installed package bundles from the target org.

src/exported.ts

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

src/interfaces/bundleSObjects.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,4 +174,90 @@ export namespace BundleSObjects {
174174
CreatedById: string;
175175
Error?: string[];
176176
} & Schema;
177+
178+
export type InstalledPackageBundleVersionComponent = {
179+
ExpectedPackageName: string;
180+
ExpectedPackageVersionNumber: string;
181+
ActualPackageName: string;
182+
ActualPackageVersionNumber: string;
183+
};
184+
185+
export type InstalledPackageBundleVersion = {
186+
Id: string;
187+
BundleName: string;
188+
BundleId: string;
189+
BundleVersionId: string;
190+
BundleVersionName: string;
191+
MajorVersion: number;
192+
MinorVersion: number;
193+
Description: string;
194+
InstalledDate: string;
195+
LastUpgradedDate: string;
196+
Components: InstalledPackageBundleVersionComponent[];
197+
};
198+
199+
export type InstalledPackageBundleVersionQueryRecord = {
200+
Id: string;
201+
PackageBundleVersion: {
202+
Id: string;
203+
VersionName: string;
204+
MajorVersion: number;
205+
MinorVersion: number;
206+
PackageBundle: {
207+
Id: string;
208+
BundleName: string;
209+
Description: string;
210+
};
211+
};
212+
InstalledDate: string;
213+
LastUpgradedDate: string;
214+
} & Schema;
215+
216+
export type InstalledBundleRecord = {
217+
Id: string;
218+
PackageBundleId?: string; // CROSSORGFOREIGNKEY field ID
219+
PackageBundleVersionId?: string; // CROSSORGFOREIGNKEY field ID
220+
BundleName: string;
221+
BundleVersionName: string;
222+
MajorVersion: number;
223+
MinorVersion: number;
224+
CreatedDate?: string;
225+
LastModifiedDate?: string;
226+
} & Schema;
227+
228+
export type InstallRequestRecord = {
229+
Id: string;
230+
} & Schema;
231+
232+
export type BundleComponentInstallRecord = {
233+
SubscriberPackageVersion: {
234+
Id: string;
235+
SubscriberPackageId: string;
236+
MajorVersion: number;
237+
MinorVersion: number;
238+
PatchVersion: number;
239+
BuildNumber: number;
240+
};
241+
InstalledComponent?: {
242+
SubscriberPackage?: {
243+
Name: string;
244+
};
245+
};
246+
SequenceOrder: number;
247+
} & Schema;
248+
249+
export type InstalledPackageRecord = {
250+
Id: string;
251+
SubscriberPackageId: string;
252+
SubscriberPackage: {
253+
Name: string;
254+
};
255+
SubscriberPackageVersion: {
256+
Id: string;
257+
MajorVersion: number;
258+
MinorVersion: number;
259+
PatchVersion: number;
260+
BuildNumber: number;
261+
};
262+
} & Schema;
177263
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
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, Messages, SfError } from '@salesforce/core';
17+
import { BundleSObjects } from '../interfaces';
18+
import { massageErrorMessage } from '../utils/bundleUtils';
19+
20+
Messages.importMessagesDirectory(__dirname);
21+
const messages = Messages.loadMessages('@salesforce/packaging', 'bundle_installed_list');
22+
23+
export class PackageBundleInstalledList {
24+
/**
25+
* Get all installed package bundles in the target org
26+
*
27+
* @param connection - Connection to the target org (where bundles are installed)
28+
* @returns Array of installed bundle versions with their component packages
29+
*/
30+
public static async getInstalledBundles(
31+
connection: Connection
32+
): Promise<BundleSObjects.InstalledPackageBundleVersion[]> {
33+
try {
34+
// Query InstalledPkgBundleVersion directly from the target org
35+
// Note: PackageBundle and PackageBundleVersion are CROSSORGFOREIGNKEY fields
36+
// Use CreatedDate and LastModifiedDate as proxies for InstalledDate and LastUpgradedDate
37+
const query =
38+
'SELECT Id, BundleName, BundleVersionName, MajorVersion, MinorVersion, ' +
39+
'PackageBundleId, PackageBundleVersionId, CreatedDate, LastModifiedDate ' +
40+
'FROM InstalledPkgBundleVersion ' +
41+
'ORDER BY CreatedDate DESC';
42+
43+
const queryResult = await connection.autoFetchQuery<BundleSObjects.InstalledBundleRecord>(query, {
44+
tooling: true,
45+
});
46+
47+
if (!queryResult.records || queryResult.records.length === 0) {
48+
return [];
49+
}
50+
51+
// For each installed bundle, get component details
52+
const installedBundles = await Promise.all(
53+
queryResult.records.map(async (record) => {
54+
// Get component details from PkgBundleVersionComponentInstallReq
55+
// We need to find the install request that corresponds to this installed bundle
56+
const components = await PackageBundleInstalledList.getBundleComponents(connection, record.Id);
57+
58+
return {
59+
Id: record.Id,
60+
BundleName: record.BundleName,
61+
BundleId: record.PackageBundleId ?? '',
62+
BundleVersionId: record.PackageBundleVersionId ?? '',
63+
BundleVersionName: record.BundleVersionName,
64+
MajorVersion: record.MajorVersion,
65+
MinorVersion: record.MinorVersion,
66+
Description: '',
67+
InstalledDate: record.CreatedDate ?? '',
68+
LastUpgradedDate: record.LastModifiedDate ?? record.CreatedDate ?? '',
69+
Components: components,
70+
};
71+
})
72+
);
73+
74+
// Filter out any null results
75+
return installedBundles.filter(
76+
(bundle): bundle is BundleSObjects.InstalledPackageBundleVersion => bundle !== null
77+
);
78+
} catch (err) {
79+
const error =
80+
err instanceof Error ? err : new Error(messages.getMessage('failedToGetInstalledBundles'));
81+
throw SfError.wrap(massageErrorMessage(error));
82+
}
83+
}
84+
85+
/**
86+
* Get the component packages for a specific installed bundle
87+
*
88+
* @param connection - Connection to the target org
89+
* @param installedBundleVersionId - The InstalledPackageBundleVersion ID
90+
* @returns Array of components with expected and actual package versions
91+
*/
92+
private static async getBundleComponents(
93+
connection: Connection,
94+
installedBundleVersionId: string
95+
): Promise<BundleSObjects.InstalledPackageBundleVersionComponent[]> {
96+
// First find the install request that created this installed bundle
97+
const installRequestQuery =
98+
'SELECT Id ' +
99+
'FROM PkgBundleVersionInstallReq ' +
100+
`WHERE InstalledPkgBundleVersionId = '${installedBundleVersionId}' ` +
101+
'ORDER BY CreatedDate DESC LIMIT 1';
102+
103+
const installRequestResult = await connection.autoFetchQuery<BundleSObjects.InstallRequestRecord>(
104+
installRequestQuery,
105+
{
106+
tooling: true,
107+
}
108+
);
109+
110+
if (!installRequestResult.records || installRequestResult.records.length === 0) {
111+
return [];
112+
}
113+
114+
const installRequestId = installRequestResult.records[0].Id;
115+
116+
// Query expected packages from PkgBundleVerCpntIstlReq (abbreviated name)
117+
// These have SubscriberPackageVersion as a foreign key showing what was expected
118+
// Use InstalledComponent to get the actual package name from InstalledSubscriberPackage
119+
const componentQuery =
120+
'SELECT SubscriberPackageVersion.Id, SubscriberPackageVersion.SubscriberPackageId, ' +
121+
'SubscriberPackageVersion.MajorVersion, SubscriberPackageVersion.MinorVersion, ' +
122+
'SubscriberPackageVersion.PatchVersion, SubscriberPackageVersion.BuildNumber, ' +
123+
'InstalledComponent.SubscriberPackage.Name, ' +
124+
'SequenceOrder ' +
125+
'FROM PkgBundleVerCpntIstlReq ' +
126+
`WHERE PkgBundleVersionInstallReqId = '${installRequestId}' ` +
127+
'ORDER BY SequenceOrder';
128+
129+
const componentResult = await connection.autoFetchQuery<BundleSObjects.BundleComponentInstallRecord>(
130+
componentQuery,
131+
{
132+
tooling: true,
133+
}
134+
);
135+
136+
if (!componentResult.records || componentResult.records.length === 0) {
137+
return [];
138+
}
139+
140+
// Query actual installed packages from the org
141+
const installedPackagesQuery =
142+
'SELECT Id, SubscriberPackageId, SubscriberPackage.Name, SubscriberPackageVersion.Id, ' +
143+
'SubscriberPackageVersion.MajorVersion, SubscriberPackageVersion.MinorVersion, ' +
144+
'SubscriberPackageVersion.PatchVersion, SubscriberPackageVersion.BuildNumber ' +
145+
'FROM InstalledSubscriberPackage ' +
146+
'ORDER BY SubscriberPackageId';
147+
148+
const installedResult = await connection.autoFetchQuery<BundleSObjects.InstalledPackageRecord>(
149+
installedPackagesQuery,
150+
{
151+
tooling: true,
152+
}
153+
);
154+
155+
// Create a map of actually installed packages by subscriber package ID
156+
const installedMap = new Map<string, { name: string; version: string }>();
157+
if (installedResult.records) {
158+
installedResult.records.forEach((pkg) => {
159+
const versionNumber = `${pkg.SubscriberPackageVersion.MajorVersion}.${pkg.SubscriberPackageVersion.MinorVersion}.${pkg.SubscriberPackageVersion.PatchVersion}.${pkg.SubscriberPackageVersion.BuildNumber}`;
160+
installedMap.set(pkg.SubscriberPackageId, {
161+
name: pkg.SubscriberPackage.Name,
162+
version: versionNumber,
163+
});
164+
});
165+
}
166+
167+
// Compare expected vs actual
168+
return componentResult.records.map((record) => {
169+
// Expected (what the bundle was supposed to install)
170+
const expectedVersion = `${record.SubscriberPackageVersion.MajorVersion}.${record.SubscriberPackageVersion.MinorVersion}.${record.SubscriberPackageVersion.PatchVersion}.${record.SubscriberPackageVersion.BuildNumber}`;
171+
const expectedName = record.InstalledComponent?.SubscriberPackage?.Name ?? 'Unknown';
172+
const subscriberPackageId = record.SubscriberPackageVersion.SubscriberPackageId;
173+
174+
// Actual (what's currently installed in the org)
175+
const installed = installedMap.get(subscriberPackageId);
176+
177+
return {
178+
ExpectedPackageName: expectedName,
179+
ExpectedPackageVersionNumber: expectedVersion,
180+
ActualPackageName: installed ? installed.name : 'Uninstalled',
181+
ActualPackageVersionNumber: installed ? installed.version : 'N/A',
182+
};
183+
});
184+
}
185+
}

0 commit comments

Comments
 (0)