Skip to content

Commit b00a59a

Browse files
authored
feat: listmetadata and describemetadata
* feat: add the mdapi:listmetadata command * fix: lint fixes and command snapshot * chore: update oclif topics * chore: add unit tests * chore: add NUTs * chore: add NUTs * chore: add a comment to the NUTs * feat: add the mdapi:describemetadata command and tests * fix: use short description
1 parent 610443d commit b00a59a

File tree

11 files changed

+765
-1
lines changed

11 files changed

+765
-1
lines changed

command-snapshot.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,5 +105,15 @@
105105
"verbose",
106106
"wait"
107107
]
108+
},
109+
{
110+
"command": "force:mdapi:listmetadata",
111+
"plugin": "@salesforce/plugin-source",
112+
"flags": ["apiversion", "json", "loglevel", "resultfile", "targetusername", "metadatatype", "folder"]
113+
},
114+
{
115+
"command": "force:mdapi:describemetadata",
116+
"plugin": "@salesforce/plugin-source",
117+
"flags": ["apiversion", "json", "loglevel", "resultfile", "targetusername", "filterknown"]
108118
}
109119
]

dreamhouse-lwc

Lines changed: 0 additions & 1 deletion
This file was deleted.

messages/md.describe.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"description": "display the metadata types enabled for your org",
3+
"examples": [
4+
"$ sfdx force:mdapi:describemetadata -a 43.0",
5+
"$ sfdx force:mdapi:describemetadata -u [email protected]",
6+
"$ sfdx force:mdapi:describemetadata -f /path/to/outputfilename.txt",
7+
"$ sfdx force:mdapi:describemetadata -u [email protected] -f /path/to/outputfilename.txt"
8+
],
9+
"flags": {
10+
"apiversion": "API version to use",
11+
"resultfile": "path to the file where results are stored",
12+
"filterknown": "filter metadata known by the CLI"
13+
},
14+
"flagsLong": {
15+
"apiversion": "The API version to use. The default is the latest API version",
16+
"resultfile": "The path to the file where the results of the command are stored. Directing the output to a file makes it easier to extract relevant information for your package.xml manifest file. The default output destination is the console.",
17+
"filterknown": "Filters all the known metadata from the result such that all that is left are the types not yet fully supported by the CLI."
18+
},
19+
"invalidResultFile": "Invalid resultfile parameter specified: %s\nMust be a valid file path."
20+
}

