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

fix: deletes singular CL when from CLs when specified #381

Merged
merged 16 commits into from
May 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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