sfmc-dataloader 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cli.mjs CHANGED
@@ -18,6 +18,7 @@ import { parseExportBasename } from './filename.mjs';
18
18
  import { importFromFile } from './import-de.mjs';
19
19
  import { clearDataExtensionRows } from './clear-de.mjs';
20
20
  import { confirmClearBeforeImport } from './confirm-clear.mjs';
21
+ import { getDeRowCount } from './row-count.mjs';
21
22
  import { multiBuExport } from './multi-bu-export.mjs';
22
23
  import { crossBuImport } from './cross-bu-import.mjs';
23
24
  import { initDebugLogger } from './debug-logger.mjs';
@@ -398,9 +399,6 @@ export async function main(argv) {
398
399
  acceptRiskFlag: acceptRisk,
399
400
  isTTY: process.stdin.isTTY === true,
400
401
  });
401
- for (const deKey of deKeys) {
402
- await clearDataExtensionRows(sdk.soap, deKey);
403
- }
404
402
  }
405
403
  for (const deKey of deKeys) {
406
404
  const candidates = await findImportCandidates(dataDir, deKey);
@@ -412,6 +410,23 @@ export async function main(argv) {
412
410
  }
413
411
  const filePath =
414
412
  candidates.length === 1 ? candidates[0] : await pickLatestByMtime(candidates);
413
+
414
+ const countBefore = await getDeRowCount(sdk, deKey);
415
+ console.error(
416
+ `Row count before import: ${countBefore ?? '(unavailable)'} (DE "${deKey}")`,
417
+ );
418
+
419
+ if (clear) {
420
+ if (countBefore === 0) {
421
+ console.error(
422
+ `Skipping clear-data for DE "${deKey}" — DE is already empty.`,
423
+ );
424
+ } else {
425
+ await clearDataExtensionRows(sdk.soap, deKey);
426
+ console.warn(`Cleared data: DE "${deKey}"`);
427
+ }
428
+ }
429
+
415
430
  const n = await importFromFile(sdk, {
416
431
  filePath,
417
432
  deKey,
@@ -419,6 +434,23 @@ export async function main(argv) {
419
434
  });
420
435
  const rel = projectRelativePosix(projectRoot, filePath);
421
436
  console.error(`Imported: ${rel} (${n} rows) -> DE ${deKey}`);
437
+
438
+ const countAfter = await getDeRowCount(sdk, deKey);
439
+ console.error(
440
+ `Row count after import: ${countAfter ?? '(unavailable)'} (DE "${deKey}")`,
441
+ );
442
+ if (countAfter === null) {
443
+ console.error(`Could not verify import result for DE "${deKey}".`);
444
+ } else if (countBefore !== null && countAfter < (countBefore ?? 0) + n) {
445
+ // Insert: expect countBefore + n; upsert on empty: same; upsert on non-empty: expect >= n
446
+ const expected =
447
+ mode === 'insert' || countBefore === 0 ? (countBefore ?? 0) + n : n;
448
+ if (countAfter < expected) {
449
+ console.error(
450
+ `Import result for DE "${deKey}" looks unexpected: expected at least ${expected} rows, got ${countAfter}. Note: the async API may not have committed all rows yet.`,
451
+ );
452
+ }
453
+ }
422
454
  }
423
455
  return 0;
424
456
  }
@@ -448,13 +480,27 @@ export async function main(argv) {
448
480
  acceptRiskFlag: acceptRisk,
449
481
  isTTY: process.stdin.isTTY === true,
450
482
  });
451
- for (const deKey of keysFromFiles) {
452
- await clearDataExtensionRows(sdk.soap, deKey);
453
- }
454
483
  }
455
484
  for (const filePath of fileList) {
456
485
  const base = path.basename(filePath);
457
486
  const { customerKey } = parseExportBasename(base);
487
+
488
+ const countBefore = await getDeRowCount(sdk, customerKey);
489
+ console.error(
490
+ `Row count before import: ${countBefore ?? '(unavailable)'} (DE "${customerKey}")`,
491
+ );
492
+
493
+ if (clear) {
494
+ if (countBefore === 0) {
495
+ console.error(
496
+ `Skipping clear-data for DE "${customerKey}" — DE is already empty.`,
497
+ );
498
+ } else {
499
+ await clearDataExtensionRows(sdk.soap, customerKey);
500
+ console.warn(`Cleared data: DE "${customerKey}"`);
501
+ }
502
+ }
503
+
458
504
  const n = await importFromFile(sdk, {
459
505
  filePath,
460
506
  deKey: customerKey,
@@ -462,6 +508,22 @@ export async function main(argv) {
462
508
  });
463
509
  const rel = projectRelativePosix(projectRoot, filePath);
464
510
  console.error(`Imported: ${rel} (${n} rows)`);
511
+
512
+ const countAfter = await getDeRowCount(sdk, customerKey);
513
+ console.error(
514
+ `Row count after import: ${countAfter ?? '(unavailable)'} (DE "${customerKey}")`,
515
+ );
516
+ if (countAfter === null) {
517
+ console.error(`Could not verify import result for DE "${customerKey}".`);
518
+ } else {
519
+ const expected =
520
+ mode === 'insert' || countBefore === 0 ? (countBefore ?? 0) + n : n;
521
+ if (countAfter < expected) {
522
+ console.error(
523
+ `Import result for DE "${customerKey}" looks unexpected: expected at least ${expected} rows, got ${countAfter}. Note: the async API may not have committed all rows yet.`,
524
+ );
525
+ }
526
+ }
465
527
  }
466
528
  return 0;
467
529
  }
