-
Notifications
You must be signed in to change notification settings - Fork 12
Clean Up Metadata Blocking Records in Production #472
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
rajender-kumar-salesforce
merged 17 commits into
release260omnistudiopackage
from
u/rajender-kumar/w-21274482/deployment
Mar 6, 2026
Merged
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
cc99fe8
fix: remove stale records
rajender-kumar-salesforce 9fafffb
fix: remove stale records
rajender-kumar-salesforce 5eba2c3
fix: review comments
rajender-kumar-salesforce ac0a89b
fix: review comments
rajender-kumar-salesforce 1159add
fix: review comments
rajender-kumar-salesforce f5a17fb
fix: feedback items
rajender-kumar-salesforce f30d373
fix: review comments
rajender-kumar-salesforce 1528d3c
fix: review comments
rajender-kumar-salesforce 902dc41
fix: review comments
rajender-kumar-salesforce ea06e66
Merge branch 'release260omnistudiopackage' of https://github.com/sale…
rajender-kumar-salesforce 40fb960
fix: add approved text
rajender-kumar-salesforce 3aebb4a
fix: test
rajender-kumar-salesforce 1d30a00
fix: review comments
rajender-kumar-salesforce c64c52a
fix: review comments
rajender-kumar-salesforce b59ec3e
fix: review comments
rajender-kumar-salesforce 19793f9
fix: review comments
rajender-kumar-salesforce 015901f
fix: review comments
rajender-kumar-salesforce File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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,40 @@ | ||
| { | ||
| "commandDescription": "Deactivates and deletes Omnistudio records (Omniscripts, Integration Procedures, Flexcards, and Data Mappers) that prevent enabling the Omnistudio Metadata API. This includes records with special characters in unique name fields or missing unique names.\n\nWarning: This action is permanent. Run in a testing sandbox and verify results before running in your production environment.", | ||
rajender-kumar-salesforce marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| "examples": ["sf omnistudio:migration:clean -u orguser@domain.com"], | ||
| "enableVerboseOutput": "Show detailed information in the command output.", | ||
| "sandboxWarning": "IMPORTANT: This command permanently deletes records. Make sure that all Omnistudio components are backed up or deployed to a testing sandbox. Verify all results before enabling the Omnistudio Metadata API in your production environment.", | ||
| "confirmDeletion": "By proceeding, you confirm that you want to deactivate and permanently delete all Omnistudio records with special characters in their unique name fields or those missing a unique name. These records are incompatible with the Metadata API. Do you want to proceed?", | ||
rajender-kumar-salesforce marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| "operationCancelled": "Operation cancelled. No changes were made.", | ||
| "deletionComplete": "Cleanup complete. All Omnistudio records with special characters and those without a unique name have been removed.", | ||
| "standardDataModelRequired": "This command only supports orgs using the standard data model.", | ||
| "metadataApiAlreadyEnabled": "The Omnistudio metadata API is already enabled. No cleanup is required.", | ||
| "errorRunningClean": "Cleanup failed: %s", | ||
rajender-kumar-salesforce marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| "specialCharCleanupPhaseStart": "Phase 1: Removing records with special characters in unique name fields.", | ||
| "specialCharCleanupSectionStart": "Scanning %s records...", | ||
| "noSpecialCharRecords": "No %s records found with special characters.", | ||
| "foundSpecialCharRecordsToRemove": "Found %s %s record(s) with special characters to remove.", | ||
| "deactivatingRecords": "Deactivating %s %s record(s)...", | ||
| "deactivatedRecords": "Successfully deactivated %s %s record(s).", | ||
| "deletingRecords": "Deleting %s %s record(s)...", | ||
| "deletedRecords": "Successfully deleted %s %s record(s).", | ||
| "errorRemovingSpecialCharRecords": "Error removing %s records with special characters: %s", | ||
| "nullUniqueNameCleanupPhaseStart": "Phase 2: Removing records without a deployment reference (missing unique names) that prevent enabling the Omnistudio metadata API.", | ||
| "nullUniqueNameCleanupSectionStart": "Scanning %s records for missing unique names...", | ||
| "noNullUniqueNameRecords": "No %s records found with missing unique names.", | ||
| "foundNullUniqueNameRecords": "Found %s %s record(s) with missing unique names.", | ||
| "deactivationFailed": "Failed to deactivate %s (%s): %s", | ||
| "deletionFailed": "Failed to delete %s (%s): %s", | ||
| "errorAssessingSpecialCharRecords": "Error scanning %s records for special characters: %s", | ||
| "errorAssessingNullUniqueNameRecords": "Error scanning %s records with missing unique names: %s", | ||
| "errorCleaningNullUniqueNameRecords": "Error removing %s records with missing unique names: %s", | ||
| "assessFlagDescription": "Preview which records would be removed by the clean command, without making any changes. Results are saved to the 'clean_assessment' folder.", | ||
| "assessPhaseStart": "Starting assessment. Your org will not be modified.", | ||
| "assessSpecialCharPhaseStart": "Phase 1 (Preview): Scanning for records with special characters in unique name fields.", | ||
| "assessNullUniqueNamePhaseStart": "Phase 2 (Preview): Scanning for records without a deployment reference.", | ||
| "assessScanningEntity": "Scanning %s...", | ||
| "assessEntityFound": "Found %s %s record(s) eligible for removal.", | ||
| "assessEntityNone": "No %s records require cleanup.", | ||
| "assessmentFileWritten": "Assessment report saved to: %s", | ||
| "assessmentComplete": "Assessment complete. Review the reports in %s before running the clean command.", | ||
| "assessmentNoRecords": "Your org is clean. No records require removal." | ||
| } | ||
This file contains hidden or 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,162 @@ | ||
| /* eslint-disable @typescript-eslint/no-unsafe-assignment */ | ||
| /* eslint-disable @typescript-eslint/no-unsafe-call */ | ||
| /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
| import * as fs from 'fs'; | ||
| import * as os from 'os'; | ||
| import * as path from 'path'; | ||
| import { Connection, Messages, Org, Logger as CoreLogger } from '@salesforce/core'; | ||
| import { SfCommand, Ux, Flags as flags } from '@salesforce/sf-plugins-core'; | ||
| import { Logger } from '../../../utils/logger'; | ||
| import { OmnistudioOrgDetails, OrgUtils } from '../../../utils/orgUtils'; | ||
| import { | ||
| initializeDataModelService, | ||
| isStandardDataModel, | ||
| isStandardDataModelWithMetadataAPIEnabled, | ||
| } from '../../../utils/dataModelService'; | ||
| import { SpecialCharacterRecordCleanupService } from '../../../utils/config/SpecialCharacterRecordCleanupService'; | ||
| import { ExistingRecordCleanupService } from '../../../utils/config/ExistingRecordCleanupService'; | ||
| import { askConfirmation } from '../../../utils/promptUtil'; | ||
|
|
||
| const ASSESS_OUTPUT_FOLDER = 'clean_assessment'; | ||
|
|
||
| Messages.importMessagesDirectory(__dirname); | ||
| const messages = Messages.loadMessages('@salesforce/plugin-omnistudio-migration-tool', 'clean'); | ||
|
|
||
| export type CleanResult = { | ||
| success: boolean; | ||
| }; | ||
|
|
||
| interface CleanFlags { | ||
| 'target-org'?: Org; | ||
| verbose?: boolean; | ||
| assess?: boolean; | ||
| } | ||
|
|
||
| export default class Clean extends SfCommand<CleanResult> { | ||
| public static description = messages.getMessage('commandDescription'); | ||
|
|
||
| public static examples = messages.getMessage('examples').split(os.EOL); | ||
|
|
||
| public static args: any = []; | ||
|
|
||
| public static readonly flags: any = { | ||
| 'target-org': flags.optionalOrg({ | ||
| summary: 'Target org username or alias', | ||
| char: 'u', | ||
| required: true, | ||
| aliases: ['targetusername'], | ||
| deprecateAliases: true, | ||
| makeDefault: false, | ||
| }), | ||
| verbose: flags.boolean({ | ||
| description: messages.getMessage('enableVerboseOutput'), | ||
| }), | ||
| assess: flags.boolean({ | ||
| description: messages.getMessage('assessFlagDescription'), | ||
| default: false, | ||
| }), | ||
| }; | ||
|
|
||
| public async run(): Promise<CleanResult> { | ||
| const { flags: parsedFlags } = await this.parse(Clean); | ||
| const ux = new Ux(); | ||
| const logger = await CoreLogger.child(this.constructor.name); | ||
| Logger.initialiseLogger(ux, logger, 'clean', parsedFlags.verbose); | ||
| try { | ||
| return await this.runClean(parsedFlags as CleanFlags, ux); | ||
| } catch (e) { | ||
| const error = e as Error; | ||
| Logger.error(messages.getMessage('errorRunningClean', [error.message])); | ||
| process.exit(1); | ||
| } | ||
| } | ||
|
|
||
| private async runClean(parsedFlags: CleanFlags, ux: Ux): Promise<CleanResult> { | ||
| // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
| const org = parsedFlags['target-org']!; | ||
| const conn: Connection = org.getConnection(); | ||
|
|
||
| const orgs: OmnistudioOrgDetails = await OrgUtils.getOrgDetails(conn); | ||
| initializeDataModelService(orgs); | ||
|
|
||
| if (!isStandardDataModel()) { | ||
| Logger.warn(messages.getMessage('standardDataModelRequired')); | ||
| return { success: false }; | ||
| } | ||
|
|
||
| if (isStandardDataModelWithMetadataAPIEnabled()) { | ||
| Logger.log(messages.getMessage('metadataApiAlreadyEnabled')); | ||
| return { success: false }; | ||
| } | ||
|
|
||
| if (parsedFlags.assess) { | ||
| return this.runAssess(conn, ux); | ||
| } | ||
|
|
||
| Logger.warn(messages.getMessage('sandboxWarning')); | ||
| const confirmed = await askConfirmation(messages.getMessage('confirmDeletion')); | ||
| if (!confirmed) { | ||
| Logger.log(messages.getMessage('operationCancelled')); | ||
| return { success: false }; | ||
| } | ||
|
|
||
| const specialCharService = new SpecialCharacterRecordCleanupService(conn, messages, ux); | ||
| await specialCharService.deactivateAndDelete(); | ||
|
|
||
| const existingRecordCleanupService = new ExistingRecordCleanupService(conn, messages, ux); | ||
| await existingRecordCleanupService.cleanAll(); | ||
|
|
||
| Logger.log(messages.getMessage('deletionComplete')); | ||
| return { success: true }; | ||
| } | ||
|
|
||
| private async runAssess(conn: Connection, ux: Ux): Promise<CleanResult> { | ||
| Logger.log(messages.getMessage('assessPhaseStart')); | ||
|
|
||
| const specialCharService = new SpecialCharacterRecordCleanupService(conn, messages, ux); | ||
| const existingRecordService = new ExistingRecordCleanupService(conn, messages, ux); | ||
|
|
||
| // Run phases sequentially so their log output does not interleave | ||
| const specialCharMap = await specialCharService.assess(); | ||
| const nullUniqueNameMap = await existingRecordService.assess(); | ||
|
|
||
| // Collect all entity names from both phases | ||
| const allEntities = new Set([...specialCharMap.keys(), ...nullUniqueNameMap.keys()]); | ||
|
|
||
| let totalRecords = 0; | ||
| const outputDir = path.join(process.cwd(), ASSESS_OUTPUT_FOLDER); | ||
rajender-kumar-salesforce marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| fs.mkdirSync(outputDir, { recursive: true }); | ||
|
|
||
| for (const entityName of allEntities) { | ||
| const specialCharRecords = specialCharMap.get(entityName) ?? []; | ||
| const nullUniqueNameRecords = nullUniqueNameMap.get(entityName) ?? []; | ||
| const entityTotal = specialCharRecords.length + nullUniqueNameRecords.length; | ||
| totalRecords += entityTotal; | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| const strip = (records: Array<Record<string, unknown>>): Array<Record<string, unknown>> => | ||
| records.map(({ attributes: _attrs, ...rest }) => rest); | ||
|
|
||
| const assessment = { | ||
| component: entityName, | ||
| specialCharacterRecords: strip(specialCharRecords), | ||
| orphanRecords: strip(nullUniqueNameRecords as unknown as Array<Record<string, unknown>>), | ||
| totalToDelete: entityTotal, | ||
| }; | ||
|
|
||
| // Use a filesystem-safe filename (remove spaces) | ||
| const fileName = `${entityName.replace(/ /g, '')}.json`; | ||
| const filePath = path.join(outputDir, fileName); | ||
| fs.writeFileSync(filePath, JSON.stringify(assessment, null, 2)); | ||
| Logger.log(messages.getMessage('assessmentFileWritten', [filePath])); | ||
| } | ||
|
|
||
| if (totalRecords === 0) { | ||
| Logger.log(messages.getMessage('assessmentNoRecords')); | ||
| } else { | ||
| Logger.log(messages.getMessage('assessmentComplete', [outputDir])); | ||
| } | ||
|
|
||
| return { success: true }; | ||
| } | ||
| } | ||
This file contains hidden or 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
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.