UNPKG

sfdx-hardis

Version:

Swiss-army-knife Toolbox for Salesforce. Allows you to define a complete CD/CD Pipeline. Orchestrate base commands and assist users with interactive wizards

249 lines (241 loc) • 13.1 kB
/* jscpd:ignore-start */ import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; import { AuthInfo, Connection, Messages, SfError } from '@salesforce/core'; import c from "chalk"; import { makeSureOrgIsConnected, promptOrgList } from '../../../common/utils/orgUtils.js'; import { isCI, uxLog } from '../../../common/utils/index.js'; import { bulkQuery } from '../../../common/utils/apiUtils.js'; import { generateCsvFile, generateReportPath } from '../../../common/utils/filesUtils.js'; import { prompts } from '../../../common/utils/prompts.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('sfdx-hardis', 'org'); export default class MultiOrgQuery extends SfCommand { static title = 'Multiple Orgs SOQL Query'; static description = ` **Executes a SOQL query across multiple Salesforce organizations and consolidates the results into a single report.** This command is highly valuable for administrators and developers who need to gather consistent data from various Salesforce environments (e.g., sandboxes, production orgs) for reporting, auditing, or comparison purposes. It streamlines the process of querying multiple orgs, eliminating the need to log into each one individually. Key functionalities: - **Flexible Query Input:** You can provide a custom SOQL query directly using the \`--query\` flag, or select from a list of predefined query templates (e.g., \`active-users\`, \`all-users\`) using the \`--query-template\` flag. - **Multiple Org Targeting:** Specify a list of Salesforce org usernames or aliases using the \`--target-orgs\` flag. If not provided, an interactive menu will allow you to select multiple authenticated orgs. - **Consolidated Report:** All query results from the different orgs are combined into a single CSV file, making data analysis and comparison straightforward. - **Authentication Handling:** For CI/CD jobs, ensure that the target orgs are already authenticated using Salesforce CLI. In interactive mode, it will prompt for authentication if an org is not connected. **Visual Demo:** [![Use in VS Code SFDX Hardis !](https://github.com/hardisgroupcom/sfdx-hardis/raw/main/docs/assets/images/multi-org-query-demo.gif)](https://marketplace.visualstudio.com/items?itemName=NicolasVuillamy.vscode-sfdx-hardis) <details markdown="1"> <summary>Technical explanations</summary> The command's technical implementation involves: - **Org Authentication and Connection:** It uses \`AuthInfo.create\` and \`Connection.create\` to establish connections to each target Salesforce org. It also leverages \`makeSureOrgIsConnected\` and \`promptOrgList\` for interactive org selection and authentication checks. - **SOQL Query Execution (Bulk API):** It executes the specified SOQL query against each connected org using \`bulkQuery\` for efficient data retrieval, especially for large datasets. - **Data Aggregation:** It collects the records from each org's query result and adds metadata about the source org (instance URL, alias, username) to each record, enabling easy identification of data origin in the consolidated report. - **Report Generation:** It uses \`generateCsvFile\` to create the final CSV report and \`generateReportPath\` to determine the output file location. - **Interactive Prompts:** The \`prompts\` library is used to guide the user through selecting a query template or entering a custom query, and for selecting target orgs if not provided as command-line arguments. - **Error Handling:** It logs errors for any orgs where the query fails, ensuring that the overall process continues and provides a clear summary of successes and failures. </details> `; static examples = [ '$ sf hardis:org:multi-org-query', '$ sf hardis:org:multi-org-query --query "SELECT Id,Username FROM User"', '$ sf hardis:org:multi-org-query --query "SELECT Id,Username FROM User" --target-orgs nico@cloudity.com nico@cloudity.com.preprod nico@cloudity.com.uat', '$ sf hardis:org:multi-org-query --query-template active-users --target-orgs nico@cloudity.com nico@cloudity.com.preprod nico@cloudity.com.uat', ]; static flags = { query: Flags.string({ char: 'q', description: 'SOQL Query to run on multiple orgs', exclusive: ["query-template"] }), "query-template": Flags.string({ char: "t", description: "Use one of predefined SOQL Query templates", options: [ "active-users", "all-users" ], exclusive: ["query"] }), "target-orgs": Flags.string({ char: "x", description: "List of org usernames or aliases.", multiple: true }), outputfile: Flags.string({ char: 'f', description: 'Force the path and name of output report file. Must end with .csv', }), debug: Flags.boolean({ char: 'd', default: false, description: messages.getMessage('debugMode'), }), websocket: Flags.string({ description: messages.getMessage('websocket'), }), skipauth: Flags.boolean({ description: 'Skip authentication check when a default username is required', }), }; allQueryTemplates = { "active-users": { label: "Active users", query: `SELECT Id, LastLoginDate, User.LastName, User.Firstname, Profile.UserLicense.Name, Profile.Name, Username, Profile.UserLicense.LicenseDefinitionKey, IsActive, CreatedDate FROM User WHERE IsActive = true ORDER BY Username ASC`, }, "all-users": { label: "All users (including inactive)", query: `SELECT Id, LastLoginDate, User.LastName, User.Firstname, Profile.UserLicense.Name, Profile.Name, Username, Profile.UserLicense.LicenseDefinitionKey, IsActive, CreatedDate FROM User ORDER BY Username ASC`, } }; query; queryTemplate; targetOrgsIds = []; targetOrgs = []; outputFile; debugMode = false; allRecords = []; successOrgs = []; errorOrgs = []; /* jscpd:ignore-end */ async run() { const { flags } = await this.parse(MultiOrgQuery); this.query = flags.query || null; this.queryTemplate = flags["query-template"] || null; this.targetOrgsIds = flags["target-orgs"] || []; this.outputFile = flags.outputfile || null; this.debugMode = flags.debug || false; // Prompt query if not specified as input argument await this.defineSoqlQuery(); // List org if not sent as input parameter await this.manageSelectOrgs(); // Perform the request on orgs await this.performQueries(); // Display results this.displayResults(); // Generate output CSV & XLS this.outputFile = await generateReportPath('multi-org-query', this.outputFile); const outputFilesRes = await generateCsvFile(this.allRecords, this.outputFile, { fileTitle: 'Multi Orgs Query Results' }); return { allRecords: this.allRecords, successOrgs: this.successOrgs, errorOrgs: this.errorOrgs, csvLogFile: this.outputFile, xlsxLogFile: outputFilesRes.xlsxFile, }; } displayResults() { uxLog("action", this, c.cyan(`Query results from ${this.targetOrgsIds.length} orgs`)); if (this.successOrgs.length > 0) { uxLog("success", this, c.green(`Successfully performed query on ${this.successOrgs.length} orgs`)); for (const org of this.successOrgs) { uxLog("log", this, c.grey(`- ${org.instanceUrl}`)); } } if (this.errorOrgs.length > 0) { uxLog("success", this, c.green(`Error while performing query on ${this.errorOrgs.length} orgs`)); for (const org of this.successOrgs) { uxLog("log", this, c.grey(`- ${org.instanceUrl}: ${org?.error?.message}`)); } } } async performQueries() { for (const orgId of this.targetOrgsIds) { const matchOrgs = this.targetOrgs.filter(org => (org.username === orgId || org.alias === orgId) && org.accessToken); if (matchOrgs.length === 0) { uxLog("warning", this, c.yellow(`Skipped ${orgId}: Unable to find authentication. Run "sf org login web" to authenticate.`)); continue; } const accessToken = matchOrgs[0].accessToken; const username = matchOrgs[0].username; const instanceUrl = matchOrgs[0].instanceUrl; const loginUrl = matchOrgs[0].loginUrl || instanceUrl; uxLog("action", this, c.cyan(`Performing query on ${c.bold(orgId)}...`)); try { const authInfo = await AuthInfo.create({ username: username }); const connectionConfig = { loginUrl: loginUrl, instanceUrl: instanceUrl, accessToken: accessToken }; const conn = await Connection.create({ authInfo: authInfo, connectionOptions: connectionConfig }); const bulkQueryRes = await bulkQuery(this.query, conn, 5); // Add org info to results const records = bulkQueryRes.records.map(record => { record.orgInstanceUrl = matchOrgs[0].instanceUrl; record.orgAlias = matchOrgs[0].alias || ""; record.orgUser = matchOrgs[0].username || ""; return record; }); this.allRecords.push(...records); this.successOrgs.push({ orgId: orgId, instanceUrl: instanceUrl, username: username }); } catch (e) { uxLog("error", this, c.red(`Error while querying ${orgId}: ${e.message}`)); this.errorOrgs.push({ org: orgId, error: e }); } } } async manageSelectOrgs() { if (this.targetOrgsIds.length === 0) { if (isCI) { throw new SfError("You must provide a list of org usernames or aliases in --target-orgs"); } this.targetOrgs = await promptOrgList(); this.targetOrgsIds = this.targetOrgs.map(org => org.alias || org.username); } // Check orgs are connected for (const orgId of this.targetOrgsIds) { const matchOrgs = this.targetOrgs.filter(org => (org.username === orgId || org.alias === orgId) && org.accessToken && org.connectedStatus === 'Connected'); if (matchOrgs.length === 0) { if (isCI) { throw new SfError(`${orgId} must be authenticated using Salesforce CLI before calling this command`); } const orgRes = await makeSureOrgIsConnected(orgId); this.targetOrgs.push(orgRes); } } } async defineSoqlQuery() { // Template is sent as input if (this.queryTemplate) { this.query = this.allQueryTemplates[this.queryTemplate].query; } if (this.query == null) { if (isCI) { throw new SfError("You must provide a valid value in --query or --query-template"); } const baseQueryPromptRes = await prompts({ type: "select", message: "Please select a predefined query, or custom SOQL option", description: "Choose a ready-made SOQL query template or enter your own custom query", placeholder: "Select a query template", choices: [ ...Object.keys(this.allQueryTemplates).map(templateId => { return { title: this.allQueryTemplates[templateId].label, description: this.allQueryTemplates[templateId].query, value: this.allQueryTemplates[templateId].query }; }), { title: "Custom SOQL Query", description: "Enter a custom SOQL query to run", value: "custom" } ] }); if (baseQueryPromptRes.value === "custom") { const queryPromptRes = await prompts({ type: 'text', message: 'Please input the SOQL Query to run in multiple orgs', description: 'Enter a custom SOQL query that will be executed across all selected Salesforce orgs', placeholder: 'Ex: SELECT Id, Name FROM Account LIMIT 10', }); this.query = queryPromptRes.value; } else { this.query = baseQueryPromptRes.value; } } } } //# sourceMappingURL=multi-org-query.js.map