@@ -12,6 +12,7 @@ import { clearDataExtensionRows } from './clear-de.mjs';
12
12
  import { confirmClearBeforeImport } from './confirm-clear.mjs';
13
13
  import { dataDirectoryForBu, projectRelativePosix } from './paths.mjs';
14
14
  import { buildExportBasename, filesystemSafeTimestamp, parseExportBasename } from './filename.mjs';
15
+ import { getDeRowCount } from './row-count.mjs';
15
16
 
16
17
  /**
17
18
  * @typedef {{ credential: string, bu: string }} CredBuTarget
@@ -184,24 +185,27 @@ export async function crossBuImport(params) {
184
185
  rows = await fetchAllRowObjects(srcSdk, deKey);
185
186
  }
186
187
 
187
- // Clear targets before import (rows already confirmed above)
188
- if (clearBeforeImport) {
189
- for (const { credential, bu } of targets) {
190
- const { mid, authCred } = resolveCredentialAndMid(
191
- mcdevrc,
192
- mcdevAuth,
193
- credential,
194
- bu,
195
- );
196
- const tgtSdk = new SDK(buildSdkAuthObject(authCred, mid), buildSdkOptions(logger));
197
- await clearDataExtensionRows(tgtSdk.soap, deKey);
198
- }
199
- }
200
-
201
188
  for (const { credential, bu } of targets) {
202
189
  const { mid, authCred } = resolveCredentialAndMid(mcdevrc, mcdevAuth, credential, bu);
203
190
  const tgtSdk = new SDK(buildSdkAuthObject(authCred, mid), buildSdkOptions(logger));
204
191
 
192
+ const countBefore = await getDeRowCount(tgtSdk, deKey);
193
+ console.error(
194
+ `Row count before import: ${countBefore ?? '(unavailable)'} (${credential}/${bu} DE "${deKey}")`,
195
+ );
196
+
197
+ // Clear target before import (already confirmed above); skip if DE is empty
198
+ if (clearBeforeImport) {
199
+ if (countBefore === 0) {
200
+ console.error(
201
+ `Skipping clear-data for ${credential}/${bu} DE "${deKey}" — DE is already empty.`,
202
+ );
203
+ } else {
204
+ await clearDataExtensionRows(tgtSdk.soap, deKey);
205
+ console.warn(`Cleared data: ${credential}/${bu} DE "${deKey}"`);
206
+ }
207
+ }
208
+
205
209
  // Write a snapshot file in the target BU's data directory.
206
210
  const dir = dataDirectoryForBu(projectRoot, credential, bu);
207
211
  await fs.mkdir(dir, { recursive: true });
@@ -214,6 +218,24 @@ export async function crossBuImport(params) {
214
218
 
215
219
  const imported = await importRowsForDe(tgtSdk, { deKey, rows, mode });
216
220
  console.error(`Imported: ${credential}/${bu} DE ${deKey} (${imported} rows)`);
221
+
222
+ const countAfter = await getDeRowCount(tgtSdk, deKey);
223
+ console.error(
224
+ `Row count after import: ${countAfter ?? '(unavailable)'} (${credential}/${bu} DE "${deKey}")`,
225
+ );
226
+ if (countAfter === null) {
227
+ console.error(
228
+ `Could not verify import result for ${credential}/${bu} DE "${deKey}".`,
229
+ );
230
+ } else {
231
+ const expected =
232
+ mode === 'insert' || countBefore === 0 ? (countBefore ?? 0) + imported : imported;
233
+ if (countAfter < expected) {
234
+ console.error(
235
+ `Import result for ${credential}/${bu} DE "${deKey}" looks unexpected: expected at least ${expected} rows, got ${countAfter}. Note: the async API may not have committed all rows yet.`,
236
+ );
237
+ }
238
+ }
217
239
  }
218
240
  }
219
241
  }
package/lib/index.mjs CHANGED
@@ -14,4 +14,5 @@ export {
14
14
  } from './config.mjs';
15
15
  export { multiBuExport } from './multi-bu-export.mjs';
16
16
  export { crossBuImport } from './cross-bu-import.mjs';
17
+ export { getDeRowCount } from './row-count.mjs';
17
18
  export { projectRelativePosix } from './paths.mjs';
@@ -0,0 +1,20 @@
1
+ import { rowsetGetPath } from './import-routes.mjs';
2
+
3
+ /**
4
+ * Fetch the total row count for a Data Extension without downloading all rows.
5
+ * Uses a pagesize=1 request so only the `count` metadata is transferred.
6
+ * Returns `null` if the count cannot be determined (e.g. permissions error).
7
+ *
8
+ * @param {{ rest: { get: (path: string) => Promise.<any> } }} sdk
9
+ * @param {string} deKey - DE external key
10
+ * @returns {Promise.<number|null>}
11
+ */
12
+ export async function getDeRowCount(sdk, deKey) {
13
+ try {
14
+ const result = await sdk.rest.get(`${rowsetGetPath(deKey)}?$page=1&$pagesize=1`);
15
+ return result?.count ?? 0;
16
+ } catch (ex) {
17
+ console.error(`Could not retrieve row count for DE "${deKey}": ${ex.message}`);
18
+ return null;
19
+ }
20
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sfmc-dataloader",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "SFMC Data Loader CLI (mcdata) to export and import Marketing Cloud Data Extension rows using mcdev project config and sfmc-sdk",
5
5
  "author": "Jörn Berkefeld <joern.berkefeld@gmail.com>",
6
6
  "license": "MIT",