Skip to content

Conversation

@MimiRaiSalesforce
Copy link
Collaborator

@MimiRaiSalesforce MimiRaiSalesforce commented Jul 22, 2025

@W-17561980@

Summary

  • Created nodes and edges according to the DependencyGraphJson. Nodes contain additional information including Package Name, Version Number, 04t ID. Edges contain a 04t source string and 04t target string.
  • Call the produce() function to output nodes and edges in DOT code format and included output according to verbose and edge direction flag


Can't display package dependencies. To display package dependencies, you must first add the calculateTransitiveDependencies parameter to the sfdx-project.json file, and set the value to "true". Next, create a new package version and then run this command using the 04t ID for the new package version.

# invalidDependencyGraphError
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We originally decided on using this error message if the DependencyGraphJson field was null from the Package2VersionCreateRequest table. However, I decided to make this error more general to also occur if values in the json are invalid (ex. cannot find a 04t listed in the JSON and therefore cannot query correct package name, version, etc. information)

version: VersionNumber;
};

export type DependencyGraphEdge = {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided to store the string SubscriberPackageVersionIds (04t...) into the DependencyGraphEdge instead of DependencyGraphNodes because writing the dot code for the edges only requires the 04t... in order to match to the id of a node in the dot code (node_04t...).

* Resolves id input to a 08c. User can input a 08c or 04t.
* Currently a 04t is not supported in filtering the Package2VersionCreateRequest. So a user's input of 04t will be resolved to a 05i and then a 08c.
*/
private async resolvePackageId(): Promise<void> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: rename to resolvePackageCreateRequestId as you want a 08c, not a 0H3 or 033. The latter are what I think of when I hear "package id".

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed to resolvePackageCreateRequestId()
While at it, I also decided to rename the instance variables packageId and resolvedPackageId to userPackageVersionId and resolvedPackageVersionId

Comment on lines 87 to 101
// First find the Package2Version ID (05i)
const query05i = `SELECT Id FROM Package2Version WHERE SubscriberPackageVersionId = '${userPackageId}'`;
const result05i = await this.connection.tooling.query<{ Id: string }>(query05i);
if (result05i.records?.length !== 1) {
throw messages.createError('invalidPackageVersionIdError', [userPackageId]);
}
const package2VersionId = result05i.records[0].Id;

// Finally resolve to the Package2VersionCreateRequest ID (08c)
const query08c = `SELECT Id FROM Package2VersionCreateRequest WHERE Package2VersionId = '${package2VersionId}'`;
const result08c = await this.connection.tooling.query<{ Id: string }>(query08c);
if (result08c.records?.length !== 1) {
throw messages.createError('invalidPackageVersionIdError', [userPackageId]);
}
this.resolvedPackageId = result08c.records[0].Id;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO both of these bits (87-93 and 96-101) should both be extracted into their own functions.

A "Clean Code" paradigm I subscribe to that functions help with self-documenting code in addition to code reuse. Thus, if you ever find yourself doing:

// Comment describing what the code below does
<code>

you should refactor this into:

def wellNamedFunctionDescribingTheCode:
    <code>

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I separated the functions to query05iFrom04t() and query08cFrom05i.

Comment on lines 108 to 115
* Checks that the given Package2VersionCreateRequest ID (08c)
* 1) exists for the given devhub org
* 2) contains the calculateTransitiveDependencies boolean set to true
* 3) has a corresponding DependencyGraphJson
*
* @returns true if DOT code can be generated, false otherwise.
*/
private async validatePackageVersion(): Promise<boolean> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another good place to break up this function into smaller functions, maybe combining the last two into one since they're similar.

E.g., something like:

verifyCreateRequestExistsOnDevHub()
verifyTransitiveDependenciesCalculated()

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created verifyCreateRequesIdExistsOnDevHub() and verifyTransitiveDependenciesCalculated().

}
}

export class DependencyDotProducer {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe split this class into its own file?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided to leave the classes all together for the following reasons:

