-
Notifications
You must be signed in to change notification settings - Fork 15
W-17561980 Outputs DOT Code for DisplayDependencies Command #762
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
W-17561980 Outputs DOT Code for DisplayDependencies Command #762
Conversation
|
|
||
| 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 |
There was a problem hiding this comment.
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 = { |
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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".
There was a problem hiding this comment.
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
| // 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; |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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.
| * 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> { |
There was a problem hiding this comment.
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()
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
packagedirectory directly correspond to an sf cli command and followed apackageCommandName.tsnaming 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.DependencyDotProducerandPackageDependencyNode) would add unnecessary files while only being used within thePackageVersionDependency.
TLDR: DependencyDotProducer and PackageDependencyNode are used very closely and only with PackageVersionDependency and should remain together for cleanliness.
| if ( | ||
| !record.Package2?.Name || | ||
| record.MajorVersion === null || | ||
| record.MinorVersion === null || | ||
| record.PatchVersion === null || | ||
| record.BuildNumber === null | ||
| ) { | ||
| throw messages.createError('invalidDependencyGraphError'); | ||
| } |
There was a problem hiding this comment.
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()
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment left behind?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deleted it 😬
| 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, | ||
| }, | ||
| ], |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
| 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, | ||
| }, | ||
| ], | ||
| }); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
…' of https://github.com/forcedotcom/packaging into t/managed-packaging/W-17561980/display-dependency-graph
@W-17561980@
Summary