diff --git a/src/collections/componentSet.ts b/src/collections/componentSet.ts index 63ac8793d2..b32029b6f8 100644 --- a/src/collections/componentSet.ts +++ b/src/collections/componentSet.ts @@ -283,7 +283,7 @@ export class ComponentSet extends LazyCollection { } } - 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; diff --git a/src/collections/componentSetBuilder.ts b/src/collections/componentSetBuilder.ts index aa2741e12f..cdff2ec4a7 100644 --- a/src/collections/componentSetBuilder.ts +++ b/src/collections/componentSetBuilder.ts @@ -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'); @@ -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(); + const { sourcepath, manifest, metadata, packagenames, apiversion, sourceapiversion, org } = options; try { if (sourcepath) { @@ -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 @@ -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); @@ -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): boolean => + !component?.manageableState || !org.exclude?.includes(component.manageableState); + + if (metadata) { + debugMsg += ` filtered by metadata: ${metadata.metadataEntries.toString()}`; + + componentFilter = (component: Partial): 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) { diff --git a/src/collections/types.ts b/src/collections/types.ts index 0d71a2b45f..f685ece2fb 100644 --- a/src/collections/types.ts +++ b/src/collections/types.ts @@ -88,4 +88,8 @@ export interface FromConnectionOptions extends OptionalTreeRegistryOptions { * filter the result components to e.g. remove managed components */ componentFilter?: (component: Partial) => boolean; + /** + * array of metadata type names to use for `connection.metadata.list()` + */ + metadataTypes?: string[]; } diff --git a/src/resolve/connectionResolver.ts b/src/resolve/connectionResolver.ts index 2dfc332da5..a8707998b9 100644 --- a/src/resolve/connectionResolver.ts +++ b/src/resolve/connectionResolver.ts @@ -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( @@ -45,9 +55,9 @@ export class ConnectionResolver { const componentTypes: Set = new Set(); const lifecycle = Lifecycle.getInstance(); const componentPromises: Array> = []; - 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; diff --git a/test/collections/componentSetBuilder.test.ts b/test/collections/componentSetBuilder.test.ts index 8d13681b0c..ddfd1959c2 100644 --- a/test/collections/componentSetBuilder.test.ts +++ b/test/collections/componentSetBuilder.test.ts @@ -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: 'manifest-test@org.com', + 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); diff --git a/test/resolve/connectionResolver.test.ts b/test/resolve/connectionResolver.test.ts index 98eab9f464..828f7aa77f 100644 --- a/test/resolve/connectionResolver.test.ts +++ b/test/resolve/connectionResolver.test.ts @@ -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([