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
1 change: 1 addition & 0 deletions src/interfaces/packagingInterfacesAndType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,7 @@ export type PackageVersionDependencyOptions = {
connection: Connection;
verbose?: boolean;
edgeDirection?: 'root-first' | 'root-last';
installationKey?: string;
};

export type DependencyGraphData = {
Expand Down
3 changes: 2 additions & 1 deletion src/package/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,14 +204,15 @@ export class Package {
packageVersionId: string,
project: SfProject | undefined,
connection: Connection,
options?: { verbose?: boolean; edgeDirection?: 'root-first' | 'root-last' }
options?: { verbose?: boolean; edgeDirection?: 'root-first' | 'root-last'; installationKey?: string }
): Promise<PackageVersionDependency> {
return PackageVersionDependency.create({
packageVersionId,
project,
connection,
verbose: options?.verbose ?? false,
edgeDirection: options?.edgeDirection ?? 'root-first',
installationKey: options?.installationKey,
});
}

Expand Down
21 changes: 17 additions & 4 deletions src/package/packageVersionDependency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
DependencyGraphEdge,
DependencyGraphData,
} from '../interfaces';
import { escapeInstallationKey } from '../utils/packageUtils';
import { VersionNumber } from './versionNumber';

Messages.importMessagesDirectory(__dirname);
Expand All @@ -38,6 +39,7 @@ export class PackageVersionDependency extends AsyncCreatable<PackageVersionDepen
private userPackageVersionId: string;
private verbose: boolean;
private edgeDirection: 'root-first' | 'root-last';
private installationKey?: string;
private resolvedPackageVersionCreateRequestId: string;
private allPackageVersionId: string;

Expand All @@ -48,6 +50,7 @@ export class PackageVersionDependency extends AsyncCreatable<PackageVersionDepen
this.userPackageVersionId = options.packageVersionId;
this.verbose = options.verbose ?? false;
this.edgeDirection = options.edgeDirection ?? 'root-first';
this.installationKey = options.installationKey;
this.resolvedPackageVersionCreateRequestId = '';
this.allPackageVersionId = '';
}
Expand Down Expand Up @@ -76,7 +79,8 @@ export class PackageVersionDependency extends AsyncCreatable<PackageVersionDepen
dependencyGraphJson,
this.verbose,
this.edgeDirection,
this.resolvedPackageVersionCreateRequestId
this.resolvedPackageVersionCreateRequestId,
this.installationKey
);
await producer.init();
return producer;
Expand All @@ -101,7 +105,10 @@ export class PackageVersionDependency extends AsyncCreatable<PackageVersionDepen
throw messages.createError('noDependencyGraphJsonMustProvideVersion');
}

