Skip to content

Commit

Permalink
fix: deletes singular CL when from CLs when specified (#381)
Browse files Browse the repository at this point in the history
* fix: deletes singular CL when from CLs when specified

* chore: ensure array when deleting last CL in CLs

* chore: delete entire CL when last one deleted, return multiple FileResponses for multiple CL

* chore: use promise array

* chore: small tweak, get CI running

* chore: bump sdr

* chore: bump core

* chore: more bugs caught by extNuts fixed

* chore: move CL delete to static method

* test: add UT for static method

* chore: top level function, other review improvements

* chore: bump testkit

* chore: remove deleting CLs from other methods

* refactor: return file as object for consumers and tests (#388)

* refactor: return file as object for consumers and tests

* chore: dedupe pjson

---------

Co-authored-by: mshanemc <shane.mclaughlin@salesforce.com>
  • Loading branch information
WillieRuemmele and mshanemc authored May 17, 2023
1 parent 4a6703f commit 26fbdb0
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 27 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@salesforce/core": "^3.36.1",
"@salesforce/kit": "^1.9.2",
"@salesforce/source-deploy-retrieve": "^8.4.0",
"fast-xml-parser": "^4.2.2",
"graceful-fs": "^4.2.11",
"isomorphic-git": "1.23.0",
"ts-retry-promise": "^0.7.0"
Expand All @@ -56,6 +57,7 @@
"@salesforce/dev-scripts": "^4.3.0",
"@salesforce/prettier-config": "^0.0.2",
"@salesforce/ts-sinon": "^1.4.6",
"@types/graceful-fs": "^4.1.6",
"@types/shelljs": "^0.8.11",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
Expand Down Expand Up @@ -158,4 +160,4 @@
"output": []
}
}
}
}
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ export {
ConflictResponse,
SourceConflictError,
} from './shared/types';
export { getKeyFromObject } from './shared/functions';
export { getKeyFromObject, deleteCustomLabels } from './shared/functions';
59 changes: 59 additions & 0 deletions src/shared/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
*/

import { sep, normalize, isAbsolute, relative } from 'path';
import * as fs from 'fs';
import { isString } from '@salesforce/ts-types';
import { SourceComponent } from '@salesforce/source-deploy-retrieve';
import { XMLBuilder, XMLParser } from 'fast-xml-parser';
import { ensureArray } from '@salesforce/kit';
import { RemoteChangeElement, ChangeResult } from './types';

export const getMetadataKey = (metadataType: string, metadataName: string): string =>
Expand Down Expand Up @@ -45,3 +48,59 @@ export const chunkArray = <T>(arr: T[], size: number): T[][] =>

export const ensureRelative = (filePath: string, projectPath: string): string =>
isAbsolute(filePath) ? relative(projectPath, filePath) : filePath;

export type ParsedCustomLabels = {
CustomLabels: { labels: Array<{ fullName: string }> };
};

/**
* A method to help delete custom labels from a file, or the entire file if there are no more labels
*
* @param filename - a path to a custom labels file
* @param customLabels - an array of SourceComponents representing the custom labels to delete
* @returns -json equivalent of the custom labels file's contents OR undefined if the file was deleted/not written
*/
export const deleteCustomLabels = async (
filename: string,
customLabels: SourceComponent[]
): Promise<ParsedCustomLabels | undefined> => {
const customLabelsToDelete = customLabels
.filter((label) => label.type.id === 'customlabel')
.map((change) => change.fullName);

// if we don't have custom labels, we don't need to do anything
if (!customLabelsToDelete.length) {
return undefined;
}
// for custom labels, we need to remove the individual label from the xml file
// so we'll parse the xml
const parser = new XMLParser({
ignoreDeclaration: false,
ignoreAttributes: false,
attributeNamePrefix: '@_',
});
const cls = parser.parse(fs.readFileSync(filename, 'utf8')) as ParsedCustomLabels;

// delete the labels from the json based on their fullName's
cls.CustomLabels.labels = ensureArray(cls.CustomLabels.labels).filter(
(label) => !customLabelsToDelete.includes(label.fullName)
);

if (cls.CustomLabels.labels.length === 0) {
// we've deleted everything, so let's delete the file
await fs.promises.unlink(filename);
return undefined;
} else {
// we need to write the file json back to xml back to the fs
const builder = new XMLBuilder({
attributeNamePrefix: '@_',
ignoreAttributes: false,
format: true,
indentBy: ' ',
});
// and then write that json back to xml and back to the fs
const xml = builder.build(cls) as string;
await fs.promises.writeFile(filename, xml);
return cls;
}
};
1 change: 0 additions & 1 deletion src/sourceTracking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import {
} from '@salesforce/source-deploy-retrieve';
// this is not exported by SDR (see the comments in SDR regarding its limitations)
import { filePathsFromMetadataComponent } from '@salesforce/source-deploy-retrieve/lib/src/utils/filePathGenerator';

import { RemoteSourceTrackingService, remoteChangeElementToChangeResult } from './shared/remoteSourceTrackingService';
import { ShadowRepo } from './shared/localShadowRepo';
import { throwIfConflicts, findConflictsInComponentSet, getDedupedConflictsFromChanges } from './shared/conflicts';
Expand Down
151 changes: 151 additions & 0 deletions test/unit/deleteCustomLabels.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* 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 fs from 'fs';
import * as sinon from 'sinon';
import { SourceComponent } from '@salesforce/source-deploy-retrieve';
import { expect } from 'chai';
import { deleteCustomLabels } from '../../src/';

describe('deleteCustomLabels', () => {
const sandbox = sinon.createSandbox();
let fsReadStub: sinon.SinonStub;
let fsWriteStub: sinon.SinonStub;
let fsUnlinkStub: sinon.SinonStub;

beforeEach(() => {
fsWriteStub = sandbox.stub(fs.promises, 'writeFile');
fsUnlinkStub = sandbox.stub(fs.promises, 'unlink');
fsReadStub = sandbox
.stub(fs, 'readFileSync')
.returns(
'<?xml version="1.0" encoding="UTF-8"?>\n' +
'<CustomLabels xmlns="http://soap.sforce.com/2006/04/metadata">\n' +
' <labels>\n' +
' <fullName>DeleteMe</fullName>\n' +
' <language>en_US</language>\n' +
' <protected>true</protected>\n' +
' <shortDescription>DeleteMe</shortDescription>\n' +
' <value>Test</value>\n' +
' </labels>\n' +
' <labels>\n' +
' <fullName>KeepMe1</fullName>\n' +
' <language>en_US</language>\n' +
' <protected>true</protected>\n' +
' <shortDescription>KeepMe1</shortDescription>\n' +
' <value>Test</value>\n' +
' </labels>\n' +
' <labels>\n' +
' <fullName>KeepMe2</fullName>\n' +
' <language>en_US</language>\n' +
' <protected>true</protected>\n' +
' <shortDescription>KeepMe2</shortDescription>\n' +
' <value>Test</value>\n' +
' </labels>\n' +
'</CustomLabels>\n'
);
});

afterEach(() => {
sandbox.restore();
});

describe('deleteCustomLabels', () => {
it('will delete a singular custom label from a file', async () => {
const labels = [
{
type: { id: 'customlabel', name: 'CustomLabel' },
fullName: 'DeleteMe',
} as SourceComponent,
];

const result = await deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels);
const resultLabels = result?.CustomLabels.labels.map((label) => label.fullName);
expect(resultLabels).to.deep.equal(['KeepMe1', 'KeepMe2']);
expect(fsReadStub.callCount).to.equal(1);
});
it('will delete a multiple custom labels from a file', async () => {
const labels = [
{
type: { id: 'customlabel', name: 'CustomLabel' },
fullName: 'KeepMe1',
},
{
type: { id: 'customlabel', name: 'CustomLabel' },
fullName: 'KeepMe2',
},
] as SourceComponent[];

const result = await deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels);
const resultLabels = result?.CustomLabels.labels.map((label) => label.fullName);
expect(resultLabels).to.deep.equal(['DeleteMe']);
expect(fsReadStub.callCount).to.equal(1);
});

it('will delete the file when everything is deleted', async () => {
const labels = [
{
type: { id: 'customlabel', name: 'CustomLabel' },
fullName: 'KeepMe1',
},
{
type: { id: 'customlabel', name: 'CustomLabel' },
fullName: 'KeepMe2',
},
{
type: { id: 'customlabel', name: 'CustomLabel' },
fullName: 'DeleteMe',
},
] as SourceComponent[];

const result = await deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels);
expect(result).to.equal(undefined);
expect(fsUnlinkStub.callCount).to.equal(1);
expect(fsReadStub.callCount).to.equal(1);
});

it('will delete the file when only a single label is present and deleted', async () => {
fsReadStub.returns(
'<?xml version="1.0" encoding="UTF-8"?>\n' +
'<CustomLabels xmlns="http://soap.sforce.com/2006/04/metadata">\n' +
' <labels>\n' +
' <fullName>DeleteMe</fullName>\n' +
' <language>en_US</language>\n' +
' <protected>true</protected>\n' +
' <shortDescription>DeleteMe</shortDescription>\n' +
' <value>Test</value>\n' +
' </labels>\n' +
'</CustomLabels>\n'
);
const labels = [
{
type: { id: 'customlabel', name: 'CustomLabel' },
fullName: 'DeleteMe',
},
] as SourceComponent[];

const result = await deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels);
expect(result).to.equal(undefined);
expect(fsUnlinkStub.callCount).to.equal(1);
expect(fsReadStub.callCount).to.equal(1);
});

