-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Multi-org reports hardis:org:multi-org-query (#858)
New command hardis:org:multi-org-query allowing to execute a SOQL Bulk Query in multiple orgs and aggregate the results in a single CSV / XLS report
- Loading branch information
Showing
3 changed files
with
234 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
/* jscpd:ignore-start */ | ||
import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; | ||
import { AuthInfo, Connection, Messages } from '@salesforce/core'; | ||
import { AnyJson } from '@salesforce/ts-types'; | ||
import c from "chalk"; | ||
import { makeSureOrgIsConnected, promptOrgList } from '../../../common/utils/orgUtils.js'; | ||
import { 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<any> { | ||
public static title = 'Multiple Orgs SOQL Query'; | ||
|
||
public static description = `Executes a SOQL query in multiple orgs and generate a single report from it`; | ||
|
||
public 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 [email protected] [email protected] [email protected]' | ||
]; | ||
|
||
public static flags: any = { | ||
query: Flags.string({ | ||
char: 'q', | ||
description: 'SOQL Query to run on multiple orgs', | ||
}), | ||
"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', | ||
}), | ||
}; | ||
|
||
protected query: string; | ||
protected targetOrgsIds: string[] = []; | ||
protected targetOrgs: any[] = [] | ||
protected outputFile; | ||
protected debugMode = false; | ||
protected allRecords: any[] = []; | ||
protected successOrgs: any[] = []; | ||
protected errorOrgs: any[] = []; | ||
|
||
/* jscpd:ignore-end */ | ||
|
||
public async run(): Promise<AnyJson> { | ||
const { flags } = await this.parse(MultiOrgQuery); | ||
this.query = flags.query || 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); | ||
|
||
return { | ||
allRecords: this.allRecords, | ||
successOrgs: this.successOrgs, | ||
errorOrgs: this.errorOrgs, | ||
csvLogFile: this.outputFile, | ||
xlsxLogFile: outputFilesRes.xlsxFile, | ||
}; | ||
} | ||
|
||
private displayResults() { | ||
if (this.successOrgs.length > 0) { | ||
uxLog(this, c.green(`Successfully performed query on ${this.successOrgs.length} orgs`)); | ||
for (const org of this.successOrgs) { | ||
uxLog(this, c.grey(`- ${org.instanceUrl}`)); | ||
} | ||
} | ||
if (this.errorOrgs.length > 0) { | ||
uxLog(this, c.green(`Error while performing query on ${this.errorOrgs.length} orgs`)); | ||
for (const org of this.successOrgs) { | ||
uxLog(this, c.grey(`- ${org.instanceUrl}: ${org?.error?.message}`)); | ||
} | ||
} | ||
} | ||
|
||
private 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(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(this, c.yellow(`Performing query on ${orgId}...`)); | ||
try { | ||
const authInfo = await AuthInfo.create({ | ||
username: username | ||
}); | ||
const connectionConfig: any = { | ||
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: any) { | ||
uxLog(this, c.red(`Error while querying ${orgId}: ${e.message}`)); | ||
this.errorOrgs.push({ org: orgId, error: e }) | ||
} | ||
|
||
} | ||
} | ||
|
||
private async manageSelectOrgs() { | ||
if (this.targetOrgsIds.length === 0) { | ||
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) { | ||
const orgRes = await makeSureOrgIsConnected(orgId); | ||
this.targetOrgs.push(orgRes); | ||
} | ||
} | ||
} | ||
|
||
private async defineSoqlQuery() { | ||
if (this.query == null) { | ||
const activeUsersQuery = `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`; | ||
const baseQueryPromptRes = await prompts({ | ||
type: "select", | ||
message: "Please select a predefined query, or custom option", | ||
choices: [ | ||
{ | ||
title: "Active users", | ||
description: activeUsersQuery, | ||
value: activeUsersQuery | ||
}, | ||
{ | ||
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', | ||
}); | ||
this.query = queryPromptRes.value; | ||
} | ||
else { | ||
this.query = baseQueryPromptRes.value; | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters