salesforce-alm
Version:
This package contains tools, and APIs, for an improved salesforce.com developer experience.
438 lines (436 loc) • 21.2 kB
JavaScript
;
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.TreeNode = exports.PackageVersionDisplayAncestryCommand = void 0;
const fs = require("fs");
const command_1 = require("@salesforce/command");
const core_1 = require("@salesforce/core");
const ConfigApi = require("../../../../lib/core/configApi");
const consts = require("../../../../lib/core/constants");
const pkgUtils = require("../../../../lib/package/packageUtils");
// Import i18n messages
const Messages = require("../../../../lib/messages");
const messages = Messages();
class PackageVersionDisplayAncestryCommand extends command_1.SfdxCommand {
constructor() {
super(...arguments);
// Add this to query calls to only show released package versions in the output
this.releasedOnlyFilter = ' AND IsReleased = true';
}
async run() {
const org = this.org || this.hubOrg;
const username = org.getUsername();
return await this._findAncestry(username, this.flags);
}
/**
* Finds the ancestry of the given package.
* <p>This was separated out from the run() method so that unit testing could actually be done. This is admittedly a bit of a hack, but I think every other command does it too?
* // TODO: Maybe some CLI whiz could make this not be needed
* </p>
*
* @param username - username of the org
* @param flags - the flags passed in
* @private
*/
// eslint-disable-next-line @typescript-eslint/no-shadow
async _findAncestry(username, flags) {
this.logger = await core_1.Logger.child(this.constructor.name);
this.logger.debug('Ancestry started with args %s', flags);
this.flags = flags; // Needed incase we're running from a unit test.
const connection = await core_1.Connection.create({
authInfo: await core_1.AuthInfo.create({ username }),
});
// Connection.create() defaults to the latest API version, but the user can override it with this flag.
if (flags.apiversion != undefined) {
connection.setApiVersion(flags.apiversion);
}
let dotcodeOutput = 'strict graph G {\n';
let unicodeOutput = '';
const forest = [];
const packageId = flags.package;
const roots = [];
// Get the roots based on what packageId is.
switch (packageId.substr(0, 3)) {
// If this an 0Ho, we need to get all the roots of this package
case '0Ho':
// Validate, and then fetch
try {
pkgUtils.validateId(pkgUtils.BY_LABEL.PACKAGE_ID, packageId);
}
catch (err) {
throw new Error(messages.getMessage('invalidId', packageId, 'package_displayancestry'));
}
// Check to see if the package is an unlocked package
// if so, throw and error since ancestry only applies to managed packages
const query = PackageVersionDisplayAncestryCommand.SELECT_PACKAGE_CONTAINER_OPTIONS + ` WHERE Id = '${packageId}'`;
const packageTypeResults = await this.executeQuery(connection, query);
if (packageTypeResults && packageTypeResults.length === 0) {
throw new Error(messages.getMessage('invalidId', packageId, 'package_displayancestry'));
}
else if (packageTypeResults &&
packageTypeResults.length === 1 &&
packageTypeResults[0]['ContainerOptions'] !== 'Managed') {
throw new Error(messages.getMessage('unlockedPackageError', [], 'package_displayancestry'));
}
const normalQuery = PackageVersionDisplayAncestryCommand.SELECT_ALL_ROOTS +
` WHERE AncestorId = NULL AND Package2Id = '${packageId}' ${this.releasedOnlyFilter}`;
const results = await this.executeQuery(connection, normalQuery);
// The package exists, but there are no versions for the provided package
if (results.length == 0) {
throw new Error(messages.getMessage('noVersionsError', [], 'package_displayancestry'));
}
results.forEach((row) => roots.push(row.SubscriberPackageVersionId));
break;
// If this is an 04t, we were already given our root id, and we also want to go up
case '04t':
// Validate id
try {
pkgUtils.validateId(pkgUtils.BY_LABEL.SUBSCRIBER_PACKAGE_VERSION_ID, packageId);
}
catch (err) {
throw new Error(messages.getMessage('invalidId', packageId, 'package_displayancestry'));
}
// Check to see if the package version is part of an unlocked package
// if so, throw and error since ancestry only applies to managed packages
const versionQuery = PackageVersionDisplayAncestryCommand.SELECT_PACKAGE_VERSION_CONTAINER_OPTIONS + ` WHERE Id = '${packageId}'`;
const packageVersionTypeResults = await this.executeQuery(connection, versionQuery);
if (packageVersionTypeResults &&
packageVersionTypeResults.length === 1 &&
packageVersionTypeResults[0]['Package2ContainerOptions'] !== 'Managed') {
throw new Error(messages.getMessage('unlockedPackageError', [], 'package_displayancestry'));
}
// since this is a package version, we don't want to filter on only released package versions
this.releasedOnlyFilter = '';
roots.push(packageId);
unicodeOutput += (await this.ancestorsFromLeaf(flags.package, connection)) + '\n\n';
break;
// Else, this is likely an alias. So attempt to find the package information from the alias.
default:
let id;
const workspaceConfigFilename = new ConfigApi.Config().getWorkspaceConfigFilename();
try {
const parseConfigFile = JSON.parse(fs.readFileSync(workspaceConfigFilename, 'utf8'));
id = parseConfigFile.packageAliases[packageId];
}
catch (err) {
throw new Error(messages.getMessage('parseError', [], 'package_displayancestry'));
}
if (id === undefined) {
throw new Error(messages.getMessage('invalidAlias', packageId, 'package_displayancestry'));
}
this.debug(`Matched ${packageId} to ${id}`);
// If we have the alias, re-run this function with the new ID, so that it can hit the two cases above
flags.package = id;
return this._findAncestry(username, flags);
}
// For every root node, build the tree below it.
for (const rootId of roots) {
const { root, dotOutput } = await this.exploreTreeFromRoot(connection, rootId);
forest.push(root);
dotcodeOutput += dotOutput;
}
dotcodeOutput += '}\n';
// Determine proper output based on flags
if (!flags.json) {
if (flags.dotcode) {
this.ux.log(dotcodeOutput);
return dotcodeOutput;
}
else {
unicodeOutput += this.createUnicodeTreeOutput(forest);
this.ux.log(unicodeOutput);
return unicodeOutput;
}
}
else {
if (flags.dotcode) {
// if they ask for *both* dotcode *and* json, give them the compiled string.
return dotcodeOutput;
}
else {
return forest;
}
}
}
/**
* Builds the bottom-up view from a leaf.
*
* @param nodeId - the 04t of this node
* @param connection - the connection object
*/
async ancestorsFromLeaf(nodeId, connection) {
let output = '';
// Start with the node, and shoot up
while (nodeId != null) {
const query = `${PackageVersionDisplayAncestryCommand.SELECT_PARENT_INFO} WHERE SubscriberPackageVersionId = '${nodeId}' ${this.releasedOnlyFilter}`;
const results = await this.executeQuery(connection, query);
if (results.length == 0) {
throw new Error(messages.getMessage('versionNotFound', nodeId, 'package_displayancestry'));
}
// @ts-ignore - ignoring this error, since results is guaranteed at runtime to have Major/Minor/etc, but TS doesn't know this at compile time.
const node = new TreeNode({ ...results[0], depthCounter: 0, SubscriberPackageVersionId: nodeId });
output += `${PackageVersionDisplayAncestryCommand.buildVersionOutput(node)} -> `;
nodeId = results[0].AncestorId;
}
// remove the last " -> " from the output string
output = output.substr(0, output.length - 4);
output += ' (root)';
return output;
}
/**
* Makes this tree from starting root this is so that we can be given a package Id and then create the forest of versions
*
* @param connection
* @param rootId the subscriber package version id for this root version.
*/
async exploreTreeFromRoot(connection, rootId) {
// Before we do anything, we need *all* the package information for this node, and they just gave us the ID
const query = PackageVersionDisplayAncestryCommand.SELECT_ROOT_INFO +
` WHERE SubscriberPackageVersionId = '${rootId}' ${this.releasedOnlyFilter}`;
const results = await this.executeQuery(connection, query);
const rootInfo = new PackageInformation(rootId, results[0].MajorVersion, results[0].MinorVersion, results[0].PatchVersion, results[0].BuildNumber);
// Setup our BFS
const visitedSet = new Set(); // If this is *always* a tree, not needed. But if there's somehow a cycle (a dev screwed something up, maybe?), this will prevent an infinite loop.
// eslint-disable-next-line no-array-constructor
const dfsStack = new Array();
const root = new TreeNode(rootInfo);
dfsStack.push(root);
// Traverse!
let dotOutput = PackageVersionDisplayAncestryCommand.buildDotNode(root);
while (dfsStack.length > 0) {
const currentNode = dfsStack.pop(); // DFS
// Skip already visited elements
if (visitedSet.has(currentNode.data.SubscriberPackageVersionId)) {
continue;
}
visitedSet.add(currentNode.data.SubscriberPackageVersionId);
// Find all children, ordered from smallest -> largest
// eslint-disable-next-line @typescript-eslint/no-shadow
const query = PackageVersionDisplayAncestryCommand.SELECT_CHILD_INFO +
` WHERE AncestorId = '${currentNode.data.SubscriberPackageVersionId}' ${this.releasedOnlyFilter}
ORDER BY MajorVersion ASC, MinorVersion ASC, PatchVersion ASC`;
// eslint-disable-next-line @typescript-eslint/no-shadow
const results = await this.executeQuery(connection, query);
// We want to print in-order, but add to our stack in reverse-order so that we both print *and* visit nodes
// left -> right, as the dfaStack will visit right -> left if we don't do this.
const reversalStack = [];
// eslint-disable-next-line no-loop-func
results.forEach((row) => {
const childPackageInfo = new PackageInformation(row.SubscriberPackageVersionId, row.MajorVersion, row.MinorVersion, row.PatchVersion, row.BuildNumber, currentNode.data.depthCounter + 1);
const childNode = new TreeNode(childPackageInfo);
currentNode.addChild(childNode);
reversalStack.push(childNode);
dotOutput += PackageVersionDisplayAncestryCommand.buildDotNode(childNode);
dotOutput += PackageVersionDisplayAncestryCommand.buildDotEdge(currentNode, childNode);
});
// Important to reverse, so that we visit the children left -> right, not right -> left.
reversalStack.reverse().forEach((child) => dfsStack.push(child));
}
return { root, dotOutput };
}
/**
* Creates the fancy NPM-LS unicode tree output
* Idea from: https://github.com/substack/node-archy
*
* @param forest
*/
createUnicodeTreeOutput(forest) {
let result = '';
// DFS from each root
for (const root of forest) {
result += this.unicodeOutputTraversal(root, null, '');
}
return result;
}
/**
* Builds the unicode output of this tree.
* <p>
* Root is handled differently, to make it look better / stand out as the root.
* This complicates the code flow somewhat.
* </p>
*
* @param node - current node
* @param parent - the parent of the current node
* @param prefix - the current prefix, so that we 'indent' far enough
*/
unicodeOutputTraversal(node, parent, prefix) {
let newPrefix = prefix;
let result = '';
// Root is special, just a line with no up-arrow part.
if (parent === null) {
newPrefix = '─';
}
else {
// If we're the last child, use └ instead of ├
if (parent.children.indexOf(node) == parent.children.length - 1) {
newPrefix += '└─';
}
else {
newPrefix += '├─';
}
}
// If we have children, add a ┬, else it's just a ─
if (node.children.length > 0) {
newPrefix += '┬ ';
}
else {
newPrefix += '─ ';
}
// This line is whatever the prefix is, followed by the number
result += newPrefix + `${PackageVersionDisplayAncestryCommand.buildVersionOutput(node)}`;
// Add 04t to output if verbose mode
if (this.flags.verbose) {
result += ` (${node.data.SubscriberPackageVersionId})`;
}
result += '\n';
// If we have children, indent a level and go in
if (node.children.length != 0) {
// Root is special (a single space)
if (parent === null) {
prefix += ' ';
}
// if we're the last child, no vertical lines, just spaces
else if (parent.children[parent.children.length - 1] === node) {
prefix += ' ';
}
// lastly is everyone else (the majority of the cases).
else {
prefix += '│ ';
}
for (const child of node.children) {
result += this.unicodeOutputTraversal(child, node, prefix);
}
}
return result;
}
/**
* Builds a node line in DOT, of the form nodeID [label="MAJOR.MINOR.PATCH"]
*
* @param currentNode
*/
static buildDotNode(currentNode) {
return `\t node${currentNode.data.SubscriberPackageVersionId} [label="${PackageVersionDisplayAncestryCommand.buildVersionOutput(currentNode)}"]\n`;
}
/**
* Builds an edge line in DOT, of the form fromNode -- toNode
*
* @param fromNode
* @param toNode
*/
static buildDotEdge(fromNode, toNode) {
return `\t node${fromNode.data.SubscriberPackageVersionId} -- node${toNode.data.SubscriberPackageVersionId}\n`;
}
/**
* Runs a single query, and returns a promise with the results
*
* @param connection
* @param query
*/
executeQuery(connection, query) {
return connection.tooling
.autoFetchQuery(query)
.then((queryResult) => {
const records = queryResult.records;
const results = []; // Array of objects.
this.logger.debug('Query results: ');
this.logger.debug(records);
// Halt here if we have nothing to return
if (!records || records.length <= 0) {
return results;
}
records.forEach((record) => {
// This seems like a hack, but TypeScript is cool so we use it. Since we (usually) want
// almost *everything* from this record, rather than specifying everything we *do* want,
// instead specify only the things we do *NOT* want, and use `propertiesWeWant`
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { attributes, ...propertiesWeWant } = record;
results.push(propertiesWeWant);
});
this.logger.debug('Parsed results: ');
this.logger.debug(results);
return results;
})
.catch(() => {
throw new Error(messages.getMessage('invalidId', '', 'package_displayancestry'));
});
}
/**
* Building {Major}.{Minor}.{Patch}.{BuildNumber} is done in many places, so centralize.
*
* @param node
*/
static buildVersionOutput(node) {
return `${node.data.MajorVersion}.${node.data.MinorVersion}.${node.data.PatchVersion}.${node.data.BuildNumber}`;
}
}
exports.PackageVersionDisplayAncestryCommand = PackageVersionDisplayAncestryCommand;
PackageVersionDisplayAncestryCommand.description = messages.getMessage('cliDescription', [], 'package_displayancestry');
PackageVersionDisplayAncestryCommand.longDescription = messages.getMessage('cliDescriptionLong', [], 'package_displayancestry');
PackageVersionDisplayAncestryCommand.help = messages.getMessage('help', [], 'package_displayancestry');
PackageVersionDisplayAncestryCommand.showProgress = false;
PackageVersionDisplayAncestryCommand.varargs = false;
PackageVersionDisplayAncestryCommand.orgType = consts.DEFAULT_DEV_HUB_USERNAME;
PackageVersionDisplayAncestryCommand.requiresDevhubUsername = true;
// The first chunk of the query is what makes them unique, and the unit tests rely on these, so making them const
// and public will allow for normalization
PackageVersionDisplayAncestryCommand.SELECT_ALL_ROOTS = 'SELECT SubscriberPackageVersionId FROM Package2Version';
PackageVersionDisplayAncestryCommand.SELECT_ROOT_INFO = 'SELECT MajorVersion, MinorVersion, PatchVersion, BuildNumber FROM Package2Version';
PackageVersionDisplayAncestryCommand.SELECT_CHILD_INFO = 'SELECT SubscriberPackageVersionId, MajorVersion, MinorVersion, PatchVersion, BuildNumber FROM Package2Version';
PackageVersionDisplayAncestryCommand.SELECT_PARENT_INFO = 'SELECT AncestorId, MajorVersion, MinorVersion, PatchVersion, BuildNumber FROM Package2Version';
PackageVersionDisplayAncestryCommand.SELECT_PACKAGE_CONTAINER_OPTIONS = 'SELECT ContainerOptions FROM Package2';
PackageVersionDisplayAncestryCommand.SELECT_PACKAGE_VERSION_CONTAINER_OPTIONS = 'SELECT Package2ContainerOptions FROM SubscriberPackageVersion';
// Parse flags
PackageVersionDisplayAncestryCommand.flagsConfig = {
// --json is configured automatically
package: command_1.flags.string({
char: 'p',
description: messages.getMessage('package', [], 'package_displayancestry'),
longDescription: messages.getMessage('packageLong', [], 'package_displayancestry'),
required: true,
}),
dotcode: command_1.flags.boolean({
description: messages.getMessage('dotcode', [], 'package_displayancestry'),
longDescription: messages.getMessage('dotcodeLong', [], 'package_displayancestry'),
}),
verbose: command_1.flags.builtin({
description: messages.getMessage('verbose', [], 'package_displayancestry'),
longDescription: messages.getMessage('verboseLong', [], 'package_displayancestry'),
}),
};
/**
* A treenode used to create the package version history for the JSON output.
*/
class TreeNode {
constructor(data) {
this.data = data;
this.children = [];
}
/**
* Adds a child to this node
*
* @param child
*/
addChild(child) {
this.children.push(child);
}
}
exports.TreeNode = TreeNode;
/**
* This is the 'data' part of TreeNode, a collection of useful version information.
*/
class PackageInformation {
constructor(SubscriberPackageVersionId, MajorVersion, MinorVersion, PatchVersion, BuildNumber, depthCounter = 0) {
this.SubscriberPackageVersionId = SubscriberPackageVersionId;
this.MajorVersion = MajorVersion;
this.MinorVersion = MinorVersion;
this.PatchVersion = PatchVersion;
this.BuildNumber = BuildNumber;
this.depthCounter = depthCounter;
}
}
//# sourceMappingURL=displayancestry.js.map