Skip to content
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

Wr/delete custom labels #613

Merged
merged 20 commits into from
May 18, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
"dependencies": {
"@oclif/core": "^2.8.2",
"@salesforce/apex-node": "^1.6.0",
"@salesforce/core": "^3.34.6",
"@salesforce/core": "^3.36.0",
"@salesforce/kit": "^1.9.2",
"@salesforce/sf-plugins-core": "^2.4.2",
"@salesforce/source-deploy-retrieve": "^8.0.2",
"@salesforce/source-deploy-retrieve": "^8.4.0",
"@salesforce/source-tracking": "^3.1.0",
"chalk": "^4.1.2",
"fs-extra": "^10.0.1",
Expand All @@ -19,7 +19,7 @@
},
"devDependencies": {
"@oclif/plugin-command-snapshot": "^3.3.10",
"@salesforce/cli-plugins-testkit": "^3.3.2",
"@salesforce/cli-plugins-testkit": "^3.3.6",
"@salesforce/dev-config": "^3.1.0",
"@salesforce/dev-scripts": "^4.3.1",
"@salesforce/plugin-command-reference": "^2.4.1",
Expand All @@ -29,7 +29,7 @@
"@salesforce/plugin-templates": "^55.4.10",
"@salesforce/plugin-user": "^2.3.6",
"@salesforce/prettier-config": "^0.0.2",
"@salesforce/source-testkit": "^2.0.43",
"@salesforce/source-testkit": "^2.0.59",
"@salesforce/ts-sinon": "1.4.6",
"@salesforce/ts-types": "^1.5.20",
"@swc/core": "1.3.39",
Expand Down Expand Up @@ -269,4 +269,4 @@
"output": []
}
}
}
}
14 changes: 12 additions & 2 deletions src/commands/project/delete/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
SourceComponent,
} from '@salesforce/source-deploy-retrieve';
import { Duration } from '@salesforce/kit';
import { ChangeResult, ConflictResponse, SourceTracking } from '@salesforce/source-tracking';
import { ChangeResult, ConflictResponse, deleteCustomLabels, SourceTracking } from '@salesforce/source-tracking';
import {
arrayWithDeprecation,
Flags,
Expand Down Expand Up @@ -343,6 +343,13 @@ export class Source extends SfCommand<DeleteSourceJson> {
private async deleteFilesLocally(): Promise<void> {
if (!this.flags['check-only'] && this.deployResult?.response?.status === RequestStatus.Succeeded) {
const promises: Array<Promise<void>> = [];
const customLabels = this.componentSet
.getSourceComponents()
.toArray()
.filter((comp) => comp.type.id === 'customlabel');
if (customLabels.length && customLabels[0].xml) {
promises.push(deleteCustomLabels(customLabels[0].xml, customLabels));
}
this.components?.filter(isSourceComponent).map((component: SourceComponent) => {
// mixed delete/deploy operations have already been deleted and stashed
if (!this.mixedDeployDelete.delete.length) {
Expand All @@ -355,7 +362,10 @@ export class Source extends SfCommand<DeleteSourceJson> {
}
}
if (component.xml) {
promises.push(fsPromises.unlink(component.xml));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for every custom label in the CS, this calls STL.deleteCustomLabels on every label in the set.

Some of those are going to be targeted multiple times?

Maybe you want to do that filter of labels once at the top of the method rather than inside the per-component loop

if (component.type.id !== 'customlabel') {
// CustomLabels handled as a special case above
promises.push(fsPromises.unlink(component.xml));
}
}
}
});
Expand Down
91 changes: 49 additions & 42 deletions src/commands/project/retrieve/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { SfCommand, toHelpSection, Flags } from '@salesforce/sf-plugins-core';
import { getString } from '@salesforce/ts-types';
import { SourceTracking, SourceConflictError } from '@salesforce/source-tracking';
import { Duration } from '@salesforce/kit';
import { MetadataApiRetrieveStatus } from '@salesforce/source-deploy-retrieve/lib/src/client/types';
import { DEFAULT_ZIP_FILE_NAME, ensuredDirFlag, zipFileFlag } from '../../../utils/flags';
import { RetrieveResultFormatter } from '../../../formatters/retrieveResultFormatter';
import { MetadataRetrieveResultFormatter } from '../../../formatters/metadataRetrieveResultFormatter';
Expand Down Expand Up @@ -161,69 +162,75 @@ export default class RetrieveMetadata extends SfCommand<RetrieveResultJson> {
}),
fileResponsesFromDelete: [],
};
// stl sets version based on config/files--if the command overrides it, we need to update
if (isChanges && flags['api-version']) {
componentSetFromNonDeletes.apiVersion = flags['api-version'];
}
this.spinner.status = messages.getMessage('spinner.sending', [
componentSetFromNonDeletes.sourceApiVersion ?? componentSetFromNonDeletes.apiVersion,
]);

this.retrieveResult = new RetrieveResult({} as MetadataApiRetrieveStatus, componentSetFromNonDeletes);
const zipFileName = flags['zip-file-name'] ?? DEFAULT_ZIP_FILE_NAME;
const retrieveOpts: RetrieveSetOptions = {
usernameOrConnection:
flags['target-org'].getUsername() ?? flags['target-org'].getConnection(flags['api-version']),
merge: true,
output: this.project.getDefaultPackage().fullPath,
packageOptions: flags['package-name'],
format,
...(format === 'metadata'
? {
singlePackage: flags['single-package'],
unzip: flags.unzip,
zipFileName,
output: flags['target-metadata-dir'],
}
: {}),
};

const retrieve = await componentSetFromNonDeletes.retrieve(retrieveOpts);
if (componentSetFromNonDeletes.size !== 0) {
// we have changes to retrieve
// stl sets version based on config/files--if the command overrides it, we need to update
if (isChanges && flags['api-version']) {
componentSetFromNonDeletes.apiVersion = flags['api-version'];
}
this.spinner.status = messages.getMessage('spinner.sending', [
componentSetFromNonDeletes.sourceApiVersion ?? componentSetFromNonDeletes.apiVersion,
]);
const retrieveOpts: RetrieveSetOptions = {
usernameOrConnection:
flags['target-org'].getUsername() ?? flags['target-org'].getConnection(flags['api-version']),
merge: true,
output: this.project.getDefaultPackage().fullPath,
packageOptions: flags['package-name'],
format,
...(format === 'metadata'
? {
singlePackage: flags['single-package'],
unzip: flags.unzip,
zipFileName,
output: flags['target-metadata-dir'],
}
: {}),
};

this.spinner.status = messages.getMessage('spinner.polling');
const retrieve = await componentSetFromNonDeletes.retrieve(retrieveOpts);

retrieve.onUpdate((data) => {
this.spinner.status = mdTransferMessages.getMessage(data.status);
});
this.spinner.status = messages.getMessage('spinner.polling');

// any thing else should stop the progress bar
retrieve.onFinish((data) => this.spinner.stop(mdTransferMessages.getMessage(data.response.status)));
retrieve.onUpdate((data) => {
this.spinner.status = mdTransferMessages.getMessage(data.status);
});

retrieve.onCancel((data) => this.spinner.stop(mdTransferMessages.getMessage(data?.status ?? 'Canceled')));
// any thing else should stop the progress bar
retrieve.onFinish((data) => this.spinner.stop(mdTransferMessages.getMessage(data.response.status)));

retrieve.onError((error: Error) => {
this.spinner.stop(error.name);
throw error;
});
retrieve.onCancel((data) => this.spinner.stop(mdTransferMessages.getMessage(data?.status ?? 'Canceled')));

retrieve.onError((error: Error) => {
this.spinner.stop(error.name);
throw error;
});

await retrieve.start();
const result = await retrieve.pollStatus(500, flags.wait.seconds);
await retrieve.start();
this.retrieveResult = await retrieve.pollStatus(500, flags.wait.seconds);
}
this.spinner.stop();

// reference the flag instead of `format` so we get correct type
const formatter = flags['target-metadata-dir']
? new MetadataRetrieveResultFormatter(result, {
? new MetadataRetrieveResultFormatter(this.retrieveResult, {
'target-metadata-dir': flags['target-metadata-dir'],
'zip-file-name': zipFileName,
unzip: flags.unzip,
})
: new RetrieveResultFormatter(result, flags['package-name'], fileResponsesFromDelete);
: new RetrieveResultFormatter(this.retrieveResult, flags['package-name'], fileResponsesFromDelete);
if (!this.jsonEnabled()) {
if (result.response.status === 'Succeeded') {
// in the case where we didn't retrieve anything, check if we have any deletes
if (this.retrieveResult.response.status === 'Succeeded' || fileResponsesFromDelete.length !== 0) {
await formatter.display();
} else {
throw new SfError(
getString(result.response, 'errorMessage', result.response.status),
getString(result.response, 'errorStatusCode', 'unknown')
getString(this.retrieveResult.response, 'errorMessage', this.retrieveResult.response.status),
getString(this.retrieveResult.response, 'errorStatusCode', 'unknown')
);
}
}
Expand Down
58 changes: 57 additions & 1 deletion test/nuts/delete/source.nut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import * as fs from 'fs';
import * as path from 'path';
import { expect } from 'chai';
import { execCmd } from '@salesforce/cli-plugins-testkit';
import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit';
import { SourceTestkit } from '@salesforce/source-testkit';
import { exec } from 'shelljs';
import { FileResponse } from '@salesforce/source-deploy-retrieve';
Expand All @@ -28,6 +28,62 @@ const isNameObsolete = async (username: string, memberType: string, memberName:
return res.IsNameObsolete;
};

describe('CustomLabels', () => {
let testkit: TestSession;

before(async () => {
testkit = await TestSession.create({
project: {
gitClone: 'https://github.com/WillieRuemmele/sfdx-delete-customlabel',
},
scratchOrgs: [{ setDefault: true, config: path.join('config', 'project-scratch-def.json') }],
devhubAuthStrategy: 'AUTO',
});
execCmd('force:source:deploy --sourcepath force-app', { ensureExitCode: 0 });
});

after(async () => {
await testkit?.clean();
});
it('will not delete the entire .xml file', () => {
const clPath = path.join(
testkit.project.dir,
'force-app',
'main',
'default',
'labels',
'CustomLabels.labels-meta.xml'
);
const result = execCmd<DeleteSourceJson>(
'project:delete:source --json --no-prompt --metadata CustomLabel:DeleteMe',
{
ensureExitCode: 0,
}
).jsonOutput?.result;
expect(fs.existsSync(clPath)).to.be.true;
expect(fs.readFileSync(clPath, 'utf8')).to.not.contain('<fullName>DeleteMe</fullName>');
expect(fs.readFileSync(clPath, 'utf8')).to.contain('<fullName>KeepMe1</fullName>');
expect(fs.readFileSync(clPath, 'utf8')).to.contain('<fullName>KeepMe2</fullName>');
expect(result?.deletedSource).to.have.length(1);
});

it('will delete the entire .xml file', () => {
const clPath = path.join(
testkit.project.dir,
'force-app',
'main',
'default',
'labels',
'CustomLabels.labels-meta.xml'
);
const result = execCmd<DeleteSourceJson>('project:delete:source --json --no-prompt --metadata CustomLabels', {
ensureExitCode: 0,
}).jsonOutput?.result;
expect(result?.deletedSource).to.have.length(3);
expect(fs.existsSync(clPath)).to.be.false;
});
});

describe('project delete source NUTs', () => {
let testkit: SourceTestkit;

Expand Down
96 changes: 96 additions & 0 deletions test/nuts/retrieve/customLabels.nut.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* 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 path from 'path';
import * as fs from 'fs';
import { expect } from 'chai';

import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit';
import { AuthInfo, Connection } from '@salesforce/core';
import { DeployResultJson, RetrieveResultJson } from '../../../src/utils/types';

let session: TestSession;

describe('CustomLabel source tracking', () => {
before(async () => {
session = await TestSession.create({
project: {
gitClone: 'https://github.com/WillieRuemmele/sfdx-delete-customlabel',
},
devhubAuthStrategy: 'AUTO',
scratchOrgs: [
{
duration: 1,
setDefault: true,
wait: 10,
config: path.join('config', 'project-scratch-def.json'),
},
],
});
});

after(async () => {
await session?.zip(undefined, 'artifacts');
await session?.clean();
});

it('pushes to initiate the remote', () => {
execCmd<DeployResultJson>('project:deploy:start --json', { ensureExitCode: 0 });
});

it("deletes the 'DeleteMe' CustomLabel", async () => {
const clFile = path.join(
session.project.dir,
'force-app',
'main',
'default',
'labels',
'CustomLabels.labels-meta.xml'
);
const conn = await Connection.create({
authInfo: await AuthInfo.create({
username: session.orgs.get('default')?.username,
}),
});
const id = (
await conn.singleRecordQuery<{ Id: string }>("SELECT Id FROM CustomLabel WHERE name = 'DeleteMe'", {
tooling: true,
})
).Id;
await conn.tooling.sobject('CustomLabel').delete(id);

const result = execCmd<RetrieveResultJson>('project:retrieve:start --json', { ensureExitCode: 0 }).jsonOutput
?.result;
expect(result?.fileProperties).length.to.equal(1);
expect(fs.existsSync(clFile)).to.be.true;
expect(fs.readFileSync(clFile, { encoding: 'utf-8' })).to.not.include('DeleteMe');
expect(fs.readFileSync(clFile, { encoding: 'utf-8' })).to.include('KeepMe1');
});

it('deletes the remaining CustomLabel', async () => {
const clFile = path.join(
session.project.dir,
'force-app',
'main',
'default',
'labels',
'CustomLabels.labels-meta.xml'
);
const conn = await Connection.create({
authInfo: await AuthInfo.create({
username: session.orgs.get('default')?.username,
}),
});
const ids = (await conn.tooling.query<{ Id: string }>('SELECT Id FROM CustomLabel')).records.map((r) => r.Id);
await conn.tooling.sobject('CustomLabel').delete(ids);

const result = execCmd<RetrieveResultJson>('force:source:pull -f', { ensureExitCode: 0 }).shellOutput.stdout;
expect(fs.existsSync(clFile)).to.be.false;
expect(result).to.contain('KeepMe1');
expect(result).to.contain('KeepMe2');
});
});
2 changes: 1 addition & 1 deletion test/nuts/tracking/forceIgnore.nut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ describe('forceignore changes', () => {

it('sf will not retrieve a remote file added to the ignore AFTER it is being tracked', () => {
// pull doesn't retrieve that change
const pullOutput = execCmd<RetrieveResultJson>('retrieve:metadata --json', {
const pullOutput = execCmd<RetrieveResultJson>('project:retrieve:start --json', {
ensureExitCode: 0,
}).jsonOutput?.result;
expect(
Expand Down
6 changes: 4 additions & 2 deletions test/nuts/tracking/remoteChanges.nut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ describe('remote changes', () => {
expect(result?.toDeploy).to.deep.equal([]);
});
it('can pull the delete', () => {
const result = execCmd<RetrieveResultJson>('retrieve:metadata --json', { ensureExitCode: 0 }).jsonOutput?.result;
const result = execCmd<RetrieveResultJson>('project:retrieve:start --json', { ensureExitCode: 0 }).jsonOutput
?.result;
assert(result);
assert(Array.isArray(result.files));
// the 2 files for the apexClass, and possibly one for the Profile (depending on whether it got created in time)
Expand Down Expand Up @@ -212,7 +213,8 @@ describe('remote changes', () => {
).to.equal(true);
});
it('can pull the add', () => {
const result = execCmd<RetrieveResultJson>('retrieve:metadata --json', { ensureExitCode: 0 }).jsonOutput?.result;
const result = execCmd<RetrieveResultJson>('project:retrieve:start --json', { ensureExitCode: 0 }).jsonOutput
?.result;
// SDR marks all retrieves as 'Changed' even if it creates new local files. This is different from toolbelt, which marked those as 'Created'
result?.files
.filter((r) => r.fullName === className)
Expand Down
Loading