Skip to content

Commit 556f916

Browse files
authored
fix: respect --no-download flag in job export (#181)
* fix: respect --no-download flag in job export (#169) Refactor SDK site archive export into three functions with clear separation of concerns: - siteArchiveExport: server-side job only (no download) - siteArchiveExportToBuffer: export + download to memory - siteArchiveExportToPath: export + download + save to disk The CLI now calls siteArchiveExport directly when --no-download is set, skipping the WebDAV download entirely. * fix: use WaitForJobOptions type for onProgress compatibility The explicit inline type annotation for onProgress was narrower than the SDK's WaitForJobOptions type (execution_status: string vs string | undefined), causing CI typecheck failure.
1 parent bc80709 commit 556f916

7 files changed

Lines changed: 109 additions & 74 deletions

File tree

.changeset/fix-no-download-flag.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@salesforce/b2c-cli': patch
3+
'@salesforce/b2c-tooling-sdk': patch
4+
---
5+
6+
Fix `--no-download` flag on `job export` to actually skip downloading the archive from the instance

packages/b2c-cli/src/commands/job/export.ts

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
import {Flags} from '@oclif/core';
77
import {JobCommand} from '@salesforce/b2c-tooling-sdk/cli';
88
import {
9+
siteArchiveExport,
910
siteArchiveExportToPath,
1011
JobExecutionError,
1112
type SiteArchiveExportResult,
1213
type ExportDataUnitsConfiguration,
14+
type WaitForJobOptions,
1315
} from '@salesforce/b2c-tooling-sdk/operations/jobs';
1416
import {t, withDocs} from '../../i18n/index.js';
1517

@@ -100,10 +102,11 @@ export default class JobExport extends JobCommand<typeof JobExport> {
100102
};
101103

102104
protected operations = {
105+
siteArchiveExport,
103106
siteArchiveExportToPath,
104107
};
105108

106-
async run(): Promise<SiteArchiveExportResult & {localPath?: string}> {
109+
async run(): Promise<SiteArchiveExportResult & {localPath?: string; archiveKept?: boolean}> {
107110
this.requireOAuthCredentials();
108111
this.requireWebDavCredentials();
109112

@@ -167,8 +170,7 @@ export default class JobExport extends JobCommand<typeof JobExport> {
167170
return {
168171
execution: {execution_status: 'finished', exit_status: {code: 'skipped'}},
169172
archiveFilename: '',
170-
archiveKept: false,
171-
} as unknown as SiteArchiveExportResult & {localPath?: string};
173+
} as unknown as SiteArchiveExportResult & {localPath?: string; archiveKept?: boolean};
172174
}
173175

174176
this.log(
@@ -179,25 +181,29 @@ export default class JobExport extends JobCommand<typeof JobExport> {
179181

180182
this.log(t('commands.job.export.dataUnits', 'Data units: {{dataUnits}}', {dataUnits: JSON.stringify(dataUnits)}));
181183

184+
const waitOptions: WaitForJobOptions = {
185+
timeout: timeout ? timeout * 1000 : undefined,
186+
onProgress: (exec, elapsed) => {
187+
if (!this.jsonEnabled()) {
188+
const elapsedSec = Math.floor(elapsed / 1000);
189+
this.log(
190+
t('commands.job.export.progress', ' Status: {{status}} ({{elapsed}}s elapsed)', {
191+
status: exec.execution_status,
192+
elapsed: elapsedSec.toString(),
193+
}),
194+
);
195+
}
196+
},
197+
};
198+
182199
try {
183-
const result = await this.operations.siteArchiveExportToPath(this.instance, dataUnits, output, {
184-
keepArchive: keepArchive || noDownload,
185-
extractZip: !zipOnly,
186-
waitOptions: {
187-
timeout: timeout ? timeout * 1000 : undefined,
188-
onProgress: (exec, elapsed) => {
189-
if (!this.jsonEnabled()) {
190-
const elapsedSec = Math.floor(elapsed / 1000);
191-
this.log(
192-
t('commands.job.export.progress', ' Status: {{status}} ({{elapsed}}s elapsed)', {
193-
status: exec.execution_status,
194-
elapsed: elapsedSec.toString(),
195-
}),
196-
);
197-
}
198-
},
199-
},
200-
});
200+
const result: SiteArchiveExportResult & {localPath?: string; archiveKept?: boolean} = noDownload
201+
? await this.operations.siteArchiveExport(this.instance, dataUnits, {waitOptions})
202+
: await this.operations.siteArchiveExportToPath(this.instance, dataUnits, output, {
203+
keepArchive,
204+
extractZip: !zipOnly,
205+
waitOptions,
206+
});
201207

202208
const durationSec = result.execution.duration ? (result.execution.duration / 1000).toFixed(1) : 'N/A';
203209
this.log(
@@ -215,7 +221,7 @@ export default class JobExport extends JobCommand<typeof JobExport> {
215221
);
216222
}
217223

218-
if (result.archiveKept) {
224+
if (noDownload || result.archiveKept) {
219225
this.log(
220226
t('commands.job.export.archiveKept', 'Archive kept at: Impex/src/instance/{{filename}}', {
221227
filename: result.archiveFilename,

packages/b2c-cli/test/commands/job/export.test.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,11 @@ describe('job export', () => {
112112
expect(result.execution.exit_status.code).to.equal('skipped');
113113
});
114114

115-
it('passes keepArchive when --no-download is set', async () => {
115+
it('calls siteArchiveExport (not siteArchiveExportToPath) when --no-download is set', async () => {
116116
const command: any = await createCommand({
117117
output: './export',
118118
'global-data': 'meta_data',
119119
'no-download': true,
120-
'zip-only': true,
121120
json: true,
122121
});
123122
stubCommon(command);
@@ -128,15 +127,18 @@ describe('job export', () => {
128127
const exportStub = sinon.stub().resolves({
129128
execution: {execution_status: 'finished', exit_status: {code: 'OK'}} as any,
130129
archiveFilename: 'a.zip',
131-
archiveKept: true,
132130
});
133-
command.operations = {...command.operations, siteArchiveExportToPath: exportStub};
131+
const exportToPathStub = sinon.stub().rejects(new Error('Should not be called'));
132+
command.operations = {
133+
...command.operations,
134+
siteArchiveExport: exportStub,
135+
siteArchiveExportToPath: exportToPathStub,
136+
};
134137

135138
await command.run();
136139

137-
const options = exportStub.getCall(0).args[3];
138-
expect(options.keepArchive).to.equal(true);
139-
expect(options.extractZip).to.equal(false);
140+
expect(exportStub.calledOnce).to.equal(true);
141+
expect(exportToPathStub.called).to.equal(false);
140142
});
141143

142144
it('shows job log and errors on JobExecutionError when show-log is true', async () => {

packages/b2c-tooling-sdk/src/operations/content/export.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import * as path from 'node:path';
99
import JSZip from 'jszip';
1010
import type {B2CInstance} from '../../instance/index.js';
1111
import {getLogger} from '../../logging/logger.js';
12-
import {siteArchiveExport} from '../jobs/site-archive.js';
12+
import {siteArchiveExportToBuffer} from '../jobs/site-archive.js';
1313
import {Library, LibraryNode} from './library.js';
1414
import type {
1515
FetchContentLibraryOptions,
@@ -68,10 +68,7 @@ export async function fetchContentLibrary(
6868

6969
const dataUnits = isSiteLibrary ? {sites: {[libraryId]: {content: true}}} : {libraries: {[libraryId]: true}};
7070

71-
const result = await siteArchiveExport(instance, dataUnits, {waitOptions});
72-
if (!result.data) {
73-
throw new Error('No archive data returned from export');
74-
}
71+
const result = await siteArchiveExportToBuffer(instance, dataUnits, {waitOptions});
7572

7673
archiveData = result.data;
7774
const zip = await JSZip.loadAsync(result.data);

packages/b2c-tooling-sdk/src/operations/jobs/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,12 @@ export type {
9090
} from './run.js';
9191

9292
// Site archive import/export
93-
export {siteArchiveImport, siteArchiveExport, siteArchiveExportToPath} from './site-archive.js';
93+
export {
94+
siteArchiveImport,
95+
siteArchiveExport,
96+
siteArchiveExportToBuffer,
97+
siteArchiveExportToPath,
98+
} from './site-archive.js';
9499

95100
export type {
96101
SiteArchiveImportOptions,

packages/b2c-tooling-sdk/src/operations/jobs/site-archive.ts

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -329,8 +329,6 @@ export interface ExportDataUnitsConfiguration {
329329
* Options for site archive export.
330330
*/
331331
export interface SiteArchiveExportOptions {
332-
/** Keep archive on instance after download (default: false) */
333-
keepArchive?: boolean;
334332
/** Wait options for job completion */
335333
waitOptions?: WaitForJobOptions;
336334
}
@@ -343,10 +341,6 @@ export interface SiteArchiveExportResult {
343341
execution: JobExecution;
344342
/** Archive filename on instance */
345343
archiveFilename: string;
346-
/** Archive content as buffer (if downloaded) */
347-
data?: Buffer;
348-
/** Whether archive was kept on instance */
349-
archiveKept: boolean;
350344
}
351345

352346
/**
@@ -384,13 +378,12 @@ export async function siteArchiveExport(
384378
options: SiteArchiveExportOptions = {},
385379
): Promise<SiteArchiveExportResult> {
386380
const logger = getLogger();
387-
const {keepArchive = false, waitOptions} = options;
381+
const {waitOptions} = options;
388382

389383
// Generate archive filename
390384
const timestamp = new Date().toISOString().replace(/[:.-]+/g, '');
391385
const archiveDirName = `${timestamp}_export`;
392386
const zipFilename = `${archiveDirName}.zip`;
393-
const webdavPath = `Impex/src/instance/${zipFilename}`;
394387

395388
logger.debug({jobId: EXPORT_JOB_ID, dataUnits}, `Executing ${EXPORT_JOB_ID} job`);
396389

@@ -450,32 +443,70 @@ export async function siteArchiveExport(
450443
throw error;
451444
}
452445

453-
// Download archive
446+
return {
447+
execution,
448+
archiveFilename: zipFilename,
449+
};
450+
}
451+
452+
/**
453+
* Exports a site archive and downloads it to memory.
454+
*
455+
* Runs the export job on the instance, downloads the archive via WebDAV,
456+
* and returns the data as a Buffer. Optionally keeps the archive on the instance.
457+
*
458+
* @param instance - B2C instance to export from
459+
* @param dataUnits - Data units configuration specifying what to export
460+
* @param options - Export and download options
461+
* @returns Export result with archive data buffer
462+
*
463+
* @example
464+
* ```typescript
465+
* const result = await siteArchiveExportDownload(instance, {
466+
* global_data: { meta_data: true }
467+
* });
468+
* const zip = await JSZip.loadAsync(result.data);
469+
* ```
470+
*/
471+
export async function siteArchiveExportToBuffer(
472+
instance: B2CInstance,
473+
dataUnits: Partial<ExportDataUnitsConfiguration>,
474+
options: SiteArchiveExportOptions & {keepArchive?: boolean} = {},
475+
): Promise<SiteArchiveExportResult & {data: Buffer; archiveKept: boolean}> {
476+
const logger = getLogger();
477+
const {keepArchive = false, ...exportOptions} = options;
478+
479+
const result = await siteArchiveExport(instance, dataUnits, exportOptions);
480+
481+
// Download archive from instance via WebDAV
482+
const webdavPath = `Impex/src/instance/${result.archiveFilename}`;
454483
logger.debug({path: webdavPath}, `Downloading archive: ${webdavPath}`);
455-
const archiveData = await instance.webdav.get(webdavPath);
484+
const data = Buffer.from(await instance.webdav.get(webdavPath));
456485

457-
// Clean up if not keeping
486+
// Clean up from instance if not keeping
458487
if (!keepArchive) {
459488
await instance.webdav.delete(webdavPath);
460489
logger.debug({path: webdavPath}, `Archive deleted: ${webdavPath}`);
461490
}
462491

463492
return {
464-
execution,
465-
archiveFilename: zipFilename,
466-
data: Buffer.from(archiveData),
493+
...result,
494+
data,
467495
archiveKept: keepArchive,
468496
};
469497
}
470498

471499
/**
472-
* Exports a site archive and saves it to a local path.
500+
* Exports a site archive, downloads it, and saves it to a local path.
501+
*
502+
* Runs the export job on the instance, downloads the archive via WebDAV,
503+
* and saves it locally. Optionally keeps the archive on the instance.
473504
*
474505
* @param instance - B2C instance to export from
475506
* @param dataUnits - Data units configuration
476507
* @param outputPath - Local path to save the archive
477-
* @param options - Export options
478-
* @returns Export result
508+
* @param options - Export and download options
509+
* @returns Export result with local path
479510
*
480511
* @example
481512
* ```typescript
@@ -490,16 +521,12 @@ export async function siteArchiveExportToPath(
490521
instance: B2CInstance,
491522
dataUnits: Partial<ExportDataUnitsConfiguration>,
492523
outputPath: string,
493-
options: SiteArchiveExportOptions & {extractZip?: boolean} = {},
494-
): Promise<SiteArchiveExportResult & {localPath: string}> {
524+
options: SiteArchiveExportOptions & {keepArchive?: boolean; extractZip?: boolean} = {},
525+
): Promise<SiteArchiveExportResult & {localPath: string; archiveKept: boolean}> {
495526
const logger = getLogger();
496-
const {extractZip = true, ...exportOptions} = options;
497-
498-
const result = await siteArchiveExport(instance, dataUnits, exportOptions);
527+
const {extractZip = true, ...downloadOptions} = options;
499528

500-
if (!result.data) {
501-
throw new Error('No archive data returned');
502-
}
529+
const result = await siteArchiveExportToBuffer(instance, dataUnits, downloadOptions);
503530

504531
// Determine output handling
505532
const isZipPath = outputPath.endsWith('.zip');

packages/b2c-tooling-sdk/test/operations/jobs/site-archive.test.ts

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,9 @@ describe('operations/jobs/site-archive', () => {
361361
expect(content.toString()).to.include('test-export-data');
362362
});
363363

364-
it('should export without downloading when localPath is not provided', async () => {
364+
it('should run export job without downloading the archive', async () => {
365+
let webdavGetRequested = false;
366+
365367
server.use(
366368
http.post(`${OCAPI_BASE}/jobs/sfcc-site-archive-export/executions`, () => {
367369
return HttpResponse.json({
@@ -379,14 +381,12 @@ describe('operations/jobs/site-archive', () => {
379381
});
380382
}),
381383
http.get(`${WEBDAV_BASE}/Impex/src/instance/*`, () => {
384+
webdavGetRequested = true;
382385
return new HttpResponse(Buffer.from('PK\x03\x04test-data'), {
383386
status: 200,
384387
headers: {'Content-Type': 'application/zip'},
385388
});
386389
}),
387-
http.delete(`${WEBDAV_BASE}/Impex/src/instance/*`, () => {
388-
return new HttpResponse(null, {status: 204});
389-
}),
390390
);
391391

392392
const result = await siteArchiveExport(
@@ -396,7 +396,8 @@ describe('operations/jobs/site-archive', () => {
396396
);
397397

398398
expect(result.execution.id).to.equal('export-2');
399-
expect(result.data).to.be.instanceOf(Buffer);
399+
expect(webdavGetRequested).to.be.false;
400+
expect(result).to.not.have.property('data');
400401
});
401402

402403
it('should throw JobExecutionError when export fails', async () => {
@@ -449,15 +450,6 @@ describe('operations/jobs/site-archive', () => {
449450
is_log_file_existing: false,
450451
});
451452
}),
452-
http.get(`${WEBDAV_BASE}/Impex/src/instance/*`, () => {
453-
return new HttpResponse(Buffer.from('PK\x03\x04test-data'), {
454-
status: 200,
455-
headers: {'Content-Type': 'application/zip'},
456-
});
457-
}),
458-
http.delete(`${WEBDAV_BASE}/Impex/src/instance/*`, () => {
459-
return new HttpResponse(null, {status: 204});
460-
}),
461453
);
462454

463455
const result = await siteArchiveExport(

0 commit comments

Comments
 (0)