diff --git a/package.json b/package.json index 1bb34f6a98..eccb531299 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/collections/componentSet.ts b/src/collections/componentSet.ts index ffce5bb427..9089387b01 100644 --- a/src/collections/componentSet.ts +++ b/src/collections/componentSet.ts @@ -81,6 +81,12 @@ export class ComponentSet extends LazyCollection { // all components stored here, regardless of what manifest they belong to private components = new DecodeableMap>(); + // 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>(), @@ -348,6 +354,8 @@ export class ComponentSet extends LazyCollection { throw new SfError(messages.getMessage('error_no_source_to_deploy'), 'ComponentSetError'); } + this.forDeploy = true; + if ( typeof options.usernameOrConnection !== 'string' && this.apiVersion && @@ -384,6 +392,8 @@ export class ComponentSet extends LazyCollection { apiVersion: this.apiVersion, }); + this.forRetrieve = true; + if ( typeof options.usernameOrConnection !== 'string' && this.apiVersion && @@ -427,8 +437,9 @@ export class ComponentSet extends LazyCollection { .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) && diff --git a/test/collections/componentSet.test.ts b/test/collections/componentSet.test.ts index a595d71814..18055ce86b 100644 --- a/test/collections/componentSet.test.ts +++ b/test/collections/componentSet.test.ts @@ -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, @@ -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', () => { diff --git a/test/snapshot/sampleProjects/customObjects-and-children/__snapshots__/verify-md-files-deploy.expected/deployOutput/mdapi/objects/Broker__c.object b/test/snapshot/sampleProjects/customObjects-and-children/__snapshots__/verify-md-files-deploy.expected/deployOutput/mdapi/objects/Broker__c.object new file mode 100644 index 0000000000..b548e844da --- /dev/null +++ b/test/snapshot/sampleProjects/customObjects-and-children/__snapshots__/verify-md-files-deploy.expected/deployOutput/mdapi/objects/Broker__c.object @@ -0,0 +1,13 @@ + + + + Title__c + false + + 30 + false + false + Text + false + + diff --git a/test/snapshot/sampleProjects/customObjects-and-children/__snapshots__/verify-md-files-deploy.expected/deployOutput/mdapi/package.xml b/test/snapshot/sampleProjects/customObjects-and-children/__snapshots__/verify-md-files-deploy.expected/deployOutput/mdapi/package.xml new file mode 100644 index 0000000000..d5ae24300e --- /dev/null +++ b/test/snapshot/sampleProjects/customObjects-and-children/__snapshots__/verify-md-files-deploy.expected/deployOutput/mdapi/package.xml @@ -0,0 +1,8 @@ + + + + Broker__c.Title__c + CustomField + + 59.0 + diff --git a/test/snapshot/sampleProjects/customObjects-and-children/__snapshots__/verify-md-files-retrieve.expected/retrieveOutput/mdapi/objects/Broker__c.object b/test/snapshot/sampleProjects/customObjects-and-children/__snapshots__/verify-md-files-retrieve.expected/retrieveOutput/mdapi/objects/Broker__c.object new file mode 100644 index 0000000000..b548e844da --- /dev/null +++ b/test/snapshot/sampleProjects/customObjects-and-children/__snapshots__/verify-md-files-retrieve.expected/retrieveOutput/mdapi/objects/Broker__c.object @@ -0,0 +1,13 @@ + + + + Title__c + false + + 30 + false + false + Text + false + + diff --git a/test/snapshot/sampleProjects/customObjects-and-children/__snapshots__/verify-md-files-retrieve.expected/retrieveOutput/mdapi/package.xml b/test/snapshot/sampleProjects/customObjects-and-children/__snapshots__/verify-md-files-retrieve.expected/retrieveOutput/mdapi/package.xml new file mode 100644 index 0000000000..5824824f46 --- /dev/null +++ b/test/snapshot/sampleProjects/customObjects-and-children/__snapshots__/verify-md-files-retrieve.expected/retrieveOutput/mdapi/package.xml @@ -0,0 +1,12 @@ + + + + Broker__c.Title__c + CustomField + + + Broker__c + CustomObject + + 59.0 + diff --git a/test/snapshot/sampleProjects/customObjects-and-children/originalMdapi2/objects/Broker__c.object b/test/snapshot/sampleProjects/customObjects-and-children/originalMdapi2/objects/Broker__c.object new file mode 100644 index 0000000000..b548e844da --- /dev/null +++ b/test/snapshot/sampleProjects/customObjects-and-children/originalMdapi2/objects/Broker__c.object @@ -0,0 +1,13 @@ + + + + Title__c + false + + 30 + false + false + Text + false + + diff --git a/test/snapshot/sampleProjects/customObjects-and-children/originalMdapi2/package.xml b/test/snapshot/sampleProjects/customObjects-and-children/originalMdapi2/package.xml new file mode 100644 index 0000000000..ff353e1677 --- /dev/null +++ b/test/snapshot/sampleProjects/customObjects-and-children/originalMdapi2/package.xml @@ -0,0 +1,8 @@ + + + + Broker__c.Title__c + CustomField + + 63.0 + diff --git a/test/snapshot/sampleProjects/customObjects-and-children/snapshots.test.ts b/test/snapshot/sampleProjects/customObjects-and-children/snapshots.test.ts index 8c9524e2b0..617cf7876d 100644 --- a/test/snapshot/sampleProjects/customObjects-and-children/snapshots.test.ts +++ b/test/snapshot/sampleProjects/customObjects-and-children/snapshots.test.ts @@ -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 */ @@ -49,3 +52,138 @@ describe('Custom objects and children', () => { ]); }); }); + +/** Return only the files involved in the conversion */ +const getConvertedFilePaths = async (outputDir: string): Promise => + 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 })]); + }); +}); diff --git a/yarn.lock b/yarn.lock index cd7030c568..3765fa1c39 100644 --- a/yarn.lock +++ b/yarn.lock @@ -604,6 +604,22 @@ node-fetch "^2.6.1" xml2js "^0.6.2" +"@jsforce/jsforce-node@^3.8.2": + version "3.8.2" + resolved "https://registry.yarnpkg.com/@jsforce/jsforce-node/-/jsforce-node-3.8.2.tgz#68b903f6733ae479086ab02ea4a2de87a7f208eb" + integrity sha512-ewaRr9JnZRW6I28C/TzUnv5p70zMrWsKCq2ovRW6X557/ikdfvA24F9k4cQXZnTG2lZLEfVn+WwdBGEtY7pPnQ== + dependencies: + "@sindresorhus/is" "^4" + base64url "^3.0.1" + csv-parse "^5.5.2" + csv-stringify "^6.4.4" + faye "^1.4.0" + form-data "^4.0.0" + https-proxy-agent "^5.0.0" + multistream "^3.1.0" + node-fetch "^2.6.1" + xml2js "^0.6.2" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -670,12 +686,12 @@ semver "^7.6.3" ts-retry-promise "^0.8.1" -"@salesforce/core@^8.11.0": - version "8.11.0" - resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-8.11.0.tgz#23d5ddcc318008230258ab449e70a26f671123c2" - integrity sha512-S4UgHKUy1hykRQVaoYm+LSktQgRhI3ltAUoLGI25/Q8gYokERTa2E7MpPMb+X/kTpjJJvDlnQqelB/sQJs/AKA== +"@salesforce/core@^8.11.4": + version "8.11.4" + resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-8.11.4.tgz#e4f049351540a46e12bb5c1b6574232fd4d1288b" + integrity sha512-6jrACrCmpic7mrnp4XQ6tiyx5FvHs101dQ2v+m8+aHF97036bul+GeeYuSjVp3ASh0sjR5CotYf7R65chd4H+A== dependencies: - "@jsforce/jsforce-node" "^3.8.1" + "@jsforce/jsforce-node" "^3.8.2" "@salesforce/kit" "^3.2.2" "@salesforce/schemas" "^1.9.0" "@salesforce/ts-types" "^2.0.10" @@ -687,9 +703,9 @@ js2xmlparser "^4.0.1" jsonwebtoken "9.0.2" jszip "3.10.1" - pino "^9.4.0" + pino "^9.7.0" pino-abstract-transport "^1.2.0" - pino-pretty "^11.2.2" + pino-pretty "^11.3.0" proper-lockfile "^4.1.2" semver "^7.6.3" ts-retry-promise "^0.8.1" @@ -4589,6 +4605,13 @@ pino-abstract-transport@^1.0.0, pino-abstract-transport@^1.2.0: readable-stream "^4.0.0" split2 "^4.0.0" +pino-abstract-transport@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz#de241578406ac7b8a33ce0d77ae6e8a0b3b68a60" + integrity sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw== + dependencies: + split2 "^4.0.0" + pino-pretty@^11.2.2: version "11.2.2" resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-11.2.2.tgz#5e8ec69b31e90eb187715af07b1d29a544e60d39" @@ -4609,6 +4632,26 @@ pino-pretty@^11.2.2: sonic-boom "^4.0.1" strip-json-comments "^3.1.1" +pino-pretty@^11.3.0: + version "11.3.0" + resolved "https://registry.yarnpkg.com/pino-pretty/-/pino-pretty-11.3.0.tgz#390b3be044cf3d2e9192c7d19d44f6b690468f2e" + integrity sha512-oXwn7ICywaZPHmu3epHGU2oJX4nPmKvHvB/bwrJHlGcbEWaVcotkpyVHMKLKmiVryWYByNp0jpgAcXpFJDXJzA== + dependencies: + colorette "^2.0.7" + dateformat "^4.6.3" + fast-copy "^3.0.2" + fast-safe-stringify "^2.1.1" + help-me "^5.0.0" + joycon "^3.1.1" + minimist "^1.2.6" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^2.0.0" + pump "^3.0.0" + readable-stream "^4.0.0" + secure-json-parse "^2.4.0" + sonic-boom "^4.0.1" + strip-json-comments "^3.1.1" + pino-std-serializers@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz#7c625038b13718dbbd84ab446bd673dc52259e3b" @@ -4631,6 +4674,23 @@ pino@^9.4.0: sonic-boom "^4.0.1" thread-stream "^3.0.0" +pino@^9.7.0: + version "9.7.0" + resolved "https://registry.yarnpkg.com/pino/-/pino-9.7.0.tgz#ff7cd86eb3103ee620204dbd5ca6ffda8b53f645" + integrity sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg== + dependencies: + atomic-sleep "^1.0.0" + fast-redact "^3.1.1" + on-exit-leak-free "^2.1.0" + pino-abstract-transport "^2.0.0" + pino-std-serializers "^7.0.0" + process-warning "^5.0.0" + quick-format-unescaped "^4.0.3" + real-require "^0.2.0" + safe-stable-stringify "^2.3.1" + sonic-boom "^4.0.1" + thread-stream "^3.0.0" + pkg-dir@^4.1.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" @@ -4683,6 +4743,11 @@ process-warning@^4.0.0: resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-4.0.0.tgz#581e3a7a1fb456c5f4fd239f76bce75897682d5a" integrity sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw== +process-warning@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-5.0.0.tgz#566e0bf79d1dff30a72d8bbbe9e8ecefe8d378d7" + integrity sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA== + process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"