Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions messages/package.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ Can't retrieve package version metadata. The specified directory must be relativ

Can't retrieve package metadata. To use this feature, you must first assign yourself the DownloadPackageVersionZips user permission. Then retry retrieving your package metadata.

# packageVersionNotFound

Can't retrieve package metadata. We can't find the package version %s. Verify that the 04t ID is correct and that the package version exists.

# packageVersionNotInDevHub

Can't retrieve package metadata. Package version %s isn't accessible from this Dev Hub org. You can only retrieve package metadata from the Dev Hub that created the package version. Verify that you specified the correct target Dev Hub.

# downloadDeveloperPackageZipHasNoDataNative2GP

Can't retrieve package metadata. The developer package zip for this native 2GP package version is unretrievable. To resolve, create a new package version with the --generate-pkg-zip flag. Then retry retrieving your package metadata.
Expand Down
121 changes: 97 additions & 24 deletions src/package/packageVersionRetrieve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ import { Connection, Logger, Messages, SfProject } from '@salesforce/core';
import { ComponentSet, MetadataConverter, ZipTreeContainer } from '@salesforce/source-deploy-retrieve';
import { env } from '@salesforce/kit';
import { PackageDir } from '@salesforce/schemas';
import { PackageVersionMetadataDownloadOptions, PackageVersionMetadataDownloadResult } from '../interfaces';
import {
PackageVersionMetadataDownloadOptions,
PackageVersionMetadataDownloadResult,
PackagingSObjects,
} from '../interfaces';
import { generatePackageAliasEntry, isPackageDirectoryEffectivelyEmpty } from '../utils/packageUtils';
import { createPackageDirEntry } from './packageCreate';
import { Package } from './package';
import { PackageVersion } from './packageVersion';
import { PackageVersion, Package2VersionFieldTypes } from './packageVersion';

Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/packaging', 'package');
Expand Down Expand Up @@ -56,33 +60,16 @@ export async function retrievePackageVersionMetadata(
throw messages.createError('sourcesDownloadDirectoryNotEmpty');
}

// Get the DeveloperUsePkgZip URL from the Package2Version record
// Resolve an alias to the underlying 04t if one was passed.
const subscriberPackageVersionId =
project.getPackageIdFromAlias(options.subscriberPackageVersionId) ?? options.subscriberPackageVersionId;

// Query Package2Version to get the record by SubscriberPackageVersionId
const queryOptions = {
whereClause: `WHERE SubscriberPackageVersionId = '${subscriberPackageVersionId}'`,
};
let versionInfo;
try {
[versionInfo] = await PackageVersion.queryPackage2Version(connection, queryOptions);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
if (msg.includes("No such column 'DeveloperUsePkgZip' on entity 'Package2Version'")) {
throw messages.createError('developerUsePkgZipFieldUnavailable');
}
if (msg.includes("sObject type 'Package2Version' is not supported.")) {
throw messages.createError('packagingNotEnabledOnOrg');
}
throw e;
}
const versionInfo = await resolvePackage2Version(connection, subscriberPackageVersionId);

if (!versionInfo?.DeveloperUsePkgZip) {
throw messages.createError('developerUsePkgZipFieldUnavailable');
}
// The package version exists in this Dev Hub, so now we fetch the download URL.
const developerUsePkgZipUrl = await fetchDeveloperUsePkgZipUrl(connection, subscriberPackageVersionId);

const responseBase64 = await connection.tooling.request<string>(versionInfo.DeveloperUsePkgZip, {
const responseBase64 = await connection.tooling.request<string>(developerUsePkgZipUrl, {
encoding: 'base64',
});
const buffer = Buffer.from(responseBase64, 'base64');
Expand Down Expand Up @@ -232,3 +219,89 @@ async function attemptToUpdateProjectJson(
);
}
}

