Skip to content

Commit

Permalink
changes
Browse files Browse the repository at this point in the history
  • Loading branch information
smitshah2531 committed Jul 22, 2020
2 parents 7f79755 + 039f1f4 commit b67ea8f
Show file tree
Hide file tree
Showing 10 changed files with 276 additions and 63 deletions.
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,4 @@
"preLaunchTask": "Compile"
}
]
}
}
62 changes: 50 additions & 12 deletions packages/apex-node/src/execute/executeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import { ExecuteAnonymousResponse, ApexExecuteOptions } from '../types';
import { nls } from '../i18n';
import { encodeBody } from './utils';
import * as readline from 'readline';

export class ExecuteService {
public readonly connection: Connection;
Expand All @@ -29,18 +30,7 @@ export class ExecuteService {
public async executeAnonymous(
options: ApexExecuteOptions
): Promise<ExecuteAnonymousResponse> {
let data: string;

if (options.apexFilePath) {
if (!existsSync(options.apexFilePath)) {
throw new Error(
nls.localize('file_not_found_error', options.apexFilePath)
);
}
data = readFileSync(options.apexFilePath, 'utf8');
} else {
data = String(options.apexCode);
}
const data = await this.getApexCode(options);

let count = 0;
while (count < 2) {
Expand All @@ -65,6 +55,53 @@ export class ExecuteService {
}
}

public async getApexCode(options: ApexExecuteOptions): Promise<string> {
if (options.apexCode) {
return String(options.apexCode);
} else if (options.apexFilePath) {
if (!existsSync(options.apexFilePath)) {
throw new Error(
nls.localize('file_not_found_error', options.apexFilePath)
);
}
return readFileSync(options.apexFilePath, 'utf8');
} else if (options.userInput) {
return await this.getUserInput();
} else {
throw new Error(nls.localize('option_exec_anon_error'));
}
}

public async getUserInput(): Promise<string> {
process.stdout.write(nls.localize('exec_anon_input_prompt'));
return new Promise<string>((resolve, reject) => {
const readInterface = readline.createInterface(
process.stdin,
process.stdout
);
const timeout = setTimeout(() => {
reject(new Error(nls.localize('exec_anon_input_timeout')));
readInterface.close();
}, 10000);

let apexCode = '';
readInterface.on('line', (input: string) => {
timeout.refresh();
apexCode = apexCode + input + '\n';
});
readInterface.on('close', () => {
resolve(apexCode);
});
readInterface.on('error', (err: Error) => {
reject(
new Error(
nls.localize('unexpected_exec_anon_input_error', err.message)
)
);
});
});
}

// Tooling API execute anonymous apex REST endpoint was not used because
// it requires multiple api calls to turn on trace flag, execute anonymous apex, and get the generated debug log
private buildExecRequest(data: string): RequestData {
Expand Down Expand Up @@ -106,6 +143,7 @@ export class ExecuteService {
return formattedResponse;
}

// TODO: make these general utils accessible to other classes
public async connectionRequest(
requestData: RequestData
): Promise<SoapResponse> {
Expand Down
8 changes: 7 additions & 1 deletion packages/apex-node/src/i18n/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,11 @@ export const messages = {
'Unexpected error while executing anonymous apex. %s',
file_not_found_error: 'File not found at the specified path: %s',
unexpected_log_get_command_error: 'Unexpected error while getting logs. %s',
num_logs_error: 'Expected number of logs to be greater than 0.'
num_logs_error: 'Expected number of logs to be greater than 0.',
option_exec_anon_error: 'Please specify an option to execute anonymous Apex.',
unexpected_exec_anon_input_error:
'Unexpected error while reading user input. %s',
exec_anon_input_prompt:
'Start typing Apex code. Press the Enter key after each line, then press CTRL+D when finished.\n',
exec_anon_input_timeout: 'Timed out while waiting for user input.'
};
23 changes: 7 additions & 16 deletions packages/apex-node/src/logs/logService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import { Connection } from '@salesforce/core';
import { ApexLogGetOptions } from '../types';
import { QueryResult } from '../types/common';
import { nls } from '../i18n';
import * as fs from 'fs';
import * as path from 'path';
import { createFiles } from '../utils';

const MAX_NUM_LOGS = 25;

Expand All @@ -29,30 +29,21 @@ export class LogService {
logIdList.push(options.logId);
}

const saveLogsMap = new Map();

const connectionRequests = logIdList.map(async id => {
const url = `${this.connection.tooling._baseUrl()}/sobjects/ApexLog/${id}/Body`;
const logRecord = await this.toolingRequest(url);
if (options.outputDir) {
this.writeLog(options.outputDir, logRecord, id);
saveLogsMap.set(path.join(options.outputDir, `${id}.log`), logRecord);
}
return logRecord;
});

const result = await Promise.all(connectionRequests);
return result;
}
createFiles(saveLogsMap);

public async writeLog(
outputDir: string,
logRecord: string,
id: string
): Promise<void> {
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
const filePath = path.join(`${outputDir}`, `${id}.log`);
const stream = fs.createWriteStream(filePath);
stream.write(logRecord);
stream.end();
return result;
}

public async getLogIds(numberOfLogs: number): Promise<string[]> {
Expand Down
1 change: 1 addition & 0 deletions packages/apex-node/src/types/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type ApexExecuteOptions = CommonOptions & {
targetUsername?: string;
apexFilePath?: string;
apexCode?: string | Buffer;
userInput?: boolean;
};

export const soapEnv = 'soapenv:Envelope';
Expand Down
37 changes: 37 additions & 0 deletions packages/apex-node/src/utils/fileSystemHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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 fs from 'fs';
import * as path from 'path';

export function ensureDirectoryExists(filePath: string): void {
if (fs.existsSync(filePath)) {
return;
}
ensureDirectoryExists(path.dirname(filePath));
fs.mkdirSync(filePath);
}

export function ensureFileExists(filePath: string): void {
ensureDirectoryExists(path.dirname(filePath));
fs.closeSync(fs.openSync(filePath, 'w'));
}

/**
* Method to save multiple files on disk.
*
* @param fileMap key = filePath, value = file contents
*/
export function createFiles(fileMap: Map<string, string>): void {
for (const filePath of fileMap.keys()) {
ensureFileExists(filePath);

const writeStream = fs.createWriteStream(filePath);
writeStream.write(fileMap.get(filePath));
writeStream.end();
}
}
8 changes: 8 additions & 0 deletions packages/apex-node/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* 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
*/

export { createFiles } from './fileSystemHandler';
73 changes: 72 additions & 1 deletion packages/apex-node/test/execute/executeService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { AuthInfo, Connection } from '@salesforce/core';
import { MockTestOrgData, testSetup } from '@salesforce/core/lib/testSetup';
import { assert, expect } from 'chai';
import * as fs from 'fs';
import * as readline from 'readline';
import { createSandbox, SinonSandbox, SinonStub } from 'sinon';
import { ExecuteService } from '../../src/execute';
import { nls } from '../../src/i18n';
Expand All @@ -17,7 +18,7 @@ import { ExecAnonResult, SoapResponse } from '../../src/types/execute';

const $$ = testSetup();

describe('Apex Execute Tests', () => {
describe('Apex Execute Tests', async () => {
const testData = new MockTestOrgData();
let mockConnection: Connection;
let sandboxStub: SinonSandbox;
Expand Down Expand Up @@ -287,4 +288,74 @@ describe('Apex Execute Tests', () => {

expect(response).to.eql(expectedResult);
});

it('should throw an error if no option is specified', async () => {
try {
const executeService = new ExecuteService(mockConnection);
await executeService.executeAnonymous({});
assert.fail();
} catch (e) {
assert.equal(nls.localize('option_exec_anon_error'), e.message);
}
});

it('should throw an error if user input fails', async () => {
const errorText = 'This is the error';
const on = (event: string, listener: (err?: Error) => {}) => {
try {
if (event === 'error') {
listener(new Error(errorText));
}
listener();
} catch (e) {
throw e;
}
};
sandboxStub
.stub(readline, 'createInterface')
//@ts-ignore
.returns({ on });

try {
const executeService = new ExecuteService(mockConnection);
await executeService.getUserInput();
} catch (e) {
assert.equal(
nls.localize('unexpected_exec_anon_input_error', errorText),
e.message
);
}
});

it('should process user input correctly', async () => {
const inputText = 'This should be the only text';
const on = (event: string, listener: (input: string) => {}) => {
listener(inputText);
};
sandboxStub
.stub(readline, 'createInterface')
//@ts-ignore
.returns({ on });

const executeService = new ExecuteService(mockConnection);
const text = await executeService.getUserInput();
expect(text).to.equal(`${inputText}\n`);
});

it('should throw error if user is idle', async () => {
const on = (event: string, listener: () => {}) => {
listener();
};
sandboxStub
.stub(readline, 'createInterface')
//@ts-ignore
.returns({ on });
});

try {
const executeService = new ExecuteService(mockConnection);
await executeService.getUserInput();
} catch (e) {
assert.equal(nls.localize('exec_anon_input_timeout'), e.message);
}
});
Loading

0 comments on commit b67ea8f

Please sign in to comment.