Skip to content

Commit 07e9276

Browse files
authored
Merge pull request #26 from amtrack/feat/allowlisting
feat: add allowlisting using --metadata
2 parents 4d0cf9d + 4c5bcdd commit 07e9276

15 files changed

+271
-173
lines changed

src/cli.ts

+17-16
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
1-
import getStdin = require('get-stdin');
2-
import MetadataComponent, {
3-
parseMetadataComponentName
4-
} from './metadata-component';
5-
6-
export async function getMetadataComponentsFromStdinOrString(
1+
export function parseCommaSeparatedValues(
72
commaSeparatedMetadataComponentNames: string
8-
): Promise<Array<MetadataComponent>> {
9-
let rawComponentNames = [];
10-
if (commaSeparatedMetadataComponentNames === '-') {
11-
rawComponentNames = (await getStdin()).split('\n');
12-
} else {
13-
rawComponentNames = commaSeparatedMetadataComponentNames.split(',');
3+
): Array<string> {
4+
if (!commaSeparatedMetadataComponentNames) {
5+
return [];
6+
}
7+
return clean(commaSeparatedMetadataComponentNames.split(','));
8+
}
9+
export function parseNewLineSeparatedValues(
10+
newLineSeparatedValues: string
11+
): Array<string> {
12+
if (!newLineSeparatedValues) {
13+
return [];
1414
}
15-
return rawComponentNames
16-
.map((x) => x.trim())
17-
.filter(Boolean)
18-
.map(parseMetadataComponentName);
15+
return clean(newLineSeparatedValues.split(/\r?\n/));
16+
}
17+
18+
function clean(values: Array<string>) {
19+
return values.map((x) => x.trim()).filter(Boolean);
1920
}

src/commands/force/mdapi/listallmetadata.ts

+25-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { flags, SfdxCommand } from '@salesforce/command';
22
import { promises as fs } from 'fs';
3+
import {
4+
parseCommaSeparatedValues,
5+
parseNewLineSeparatedValues
6+
} from '../../../cli';
37
import { listAllMetadata } from '../../../listallmetadata';
8+
import getStdin = require('get-stdin');
49

510
export default class MdapiListAllMetadataCommand extends SfdxCommand {
611
public static description = `list all metadata components
@@ -67,10 +72,15 @@ export default class MdapiListAllMetadataCommand extends SfdxCommand {
6772
children: flags.boolean({
6873
description: `list metadata components of child types (e.g. 'CustomField' children of 'CustomObject')`
6974
}),
75+
metadata: flags.string({
76+
char: 'm',
77+
description: `comma-separated list of metadata component name expressions to list
78+
Example: 'CustomObject:*,CustomField:Account.*'`
79+
}),
7080
ignore: flags.string({
71-
description: `ignore metadata components matching the pattern in the format of <type>:<fullName>
72-
Examples: 'InstalledPackage:*', 'Profile:*', 'Report:unfiled$public/*', 'CustomField:Account.*'`,
73-
multiple: true
81+
char: 'i',
82+
description: `comma-separated list of metadata component name expressions to ignore
83+
Example: 'InstalledPackage:*,Profile:*,Report:unfiled$public/*,CustomField:Account.*'`
7484
}),
7585
names: flags.boolean({
7686
description: `output only component names (e.g. 'CustomObject:Account',...)`
@@ -79,7 +89,18 @@ export default class MdapiListAllMetadataCommand extends SfdxCommand {
7989

8090
public async run(): Promise<any> {
8191
const conn = this.org.getConnection();
82-
const fileProperties = await listAllMetadata(conn, this.flags);
92+
let allowPatterns =
93+
this.flags.metadata === '-'
94+
? parseNewLineSeparatedValues(await getStdin())
95+
: parseCommaSeparatedValues(this.flags.metadata);
96+
allowPatterns = allowPatterns.length ? allowPatterns : ['**/*'];
97+
const ignorePatterns = parseCommaSeparatedValues(this.flags.ignore);
98+
const fileProperties = await listAllMetadata(
99+
conn,
100+
this.flags,
101+
allowPatterns,
102+
ignorePatterns
103+
);
83104
if (this.flags.resultfile) {
84105
const fileData: string = JSON.stringify(fileProperties, null, 4);
85106
await fs.writeFile(this.flags.resultfile, fileData);

src/commands/package.xml/create.ts

+14-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { flags, SfdxCommand } from '@salesforce/command';
22
import { promises as fs } from 'fs';
3-
import { getMetadataComponentsFromStdinOrString } from '../../cli';
3+
import {
4+
parseCommaSeparatedValues,
5+
parseNewLineSeparatedValues
6+
} from '../../cli';
7+
import { parseMetadataComponentName } from '../../metadata-component';
48
import PackageXml from '../../package-xml';
9+
import getStdin = require('get-stdin');
510

611
export default class PackageXmlCreateCommand extends SfdxCommand {
712
public static description = `create a package.xml manifest`;
@@ -43,10 +48,14 @@ export default class PackageXmlCreateCommand extends SfdxCommand {
4348
if (this.flags.apiversion) {
4449
meta['version'] = this.flags.apiversion;
4550
}
46-
const metadataComponents = await getMetadataComponentsFromStdinOrString(
47-
this.flags.metadata
48-
);
49-
const packageXml = new PackageXml(metadataComponents, meta).toString();
51+
const metadataComponentNames =
52+
this.flags.metadata === '-'
53+
? parseNewLineSeparatedValues(await getStdin())
54+
: parseCommaSeparatedValues(this.flags.metadata);
55+
const packageXml = new PackageXml(
56+
metadataComponentNames.map(parseMetadataComponentName),
57+
meta
58+
).toString();
5059
if (this.flags.resultfile) {
5160
await fs.writeFile(this.flags.resultfile, packageXml);
5261
} else {

src/commands/package.xml/generate.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { flags, SfdxCommand } from '@salesforce/command';
22
import { promises as fs } from 'fs';
3-
import { ignoreMatching } from '../../ignore';
3+
import { parseCommaSeparatedValues } from '../../cli';
4+
import { match } from '../../match';
45
import { toMetadataComponentName } from '../../metadata-component';
56
import PackageXml from '../../package-xml';
67

@@ -53,9 +54,9 @@ export default class PackageXmlGenerateCommand extends SfdxCommand {
5354
description: 'path to the generated package.xml file'
5455
}),
5556
ignore: flags.string({
56-
description: `ignore metadata components matching the pattern in the format of <type>:<fullName>
57-
Example: --ignore "Profile:*" --ignore "Report:unfiled$public/*" --ignore "CustomField:Account.*"`,
58-
multiple: true
57+
char: 'i',
58+
description: `comma-separated list of metadata component name expressions to ignore
59+
Example: 'InstalledPackage:*,Profile:*,Report:unfiled$public/*,CustomField:Account.*'`
5960
}),
6061
// ignorefile: flags.filepath({
6162
// description: `same as --ignore, just one ignore pattern per line in a file`
@@ -83,12 +84,12 @@ export default class PackageXmlGenerateCommand extends SfdxCommand {
8384
}
8485
const ignorePatterns = [];
8586
if (this.flags.ignore) {
86-
ignorePatterns.push(...this.flags.ignore);
87+
ignorePatterns.push(...parseCommaSeparatedValues(this.flags.ignore));
8788
}
8889
if (this.flags.defaultignore) {
8990
ignorePatterns.push(...this.flags.defaultignore);
9091
}
91-
const [keep] = ignoreMatching(
92+
const [, keep] = match(
9293
fileProperties,
9394
ignorePatterns,
9495
toMetadataComponentName

src/listallmetadata.ts

+14-13
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { OutputFlags } from '@oclif/parser';
22
import { Connection } from '@salesforce/core';
33
import { FileProperties } from 'jsforce';
4-
import { ignoreMatching } from './ignore';
4+
import { match } from './match';
55
import { toMetadataComponentName } from './metadata-component';
66
import ChildMetadataLister from './metadata-lister/children';
77
import FolderBasedMetadataLister from './metadata-lister/folderbased';
@@ -10,7 +10,9 @@ import StandardValueSetMetadataLister from './metadata-lister/standardvaluesets'
1010

1111
export async function listAllMetadata(
1212
conn: Connection,
13-
flags: OutputFlags<any>
13+
flags: OutputFlags<any>,
14+
allowPatterns?: Array<string>,
15+
ignorePatterns?: Array<string>
1416
): Promise<Array<FileProperties>> {
1517
const describeMetadataResult = await conn.metadata.describe();
1618
const metadataListers = [
@@ -19,28 +21,27 @@ export async function listAllMetadata(
1921
StandardValueSetMetadataLister,
2022
ChildMetadataLister
2123
];
22-
// TODO: filter describeMetadataResult using ignorePatterns
2324
const result = [];
2425
for (const MetadataListerImplementation of metadataListers) {
2526
const listerId = MetadataListerImplementation.id;
26-
const instance = new MetadataListerImplementation();
27+
const instance = new MetadataListerImplementation(
28+
allowPatterns,
29+
ignorePatterns
30+
);
2731
if (flags[listerId]) {
2832
const fileProperties = await instance.run(
2933
conn,
3034
describeMetadataResult,
3135
result
3236
);
33-
const [keep, ignored] = ignoreMatching(
37+
// postfilter
38+
const [matches] = match(
3439
fileProperties,
35-
flags.ignore ? flags.ignore : [],
36-
toMetadataComponentName
40+
allowPatterns,
41+
toMetadataComponentName,
42+
{ ignore: ignorePatterns }
3743
);
38-
if (ignored.length) {
39-
console.error(
40-
`ignored: ${JSON.stringify(ignored.map(toMetadataComponentName))}`
41-
);
42-
}
43-
result.push(...keep);
44+
result.push(...matches);
4445
}
4546
}
4647
return result;
+13-12
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,32 @@
11
import * as picomatch from 'picomatch';
22

3-
interface ToStringFunction {
3+
export interface ToStringFunction {
44
(item: any): string;
55
}
66

77
/**
88
*
99
* @param collection an array of strings or objects
10-
* @param ignorePatterns a list of ignore patterns similar to .gitignore entries
11-
* @param toString if the collection is an array of objects: a function to convert an item of the collection to a string to be able to match against the ignorePatterns
10+
* @param patterns a list of patterns similar to .gitignore entries
11+
* @param toString if the collection is an array of objects: a function to convert an item of the collection to a string to be able to match against the allowPatterns
1212
* @param picoMatchOptions options for picomatch library
13+
* @returns Array with [matched, unmatched]
1314
*/
14-
export function ignoreMatching(
15+
export function match(
1516
collection: Array<any>,
16-
ignorePatterns: Array<string>,
17+
patterns: Array<string>,
1718
toString?: ToStringFunction,
1819
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
1920
picoMatchOptions?: any
2021
): Array<any> {
21-
const ignore = [];
22-
const keep = collection.filter((item) => {
22+
const unmatched = [];
23+
const matched = collection.filter((item) => {
2324
const str = toString ? toString(item) : item;
24-
const isMatch = picomatch.isMatch(str, ignorePatterns, picoMatchOptions);
25-
if (isMatch) {
26-
ignore.push(item);
25+
const isMatch = picomatch.isMatch(str, patterns, picoMatchOptions);
26+
if (!isMatch) {
27+
unmatched.push(item);
2728
}
28-
return !isMatch;
29+
return isMatch;
2930
});
30-
return [keep, ignore];
31+
return [matched, unmatched];
3132
}

src/metadata-lister.ts

+34
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,37 @@
1+
import { Connection, DescribeMetadataResult, FileProperties } from 'jsforce';
2+
import { match, ToStringFunction } from './match';
3+
4+
export interface IMetadataLister {
5+
id: string;
6+
run: (
7+
conn: Connection,
8+
describeMetadataResult: DescribeMetadataResult,
9+
fileProperties: Array<FileProperties>,
10+
allowPatterns: Array<string>,
11+
ignorePatterns: Array<string>
12+
) => Promise<Array<FileProperties>>;
13+
}
14+
115
export default abstract class MetadataLister {
216
public static id: string;
17+
private allowPatterns: Array<string>;
18+
private ignorePatterns: Array<string>;
19+
20+
constructor(allowPatterns: Array<string>, ignorePatterns: Array<string>) {
21+
this.allowPatterns = allowPatterns;
22+
this.ignorePatterns = ignorePatterns;
23+
}
24+
25+
abstract run(
26+
conn: Connection,
27+
describeMetadataResult: DescribeMetadataResult,
28+
fileProperties: Array<FileProperties>
29+
): Promise<Array<FileProperties>>;
30+
31+
public filter(items: Array<any>, toString?: ToStringFunction): Array<any> {
32+
const [matched] = match(items, this.allowPatterns, toString, {
33+
ignore: this.ignorePatterns
34+
});
35+
return matched;
36+
}
337
}

src/metadata-lister/children.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ export default class ChildrenMetadata extends MetadataLister {
2525
const childMetadataQueries = childMetadataTypes.map((childMetadataType) => {
2626
return { type: childMetadataType };
2727
});
28-
let result = await listMetadataInChunks(conn, childMetadataQueries);
28+
const filteredChildMetadataQueries = this.filter(
29+
childMetadataQueries,
30+
(x) => `${x.type}:`
31+
);
32+
let result = await listMetadataInChunks(conn, filteredChildMetadataQueries);
2933
let personAccountRecordTypes = [];
3034
try {
3135
personAccountRecordTypes = await queryPersonAccountRecordTypes(conn);

src/metadata-lister/folderbased.ts

+26-24
Original file line numberDiff line numberDiff line change
@@ -18,29 +18,31 @@ export default class FolderBasedMetadata extends MetadataLister {
1818
// eslint-disable-next-line @typescript-eslint/no-unused-vars
1919
fileProperties: Array<FileProperties>
2020
): Promise<Array<FileProperties>> {
21-
return await listFolderBasedMetadata(conn);
21+
const folderTypes = Object.keys(FOLDER_BASED_METADATA_MAP);
22+
const folderQueries = folderTypes.map((folderType) => {
23+
return {
24+
type: folderType
25+
};
26+
});
27+
const filteredFolderQueries = this.filter(
28+
folderQueries,
29+
(x) => `${x.type}:`
30+
);
31+
const folders = await listMetadataInChunks(conn, filteredFolderQueries);
32+
const inFolderQueries = folders.map((folder) => {
33+
return {
34+
type: FOLDER_BASED_METADATA_MAP[folder.type],
35+
folder: folder.fullName
36+
};
37+
});
38+
const filteredInFolderQueries = this.filter(
39+
inFolderQueries,
40+
(x) => `${x.type}:${x.folder}`
41+
);
42+
const inFolderFileProperties = await listMetadataInChunks(
43+
conn,
44+
filteredInFolderQueries
45+
);
46+
return [...folders, ...inFolderFileProperties];
2247
}
2348
}
24-
25-
export async function listFolderBasedMetadata(
26-
conn: Connection
27-
): Promise<Array<FileProperties>> {
28-
const folderTypes = Object.keys(FOLDER_BASED_METADATA_MAP);
29-
const folderQueries = folderTypes.map((folderType) => {
30-
return {
31-
type: folderType
32-
};
33-
});
34-
const folders = await listMetadataInChunks(conn, folderQueries);
35-
const inFolderQueries = folders.map((folder) => {
36-
return {
37-
type: FOLDER_BASED_METADATA_MAP[folder.type],
38-
folder: folder.fullName
39-
};
40-
});
41-
const inFolderFileProperties = await listMetadataInChunks(
42-
conn,
43-
inFolderQueries
44-
);
45-
return [...folders, ...inFolderFileProperties];
46-
}

src/metadata-lister/regular.ts

+11-8
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,18 @@ export default class RegularMetadata extends MetadataLister {
1212
// eslint-disable-next-line @typescript-eslint/no-unused-vars
1313
fileProperties: Array<FileProperties>
1414
): Promise<Array<FileProperties>> {
15-
const metadataTypes = describeMetadataResult.metadataObjects.map(
16-
(metadataType) => metadataType.xmlName
15+
const metadataQueries = describeMetadataResult.metadataObjects.map(
16+
(cmp) => {
17+
return {
18+
type: cmp.xmlName
19+
};
20+
}
21+
);
22+
const filteredMetadataQueries = this.filter(
23+
metadataQueries,
24+
(x) => `${x.type}:`
1725
);
18-
const metadataQueries = metadataTypes.map((metadataType) => {
19-
return {
20-
type: metadataType
21-
};
22-
});
23-
let result = await listMetadataInChunks(conn, metadataQueries);
26+
let result = await listMetadataInChunks(conn, filteredMetadataQueries);
2427
result = fixNilType(result, describeMetadataResult);
2528
result = fixCustomFeedFilter(result);
2629
return result;

0 commit comments

Comments
 (0)