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 +32 -16
- package/lib/cli.mjs +110 -6
- package/lib/cross-bu-import.mjs +63 -40
- package/lib/index.mjs +1 -0
- package/lib/row-count.mjs +20 -0
- package/package.json +2 -2
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 --
|
|
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 platform
|
|
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
|
|
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
|
-
|
|
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
|
|
105
|
-
|
|
|
106
|
-
| `-p, --project`
|
|
107
|
-
| `--format`
|
|
108
|
-
| `--git`
|
|
109
|
-
| `--mode`
|
|
110
|
-
| `--from <cred>/<bu>`
|
|
111
|
-
| `--to <cred>/<bu>`
|
|
112
|
-
| `--
|
|
113
|
-
| `--
|
|
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
|
}
|
package/lib/cross-bu-import.mjs
CHANGED
|
@@ -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] -
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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,
|
|
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
|
@@ -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.
|
|
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
|
|
47
|
+
"test": "node --test test/*.test.js",
|
|
48
48
|
"lint": "eslint .",
|
|
49
49
|
"lint:fix": "eslint --fix .",
|
|
50
50
|
"format": "prettier --write .",
|