diff --git a/src/interfaces/packagingInterfacesAndType.ts b/src/interfaces/packagingInterfacesAndType.ts index 87242497e..e67acf16e 100644 --- a/src/interfaces/packagingInterfacesAndType.ts +++ b/src/interfaces/packagingInterfacesAndType.ts @@ -520,6 +520,7 @@ export type PackageVersionDependencyOptions = { connection: Connection; verbose?: boolean; edgeDirection?: 'root-first' | 'root-last'; + installationKey?: string; }; export type DependencyGraphData = { diff --git a/src/package/package.ts b/src/package/package.ts index 38a2577bf..5963dbcc1 100644 --- a/src/package/package.ts +++ b/src/package/package.ts @@ -204,7 +204,7 @@ 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 { return PackageVersionDependency.create({ packageVersionId, @@ -212,6 +212,7 @@ export class Package { connection, verbose: options?.verbose ?? false, edgeDirection: options?.edgeDirection ?? 'root-first', + installationKey: options?.installationKey, }); } diff --git a/src/package/packageVersionDependency.ts b/src/package/packageVersionDependency.ts index 8638e5663..6dd003298 100644 --- a/src/package/packageVersionDependency.ts +++ b/src/package/packageVersionDependency.ts @@ -23,6 +23,7 @@ import { DependencyGraphEdge, DependencyGraphData, } from '../interfaces'; +import { escapeInstallationKey } from '../utils/packageUtils'; import { VersionNumber } from './versionNumber'; Messages.importMessagesDirectory(__dirname); @@ -38,6 +39,7 @@ export class PackageVersionDependency extends AsyncCreatable } | null; }>(query); @@ -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[] = []; @@ -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; } @@ -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; diff --git a/test/package/packageVersionDependencies.test.ts b/test/package/packageVersionDependencies.test.ts index b322b6396..1b5d119d4 100644 --- a/test/package/packageVersionDependencies.test.ts +++ b/test/package/packageVersionDependencies.test.ts @@ -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