it('no custom labels, quick exit and do nothing', async () => {
const labels = [
{
type: { id: 'apexclass', name: 'ApexClass' },
fullName: 'DeleteMe',
},
] as SourceComponent[];

const result = await deleteCustomLabels('labels/CustomLabels.labels-meta.xml', labels);
expect(result).to.equal(undefined);
expect(fsUnlinkStub.callCount).to.equal(0);
expect(fsWriteStub.callCount).to.equal(0);
expect(fsReadStub.callCount).to.equal(0);
});
});
});
25 changes: 1 addition & 24 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -890,30 +890,7 @@
strip-ansi "6.0.1"
ts-retry-promise "^0.7.0"

"@salesforce/core@^3.34.6":
version "3.34.7"
resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-3.34.7.tgz#445efe5c76fbab53e6c891563aa9b0dc5dd24179"
integrity sha512-C4zyXzLAV5ITMChC8dCP+6Kk3t5vloyP2eXpqBOw96OzF5OaCiN5/TayNN8YJl64pvFFny7FgAQPKk7omFXNSA==
dependencies:
"@salesforce/bunyan" "^2.0.0"
"@salesforce/kit" "^1.9.2"
"@salesforce/schemas" "^1.5.1"
"@salesforce/ts-types" "^1.7.2"
"@types/graceful-fs" "^4.1.6"
"@types/semver" "^7.3.13"
ajv "^8.12.0"
archiver "^5.3.0"
change-case "^4.1.2"
debug "^3.2.7"
faye "^1.4.0"
form-data "^4.0.0"
graceful-fs "^4.2.11"
js2xmlparser "^4.0.1"
jsforce "^2.0.0-beta.21"
jsonwebtoken "9.0.0"
ts-retry-promise "^0.7.0"

"@salesforce/core@^3.34.8", "@salesforce/core@^3.36.0", "@salesforce/core@^3.36.1":
"@salesforce/core@^3.34.6", "@salesforce/core@^3.34.8", "@salesforce/core@^3.36.0", "@salesforce/core@^3.36.1":
version "3.36.1"
resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-3.36.1.tgz#053a5e1079b9749b62e461e6ac3e630b5689694a"
integrity sha512-kcjyr9bj35nnL8Bqv8U39xeho3CrZYXJiS/X5X1eEHVNZLd9zckrmKrh1V7z8ElCFpsJrewT989SJsdvi9kE8w==
Expand Down

0 comments on commit 26fbdb0

Please sign in to comment.