Skip to content
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

Merged
merged 15 commits into from
Apr 10, 2019
13 changes: 13 additions & 0 deletions packages/salesforcedx-vscode-core/src/orgBrowser/index.ts
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';
154 changes: 154 additions & 0 deletions packages/salesforcedx-vscode-core/src/orgBrowser/orgMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
* 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) {
Copy link
Collaborator Author

@AnanyaJha AnanyaJha Apr 5, 2019

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

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[] {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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

try {
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.sendEventData('Metadata Types Quantity', undefined, {
metadataTypes: metadataTypes.length
});
return metadataTypes;
} catch (e) {
throw e;
}
}

export async function onUsernameChange() {
const metadataTypesPath = await getMetadataTypesPath();
if (
!isNullOrUndefined(metadataTypesPath) &&
!fs.existsSync(metadataTypesPath)
) {
await forceDescribeMetadata(metadataTypesPath);
}
}
10 changes: 10 additions & 0 deletions packages/salesforcedx-vscode-core/src/telemetry/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,16 @@ export class TelemetryService {
}
}

public sendEventData(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to add a test for this in test/vscode-integration/telemetry/index.test.ts since no other method is testing sending measures.

eventName: string,
properties?: { [key: string]: string },
measures?: { [key: string]: number }
): void {
if (this.reporter !== undefined && this.isTelemetryEnabled) {
this.reporter.sendTelemetryEvent(eventName, properties, measures);
}
}

public sendErrorEvent(errorMsg: string, callstack: string): void {
if (this.reporter !== undefined && this.isTelemetryEnabled) {
this.reporter.sendTelemetryEvent('error', {
Expand Down
2 changes: 1 addition & 1 deletion packages/salesforcedx-vscode-core/src/util/authInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from '@salesforce/core';
import * as path from 'path';
import { nls } from '../messages';
import { getRootWorkspacePath, hasRootWorkspace } from './index';
import { getRootWorkspacePath } from './index';
export class OrgAuthInfo {
public static async getDefaultUsernameOrAlias(): Promise<string | undefined> {
try {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* 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 * as path from 'path';
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('should return the path for a given username', async () => {
getDefaultUsernameStub.returns('defaultAlias');
getUsernameStub.returns('test-username1@example.com');
const filePath = path.join(
rootWorkspacePath,
'.sfdx',
'orgs',
'test-username1@example.com',
'metadata',
'metadataTypes.json'
);
expect(await getMetadataTypesPath()).to.equal(filePath);
});

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;
} finally {
expect(errorWasThrown).to.be.true;
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -230,5 +230,19 @@ describe('Telemetry', () => {
};
assert.calledWith(reporter, 'commandExecution', match(expectedData));
});

it('should send correct data format on sendEventData', async () => {
mockContext = new MockContext(true);

const telemetryService = TelemetryService.getInstance();
telemetryService.initializeService(mockContext, machineId);

const eventName = 'eventName';
const property = { property: 'property for event' };
const measure = { measure: 123456 };
telemetryService.sendEventData(eventName, property, measure);

assert.calledWith(reporter, eventName, property, measure);
});
});
});