diff --git a/src/collections/componentSet.ts b/src/collections/componentSet.ts index 16da2c45e2..dcba217840 100644 --- a/src/collections/componentSet.ts +++ b/src/collections/componentSet.ts @@ -30,7 +30,7 @@ import { import { LazyCollection } from './lazyCollection'; import { j2xParser } from 'fast-xml-parser'; import { Logger } from '@salesforce/core'; -import { RegistryAccess } from '../registry'; +import { MetadataType, RegistryAccess } from '../registry'; export type DeploySetOptions = Omit; export type RetrieveSetOptions = Omit; @@ -269,11 +269,21 @@ export class ComponentSet extends LazyCollection { const typeMap = new Map(); - const addToTypeMap = (typeName: string, fullName: string): void => { - if (!typeMap.has(typeName)) { - typeMap.set(typeName, []); + const addToTypeMap = (type: MetadataType, fullName: string): void => { + if (type.isAddressable !== false) { + const typeName = type.name; + if (!typeMap.has(typeName)) { + typeMap.set(typeName, []); + } + const typeEntry = typeMap.get(typeName); + if (fullName === ComponentSet.WILDCARD) { + typeMap.set(typeName, [fullName]); + } else { + if (!typeEntry.includes(fullName) && !typeEntry.includes(ComponentSet.WILDCARD)) { + typeMap.get(typeName).push(fullName); + } + } } - typeMap.get(typeName).push(fullName); }; for (const key of components.keys()) { @@ -283,17 +293,13 @@ export class ComponentSet extends LazyCollection { if (type.folderContentType) { type = this.registry.getTypeByName(type.folderContentType); } - if (type.isAddressable !== false) { - addToTypeMap(type.name, fullName); - } + addToTypeMap(type, fullName); // Add children const componentMap = components.get(key); for (const comp of componentMap.values()) { for (const child of comp.getChildren()) { - if (child.isAddressable) { - addToTypeMap(child.type.name, child.fullName); - } + addToTypeMap(child.type, child.fullName); } } } diff --git a/src/registry/registry.json b/src/registry/registry.json index 7f00ee8c9a..26ef65e261 100644 --- a/src/registry/registry.json +++ b/src/registry/registry.json @@ -21,6 +21,7 @@ "customlabel": { "id": "customlabel", "name": "CustomLabel", + "xmlElementName": "labels", "ignoreParentName": true, "uniqueIdElement": "fullName", "directoryName": "labels", @@ -599,42 +600,56 @@ "workflowfieldupdate": { "id": "workflowfieldupdate", "name": "WorkflowFieldUpdate", + "xmlElementName": "fieldUpdates", + "uniqueIdElement": "fullName", "directoryName": "workflowFieldUpdates", "suffix": "workflowFieldUpdate" }, "workflowknowledgepublish": { "id": "workflowknowledgepublish", "name": "WorkflowKnowledgePublish", + "xmlElementName": "knowledgePublishes", + "uniqueIdElement": "fullName", "directoryName": "workflowKnowledgePublishs", "suffix": "workflowKnowledgePublish" }, "workflowtask": { "id": "workflowtask", "name": "WorkflowTask", + "xmlElementName": "tasks", + "uniqueIdElement": "fullName", "directoryName": "workflowTasks", "suffix": "workflowTask" }, "workflowalert": { "id": "workflowalert", "name": "WorkflowAlert", + "xmlElementName": "alerts", + "uniqueIdElement": "fullName", "directoryName": "workflowAlerts", "suffix": "workflowAlert" }, "workflowsend": { "id": "workflowsend", "name": "WorkflowSend", + "xmlElementName": "sends", + "uniqueIdElement": "fullName", "directoryName": "workflowSends", "suffix": "workflowSend" }, "workflowoutboundmessage": { "id": "workflowoutboundmessage", "name": "WorkflowOutboundMessage", + "xmlElementName": "outboundMessages", + "uniqueIdElement": "fullName", "directoryName": "workflowOutboundMessages", "suffix": "workflowOutboundMessage" }, "workflowrule": { "id": "workflowrule", "name": "WorkflowRule", + "xmlElementName": "rules", + "uniqueIdElement": "fullName", "directoryName": "workflowRules", "suffix": "workflowRule" } @@ -670,6 +685,8 @@ "assignmentrule": { "id": "assignmentrule", "name": "AssignmentRule", + "xmlElementName": "assignmentRule", + "uniqueIdElement": "fullName", "directoryName": "assignmentRules", "suffix": "assignmentRule" } @@ -694,6 +711,8 @@ "autoresponserule": { "id": "autoresponserule", "name": "AutoResponseRule", + "xmlElementName": "autoResponseRule", + "uniqueIdElement": "fullName", "directoryName": "autoResponseRules", "suffix": "autoResponseRule" } @@ -718,6 +737,8 @@ "escalationrule": { "id": "escalationrule", "name": "EscalationRule", + "xmlElementName": "escalationRule", + "uniqueIdElement": "fullName", "directoryName": "escalationRules", "suffix": "escalationRule" } @@ -929,6 +950,8 @@ "matchingrule": { "id": "matchingrule", "name": "MatchingRule", + "xmlElementName": "matchingRules", + "uniqueIdElement": "fullName", "directoryName": "matchingRules", "suffix": "matchingRule" } @@ -1134,24 +1157,32 @@ "sharingownerrule": { "id": "sharingownerrule", "name": "SharingOwnerRule", + "xmlElementName": "sharingOwnerRules", + "uniqueIdElement": "fullName", "directoryName": "sharingOwnerRules", "suffix": "sharingOwnerRule" }, "sharingcriteriarule": { "id": "sharingcriteriarule", "name": "SharingCriteriaRule", + "xmlElementName": "sharingCriteriaRules", + "uniqueIdElement": "fullName", "directoryName": "sharingCriteriaRules", "suffix": "sharingCriteriaRule" }, "sharingguestrule": { "id": "sharingguestrule", "name": "SharingGuestRule", + "xmlElementName": "sharingGuestRules", + "uniqueIdElement": "fullName", "directoryName": "sharingGuestRules", "suffix": "sharingGuestRule" }, "sharingterritoryrule": { "id": "sharingterritoryrule", "name": "SharingTerritoryRule", + "xmlElementName": "sharingTerritoryRules", + "uniqueIdElement": "fullName", "directoryName": "sharingTerritoryRules", "suffix": "sharingTerritoryRule" } @@ -1350,6 +1381,8 @@ "managedtopic": { "id": "managedtopic", "name": "ManagedTopic", + "xmlElementName": "managedTopic", + "uniqueIdElement": "fullName", "directoryName": "managedTopics", "suffix": "managedTopic" } diff --git a/src/registry/types.ts b/src/registry/types.ts index bf8d456374..3dffa1fcae 100644 --- a/src/registry/types.ts +++ b/src/registry/types.ts @@ -74,6 +74,10 @@ export interface MetadataType { * If the parent name should be ignored when constructing the type's fullName */ ignoreParentName?: boolean; + /** + * The XML element name for the type in the xml file used for constructing child components. + */ + xmlElementName?: string; /** * When converting deploying source, this will update the suffix in the output or temporary directory (metadata format) * Use this, along with additional suffix keys in the registry, to support incorrect suffixes from existing code diff --git a/src/resolve/sourceComponent.ts b/src/resolve/sourceComponent.ts index 397de583a6..2558cc8390 100644 --- a/src/resolve/sourceComponent.ts +++ b/src/resolve/sourceComponent.ts @@ -201,15 +201,17 @@ export class SourceComponent implements MetadataComponent { return children; } + // Get the children for non-decomposed types that have an xmlElementName + // and uniqueIdElement defined in the registry. + // E.g., CustomLabels, Workflows, SharingRules, AssignmentRules. private getNonDecomposedChildren(): SourceComponent[] { - // this method only applies to customlabels type const parsed = this.parseXmlSync(); - const xmlPathToChildren = `${this.type.name}.${this.type.directoryName}`; const children: SourceComponent[] = []; for (const childTypeId of Object.keys(this.type.children.types)) { const childType = this.type.children.types[childTypeId]; const uniqueIdElement = childType.uniqueIdElement; if (uniqueIdElement) { + const xmlPathToChildren = `${this.type.name}.${childType.xmlElementName}`; const elements = normalizeToArray(get(parsed, xmlPathToChildren, [])); const childComponents = elements.map((element) => { return new SourceComponent( diff --git a/test/collections/componentSet.test.ts b/test/collections/componentSet.test.ts index 3aa9fc1e08..30c6970808 100644 --- a/test/collections/componentSet.test.ts +++ b/test/collections/componentSet.test.ts @@ -29,6 +29,7 @@ import { decomposedtoplevel, matchingContentFile, } from '../mock/registry'; +import { MATCHING_RULES_COMPONENT } from '../mock/registry/type-constants/nonDecomposedConstants'; import * as manifestFiles from '../mock/registry/manifestConstants'; const env = createSandbox(); @@ -419,6 +420,14 @@ describe('ComponentSet', () => { ]); }); + it('should include required child types as defined in the registry', () => { + const set = new ComponentSet([MATCHING_RULES_COMPONENT]); + expect(set.getObject().Package.types).to.deep.equal([ + { name: MATCHING_RULES_COMPONENT.type.name, members: [MATCHING_RULES_COMPONENT.name] }, + { name: 'MatchingRule', members: ['MatchingRules.My_Account_Matching_Rule'] }, + ]); + }); + it('should exclude components that are not addressable as defined in the registry', () => { const type: MetadataType = { id: 'customfieldtranslation', diff --git a/test/mock/registry/mockRegistry.ts b/test/mock/registry/mockRegistry.ts index 48f9ebcf45..aa8c9a7b6e 100644 --- a/test/mock/registry/mockRegistry.ts +++ b/test/mock/registry/mockRegistry.ts @@ -124,6 +124,7 @@ export const mockRegistryData = { nondecomposedchild: { id: 'nondecomposedchild', name: 'nondecomposedchild', + xmlElementName: 'nondecomposed', ignoreParentName: true, uniqueIdElement: 'id', directoryName: 'nondecomposed', diff --git a/test/resolve/sourceComponent.test.ts b/test/resolve/sourceComponent.test.ts index cf513fc87e..4e34b05f39 100644 --- a/test/resolve/sourceComponent.test.ts +++ b/test/resolve/sourceComponent.test.ts @@ -23,14 +23,16 @@ import { VIRTUAL_DIR, COMPONENT_1_XML_PATH, CHILD_2_NAME, - MATCHING_RULES_COMPONENT, + MATCHING_RULES_TYPE, + MATCHING_RULES_COMPONENT_XML_PATH, + TREE, } from '../mock/registry/type-constants/nonDecomposedConstants'; import { createSandbox } from 'sinon'; -import { MetadataType } from '../../src'; import { join } from 'path'; import { DecomposedSourceAdapter } from '../../src/resolve/adapters'; import { TypeInferenceError } from '../../src/errors'; import { nls } from '../../src/i18n'; +import { MetadataType } from '../../src'; const env = createSandbox(); @@ -312,7 +314,18 @@ describe('SourceComponent', () => { // https://github.com/forcedotcom/salesforcedx-vscode/issues/3210 it('should return empty children for types that do not have uniqueIdElement but xmlPathToChildren returns elements', () => { - expect(MATCHING_RULES_COMPONENT.getChildren()).to.deep.equal([]); + const noUniqueIdElementType: MetadataType = JSON.parse(JSON.stringify(MATCHING_RULES_TYPE)); + // remove the uniqueElementType for this test + delete noUniqueIdElementType.children.types.matchingrule.uniqueIdElement; + const noUniqueIdElement_Component = new SourceComponent( + { + name: noUniqueIdElementType.name, + type: noUniqueIdElementType, + xml: MATCHING_RULES_COMPONENT_XML_PATH, + }, + TREE + ); + expect(noUniqueIdElement_Component.getChildren()).to.deep.equal([]); }); }); });