Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into sm/registry-from-inte…
Browse files Browse the repository at this point in the history
…rnal-describe
  • Loading branch information
mshanemc committed Aug 15, 2024
2 parents e1edd17 + 7d0821b commit a39b463
Show file tree
Hide file tree
Showing 45 changed files with 1,249 additions and 1,861 deletions.
2,032 changes: 423 additions & 1,609 deletions CHANGELOG.md

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions HANDBOOK.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,16 @@ Be careful when instantiating classes (ex: ComponentSet) that will default a Reg

**Updating presets** If you do need to update a preset to make a breaking change, it's better to copy it to a new preset and give it a unique name (ex: `decomposeFooV2`). This preserves the existing behavior for existing projects with the old preset.

Presets **can** remove strings from the default metadataRegistry by setting values to empty string ex:

```json
{
"childTypes": {
"somethingThatIsUsuallyAChild": ""
}
}
```

### Querying registry data

While it’s perfectly fine to reference the registry export directly, the `RegistryAccess` class was created to make accessing the object a bit more streamlined. Querying types and searching the registry is oftentimes easier and cleaner this way and contains built-in checking for whether or not a metadata type exists. Here’s a comparison of using each:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@salesforce/source-deploy-retrieve",
"version": "12.2.1",
"version": "12.4.0",
"description": "JavaScript library to run Salesforce metadata deploys and retrieves",
"main": "lib/src/index.js",
"author": "Salesforce",
Expand Down
19 changes: 18 additions & 1 deletion src/Presets.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,12 @@ Simple fields (ex: `fullName`) can remain in the top-level `Account.workflow-met

## `decomposeCustomLabelsBeta`

> This will definitely not become GA. Based on user feedback, we replaced it with `decomposeCustomLabelsBeta2`
CustomLabels are decomposed to a folder named `CustomLabels` the labels are then placed into individual files

metadata format
`/labels/CustomLabels.customlabes-meta.xml`
`/labels/CustomLabels.customlabels-meta.xml`

source format

Expand All @@ -77,3 +79,18 @@ source format
/labels/CustomLabels/b.label-meta.xml
/labels/CustomLabels/c.label-meta.xml
```

## `decomposeCustomLabelsBeta2`

CustomLabels are decomposed to a folder named `labels`; the labels are then placed into individual files. There is no top-level file.

metadata format
`/labels/CustomLabels.customlabels-meta.xml`

source format

```txt
/labels/a.label-meta.xml
/labels/b.label-meta.xml
/labels/c.label-meta.xml
```
8 changes: 2 additions & 6 deletions src/collections/componentSetBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,12 +284,8 @@ export const entryToTypeAndName =
// split on the first colon, and then join the rest back together to support names that include colons
const [typeName, ...name] = rawEntry.split(':');
const type = reg.getTypeByName(typeName.trim());
const parent = reg.getParentType(type.name);
// If a user is requesting a child type that is unaddressable (more common with custom registries to create proper behavior)
// throw an error letting them know to use the entire parent instead
// or if they're requesting a COFT, unadressable without parent, don't throw because the parent could be requested - we don't know at this point
if (type.isAddressable === false && parent !== undefined && !type.unaddressableWithoutParent) {
throw new Error(`Cannot use this type, instead use ${parent.name}`);
if (type.name === 'CustomLabels' && type.strategies?.transformer === 'decomposedLabels') {
throw new Error('Use CustomLabel instead of CustomLabels for decomposed labels');
}
return { type, metadataName: name.length ? name.join(':').trim() : '*' };
};
Expand Down
3 changes: 2 additions & 1 deletion src/convert/convertContext/convertContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import { RecompositionFinalizer } from './recompositionFinalizer';
import { NonDecompositionFinalizer } from './nonDecompositionFinalizer';
import { DecompositionFinalizer } from './decompositionFinalizer';
import { ConvertTransactionFinalizer } from './transactionFinalizer';

