diff --git a/messages/retrieve.json b/messages/retrieve.json index 6a54ef616..b6726e04f 100644 --- a/messages/retrieve.json +++ b/messages/retrieve.json @@ -23,6 +23,7 @@ "SourceRetrieveError": "Could not retrieve files in the sourcepath%s", "retrieveTimeout": "Your retrieve request did not complete within the specified wait time [%s minutes]. Try again with a longer wait time.", "retrievedSourceHeader": "Retrieved Source", + "retrievedSourceWarningsHeader": "Retrieved Source Warnings", "fullNameTableColumn": "FULL NAME", "typeTableColumn": "TYPE", "workspacePathTableColumn": "PROJECT PATH", diff --git a/src/formatters/retrieveResultFormatter.ts b/src/formatters/retrieveResultFormatter.ts index e3b396d7f..80a356e16 100644 --- a/src/formatters/retrieveResultFormatter.ts +++ b/src/formatters/retrieveResultFormatter.ts @@ -5,12 +5,17 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import { blue } from 'chalk'; +import { blue, yellow } from 'chalk'; import { UX } from '@salesforce/command'; import { Logger, Messages } from '@salesforce/core'; import { get, getString, getNumber } from '@salesforce/ts-types'; import { RetrieveResult, MetadataApiRetrieveStatus } from '@salesforce/source-deploy-retrieve'; -import { FileResponse, RequestStatus, RetrieveMessage } from '@salesforce/source-deploy-retrieve/lib/src/client/types'; +import { + ComponentStatus, + FileResponse, + RequestStatus, + RetrieveMessage, +} from '@salesforce/source-deploy-retrieve/lib/src/client/types'; import { ResultFormatter, ResultFormatterOptions } from './resultFormatter'; Messages.importMessagesDirectory(__dirname); @@ -31,11 +36,14 @@ export interface RetrieveCommandResult { export class RetrieveResultFormatter extends ResultFormatter { protected result: RetrieveResult; protected fileResponses: FileResponse[]; + protected warnings: RetrieveMessage[]; public constructor(logger: Logger, ux: UX, options: ResultFormatterOptions, result: RetrieveResult) { super(logger, ux, options); this.result = result; this.fileResponses = result?.getFileResponses ? result.getFileResponses() : []; + const warnMessages = get(result, 'response.messages', []) as RetrieveMessage | RetrieveMessage[]; + this.warnings = Array.isArray(warnMessages) ? warnMessages : [warnMessages]; } /** @@ -44,12 +52,10 @@ export class RetrieveResultFormatter extends ResultFormatter { * @returns RetrieveCommandResult */ public getJson(): RetrieveCommandResult { - const warnMessages = get(this.result, 'response.messages', []); - const warnings = Array.isArray(warnMessages) ? warnMessages : [warnMessages]; return { inboundFiles: this.fileResponses, packages: [], - warnings, + warnings: this.warnings, response: this.result.response, }; } @@ -64,34 +70,21 @@ export class RetrieveResultFormatter extends ResultFormatter { return; } - this.ux.styledHeader(blue(messages.getMessage('retrievedSourceHeader'))); if (this.isSuccess()) { - if (this.fileResponses?.length) { - this.sortFileResponses(this.fileResponses); - this.asRelativePaths(this.fileResponses); - const columns = [ - { key: 'fullName', label: 'FULL NAME' }, - { key: 'type', label: 'TYPE' }, - { key: 'filePath', label: 'PROJECT PATH' }, - ]; - this.ux.table(this.fileResponses, { columns }); + if (this.warnings.length) { + this.displayWarnings(); + } + this.ux.styledHeader(blue(messages.getMessage('retrievedSourceHeader'))); + const retrievedFiles = this.fileResponses.filter((fr) => fr.state !== ComponentStatus.Failed); + if (retrievedFiles?.length) { + this.displaySuccesses(retrievedFiles); } else { this.ux.log(messages.getMessage('NoResultsFound')); } } else { - const unknownMsg: RetrieveMessage[] = [{ fileName: 'unknown', problem: 'unknown' }]; - const responseMsgs = get(this.result, 'response.messages', unknownMsg) as RetrieveMessage | RetrieveMessage[]; - const errMsgs = Array.isArray(responseMsgs) ? responseMsgs : [responseMsgs]; - const errMsgsForDisplay = errMsgs.reduce((p, c) => `${p}\n${c.fileName}: ${c.problem}`, ''); - this.ux.log(`Retrieve Failed due to: ${errMsgsForDisplay}`); + this.displayErrors(); } - // if (results.status === 'SucceededPartial' && results.successes.length && results.failures.length) { - // this.ux.log(''); - // this.ux.styledHeader(yellow(messages.getMessage('metadataNotFoundWarning'))); - // results.failures.forEach((warning) => this.ux.log(warning.message)); - // } - // Display any package retrievals // if (results.packages && results.packages.length) { // this.logger.styledHeader(this.logger.color.blue('Retrieved Packages')); @@ -109,4 +102,33 @@ export class RetrieveResultFormatter extends ResultFormatter { protected hasComponents(): boolean { return getNumber(this.result, 'components.size', 0) === 0; } + + private displayWarnings(): void { + this.ux.styledHeader(yellow(messages.getMessage('retrievedSourceWarningsHeader'))); + const columns = [ + { key: 'fileName', label: 'FILE NAME' }, + { key: 'problem', label: 'PROBLEM' }, + ]; + this.ux.table(this.warnings, { columns }); + this.ux.log(); + } + + private displaySuccesses(retrievedFiles: FileResponse[]): void { + this.sortFileResponses(retrievedFiles); + this.asRelativePaths(retrievedFiles); + const columns = [ + { key: 'fullName', label: 'FULL NAME' }, + { key: 'type', label: 'TYPE' }, + { key: 'filePath', label: 'PROJECT PATH' }, + ]; + this.ux.table(retrievedFiles, { columns }); + } + + private displayErrors(): void { + const unknownMsg: RetrieveMessage[] = [{ fileName: 'unknown', problem: 'unknown' }]; + const responseMsgs = get(this.result, 'response.messages', unknownMsg) as RetrieveMessage | RetrieveMessage[]; + const errMsgs = Array.isArray(responseMsgs) ? responseMsgs : [responseMsgs]; + const errMsgsForDisplay = errMsgs.reduce((p, c) => `${p}\n${c.fileName}: ${c.problem}`, ''); + this.ux.log(`Retrieve Failed due to: ${errMsgsForDisplay}`); + } } diff --git a/test/commands/source/retrieveResponses.ts b/test/commands/source/retrieveResponses.ts index f16f1094c..b17d3068d 100644 --- a/test/commands/source/retrieveResponses.ts +++ b/test/commands/source/retrieveResponses.ts @@ -9,43 +9,46 @@ import { RetrieveResult } from '@salesforce/source-deploy-retrieve'; import { RequestStatus } from '@salesforce/source-deploy-retrieve/lib/src/client/types'; import { MetadataApiRetrieveStatus } from '@salesforce/source-deploy-retrieve/lib/src/client/types'; +const packageFileProp = { + createdById: '00521000007KA39AAG', + createdByName: 'User User', + createdDate: '2021-04-28T17:12:58.964Z', + fileName: 'unpackaged/package.xml', + fullName: 'unpackaged/package.xml', + id: '', + lastModifiedById: '00521000007KA39AAG', + lastModifiedByName: 'User User', + lastModifiedDate: '2021-04-28T17:12:58.964Z', + manageableState: 'unmanaged', + type: 'Package', +}; + +const apexClassFileProp = { + createdById: '00521000007KA39AAG', + createdByName: 'User User', + createdDate: '2021-04-23T18:55:07.000Z', + fileName: 'unpackaged/classes/ProductController.cls', + fullName: 'ProductController', + id: '01p2100000A6XiqAAF', + lastModifiedById: '00521000007KA39AAG', + lastModifiedByName: 'User User', + lastModifiedDate: '2021-04-27T22:18:05.000Z', + manageableState: 'unmanaged', + type: 'ApexClass', +}; + const baseRetrieveResponse = { done: true, - fileProperties: [ - { - createdById: '00521000007KA39AAG', - createdByName: 'User User', - createdDate: '2021-04-23T18:55:07.000Z', - fileName: 'unpackaged/classes/ProductController.cls', - fullName: 'ProductController', - id: '01p2100000A6XiqAAF', - lastModifiedById: '00521000007KA39AAG', - lastModifiedByName: 'User User', - lastModifiedDate: '2021-04-27T22:18:05.000Z', - manageableState: 'unmanaged', - type: 'ApexClass', - }, - { - createdById: '00521000007KA39AAG', - createdByName: 'User User', - createdDate: '2021-04-28T17:12:58.964Z', - fileName: 'unpackaged/package.xml', - fullName: 'unpackaged/package.xml', - id: '', - lastModifiedById: '00521000007KA39AAG', - lastModifiedByName: 'User User', - lastModifiedDate: '2021-04-28T17:12:58.964Z', - manageableState: 'unmanaged', - type: 'Package', - }, - ], + fileProperties: [apexClassFileProp, packageFileProp], id: '09S21000002jxznEAA', status: 'Succeeded', success: true, zipFile: 'UEsDBBQA...some_long_string', }; -export type RetrieveResponseType = 'success' | 'inProgress' | 'failed' | 'empty'; +const warningMessage = "Entity of type 'ApexClass' named 'ProductController' cannot be found"; + +export type RetrieveResponseType = 'success' | 'inProgress' | 'failed' | 'empty' | 'warnings'; export const getRetrieveResponse = ( type: RetrieveResponseType, @@ -69,6 +72,14 @@ export const getRetrieveResponse = ( response.fileProperties = []; } + if (type === 'warnings') { + response.messages = { + fileName: packageFileProp.fileName, + problem: warningMessage, + }; + response.fileProperties = [packageFileProp]; + } + return response as MetadataApiRetrieveStatus; }; @@ -85,12 +96,24 @@ export const getRetrieveResult = ( fileProps = Array.isArray(fileProps) ? fileProps : [fileProps]; return fileProps .filter((p) => p.type !== 'Package') - .map((comp) => ({ - fullName: comp.fullName, - filePath: comp.fileName, - state: 'Changed', - type: comp.type, - })); + .map((comp) => { + if (type === 'warnings') { + return { + fullName: apexClassFileProp.fullName, + state: 'Failed', + type: apexClassFileProp.type, + error: warningMessage, + problemType: 'Error', + }; + } else { + return { + fullName: comp.fullName, + filePath: comp.fileName, + state: 'Changed', + type: comp.type, + }; + } + }); }, } as RetrieveResult; }; diff --git a/test/formatters/retrieveResultFormatter.test.ts b/test/formatters/retrieveResultFormatter.test.ts index d779b52ba..7e4b1dd1c 100644 --- a/test/formatters/retrieveResultFormatter.test.ts +++ b/test/formatters/retrieveResultFormatter.test.ts @@ -21,6 +21,7 @@ describe('RetrieveResultFormatter', () => { const retrieveResultFailure = getRetrieveResult('failed'); const retrieveResultInProgress = getRetrieveResult('inProgress'); const retrieveResultEmpty = getRetrieveResult('empty'); + const retrieveResultWarnings = getRetrieveResult('warnings'); const logger = Logger.childFromRoot('retrieveTestLogger').useMemoryLogging(); let ux; @@ -88,14 +89,16 @@ describe('RetrieveResultFormatter', () => { expect(formatter.getJson()).to.deep.equal(expectedSuccessResults); }); - it.skip('should return expected json for a success with warnings', async () => { + it('should return expected json for a success with warnings', async () => { + const warnMessages = retrieveResultWarnings.response.messages; + const warnings = Array.isArray(warnMessages) ? warnMessages : [warnMessages]; const expectedSuccessResults: RetrieveCommandResult = { - inboundFiles: retrieveResultSuccess.getFileResponses(), + inboundFiles: retrieveResultWarnings.getFileResponses(), packages: [], - warnings: [], - response: cloneJson(retrieveResultSuccess.response), + warnings, + response: cloneJson(retrieveResultWarnings.response), }; - const formatter = new RetrieveResultFormatter(logger, ux, {}, retrieveResultSuccess); + const formatter = new RetrieveResultFormatter(logger, ux, {}, retrieveResultWarnings); expect(formatter.getJson()).to.deep.equal(expectedSuccessResults); }); }); @@ -133,6 +136,19 @@ describe('RetrieveResultFormatter', () => { expect(logStub.firstCall.args[0]).to.contain('Retrieve Failed due to:'); }); + it('should output as expected for warnings', async () => { + const formatter = new RetrieveResultFormatter(logger, ux, {}, retrieveResultWarnings); + formatter.display(); + // Should call styledHeader for warnings and the standard "Retrieved Source" header + expect(styledHeaderStub.calledTwice).to.equal(true); + expect(logStub.called).to.equal(true); + expect(tableStub.calledOnce).to.equal(true); + expect(styledHeaderStub.firstCall.args[0]).to.contain('Retrieved Source Warnings'); + const warnMessages = retrieveResultWarnings.response.messages; + const warnings = Array.isArray(warnMessages) ? warnMessages : [warnMessages]; + expect(tableStub.firstCall.args[0]).to.deep.equal(warnings); + }); + it('should output a message when no results were returned', async () => { const formatter = new RetrieveResultFormatter(logger, ux, {}, retrieveResultEmpty); formatter.display();