Skip to content

Commit 6016bb9

Browse files
committed
feat: did you mean when types aren't in Registry
1 parent c4eedfb commit 6016bb9

File tree

4 files changed

+101
-23
lines changed

4 files changed

+101
-23
lines changed

messages/sdr.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,16 @@ If the type is available via Metadata API but not in the registry
190190

191191
- Open an issue <https://github.com/forcedotcom/cli/issues>
192192
- Add the type via PR. Instructions: <https://github.com/forcedotcom/source-deploy-retrieve/blob/main/contributing/metadata.md>
193+
194+
# type_name_suggestions
195+
196+
Confirm the metadata type name is correct. Validate against the registry at:
197+
<https://github.com/forcedotcom/source-deploy-retrieve/blob/main/src/registry/metadataRegistry.json>
198+
199+
If the type is not listed in the registry, check that it has Metadata API support via the Metadata Coverage Report:
200+
<https://developer.salesforce.com/docs/metadata-coverage>
201+
202+
If the type is available via Metadata API but not in the registry
203+
204+
- Open an issue <https://github.com/forcedotcom/cli/issues>
205+
- Add the type via PR. Instructions: <https://github.com/forcedotcom/source-deploy-retrieve/blob/main/contributing/metadata.md>

src/registry/levenshtein.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright (c) 2023, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
import { Messages } from '@salesforce/core/messages';
9+
import * as Levenshtein from 'fast-levenshtein';
10+
import { MetadataRegistry } from './types';
11+
12+
Messages.importMessagesDirectory(__dirname);
13+
const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr');
14+
15+
/** "did you mean" for Metadata type names */
16+
export const getTypeSuggestions = (registry: MetadataRegistry, typeName: string): string[] => {
17+
const scores = getScores(
18+
Object.values(registry.types).map((t) => t.name),
19+
typeName
20+
);
21+
22+
const guesses = getLowestScores(scores);
23+
return guesses.length
24+
? [
25+
'Did you mean one of the following types?',
26+
...guesses.map((guess) => guess.registryKey),
27+
...messages.getMessages('type_name_suggestions'),
28+
]
29+
: messages.getMessages('type_name_suggestions');
30+
};
31+
32+
export const getSuffixGuesses = (suffixes: string[], input: string): string[] => {
33+
const scores = getScores(suffixes, input);
34+
return getLowestScores(scores).map((g) => g.registryKey);
35+
};
36+
37+
type LevenshteinScore = {
38+
registryKey: string;
39+
score: number;
40+
};
41+
42+
const getScores = (choices: string[], input: string): LevenshteinScore[] =>
43+
choices.map((registryKey) => ({
44+
registryKey,
45+
score: Levenshtein.get(input, registryKey, { useCollator: true }),
46+
}));
47+
48+
/** Levenshtein uses positive integers for scores, find all scores that match the lowest score */
49+
const getLowestScores = (scores: LevenshteinScore[]): LevenshteinScore[] => {
50+
const sortedScores = scores.sort(levenshteinSorter);
51+
const lowestScore = scores[0].score;
52+
return sortedScores.filter((score) => score.score === lowestScore);
53+
};
54+
55+
const levenshteinSorter = (a: LevenshteinScore, b: LevenshteinScore): number => a.score - b.score;

src/registry/registryAccess.ts

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77
import { Messages, SfError } from '@salesforce/core';
8-
import * as Levenshtein from 'fast-levenshtein';
98
import { MetadataRegistry, MetadataType } from './types';
109
import { getEffectiveRegistry } from './variants';
10+
import { getSuffixGuesses, getTypeSuggestions } from './levenshtein';
1111

1212
/**
1313
* Container for querying metadata registry data.
@@ -50,7 +50,11 @@ export class RegistryAccess {
5050
);
5151
}
5252
if (!this.registry.types[lower]) {
53-
throw new SfError(messages.getMessage('error_missing_type_definition', [lower]), 'RegistryError');
53+
throw SfError.create({
54+
message: messages.getMessage('error_missing_type_definition', [lower]),
55+
name: 'RegistryError',
56+
actions: getTypeSuggestions(this.registry, lower),
57+
});
5458
}
5559
const alias = this.registry.types[lower].aliasFor;
5660
// redirect via alias
@@ -79,27 +83,13 @@ export class RegistryAccess {
7983
public guessTypeBySuffix(
8084
suffix: string
8185
): Array<{ suffixGuess: string; metadataTypeGuess: MetadataType }> | undefined {
82-
const registryKeys = Object.keys(this.registry.suffixes);
83-
84-
const scores = registryKeys.map((registryKey) => ({
85-
registryKey,
86-
score: Levenshtein.get(suffix, registryKey, { useCollator: true }),
87-
}));
88-
const sortedScores = scores.sort((a, b) => a.score - b.score);
89-
const lowestScore = sortedScores[0].score;
90-
// Levenshtein uses positive integers for scores, find all scores that match the lowest score
91-
const guesses = sortedScores.filter((score) => score.score === lowestScore);
92-
93-
if (guesses.length > 0) {
94-
return guesses.map((guess) => {
95-
const typeId = this.registry.suffixes[guess.registryKey];
96-
const metadataType = this.getTypeByName(typeId);
97-
return {
98-
suffixGuess: guess.registryKey,
99-
metadataTypeGuess: metadataType,
100-
};
101-
});
102-
}
86+
const guesses = getSuffixGuesses(Object.keys(this.registry.suffixes), suffix);
87+
return guesses.length
88+
? guesses.map((guess) => ({
89+
suffixGuess: guess,
90+
metadataTypeGuess: this.getTypeByName(this.registry.suffixes[guess]),
91+
}))
92+
: undefined;
10393
}
10494

10595
/**

test/registry/registryAccess.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,26 @@ describe('RegistryAccess', () => {
4343
messages.getMessage('error_missing_type_definition', ['typewithoutdef'])
4444
);
4545
});
46+
47+
describe('suggestions for type name', () => {
48+
it('should provide suggestions for unresolvable types that are close', () => {
49+
try {
50+
registryAccess.getTypeByName('Worflow');
51+
} catch (e) {
52+
assert(e instanceof SfError);
53+
expect(e.actions).to.have.length.greaterThan(0);
54+
expect(e.actions).to.deep.include('Workflow');
55+
}
56+
});
57+
it('should provide several suggestions for unresolvable types that are nowhere', () => {
58+
try {
59+
registryAccess.getTypeByName('&&&&&&');
60+
} catch (e) {
61+
assert(e instanceof SfError);
62+
expect(e.actions).to.have.length.greaterThan(1);
63+
}
64+
});
65+
});
4666
});
4767

4868
describe('getTypeBySuffix', () => {

0 commit comments

Comments
 (0)