Skip to content

Commit 4db9b18

Browse files
shetzelmshanemc
andauthored
feat: add retrieve in mdapi format (#519)
* feat: add retrieve in mdapi format * feat: add unit tests for mdapi retrieve * chore: adding types for SDR * fix: syntax improvement * fix: do not send empty unpackaged manifest Co-authored-by: Shane McLaughlin <[email protected]>
1 parent 53b6862 commit 4db9b18

File tree

3 files changed

+126
-14
lines changed

3 files changed

+126
-14
lines changed

src/client/metadataApiRetrieve.ts

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
* Licensed under the BSD 3-Clause license.
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
7+
import * as path from 'path';
8+
import * as fs from 'graceful-fs';
9+
import * as unzipper from 'unzipper';
710
import { asBoolean, isString } from '@salesforce/ts-types';
811
import { ConvertOutputConfig, MetadataConverter } from '../convert';
912
import { ComponentSet } from '../collections';
@@ -152,6 +155,38 @@ export class MetadataApiRetrieve extends MetadataTransfer<MetadataApiRetrieveSta
152155
this.canceled = true;
153156
}
154157

158+
public async post(result: MetadataApiRetrieveStatus): Promise<RetrieveResult> {
159+
let components: ComponentSet;
160+
const isMdapiRetrieve = this.options.format === 'metadata';
161+
162+
if (result.status === RequestStatus.Succeeded) {
163+
const zipFileContents = Buffer.from(result.zipFile, 'base64');
164+
if (isMdapiRetrieve) {
165+
const name = this.options.zipFileName || 'unpackaged.zip';
166+
const zipFilePath = path.join(this.options.output, name);
167+
fs.writeFileSync(zipFilePath, zipFileContents);
168+
169+
if (this.options.unzip) {
170+
const dir = await unzipper.Open.buffer(zipFileContents);
171+
const extractPath = path.join(this.options.output, path.parse(name).name);
172+
await dir.extract({ path: extractPath });
173+
}
174+
} else {
175+
components = await this.extract(zipFileContents);
176+
}
177+
}
178+
179+
components ??= new ComponentSet(undefined, this.options.registry);
180+
181+
if (!isMdapiRetrieve) {
182+
// This should only be done when retrieving source format since retrieving
183+
// mdapi format has no conversion.
184+
await this.maybeSaveTempDirectory('source', components);
185+
}
186+
187+
return new RetrieveResult(result, components, this.components);
188+
}
189+
155190
protected async pre(): Promise<AsyncResult> {
156191
const packageNames = this.getPackageNames();
157192

@@ -169,26 +204,21 @@ export class MetadataApiRetrieve extends MetadataTransfer<MetadataApiRetrieveSta
169204
// otherwise don't - it causes errors if undefined or an empty array
170205
if (packageNames?.length) {
171206
requestBody.packageNames = packageNames;
207+
// delete unpackaged when no components and metadata format to prevent
208+
// sending an empty unpackaged manifest.
209+
if (this.options.format === 'metadata' && this.components.size === 0) {
210+
delete requestBody.unpackaged;
211+
}
212+
}
213+
if (this.options.singlePackage) {
214+
requestBody.singlePackage = this.options.singlePackage;
172215
}
173216

174217
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
175218
// @ts-ignore required callback
176219
return connection.metadata.retrieve(requestBody);
177220
}
178221

179-
protected async post(result: MetadataApiRetrieveStatus): Promise<RetrieveResult> {
180-
let components: ComponentSet;
181-
if (result.status === RequestStatus.Succeeded) {
182-
components = await this.extract(Buffer.from(result.zipFile, 'base64'));
183-
}
184-
185-
components = components ?? new ComponentSet(undefined, this.options.registry);
186-
187-
await this.maybeSaveTempDirectory('source', components);
188-
189-
return new RetrieveResult(result, components, this.components);
190-
}
191-
192222
private getPackageNames(): string[] {
193223
return this.getPackageOptions()?.map((pkg) => pkg.name);
194224
}

src/client/types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ComponentSet } from '../collections';
88
import { PackageTypeMembers } from '../collections/types';
99
import { SourcePath } from '../common/types';
1010
import { MetadataComponent, SourceComponent } from '../resolve';
11+
import { SfdxFileFormat } from '../convert';
1112

1213
// ------------------------------------------------
1314
// API results reformatted for source development
@@ -326,6 +327,23 @@ export interface RetrieveOptions {
326327
* A list of package names to retrieve, or package names and their retrieval locations.
327328
*/
328329
packageOptions?: PackageOptions;
330+
/**
331+
* The file format desired for the retrieved files.
332+
*/
333+
format?: SfdxFileFormat;
334+
/**
335+
* Specifies whether only a single package is being retrieved (true) or not (false).
336+
* If false, then more than one package is being retrieved.
337+
*/
338+
singlePackage?: boolean;
339+
/**
340+
* The name of the retrieved zip file containing the source from the org. Only applies when `format: metadata`.
341+
*/
342+
zipFileName?: string;
343+
/**
344+
* Specifies whether to unzip the retrieved zip file. Only applies when `format: metadata`.
345+
*/
346+
unzip?: boolean;
329347
}
330348

