Skip to content

Commit

Permalink
feat: build ComponentSet from metadata and org connection
Browse files Browse the repository at this point in the history
  • Loading branch information
shetzel committed Nov 30, 2023
1 parent 77816d9 commit 0c9d568
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 13 deletions.
2 changes: 1 addition & 1 deletion src/collections/componentSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ export class ComponentSet extends LazyCollection<MetadataComponent> {
}
}

const connectionResolver = new ConnectionResolver(usernameOrConnection, options.registry);
const connectionResolver = new ConnectionResolver(usernameOrConnection, options.registry, options.metadataTypes);
const manifest = await connectionResolver.resolve(options.componentFilter);
const result = new ComponentSet([], options.registry);
result.apiVersion = manifest.apiVersion;
Expand Down
50 changes: 42 additions & 8 deletions src/collections/componentSetBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { StateAggregator, Logger, SfError, Messages } from '@salesforce/core';
import * as fs from 'graceful-fs';
import { ComponentSet } from '../collections';
import { RegistryAccess } from '../registry';
import { FileProperties } from '../client';

Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/source-deploy-retrieve', 'sdr');
Expand Down Expand Up @@ -57,6 +58,11 @@ export class ComponentSetBuilder {
const logger = Logger.childFromRoot('componentSetBuilder');
let componentSet: ComponentSet | undefined;

// A map used when building a ComponentSet from metadata type/name pairs
// key = a metadata type
// value = an array of metadata names
const mdMap = new Map<string, string[]>();

const { sourcepath, manifest, metadata, packagenames, apiversion, sourceapiversion, org } = options;
try {
if (sourcepath) {
Expand Down Expand Up @@ -106,6 +112,15 @@ export class ComponentSetBuilder {
const splitEntry = rawEntry.split(':').map((entry) => entry.trim());
// The registry will throw if it doesn't know what this type is.
registry.getTypeByName(splitEntry[0]);

// Add metadata entries to a map for possible use with an org connection below.
const mdMapEntry = mdMap.get(splitEntry[0]);
if (mdMapEntry) {
mdMapEntry.push(splitEntry[1]);
} else {
mdMap.set(splitEntry[0], [splitEntry[1]]);
}

// this '.*' is a surprisingly valid way to specify a metadata, especially a DEB :sigh:
// https://github.com/salesforcecli/plugin-deploy-retrieve/blob/main/test/nuts/digitalExperienceBundle/constants.ts#L140
// because we're filtering from what we have locally, this won't allow you to retrieve new metadata (on the server only) using the partial wildcard
Expand All @@ -119,7 +134,7 @@ export class ComponentSetBuilder {
})
.getSourceComponents()
.toArray()
.filter((cs) => Boolean(cs.fullName.match(new RegExp(splitEntry[1]))))
.filter((cs) => new RegExp(splitEntry[1]).test(cs.fullName))
.map((match) => {
compSetFilter.add(match);
componentSet?.add(match);
Expand Down Expand Up @@ -147,15 +162,34 @@ export class ComponentSetBuilder {
// Resolve metadata entries with an org connection
if (org) {
componentSet ??= new ComponentSet();
logger.debug(`Building ComponentSet from targetUsername: ${org.username}`);

let debugMsg = `Building ComponentSet from targetUsername: ${org.username}`;

// *** Default Filter ***
// exclude components based on the results of componentFilter function
// components with namespacePrefix where org.exclude includes manageableState (to exclude managed packages)
// components with namespacePrefix where manageableState equals undefined (to exclude components e.g. InstalledPackage)
// components where org.exclude includes manageableState (to exclude packages without namespacePrefix e.g. unlocked packages)
let componentFilter = (component: Partial<FileProperties>): boolean =>
!component?.manageableState || !org.exclude?.includes(component.manageableState);

if (metadata) {
debugMsg += ` filtered by metadata: ${metadata.metadataEntries.toString()}`;

componentFilter = (component: Partial<FileProperties>): boolean => {
if (component.type && component.fullName) {
const mdMapEntry = mdMap.get(component.type);
return !!mdMapEntry && mdMapEntry.some((mdName) => new RegExp(mdName).test(component.fullName as string));
}
return false;
};
}

logger.debug(debugMsg);
const fromConnection = await ComponentSet.fromConnection({
usernameOrConnection: (await StateAggregator.getInstance()).aliases.getUsername(org.username) ?? org.username,
// exclude components based on the results of componentFilter function
// components with namespacePrefix where org.exclude includes manageableState (to exclude managed packages)
// components with namespacePrefix where manageableState equals undefined (to exclude components e.g. InstalledPackage)
// components where org.exclude includes manageableState (to exclude packages without namespacePrefix e.g. unlocked packages)
componentFilter: (component): boolean =>
!component?.manageableState || !org.exclude?.includes(component.manageableState),
componentFilter,
metadataTypes: mdMap.size ? Array.from(mdMap.keys()) : undefined,
});

for (const comp of fromConnection) {
Expand Down
4 changes: 4 additions & 0 deletions src/collections/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,8 @@ export interface FromConnectionOptions extends OptionalTreeRegistryOptions {
* filter the result components to e.g. remove managed components
*/
componentFilter?: (component: Partial<FileProperties>) => boolean;
/**
* array of metadata type names to use for `connection.metadata.list()`
*/
metadataTypes?: string[];
}
18 changes: 14 additions & 4 deletions src/resolve/connectionResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,20 @@ export class ConnectionResolver {
private connection: Connection;
private registry: RegistryAccess;

public constructor(connection: Connection, registry = new RegistryAccess()) {
// Array of metadata type names to use for listMembers. By default it includes
// all types defined in the registry.
private mdTypeNames: string[];

public constructor(connection: Connection, registry = new RegistryAccess(), mdTypes?: string[]) {
this.connection = connection;
this.registry = registry;
this.logger = Logger.childFromRoot(this.constructor.name);
if (mdTypes?.length) {
// ensure the types passed in are valid per the registry
this.mdTypeNames = mdTypes.filter((t) => this.registry.getTypeByName(t));
} else {
this.mdTypeNames = Object.values(defaultRegistry.types).map((t) => t.name);
}
}

public async resolve(
Expand All @@ -45,9 +55,9 @@ export class ConnectionResolver {
const componentTypes: Set<MetadataType> = new Set();
const lifecycle = Lifecycle.getInstance();
const componentPromises: Array<Promise<FileProperties[]>> = [];
for (const type of Object.values(defaultRegistry.types)) {
componentPromises.push(this.listMembers({ type: type.name }));
}

this.mdTypeNames.forEach((type) => componentPromises.push(this.listMembers({ type })));

(await Promise.all(componentPromises)).map(async (componentResult) => {
for (const component of componentResult) {
let componentType: MetadataType;
Expand Down
30 changes: 30 additions & 0 deletions test/collections/componentSetBuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,36 @@ describe('ComponentSetBuilder', () => {
expect(compSet.has(apexClassComponent)).to.equal(true);
});

it('should create ComponentSet from org connection and metadata', async () => {
const mdCompSet = new ComponentSet();
mdCompSet.add(apexClassComponent);

fromSourceStub.returns(mdCompSet);
const packageDir1 = path.resolve('force-app');

componentSet.add(apexClassWildcardMatch);
fromConnectionStub.resolves(componentSet);
const options = {
sourcepath: undefined,
metadata: {
metadataEntries: ['ApexClass:MyClas*'],
directoryPaths: [packageDir1],
},
manifest: undefined,
org: {
username: '[email protected]',
exclude: [],
},
};

const compSet = await ComponentSetBuilder.build(options);
expect(fromSourceStub.calledTwice).to.equal(true);
expect(fromConnectionStub.calledOnce).to.equal(true);
expect(compSet.size).to.equal(2);
expect(compSet.has(apexClassComponent)).to.equal(true);
expect(compSet.has(apexClassWildcardMatch)).to.equal(true);
});

it('should create ComponentSet from manifest and multiple package', async () => {
fileExistsSyncStub.returns(true);

Expand Down
33 changes: 33 additions & 0 deletions test/resolve/connectionResolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,39 @@ describe('ConnectionResolver', () => {
];
expect(result.components).to.deep.equal(expected);
});
it('should resolve components with specified types', async () => {
const metadataQueryStub = $$.SANDBOX.stub(connection.metadata, 'list');

metadataQueryStub.withArgs({ type: 'ApexClass' }).resolves([
{
...StdFileProperty,
fileName: 'classes/MyApexClass1.class',
fullName: 'MyApexClass1',
type: 'ApexClass',
},
{
...StdFileProperty,
fileName: 'classes/MyApexClass2.class',
fullName: 'MyApexClass2',
type: 'ApexClass',
},
]);

const resolver = new ConnectionResolver(connection, undefined, ['ApexClass']);
const result = await resolver.resolve();
const expected: MetadataComponent[] = [
{
fullName: 'MyApexClass1',
type: registry.types.apexclass,
},
{
fullName: 'MyApexClass2',
type: registry.types.apexclass,
},
];
expect(result.components).to.deep.equal(expected);
expect(metadataQueryStub.calledOnce).to.be.true;
});
it('should resolve components with invalid type returned by metadata api', async () => {
const metadataQueryStub = $$.SANDBOX.stub(connection.metadata, 'list');
metadataQueryStub.withArgs({ type: 'CustomLabels' }).resolves([
Expand Down

2 comments on commit 0c9d568

@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: 0c9d568 Previous: dd97e25 Ratio
eda-componentSetCreate-linux 200 ms 206 ms 0.97
eda-sourceToMdapi-linux 4937 ms 4840 ms 1.02
eda-sourceToZip-linux 3904 ms 5152 ms 0.76
eda-mdapiToSource-linux 3085 ms 2975 ms 1.04
lotsOfClasses-componentSetCreate-linux 374 ms 383 ms 0.98
lotsOfClasses-sourceToMdapi-linux 5767 ms 6107 ms 0.94
lotsOfClasses-sourceToZip-linux 5738 ms 5052 ms 1.14
lotsOfClasses-mdapiToSource-linux 3428 ms 3474 ms 0.99
lotsOfClassesOneDir-componentSetCreate-linux 632 ms 644 ms 0.98
lotsOfClassesOneDir-sourceToMdapi-linux 9019 ms 9091 ms 0.99
lotsOfClassesOneDir-sourceToZip-linux 8250 ms 7402 ms 1.11
lotsOfClassesOneDir-mdapiToSource-linux 6190 ms 6096 ms 1.02

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: 0c9d568 Previous: dd97e25 Ratio
eda-componentSetCreate-win32 418 ms 412 ms 1.01
eda-sourceToMdapi-win32 6788 ms 5971 ms 1.14
eda-sourceToZip-win32 5811 ms 4963 ms 1.17
eda-mdapiToSource-win32 6056 ms 5826 ms 1.04
lotsOfClasses-componentSetCreate-win32 944 ms 885 ms 1.07
lotsOfClasses-sourceToMdapi-win32 10733 ms 10012 ms 1.07
lotsOfClasses-sourceToZip-win32 7949 ms 7309 ms 1.09
lotsOfClasses-mdapiToSource-win32 7431 ms 7562 ms 0.98
lotsOfClassesOneDir-componentSetCreate-win32 1501 ms 1490 ms 1.01
lotsOfClassesOneDir-sourceToMdapi-win32 16765 ms 16628 ms 1.01
lotsOfClassesOneDir-sourceToZip-win32 13000 ms 10759 ms 1.21
lotsOfClassesOneDir-mdapiToSource-win32 15529 ms 13835 ms 1.12

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

Please sign in to comment.