-
Notifications
You must be signed in to change notification settings - Fork 143
W-16273581 fix: manifests for custom object can omit parent #1375
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
Changes from all commits
80bbc5f
feb8f4e
29f7a24
d9d000a
761b0f6
7b17eb2
a489bab
e6d262d
e671e8a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ import { | |
| SfProject, | ||
| } from '@salesforce/core'; | ||
| import { isString } from '@salesforce/ts-types'; | ||
| import { objectHasSomeRealValues } from '../utils/decomposed'; | ||
| import { MetadataApiDeploy, MetadataApiDeployOptions } from '../client/metadataApiDeploy'; | ||
| import { MetadataApiRetrieve } from '../client/metadataApiRetrieve'; | ||
| import type { MetadataApiRetrieveOptions } from '../client/types'; | ||
|
|
@@ -406,74 +407,60 @@ export class ComponentSet extends LazyCollection<MetadataComponent> { | |
| * @returns Object representation of a package manifest | ||
| */ | ||
| public async getObject(destructiveType?: DestructiveChangesType): Promise<PackageManifestObject> { | ||
| const version = await this.getApiVersion(); | ||
|
|
||
| // If this ComponentSet has components marked for delete, we need to | ||
| // only include those components in a destructiveChanges.xml and | ||
| // all other components in the regular manifest. | ||
| let components = this.components; | ||
| if (this.getTypesOfDestructiveChanges().length) { | ||
| components = destructiveType ? this.destructiveComponents[destructiveType] : this.manifestComponents; | ||
| } | ||
| const components = this.getTypesOfDestructiveChanges().length | ||
| ? destructiveType | ||
| ? this.destructiveComponents[destructiveType] | ||
| : this.manifestComponents | ||
| : this.components; | ||
|
|
||
| const typeMap = new Map<string, string[]>(); | ||
| const typeMap = new Map<string, Set<string>>(); | ||
|
|
||
| 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 && !type.supportsWildcardAndName && !destructiveType) { | ||
| // if the type doesn't support mixed wildcards and specific names, overwrite the names to be a wildcard | ||
| typeMap.set(typeName, [fullName]); | ||
| } else if ( | ||
| typeEntry && | ||
| !typeEntry.includes(fullName) && | ||
| (!typeEntry.includes(ComponentSet.WILDCARD) || type.supportsWildcardAndName) | ||
| ) { | ||
| // if the type supports both wildcards and names, add them regardless | ||
| typeMap.get(typeName)?.push(fullName); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| for (const key of components.keys()) { | ||
| [...components.entries()].map(([key, cmpMap]) => { | ||
| const [typeId, fullName] = splitOnFirstDelimiter(key); | ||
| let type = this.registry.getTypeByName(typeId); | ||
|
|
||
| if (type.folderContentType) { | ||
| type = this.registry.getTypeByName(type.folderContentType); | ||
| } | ||
| addToTypeMap( | ||
| type, | ||
| // they're reassembled like CustomLabels.MyLabel | ||
| this.registry.getParentType(type.name)?.strategies?.recomposition === 'startEmpty' && fullName.includes('.') | ||
| ? fullName.split('.')[1] | ||
| : fullName | ||
| ); | ||
| const type = this.registry.getTypeByName(typeId); | ||
|
|
||
| // Add children | ||
| const componentMap = components.get(key); | ||
| if (componentMap) { | ||
| for (const comp of componentMap.values()) { | ||
| for (const child of comp.getChildren()) { | ||
| addToTypeMap(child.type, child.fullName); | ||
| } | ||
| [...(cmpMap?.values() ?? [])] | ||
| .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" | ||
| if ( | ||
| type.strategies?.transformer === 'decomposed' && | ||
| // exclude (ex: CustomObjectTranslation) where there are no addressable children | ||
| Object.values(type.children?.types ?? {}).some((t) => t.unaddressableWithoutParent !== true) && | ||
| Object.values(type.children?.types ?? {}).some((t) => t.isAddressable !== false) | ||
| ) { | ||
| const parentComp = [...(cmpMap?.values() ?? [])].find((c) => c.fullName === fullName); | ||
| if (parentComp?.xml && !objectHasSomeRealValues(type)(parentComp.parseXmlSync())) { | ||
| return; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| addToTypeMap({ | ||
| typeMap, | ||
| type: type.folderContentType ? this.registry.getTypeByName(type.folderContentType) : type, | ||
| fullName: | ||
| this.registry.getParentType(type.name)?.strategies?.recomposition === 'startEmpty' && fullName.includes('.') | ||
| ? // they're reassembled like CustomLabels.MyLabel | ||
| fullName.split('.')[1] | ||
| : fullName, | ||
| destructiveType, | ||
| }); | ||
| }); | ||
|
|
||
| const typeMembers = Array.from(typeMap.entries()) | ||
| .map(([typeName, members]) => ({ members: members.sort(), name: typeName })) | ||
| .map(([typeName, members]) => ({ members: [...members].sort(), name: typeName })) | ||
| .sort((a, b) => (a.name > b.name ? 1 : -1)); | ||
|
|
||
| return { | ||
| Package: { | ||
| ...{ | ||
| types: typeMembers, | ||
| version, | ||
| version: await this.getApiVersion(), | ||
| }, | ||
| ...(this.fullName ? { fullName: this.fullName } : {}), | ||
| }, | ||
|
|
@@ -750,3 +737,28 @@ const splitOnFirstDelimiter = (input: string): [string, string] => { | |
| const indexOfSplitChar = input.indexOf(KEY_DELIMITER); | ||
| return [input.substring(0, indexOfSplitChar), input.substring(indexOfSplitChar + 1)]; | ||
| }; | ||
|
|
||
| /** side effect: mutates the typeMap property */ | ||
| const addToTypeMap = ({ | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this code is simplified and now uses a set instead of an array |
||
| typeMap, | ||
| type, | ||
| fullName, | ||
| destructiveType, | ||
| }: { | ||
| typeMap: Map<string, Set<string>>; | ||
| type: MetadataType; | ||
| fullName: string; | ||
| destructiveType?: DestructiveChangesType; | ||
| }): void => { | ||
| if (type.isAddressable === false) return; | ||
| if (fullName === ComponentSet.WILDCARD && !type.supportsWildcardAndName && !destructiveType) { | ||
| // if the type doesn't support mixed wildcards and specific names, overwrite the names to be a wildcard | ||
| typeMap.set(type.name, new Set([fullName])); | ||
| return; | ||
| } | ||
| const existing = typeMap.get(type.name) ?? new Set<string>(); | ||
| if (!existing.has(ComponentSet.WILDCARD) || type.supportsWildcardAndName) { | ||
| // if the type supports both wildcards and names, add them regardless | ||
| typeMap.set(type.name, existing.add(fullName)); | ||
| } | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,7 @@ | |
| import { join } from 'node:path'; | ||
| import { JsonMap } from '@salesforce/ts-types'; | ||
| import { Messages } from '@salesforce/core'; | ||
| import { extractUniqueElementValue, getXmlElement } from '../../utils/decomposed'; | ||
| import { extractUniqueElementValue, getXmlElement, unwrapAndOmitNS } from '../../utils/decomposed'; | ||
| import { MetadataComponent } from '../../resolve/types'; | ||
| import { XML_NS_KEY, XML_NS_URL } from '../../common/constants'; | ||
| import { ComponentSet } from '../../collections/componentSet'; | ||
|
|
@@ -199,19 +199,3 @@ const getXmlFromCache = | |
| } | ||
| return xmlCache.get(key) ?? {}; | ||
| }; | ||
|
|
||
| /** composed function, exported from module for test */ | ||
| export const unwrapAndOmitNS = | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. moved for easier sharing |
||
| (outerType: string) => | ||
| (xml: JsonMap): JsonMap => | ||
| omitNsKey(unwrapXml(outerType)(xml)); | ||
|
|
||
| /** Remove the namespace key from the json object. Only the parent needs one */ | ||
| const omitNsKey = (obj: JsonMap): JsonMap => | ||
| Object.fromEntries(Object.entries(obj).filter(([key]) => key !== XML_NS_KEY)) as JsonMap; | ||
|
|
||
| const unwrapXml = | ||
| (outerType: string) => | ||
| (xml: JsonMap): JsonMap => | ||
| // assert that the outerType is also a metadata type name (ex: CustomObject) | ||
| (xml[outerType] as JsonMap) ?? xml; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the children are always added. the parent might or might not be depending on if it has content.
doing the children first lets the parent conditionally return early