Skip to content
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"node": ">=18.0.0"
},
"dependencies": {
"@salesforce/core": "^8.11.0",
"@salesforce/core": "^8.11.4",
"@salesforce/kit": "^3.2.3",
"@salesforce/ts-types": "^2.0.12",
"@salesforce/types": "^1.3.0",
Expand Down
13 changes: 12 additions & 1 deletion src/collections/componentSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ export class ComponentSet extends LazyCollection<MetadataComponent> {
// all components stored here, regardless of what manifest they belong to
private components = new DecodeableMap<string, DecodeableMap<string, SourceComponent>>();

// whether this component set is being used for a deploy
// @ts-expect-error this is currently not used but could be used in the future
private forDeploy = false;
// whether this component set is being used for a retrieve
private forRetrieve = false;

// internal component maps used by this.getObject() when building manifests.
private destructiveComponents = {
[DestructiveChangesType.PRE]: new DecodeableMap<string, DecodeableMap<string, SourceComponent>>(),
Expand Down Expand Up @@ -348,6 +354,8 @@ export class ComponentSet extends LazyCollection<MetadataComponent> {
throw new SfError(messages.getMessage('error_no_source_to_deploy'), 'ComponentSetError');
}

this.forDeploy = true;

if (
typeof options.usernameOrConnection !== 'string' &&
this.apiVersion &&
Expand Down Expand Up @@ -384,6 +392,8 @@ export class ComponentSet extends LazyCollection<MetadataComponent> {
apiVersion: this.apiVersion,
});

this.forRetrieve = true;

if (
typeof options.usernameOrConnection !== 'string' &&
this.apiVersion &&
Expand Down Expand Up @@ -427,8 +437,9 @@ export class ComponentSet extends LazyCollection<MetadataComponent> {
.flatMap((c) => c.getChildren())
.map((child) => addToTypeMap({ typeMap, type: child.type, fullName: child.fullName, destructiveType }));

// logic: if this is a decomposed type, skip its inclusion in the manifest if the parent is "empty"
// logic: if this is a decomposed type not being retrieved, skip its inclusion in the manifest if the parent is "empty"
if (
!this.forRetrieve &&
type.strategies?.transformer === 'decomposed' &&
// exclude (ex: CustomObjectTranslation) where there are no addressable children
Object.values(type.children?.types ?? {}).some((t) => t.unaddressableWithoutParent !== true) &&
Expand Down
26 changes: 25 additions & 1 deletion test/collections/componentSet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -980,7 +980,7 @@ describe('ComponentSet', () => {
]);
});

it('omits empty parents from the package manifest', async () => {
it('omits empty parents from the package manifest when not a retrieve', async () => {
const set = new ComponentSet([
DECOMPOSED_CHILD_COMPONENT_1_EMPTY,
DECOMPOSED_CHILD_COMPONENT_2_EMPTY,
Expand All @@ -999,6 +999,30 @@ describe('ComponentSet', () => {
},
]);
});

it('does not omit empty parents from the package manifest for retrieves', async () => {
const set = new ComponentSet([
DECOMPOSED_CHILD_COMPONENT_1_EMPTY,
DECOMPOSED_CHILD_COMPONENT_2_EMPTY,
DECOMPOSED_COMPONENT_EMPTY,
]);
// @ts-expect-error modifying private property
set.forRetrieve = true;
expect((await set.getObject()).Package.types).to.deep.equal([
{
name: DECOMPOSED_CHILD_COMPONENT_1_EMPTY.type.name,
members: [DECOMPOSED_CHILD_COMPONENT_1_EMPTY.fullName],
},
{
name: DECOMPOSED_COMPONENT_EMPTY.type.name,
members: [DECOMPOSED_COMPONENT_EMPTY.fullName],
},
{
name: DECOMPOSED_CHILD_COMPONENT_2_EMPTY.type.name,
members: [DECOMPOSED_CHILD_COMPONENT_2_EMPTY.fullName],
},
]);
});
});

describe('getPackageXml', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
<fields>
<fullName>Title__c</fullName>
<externalId>false</externalId>
<label>Title</label>
<length>30</length>
<required>false</required>
<trackTrending>false</trackTrending>
<type>Text</type>
<unique>false</unique>
</fields>
</CustomObject>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<types>
<members>Broker__c.Title__c</members>
<name>CustomField</name>
</types>
<version>59.0</version>
</Package>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
<fields>
<fullName>Title__c</fullName>
<externalId>false</externalId>
<label>Title</label>
<length>30</length>
<required>false</required>
<trackTrending>false</trackTrending>
<type>Text</type>
<unique>false</unique>
</fields>
</CustomObject>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<types>
<members>Broker__c.Title__c</members>
<name>CustomField</name>
</types>
<types>
<members>Broker__c</members>
<name>CustomObject</name>
</types>
<version>59.0</version>
</Package>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
<fields>
<fullName>Title__c</fullName>
<externalId>false</externalId>
<label>Title</label>
<length>30</length>
<required>false</required>
<trackTrending>false</trackTrending>
<type>Text</type>
<unique>false</unique>
</fields>
</CustomObject>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
<types>
<members>Broker__c.Title__c</members>
<name>CustomField</name>
</types>
<version>63.0</version>
</Package>
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ import * as path from 'node:path';
import {
FORCE_APP,
MDAPI_OUT,
dirEntsToPaths,
dirsAreIdentical,
fileSnap,
mdapiToSource,
sourceToMdapi,
} from '../../helper/conversions';
import { MetadataConverter } from '../../../../src/convert/metadataConverter';
import { ComponentSetBuilder } from '../../../../src/collections/componentSetBuilder';

// we don't want failing tests outputting over each other
/* eslint-disable no-await-in-loop */
Expand Down Expand Up @@ -49,3 +52,138 @@ describe('Custom objects and children', () => {
]);
});
});

/** Return only the files involved in the conversion */
const getConvertedFilePaths = async (outputDir: string): Promise<string[]> =>
dirEntsToPaths(
await fs.promises.readdir(outputDir, {
recursive: true,
withFileTypes: true,
})
);

describe('CustomField with empty CustomObject - retrieve', () => {
const testDir = path.join('test', 'snapshot', 'sampleProjects', 'customObjects-and-children');

// The directory of snapshots containing expected conversion results
const snapshotsDir = path.join(testDir, '__snapshots__');

// MDAPI format of the original source
const sourceDir = path.join(testDir, 'originalMdapi2');

// The directory where metadata is converted as part of retrieve testing
const retrieveOutput = path.join(testDir, 'retrieveOutput');

// This test verifies that 1 custom field with an empty parent
// does not omit the parent from the package manifest for a retrieve when
// converting from source to mdapi.
it('verify md files retrieve', async () => {
const cs = await ComponentSetBuilder.build({
metadata: {
metadataEntries: ['CustomObject:Broker__c'],
directoryPaths: [sourceDir],
},
projectDir: testDir,
});

const sourceOutputDir = path.join(retrieveOutput, 'source');
const mdOutputDir = path.join(retrieveOutput, 'mdapi');

await new MetadataConverter().convert(cs, 'source', {
type: 'directory',
outputDirectory: sourceOutputDir,
genUniqueDir: false,
});

const cs2 = await ComponentSetBuilder.build({
metadata: {
metadataEntries: ['CustomObject:Broker__c'],
directoryPaths: [sourceOutputDir],
},
projectDir: testDir,
});

// @ts-expect-error modifying private property
cs2.forRetrieve = true;

await new MetadataConverter().convert(cs2, 'metadata', {
type: 'directory',
outputDirectory: mdOutputDir,
genUniqueDir: false,
});

const convertedFiles = await getConvertedFilePaths(mdOutputDir);
for (const file of convertedFiles) {
await fileSnap(file, testDir);
}
const expectedOutputDir = path.join(snapshotsDir, 'verify-md-files-retrieve.expected', 'retrieveOutput', 'mdapi');
await dirsAreIdentical(expectedOutputDir, mdOutputDir);
});

after(async () => {
await Promise.all([fs.promises.rm(retrieveOutput, { recursive: true, force: true })]);
});
});

describe('CustomField with empty CustomObject - deploy', () => {
const testDir = path.join('test', 'snapshot', 'sampleProjects', 'customObjects-and-children');

// The directory of snapshots containing expected conversion results
const snapshotsDir = path.join(testDir, '__snapshots__');

// MDAPI format of the original source
const sourceDir = path.join(testDir, 'originalMdapi2');

// The directory where metadata is converted as part of deploy testing
const deployOutput = path.join(testDir, 'deployOutput');

// This test verifies that 1 custom field with an empty parent
// omits the parent from the package manifest for a deploy when
// converting from source to mdapi.
it('verify md files deploy', async () => {
const cs = await ComponentSetBuilder.build({
metadata: {
metadataEntries: ['CustomObject:Broker__c'],
directoryPaths: [sourceDir],
},
projectDir: testDir,
});

const sourceOutputDir = path.join(deployOutput, 'source');
const mdOutputDir = path.join(deployOutput, 'mdapi');

await new MetadataConverter().convert(cs, 'source', {
type: 'directory',
outputDirectory: sourceOutputDir,
genUniqueDir: false,
});

const cs2 = await ComponentSetBuilder.build({
metadata: {
metadataEntries: ['CustomObject:Broker__c'],
directoryPaths: [sourceOutputDir],
},
projectDir: testDir,
});

// @ts-expect-error modifying private property
cs2.forDeploy = true;

await new MetadataConverter().convert(cs2, 'metadata', {
type: 'directory',
outputDirectory: mdOutputDir,
genUniqueDir: false,
});

const convertedFiles = await getConvertedFilePaths(mdOutputDir);
for (const file of convertedFiles) {
await fileSnap(file, testDir);
}
const expectedOutputDir = path.join(snapshotsDir, 'verify-md-files-deploy.expected', 'deployOutput', 'mdapi');
await dirsAreIdentical(expectedOutputDir, mdOutputDir);
});

after(async () => {
await Promise.all([fs.promises.rm(deployOutput, { recursive: true, force: true })]);
});
});
Loading
Loading