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
270 lines (261 loc) • 14.4 kB
JavaScript
/* jscpd:ignore-start */
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import { Messages, SfError } from '@salesforce/core';
import c from 'chalk';
import fs from 'fs-extra';
import pascalcase from 'pascalcase';
import * as path from 'path';
import { uxLog } from '../../../../common/utils/index.js';
import { DATA_FOLDERS_ROOT } from '../../../../common/utils/dataUtils.js';
import { prompts } from '../../../../common/utils/prompts.js';
import { WebSocketClient } from '../../../../common/websocketClient.js';
import { getConfig, setConfig } from '../../../../config/index.js';
import { PACKAGE_ROOT_DIR } from '../../../../settings.js';
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('sfdx-hardis', 'org');
export default class ConfigureData extends SfCommand {
static title = 'Configure Data project';
static description = `
## Command Behavior
**Configures a Salesforce Data Migration Utility (SFDMU) project for data export and import operations.**
This command assists in setting up SFDMU workspaces, which are essential for managing data within your Salesforce environments. It streamlines the creation of \`export.json\` files and related configurations, enabling efficient data seeding, migration, and synchronization.
Key functionalities:
- **Template-Based Configuration:** Allows you to choose from predefined SFDMU templates or start with a blank configuration. Templates can pre-populate \`export.json\` with common data migration scenarios.
- **Interactive Setup:** Guides you through the process of defining the SFDMU project folder name, label, and description.
- **\`export.json\` Generation:** Creates the \`export.json\` file, which is the core configuration file for SFDMU, defining objects to export/import, queries, and operations.
- **Additional File Generation:** Can generate additional configuration files, such as a \`badwords.json\` file for data filtering scenarios.
- **Scratch Org Integration:** Offers to automatically configure the SFDMU project to be used for data import when initializing a new scratch org, ensuring consistent test data across development environments.
See this article for a practical example:
[](https://nicolas.vuillamy.fr/how-to-detect-bad-words-in-salesforce-records-using-sfdx-data-loader-and-sfdx-hardis-171db40a9bac)
<details markdown="1">
<summary>Technical explanations</summary>
The command's technical implementation involves:
- **SFDMU Integration:** It acts as a setup wizard for SFDMU, generating the necessary configuration files that the \`sfdmu\` plugin consumes.
- **Interactive Prompts:** Uses the \`prompts\` library to gather user input for various configuration parameters, such as the data path, label, and description.
- **File System Operations:** Employs \`fs-extra\` to create directories (e.g., \`data/your-project-name/\`) and write the \`export.json\` and any additional configuration files.
- **JSON Manipulation:** Constructs the \`export.json\` content dynamically based on user input and selected templates, including defining objects, queries, and operations.
- **PascalCase Conversion:** Uses \`pascalcase\` to format the SFDMU folder name consistently.
- **Configuration Persistence:** Updates the project's \`sfdx-hardis.yml\` file (via \`setConfig\`) to include the newly configured data package if it's intended for scratch org initialization.
- **WebSocket Communication:** Uses \`WebSocketClient.requestOpenFile\` to open the generated \`export.json\` file in VS Code, facilitating immediate configuration.
- **Required Plugin Check:** Explicitly lists \`sfdmu\` as a required plugin, ensuring the necessary dependency is present.
</details>
`;
static examples = ['$ sf hardis:org:configure:data'];
static flags = {
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',
}),
};
// Set this to true if your command requires a project workspace; 'requiresProject' is false by default
static requiresProject = false;
// List required plugins, their presence will be tested before running the command
static requiresSfdxPlugins = ['sfdmu'];
additionalFiles = [];
dataPath;
sfdmuConfig;
importInScratchOrgs;
/* jscpd:ignore-end */
async run() {
const template = await this.selectTemplate();
if (template === 'blank') {
// Request info to build sfdmu workspace
await this.buildExportJsonInfo();
}
else {
await this.buildExportJsonInfoFromTemplate(template);
}
// Check if not already existing
const { exportJsonFile, sfdmuProjectFolder } = await this.generateConfigurationFiles();
await this.promptImportInScratchOrgs(sfdmuProjectFolder);
// Set bac initial cwd
const sfdmuBaseDoc = "https://help.sfdmu.com/configuration";
const sfdmuExternalIdsDoc = "https://help.sfdmu.com/full-documentation/advanced-features/composite-external-id-keys";
const message = c.cyan(`Successfully initialized sfdmu project ${c.green(sfdmuProjectFolder)}, with ${c.green('export.json')} file.`);
uxLog("other", this, message);
uxLog("log", this, c.grey(`You can now configure it using SFDMU documentation: ${c.yellow(sfdmuBaseDoc)}.`));
uxLog("log", this, c.grey(`If you don't have a unique field to identify an object, use composite external ids: ${c.yellow(sfdmuExternalIdsDoc)}.`));
// Trigger command to open SFDMU config file in VS Code extension
if (WebSocketClient.isAliveWithLwcUI()) {
WebSocketClient.sendReportFileMessage(exportJsonFile, 'Edit your SFDMU export.json file', 'report');
WebSocketClient.sendReportFileMessage(sfdmuBaseDoc, 'SFDMU documentation (Basic)', 'docUrl');
WebSocketClient.sendReportFileMessage(sfdmuExternalIdsDoc, 'SFDMU documentation (External Ids)', 'docUrl');
}
else {
WebSocketClient.requestOpenFile(exportJsonFile);
}
return { outputString: message };
}
async generateConfigurationFiles() {
const sfdmuProjectFolder = path.join(DATA_FOLDERS_ROOT, this.dataPath);
if (fs.existsSync(sfdmuProjectFolder)) {
throw new SfError(`[sfdx-hardis]${c.red(`Folder ${c.bold(sfdmuProjectFolder)} already exists`)}`);
}
// Create folder & export.json
await fs.ensureDir(sfdmuProjectFolder);
const exportJsonFile = path.join(sfdmuProjectFolder, 'export.json');
await fs.writeFile(exportJsonFile, JSON.stringify(this.sfdmuConfig, null, 2));
uxLog("action", this, c.cyan('Generated SFDMU config file ' + exportJsonFile));
for (const additionalFile of this.additionalFiles) {
const additionalFileFull = path.join(sfdmuProjectFolder, additionalFile.path);
await fs.writeFile(additionalFileFull, additionalFile.text);
uxLog("action", this, c.cyan(additionalFile.message + ': ') + c.yellow(additionalFileFull));
if (WebSocketClient.isAliveWithLwcUI()) {
WebSocketClient.sendReportFileMessage(additionalFileFull, additionalFile.message, 'report');
}
else {
WebSocketClient.requestOpenFile(additionalFileFull);
}
}
return { exportJsonFile, sfdmuProjectFolder };
}
async buildExportJsonInfo() {
const resp = await this.promptExportInfo();
// Collect / reformat data
this.dataPath = pascalcase(resp.dataPath);
const sfdxHardisLabel = resp.sfdxHardisLabel;
const sfdxHardisDescription = resp.sfdxHardisDescription;
const additionalConfig = resp.additional || [];
this.sfdmuConfig = {
sfdxHardisLabel: sfdxHardisLabel,
sfdxHardisDescription: sfdxHardisDescription,
objects: [
{
query: "SELECT all FROM Account WHERE Name='sfdx-hardis'",
operation: 'Upsert',
externalId: 'Name',
},
],
};
// Manage badwords filter option
if (additionalConfig.includes('badwordsFilter')) {
const badwordsFileName = 'badwords.json';
this.sfdmuConfig.objects[0] = [
{
query: 'SELECT all FROM Lead',
operation: 'Readonly',
targetRecordsFilter: 'core:DetectBadwords',
filterRecordsAddons: [
{
module: 'core:RecordsFilter',
args: {
filterType: 'BadWords',
settings: {
badwordsFile: badwordsFileName,
detectFields: ['Description'],
highlightWords: true,
outputMatches: false,
},
},
},
],
},
];
if (!fs.existsSync('badwords.json')) {
const badwordsSample = {
badwords: ['write', 'your', 'bad', 'words', 'and expressions', 'here'],
};
this.additionalFiles.push({
path: badwordsFileName,
text: JSON.stringify(badwordsSample, null, 2),
message: 'Sample badwords file has been generated and needs to be updated',
});
}
}
}
async promptExportInfo() {
return await prompts([
{
type: 'text',
name: 'dataPath',
message: c.cyanBright('Please input the SFDMU folder name (PascalCase format)'),
description: 'The folder name that will contain the SFDMU data configuration files',
placeholder: 'Ex: ProductsActive',
},
{
type: 'text',
name: 'sfdxHardisLabel',
message: c.cyanBright('Please input the SFDMU config label'),
description: 'A human-readable label for this data configuration',
placeholder: 'Ex: Active Products',
},
{
type: 'text',
name: 'sfdxHardisDescription',
message: c.cyanBright('Please input the SFDMU config description'),
description: 'A detailed description explaining what this data configuration does',
placeholder: 'Ex: Active products are used for scratch org initialization and in deployments',
},
{
type: 'multiselect',
name: 'additional',
message: c.cyanBright('Please select additional options if you need them'),
description: 'Choose optional features to include in the data configuration (select nothing to skip)',
choices: [
{
title: 'Bad words detector',
description: 'Can detect a list of bad words in records',
value: 'badwordsFilter',
},
],
},
]);
}
async selectTemplate() {
const templateChoices = [];
const templatesFolder = path.join(PACKAGE_ROOT_DIR, 'defaults/templates/sfdmu');
const templateFiles = fs.readdirSync(templatesFolder);
for (const templateFile of templateFiles) {
const templateName = path.basename(templateFile).replace('.json', '');
templateChoices.push({
title: `📝 ${templateName}`,
value: path.join(templatesFolder, templateFile),
description: `sfdx-hardis template for ${templateName}`,
});
}
const defaultTemplateChoice = {
title: '📄 Blank template',
value: 'blank',
description: 'Configure your data import/export from scratch 😊',
};
const templateResp = await prompts({
type: 'select',
name: 'template',
message: c.cyanBright('Please select a SFDMU template, or the blank one'),
description: 'Choose a pre-configured SFDMU template for data operations or start with a blank configuration',
placeholder: 'Select a template',
choices: [...[defaultTemplateChoice], ...templateChoices],
});
return templateResp.template;
}
async buildExportJsonInfoFromTemplate(templateFile) {
const templateName = path.basename(templateFile).replace('.json', '');
this.dataPath = pascalcase(templateName);
this.sfdmuConfig = JSON.parse(fs.readFileSync(templateFile, 'utf-8'));
}
async promptImportInScratchOrgs(sfdmuProjectFolder) {
const importResp = await prompts({
type: 'confirm',
name: 'importInScratchOrgs',
message: c.cyanBright('Do you want this SFDMU config to be used to import data when initializing a new scratch org ?'),
description: 'Automatically import this data set when creating new scratch orgs for development and testing',
default: false,
});
this.importInScratchOrgs = importResp.importInScratchOrgs === true;
// Manage dataPackages if importInScratchOrgs is true
if (this.importInScratchOrgs === true) {
const config = await getConfig('project');
const dataPackages = config.dataPackages || [];
dataPackages.push({ dataPath: sfdmuProjectFolder.replace(/\\/g, '/'), importInScratchOrgs: true });
await setConfig('project', { dataPackages: dataPackages });
}
}
}
//# sourceMappingURL=data.js.map