Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into devScripts2024-07-28
Browse files Browse the repository at this point in the history
  • Loading branch information
mshanemc committed Jul 30, 2024
2 parents 9ecaf70 + a66f50d commit a6d0e7f
Show file tree
Hide file tree
Showing 9 changed files with 860 additions and 793 deletions.
1,267 changes: 634 additions & 633 deletions METADATA_SUPPORT.md

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions messages/sdr.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,16 @@ If the type is available via Metadata API but not in the registry

- Open an issue <https://github.com/forcedotcom/cli/issues>
- Add the type via PR. Instructions: <https://github.com/forcedotcom/source-deploy-retrieve/blob/main/contributing/metadata.md>

# type_name_suggestions

Confirm the metadata type name is correct. Validate against the registry at:
<https://github.com/forcedotcom/source-deploy-retrieve/blob/main/src/registry/metadataRegistry.json>

If the type is not listed in the registry, check that it has Metadata API support via the Metadata Coverage Report:
<https://developer.salesforce.com/docs/metadata-coverage>

If the type is available via Metadata API but not in the registry

- Open an issue <https://github.com/forcedotcom/cli/issues>
- Add the type via PR. Instructions: <https://github.com/forcedotcom/source-deploy-retrieve/blob/main/contributing/metadata.md>
230 changes: 107 additions & 123 deletions src/collections/componentSetBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,137 +70,132 @@ export class ComponentSetBuilder {
* @param options: options for creating a ComponentSet
*/

// eslint-disable-next-line complexity
public static async build(options: ComponentSetOptions): Promise<ComponentSet> {
const logger = Logger.childFromRoot('componentSetBuilder');
let componentSet: ComponentSet | undefined;

const { sourcepath, manifest, metadata, packagenames, apiversion, sourceapiversion, org, projectDir } = options;
const registryAccess = new RegistryAccess(undefined, projectDir);

try {
if (sourcepath) {
logger.debug(`Building ComponentSet from sourcepath: ${sourcepath.join(', ')}`);
const fsPaths = sourcepath.map(validateAndResolvePath);
componentSet = ComponentSet.fromSource({
fsPaths,
registry: registryAccess,
});
}
const { sourcepath, manifest, metadata, packagenames, org } = options;
const registry = new RegistryAccess(undefined, options.projectDir);

// Return empty ComponentSet and use packageNames in the connection via `.retrieve` options
if (packagenames) {
logger.debug(`Building ComponentSet for packagenames: ${packagenames.toString()}`);
componentSet ??= new ComponentSet(undefined, registryAccess);
}
if (sourcepath) {
logger.debug(`Building ComponentSet from sourcepath: ${sourcepath.join(', ')}`);
const fsPaths = sourcepath.map(validateAndResolvePath);
componentSet = ComponentSet.fromSource({
fsPaths,
registry,
});
}

// Resolve manifest with source in package directories.
if (manifest) {
logger.debug(`Building ComponentSet from manifest: ${manifest.manifestPath}`);
assertFileExists(manifest.manifestPath);

logger.debug(`Searching in packageDir: ${manifest.directoryPaths.join(', ')} for matching metadata`);
componentSet = await ComponentSet.fromManifest({
manifestPath: manifest.manifestPath,
resolveSourcePaths: manifest.directoryPaths,
forceAddWildcards: true,
destructivePre: manifest.destructiveChangesPre,
destructivePost: manifest.destructiveChangesPost,
registry: registryAccess,
});
}
// Return empty ComponentSet and use packageNames in the connection via `.retrieve` options
if (packagenames) {
logger.debug(`Building ComponentSet for packagenames: ${packagenames.toString()}`);
componentSet ??= new ComponentSet(undefined, registry);
}

// Resolve metadata entries with source in package directories.
if (metadata) {
logger.debug(`Building ComponentSet from metadata: ${metadata.metadataEntries.toString()}`);
const directoryPaths = metadata.directoryPaths;
componentSet ??= new ComponentSet(undefined, registryAccess);
const componentSetFilter = new ComponentSet(undefined, registryAccess);

// Build a Set of metadata entries
metadata.metadataEntries
.map(entryToTypeAndName(registryAccess))
.flatMap(typeAndNameToMetadataComponents({ directoryPaths, registry: registryAccess }))
.map(addToComponentSet(componentSet))
.map(addToComponentSet(componentSetFilter));

logger.debug(`Searching for matching metadata in directories: ${directoryPaths.join(', ')}`);

// add destructive changes if defined. Because these are deletes, all entries
// are resolved to SourceComponents
if (metadata.destructiveEntriesPre) {
metadata.destructiveEntriesPre
.map(entryToTypeAndName(registryAccess))
.map(assertNoWildcardInDestructiveEntries)
.flatMap(typeAndNameToMetadataComponents({ directoryPaths, registry: registryAccess }))
.map((mdComponent) => new SourceComponent({ type: mdComponent.type, name: mdComponent.fullName }))
.map(addToComponentSet(componentSet, DestructiveChangesType.PRE));
}
if (metadata.destructiveEntriesPost) {
metadata.destructiveEntriesPost
.map(entryToTypeAndName(registryAccess))
.map(assertNoWildcardInDestructiveEntries)
.flatMap(typeAndNameToMetadataComponents({ directoryPaths, registry: registryAccess }))
.map((mdComponent) => new SourceComponent({ type: mdComponent.type, name: mdComponent.fullName }))
.map(addToComponentSet(componentSet, DestructiveChangesType.POST));
}
// Resolve manifest with source in package directories.
if (manifest) {
logger.debug(`Building ComponentSet from manifest: ${manifest.manifestPath}`);
assertFileExists(manifest.manifestPath);

logger.debug(`Searching in packageDir: ${manifest.directoryPaths.join(', ')} for matching metadata`);
componentSet = await ComponentSet.fromManifest({
manifestPath: manifest.manifestPath,
resolveSourcePaths: manifest.directoryPaths,
forceAddWildcards: true,
destructivePre: manifest.destructiveChangesPre,
destructivePost: manifest.destructiveChangesPost,
registry,
});
}

const resolvedComponents = ComponentSet.fromSource({
fsPaths: directoryPaths,
include: componentSetFilter,
registry: registryAccess,
});

if (resolvedComponents.forceIgnoredPaths) {
// if useFsForceIgnore = true, then we won't be able to resolve a forceignored path,
// which we need to do to get the ignored source component
const resolver = new MetadataResolver(registryAccess, undefined, false);

for (const ignoredPath of resolvedComponents.forceIgnoredPaths ?? []) {
resolver.getComponentsFromPath(ignoredPath).map((ignored) => {
componentSet = componentSet?.filter(
(resolved) => !(resolved.fullName === ignored.name && resolved.type === ignored.type)
);
});
}
componentSet.forceIgnoredPaths = resolvedComponents.forceIgnoredPaths;
}
// Resolve metadata entries with source in package directories.
if (metadata) {
logger.debug(`Building ComponentSet from metadata: ${metadata.metadataEntries.toString()}`);
const directoryPaths = metadata.directoryPaths;
componentSet ??= new ComponentSet(undefined, registry);
const componentSetFilter = new ComponentSet(undefined, registry);

// Build a Set of metadata entries
metadata.metadataEntries
.map(entryToTypeAndName(registry))
.flatMap(typeAndNameToMetadataComponents({ directoryPaths, registry }))
.map(addToComponentSet(componentSet))
.map(addToComponentSet(componentSetFilter));

logger.debug(`Searching for matching metadata in directories: ${directoryPaths.join(', ')}`);

// add destructive changes if defined. Because these are deletes, all entries
// are resolved to SourceComponents
if (metadata.destructiveEntriesPre) {
metadata.destructiveEntriesPre
.map(entryToTypeAndName(registry))
.map(assertNoWildcardInDestructiveEntries)
.flatMap(typeAndNameToMetadataComponents({ directoryPaths, registry }))
.map((mdComponent) => new SourceComponent({ type: mdComponent.type, name: mdComponent.fullName }))
.map(addToComponentSet(componentSet, DestructiveChangesType.PRE));
}
if (metadata.destructiveEntriesPost) {
metadata.destructiveEntriesPost
.map(entryToTypeAndName(registry))
.map(assertNoWildcardInDestructiveEntries)
.flatMap(typeAndNameToMetadataComponents({ directoryPaths, registry }))
.map((mdComponent) => new SourceComponent({ type: mdComponent.type, name: mdComponent.fullName }))
.map(addToComponentSet(componentSet, DestructiveChangesType.POST));
}

resolvedComponents.toArray().map(addToComponentSet(componentSet));
const resolvedComponents = ComponentSet.fromSource({
fsPaths: directoryPaths,
include: componentSetFilter,
registry,
});

if (resolvedComponents.forceIgnoredPaths) {
// if useFsForceIgnore = true, then we won't be able to resolve a forceignored path,
// which we need to do to get the ignored source component
const resolver = new MetadataResolver(registry, undefined, false);

for (const ignoredPath of resolvedComponents.forceIgnoredPaths ?? []) {
resolver.getComponentsFromPath(ignoredPath).map((ignored) => {
componentSet = componentSet?.filter(
(resolved) => !(resolved.fullName === ignored.name && resolved.type === ignored.type)
);
});
}
componentSet.forceIgnoredPaths = resolvedComponents.forceIgnoredPaths;
}

// Resolve metadata entries with an org connection
if (org) {
componentSet ??= new ComponentSet(undefined, registryAccess);
resolvedComponents.toArray().map(addToComponentSet(componentSet));
}

logger.debug(
`Building ComponentSet from targetUsername: ${org.username} ${
metadata ? `filtered by metadata: ${metadata.metadataEntries.toString()}` : ''
}`
);
// Resolve metadata entries with an org connection
if (org) {
componentSet ??= new ComponentSet(undefined, registry);

const mdMap = metadata
? buildMapFromComponents(metadata.metadataEntries.map(entryToTypeAndName(registryAccess)))
: (new Map() as MetadataMap);
logger.debug(
`Building ComponentSet from targetUsername: ${org.username} ${
metadata ? `filtered by metadata: ${metadata.metadataEntries.toString()}` : ''
}`
);

const fromConnection = await ComponentSet.fromConnection({
usernameOrConnection: (await StateAggregator.getInstance()).aliases.getUsername(org.username) ?? org.username,
componentFilter: getOrgComponentFilter(org, mdMap, metadata),
metadataTypes: mdMap.size ? Array.from(mdMap.keys()) : undefined,
registry: registryAccess,
});
const mdMap = metadata
? buildMapFromComponents(metadata.metadataEntries.map(entryToTypeAndName(registry)))
: (new Map() as MetadataMap);

fromConnection.toArray().map(addToComponentSet(componentSet));
}
} catch (e) {
return componentSetBuilderErrorHandler(e);
const fromConnection = await ComponentSet.fromConnection({
usernameOrConnection: (await StateAggregator.getInstance()).aliases.getUsername(org.username) ?? org.username,
componentFilter: getOrgComponentFilter(org, mdMap, metadata),
metadataTypes: mdMap.size ? Array.from(mdMap.keys()) : undefined,
registry,
});

fromConnection.toArray().map(addToComponentSet(componentSet));
}

// there should have been a componentSet created by this point.
componentSet = assertComponentSetIsNotUndefined(componentSet);
componentSet.apiVersion ??= apiversion;
componentSet.sourceApiVersion ??= sourceapiversion;
componentSet.projectDirectory = projectDir;
componentSet.apiVersion ??= options.apiversion;
componentSet.sourceApiVersion ??= options.sourceapiversion;
componentSet.projectDirectory = options.projectDir;

logComponents(logger, componentSet);
return componentSet;
Expand All @@ -214,17 +209,6 @@ const addToComponentSet =
return cmp;
};

const componentSetBuilderErrorHandler = (e: unknown): never => {
if (e instanceof Error && e.message.includes('Missing metadata type definition in registry for id')) {
// to remain generic to catch missing metadata types regardless of parameters, split on '
// example message : Missing metadata type definition in registry for id 'NonExistentType'
const issueType = e.message.split("'")[1];
throw new SfError(`The specified metadata type is unsupported: [${issueType}]`);
} else {
throw e;
}
};

const validateAndResolvePath = (filepath: string): string => path.resolve(assertFileExists(filepath));

const assertFileExists = (filepath: string): string => {
Expand Down
57 changes: 57 additions & 0 deletions src/registry/levenshtein.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* 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 { Messages } from '@salesforce/core/messages';
import * as Levenshtein from 'fast-levenshtein';
import { MetadataRegistry } from './types';

Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr');

/** "did you mean" for Metadata type names */
export const getTypeSuggestions = (registry: MetadataRegistry, typeName: string): string[] => {
const scores = getScores(
Object.values(registry.types).map((t) => t.name),
typeName
);

const guesses = getLowestScores(scores).map((guess) => guess.registryKey);
return [
...(guesses.length
? [
`Did you mean one of the following types? [${guesses.join(',')}]`,
'', // Add a blank line for better readability
]
: []),
messages.getMessage('type_name_suggestions'),
];
};

export const getSuffixGuesses = (suffixes: string[], input: string): string[] => {
const scores = getScores(suffixes, input);
return getLowestScores(scores).map((g) => g.registryKey);
};

type LevenshteinScore = {
registryKey: string;
score: number;
};

const getScores = (choices: string[], input: string): LevenshteinScore[] =>
choices.map((registryKey) => ({
registryKey,
score: Levenshtein.get(input, registryKey, { useCollator: true }),
}));

/** Levenshtein uses positive integers for scores, find all scores that match the lowest score */
const getLowestScores = (scores: LevenshteinScore[]): LevenshteinScore[] => {
const sortedScores = scores.sort(levenshteinSorter);
const lowestScore = scores[0].score;
return sortedScores.filter((score) => score.score === lowestScore);
};

const levenshteinSorter = (a: LevenshteinScore, b: LevenshteinScore): number => a.score - b.score;
36 changes: 13 additions & 23 deletions src/registry/registryAccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { Messages, SfError } from '@salesforce/core';
import * as Levenshtein from 'fast-levenshtein';
import { MetadataRegistry, MetadataType } from './types';
import { getEffectiveRegistry } from './variants';
import { getSuffixGuesses, getTypeSuggestions } from './levenshtein';

/**
* Container for querying metadata registry data.
Expand Down Expand Up @@ -50,7 +50,11 @@ export class RegistryAccess {
);
}
if (!this.registry.types[lower]) {
throw new SfError(messages.getMessage('error_missing_type_definition', [lower]), 'RegistryError');
throw SfError.create({
message: messages.getMessage('error_missing_type_definition', [name]),
name: 'RegistryError',
actions: getTypeSuggestions(this.registry, lower),
});
}
const alias = this.registry.types[lower].aliasFor;
// redirect via alias
Expand Down Expand Up @@ -79,27 +83,13 @@ export class RegistryAccess {
public guessTypeBySuffix(
suffix: string
): Array<{ suffixGuess: string; metadataTypeGuess: MetadataType }> | undefined {
const registryKeys = Object.keys(this.registry.suffixes);

const scores = registryKeys.map((registryKey) => ({
registryKey,
score: Levenshtein.get(suffix, registryKey, { useCollator: true }),
}));
const sortedScores = scores.sort((a, b) => a.score - b.score);
const lowestScore = sortedScores[0].score;
// Levenshtein uses positive integers for scores, find all scores that match the lowest score
const guesses = sortedScores.filter((score) => score.score === lowestScore);

if (guesses.length > 0) {
return guesses.map((guess) => {
const typeId = this.registry.suffixes[guess.registryKey];
const metadataType = this.getTypeByName(typeId);
return {
suffixGuess: guess.registryKey,
metadataTypeGuess: metadataType,
};
});
}
const guesses = getSuffixGuesses(Object.keys(this.registry.suffixes), suffix);
return guesses.length
? guesses.map((guess) => ({
suffixGuess: guess,
metadataTypeGuess: this.getTypeByName(this.registry.suffixes[guess]),
}))
: undefined;
}

/**
Expand Down
Loading

2 comments on commit a6d0e7f

@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: a6d0e7f Previous: 7d7d01c Ratio
eda-componentSetCreate-linux 176 ms 189 ms 0.93
eda-sourceToMdapi-linux 2314 ms 2332 ms 0.99
eda-sourceToZip-linux 1794 ms 1821 ms 0.99
eda-mdapiToSource-linux 2806 ms 2936 ms 0.96
lotsOfClasses-componentSetCreate-linux 358 ms 368 ms 0.97
lotsOfClasses-sourceToMdapi-linux 3639 ms 3740 ms 0.97
lotsOfClasses-sourceToZip-linux 3123 ms 3065 ms 1.02
lotsOfClasses-mdapiToSource-linux 3436 ms 3548 ms 0.97
lotsOfClassesOneDir-componentSetCreate-linux 610 ms 618 ms 0.99
lotsOfClassesOneDir-sourceToMdapi-linux 6375 ms 6550 ms 0.97
lotsOfClassesOneDir-sourceToZip-linux 5586 ms 5696 ms 0.98
lotsOfClassesOneDir-mdapiToSource-linux 6224 ms 6354 ms 0.98

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: a6d0e7f Previous: 7d7d01c Ratio
eda-componentSetCreate-win32 368 ms 428 ms 0.86
eda-sourceToMdapi-win32 4074 ms 4097 ms 0.99
eda-sourceToZip-win32 2698 ms 2915 ms 0.93
eda-mdapiToSource-win32 5708 ms 6084 ms 0.94
lotsOfClasses-componentSetCreate-win32 867 ms 945 ms 0.92
lotsOfClasses-sourceToMdapi-win32 7434 ms 7854 ms 0.95
lotsOfClasses-sourceToZip-win32 4834 ms 4666 ms 1.04
lotsOfClasses-mdapiToSource-win32 7370 ms 7530 ms 0.98
lotsOfClassesOneDir-componentSetCreate-win32 1473 ms 1467 ms 1.00
lotsOfClassesOneDir-sourceToMdapi-win32 13239 ms 13244 ms 1.00
lotsOfClassesOneDir-sourceToZip-win32 8687 ms 9068 ms 0.96
lotsOfClassesOneDir-mdapiToSource-win32 13491 ms 13700 ms 0.98

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

Please sign in to comment.