331349
export interface MetadataApiDeployOptions {

test/client/metadataApiRetrieve.test.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@
77
import { fail } from 'assert';
88
import { join } from 'path';
99
import { expect } from 'chai';
10-
import { createSandbox, match } from 'sinon';
10+
import * as unzipper from 'unzipper';
11+
import { createSandbox, match, SinonStub } from 'sinon';
1112
import { getString } from '@salesforce/ts-types';
1213
import * as fs from 'graceful-fs';
1314
import {
1415
ComponentSet,
1516
ComponentStatus,
1617
FileResponse,
18+
MetadataApiRetrieve,
1719
MetadataApiRetrieveStatus,
20+
RequestStatus,
1821
RetrieveResult,
1922
SourceComponent,
2023
VirtualTreeContainer,
@@ -407,6 +410,67 @@ describe('MetadataApiRetrieve', () => {
407410
});
408411
});
409412

413+
describe('post', () => {
414+
const output = join('mdapi', 'retrieve', 'dir');
415+
const format = 'metadata';
416+
const zipFile = 'abcd1234';
417+
const zipFileContents = Buffer.from(zipFile, 'base64');
418+
const usernameOrConnection = '[email protected]';
419+
const fakeResults = { status: RequestStatus.Succeeded, zipFile } as MetadataApiRetrieveStatus;
420+
let writeFileStub: SinonStub;
421+
let openBufferStub: SinonStub;
422+
let extractStub: SinonStub;
423+
const mdapiRetrieveExtractStub = env.stub().resolves({});
424+
425+
beforeEach(() => {
426+
writeFileStub = env.stub(fs, 'writeFileSync');
427+
extractStub = env.stub().resolves();
428+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
429+
openBufferStub = env.stub(unzipper.Open, 'buffer').resolves({ extract: extractStub } as any);
430+
});
431+
432+
it('should write the retrieved zip when format=metadata', async () => {
433+
const mdapiRetrieve = new MetadataApiRetrieve({ usernameOrConnection, output, format });
434+
// @ts-ignore overriding private method
435+
mdapiRetrieve.extract = mdapiRetrieveExtractStub;
436+
await mdapiRetrieve.post(fakeResults);
437+
438+
expect(writeFileStub.calledOnce).to.be.true;
439+
expect(writeFileStub.firstCall.args[0]).to.equal(join(output, 'unpackaged.zip'));
440+
expect(writeFileStub.firstCall.args[1]).to.deep.equal(zipFileContents);
441+
expect(openBufferStub.called).to.be.false;
442+
expect(mdapiRetrieveExtractStub.called).to.be.false;
443+
});
444+
445+
it('should unzip the retrieved zip when format=metadata and unzip=true', async () => {
446+
const mdapiRetrieve = new MetadataApiRetrieve({ usernameOrConnection, output, format, unzip: true });
447+
// @ts-ignore overriding private method
448+
mdapiRetrieve.extract = mdapiRetrieveExtractStub;
449+
await mdapiRetrieve.post(fakeResults);
450+
451+
expect(writeFileStub.calledOnce).to.be.true;
452+
expect(writeFileStub.firstCall.args[0]).to.equal(join(output, 'unpackaged.zip'));
453+
expect(writeFileStub.firstCall.args[1]).to.deep.equal(zipFileContents);
454+
expect(openBufferStub.called).to.be.true;
455+
expect(extractStub.called).to.be.true;
456+
expect(mdapiRetrieveExtractStub.called).to.be.false;
457+
});
458+
459+
it('should write the retrieved zip with specified name when format=metadata and zipFileName is set', async () => {
460+
const zipFileName = 'retrievedFiles.zip';
461+
const mdapiRetrieve = new MetadataApiRetrieve({ usernameOrConnection, output, format, zipFileName });
462+
// @ts-ignore overriding private method
463+
mdapiRetrieve.extract = mdapiRetrieveExtractStub;
464+
await mdapiRetrieve.post(fakeResults);
465+
466+
expect(writeFileStub.calledOnce).to.be.true;
467+
expect(writeFileStub.firstCall.args[0]).to.equal(join(output, zipFileName));
468+
expect(writeFileStub.firstCall.args[1]).to.deep.equal(zipFileContents);
469+
expect(openBufferStub.called).to.be.false;
470+
expect(mdapiRetrieveExtractStub.called).to.be.false;
471+
});
472+
});
473+
410474
describe('cancel', () => {
411475
it('should immediately stop polling', async () => {
412476
const component = COMPONENT;

0 commit comments

Comments
 (0)