/**
* Query the Package2Version record for the given 04t, mapping a missing row to the right error: no
* packaging support on the org, not found anywhere, or found but not in this Dev Hub. Selects only
* columns any authenticated user can read, so the query always succeeds; selecting DeveloperUsePkgZip
* here (which requires the DownloadPackageVersionZips permission) would throw "No such column" for a
* user without that permission and mask those cases as a permission problem.
*/
async function resolvePackage2Version(
connection: Connection,
subscriberPackageVersionId: string
): Promise<PackagingSObjects.Package2Version> {
const queryOptions = {
whereClause: `WHERE SubscriberPackageVersionId = '${subscriberPackageVersionId}'`,
fields: ['Package2Id', 'ConvertedFromVersionId'] as Package2VersionFieldTypes,
};
let versionInfo;
try {
[versionInfo] = await PackageVersion.queryPackage2Version(connection, queryOptions);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
if (msg.includes("sObject type 'Package2Version' is not supported.")) {
throw messages.createError('packagingNotEnabledOnOrg');
}
throw e;
}

// No Package2Version row in this Dev Hub. Use SubscriberPackageVersion, a global view any
// authenticated user can query, to tell "doesn't exist anywhere" from "exists but not here".
if (!versionInfo) {
const exists = await subscriberPackageVersionExists(connection, subscriberPackageVersionId);
throw messages.createError(exists ? 'packageVersionNotInDevHub' : 'packageVersionNotFound', [
subscriberPackageVersionId,
]);
}

return versionInfo;
}

/**
* Fetch the DeveloperUsePkgZip download URL for a Package2Version row already confirmed to exist in
* this Dev Hub. Reading this column requires the DownloadPackageVersionZips permission, so once the
* row is known to exist, both a "No such column" error and a null value reliably mean the user lacks
* that permission.
*/
async function fetchDeveloperUsePkgZipUrl(connection: Connection, subscriberPackageVersionId: string): Promise<string> {
const queryOptions = {
whereClause: `WHERE SubscriberPackageVersionId = '${subscriberPackageVersionId}'`,
fields: ['DeveloperUsePkgZip'] as Package2VersionFieldTypes,
};
let versionInfo;
try {
[versionInfo] = await PackageVersion.queryPackage2Version(connection, queryOptions);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
if (msg.includes("No such column 'DeveloperUsePkgZip' on entity 'Package2Version'")) {
throw messages.createError('developerUsePkgZipFieldUnavailable');
}
throw e;
}

if (!versionInfo?.DeveloperUsePkgZip) {
throw messages.createError('developerUsePkgZipFieldUnavailable');
}

return versionInfo.DeveloperUsePkgZip;
}

/**
* True if a SubscriberPackageVersion with this 04t ID exists anywhere. That global view is
* queryable by any authenticated user, so it tells "not found" apart from "not in this Dev Hub".
*/
async function subscriberPackageVersionExists(
connection: Connection,
subscriberPackageVersionId: string
): Promise<boolean> {
try {
const result = await connection.tooling.query<{ Id: string }>(
`SELECT Id FROM SubscriberPackageVersion WHERE Id = '${subscriberPackageVersionId}' LIMIT 1`
);
return (result.records?.length ?? 0) > 0;
} catch {
// If even the existence probe fails (malformed ID, etc.), fall back to "not found".
return false;
}
}
99 changes: 80 additions & 19 deletions test/package/packageVersionMetadataRetrieve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,20 @@ import { Package } from '../../src/package/package';
import { PackageVersion } from '../../src/package/packageVersion';
import { PackageType } from '../../src/interfaces/packagingInterfacesAndType';

// The retrieve flow issues two distinct queryPackage2Version calls that share a whereClause but
// differ by their selected fields: a disambiguation query (non-gated columns) and a separate fetch
// of the permission-gated DeveloperUsePkgZip URL. Match each by its field list.
const disambiguationQuery = (subscriberPackageVersionId: string): sinon.SinonMatcher =>
sinon.match({
whereClause: `WHERE SubscriberPackageVersionId = '${subscriberPackageVersionId}'`,
fields: ['Package2Id', 'ConvertedFromVersionId'],
});
const zipUrlQuery = (subscriberPackageVersionId: string): sinon.SinonMatcher =>
sinon.match({
whereClause: `WHERE SubscriberPackageVersionId = '${subscriberPackageVersionId}'`,
fields: ['DeveloperUsePkgZip'],
});

