Skip to content

Commit a2330ed

Browse files
authored
fix: display retrieve warnings (#121)
Changes the retrieveResultFormatter to display warnings properly
1 parent 95a7601 commit a2330ed

File tree

4 files changed

+128
-66
lines changed

4 files changed

+128
-66
lines changed

messages/retrieve.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"SourceRetrieveError": "Could not retrieve files in the sourcepath%s",
2424
"retrieveTimeout": "Your retrieve request did not complete within the specified wait time [%s minutes]. Try again with a longer wait time.",
2525
"retrievedSourceHeader": "Retrieved Source",
26+
"retrievedSourceWarningsHeader": "Retrieved Source Warnings",
2627
"fullNameTableColumn": "FULL NAME",
2728
"typeTableColumn": "TYPE",
2829
"workspacePathTableColumn": "PROJECT PATH",

src/formatters/retrieveResultFormatter.ts

Lines changed: 48 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,17 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

8-
import { blue } from 'chalk';
8+
import { blue, yellow } from 'chalk';
99
import { UX } from '@salesforce/command';
1010
import { Logger, Messages } from '@salesforce/core';
1111
import { get, getString, getNumber } from '@salesforce/ts-types';
1212
import { RetrieveResult, MetadataApiRetrieveStatus } from '@salesforce/source-deploy-retrieve';
13-
import { FileResponse, RequestStatus, RetrieveMessage } from '@salesforce/source-deploy-retrieve/lib/src/client/types';
13+
import {
14+
ComponentStatus,
15+
FileResponse,
16+
RequestStatus,
17+
RetrieveMessage,
18+
} from '@salesforce/source-deploy-retrieve/lib/src/client/types';
1419
import { ResultFormatter, ResultFormatterOptions } from './resultFormatter';
1520

1621
Messages.importMessagesDirectory(__dirname);
@@ -31,11 +36,14 @@ export interface RetrieveCommandResult {
3136
export class RetrieveResultFormatter extends ResultFormatter {
3237
protected result: RetrieveResult;
3338
protected fileResponses: FileResponse[];
39+
protected warnings: RetrieveMessage[];
3440

3541
public constructor(logger: Logger, ux: UX, options: ResultFormatterOptions, result: RetrieveResult) {
3642
super(logger, ux, options);
3743
this.result = result;
3844
this.fileResponses = result?.getFileResponses ? result.getFileResponses() : [];
45+
const warnMessages = get(result, 'response.messages', []) as RetrieveMessage | RetrieveMessage[];
46+
this.warnings = Array.isArray(warnMessages) ? warnMessages : [warnMessages];
3947
}
4048

4149
/**
@@ -44,12 +52,10 @@ export class RetrieveResultFormatter extends ResultFormatter {
4452
* @returns RetrieveCommandResult
4553
*/
4654
public getJson(): RetrieveCommandResult {
47-
const warnMessages = get(this.result, 'response.messages', []);
48-
const warnings = Array.isArray(warnMessages) ? warnMessages : [warnMessages];
4955
return {
5056
inboundFiles: this.fileResponses,
5157
packages: [],
52-
warnings,
58+
warnings: this.warnings,
5359
response: this.result.response,
5460
};
5561
}
@@ -64,34 +70,21 @@ export class RetrieveResultFormatter extends ResultFormatter {
6470
return;
6571
}
6672

67-
this.ux.styledHeader(blue(messages.getMessage('retrievedSourceHeader')));
6873
if (this.isSuccess()) {
69-
if (this.fileResponses?.length) {
70-
this.sortFileResponses(this.fileResponses);
71-
this.asRelativePaths(this.fileResponses);
72-
const columns = [
73-
{ key: 'fullName', label: 'FULL NAME' },
74-
{ key: 'type', label: 'TYPE' },
75-
{ key: 'filePath', label: 'PROJECT PATH' },
76-
];
77-
this.ux.table(this.fileResponses, { columns });
74+
if (this.warnings.length) {
75+
this.displayWarnings();
76+
}
77+
this.ux.styledHeader(blue(messages.getMessage('retrievedSourceHeader')));
78+
const retrievedFiles = this.fileResponses.filter((fr) => fr.state !== ComponentStatus.Failed);
79+
if (retrievedFiles?.length) {
80+
this.displaySuccesses(retrievedFiles);
7881
} else {
7982
this.ux.log(messages.getMessage('NoResultsFound'));
8083
}
8184
} else {
82-
const unknownMsg: RetrieveMessage[] = [{ fileName: 'unknown', problem: 'unknown' }];
83-
const responseMsgs = get(this.result, 'response.messages', unknownMsg) as RetrieveMessage | RetrieveMessage[];
84-
const errMsgs = Array.isArray(responseMsgs) ? responseMsgs : [responseMsgs];
85-
const errMsgsForDisplay = errMsgs.reduce<string>((p, c) => `${p}\n${c.fileName}: ${c.problem}`, '');
86-
this.ux.log(`Retrieve Failed due to: ${errMsgsForDisplay}`);
85+
this.displayErrors();
8786
}
8887

89-
// if (results.status === 'SucceededPartial' && results.successes.length && results.failures.length) {
90-
// this.ux.log('');
91-
// this.ux.styledHeader(yellow(messages.getMessage('metadataNotFoundWarning')));
92-
// results.failures.forEach((warning) => this.ux.log(warning.message));
93-
// }
94-
9588
// Display any package retrievals
9689
// if (results.packages && results.packages.length) {
9790
// this.logger.styledHeader(this.logger.color.blue('Retrieved Packages'));
@@ -109,4 +102,33 @@ export class RetrieveResultFormatter extends ResultFormatter {
109102
protected hasComponents(): boolean {
110103
return getNumber(this.result, 'components.size', 0) === 0;
111104
}
105+
106+
private displayWarnings(): void {
107+
this.ux.styledHeader(yellow(messages.getMessage('retrievedSourceWarningsHeader')));
108+
const columns = [
109+
{ key: 'fileName', label: 'FILE NAME' },
110+
{ key: 'problem', label: 'PROBLEM' },
111+
];
112+
this.ux.table(this.warnings, { columns });
113+
this.ux.log();
114+
}
115+
116+
private displaySuccesses(retrievedFiles: FileResponse[]): void {
117+
this.sortFileResponses(retrievedFiles);
118+
this.asRelativePaths(retrievedFiles);
119+
const columns = [
120+
{ key: 'fullName', label: 'FULL NAME' },
121+
{ key: 'type', label: 'TYPE' },
122+
{ key: 'filePath', label: 'PROJECT PATH' },
123+
];
124+
this.ux.table(retrievedFiles, { columns });
125+
}
126+
127+
private displayErrors(): void {
128+
const unknownMsg: RetrieveMessage[] = [{ fileName: 'unknown', problem: 'unknown' }];
129+
const responseMsgs = get(this.result, 'response.messages', unknownMsg) as RetrieveMessage | RetrieveMessage[];
130+
const errMsgs = Array.isArray(responseMsgs) ? responseMsgs : [responseMsgs];
131+
const errMsgsForDisplay = errMsgs.reduce<string>((p, c) => `${p}\n${c.fileName}: ${c.problem}`, '');
132+
this.ux.log(`Retrieve Failed due to: ${errMsgsForDisplay}`);
133+
}
112134
}

test/commands/source/retrieveResponses.ts

Lines changed: 58 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,43 +9,46 @@ import { RetrieveResult } from '@salesforce/source-deploy-retrieve';
99
import { RequestStatus } from '@salesforce/source-deploy-retrieve/lib/src/client/types';
1010
import { MetadataApiRetrieveStatus } from '@salesforce/source-deploy-retrieve/lib/src/client/types';
1111

12+
const packageFileProp = {
13+
createdById: '00521000007KA39AAG',
14+
createdByName: 'User User',
15+
createdDate: '2021-04-28T17:12:58.964Z',
16+
fileName: 'unpackaged/package.xml',
17+
fullName: 'unpackaged/package.xml',
18+
id: '',
19+
lastModifiedById: '00521000007KA39AAG',
20+
lastModifiedByName: 'User User',
21+
lastModifiedDate: '2021-04-28T17:12:58.964Z',
22+
manageableState: 'unmanaged',
23+
type: 'Package',
24+
};
25+
26+
const apexClassFileProp = {
27+
createdById: '00521000007KA39AAG',
28+
createdByName: 'User User',
29+
createdDate: '2021-04-23T18:55:07.000Z',
30+
fileName: 'unpackaged/classes/ProductController.cls',
31+
fullName: 'ProductController',
32+
id: '01p2100000A6XiqAAF',
33+
lastModifiedById: '00521000007KA39AAG',
34+
lastModifiedByName: 'User User',
35+
lastModifiedDate: '2021-04-27T22:18:05.000Z',
36+
manageableState: 'unmanaged',
37+
type: 'ApexClass',
38+
};
39+
1240
const baseRetrieveResponse = {
1341
done: true,
14-
fileProperties: [
15-
{
16-
createdById: '00521000007KA39AAG',
17-
createdByName: 'User User',
18-
createdDate: '2021-04-23T18:55:07.000Z',
19-
fileName: 'unpackaged/classes/ProductController.cls',
20-
fullName: 'ProductController',
21-
id: '01p2100000A6XiqAAF',
22-
lastModifiedById: '00521000007KA39AAG',
23-
lastModifiedByName: 'User User',
24-
lastModifiedDate: '2021-04-27T22:18:05.000Z',
25-
manageableState: 'unmanaged',
26-
type: 'ApexClass',
27-
},
28-
{
29-
createdById: '00521000007KA39AAG',
30-
createdByName: 'User User',
31-
createdDate: '2021-04-28T17:12:58.964Z',
32-
fileName: 'unpackaged/package.xml',
33-
fullName: 'unpackaged/package.xml',
34-
id: '',
35-
lastModifiedById: '00521000007KA39AAG',
36-
lastModifiedByName: 'User User',
37-
lastModifiedDate: '2021-04-28T17:12:58.964Z',
38-
manageableState: 'unmanaged',
39-
type: 'Package',
40-
},
41-
],
42+
fileProperties: [apexClassFileProp, packageFileProp],
4243
id: '09S21000002jxznEAA',
4344
status: 'Succeeded',
4445
success: true,
4546
zipFile: 'UEsDBBQA...some_long_string',
4647
};
4748

48-
export type RetrieveResponseType = 'success' | 'inProgress' | 'failed' | 'empty';
49+
const warningMessage = "Entity of type 'ApexClass' named 'ProductController' cannot be found";
50+
51+
export type RetrieveResponseType = 'success' | 'inProgress' | 'failed' | 'empty' | 'warnings';
4952

5053
export const getRetrieveResponse = (
5154
type: RetrieveResponseType,
@@ -69,6 +72,14 @@ export const getRetrieveResponse = (
6972
response.fileProperties = [];
7073
}
7174

75+
if (type === 'warnings') {
76+
response.messages = {
77+
fileName: packageFileProp.fileName,
78+
problem: warningMessage,
79+
};
80+
response.fileProperties = [packageFileProp];
81+
}
82+
7283
return response as MetadataApiRetrieveStatus;
7384
};
7485

@@ -85,12 +96,24 @@ export const getRetrieveResult = (
8596
fileProps = Array.isArray(fileProps) ? fileProps : [fileProps];
8697
return fileProps
8798
.filter((p) => p.type !== 'Package')
88-
.map((comp) => ({
89-
fullName: comp.fullName,
90-
filePath: comp.fileName,
91-
state: 'Changed',
92-
type: comp.type,
93-
}));
99+
.map((comp) => {
100+
if (type === 'warnings') {
101+
return {
102+
fullName: apexClassFileProp.fullName,
103+
state: 'Failed',
104+
type: apexClassFileProp.type,
105+
error: warningMessage,
106+
problemType: 'Error',
107+
};
108+
} else {
109+
return {
110+
fullName: comp.fullName,
111+
filePath: comp.fileName,
112+
state: 'Changed',
113+
type: comp.type,
114+
};
115+
}
116+
});
94117
},
95118
} as RetrieveResult;
96119
};

test/formatters/retrieveResultFormatter.test.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ describe('RetrieveResultFormatter', () => {
2121
const retrieveResultFailure = getRetrieveResult('failed');
2222
const retrieveResultInProgress = getRetrieveResult('inProgress');
2323
const retrieveResultEmpty = getRetrieveResult('empty');
24+
const retrieveResultWarnings = getRetrieveResult('warnings');
2425

2526
const logger = Logger.childFromRoot('retrieveTestLogger').useMemoryLogging();
2627
let ux;
@@ -88,14 +89,16 @@ describe('RetrieveResultFormatter', () => {
8889
expect(formatter.getJson()).to.deep.equal(expectedSuccessResults);
8990
});
9091

91-
it.skip('should return expected json for a success with warnings', async () => {
92+
it('should return expected json for a success with warnings', async () => {
93+
const warnMessages = retrieveResultWarnings.response.messages;
94+
const warnings = Array.isArray(warnMessages) ? warnMessages : [warnMessages];
9295
const expectedSuccessResults: RetrieveCommandResult = {
93-
inboundFiles: retrieveResultSuccess.getFileResponses(),
96+
inboundFiles: retrieveResultWarnings.getFileResponses(),
9497
packages: [],
95-
warnings: [],
96-
response: cloneJson(retrieveResultSuccess.response),
98+
warnings,
99+
response: cloneJson(retrieveResultWarnings.response),
97100
};
98-
const formatter = new RetrieveResultFormatter(logger, ux, {}, retrieveResultSuccess);
101+
const formatter = new RetrieveResultFormatter(logger, ux, {}, retrieveResultWarnings);
99102
expect(formatter.getJson()).to.deep.equal(expectedSuccessResults);
100103
});
101104
});
@@ -133,6 +136,19 @@ describe('RetrieveResultFormatter', () => {
133136
expect(logStub.firstCall.args[0]).to.contain('Retrieve Failed due to:');
134137
});
135138

139+
it('should output as expected for warnings', async () => {
140+
const formatter = new RetrieveResultFormatter(logger, ux, {}, retrieveResultWarnings);
141+
formatter.display();
142+
// Should call styledHeader for warnings and the standard "Retrieved Source" header
143+
expect(styledHeaderStub.calledTwice).to.equal(true);
144+
expect(logStub.called).to.equal(true);
145+
expect(tableStub.calledOnce).to.equal(true);
146+
expect(styledHeaderStub.firstCall.args[0]).to.contain('Retrieved Source Warnings');
147+
const warnMessages = retrieveResultWarnings.response.messages;
148+
const warnings = Array.isArray(warnMessages) ? warnMessages : [warnMessages];
149+
expect(tableStub.firstCall.args[0]).to.deep.equal(warnings);
150+
});
151+
136152
it('should output a message when no results were returned', async () => {
137153
const formatter = new RetrieveResultFormatter(logger, ux, {}, retrieveResultEmpty);
138154
formatter.display();

0 commit comments

Comments
 (0)