const query = `SELECT Dependencies FROM SubscriberPackageVersion WHERE Id = '${this.allPackageVersionId}'`;
let query = `SELECT Dependencies FROM SubscriberPackageVersion WHERE Id = '${this.allPackageVersionId}'`;
if (this.installationKey) {
query += ` AND InstallationKey = '${escapeInstallationKey(this.installationKey)}'`;
}
const result = await this.connection.tooling.query<{
Dependencies: { ids: Array<{ subscriberPackageVersionId: string }> } | null;
}>(query);
Expand Down Expand Up @@ -219,6 +226,7 @@ export class DependencyDotProducer {
private resolvedPackageVersionId: string;
private subscriberPackageVersionId: string;
private connection: Connection;
private installationKey?: string;
private dependencyGraphData!: DependencyGraphData;
private selectedNodeIds: string[] = [];

Expand All @@ -227,13 +235,15 @@ export class DependencyDotProducer {
dependencyGraphString: string,
verbose: boolean,
edgeDirection: 'root-first' | 'root-last',
resolvedPackageVersionId: string
resolvedPackageVersionId: string,
installationKey?: string
) {
this.verbose = verbose;
this.edgeDirection = edgeDirection;
this.resolvedPackageVersionId = resolvedPackageVersionId;
this.connection = connection;
this.dependencyGraphString = dependencyGraphString;
this.installationKey = installationKey;
this.subscriberPackageVersionId = VERSION_BEING_BUILT;
}

Expand Down Expand Up @@ -276,7 +286,10 @@ export class DependencyDotProducer {
selectedNodes.push(this.subscriberPackageVersionId);
} else if (this.subscriberPackageVersionId.startsWith('04t')) {
selectedNodes.push(this.subscriberPackageVersionId);
const query = `SELECT Dependencies FROM SubscriberPackageVersion WHERE Id = '${this.subscriberPackageVersionId}'`;
let query = `SELECT Dependencies FROM SubscriberPackageVersion WHERE Id = '${this.subscriberPackageVersionId}'`;
if (this.installationKey) {
query += ` AND InstallationKey = '${escapeInstallationKey(this.installationKey)}'`;
}
try {
const result = await this.connection.tooling.query<{
Dependencies: { ids: Array<{ subscriberPackageVersionId: string }> } | null;
Expand Down
106 changes: 106 additions & 0 deletions test/package/packageVersionDependencies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,112 @@ describe('Package Version Dependencies', () => {
expect(connectionStub.firstCall.args[0]).to.contain('04tXXXXXXXXXXXXXX0');
expect(connectionStub.firstCall.args[0]).to.contain('Package2Version');
});

it('should include installation key in SubscriberPackageVersion query for flat dependency graph', async () => {
// Use the same pattern as the existing flat graph test above
// Calls 0,1,2,3: resolve 04t and check CalcTransitiveDependencies (reused for 05i/08c resolution)
resolveDependencyGraphJsonCall(connectionStub, [0, 1, 2, 3], false, null);
// Call 4: createFlatDependencyGraph - SubscriberPackageVersion query (key should be appended)
connectionStub.onCall(4).resolves({
records: [{ Dependencies: { ids: [{ subscriberPackageVersionId: '04tXXXXXXXXXXXXXX1' }] } }],
});
// Calls 5,6: node resolution (Package2Version queries for each node in flat graph)
resolveDependencyNodeCall(connectionStub, 5, '04tXXXXXXXXXXXXXX0', 'ProtectedPkg', 1, 0, 0, 1);
resolveDependencyNodeCall(connectionStub, 6, '04tXXXXXXXXXXXXXX1', 'DependencyPkg', 2, 0, 0, 1);

const pvd = await PackageVersionDependency.create({
connection: mockConnection,
project: mockProject,
packageVersionId: '04tXXXXXXXXXXXXXX0',
installationKey: 'mySecretKey123',
});

await pvd.getDependencyDotProducer();

// Call 4: createFlatDependencyGraph query should contain the installation key
const flatDependencyQuery = connectionStub.getCall(4).args[0] as string;
expect(flatDependencyQuery).to.contain('SubscriberPackageVersion');
expect(flatDependencyQuery).to.contain("InstallationKey = 'mySecretKey123'");
});

it('should not include installation key in queries when no key is provided', async () => {
resolveDependencyGraphJsonCall(connectionStub, [0, 1, 2, 3], false, null);
// Call 4: createFlatDependencyGraph
connectionStub.onCall(4).resolves({
records: [{ Dependencies: { ids: [{ subscriberPackageVersionId: '04tXXXXXXXXXXXXXX1' }] } }],
});
// Calls 5,6: node resolution
resolveDependencyNodeCall(connectionStub, 5, '04tXXXXXXXXXXXXXX0', 'UnprotectedPkg', 1, 0, 0, 1);
resolveDependencyNodeCall(connectionStub, 6, '04tXXXXXXXXXXXXXX1', 'DependencyPkg', 2, 0, 0, 1);

const pvd = await PackageVersionDependency.create({
connection: mockConnection,
project: mockProject,
packageVersionId: '04tXXXXXXXXXXXXXX0',
});

await pvd.getDependencyDotProducer();

// Call 4: createFlatDependencyGraph query should NOT contain InstallationKey
const flatDependencyQuery = connectionStub.getCall(4).args[0] as string;
expect(flatDependencyQuery).to.contain('SubscriberPackageVersion');
expect(flatDependencyQuery).to.not.contain('InstallationKey');
});

it('should escape special characters in installation key', async () => {
resolveDependencyGraphJsonCall(connectionStub, [0, 1, 2, 3], false, null);
// Call 4: createFlatDependencyGraph
connectionStub.onCall(4).resolves({
records: [{ Dependencies: { ids: [{ subscriberPackageVersionId: '04tXXXXXXXXXXXXXX1' }] } }],
});
// Calls 5,6: node resolution
resolveDependencyNodeCall(connectionStub, 5, '04tXXXXXXXXXXXXXX0', 'ProtectedPkg', 1, 0, 0, 1);
resolveDependencyNodeCall(connectionStub, 6, '04tXXXXXXXXXXXXXX1', 'DependencyPkg', 2, 0, 0, 1);

const pvd = await PackageVersionDependency.create({
connection: mockConnection,
project: mockProject,
packageVersionId: '04tXXXXXXXXXXXXXX0',
installationKey: "key'with\\special",
});

await pvd.getDependencyDotProducer();

// The key should be escaped: single quotes and backslashes
const flatDependencyQuery = connectionStub.getCall(4).args[0] as string;
expect(flatDependencyQuery).to.contain("InstallationKey = 'key\\'with\\\\special'");
});

it('should include installation key in addSelectedNodeIds query for transitive dependency graph', async () => {
const dependencyGraphJson = JSON.stringify({
creator: 'test-creator',
nodes: [{ id: VERSION_BEING_BUILT }, { id: '04tXXXXXXXXXXXXXX1' }],
edges: [{ source: '04tXXXXXXXXXXXXXX1', target: VERSION_BEING_BUILT }],
});
// Calls 0,1,2: resolve 08c and get transitive dependency graph
resolveDependencyGraphJsonCall(connectionStub, [0, 1, 2], true, dependencyGraphJson);
// Call 3: createVersionBeingBuiltNode (resolves VERSION_BEING_BUILT to 04tXXXXXXXXXXXXXX3)
resolveVersionBeingBuiltNodeCall(connectionStub, 3, '04tXXXXXXXXXXXXXX3', 'ProtectedPkg', 1, 0, 0, 0);
// Call 4: resolveDependencyNode for 04tXXXXXXXXXXXXXX1
resolveDependencyNodeCall(connectionStub, 4, '04tXXXXXXXXXXXXXX1', 'DependencyPkg', 2, 0, 0, 1);
// Call 5: addSelectedNodeIds - SubscriberPackageVersion query (subscriberPackageVersionId is now 04tXXXXXXXXXXXXXX3)
resolveSelectedNodeIdsCall(connectionStub, 5, ['04tXXXXXXXXXXXXXX1']);

const pvd = await PackageVersionDependency.create({
connection: mockConnection,
project: mockProject,
packageVersionId: '08cXXXXXXXXXXXXXXX',
installationKey: 'transitiveKey456',
});

await pvd.getDependencyDotProducer();

// Call 5: addSelectedNodeIds query should contain the installation key
const addSelectedNodeIdsQuery = connectionStub.getCall(5).args[0] as string;
expect(addSelectedNodeIdsQuery).to.contain('SubscriberPackageVersion');
expect(addSelectedNodeIdsQuery).to.contain('04tXXXXXXXXXXXXXX3');
expect(addSelectedNodeIdsQuery).to.contain("InstallationKey = 'transitiveKey456'");
});
});

// package version is validated multiple times and requires multiple identical resolves
Expand Down
Loading