import { DecomposedLabelsFinalizer } from './decomposedLabelsFinalizer';
/**
* A state manager over the course of a single metadata conversion call.
*/
export class ConvertContext {
public readonly decomposition = new DecompositionFinalizer();
public readonly recomposition = new RecompositionFinalizer();
public readonly nonDecomposition = new NonDecompositionFinalizer();
public readonly decomposedLabels = new DecomposedLabelsFinalizer();

// eslint-disable-next-line @typescript-eslint/require-await
public async *executeFinalizers(defaultDirectory?: string): AsyncIterable<WriterFormat[]> {
Expand Down
76 changes: 76 additions & 0 deletions src/convert/convertContext/decomposedLabelsFinalizer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright (c) 2023, 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 { join } from 'node:path';
import { ensure, JsonMap } from '@salesforce/ts-types';
import type { CustomLabel } from '@jsforce/jsforce-node/lib/api/metadata';
import { customLabelHasFullName } from '../../utils/metadata';
import { MetadataType } from '../../registry';
import { XML_NS_KEY, XML_NS_URL } from '../../common/constants';
import { JsToXml } from '../streams';
import { WriterFormat } from '../types';
import { ConvertTransactionFinalizer } from './transactionFinalizer';

type CustomLabelState = {
/*
* Incoming child xml (CustomLabel) keyed by label fullname
*/
customLabelByFullName: Map<string, CustomLabel>;
};

/**
* Merges child components that share the same parent in the conversion pipeline
* into a single file.
*
* Inserts unclaimed child components into the parent that belongs to the default package
*/
export class DecomposedLabelsFinalizer extends ConvertTransactionFinalizer<CustomLabelState> {
public transactionState: CustomLabelState = {
customLabelByFullName: new Map(),
};

/** to support custom presets (the only way this code should get hit at all pass in the type from a transformer that has registry access */
public customLabelsType?: MetadataType;

// have to maintain the existing interface
// eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars
public async finalize(defaultDirectory?: string): Promise<WriterFormat[]> {
if (this.transactionState.customLabelByFullName.size === 0) {
return [];
}
return [
{
component: {
type: ensure(this.customLabelsType, 'DecomposedCustomLabelsFinalizer should have set customLabelsType'),
fullName: 'CustomLabels',
},
writeInfos: [
{
output: join(
ensure(this.customLabelsType?.directoryName, 'directoryName missing from customLabels type'),
'CustomLabels.labels'
),
source: new JsToXml(generateXml(this.transactionState.customLabelByFullName)),
},
],
},
];
}
}

/** Return a json object that's built up from the mergeMap children */
const generateXml = (children: Map<string, CustomLabel>): JsonMap => ({
['CustomLabels']: {
[XML_NS_KEY]: XML_NS_URL,
// for CustomLabels, that's `labels`
labels: Array.from(children.values()).filter(customLabelHasFullName).sort(sortLabelsByFullName),
},
});

type CustomLabelWithFullName = CustomLabel & { fullName: string };

const sortLabelsByFullName = (a: CustomLabelWithFullName, b: CustomLabelWithFullName): number =>
a.fullName.localeCompare(b.fullName);
3 changes: 1 addition & 2 deletions src/convert/convertContext/recompositionFinalizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { extractUniqueElementValue, getXmlElement, unwrapAndOmitNS } from '../..
import { MetadataComponent } from '../../resolve/types';
import { XML_NS_KEY, XML_NS_URL } from '../../common/constants';
import { ComponentSet } from '../../collections/componentSet';
import { RecompositionStrategy } from '../../registry/types';
import { SourceComponent } from '../../resolve/sourceComponent';
import { JsToXml } from '../streams';
import { WriterFormat } from '../types';
Expand Down Expand Up @@ -127,7 +126,7 @@ const recompose =
const getStartingXml =
(cache: XmlCache) =>
async (parent: SourceComponent): Promise<JsonMap> =>
parent.type.strategies?.recomposition === RecompositionStrategy.StartEmpty
parent.type.strategies?.recomposition === 'startEmpty'
? {}
: unwrapAndOmitNS(parent.type.name)(await getXmlFromCache(cache)(parent)) ?? {};

Expand Down
14 changes: 11 additions & 3 deletions src/convert/streams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,13 @@ export class ComponentConverter extends Transform {
case 'source':
if (mergeWith) {
for (const mergeComponent of mergeWith) {
converts.push(transformer.toSourceFormat(chunk, mergeComponent));
converts.push(
transformer.toSourceFormat({ component: chunk, mergeWith: mergeComponent, mergeSet: this.mergeSet })
);
}
}
if (converts.length === 0) {
converts.push(transformer.toSourceFormat(chunk));
converts.push(transformer.toSourceFormat({ component: chunk, mergeSet: this.mergeSet }));
}
break;
case 'metadata':
Expand Down Expand Up @@ -158,7 +160,13 @@ export class StandardWriter extends ComponentWriter {
}

// if there are children, resolve each file. o/w just pick one of the files to resolve
if (toResolve.size === 0 || chunk.component.type.children) {
// "resolve" means "make these show up in the FileResponses"
if (
toResolve.size === 0 ||
chunk.component.type.children !== undefined ||
// make each decomposed label show up in the fileResponses
chunk.component.type.strategies?.transformer === 'decomposedLabels'
) {
// This is a workaround for a server side ListViews bug where
// duplicate components are sent. W-9614275
if (toResolve.has(info.output)) {
Expand Down
5 changes: 4 additions & 1 deletion src/convert/transformers/baseMetadataTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,8 @@ export abstract class BaseMetadataTransformer implements MetadataTransformer {
}

public abstract toMetadataFormat(component: SourceComponent): Promise<WriteInfo[]>;
public abstract toSourceFormat(component: SourceComponent, mergeWith?: SourceComponent): Promise<WriteInfo[]>;
public abstract toSourceFormat(input: {
component: SourceComponent;
mergeWith?: SourceComponent;
}): Promise<WriteInfo[]>;
}
53 changes: 53 additions & 0 deletions src/convert/transformers/decomposeLabelsTransformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright (c) 2023, 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 type { CustomLabel } from '@jsforce/jsforce-node/lib/api/metadata';
import { ensureArray } from '@salesforce/kit';
import { customLabelHasFullName } from '../../utils/metadata';
import { calculateRelativePath } from '../../utils/path';
import { SourceComponent } from '../../resolve/sourceComponent';
import { ToSourceFormatInput, WriteInfo } from '../types';
import { JsToXml } from '../streams';
import { unwrapAndOmitNS } from '../../utils/decomposed';
import { DefaultMetadataTransformer } from './defaultMetadataTransformer';

/* Use for the metadata type CustomLabels */
export class LabelsMetadataTransformer extends DefaultMetadataTransformer {
/** CustomLabels file => Array of CustomLabel WriteInfo (one for each label) */
public async toSourceFormat({ component, mergeSet }: ToSourceFormatInput): Promise<WriteInfo[]> {
const labelType = this.registry.getTypeByName('CustomLabel');
const partiallyAppliedPathCalculator = calculateRelativePath('source')({
self: labelType,
});
const xml = unwrapAndOmitNS('CustomLabels')(await component.parseXml()) as { labels: CustomLabel | CustomLabel[] };
return ensureArray(xml.labels) // labels could parse to a single object and not an array if there's only 1 label
.filter(customLabelHasFullName)
.map((l) => ({
// split each label into a separate label file
output:
// if present in the merge set, use that xml path, otherwise use the default path
mergeSet?.getComponentFilenamesByNameAndType({ fullName: l.fullName, type: labelType.name })?.[0] ??
partiallyAppliedPathCalculator(l.fullName)(`${l.fullName}.label-meta.xml`),
source: new JsToXml({ CustomLabel: l }),
}));
}
}

/* Use for the metadata type CustomLabel */
export class LabelMetadataTransformer extends DefaultMetadataTransformer {
public async toMetadataFormat(component: SourceComponent): Promise<WriteInfo[]> {
// only need to do this once
this.context.decomposedLabels.customLabelsType ??= this.registry.getTypeByName('CustomLabels');
this.context.decomposedLabels.transactionState.customLabelByFullName.set(
component.fullName,
unwrapAndOmitNS('CustomLabel')(await component.parseXml()) as CustomLabel
);
return [];
}

// toSourceFormat uses the default (merge them with the existing label)
}
8 changes: 4 additions & 4 deletions src/convert/transformers/decomposedMetadataTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ import { calculateRelativePath } from '../../utils/path';
import { ForceIgnore } from '../../resolve/forceIgnore';
import { extractUniqueElementValue, objectHasSomeRealValues } from '../../utils/decomposed';
import type { MetadataComponent } from '../../resolve/types';
import { DecompositionStrategy, type MetadataType } from '../../registry/types';
import { type MetadataType } from '../../registry/types';
import { SourceComponent } from '../../resolve/sourceComponent';
import { JsToXml } from '../streams';
import type { WriteInfo, XmlObj } from '../types';
import type { ToSourceFormatInput, WriteInfo, XmlObj } from '../types';
import { META_XML_SUFFIX, XML_NS_KEY, XML_NS_URL } from '../../common/constants';
import type { SourcePath } from '../../common/types';
import { ComponentSet } from '../../collections/componentSet';
Expand Down Expand Up @@ -60,7 +60,7 @@ export class DecomposedMetadataTransformer extends BaseMetadataTransformer {
return [];
}

public async toSourceFormat(component: SourceComponent, mergeWith?: SourceComponent): Promise<WriteInfo[]> {
public async toSourceFormat({ component, mergeWith }: ToSourceFormatInput): Promise<WriteInfo[]> {
const forceIgnore = component.getForceIgnore();

// if the whole parent is ignored, we won't worry about decomposing things
Expand Down Expand Up @@ -265,7 +265,7 @@ const getDefaultOutput = (component: MetadataComponent): SourcePath => {
// there could be a '.' inside the child name (ex: PermissionSet.FieldPermissions.field uses Obj__c.Field__c)
const childName = tail.length ? tail.join('.') : undefined;
const output = join(
parent?.type.strategies?.decomposition === DecompositionStrategy.FolderPerType ? type.directoryName : '',
parent?.type.strategies?.decomposition === 'folderPerType' ? type.directoryName : '',
`${childName ?? baseName}.${ensureString(component.type.suffix)}${META_XML_SUFFIX}`
);
return join(calculateRelativePath('source')({ self: parent?.type ?? type })(fullName)(baseName), output);
Expand Down
8 changes: 7 additions & 1 deletion src/convert/transformers/defaultMetadataTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,13 @@ export class DefaultMetadataTransformer extends BaseMetadataTransformer {
}

// eslint-disable-next-line @typescript-eslint/require-await, class-methods-use-this
public async toSourceFormat(component: SourceComponent, mergeWith?: SourceComponent): Promise<WriteInfo[]> {
public async toSourceFormat({
component,
mergeWith,
}: {
component: SourceComponent;
mergeWith?: SourceComponent;
}): Promise<WriteInfo[]> {
return getWriteInfos(component, 'source', mergeWith);
}
}
Expand Down
14 changes: 9 additions & 5 deletions src/convert/transformers/metadataTransformerFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import { MetadataTransformer } from '../types';
import { SourceComponent } from '../../resolve/sourceComponent';
import { ConvertContext } from '../convertContext/convertContext';
import { RegistryAccess } from '../../registry/registryAccess';
import { TransformerStrategy } from '../../registry/types';
import { DefaultMetadataTransformer } from './defaultMetadataTransformer';
import { DecomposedMetadataTransformer } from './decomposedMetadataTransformer';
import { StaticResourceMetadataTransformer } from './staticResourceMetadataTransformer';
import { NonDecomposedMetadataTransformer } from './nonDecomposedMetadataTransformer';
import { LabelMetadataTransformer, LabelsMetadataTransformer } from './decomposeLabelsTransformer';

Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr');
Expand All @@ -32,15 +32,19 @@ export class MetadataTransformerFactory {
const type = component.parent ? component.parent.type : component.type;
const transformerId = type.strategies?.transformer;
switch (transformerId) {
case TransformerStrategy.Standard:
case 'standard':
case undefined:
return new DefaultMetadataTransformer(this.registry, this.context);
case TransformerStrategy.Decomposed:
case 'decomposed':
return new DecomposedMetadataTransformer(this.registry, this.context);
case TransformerStrategy.StaticResource:
case 'staticResource':
return new StaticResourceMetadataTransformer(this.registry, this.context);
case TransformerStrategy.NonDecomposed:
case 'nonDecomposed':
return new NonDecomposedMetadataTransformer(this.registry, this.context);
case 'decomposedLabels':
return component.type.name === 'CustomLabels'
? new LabelsMetadataTransformer(this.registry, this.context)
: new LabelMetadataTransformer(this.registry, this.context);
default:
throw messages.createError('error_missing_transformer', [type.name, transformerId]);
}
Expand Down
5 changes: 2 additions & 3 deletions src/convert/transformers/nonDecomposedMetadataTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
import { get, getString, JsonMap } from '@salesforce/ts-types';
import { ensureArray } from '@salesforce/kit';
import { Messages } from '@salesforce/core';
import { WriteInfo } from '../types';
import { SourceComponent } from '../../resolve/sourceComponent';
import { ToSourceFormatInput, WriteInfo } from '../types';
import { DecomposedMetadataTransformer } from './decomposedMetadataTransformer';
Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr');
Expand All @@ -22,7 +21,7 @@ const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sd
export class NonDecomposedMetadataTransformer extends DecomposedMetadataTransformer {
// streams uses mergeWith for all types. Removing it would break the interface
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public async toSourceFormat(component: SourceComponent, mergeWith?: SourceComponent): Promise<WriteInfo[]> {
public async toSourceFormat({ component, mergeWith }: ToSourceFormatInput): Promise<WriteInfo[]> {
// this will only include the incoming (retrieved) labels, not the local file
const parentXml = await component.parseXml();
const xmlPathToChildren = `${component.type.name}.${component.type.directoryName}`;
Expand Down
4 changes: 2 additions & 2 deletions src/convert/transformers/staticResourceMetadataTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { createWriteStream } from 'graceful-fs';
import { Logger, Messages, SfError } from '@salesforce/core';
import { isEmpty } from '@salesforce/kit';
import { baseName } from '../../utils/path';
import { WriteInfo } from '../types';
import { ToSourceFormatInput, WriteInfo } from '../types';
import { SourceComponent } from '../../resolve/sourceComponent';
import { SourcePath } from '../../common/types';
import { ensureFileExists } from '../../utils/fileSystemHandler';
Expand Down Expand Up @@ -97,7 +97,7 @@ export class StaticResourceMetadataTransformer extends BaseMetadataTransformer {
];
}

public async toSourceFormat(component: SourceComponent, mergeWith?: SourceComponent): Promise<WriteInfo[]> {
public async toSourceFormat({ component, mergeWith }: ToSourceFormatInput): Promise<WriteInfo[]> {
const { xml, content } = component;

if (!content) {
Expand Down
Loading

2 comments on commit a39b463

@svc-cli-bot
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Benchmark suite Current: a39b463 Previous: 11fb229 Ratio
eda-componentSetCreate-linux 236 ms 234 ms 1.01
eda-sourceToMdapi-linux 2407 ms 2417 ms 1.00
eda-sourceToZip-linux 1893 ms 1862 ms 1.02
eda-mdapiToSource-linux 2900 ms 2832 ms 1.02
lotsOfClasses-componentSetCreate-linux 427 ms 433 ms 0.99
lotsOfClasses-sourceToMdapi-linux 3695 ms 3632 ms 1.02
lotsOfClasses-sourceToZip-linux 3171 ms 3097 ms 1.02
lotsOfClasses-mdapiToSource-linux 3604 ms 3567 ms 1.01
lotsOfClassesOneDir-componentSetCreate-linux 749 ms 756 ms 0.99
lotsOfClassesOneDir-sourceToMdapi-linux 6539 ms 6482 ms 1.01
lotsOfClassesOneDir-sourceToZip-linux 5633 ms 5607 ms 1.00
lotsOfClassesOneDir-mdapiToSource-linux 6531 ms 6477 ms 1.01

This comment was automatically generated by workflow using github-action-benchmark.

@svc-cli-bot
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Benchmark suite Current: a39b463 Previous: 11fb229 Ratio
eda-componentSetCreate-win32 614 ms 624 ms 0.98
eda-sourceToMdapi-win32 4219 ms 4211 ms 1.00
eda-sourceToZip-win32 3113 ms 2872 ms 1.08
eda-mdapiToSource-win32 5766 ms 5612 ms 1.03
lotsOfClasses-componentSetCreate-win32 1180 ms 1161 ms 1.02
lotsOfClasses-sourceToMdapi-win32 7601 ms 7496 ms 1.01
lotsOfClasses-sourceToZip-win32 5052 ms 4955 ms 1.02
lotsOfClasses-mdapiToSource-win32 7604 ms 7558 ms 1.01
lotsOfClassesOneDir-componentSetCreate-win32 2068 ms 2076 ms 1.00
lotsOfClassesOneDir-sourceToMdapi-win32 13557 ms 13490 ms 1.00
lotsOfClassesOneDir-sourceToZip-win32 9109 ms 8863 ms 1.03
lotsOfClassesOneDir-mdapiToSource-win32 13824 ms 13761 ms 1.00

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.