-
Notifications
You must be signed in to change notification settings - Fork 407
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Retrieve default org's metadata types #1237
Changes from 9 commits
2b86759
e2e4f78
3f9ccc1
d5d15a2
84283c0
cc6ca66
a05388d
ad61ee6
0218998
d05fbc8
cf9c780
b9ec4e8
2b40a0b
43aeab9
faf24fb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
/* | ||
* Copyright (c) 2019, 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 | ||
*/ | ||
export { | ||
onUsernameChange, | ||
forceDescribeMetadata, | ||
ForceDescribeMetadataExecutor, | ||
getMetadataTypesPath, | ||
buildTypesList | ||
} from './orgMetadata'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
/* | ||
* Copyright (c) 2019, 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 { | ||
CliCommandExecutor, | ||
Command, | ||
SfdxCommandBuilder | ||
} from '@salesforce/salesforcedx-utils-vscode/out/src/cli'; | ||
import { ContinueResponse } from '@salesforce/salesforcedx-utils-vscode/out/src/types'; | ||
import * as fs from 'fs'; | ||
import * as path from 'path'; | ||
import { Observable } from 'rxjs/Observable'; | ||
import { isNullOrUndefined } from 'util'; | ||
import * as vscode from 'vscode'; | ||
import { channelService } from '../channels'; | ||
import { | ||
EmptyParametersGatherer, | ||
SfdxCommandlet, | ||
SfdxCommandletExecutor, | ||
SfdxWorkspaceChecker | ||
} from '../commands'; | ||
import { nls } from '../messages'; | ||
import { notificationService, ProgressNotification } from '../notifications'; | ||
import { taskViewService } from '../statuses'; | ||
import { telemetryService } from '../telemetry'; | ||
import { getRootWorkspacePath, hasRootWorkspace, OrgAuthInfo } from '../util'; | ||
|
||
export class ForceDescribeMetadataExecutor extends SfdxCommandletExecutor< | ||
string | ||
> { | ||
private outputPath: string; | ||
|
||
public constructor(outputPath: string) { | ||
super(); | ||
this.outputPath = outputPath; | ||
} | ||
|
||
public build(data: {}): Command { | ||
return new SfdxCommandBuilder() | ||
.withArg('force:mdapi:describemetadata') | ||
.withJson() | ||
.withFlag('-f', this.outputPath) | ||
.withLogName('force_describe_metadata') | ||
.build(); | ||
} | ||
|
||
public execute(response: ContinueResponse<string>): void { | ||
const startTime = process.hrtime(); | ||
const cancellationTokenSource = new vscode.CancellationTokenSource(); | ||
const cancellationToken = cancellationTokenSource.token; | ||
|
||
const execution = new CliCommandExecutor(this.build(response.data), { | ||
cwd: getRootWorkspacePath() | ||
}).execute(cancellationToken); | ||
|
||
execution.processExitSubject.subscribe(async data => { | ||
this.logMetric(execution.command.logName, startTime); | ||
buildTypesList(this.outputPath); | ||
}); | ||
notificationService.reportExecutionError( | ||
execution.command.toString(), | ||
(execution.stderrSubject as any) as Observable<Error | undefined> | ||
); | ||
channelService.streamCommandOutput(execution); | ||
ProgressNotification.show(execution, cancellationTokenSource); | ||
taskViewService.addCommandExecution(execution, cancellationTokenSource); | ||
} | ||
} | ||
|
||
const workspaceChecker = new SfdxWorkspaceChecker(); | ||
const parameterGatherer = new EmptyParametersGatherer(); | ||
|
||
export async function forceDescribeMetadata(outputPath?: string) { | ||
if (isNullOrUndefined(outputPath)) { | ||
outputPath = await getMetadataTypesPath(); | ||
} | ||
const describeExecutor = new ForceDescribeMetadataExecutor(outputPath!); | ||
const commandlet = new SfdxCommandlet( | ||
workspaceChecker, | ||
parameterGatherer, | ||
describeExecutor | ||
); | ||
await commandlet.run(); | ||
} | ||
|
||
export async function getMetadataTypesPath(): Promise<string | undefined> { | ||
if (hasRootWorkspace()) { | ||
const workspaceRootPath = getRootWorkspacePath(); | ||
const defaultUsernameOrAlias = await OrgAuthInfo.getDefaultUsernameOrAlias(); | ||
const defaultUsernameIsSet = typeof defaultUsernameOrAlias !== 'undefined'; | ||
|
||
if (defaultUsernameIsSet) { | ||
const username = await OrgAuthInfo.getUsername(defaultUsernameOrAlias!); | ||
const metadataTypesPath = path.join( | ||
workspaceRootPath, | ||
'.sfdx', | ||
'orgs', | ||
username, | ||
'metadata', | ||
'metadataTypes.json' | ||
); | ||
return metadataTypesPath; | ||
} else { | ||
const err = nls.localize('error_no_default_username'); | ||
telemetryService.sendError(err); | ||
throw new Error(err); | ||
} | ||
} else { | ||
const err = nls.localize('cannot_determine_workspace'); | ||
telemetryService.sendError(err); | ||
throw new Error(err); | ||
} | ||
} | ||
|
||
export type MetadataObject = { | ||
directoryName: string; | ||
inFolder: boolean; | ||
metaFile: boolean; | ||
suffix: string; | ||
xmlName: string; | ||
}; | ||
|
||
export function buildTypesList(metadataTypesPath: string): string[] { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would be used to populate the top level child nodes for the metadata types |
||
if (fs.existsSync(metadataTypesPath)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we're parsing a json, I think we should wrap the method on a try catch that surfaces that throws the error and have the code calling this handle surfacing the message. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, that works |
||
const fileData = JSON.parse(fs.readFileSync(metadataTypesPath, 'utf8')); | ||
const metadataObjects = fileData.metadataObjects as MetadataObject[]; | ||
const metadataTypes = []; | ||
for (const metadataObject of metadataObjects) { | ||
if (!isNullOrUndefined(metadataObject.xmlName)) { | ||
metadataTypes.push(metadataObject.xmlName); | ||
} | ||
} | ||
telemetryService.sendMetadataTypes(undefined, { | ||
metadataTypes: metadataTypes.length | ||
}); | ||
return metadataTypes; | ||
} else { | ||
const err = | ||
'There was an error retrieving metadata type information. Refresh the view to retry.'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ruthemmanuelle If we run into an error when trying to retrieve the metadata types from the generated output file, I think we should throw an error. If the user runs into this scenario, they should be able to refresh the view to generate the output file and load the view again. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, I just saw this. Can we tell them how to refresh the view? Or is it obvious? Not sure what this UI looks like. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've taken out this message with my latest change, but at some point we will have to display a message similar to this. I think the view will include a refresh button in the top right corner, so we would want to point the user to that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, it sounds like refreshing will be intuitive, so the message you had (or something similar to it) should be good. Let me know when you're ready to add the new message. |
||
telemetryService.sendError(err); | ||
throw new Error(err); | ||
} | ||
} | ||
|
||
export async function onUsernameChange() { | ||
const metadataTypesPath = await getMetadataTypesPath(); | ||
if ( | ||
!isNullOrUndefined(metadataTypesPath) && | ||
!fs.existsSync(metadataTypesPath) | ||
) { | ||
await forceDescribeMetadata(metadataTypesPath); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -164,6 +164,19 @@ export class TelemetryService { | |
} | ||
} | ||
|
||
public sendMetadataTypes( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be a more generic method that we use to log things that are not errors or command executions ? It could be something like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I think that's a good idea! |
||
properties?: { [key: string]: string }, | ||
measures?: { [key: string]: number } | ||
): void { | ||
if (this.reporter !== undefined && this.isTelemetryEnabled) { | ||
this.reporter.sendTelemetryEvent( | ||
'metadataTypesQuantity', | ||
properties, | ||
measures | ||
); | ||
} | ||
} | ||
|
||
public sendErrorEvent(errorMsg: string, callstack: string): void { | ||
if (this.reporter !== undefined && this.isTelemetryEnabled) { | ||
this.reporter.sendTelemetryEvent('error', { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
/* | ||
* Copyright (c) 2019, 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 { expect } from 'chai'; | ||
import * as fs from 'fs'; | ||
import { SinonStub, stub } from 'sinon'; | ||
import { isNullOrUndefined } from 'util'; | ||
import { nls } from '../../../src/messages'; | ||
import { | ||
buildTypesList, | ||
ForceDescribeMetadataExecutor, | ||
getMetadataTypesPath | ||
} from '../../../src/orgBrowser'; | ||
import { | ||
getRootWorkspacePath, | ||
hasRootWorkspace, | ||
OrgAuthInfo | ||
} from '../../../src/util'; | ||
|
||
describe('Force Describe Metadata', () => { | ||
it('Should build describe metadata command', async () => { | ||
const outputPath = 'outputPath'; | ||
const forceDescribeMetadataExec = new ForceDescribeMetadataExecutor( | ||
outputPath | ||
); | ||
const forceDescribeMetadataCmd = forceDescribeMetadataExec.build({}); | ||
expect(forceDescribeMetadataCmd.toCommand()).to.equal( | ||
`sfdx force:mdapi:describemetadata --json --loglevel fatal -f ${outputPath}` | ||
); | ||
}); | ||
}); | ||
|
||
// tslint:disable:no-unused-expression | ||
describe('getMetadataTypesPath', () => { | ||
let getDefaultUsernameStub: SinonStub; | ||
let getUsernameStub: SinonStub; | ||
const rootWorkspacePath = getRootWorkspacePath(); | ||
beforeEach(() => { | ||
getDefaultUsernameStub = stub(OrgAuthInfo, 'getDefaultUsernameOrAlias'); | ||
getUsernameStub = stub(OrgAuthInfo, 'getUsername'); | ||
}); | ||
afterEach(() => { | ||
getDefaultUsernameStub.restore(); | ||
getUsernameStub.restore(); | ||
}); | ||
|
||
it('returns the path for a given username', async () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpick, change the title to |
||
getDefaultUsernameStub.returns('defaultUsername'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should always return an username in email format e.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I kept the |
||
getUsernameStub.returns('defaultUsername'); | ||
expect(await getMetadataTypesPath()).to.equal( | ||
`${rootWorkspacePath}/.sfdx/orgs/defaultUsername/metadata/metadataTypes.json` | ||
); | ||
}); | ||
|
||
it('should throw an error if default username is not set', async () => { | ||
getDefaultUsernameStub.returns(undefined); | ||
let errorWasThrown = false; | ||
try { | ||
await getMetadataTypesPath(); | ||
} catch (e) { | ||
errorWasThrown = true; | ||
expect(e.message).to.equal(nls.localize('error_no_default_username')); | ||
} finally { | ||
expect(getUsernameStub.called).to.be.false; | ||
expect(errorWasThrown).to.be.true; | ||
} | ||
}); | ||
}); | ||
|
||
describe('build metadata types list', () => { | ||
let readFileStub: SinonStub; | ||
let fileExistStub: SinonStub; | ||
beforeEach(() => { | ||
readFileStub = stub(fs, 'readFileSync'); | ||
fileExistStub = stub(fs, 'existsSync'); | ||
}); | ||
afterEach(() => { | ||
readFileStub.restore(); | ||
fileExistStub.restore(); | ||
}); | ||
it('should return a list of xmlNames when given a list of metadata objects', async () => { | ||
const metadataTypesPath = 'metadataTypesPath'; | ||
fileExistStub.returns(true); | ||
const fileData = JSON.stringify({ | ||
metadataObjects: [ | ||
{ xmlName: 'fakeName1', suffix: 'fakeSuffix1' }, | ||
{ xmlName: 'fakeName2', suffix: 'fakeSuffix2' } | ||
], | ||
extraField1: 'extraData1', | ||
extraField2: 'extraData2' | ||
}); | ||
readFileStub.returns(fileData); | ||
const xmlNames = buildTypesList(metadataTypesPath); | ||
if (!isNullOrUndefined(xmlNames)) { | ||
expect(xmlNames[0]).to.equal('fakeName1'); | ||
expect(xmlNames[1]).to.equal('fakeName2'); | ||
} | ||
}); | ||
it('should throw an error if the file does not exist yet', async () => { | ||
const metadataTypesPath = 'invalidPath'; | ||
fileExistStub.returns(false); | ||
let errorWasThrown = false; | ||
try { | ||
buildTypesList(metadataTypesPath); | ||
} catch (e) { | ||
errorWasThrown = true; | ||
expect(e.message).to.equal( | ||
'There was an error retrieving metadata type information. Refresh the view to retry.' | ||
); | ||
} finally { | ||
expect(errorWasThrown).to.be.true; | ||
} | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should get called whenever the view is refreshed