Skip to content

Commit

Permalink
feat: listmetadata and describemetadata
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
shetzel authored Nov 9, 2021
1 parent 610443d commit b00a59a
Show file tree
Hide file tree
Showing 11 changed files with 765 additions and 1 deletion.
10 changes: 10 additions & 0 deletions command-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,5 +105,15 @@
"verbose",
"wait"
]
},
{
"command": "force:mdapi:listmetadata",
"plugin": "@salesforce/plugin-source",
"flags": ["apiversion", "json", "loglevel", "resultfile", "targetusername", "metadatatype", "folder"]
},
{
"command": "force:mdapi:describemetadata",
"plugin": "@salesforce/plugin-source",
"flags": ["apiversion", "json", "loglevel", "resultfile", "targetusername", "filterknown"]
}
]
1 change: 0 additions & 1 deletion dreamhouse-lwc
Submodule dreamhouse-lwc deleted from ae7f21
20 changes: 20 additions & 0 deletions messages/md.describe.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"description": "display the metadata types enabled for your org",
"examples": [
"$ sfdx force:mdapi:describemetadata -a 43.0",
"$ sfdx force:mdapi:describemetadata -u [email protected]",
"$ sfdx force:mdapi:describemetadata -f /path/to/outputfilename.txt",
"$ sfdx force:mdapi:describemetadata -u [email protected] -f /path/to/outputfilename.txt"
],
"flags": {
"apiversion": "API version to use",
"resultfile": "path to the file where results are stored",
"filterknown": "filter metadata known by the CLI"
},
"flagsLong": {
"apiversion": "The API version to use. The default is the latest API version",
"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.",
"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."
},
"invalidResultFile": "Invalid resultfile parameter specified: %s\nMust be a valid file path."
}
28 changes: 28 additions & 0 deletions messages/md.list.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"description": "display properties of metadata components of a specified type",
"examples": [
"$ sfdx force:mdapi:listmetadata -m CustomObject",
"$ sfdx force:mdapi:listmetadata -m CustomObject -a 43.0",
"$ sfdx force:mdapi:listmetadata -m CustomObject -u [email protected]",
"$ sfdx force:mdapi:listmetadata -m CustomObject -f /path/to/outputfilename.txt",
"$ sfdx force:mdapi:listmetadata -m Dashboard --folder foldername",
"$ sfdx force:mdapi:listmetadata -m Dashboard --folder foldername -a 43.0",
"$ sfdx force:mdapi:listmetadata -m Dashboard --folder foldername -u [email protected]",
"$ sfdx force:mdapi:listmetadata -m Dashboard --folder foldername -f /path/to/outputfilename.txt",
"$ sfdx force:mdapi:listmetadata -m CustomObject -u [email protected] -f /path/to/outputfilename.txt"
],
"flags": {
"apiversion": "API version to use",
"resultfile": "path to the file where results are stored",
"metadatatype": "metadata type to be retrieved, such as CustomObject; metadata type value is case-sensitive",
"folder": "folder associated with the component; required for components that use folders; folder names are case-sensitive"
},
"flagsLong": {
"apiversion": "The API version to use. The default is the latest API version",
"resultfile": "The path to the file where the results of the command are stored. The default output destination is the console.",
"metadatatype": "The metadata type to be retrieved, such as CustomObject or Report. The metadata type value is case-sensitive.",
"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."
},
"invalidResultFile": "Invalid resultfile parameter specified: %s\nMust be a valid file path.",
"noMatchingMetadata": "No metadata found for type: %s in org: %s"
}
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@
}
}
}
},
"mdapi": {
"description": "retrieve and deploy metadata using Metadata API"
}
}
}
Expand Down Expand Up @@ -160,6 +163,7 @@
"test:nuts:tracking:forceignore": "mocha \"test/nuts/trackingCommands/forceIgnore.nut.ts\" --slow 3000 --timeout 600000 --retries 0",
"test:nuts:tracking:remote": "mocha \"test/nuts/trackingCommands/remoteChanges.nut.ts\" --slow 3000 --timeout 600000 --retries 0",
"test:nuts:tracking:resetClear": "mocha \"test/nuts/trackingCommands/resetClear.nut.ts\" --slow 3000 --timeout 600000 --retries 0",
"test:nuts:mdapi": "mocha \"test/nuts/mdapi.nut.ts\" --slow 3000 --timeout 600000 --retries 0",
"version": "oclif-dev readme"
},
"husky": {
Expand Down
119 changes: 119 additions & 0 deletions src/commands/force/mdapi/describemetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import * as os from 'os';
import * as path from 'path';
import * as fs from 'fs';
import { flags, FlagsConfig } from '@salesforce/command';
import { Messages, SfdxError } from '@salesforce/core';
import { DescribeMetadataResult } from 'jsforce';
import { RegistryAccess } from '@salesforce/source-deploy-retrieve';
import { SourceCommand } from '../../../sourceCommand';

Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/plugin-source', 'md.describe');

interface FsError extends Error {
code: string;
}

export class DescribeMetadata extends SourceCommand {
public static readonly description = messages.getMessage('description');
public static readonly examples = messages.getMessage('examples').split(os.EOL);
public static readonly requiresUsername = true;
public static readonly flagsConfig: FlagsConfig = {
apiversion: flags.builtin({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore force char override for backward compat
char: 'a',
description: messages.getMessage('flags.apiversion'),
longDescription: messages.getMessage('flagsLong.apiversion'),
}),
resultfile: flags.filepath({
char: 'f',
description: messages.getMessage('flags.resultfile'),
longDescription: messages.getMessage('flagsLong.resultfile'),
}),
filterknown: flags.boolean({
char: 'k',
description: messages.getMessage('flags.filterknown'),
longDescription: messages.getMessage('flagsLong.filterknown'),
hidden: true,
}),
};

private describeResult: DescribeMetadataResult;
private targetFilePath: string;

public async run(): Promise<DescribeMetadataResult> {
await this.describe();
this.resolveSuccess();
return this.formatResult();
}

protected async describe(): Promise<void> {
const apiversion = this.getFlag<string>('apiversion');

this.validateResultFile();

const connection = this.org.getConnection();
this.describeResult = await connection.metadata.describe(apiversion);

if (this.flags.filterknown) {
this.logger.debug('Filtering for only metadata types unregistered in the CLI');
const registry = new RegistryAccess();
this.describeResult.metadataObjects = this.describeResult.metadataObjects.filter((md) => {
try {
// An error is thrown when a type can't be found by name, and we want
// the ones that can't be found.
registry.getTypeByName(md.xmlName);
return false;
} catch (e) {
return true;
}
});
}
}

// No-op implementation since any describe metadata status would be a success.
// The only time this command would report an error is if it failed
// flag parsing or some error during the request, and those are captured
// by the command framework.
/* eslint-disable-next-line @typescript-eslint/no-empty-function */
protected resolveSuccess(): void {}

protected formatResult(): DescribeMetadataResult {
if (this.targetFilePath) {
fs.writeFileSync(this.targetFilePath, JSON.stringify(this.describeResult, null, 2));
this.ux.log(`Wrote result file to ${this.targetFilePath}.`);
} else if (!this.isJsonOutput()) {
this.ux.styledJSON(this.describeResult);
}
return this.describeResult;
}

private validateResultFile(): void {
if (this.flags.resultfile) {
this.targetFilePath = path.resolve(this.flags.resultfile);
// Ensure path exists
fs.mkdirSync(path.dirname(this.targetFilePath), { recursive: true });
try {
const stat = fs.statSync(this.targetFilePath);
if (!stat.isFile()) {
throw SfdxError.create('@salesforce/plugin-source', 'md.describe', 'invalidResultFile', [
this.targetFilePath,
]);
}
} catch (err: unknown) {
const e = err as FsError;
if (e.code !== 'ENOENT') {
throw err;
}
}
}
}
}
116 changes: 116 additions & 0 deletions src/commands/force/mdapi/listmetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import * as os from 'os';
import * as path from 'path';
import * as fs from 'fs';
import { flags, FlagsConfig } from '@salesforce/command';
import { Messages, SfdxError } from '@salesforce/core';
import { Optional } from '@salesforce/ts-types';
import { FileProperties, ListMetadataQuery } from 'jsforce';
import { SourceCommand } from '../../../sourceCommand';

Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/plugin-source', 'md.list');

export type ListMetadataCommandResult = FileProperties[];

interface FsError extends Error {
code: string;
}

export class ListMetadata extends SourceCommand {
public static readonly description = messages.getMessage('description');
public static readonly examples = messages.getMessage('examples').split(os.EOL);
public static readonly requiresUsername = true;
public static readonly flagsConfig: FlagsConfig = {
apiversion: flags.builtin({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore force char override for backward compat
char: 'a',
description: messages.getMessage('flags.apiversion'),
longDescription: messages.getMessage('flagsLong.apiversion'),
}),
resultfile: flags.filepath({
char: 'f',
description: messages.getMessage('flags.resultfile'),
longDescription: messages.getMessage('flagsLong.resultfile'),
}),
metadatatype: flags.string({
char: 'm',
description: messages.getMessage('flags.metadatatype'),
longDescription: messages.getMessage('flagsLong.metadatatype'),
required: true,
}),
folder: flags.string({
description: messages.getMessage('flags.folder'),
longDescription: messages.getMessage('flagsLong.folder'),
}),
};

private listResult: Optional<FileProperties[]>;
private targetFilePath: string;

public async run(): Promise<ListMetadataCommandResult> {
await this.list();
this.resolveSuccess();
return this.formatResult();
}

protected async list(): Promise<void> {
const apiversion = this.getFlag<string>('apiversion');
const type = this.getFlag<string>('metadatatype');
const folder = this.getFlag<string>('folder');

this.validateResultFile();

const query: ListMetadataQuery = { type, folder };
const connection = this.org.getConnection();
const result = (await connection.metadata.list(query, apiversion)) || [];
this.listResult = Array.isArray(result) ? result : [result];
}

// No-op implementation since any list metadata status would be a success.
// The only time this command would report an error is if it failed
// flag parsing or some error during the request, and those are captured
// by the command framework.
/* eslint-disable-next-line @typescript-eslint/no-empty-function */
protected resolveSuccess(): void {}

protected formatResult(): ListMetadataCommandResult {
if (this.targetFilePath) {
fs.writeFileSync(this.targetFilePath, JSON.stringify(this.listResult, null, 2));
this.ux.log(`Wrote result file to ${this.targetFilePath}.`);
} else if (!this.isJsonOutput()) {
if (this.listResult.length) {
this.ux.styledJSON(this.listResult);
} else {
this.ux.log(messages.getMessage('noMatchingMetadata', [this.flags.metadatatype, this.org.getUsername()]));
}
}
return this.listResult;
}

private validateResultFile(): void {
if (this.flags.resultfile) {
this.targetFilePath = path.resolve(this.flags.resultfile);
// Ensure path exists
fs.mkdirSync(path.dirname(this.targetFilePath), { recursive: true });
try {
const stat = fs.statSync(this.targetFilePath);
if (!stat.isFile()) {
throw SfdxError.create('@salesforce/plugin-source', 'md.list', 'invalidResultFile', [this.targetFilePath]);
}
} catch (err: unknown) {
const e = err as FsError;
if (e.code !== 'ENOENT') {
throw err;
}
}
}
}
}
Loading

0 comments on commit b00a59a

Please sign in to comment.