diff --git a/README.md b/README.md index 125e4cc..5cdf29b 100644 --- a/README.md +++ b/README.md @@ -53,8 +53,7 @@ sfdx plugins:link ## Commands - -- [`sfdx oa:apex:log:delete [-c] [-a] [-u ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-oaapexlogdelete--c--a--u-string---apiversion-string---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) +* [`sfdx oa:apex:log:delete [-c] [-a] [-u ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-oaapexlogdelete--c--a--u-string---apiversion-string---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) ## `sfdx oa:apex:log:delete [-c] [-a] [-u ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]` @@ -62,7 +61,7 @@ delete ApexLog entries from Your org ``` USAGE - $ sfdx oa:apex:log:delete [-c] [-a] [-u ] [--apiversion ] [--json] [--loglevel + $ sfdx oa:apex:log:delete [-c] [-a] [-u ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL] OPTIONS @@ -103,8 +102,16 @@ EXAMPLES Number of ApexLog records to be deleted: 7 Delete job is started. Id of the job: 7501w000002WEuKAAW To poll status of the job, run command 'sfdx force:data:bulk:status -i 7501w000002WEsi' + + sfdx oa:apex:log:delete --json' + { + "status": 0, + "result": { + "numberOfQueriedLogs": 3, + "jobID": "7501w000002WnWcAAK" + } + } ``` _See code: [lib\commands\oa\apex\log\delete.js](https://github.com/osieckiAdam/osiecki-sfdx-plugins/blob/v0.1.1/lib\commands\oa\apex\log\delete.js)_ - diff --git a/messages/delete.json b/messages/delete.json index 7ef17aa..dd627fa 100644 --- a/messages/delete.json +++ b/messages/delete.json @@ -5,7 +5,7 @@ "recordsLenghtMessage": "Number of ApexLog records to be deleted: %i", "jobStartedMessage": "Delete job is started. Id of the job: %s", "jobStartedMessageAsync": "To poll status of the job, run command 'sfdx force:data:bulk:status -i %s -u %s", - "jobFinishedMessage": "\nJob is finished, status of the job is: %s\nTotal processing time was %i ms", + "jobFinishedMessage": "\nJob is finished", "failedRecordsMessage": "Number of failed records: %i", "successMessage": "All records were deleted sucessfully", "noLogsMessage": "There are no Apex logs on Your org!", diff --git a/package.json b/package.json index 8e308ce..2ee4281 100644 --- a/package.json +++ b/package.json @@ -8,23 +8,25 @@ "@oclif/command": "^1", "@oclif/config": "^1", "@oclif/errors": "^1", - "@salesforce/command": "^1.4.1", - "@salesforce/core": "^1.3.2", + "@salesforce/command": "^2", + "@salesforce/core": "^2", + "@types/jsforce": "^1.9.13", "tslib": "^1" }, "devDependencies": { "@oclif/dev-cli": "^1", - "@oclif/plugin-help": "^2", + "@oclif/plugin-help": "^2.2.3", "@oclif/test": "^1", "@salesforce/dev-config": "1.4.1", - "@types/chai": "^4", + "@types/chai": "^4.2.7", "@types/mocha": "^5", - "@types/node": "^10", - "chai": "^4", + "@types/node": "^10.17.13", + "chai": "^4.2.0", "globby": "^8", - "mocha": "^5", + "mocha": "^5.2.0", "nyc": "^14", "rimraf": "^3.0.0", + "testdouble": "^3.12.4", "ts-node": "^8", "tslint": "^5" }, diff --git a/src/commands/oa/apex/log/delete.ts b/src/commands/oa/apex/log/delete.ts index cdbfbe9..e156cad 100644 --- a/src/commands/oa/apex/log/delete.ts +++ b/src/commands/oa/apex/log/delete.ts @@ -1,100 +1,137 @@ -import { SfdxCommand, flags, core } from '@salesforce/command'; +import { core, flags, SfdxCommand } from '@salesforce/command'; import { AnyJson } from '@salesforce/ts-types'; +import { Batch } from 'jsforce'; core.Messages.importMessagesDirectory(__dirname); const messages = core.Messages.loadMessages('osiecki-sfdx-plugins', 'delete'); export default class Delete extends SfdxCommand { - - public static description = messages.getMessage('deleteCommandDescription'); - protected static requiresUsername = true; - protected static flagsConfig = { - checkonly: flags.boolean({ char: 'c', description: messages.getMessage('checkOnlyFlagDescription') }), - async: flags.boolean({ char: 'a', description: messages.getMessage('asyncFlagDescription') }) - }; - public static examples = [ - ` -sfdx oa:apex:log:delete + public static description = messages.getMessage('deleteCommandDescription'); + public static examples = [ + ` +sfdx oa:apex:log:delete Number of ApexLog records to be deleted: 100 Delete job is started. Id of the job: 7501w000002WA2EAAW Processed records: 100 / 100 Job is finished, status of the job is: Completed Total processing time was 2154 ms All records were deleted sucessfully`, - ` + ` sfdx oa:apex:log:delete -c -u username@your.org Number of ApexLog records to be deleted: 10`, - ` + ` sfdx oa:apex:log:delete --async Number of ApexLog records to be deleted: 7 Delete job is started. Id of the job: 7501w000002WEuKAAW - To poll status of the job, run command 'sfdx force:data:bulk:status -i 7501w000002WEsi'` - ]; + To poll status of the job, run command 'sfdx force:data:bulk:status -i 7501w000002WEsi'`, + ` +sfdx oa:apex:log:delete --json' + { + "status": 0, + "result": { + "numberOfQueriedLogs": 3, + "jobID": "7501w000002WnWcAAK" + } + } + ` + ]; - public async run(): Promise { - let resultsLength = 0; - const that = this; - const conn = this.org.getConnection(); + protected static requiresUsername = true; + protected static flagsConfig = { + checkonly: flags.boolean({ char: 'c', description: messages.getMessage('checkOnlyFlagDescription') }), + async: flags.boolean({ char: 'a', description: messages.getMessage('asyncFlagDescription') }) + }; - conn - .sobject("ApexLog") - .find({}, ["Id"]) - .sort({ LogLength: -1 }) - .execute({ autoFetch: true, maxFetch: 10000 }, function (err, records) { - if (err) { - return console.error(err); - } - resultsLength = records.length; + public async run(): Promise { - let job = conn.bulk.createJob("ApexLog", "delete"); - if (records.length > 0) { - that.ux.log(messages.getMessage('recordsLenghtMessage', [records.length])); - if (!that.flags.checkonly) { - let batch = job.createBatch(); - batch.execute(records); - batch.on("queue", batchInfo => { - that.ux.log(messages.getMessage("jobStartedMessage", [batchInfo.jobId])); - if (that.flags.async) { - return that.ux.log(messages.getMessage("jobStartedMessageAsync", [batchInfo.jobId, that.flags.targetusername])); - } - batch.poll(2000, 10000000); - }); - batch.on("progress", response => { - batch.check().then(batchInfo => { - process.stdout.write( - "\x1Bc\rProcessed records: " + - //@ts-ignore - batchInfo.numberRecordsProcessed + - " / " + - records.length - ); - }); - }); - batch.on("response", response => { - batch.check().then(batchDetails => { - that.ux.log( - //@ts-ignore - messages.getMessage("jobFinishedMessage", [batchDetails.state, batchDetails.totalProcessingTime]) - ); - //@ts-ignore - if (batchDetails.numberRecordsFailed !== "0") { - that.ux.log( - //@ts-ignore - messages.getMessage("failedRecordsMessage", [batchDetails.numberRecordsFailed]) - ); - } else { - that.ux.log(messages.getMessage("successMessage")); - } - }); - }); - batch.on("error", response => { - return console.error(response); - }); - } - } else { - that.ux.log(messages.getMessage('noLogsMessage')); - } - }); - return { numberOfQueriedLogs: resultsLength }; + let response: AnyJson = ({ numberOfQueriedLogs: 0 }); + const conn = this.org.getConnection(); + const apexLogRecords = await this.getAllApexLogs(); + + if (apexLogRecords.length > 0) { + this.ux.log(messages.getMessage('recordsLenghtMessage', [apexLogRecords.length])); + if (this.flags.checkonly) { + response = ({ numberOfQueriedLogs: apexLogRecords.length }); + } else { + const batch = this.createBatch(conn, apexLogRecords); + response = await this.trackBatchProgress(batch, apexLogRecords.length); + } + } else { + this.ux.log(messages.getMessage('noLogsMessage')); + response = ({ numberOfQueriedLogs: 0 }); } + return response; + } + + public async getAllApexLogs() { + const response = this.org.getConnection() + .sobject('ApexLog') + .find({}, ['Id']) + .execute({ autoFetch: true, maxFetch: 10000 }, async (err, records) => { + if (err) { + return console.error(err); + } + return records; + }); + return response; + } + + public createBatch(conn: core.Connection, records: Array<{ Id?: string; }>): Batch { + return conn.bulk.createJob('ApexLog', 'delete').createBatch().execute(records); + } + + private async trackBatchProgress(batch: Batch, totalNumberOfRecords: number) { + let jobId: string; + interface JobResultItem { + id: string; + success: boolean; + errors: AnyJson[]; + } + + const failedRecords: JobResultItem[] = new Array(); + const response = await new Promise((resolve, reject) => { + batch.on('queue', batchDetails => { + + jobId = batchDetails.jobId; + this.ux.log(messages.getMessage('jobStartedMessage', [jobId])); + if (this.flags.async) { + this.ux.log(messages.getMessage('jobStartedMessageAsync', [jobId, this.flags.targetusername])); + resolve({ numberOfQueriedLogs: totalNumberOfRecords, jobID: jobId }); + } else { + batch.poll(2000, 10000000); + if (!this.flags.json) { + batch.on('progress', batchInfo => { + this.logProgress(batchInfo.numberRecordsProcessed, totalNumberOfRecords); + }); + } + batch.on('response', records => { + records.forEach((element: JobResultItem) => { + if (!element.success) { + failedRecords.push(element); + } + }); + if (!this.flags.json) { + this.logProgress(records.length, totalNumberOfRecords); + } + this.ux.log(messages.getMessage('jobFinishedMessage')); + if (failedRecords.length > 0) { + this.ux.log(messages.getMessage('failedRecordsMessage', [failedRecords.length])); + } else { + this.ux.log(messages.getMessage('successMessage')); + } + resolve({ numberOfQueriedLogs: totalNumberOfRecords, jobID: jobId, numberOfFailedRecords: failedRecords.length }); + }); + batch.on('error', res => { + console.log(res); + reject(res); + }); + } + }); + }); + return response; + } + + private logProgress(processedItems: string, totalItems: number): void { + process.stdout.write('\x1Bc\rProcessed records: ' + processedItems + ' / ' + totalItems); + } } diff --git a/test/commands/apex/log/delete.test.ts b/test/commands/apex/log/delete.test.ts new file mode 100644 index 0000000..8648dfb --- /dev/null +++ b/test/commands/apex/log/delete.test.ts @@ -0,0 +1,179 @@ +import { $$, expect, test } from '@salesforce/command/lib/test'; +import * as deleteClass from '../../../../src/commands/oa/apex/log/delete'; +import { EventEmitter } from 'events'; + +class BatchMock extends EventEmitter { poll() { } } +const mockSubscripton = new BatchMock(); + +describe('apex:log:delete, 2 records queried', () => { + + beforeEach(() => { + $$.SANDBOX.stub(deleteClass.default.prototype, 'getAllApexLogs').callsFake(async () => { + return ([ + { Id: '07L1w000007NcJtEAK' }, { Id: '07L1w000007NcJoEAK' } + ]); + }); + + //@ts-ignore + $$.SANDBOX.stub(deleteClass.default.prototype, 'createBatch').callsFake(() => { + return mockSubscripton; + }); + setTimeout(() => { + mockSubscripton.emit('queue', { jobId: '1231233' }); + }, 600) + + }) + + afterEach(() => { + mockSubscripton.removeAllListeners('queue'); + mockSubscripton.removeAllListeners('progress'); + mockSubscripton.removeAllListeners('error'); + }) + + describe('apex:log:delete', () => { + + beforeEach(() => { + setTimeout(() => { + mockSubscripton.emit('progress', { numberRecordsProcessed: '1' }); + }, 700) + setTimeout(() => { + mockSubscripton.emit('response', [{ id: '07L1w000007NcJtEAK', success: true, errors: [] }, { id: '07L1w000007NcJoEAK', success: true, errors: [] }]); + }, 800) + }) + + test + .withOrg({ username: 'test@org.com' }, true) + .stdout() + .command(['oa:apex:log:delete', '-u', 'test@org.com']) + .it('runs oa:apex:log:delete -u test@org.com', (ctx) => { + expect(ctx.stdout).to.contain('Number of ApexLog records to be deleted: 2'); + expect(ctx.stdout).to.contain('Delete job is started. Id of the job: 1231233'); + expect(ctx.stdout).to.contain('Processed records: 1 / 2'); + expect(ctx.stdout).to.contain('Processed records: 2 / 2'); + expect(ctx.stdout).to.contain('Job is finished'); + expect(ctx.stdout).to.contain('All records were deleted sucessfully'); + }); + + test + .withOrg({ username: 'test@org.com' }, true) + .stdout() + .command(['oa:apex:log:delete', '-u', 'test@org.com', '--json']) + .it('runs oa:apex:log:delete -u test@org.com --json', (ctx) => { + expect(ctx.stdout).to.contain('"numberOfQueriedLogs": 2'); + expect(ctx.stdout).to.contain('"jobID": "1231233"'); + }); + + test + .withOrg({ username: 'test@org.com' }, true) + .stdout() + .command(['oa:apex:log:delete', '-u', 'test@org.com', '-a']) + .it('runs oa:apex:log:delete -u test@org.com -a', (ctx) => { + expect(ctx.stdout).to.contain('Number of ApexLog records to be deleted: 2'); + expect(ctx.stdout).to.contain("Delete job is started. Id of the job: 1231233"); + expect(ctx.stdout).to.contain("To poll status of the job, run command 'sfdx force:data:bulk:status -i 1231233 -u test@org.com"); + }); + + test + .withOrg({ username: 'test@org.com' }, true) + .stdout() + .command(['oa:apex:log:delete', '-u', 'test@org.com', '-a', '--json']) + .it('runs oa:apex:log:delete -u test@org.com -a --json', (ctx) => { + expect(ctx.stdout).to.contain('"numberOfQueriedLogs": 2'); + expect(ctx.stdout).to.contain('"jobID": "1231233"'); + }); + }) + + describe('apex:log:delete, 1 record failed', () => { + + beforeEach(() => { + setTimeout(() => { + mockSubscripton.emit('response', + [{ id: '07L1w000007NcJtEAK', success: true, errors: [] }, { id: '07L1w000007NcJoEAK', success: false, errors: ['test error'] }]); + }, 600) + }) + + test + .withOrg({ username: 'test@org.com' }, true) + .stdout() + .command(['oa:apex:log:delete', '-u', 'test@org.com']) + .it('runs oa:apex:log:delete -u test@org.com', (ctx) => { + expect(ctx.stdout).to.contain('Number of failed records: 1'); + }); + + test + .withOrg({ username: 'test@org.com' }, true) + .stdout() + .command(['oa:apex:log:delete', '-u', 'test@org.com', '--json']) + .it('runs oa:apex:log:delete -u test@org.com --json', (ctx) => { + expect(ctx.stdout).to.contain('"numberOfFailedRecords": 1'); + }); + }) + + describe('apex:log:delete returns error', () => { + + beforeEach(() => { + setTimeout(() => { + mockSubscripton.emit('error', ('test error message')); + }, 600) + }) + + test + .withOrg({ username: 'test@org.com' }, true) + .stdout() + .command(['oa:apex:log:delete', '-u', 'test@org.com']) + .it('runs oa:apex:log:delete -u test@org.com', (ctx) => { + expect(ctx.stdout).to.contain('test error message'); + }); + }); +}); + +describe('apex:log:delete, check only', () => { + + beforeEach(() => { + $$.SANDBOX.stub(deleteClass.default.prototype, 'getAllApexLogs').callsFake(async () => { + return ([ + { Id: '07L1w000007NcJtEAK' }, { Id: '07L1w000007NcJoEAK' } + ]); + }); + }) + + test + .withOrg({ username: 'test@org.com' }, true) + .stdout() + .command(['oa:apex:log:delete', '-u', 'test@org.com', '-c']) + .it('runs oa:apex:log:delete -u test@org.com -c', (ctx) => { + expect(ctx.stdout).to.contain('Number of ApexLog records to be deleted: 2'); + }); + + test + .withOrg({ username: 'test@org.com' }, true) + .stdout() + .command(['oa:apex:log:delete', '-u', 'test@org.com', '-c', '--json']) + .it('runs oa:apex:log:delete -u test@org.com -c --json', (ctx) => { + expect(ctx.stdout).to.contain('"numberOfQueriedLogs": 2'); + }); +}); + +describe('apex:log:delete, zero logs queried', () => { + beforeEach(() => { + $$.SANDBOX.stub(deleteClass.default.prototype, 'getAllApexLogs').callsFake(async () => { + return Promise.resolve([]); + }); + }) + + test + .withOrg({ username: 'test@org.com' }, true) + .stdout() + .command(['oa:apex:log:delete', '-u', 'test@org.com']) + .it('runs oa:apex:log:delete -u test@org.com', (ctx) => { + expect(ctx.stdout).to.contain('There are no Apex logs on Your org!'); + }); + + test + .withOrg({ username: 'test@org.com' }, true) + .stdout() + .command(['oa:apex:log:delete', '-u', 'test@org.com', '--json']) + .it('runs oa:apex:log:delete -u test@org.com --json', (ctx) => { + expect(ctx.stdout).to.contain('"numberOfQueriedLogs": 0'); + }); +});