  • I decided to follow a similar structure to the packageAncestry class which contained multiple classes in the same file.
  • The majority of the files in this package directory directly correspond to an sf cli command and followed a packageCommandName.ts naming structure. The files that deviated from this structure (ex. subscriberPackageVersion.ts, versionNumber.ts) contained classes that were used in multiple other classes. Splitting the other classes in this file (eg. DependencyDotProducer and PackageDependencyNode) would add unnecessary files while only being used within the PackageVersionDependency.

TLDR: DependencyDotProducer and PackageDependencyNode are used very closely and only with PackageVersionDependency and should remain together for cleanliness.

Comment on lines 272 to 280
if (
!record.Package2?.Name ||
record.MajorVersion === null ||
record.MinorVersion === null ||
record.PatchVersion === null ||
record.BuildNumber === null
) {
throw messages.createError('invalidDependencyGraphError');
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a less obvious but still a good place to think of creating a separate function. Something like:

throwErrorOnInvalidRecord()

or

verifyNoFieldsAreNull()

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created the throwErrorOnInvalidRecord() function.

import { PackageVersionDependency, VERSION_BEING_BUILT } from '../../src/package/packageVersionDependency';

Messages.importMessagesDirectory(__dirname);
// const messages = Messages.loadMessages('@salesforce/packaging', 'package_version_dependency');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment left behind?

Copy link
Collaborator Author

@MimiRaiSalesforce MimiRaiSalesforce Jul 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleted it 😬

Comment on lines 110 to 133
const pvd = await PackageVersionDependency.create({
connection: mockConnection,
project: mockProject,
packageId: '08cXXXXXXXXXXXXXXX',
});
try {
await pvd.getDependencyDotProducer();
expect.fail('Expected TransitiveDependenciesRequiredError to be thrown');
} catch (error: unknown) {
expect((error as Error).name).to.equal('TransitiveDependenciesRequiredError');
}
expect(connectionStub.calledOnce).to.be.true;
expect(connectionStub.firstCall.args[0]).to.contain('08cXXXXXXXXXXXXXXX');
expect(connectionStub.firstCall.args[0]).to.contain('Package2VersionCreateRequest');
});

it('should throw an error if transitive dependency json is null', async () => {
connectionStub.onFirstCall().resolves({
records: [
{
CalcTransitiveDependencies: true,
DependencyGraphJson: null,
},
],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These lines of code are identical to the test below it, with the sole exception of:

      expect.fail('Expected TransitiveDependenciesRequiredError to be thrown');

vs

      expect.fail('Expected InvalidDependencyGraphError to be thrown');

There should be some way to remove the duplicate code. Since there's only two cases, a function (w/ a parameter for the error message name) is probably easiest

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference between the two test cases is also that the values connectionStub resolves CalcTransitiveDependencies and DependencyGraphJson to. While I code create an additional function and pass in the parameters of these values and the expected errors, I don't think it would simplify the tests cases that significantly if only 2 tests use this function.

Comment on lines 160 to 216
connectionStub.onCall(0).resolves({
records: [
{
CalcTransitiveDependencies: true,
DependencyGraphJson: dependencyGraphJson,
},
],
});
// Second call: getDependencyDotProducer (same query)
connectionStub.onCall(1).resolves({
records: [
{
CalcTransitiveDependencies: true,
DependencyGraphJson: dependencyGraphJson,
},
],
});
// Third call: Setup mock for VERSION_BEING_BUILT node query
connectionStub.onCall(2).resolves({
records: [
{
Package2: { Name: 'TestPackage1' },
Package2Version: {
SubscriberPackageVersionId: '04tXXXXXXXXXXXXXX3',
MajorVersion: 1,
MinorVersion: 0,
PatchVersion: 0,
BuildNumber: 0,
},
},
],
});
// Fourth call: Setup mocks for regular package version nodes
connectionStub.onCall(3).resolves({
records: [
{
SubscriberPackageVersionId: '04tXXXXXXXXXXXXXX1',
Package2: { Name: 'DependencyPackage1' },
MajorVersion: 2,
MinorVersion: 1,
PatchVersion: 0,
BuildNumber: 0,
},
],
});
connectionStub.onCall(4).resolves({
records: [
{
SubscriberPackageVersionId: '04tXXXXXXXXXXXXXX2',
Package2: { Name: 'DependencyPackage2' },
MajorVersion: 1,
MinorVersion: 2,
PatchVersion: 3,
BuildNumber: 0,
},
],
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't love all this copious boilerplate just to setup a test, especially with it being repeated almost verbatim in the next test.

Trying to think of a more concise way to represent these, what I've currently got is you create a new object to represent this data and then store them in an array, e.g.

[
    new MockedDependency("TestPackage1", "1.0.0.0"),
    new MockedDependency("DependencyPackage1", "2.1.0.0"),
    new MockedDependency("DependencyPackage2", "1.2.3.0")
]

and then you have a helper function/class process these to create the versionIds in order, setup the stubs, and ideally also generate the asserts for you.

This is something Cursor should be really good at. See what I did in Package2DependencyGraphTest as an example, as I had the same issue: lots of boiler plate which a custom test library (written via Gemini) helped me compress.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This helped a lot! There were 3 main resolutions which I simplified to the functions: resolveDependencyGraphJsonCall(), resolveVersionBeingBuiltNodeCall(), and resolveDependencyNodeCall()

Due to the multiple checks to validate the 04t ID, calculateTransitiveDependencies, and DependencyGraphJson, resolveDependencyGraphJsonCall() is called multiple times. The best resolution I could think of to prevent writing the same call multiple times was to pass in an array of callIndices.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants