sfmc-dataloader 2.1.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # sfmc-dataloader
2
2
 
3
- Command-line tool **`mcdata`** to export and import Salesforce Marketing Cloud Data Extension rows using the same project files as [mcdev](https://github.com/Accenture/sfmc-devtools) (`.mcdevrc.json`, `.mcdev-auth.json`) and [sfmc-sdk](https://www.npmjs.com/package/sfmc-sdk) for REST/SOAP.
3
+ Command-line tool **`mcdata`** to export and import **Salesforce Marketing Cloud Engagement** Data Extension rows using the same project files as [mcdev](https://github.com/Accenture/sfmc-devtools) (`.mcdevrc.json`, `.mcdev-auth.json`) and [sfmc-sdk](https://www.npmjs.com/package/sfmc-sdk) for REST/SOAP.
4
4
 
5
5
  ## Requirements
6
6
 
@@ -41,12 +41,12 @@ Creates one file per BU/DE combination using the same `.mcdata.` naming rules.
41
41
  ### Import — single BU
42
42
 
43
43
  ```bash
44
- mcdata import MyCred/MyBU --de MyDE_CustomerKey --format csv --mode upsert
44
+ mcdata import MyCred/MyBU --de MyDE_CustomerKey --mode upsert
45
45
  ```
46
46
 
47
47
  Imports use the **asynchronous** bulk row API only: `POST` for `--mode insert`, `PUT` for `--mode upsert` (same endpoint path).
48
48
 
49
- Resolves the latest matching export file under `./data/MyCred/MyBU/` for that DE key.
49
+ Resolves the latest matching export file under `./data/MyCred/MyBU/` for that DE key. The file format is detected automatically from the file extension (`.csv`, `.tsv`, `.json`).
50
50
 
51
51
  Import from explicit paths (the DE key is taken from the `.mcdata.` basename):
52
52
 
@@ -56,8 +56,8 @@ mcdata import MyCred/MyBU --file ./data/MyCred/MyBU/encoded%2Bkey.mcdata.2026-04
56
56
 
57
57
  #### Upsert vs insert
58
58
 
59
- - **Upsert** follows the platforms usual behaviour: update when a primary key matches, otherwise insert. For Data Extensions **without** a primary key, upsert may not behave as expected; prefer **`--mode insert`** for those.
60
- - **Insert** always adds new rows. Running import twice with insert can create **duplicate** rows if the same file is applied again—use upsert when keys are defined and you need idempotent runs.
59
+ - **Upsert** follows the platform's usual behaviour: update when a primary key matches, otherwise insert. For Data Extensions **without** a primary key, upsert **will fail**; use **`--mode insert`** for those.
60
+ - **Insert** always adds new rows. Running import twice with insert can create **duplicate** rows if the same file is applied again—use upsert when primary keys are defined and you need to ensure repeated runs always have the same outcome.
61
61
 
62
62
  ### Import — one source BU into multiple target BUs (API mode)
63
63
 
@@ -67,7 +67,7 @@ Use `--from` (one source) and `--to` (repeatable targets) for a cross-BU import
67
67
  mcdata import --from MyCred/Dev --to MyCred/QA --to MyCred/Prod --de Contact_DE
68
68
  ```
69
69
 
70
- Before the import starts you will be offered the option to export the current data from each target BU as a timestamped backup. A download file is also written to each target BU's data directory so there is a traceable record of exactly what was imported.
70
+ An optional pre-import backup exports current target BU data as **timestamped** files (backup filenames always include the timestamp, regardless of `--git`). Use `--backup-before-import` to run the backup without a prompt (CI-safe), or `--no-backup-before-import` to skip it entirely. When neither flag is provided the CLI prompts interactively (TTY only). A snapshot file is also written to each target BU's data directory as a record of what was imported.
71
71
 
72
72
  ### Import — local export files into multiple target BUs (file mode)
73
73
 
@@ -80,6 +80,20 @@ mcdata import --to MyCred/QA --to MyCred/Prod \
80
80
 
81
81
  Multiple files can be supplied to push several DEs in one command. Pass **`--git`** on export if you rely on stable `*.mcdata.<ext>` names for snapshots in this flow.
82
82
 
83
+ ### Backup target DE before import
84
+
85
+ Use `--backup-before-import` on any import command (single-BU or cross-BU) to export a timestamped snapshot of the current target DE rows before the import runs. The backup filename always includes a timestamp regardless of whether `--git` is set.
86
+
87
+ ```bash
88
+ mcdata import MyCred/MyBU --de MyKey --backup-before-import
89
+ ```
90
+
91
+ In CI, combine with `--no-backup-before-import` to suppress any TTY prompt:
92
+
93
+ ```bash
94
+ mcdata import MyCred/MyBU --de MyKey --no-backup-before-import
95
+ ```
96
+
83
97
  ### Clear all rows before import
84
98
 
85
99
  **Dangerous:** removes every row in the target Data Extension(s) before uploading.
@@ -101,16 +115,18 @@ Interactive: type `YES` when prompted. In CI, add `--i-accept-clear-data-risk` a
101
115
 
102
116
  ## Options
103
117
 
104
- | Option | Description |
105
- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------- |
106
- | `-p, --project` | Project root (default: cwd) |
107
- | `--format` | `csv` (default), `tsv`, or `json` |
108
- | `--git` | Stable export filenames: `<key>.mcdata.<ext>` (no timestamp segment) |
109
- | `--mode` | `upsert` (default) or `insert` — async bulk REST API only |
110
- | `--from <cred>/<bu>` | Export: source BU (repeatable). Import API mode: single source BU (use with `--to` and `--de`) |
111
- | `--to <cred>/<bu>` | Import: target BU (repeatable). API mode: use with `--from`/`--de`. File mode: use with `--file` (no `--from` needed) |
112
- | `--clear-before-import` | SOAP `ClearData` before REST import |
113
- | `--i-accept-clear-data-risk` | Non-interactive consent for clear |
118
+ | Option | Description |
119
+ | ----------------------------- | --------------------------------------------------------------------------------------------------------------------- |
120
+ | `-p, --project` | Project root (default: cwd) |
121
+ | `--format` | `csv` (default), `tsv`, or `json` — **export only**; import format is detected from the file extension |
122
+ | `--git` | Stable export filenames: `<key>.mcdata.<ext>` (no timestamp segment) |
123
+ | `--mode` | `upsert` (default) or `insert` — async bulk REST API only |
124
+ | `--from <cred>/<bu>` | Export: source BU (repeatable). Import API mode: single source BU (use with `--to` and `--de`) |
125
+ | `--to <cred>/<bu>` | Import: target BU (repeatable). API mode: use with `--from`/`--de`. File mode: use with `--file` (no `--from` needed) |
126
+ | `--backup-before-import` | Export target DE data as a timestamped backup before import (no prompt; always timestamped) |
127
+ | `--no-backup-before-import` | Skip the backup prompt even in interactive (TTY) sessions |
128
+ | `--clear-before-import` | SOAP `ClearData` before REST import |
129
+ | `--i-accept-clear-data-risk` | Non-interactive consent for clear |
114
130
 
115
131
  Log lines use paths **relative** to the project root (POSIX-style, `./…`) and include **row counts** where applicable.
116
132
 
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';
@@ -53,6 +54,8 @@ Options:
53
54
 
54
55
  Import options:
55
56
  --mode <upsert|insert> Row write mode (default: upsert; async REST bulk API)
57
+ --backup-before-import Export target DE data as a timestamped backup before import (no prompt)
58
+ --no-backup-before-import Skip the backup prompt even in interactive (TTY) sessions
56
59
  --clear-before-import SOAP ClearData before import (destructive; see below)
57
60
  --i-accept-clear-data-risk Non-interactive acknowledgement for --clear-before-import
58
61
 
@@ -96,6 +99,8 @@ export async function main(argv) {
96
99
  to: { type: 'string', multiple: true },
97
100
  git: { type: 'boolean', default: false },
98
101
  mode: { type: 'string' },
102
+ 'backup-before-import': { type: 'boolean', default: false },
103
+ 'no-backup-before-import': { type: 'boolean', default: false },
99
104
  'clear-before-import': { type: 'boolean', default: false },
100
105
  'i-accept-clear-data-risk': { type: 'boolean', default: false },
101
106
  'json-pretty': { type: 'boolean', default: false },
@@ -135,6 +140,12 @@ export async function main(argv) {
135
140
  return 1;
136
141
  }
137
142
 
143
+ const backupBeforeImport = values['backup-before-import']
144
+ ? true
145
+ : values['no-backup-before-import']
146
+ ? false
147
+ : undefined;
148
+
138
149
  const useGit = values.git === true;
139
150
  const fromFlags = values.from ?? [];
140
151
  const toFlags = values.to ?? [];
@@ -264,6 +275,7 @@ export async function main(argv) {
264
275
  targets,
265
276
  format: /** @type {'csv'|'tsv'|'json'} */ (fmt),
266
277
  mode: /** @type {'upsert'|'insert'} */ (mode),
278
+ backupBeforeImport,
267
279
  clearBeforeImport: clear,
268
280
  acceptRiskFlag: acceptRisk,
269
281
  isTTY: process.stdin.isTTY === true,
@@ -330,6 +342,7 @@ export async function main(argv) {
330
342
  deKeys,
331
343
  format: /** @type {'csv'|'tsv'|'json'} */ (fmt),
332
344
  mode: /** @type {'upsert'|'insert'} */ (mode),
345
+ backupBeforeImport,
333
346
  clearBeforeImport: clear,
334
347
  acceptRiskFlag: acceptRisk,
335
348
  isTTY: process.stdin.isTTY === true,
@@ -365,15 +378,27 @@ export async function main(argv) {
365
378
  if (hasDe) {
366
379
  const deKeys = [values.de ?? []].flat();
367
380
  const dataDir = dataDirectoryForBu(projectRoot, credential, bu);
381
+ if (backupBeforeImport === true) {
382
+ for (const deKey of deKeys) {
383
+ const { path: outPath, rowCount } = await exportDataExtensionToFile(sdk, {
384
+ projectRoot,
385
+ credentialName: credential,
386
+ buName: bu,
387
+ deKey,
388
+ format: /** @type {'csv'|'tsv'|'json'} */ (fmt),
389
+ useGit: false,
390
+ });
391
+ console.error(
392
+ `Backup export: ${projectRelativePosix(projectRoot, outPath)} (${rowCount} rows)`,
393
+ );
394
+ }
395
+ }
368
396
  if (clear) {
369
397
  await confirmClearBeforeImport({
370
398
  deKeys,
371
399
  acceptRiskFlag: acceptRisk,
372
400
  isTTY: process.stdin.isTTY === true,
373
401
  });
374
- for (const deKey of deKeys) {
375
- await clearDataExtensionRows(sdk.soap, deKey);
376
- }
377
402
  }
378
403
  for (const deKey of deKeys) {
379
404
  const candidates = await findImportCandidates(dataDir, deKey);
@@ -385,6 +410,23 @@ export async function main(argv) {
385
410
  }
386
411
  const filePath =
387
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
+
388
430
  const n = await importFromFile(sdk, {
389
431
  filePath,
390
432
  deKey,
@@ -392,6 +434,23 @@ export async function main(argv) {
392
434
  });
393
435
  const rel = projectRelativePosix(projectRoot, filePath);
394
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
+ }
395
454
  }
396
455
  return 0;
397
456
  }
@@ -400,19 +459,48 @@ export async function main(argv) {
400
459
  const keysFromFiles = fileList.map(
401
460
  (fp) => parseExportBasename(path.basename(fp)).customerKey,
402
461
  );
462
+ if (backupBeforeImport === true) {
463
+ for (const deKey of keysFromFiles) {
464
+ const { path: outPath, rowCount } = await exportDataExtensionToFile(sdk, {
465
+ projectRoot,
466
+ credentialName: credential,
467
+ buName: bu,
468
+ deKey,
469
+ format: /** @type {'csv'|'tsv'|'json'} */ (fmt),
470
+ useGit: false,
471
+ });
472
+ console.error(
473
+ `Backup export: ${projectRelativePosix(projectRoot, outPath)} (${rowCount} rows)`,
474
+ );
475
+ }
476
+ }
403
477
  if (clear) {
404
478
  await confirmClearBeforeImport({
405
479
  deKeys: keysFromFiles,
406
480
  acceptRiskFlag: acceptRisk,
407
481
  isTTY: process.stdin.isTTY === true,
408
482
  });
409
- for (const deKey of keysFromFiles) {
410
- await clearDataExtensionRows(sdk.soap, deKey);
411
- }
412
483
  }
413
484
  for (const filePath of fileList) {
414
485
  const base = path.basename(filePath);
415
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
+
416
504
  const n = await importFromFile(sdk, {
417
505
  filePath,
418
506
  deKey: customerKey,
@@ -420,6 +508,22 @@ export async function main(argv) {
420
508
  });
421
509
  const rel = projectRelativePosix(projectRoot, filePath);
422
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
+ }
423
527
  }
424
528
  return 0;
425
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
@@ -73,10 +74,11 @@ async function offerPreExportBackup({ targets, deKeys, stdin: stdinStream, stdou
73
74
  * @param {CredBuTarget[]} params.targets
74
75
  * @param {'csv'|'tsv'|'json'} params.format
75
76
  * @param {'upsert'|'insert'} params.mode
77
+ * @param {boolean} [params.backupBeforeImport] - true=always backup, false=never backup, undefined=TTY prompt
76
78
  * @param {boolean} params.clearBeforeImport
77
79
  * @param {boolean} params.acceptRiskFlag
78
80
  * @param {boolean} params.isTTY
79
- * @param {boolean} [params.useGit] - stable snapshot basename (no timestamp)
81
+ * @param {boolean} [params.useGit] - accepted for API compatibility but ignored; snapshot files always use a timestamped name
80
82
  * @param {import('./config.mjs').DebugLogger|null} [params.logger] - debug logger for API requests/responses
81
83
  * @param {NodeJS.ReadableStream} [params.stdin] Override for testing
82
84
  * @param {NodeJS.WritableStream} [params.stdout] Override for testing
@@ -90,10 +92,10 @@ export async function crossBuImport(params) {
90
92
  targets,
91
93
  format,
92
94
  mode,
95
+ backupBeforeImport,
93
96
  clearBeforeImport,
94
97
  acceptRiskFlag,
95
98
  isTTY,
96
- useGit = false,
97
99
  logger = null,
98
100
  } = params;
99
101
  const stdin = params.stdin;
@@ -135,29 +137,29 @@ export async function crossBuImport(params) {
135
137
  }
136
138
 
137
139
  // Optional pre-import backup of target BU data
138
- if (isTTY) {
139
- const doBackup = await offerPreExportBackup({ targets, deKeys, stdin, stdout });
140
- if (doBackup) {
141
- for (const { credential, bu } of targets) {
142
- const { mid, authCred } = resolveCredentialAndMid(
143
- mcdevrc,
144
- mcdevAuth,
145
- credential,
146
- bu,
147
- );
148
- const tgtSdk = new SDK(buildSdkAuthObject(authCred, mid), buildSdkOptions(logger));
149
- for (const deKey of deKeys) {
150
- const { path: outPath, rowCount } = await exportDataExtensionToFile(tgtSdk, {
151
- projectRoot,
152
- credentialName: credential,
153
- buName: bu,
154
- deKey,
155
- format,
156
- useGit: false,
157
- });
158
- const rel = projectRelativePosix(projectRoot, outPath);
159
- console.error(`Backup export: ${rel} (${rowCount} rows)`);
160
- }
140
+ const shouldBackup =
141
+ backupBeforeImport === true
142
+ ? true
143
+ : backupBeforeImport === false
144
+ ? false
145
+ : isTTY
146
+ ? await offerPreExportBackup({ targets, deKeys, stdin, stdout })
147
+ : false;
148
+ if (shouldBackup) {
149
+ for (const { credential, bu } of targets) {
150
+ const { mid, authCred } = resolveCredentialAndMid(mcdevrc, mcdevAuth, credential, bu);
151
+ const tgtSdk = new SDK(buildSdkAuthObject(authCred, mid), buildSdkOptions(logger));
152
+ for (const deKey of deKeys) {
153
+ const { path: outPath, rowCount } = await exportDataExtensionToFile(tgtSdk, {
154
+ projectRoot,
155
+ credentialName: credential,
156
+ buName: bu,
157
+ deKey,
158
+ format,
159
+ useGit: false,
160
+ });
161
+ const rel = projectRelativePosix(projectRoot, outPath);
162
+ console.error(`Backup export: ${rel} (${rowCount} rows)`);
161
163
  }
162
164
  }
163
165
  }
@@ -183,29 +185,32 @@ export async function crossBuImport(params) {
183
185
  rows = await fetchAllRowObjects(srcSdk, deKey);
184
186
  }
185
187
 
186
- // Clear targets before import (rows already confirmed above)
187
- if (clearBeforeImport) {
188
- for (const { credential, bu } of targets) {
189
- const { mid, authCred } = resolveCredentialAndMid(
190
- mcdevrc,
191
- mcdevAuth,
192
- credential,
193
- bu,
194
- );
195
- const tgtSdk = new SDK(buildSdkAuthObject(authCred, mid), buildSdkOptions(logger));
196
- await clearDataExtensionRows(tgtSdk.soap, deKey);
197
- }
198
- }
199
-
200
188
  for (const { credential, bu } of targets) {
201
189
  const { mid, authCred } = resolveCredentialAndMid(mcdevrc, mcdevAuth, credential, bu);
202
190
  const tgtSdk = new SDK(buildSdkAuthObject(authCred, mid), buildSdkOptions(logger));
203
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
+
204
209
  // Write a snapshot file in the target BU's data directory.
205
210
  const dir = dataDirectoryForBu(projectRoot, credential, bu);
206
211
  await fs.mkdir(dir, { recursive: true });
207
212
  const ts = filesystemSafeTimestamp();
208
- const basename = buildExportBasename(deKey, ts, format, useGit);
213
+ const basename = buildExportBasename(deKey, ts, format, false);
209
214
  const snapshotPath = path.join(dir, basename);
210
215
  await fs.writeFile(snapshotPath, serializeRows(rows, format, false), 'utf8');
211
216
  const snapRel = projectRelativePosix(projectRoot, snapshotPath);
@@ -213,6 +218,24 @@ export async function crossBuImport(params) {
213
218
 
214
219
  const imported = await importRowsForDe(tgtSdk, { deKey, rows, mode });
215
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
+ }
216
239
  }
217
240
  }
218
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.1.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",
@@ -44,7 +44,7 @@
44
44
  "sfmc-sdk": "3.0.3"
45
45
  },
46
46
  "scripts": {
47
- "test": "node --test test/**/*.test.js",
47
+ "test": "node --test test/*.test.js",
48
48
  "lint": "eslint .",
49
49
  "lint:fix": "eslint --fix .",
50
50
  "format": "prettier --write .",