describe('Package Version Retrieve', () => {
const $$ = instantiateContext();
const testOrg = new MockTestOrgData();
Expand Down Expand Up @@ -111,6 +125,7 @@ describe('Package Version Retrieve', () => {
let queryPackage2VersionStub: sinon.SinonStub;
let requestMetadataZipStub: sinon.SinonStub;
let getPackageDataStub: sinon.SinonStub;
let toolingQueryStub: sinon.SinonStub;

beforeEach(async () => {
$$.inProject(true);
Expand All @@ -133,10 +148,13 @@ describe('Package Version Retrieve', () => {
requestMetadataZipStub.withArgs(metadataZipURL2GP, { encoding: 'base64' }).resolves(secondGenBytesBase64);
requestMetadataZipStub.withArgs(metadataZipURL1GP, { encoding: 'base64' }).resolves(firstGenBytesBase64);

// SubscriberPackageVersion existence probe used to disambiguate retrieve failures.
// Default to "exists" so happy-path tests are unaffected; override per-test as needed.
toolingQueryStub = $$.SANDBOX.stub(connection.tooling, 'query');
toolingQueryStub.resolves({ records: [{ Id: packageVersionId2GP }], done: true, totalSize: 1 });

queryPackage2VersionStub = $$.SANDBOX.stub(PackageVersion, 'queryPackage2Version');
queryPackage2VersionStub
.withArgs(connection, { whereClause: `WHERE SubscriberPackageVersionId = '${packageVersionId2GP}'` })
.resolves([mockPackage2Version]);
queryPackage2VersionStub.resolves([mockPackage2Version]);

$$.SANDBOX.stub(packageUtils, 'generatePackageAliasEntry').resolves([
`${packageName}@0.1.0-1-main`,
Expand Down Expand Up @@ -211,18 +229,49 @@ describe('Package Version Retrieve', () => {
});

it('should not add a packageDirectory entry to sfdx-project.json after retrieving a managed 1GP version', async () => {
// For 1GP packages, queryPackage2Version returns empty array, which means no Package2Version found
queryPackage2VersionStub
.withArgs(connection, { whereClause: `WHERE SubscriberPackageVersionId = '${packageVersionId1GP}'` })
.resolves([]);
// For 1GP packages, queryPackage2Version returns empty array, which means no Package2Version found.
queryPackage2VersionStub.withArgs(connection, disambiguationQuery(packageVersionId1GP)).resolves([]);
// The SubscriberPackageVersion exists globally, so this resolves to "not in this Dev Hub".
toolingQueryStub.resolves({ records: [{ Id: packageVersionId1GP }], done: true, totalSize: 1 });

try {
await Package.downloadPackageVersionMetadata(project, downloadOptions1GP, connection);
assert.fail('Expected test execution to raise an error');
} catch (e) {
const error = e as SfError;
expect(error.message).to.equal(
"Can't retrieve package metadata. To use this feature, you must first assign yourself the DownloadPackageVersionZips user permission. Then retry retrieving your package metadata."
"Can't retrieve package metadata. Package version 04txx00000000001gp isn't accessible from this Dev Hub org. You can only retrieve package metadata from the Dev Hub that created the package version. Verify that you specified the correct target Dev Hub."
);
}
});

it('should throw packageVersionNotFound when no Package2Version row exists and the 04t is unknown', async () => {
queryPackage2VersionStub.withArgs(connection, disambiguationQuery(packageVersionId1GP)).resolves([]);
// No SubscriberPackageVersion either => the 04t doesn't exist anywhere.
toolingQueryStub.resolves({ records: [], done: true, totalSize: 0 });

try {
await Package.downloadPackageVersionMetadata(project, downloadOptions1GP, connection);
assert.fail('Expected test execution to raise an error');
} catch (e) {
const error = e as SfError;
expect(error.message).to.equal(
"Can't retrieve package metadata. We can't find the package version 04txx00000000001gp. Verify that the 04t ID is correct and that the package version exists."
);
}
});

it('should throw packageVersionNotInDevHub when no Package2Version row exists but the SubscriberPackageVersion does', async () => {
queryPackage2VersionStub.withArgs(connection, disambiguationQuery(packageVersionId2GP)).resolves([]);
toolingQueryStub.resolves({ records: [{ Id: packageVersionId2GP }], done: true, totalSize: 1 });

try {
await Package.downloadPackageVersionMetadata(project, downloadOptions2GP, connection);
assert.fail('Expected test execution to raise an error');
} catch (e) {
const error = e as SfError;
expect(error.message).to.equal(
"Can't retrieve package metadata. Package version 04txx00000000002gp isn't accessible from this Dev Hub org. You can only retrieve package metadata from the Dev Hub that created the package version. Verify that you specified the correct target Dev Hub."
);
}
});
Expand Down Expand Up @@ -325,7 +374,7 @@ describe('Package Version Retrieve', () => {
it('should throw the native-2GP "unretrievable dev zip" error when ZipTreeContainer is empty and ConvertedFromVersionId is unset', async () => {
$$.SANDBOX.stub(ZipTreeContainer, 'create').rejects(new Error('data length = 0'));
queryPackage2VersionStub
.withArgs(connection, { whereClause: `WHERE SubscriberPackageVersionId = '${packageVersionId2GP}'` })
.withArgs(connection, disambiguationQuery(packageVersionId2GP))
.resolves([{ ...mockPackage2Version, ConvertedFromVersionId: '' }]);

try {
Expand All @@ -343,7 +392,7 @@ describe('Package Version Retrieve', () => {
it('should throw the converted-2GP "unretrievable dev zip" error when ZipTreeContainer is empty and ConvertedFromVersionId is set', async () => {
$$.SANDBOX.stub(ZipTreeContainer, 'create').rejects(new Error('data length = 0'));
queryPackage2VersionStub
.withArgs(connection, { whereClause: `WHERE SubscriberPackageVersionId = '${packageVersionId2GP}'` })
.withArgs(connection, disambiguationQuery(packageVersionId2GP))
.resolves([{ ...mockPackage2Version, ConvertedFromVersionId: '04txx0000004HwAAAU' }]);

try {
Expand All @@ -358,16 +407,28 @@ describe('Package Version Retrieve', () => {
}
});

it('should fail if the DeveloperUsePkgZip field is inaccessible to the user', async () => {
// Mock Package2Version without DeveloperUsePkgZip field to simulate field access issue
it('should fail if the DeveloperUsePkgZip field value is empty for the user', async () => {
// Row exists (disambiguation succeeds), but the gated URL query comes back empty: no permission.
queryPackage2VersionStub
.withArgs(connection, { whereClause: `WHERE SubscriberPackageVersionId = '${packageVersionId2GP}'` })
.resolves([
{
...mockPackage2Version,
DeveloperUsePkgZip: undefined, // This will cause the error
},
]);
.withArgs(connection, zipUrlQuery(packageVersionId2GP))
.resolves([{ DeveloperUsePkgZip: undefined }]);

try {
await Package.downloadPackageVersionMetadata(project, downloadOptions2GP, connection);
assert.fail('Expected test execution to raise an error');
} catch (e) {
const error = e as SfError;
expect(error.message).to.equal(
"Can't retrieve package metadata. To use this feature, you must first assign yourself the DownloadPackageVersionZips user permission. Then retry retrieving your package metadata."
);
}
});

it('should fail if the DeveloperUsePkgZip column is not selectable for the user (No such column)', async () => {
// Selecting the gated column throws "No such column"; that must still surface the perm message.
queryPackage2VersionStub
.withArgs(connection, zipUrlQuery(packageVersionId2GP))
.rejects(new Error("No such column 'DeveloperUsePkgZip' on entity 'Package2Version'."));

try {
await Package.downloadPackageVersionMetadata(project, downloadOptions2GP, connection);
Expand Down
Loading