generated from salesforcecli/plugin-template
-
Notifications
You must be signed in to change notification settings - Fork 20
Wr/delete #199
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
Wr/delete #199
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
2eb3f76
feat: add source:delete command
WillieRuemmele d4004db
chore: handle xml or content only metadata
WillieRuemmele ced2483
chore: fix bundle types
WillieRuemmele fe37406
fix: add REST to delete, add NUTs
WillieRuemmele fe5eee2
docs: add long descriptions where appropriate
WillieRuemmele 82e3745
chore: 3/4 of Steve's comments addressed
WillieRuemmele 312e6b9
chore: swap prompt to confirm
WillieRuemmele fca05e4
chore: attempt to create TestKit differently
WillieRuemmele 717db9a
chore: attempt to create TestKit differently
WillieRuemmele de9bd53
chore: fix delete with tests on windows NUT
WillieRuemmele a20f8b6
chore: backout server error messages
WillieRuemmele d0bf60a
refactor: use sdr walkContent
mshanemc 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| { | ||
| "description": "delete source from your project and from a non-source-tracked org \n Use this command to delete components from orgs that don’t have source tracking.\nTo remove deleted items from scratch orgs, which have change tracking, use \"sfdx force:source:push\".", | ||
| "examples": ["$ sfdx force:source:delete -m <metadata>", "$ sfdx force:source:delete -p path/to/source"], | ||
| "flags": { | ||
| "sourcepath": "comma-separated list of source file paths to delete", | ||
| "metadata": "comma-separated list of names of metadata components to delete", | ||
| "noprompt": "do not prompt for delete confirmation", | ||
| "wait": "wait time for command to finish in minutes", | ||
| "checkonly": "validate delete command but do not delete from the org or delete files locally", | ||
| "testLevel": "deployment testing level", | ||
| "runTests": "tests to run if --testlevel RunSpecifiedTests", | ||
| "verbose": "verbose output of delete result", | ||
|
|
||
| "checkonlyLong": "Validates the deleted metadata and runs all Apex tests, but prevents the deletion from being saved to the org. \nIf you change a field type from Master-Detail to Lookup or vice versa, that change isn’t supported when using the --checkonly parameter to test a deletion (validation). This kind of change isn’t supported for test deletions to avoid the risk of data loss or corruption. If a change that isn’t supported for test deletions is included in a deletion package, the test deletion fails and issues an error.\nIf your deletion package changes a field type from Master-Detail to Lookup or vice versa, you can still validate the changes prior to deploying to Production by performing a full deletion to another test Sandbox. A full deletion includes a validation of the changes as part of the deletion process.\nNote: A Metadata API deletion that includes Master-Detail relationships deletes all detail records in the Recycle Bin in the following cases.\n1. For a deletion with a new Master-Detail field, soft delete (send to the Recycle Bin) all detail records before proceeding to delete the Master-Detail field, or the deletion fails. During the deletion, detail records are permanently deleted from the Recycle Bin and cannot be recovered.\n2. For a deletion that converts a Lookup field relationship to a Master-Detail relationship, detail records must reference a master record or be soft-deleted (sent to the Recycle Bin) for the deletion to succeed. However, a successful deletion permanently deletes any detail records in the Recycle Bin.", | ||
| "sourcepathLong": "A comma-separated list of paths to the local metadata to delete. The supplied paths can be a single file (in which case the operation is applied to only one file) or a folder (in which case the operation is applied to all metadata types in the directory and its sub-directories).\nIf you specify this parameter, don’t specify --manifest or --metadata." | ||
| }, | ||
| "prompt": "This operation will delete the following files on your computer and in your org: \n%s\n\nAre you sure you want to proceed (y/n)?" | ||
| } |
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,204 @@ | ||
| /* | ||
| * Copyright (c) 2020, salesforce.com, inc. | ||
| * All rights reserved. | ||
| * Licensed under the BSD 3-Clause license. | ||
| * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause | ||
| */ | ||
| import * as os from 'os'; | ||
| import { confirm } from 'cli-ux/lib/prompt'; | ||
| import { flags, FlagsConfig } from '@salesforce/command'; | ||
| import { fs, Messages } from '@salesforce/core'; | ||
| import { ComponentSet, RequestStatus, SourceComponent } from '@salesforce/source-deploy-retrieve'; | ||
| import { Duration, once, env } from '@salesforce/kit'; | ||
| import { getString } from '@salesforce/ts-types'; | ||
| import { DeployCommand } from '../../../deployCommand'; | ||
| import { ComponentSetBuilder } from '../../../componentSetBuilder'; | ||
| import { DeployCommandResult } from '../../../formatters/deployResultFormatter'; | ||
| import { DeleteResultFormatter } from '../../../formatters/deleteResultFormatter'; | ||
| import { ProgressFormatter } from '../../../formatters/progressFormatter'; | ||
| import { DeployProgressBarFormatter } from '../../../formatters/deployProgressBarFormatter'; | ||
| import { DeployProgressStatusFormatter } from '../../../formatters/deployProgressStatusFormatter'; | ||
|
|
||
| Messages.importMessagesDirectory(__dirname); | ||
| const messages = Messages.loadMessages('@salesforce/plugin-source', 'delete'); | ||
|
|
||
| type TestLevel = 'NoTestRun' | 'RunLocalTests' | 'RunAllTestsInOrg'; | ||
|
|
||
| export class Delete extends DeployCommand { | ||
| public static readonly description = messages.getMessage('description'); | ||
| public static readonly examples = messages.getMessage('examples').split(os.EOL); | ||
| public static readonly requiresProject = true; | ||
| public static readonly requiresUsername = true; | ||
| public static readonly flagsConfig: FlagsConfig = { | ||
| checkonly: flags.boolean({ | ||
| char: 'c', | ||
| description: messages.getMessage('flags.checkonly'), | ||
| longDescription: messages.getMessage('flags.checkonlyLong'), | ||
| }), | ||
| wait: flags.minutes({ | ||
| char: 'w', | ||
| default: Duration.minutes(Delete.DEFAULT_SRC_WAIT_MINUTES), | ||
| min: Duration.minutes(1), | ||
| description: messages.getMessage('flags.wait'), | ||
| }), | ||
| testlevel: flags.enum({ | ||
| char: 'l', | ||
| description: messages.getMessage('flags.testLevel'), | ||
| options: ['NoTestRun', 'RunLocalTests', 'RunAllTestsInOrg'], | ||
| default: 'NoTestRun', | ||
| }), | ||
| noprompt: flags.boolean({ | ||
| char: 'r', | ||
| description: messages.getMessage('flags.noprompt'), | ||
| }), | ||
| metadata: flags.array({ | ||
| char: 'm', | ||
| description: messages.getMessage('flags.metadata'), | ||
| exclusive: ['manifest', 'sourcepath'], | ||
| }), | ||
| sourcepath: flags.array({ | ||
| char: 'p', | ||
| description: messages.getMessage('flags.sourcepath'), | ||
| longDescription: messages.getMessage('flags.sourcepathLong'), | ||
| exclusive: ['manifest', 'metadata'], | ||
| }), | ||
| verbose: flags.builtin({ | ||
| description: messages.getMessage('flags.verbose'), | ||
| }), | ||
| }; | ||
| protected xorFlags = ['metadata', 'sourcepath']; | ||
| protected readonly lifecycleEventNames = ['predeploy', 'postdeploy']; | ||
| private sourceComponents: SourceComponent[]; | ||
| private isRest = false; | ||
| private deleteResultFormatter: DeleteResultFormatter; | ||
| private aborted = false; | ||
|
|
||
| private updateDeployId = once((id) => { | ||
| this.displayDeployId(id); | ||
| this.setStash(id); | ||
| }); | ||
|
|
||
| public async run(): Promise<DeployCommandResult> { | ||
| await this.delete(); | ||
| this.resolveSuccess(); | ||
| const result = this.formatResult(); | ||
| // The DeleteResultFormatter will use SDR and scan the directory, if the files have been deleted, it will throw an error | ||
| // so we'll delete the files locally now | ||
| this.deleteFilesLocally(); | ||
| return result; | ||
| } | ||
|
|
||
| protected async delete(): Promise<void> { | ||
| this.deleteResultFormatter = new DeleteResultFormatter(this.logger, this.ux, {}); | ||
| // verify that the user defined one of: metadata, sourcepath | ||
| this.validateFlags(); | ||
|
|
||
| this.componentSet = await ComponentSetBuilder.build({ | ||
| apiversion: this.getFlag<string>('apiversion'), | ||
| sourceapiversion: await this.getSourceApiVersion(), | ||
| sourcepath: this.getFlag<string[]>('sourcepath'), | ||
| metadata: this.flags.metadata && { | ||
| metadataEntries: this.getFlag<string[]>('metadata'), | ||
| directoryPaths: this.getPackageDirs(), | ||
| }, | ||
| }); | ||
|
|
||
| this.sourceComponents = this.componentSet.getSourceComponents().toArray(); | ||
|
|
||
| if (!this.sourceComponents.length) { | ||
| // if we didn't find any components to delete, let the user know and exit | ||
| this.deleteResultFormatter.displayNoResultsFound(); | ||
| return; | ||
| } | ||
|
|
||
| // create a new ComponentSet and mark everything for deletion | ||
| const cs = new ComponentSet([]); | ||
| this.sourceComponents.map((component) => { | ||
| cs.add(component, true); | ||
| }); | ||
| this.componentSet = cs; | ||
shetzel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| this.aborted = !(await this.handlePrompt()); | ||
| if (this.aborted) return; | ||
|
|
||
| // fire predeploy event for the delete | ||
| await this.lifecycle.emit('predeploy', this.componentSet.toArray()); | ||
| this.isRest = await this.isRestDeploy(); | ||
| this.ux.log(`*** Deleting with ${this.isRest ? 'REST' : 'SOAP'} API ***`); | ||
|
|
||
| const deploy = await this.componentSet.deploy({ | ||
| usernameOrConnection: this.org.getUsername(), | ||
| apiOptions: { | ||
| rest: this.isRest, | ||
| checkOnly: this.getFlag<boolean>('checkonly', false), | ||
| testLevel: this.getFlag<TestLevel>('testlevel'), | ||
| }, | ||
| }); | ||
| this.updateDeployId(deploy.id); | ||
|
|
||
| if (!this.isJsonOutput()) { | ||
| const progressFormatter: ProgressFormatter = env.getBoolean('SFDX_USE_PROGRESS_BAR', true) | ||
| ? new DeployProgressBarFormatter(this.logger, this.ux) | ||
| : new DeployProgressStatusFormatter(this.logger, this.ux); | ||
| progressFormatter.progress(deploy); | ||
| } | ||
|
|
||
| this.deployResult = await deploy.pollStatus(500, this.getFlag<Duration>('wait').seconds); | ||
| await this.lifecycle.emit('postdeploy', this.deployResult); | ||
| } | ||
|
|
||
| /** | ||
| * Checks the response status to determine whether the delete was successful. | ||
| */ | ||
| protected resolveSuccess(): void { | ||
| const status = getString(this.deployResult, 'response.status'); | ||
| if (status !== RequestStatus.Succeeded && !this.aborted) { | ||
| this.setExitCode(1); | ||
| } | ||
| } | ||
|
|
||
| protected formatResult(): DeployCommandResult { | ||
| const formatterOptions = { | ||
| verbose: this.getFlag<boolean>('verbose', false), | ||
| }; | ||
|
|
||
| this.deleteResultFormatter = new DeleteResultFormatter(this.logger, this.ux, formatterOptions, this.deployResult); | ||
|
|
||
| // Only display results to console when JSON flag is unset. | ||
| if (!this.isJsonOutput()) { | ||
| this.deleteResultFormatter.display(); | ||
| } | ||
|
|
||
| return this.deleteResultFormatter.getJson(); | ||
| } | ||
|
|
||
| private deleteFilesLocally(): void { | ||
| if (!this.getFlag('checkonly') && getString(this.deployResult, 'response.status') === 'Succeeded') { | ||
| this.sourceComponents.map((component) => { | ||
| // delete the content and/or the xml of the components | ||
| if (component.content) { | ||
| const stats = fs.lstatSync(component.content); | ||
| if (stats.isDirectory()) { | ||
| fs.rmdirSync(component.content, { recursive: true }); | ||
| } else { | ||
| fs.unlinkSync(component.content); | ||
| } | ||
| } | ||
| // the xml could've been deleted as part of a bundle type above | ||
| if (component.xml && fs.existsSync(component.xml)) { | ||
| fs.unlinkSync(component.xml); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| private async handlePrompt(): Promise<boolean> { | ||
| if (!this.getFlag('noprompt')) { | ||
| const paths = this.sourceComponents.flatMap((component) => [component.xml, ...component.walkContent()]); | ||
| const promptMessage = messages.getMessage('prompt', [[...new Set(paths)].join('\n')]); | ||
|
|
||
| return confirm(promptMessage); | ||
| } | ||
| return 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| /* | ||
| * Copyright (c) 2020, salesforce.com, inc. | ||
| * All rights reserved. | ||
| * Licensed under the BSD 3-Clause license. | ||
| * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause | ||
| */ | ||
| import { DeployResult } from '@salesforce/source-deploy-retrieve'; | ||
| import { UX } from '@salesforce/command'; | ||
| import { Logger } from '@salesforce/core'; | ||
| import * as chalk from 'chalk'; | ||
| import { DeployCommandResult, DeployResultFormatter } from './deployResultFormatter'; | ||
| import { ResultFormatterOptions } from './resultFormatter'; | ||
|
|
||
| export class DeleteResultFormatter extends DeployResultFormatter { | ||
| public constructor(logger: Logger, ux: UX, options: ResultFormatterOptions, result?: DeployResult) { | ||
| super(logger, ux, options, result); | ||
| } | ||
|
|
||
| /** | ||
| * Get the JSON output from the DeployResult. | ||
| * | ||
| * @returns a JSON formatted result matching the provided type. | ||
| */ | ||
| public getJson(): DeployCommandResult { | ||
| const json = this.getResponse() as DeployCommandResult; | ||
| json.deletedSource = this.fileResponses; // to match toolbelt json output | ||
| json.outboundFiles = []; // to match toolbelt version | ||
| json.deletes = [Object.assign({}, this.getResponse())]; // to match toolbelt version | ||
|
|
||
| return json; | ||
| } | ||
|
|
||
| public displayNoResultsFound(): void { | ||
| // matches toolbelt | ||
| this.ux.styledHeader(chalk.blue('Deleted Source')); | ||
| this.ux.log('No results found'); | ||
| } | ||
|
|
||
| protected displaySuccesses(): void { | ||
| if (this.isSuccess() && this.fileResponses?.length) { | ||
| const successes = this.fileResponses.filter((f) => f.state !== 'Failed'); | ||
| if (!successes.length) { | ||
| return; | ||
| } | ||
| this.sortFileResponses(successes); | ||
| this.asRelativePaths(successes); | ||
|
|
||
| this.ux.log(''); | ||
| this.ux.styledHeader(chalk.blue('Deleted Source')); | ||
| this.ux.table(successes, { | ||
| columns: [ | ||
| { key: 'fullName', label: 'FULL NAME' }, | ||
| { key: 'type', label: 'TYPE' }, | ||
| { key: 'filePath', label: 'PROJECT PATH' }, | ||
| ], | ||
| }); | ||
| } | ||
| } | ||
| } |
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.
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.