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
605 lines (598 loc) • 31.6 kB
JavaScript
// QUESTION: Where are the objects extracted in this code?
// ANSWER: The objects are extracted in the generateObjectsList function.
// This function calls conn.describeGlobal() to get all SObjects, then queries each one to check if it has records.
// The user is then prompted to select which objects to extract. The selected objects are returned and used in the rest of the extraction process.
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import { prompts } from '../../../../common/utils/prompts.js';
import { uxLog } from '../../../../common/utils/index.js';
import c from 'chalk';
import { generateCsvFile, generateReportPath, createXlsxFromCsvFiles } from '../../../../common/utils/filesUtils.js';
import { bulkQuery } from '../../../../common/utils/apiUtils.js';
import * as path from 'path';
import { getReportDirectory } from '../../../../config/index.js';
import { Messages } from '@salesforce/core';
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('sfdx-hardis', 'org');
// Main command class for extracting Salesforce org profiles and related metadata
export default class ProfilesExtract extends SfCommand {
static description = `
## Command Behavior
**Guides administrators through extracting Salesforce profiles, personas, and related metadata into structured CSV/XLSX deliverables.**
The command inventories SObjects that contain data, lets the user pick the ones to document, and then produces persona-centric spreadsheets that cover users, personas, relationships, record types, apps, permissions, tabs, fields, and permission sets. The output consolidates everything into both CSV files and a single Excel workbook, making it easy to audit access models or prepare remediation plans.
Key capabilities:
- **Interactive object discovery:** Lists queryable objects with records and allows multi-selection.
- **Persona modeling:** Lets users define the number of personas and generates cross-object matrices that leverage Excel formulas for faster updates.
- **Comprehensive metadata export:** Captures users, record types, apps, permissions, tabs, fields, and permission sets with persona/profile visibility indicators.
- **Profile field access coverage:** Retrieves FieldPermissions to surface read/edit status per profile and field.
- **Consolidated reporting:** Produces standalone CSVs plus an aggregated XLSX stored in the report directory.
<details markdown="1">
<summary>Technical explanations</summary>
- **Salesforce connectivity:** Uses the requested target org connection from \`Flags.requiredOrg\` to fetch metadata and records.
- **Bulk/REST queries:** Relies on \`bulkQuery\` and standard SOQL to evaluate record counts and pull FieldPermissions, Users, RecordTypes, Applications, Tabs, and PermissionSets.
- **Describe calls:** Invokes \`describeGlobal\` and \`describeSObject\` to enumerate objects and field-level metadata, including picklists and formulas.
- **Prompt-driven input:** Utilizes the shared \`prompts\` utility to collect object selections and persona counts, ensuring consistent CLI UX.
- **Reporting pipeline:** Writes intermediate CSV files via \`generateCsvFile\`, stores them under the report directory from \`getReportDirectory\`, and finally merges them using \`createXlsxFromCsvFiles\`.
- **Logging & diagnostics:** Uses \`uxLog\` with chalk coloring for progress, warnings, and debug output, integrating with the project-wide logging style.
</details>
`;
static examples = [
`$ sf hardis:project:clean:profiles-extract`,
`$ sf hardis:project:clean:profiles-extract --target-org my-org`,
];
/* jscpd:ignore-start */
static flags = {
'target-org': Flags.requiredOrg({
char: 'o',
description: 'The target Salesforce org to fetch SObjects from.',
}),
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',
}),
};
/* jscpd:ignore-end */
csvFiles = [];
outputFile = '';
activeProfileNames = new Set();
/**
* Main entry point for the command. Orchestrates the extraction process:
* - Prompts user to select objects
* - Extracts users, personas, relations, record types, apps, permissions, tabs, and object fields
* - Generates CSV and XLSX reports
*/
async run() {
const { flags } = await this.parse(ProfilesExtract);
const conn = flags['target-org'].getConnection();
let selectedObjects = [];
let numberOfPersonas = 1;
try {
selectedObjects = await this.generateObjectsList(conn);
uxLog("action", this, c.green('Handling extract of Users...'));
await this.generateUsersExtract(conn);
numberOfPersonas = await this.generatePersonaExtract();
uxLog("action", this, c.green(`Generating extracts for ${numberOfPersonas} personas...`));
await this.generateRelationExtract(selectedObjects, numberOfPersonas);
await this.generateRTExtract(conn, selectedObjects, numberOfPersonas);
await this.generateAppsExtract(conn, numberOfPersonas);
await this.generatePermissionsExtract(conn, numberOfPersonas);
await this.generateTabsExtract(conn, numberOfPersonas);
await this.generatePermissionSetsExtract(conn, numberOfPersonas);
// 1. Extract profile field access and get all profiles
uxLog("action", this, c.green('Extracting profile field access...'));
const profileFieldAccess = await this.getProfileFieldAccessData(conn, selectedObjects);
const profileNames = Array.from(new Set(profileFieldAccess.map(r => r.Profile).filter(Boolean)));
// 2. Pass profileNames to generateObjectFieldsExtract
await this.generateObjectFieldsExtract(conn, selectedObjects, numberOfPersonas, profileNames, profileFieldAccess);
// 3. Write the profile field access CSV (as before)
if (profileFieldAccess.length > 0) {
const reportDir = await getReportDirectory();
this.outputFile = path.join(reportDir, 'ProfileFieldAccess.csv');
await generateCsvFile(profileFieldAccess, this.outputFile, { fileTitle: 'Profile Field Access', noExcel: true });
this.csvFiles.push(this.outputFile);
}
else {
uxLog('log', this, c.yellow('No profile field access records found, skipping ProfileFieldAccess.csv.'));
}
this.outputFile = '';
this.outputFile = await generateReportPath('profiles-extract', this.outputFile);
uxLog("action", this, c.green('Generating final XLSX report...'));
await createXlsxFromCsvFiles(this.csvFiles, this.outputFile, { fileTitle: 'profiles extract' });
}
catch (error) {
uxLog('log', this, c.red('Failed to fetch SObjects.'));
throw error;
}
}
/**
* Extracts field-level access for profiles using FieldPermissions object.
* Generates a CSV report of profile-field access.
* @param conn Salesforce connection
*/
// Returns all profile field access records as an array
async getProfileFieldAccessData(conn, selectedObjects) {
const fieldAccessRecords = [];
try {
let soql = `SELECT Field, PermissionsRead, PermissionsEdit, SObjectType, Parent.Profile.Name FROM FieldPermissions WHERE Parent.ProfileId != null`;
if (selectedObjects && selectedObjects.length > 0) {
const objectList = selectedObjects.map(obj => `'${obj}'`).join(", ");
soql += ` AND SObjectType IN (${objectList})`;
}
// Add filter for active profiles
if (this.activeProfileNames && this.activeProfileNames.size > 0) {
const profileList = Array.from(this.activeProfileNames)
.filter((name) => !!name)
.map(name => `'${name.replace(/'/g, "''")}'`).join(", ");
soql += ` AND Parent.Profile.Name IN (${profileList})`;
}
const result = await bulkQuery(soql, conn);
result.records.forEach((rec) => {
fieldAccessRecords.push({
Profile: rec['Parent.Profile.Name'],
SObjectType: rec['SobjectType'],
Field: rec['Field'],
PermissionsRead: rec['PermissionsRead'] === true || rec['PermissionsRead'] === 'true' ? 'Yes' : 'No',
PermissionsEdit: rec['PermissionsEdit'] === true || rec['PermissionsEdit'] === 'true' ? 'Yes' : 'No',
});
});
uxLog('log', this, c.green(`Fetched ${fieldAccessRecords.length} profile field access records.`));
}
catch (error) {
uxLog('warning', this, c.yellow(`Failed to query FieldPermissions: ${error.message}`));
}
return fieldAccessRecords;
}
/**
* Prompts the user to select Salesforce objects (SObjects) that have records in the org.
* Generates a CSV report of the selected objects.
* @param conn Salesforce connection
* @returns Array of selected object API names
*/
async generateObjectsList(conn) {
let selectedObjects = [];
uxLog('action', this, c.green('Fetching SObjects list...'));
let sobjectsList = [];
try {
// Exclude objects whose API names start with any of these prefixes
const excludedPrefixes = [
'Active',
'Apex',
'AuraDefinition',
'Business',
'Content',
'Dashboard',
'Email',
'Flow',
'Forecasting',
'Formula',
'ListView',
'LoginGeo',
'Marketing',
'MatchingRule',
'PermissionSet',
'UiFormula',
'WebLink',
'pi__'
];
// Exclude objects whose API names are in this explicit list
const excludedObjects = [
'AppDefinition', 'AppMenu', 'AssignmentRule', 'AsyncApexJob', 'AuthProvider', 'AuthSession',
'BrowserPolicyViolation', 'CampaignInfluenceModel', 'CaseStatus', 'ClientBrowser', 'Community',
'ConnectedApplication', 'ContractStatus', 'CronJobDetail', 'CronTrigger', 'CustomNotificationType',
'CustomPermission', 'DataType', 'DeleteEvent', 'Domain', 'DuplicateRule', 'EntityDefinition',
'FeedItem', 'FieldPermissions', 'FieldSecurityClassification', 'FileSearchActivity', 'FiscalYearSettings',
'Folder', 'Group', 'GroupMember', 'NamedCredential', 'OauthToken', 'ObjectPermissions', 'OrderStatus',
'OrgWideEmailAddress', 'Organization', 'PackageLicense', 'PartnerRole', 'Period', 'Profile', 'Publisher',
'QueueSobject', 'RecentlyViewed', 'RecordType', 'Report', 'Scontrol', 'SetupAuditTrail', 'SetupEntityAccess',
'SolutionStatus', 'StandardInvocableActionType', 'StaticResource', 'TabDefinition', 'TenantUsageEntitlement',
'Translation', 'VerificationHistory'
];
const sobjects = await conn.describeGlobal();
sobjectsList = sobjects.sobjects
.filter(sobj => sobj.queryable &&
!excludedPrefixes.some(prefix => sobj.name.startsWith(prefix)) &&
!excludedObjects.includes(sobj.name) &&
!sobj.name.endsWith('History') &&
!sobj.name.endsWith('Share'))
.map((sobject) => ({
label: sobject.label,
name: sobject.name,
masterObject: '',
objectType: sobject.name.endsWith('__c') ? 'Custom' : 'Standard',
}));
uxLog('log', this, c.green('Fetching SObjects completed.'));
uxLog('log', this, c.green(`Fetched ${sobjectsList.length} SObjects.`));
const sobjectsWithRecords = [];
uxLog('action', this, 'Checking SObjects for records...');
for (const sobject of sobjectsList) {
try {
const result = await conn.query(`SELECT COUNT() FROM ${sobject.name}`);
if (result.totalSize > 0) {
sobjectsWithRecords.push({ Object_Label: sobject.label, API_Name: sobject.name, Object_Type: sobject.objectType });
}
uxLog('log', this, `Checked ${sobject.name}: ${result.totalSize} records.`);
}
catch (error) {
uxLog('warning', this, c.yellow(`Failed to query ${sobject.name}: ${error.message}`));
}
}
this.spinner.stop();
if (sobjectsWithRecords.length === 0) {
uxLog('warning', this, c.red('No SObjects with records found.'));
return [];
}
const choices = [];
for (const sobject of sobjectsWithRecords) {
choices.push({
title: `${sobject.API_Name} - ${sobject.Object_Label} - ${sobject.Object_Type}`,
value: sobject.API_Name,
});
}
const statusRes = await prompts({
message: "Please select SObjects to add in the output Excel file",
type: "multiselect",
description: "Be careful, you can't update the selection later without re-running the command :)",
choices: choices,
});
if (statusRes && statusRes.value !== "all") {
selectedObjects = statusRes.value;
uxLog('log', this, `You selected ${selectedObjects.length} objects.`);
}
uxLog("log", this, c.green('Generating Objects.csv report...'));
const reportDir = await getReportDirectory();
this.outputFile = path.join(reportDir, 'Objects.csv');
// Without xlsx
await generateCsvFile(sobjectsWithRecords.filter((sobj) => selectedObjects.includes(sobj.API_Name)), this.outputFile, { fileTitle: 'profiles extract', noExcel: true });
// With xlsx
// this.outputFilesRes = await generateCsvFile(sobjectsWithRecords.filter((sobj) => selectedObjects.includes(sobj.API_Name)), this.outputFile, { fileTitle: 'profiles extract' });
this.csvFiles.push(this.outputFile);
}
catch (error) {
uxLog('log', this, c.red('Failed to fetch SObjects.'));
throw error;
}
return selectedObjects;
}
/**
* Extracts active users from the org, including their username, role, and profile.
* Generates a CSV report of users.
* @param conn Salesforce connection
*/
async generateUsersExtract(conn) {
const usersRecords = [];
const userQuery = "SELECT username, UserRole.Name, Profile.Name FROM User WHERE IsActive = true order by username";
const userResult = await bulkQuery(userQuery, conn);
usersRecords.push(...userResult.records.map((user) => ({
User: user.Username,
Role: user['UserRole.Name'],
Profile: user['Profile.Name'],
Profile_to_Associate: '',
New_Persona: '',
New_Role: '',
})));
// Build set of active profile names (filter out empty/null)
this.activeProfileNames = new Set(userResult.records.map((user) => user['Profile.Name']).filter((n) => !!n));
uxLog('log', this, c.green(`Fetched ${userResult.records.length} active users.`));
uxLog('log', this, c.cyan(`Active profiles: ${Array.from(this.activeProfileNames).join(', ')}`));
const reportDir = await getReportDirectory();
this.outputFile = path.join(reportDir, 'Users.csv');
await generateCsvFile(usersRecords, this.outputFile, { fileTitle: 'Users extract', noExcel: true });
this.csvFiles.push(this.outputFile);
return;
}
/**
* Prompts the user for the number of personas to create.
* Generates a CSV report listing the personas.
* @returns The number of personas
*/
async generatePersonaExtract() {
let numberOfPersonas = 1;
const statusRes = await prompts({
message: "Please enter the number of personas to create",
type: "number",
description: "One tab by personal will be created in the final Excel file",
placeholder: "Input a number of personas (better too much than too few!)",
});
if (statusRes && statusRes.value !== 0) {
numberOfPersonas = statusRes.value;
uxLog('log', this, `Creation of ${numberOfPersonas} personas.`);
}
const persona = [];
for (let i = 1; i <= numberOfPersonas; i++) {
persona.push({ 'Persona Name': `Persona${i}` });
}
const reportDir = await getReportDirectory();
this.outputFile = path.join(reportDir, 'persona.csv');
await generateCsvFile(persona, this.outputFile, { fileTitle: 'persona extract', noExcel: true });
this.csvFiles.push(this.outputFile);
return numberOfPersonas;
}
/**
* Generates a CSV mapping each selected object to persona permissions (Read, Create, Edit, Delete).
* @param selectedObjects Array of object API names
* @param numberOfPersonas Number of personas
*/
async generateRelationExtract(selectedObjects, numberOfPersonas) {
const relationRecords = [];
// Generate dynamic persona columns using Excel formulas
selectedObjects.forEach((objName) => {
const personaCols = {};
for (let i = 1; i <= numberOfPersonas; i++) {
// Persona row in persona sheet is i+1 (header is row 1)
const personaRow = i + 1;
personaCols[`=persona!A${personaRow}&"_Read"`] = '';
personaCols[`=persona!A${personaRow}&"_Create"`] = '';
personaCols[`=persona!A${personaRow}&"_Edit"`] = '';
personaCols[`=persona!A${personaRow}&"_Delete"`] = '';
personaCols[`=persona!A${personaRow}&"_ViewAll"`] = '';
personaCols[`=persona!A${personaRow}&"_ModifyAll"`] = '';
}
relationRecords.push({
Object: objName,
...personaCols,
});
});
const reportDir = await getReportDirectory();
this.outputFile = path.join(reportDir, 'Relation.csv');
await generateCsvFile(relationRecords, this.outputFile, { fileTitle: 'Relation Object Persona', noExcel: true });
this.csvFiles.push(this.outputFile);
return;
}
/**
* Extracts record types for each selected object and maps persona access (Active, Default).
* Generates a CSV report of record types per object.
* @param conn Salesforce connection
* @param selectedObjects Array of object API names
* @param numberOfPersonas Number of personas
*/
async generateRTExtract(conn, selectedObjects, numberOfPersonas) {
const recordTypesRecords = [];
for (const objName of selectedObjects) {
try {
const rtResult = await conn.query(`SELECT Id, Name, DeveloperName FROM RecordType WHERE SobjectType='${objName}' ORDER BY Name`);
rtResult.records.forEach((rt) => {
const personaCols = {};
for (let i = 1; i <= numberOfPersonas; i++) {
const personaRow = i + 1;
personaCols[`=persona!A${personaRow}&"_Active"`] = '';
personaCols[`=persona!A${personaRow}&"_Default"`] = '';
}
recordTypesRecords.push({
Object: objName,
Record_Type: rt.Name,
...personaCols,
});
});
}
catch (error) {
uxLog('warning', this, c.yellow(`Failed to query RecordTypes for ${objName}: ${error.message}`));
}
}
const reportDir = await getReportDirectory();
this.outputFile = path.join(reportDir, 'RecordTypes.csv');
await generateCsvFile(recordTypesRecords, this.outputFile, { fileTitle: 'Record Types extract', noExcel: true });
this.csvFiles.push(this.outputFile);
return;
}
/**
* Extracts custom applications (AppDefinition) and maps persona access (Active, Default).
* Generates a CSV report of applications.
* @param conn Salesforce connection
* @param numberOfPersonas Number of personas
*/
/* jscpd:ignore-start */
async generateAppsExtract(conn, numberOfPersonas) {
const appsRecords = [];
try {
const rtResult = await conn.query(`SELECT Id, DurableId, DeveloperName, MasterLabel FROM AppDefinition WHERE NamespacePrefix != 'standard'`);
rtResult.records.forEach((rt) => {
const personaCols = {};
for (let i = 1; i <= numberOfPersonas; i++) {
const personaRow = i + 1;
personaCols[`=persona!A${personaRow}&"_Active"`] = '';
personaCols[`=persona!A${personaRow}&"_Default"`] = '';
}
appsRecords.push({
Application: rt.MasterLabel,
DeveloperName: rt.DeveloperName,
...personaCols,
});
});
}
catch (error) {
uxLog('warning', this, c.yellow(`Failed to query Applications : ${error.message}`));
}
const reportDir = await getReportDirectory();
this.outputFile = path.join(reportDir, 'Applications.csv');
await generateCsvFile(appsRecords, this.outputFile, { fileTitle: 'Applications extract', noExcel: true });
this.csvFiles.push(this.outputFile);
return;
}
/* jscpd:ignore-end */
/**
* Extracts all boolean permission fields from PermissionSet object.
* Generates a CSV report mapping each permission to personas.
* @param conn Salesforce connection
* @param numberOfPersonas Number of personas
*/
async generatePermissionsExtract(conn, numberOfPersonas) {
const permissionsRecords = [];
// Describe the PermissionSet object
const desc = await conn.describeSObject("PermissionSet");
// Filter only fields that start with "Permissions"
const permissionFields = desc.fields.filter(f => f.name.startsWith("Permissions") && f.type === "boolean");
// Print API name + Label
permissionFields.forEach(field => {
const personaCols = {};
for (let i = 1; i <= numberOfPersonas; i++) {
const personaRow = i + 1;
personaCols[`=persona!A${personaRow}`] = '';
}
permissionsRecords.push({
Permission_Label: field.label,
Permission_API_Name: field.name,
...personaCols,
});
});
const reportDir = await getReportDirectory();
this.outputFile = path.join(reportDir, 'Permissions.csv');
await generateCsvFile(permissionsRecords, this.outputFile, { fileTitle: 'Permissions extract', noExcel: true });
this.csvFiles.push(this.outputFile);
return;
}
/**
* Extracts custom tabs and maps persona access.
* Generates a CSV report of tabs.
* @param conn Salesforce connection
* @param numberOfPersonas Number of personas
*/
async generateTabsExtract(conn, numberOfPersonas) {
const tabsRecords = [];
try {
const tabsResult = await conn.query(`SELECT Name, Label FROM TabDefinition WHERE IsCustom = true`);
tabsResult.records.forEach((tab) => {
const personaCols = {};
for (let i = 1; i <= numberOfPersonas; i++) {
const personaRow = i + 1;
personaCols[`=persona!A${personaRow}`] = '';
}
tabsRecords.push({
Tab_Label: tab.Label,
Tab_API_Name: tab.Name,
...personaCols,
});
});
const reportDir = await getReportDirectory();
this.outputFile = path.join(reportDir, 'Tabs.csv');
await generateCsvFile(tabsRecords, this.outputFile, { fileTitle: 'Tabs extract', noExcel: true });
this.csvFiles.push(this.outputFile);
}
catch (error) {
uxLog('warning', this, c.yellow(`Failed to query Tabs : ${error.message}`));
}
return;
}
/**
* For each selected object, extracts all fields and their metadata (label, type, picklist values, etc.).
* Maps persona visibility and read-only status for each field.
* Generates a CSV report per object.
* @param conn Salesforce connection
* @param selectedObjects Array of object API names
* @param numberOfPersonas Number of personas
*/
async generateObjectFieldsExtract(conn, selectedObjects, numberOfPersonas, profileNames = [], profileFieldAccess = []) {
const fieldsRecords = [];
for (const objName of selectedObjects) {
try {
fieldsRecords.length = 0;
const desc = await conn.describeSObject(objName);
desc.fields.forEach((field) => {
let picklistValues = '';
if (field.picklistValues && field.picklistValues.length > 0) {
picklistValues = field.picklistValues.map(pv => pv.value).join('; ');
}
// Add persona columns using formula logic
const personaCols = {};
for (let i = 1; i <= numberOfPersonas; i++) {
const personaRow = i + 1;
personaCols[`=persona!A${personaRow}&"_View"`] = '';
personaCols[`=persona!A${personaRow}&"_Edit"`] = '';
}
// Add one column per profile, fill with 'none', 'edit', or 'read'
const profileCols = (profileNames || []).reduce((acc, profile) => {
// Find access for this field/profile/object (case-insensitive, and check both SObjectType and API_Name)
const access = profileFieldAccess.find((rec) => {
const profileMatch = rec.Profile === profile;
const objectMatch = (rec.SObjectType === objName || rec.SObjectType?.toLowerCase() === objName.toLowerCase());
// rec.Field can be 'ObjectName.FieldName' or just 'FieldName'
let recFieldName = rec.Field;
if (recFieldName && recFieldName.includes('.')) {
recFieldName = recFieldName.split('.').pop();
}
const fieldMatch = (recFieldName === field.name || recFieldName?.toLowerCase() === field.name.toLowerCase());
return profileMatch && objectMatch && fieldMatch;
});
let value = 'none';
if (access) {
if (access.PermissionsEdit === 'Yes') {
value = 'edit';
}
else if (access.PermissionsRead === 'Yes') {
value = 'read';
}
}
acc[`Profile_${profile}`] = value;
return acc;
}, {});
fieldsRecords.push({
Field_Label: field.label,
API_Name: field.name,
Data_Type: field.type,
Length: field.length ? field.length.toString() : '',
Field_Type: field.calculated ? 'Formula' : (field.type === 'reference' ? 'Lookup' : field.type),
Required: field.nillable ? 'No' : 'Yes',
PicklistValues: picklistValues,
Formula: field.calculated ? field.calculatedFormula : '',
ExternalId: field.externalId ? 'Yes' : 'No',
TrackHistory: field.trackHistory ? 'Yes' : 'No',
Description: field.description ? field.description : '',
HelpText: field.inlineHelpText ? field.inlineHelpText : '',
...personaCols,
...profileCols,
});
});
}
catch (error) {
uxLog('warning', this, c.yellow(`Failed to describe fields for ${objName}: ${error.message}`));
}
const reportDir = await getReportDirectory();
this.outputFile = path.join(reportDir, `${objName} Fields.csv`);
await generateCsvFile(fieldsRecords, this.outputFile, { fileTitle: `${objName} Fields extract`, noExcel: true });
this.csvFiles.push(this.outputFile);
}
return;
}
/**
* Extracts all Permission Sets in the org.
* Generates a CSV report mapping each permission set to personas.
* @param conn
* @param numberOfPersonas
*/
async generatePermissionSetsExtract(conn, numberOfPersonas) {
const permissionSetsRecords = [];
try {
const result = await conn.query(`SELECT Name, Label, Description, IsCustom FROM PermissionSet WHERE IsOwnedByProfile = false`);
result.records.forEach((ps) => {
const personaCols = {};
for (let i = 1; i <= numberOfPersonas; i++) {
const personaRow = i + 1;
personaCols[`=persona!A${personaRow}&"_Assigned"`] = ''; // Dynamic column for each persona
}
permissionSetsRecords.push({
Name: ps.Name,
Label: ps.Label || '',
Description: ps.Description || '',
IsCustom: ps.IsCustom ? 'Yes' : 'No',
...personaCols,
});
});
const reportDir = await getReportDirectory();
this.outputFile = path.join(reportDir, 'PermissionSets.csv');
await generateCsvFile(permissionSetsRecords, this.outputFile, {
fileTitle: 'Permission Sets extract',
noExcel: true,
});
this.csvFiles.push(this.outputFile);
}
catch (error) {
uxLog('warning', this, c.yellow(`Failed to query PermissionSets: ${error.message}`));
}
}
}
//# sourceMappingURL=profiles-extract.js.map