messages/md.list.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"description": "display properties of metadata components of a specified type",
3+
"examples": [
4+
"$ sfdx force:mdapi:listmetadata -m CustomObject",
5+
"$ sfdx force:mdapi:listmetadata -m CustomObject -a 43.0",
6+
"$ sfdx force:mdapi:listmetadata -m CustomObject -u [email protected]",
7+
"$ sfdx force:mdapi:listmetadata -m CustomObject -f /path/to/outputfilename.txt",
8+
"$ sfdx force:mdapi:listmetadata -m Dashboard --folder foldername",
9+
"$ sfdx force:mdapi:listmetadata -m Dashboard --folder foldername -a 43.0",
10+
"$ sfdx force:mdapi:listmetadata -m Dashboard --folder foldername -u [email protected]",
11+
"$ sfdx force:mdapi:listmetadata -m Dashboard --folder foldername -f /path/to/outputfilename.txt",
12+
"$ sfdx force:mdapi:listmetadata -m CustomObject -u [email protected] -f /path/to/outputfilename.txt"
13+
],
14+
"flags": {
15+
"apiversion": "API version to use",
16+
"resultfile": "path to the file where results are stored",
17+
"metadatatype": "metadata type to be retrieved, such as CustomObject; metadata type value is case-sensitive",
18+
"folder": "folder associated with the component; required for components that use folders; folder names are case-sensitive"
19+
},
20+
"flagsLong": {
21+
"apiversion": "The API version to use. The default is the latest API version",
22+
"resultfile": "The path to the file where the results of the command are stored. The default output destination is the console.",
23+
"metadatatype": "The metadata type to be retrieved, such as CustomObject or Report. The metadata type value is case-sensitive.",
24+
"folder": "The folder associated with the component. This parameter is required for components that use folders, such as Dashboard, Document, EmailTemplate, or Report. The folder name value is case-sensitive."
25+
},
26+
"invalidResultFile": "Invalid resultfile parameter specified: %s\nMust be a valid file path.",
27+
"noMatchingMetadata": "No metadata found for type: %s in org: %s"
28+
}

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@
115115
}
116116
}
117117
}
118+
},
119+
"mdapi": {
120+
"description": "retrieve and deploy metadata using Metadata API"
118121
}
119122
}
120123
}
@@ -160,6 +163,7 @@
160163
"test:nuts:tracking:forceignore": "mocha \"test/nuts/trackingCommands/forceIgnore.nut.ts\" --slow 3000 --timeout 600000 --retries 0",
161164
"test:nuts:tracking:remote": "mocha \"test/nuts/trackingCommands/remoteChanges.nut.ts\" --slow 3000 --timeout 600000 --retries 0",
162165
"test:nuts:tracking:resetClear": "mocha \"test/nuts/trackingCommands/resetClear.nut.ts\" --slow 3000 --timeout 600000 --retries 0",
166+
"test:nuts:mdapi": "mocha \"test/nuts/mdapi.nut.ts\" --slow 3000 --timeout 600000 --retries 0",
163167
"version": "oclif-dev readme"
164168
},
165169
"husky": {
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright (c) 2020, 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 * as os from 'os';
9+
import * as path from 'path';
10+
import * as fs from 'fs';
11+
import { flags, FlagsConfig } from '@salesforce/command';
12+
import { Messages, SfdxError } from '@salesforce/core';
13+
import { DescribeMetadataResult } from 'jsforce';
14+
import { RegistryAccess } from '@salesforce/source-deploy-retrieve';
15+
import { SourceCommand } from '../../../sourceCommand';
16+
17+
Messages.importMessagesDirectory(__dirname);
18+
const messages = Messages.loadMessages('@salesforce/plugin-source', 'md.describe');
19+
20+
interface FsError extends Error {
21+
code: string;
22+
}
23+
24+
export class DescribeMetadata extends SourceCommand {
25+
public static readonly description = messages.getMessage('description');
26+
public static readonly examples = messages.getMessage('examples').split(os.EOL);
27+
public static readonly requiresUsername = true;
28+
public static readonly flagsConfig: FlagsConfig = {
29+
apiversion: flags.builtin({
30+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
31+
// @ts-ignore force char override for backward compat
32+
char: 'a',
33+
description: messages.getMessage('flags.apiversion'),
34+
longDescription: messages.getMessage('flagsLong.apiversion'),
35+
}),
36+
resultfile: flags.filepath({
37+
char: 'f',
38+
description: messages.getMessage('flags.resultfile'),
39+
longDescription: messages.getMessage('flagsLong.resultfile'),
40+
}),
41+
filterknown: flags.boolean({
42+
char: 'k',
43+
description: messages.getMessage('flags.filterknown'),
44+
longDescription: messages.getMessage('flagsLong.filterknown'),
45+
hidden: true,
46+
}),
47+
};
48+
49+
private describeResult: DescribeMetadataResult;
50+
private targetFilePath: string;
51+
52+
public async run(): Promise<DescribeMetadataResult> {
53+
await this.describe();
54+
this.resolveSuccess();
55+
return this.formatResult();
56+
}
57+
58+
protected async describe(): Promise<void> {
59+
const apiversion = this.getFlag<string>('apiversion');
60+
61+
this.validateResultFile();
62+
63+
const connection = this.org.getConnection();
64+
this.describeResult = await connection.metadata.describe(apiversion);
65+
66+
if (this.flags.filterknown) {
67+
this.logger.debug('Filtering for only metadata types unregistered in the CLI');
68+
const registry = new RegistryAccess();
69+
this.describeResult.metadataObjects = this.describeResult.metadataObjects.filter((md) => {
70+
try {
71+
// An error is thrown when a type can't be found by name, and we want
72+
// the ones that can't be found.
73+
registry.getTypeByName(md.xmlName);
74+
return false;
75+
} catch (e) {
76+
return true;
77+
}
78+
});
79+
}
80+
}
81+
82+
// No-op implementation since any describe metadata status would be a success.
83+
// The only time this command would report an error is if it failed
84+
// flag parsing or some error during the request, and those are captured
85+
// by the command framework.
86+
/* eslint-disable-next-line @typescript-eslint/no-empty-function */
87+
protected resolveSuccess(): void {}
88+
89+
protected formatResult(): DescribeMetadataResult {
90+
if (this.targetFilePath) {
91+
fs.writeFileSync(this.targetFilePath, JSON.stringify(this.describeResult, null, 2));
92+
this.ux.log(`Wrote result file to ${this.targetFilePath}.`);
93+
} else if (!this.isJsonOutput()) {
94+
this.ux.styledJSON(this.describeResult);
95+
}
96+
return this.describeResult;
97+
}
98+
99+
private validateResultFile(): void {
100+
if (this.flags.resultfile) {
101+
this.targetFilePath = path.resolve(this.flags.resultfile);
102+
// Ensure path exists
103+
fs.mkdirSync(path.dirname(this.targetFilePath), { recursive: true });
104+
try {
105+
const stat = fs.statSync(this.targetFilePath);
106+
if (!stat.isFile()) {
107+
throw SfdxError.create('@salesforce/plugin-source', 'md.describe', 'invalidResultFile', [
108+
this.targetFilePath,
109+
]);
110+
}
111+
} catch (err: unknown) {
112+
const e = err as FsError;
113+
if (e.code !== 'ENOENT') {
114+
throw err;
115+
}
116+
}
117+
}
118+
}
119+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright (c) 2020, 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 * as os from 'os';
9+
import * as path from 'path';
10+
import * as fs from 'fs';
11+
import { flags, FlagsConfig } from '@salesforce/command';
12+
import { Messages, SfdxError } from '@salesforce/core';
13+
import { Optional } from '@salesforce/ts-types';
14+
import { FileProperties, ListMetadataQuery } from 'jsforce';
15+
import { SourceCommand } from '../../../sourceCommand';
16+
17+
Messages.importMessagesDirectory(__dirname);
18+
const messages = Messages.loadMessages('@salesforce/plugin-source', 'md.list');
19+
20+
export type ListMetadataCommandResult = FileProperties[];
21+
22+
interface FsError extends Error {
23+
code: string;
24+
}
25+
26+
export class ListMetadata extends SourceCommand {
27+
public static readonly description = messages.getMessage('description');
28+
public static readonly examples = messages.getMessage('examples').split(os.EOL);
29+
public static readonly requiresUsername = true;
30+
public static readonly flagsConfig: FlagsConfig = {
31+
apiversion: flags.builtin({
32+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
33+
// @ts-ignore force char override for backward compat
34+
char: 'a',
35+
description: messages.getMessage('flags.apiversion'),
36+
longDescription: messages.getMessage('flagsLong.apiversion'),
37+
}),
38+
resultfile: flags.filepath({
39+
char: 'f',
40+
description: messages.getMessage('flags.resultfile'),
41+
longDescription: messages.getMessage('flagsLong.resultfile'),
42+
}),
43+
metadatatype: flags.string({
44+
char: 'm',
45+
description: messages.getMessage('flags.metadatatype'),
46+
longDescription: messages.getMessage('flagsLong.metadatatype'),
47+
required: true,
48+
}),
49+
folder: flags.string({
50+
description: messages.getMessage('flags.folder'),
51+
longDescription: messages.getMessage('flagsLong.folder'),
52+
}),
53+
};
54+
55+
private listResult: Optional<FileProperties[]>;
56+
private targetFilePath: string;
57+
58+
public async run(): Promise<ListMetadataCommandResult> {
59+
await this.list();
60+
this.resolveSuccess();
61+
return this.formatResult();
62+
}
63+
64+
protected async list(): Promise<void> {
65+
const apiversion = this.getFlag<string>('apiversion');
66+
const type = this.getFlag<string>('metadatatype');
67+
const folder = this.getFlag<string>('folder');
68+
69+
this.validateResultFile();
70+
71+
const query: ListMetadataQuery = { type, folder };
72+
const connection = this.org.getConnection();
73+
const result = (await connection.metadata.list(query, apiversion)) || [];
74+
this.listResult = Array.isArray(result) ? result : [result];
75+
}
76+
77+
// No-op implementation since any list metadata status would be a success.
78+
// The only time this command would report an error is if it failed
79+
// flag parsing or some error during the request, and those are captured
80+
// by the command framework.
81+
/* eslint-disable-next-line @typescript-eslint/no-empty-function */
82+
protected resolveSuccess(): void {}
83+
84+
protected formatResult(): ListMetadataCommandResult {
85+
if (this.targetFilePath) {
86+
fs.writeFileSync(this.targetFilePath, JSON.stringify(this.listResult, null, 2));
87+
this.ux.log(`Wrote result file to ${this.targetFilePath}.`);
88+
} else if (!this.isJsonOutput()) {
89+
if (this.listResult.length) {
90+
this.ux.styledJSON(this.listResult);
91+
} else {
92+
this.ux.log(messages.getMessage('noMatchingMetadata', [this.flags.metadatatype, this.org.getUsername()]));
93+
}
94+
}
95+
return this.listResult;
96+
}
97+
98+
private validateResultFile(): void {
99+
if (this.flags.resultfile) {
100+
this.targetFilePath = path.resolve(this.flags.resultfile);
101+
// Ensure path exists
102+
fs.mkdirSync(path.dirname(this.targetFilePath), { recursive: true });
103+
try {
104+
const stat = fs.statSync(this.targetFilePath);
105+
if (!stat.isFile()) {
106+
throw SfdxError.create('@salesforce/plugin-source', 'md.list', 'invalidResultFile', [this.targetFilePath]);
107+
}
108+
} catch (err: unknown) {
109+
const e = err as FsError;
110+
if (e.code !== 'ENOENT') {
111+
throw err;
112+
}
113+
}
114+
}
115+
}
116+
}

0 commit comments

Comments
 (0)