sfmc-dataloader 2.1.0 → 2.2.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 +42 -0
- package/lib/cross-bu-import.mjs +27 -26
- 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
|
@@ -53,6 +53,8 @@ Options:
|
|
|
53
53
|
|
|
54
54
|
Import options:
|
|
55
55
|
--mode <upsert|insert> Row write mode (default: upsert; async REST bulk API)
|
|
56
|
+
--backup-before-import Export target DE data as a timestamped backup before import (no prompt)
|
|
57
|
+
--no-backup-before-import Skip the backup prompt even in interactive (TTY) sessions
|
|
56
58
|
--clear-before-import SOAP ClearData before import (destructive; see below)
|
|
57
59
|
--i-accept-clear-data-risk Non-interactive acknowledgement for --clear-before-import
|
|
58
60
|
|
|
@@ -96,6 +98,8 @@ export async function main(argv) {
|
|
|
96
98
|
to: { type: 'string', multiple: true },
|
|
97
99
|
git: { type: 'boolean', default: false },
|
|
98
100
|
mode: { type: 'string' },
|
|
101
|
+
'backup-before-import': { type: 'boolean', default: false },
|
|
102
|
+
'no-backup-before-import': { type: 'boolean', default: false },
|
|
99
103
|
'clear-before-import': { type: 'boolean', default: false },
|
|
100
104
|
'i-accept-clear-data-risk': { type: 'boolean', default: false },
|
|
101
105
|
'json-pretty': { type: 'boolean', default: false },
|
|
@@ -135,6 +139,12 @@ export async function main(argv) {
|
|
|
135
139
|
return 1;
|
|
136
140
|
}
|
|
137
141
|
|
|
142
|
+
const backupBeforeImport = values['backup-before-import']
|
|
143
|
+
? true
|
|
144
|
+
: values['no-backup-before-import']
|
|
145
|
+
? false
|
|
146
|
+
: undefined;
|
|
147
|
+
|
|
138
148
|
const useGit = values.git === true;
|
|
139
149
|
const fromFlags = values.from ?? [];
|
|
140
150
|
const toFlags = values.to ?? [];
|
|
@@ -264,6 +274,7 @@ export async function main(argv) {
|
|
|
264
274
|
targets,
|
|
265
275
|
format: /** @type {'csv'|'tsv'|'json'} */ (fmt),
|
|
266
276
|
mode: /** @type {'upsert'|'insert'} */ (mode),
|
|
277
|
+
backupBeforeImport,
|
|
267
278
|
clearBeforeImport: clear,
|
|
268
279
|
acceptRiskFlag: acceptRisk,
|
|
269
280
|
isTTY: process.stdin.isTTY === true,
|
|
@@ -330,6 +341,7 @@ export async function main(argv) {
|
|
|
330
341
|
deKeys,
|
|
331
342
|
format: /** @type {'csv'|'tsv'|'json'} */ (fmt),
|
|
332
343
|
mode: /** @type {'upsert'|'insert'} */ (mode),
|
|
344
|
+
backupBeforeImport,
|
|
333
345
|
clearBeforeImport: clear,
|
|
334
346
|
acceptRiskFlag: acceptRisk,
|
|
335
347
|
isTTY: process.stdin.isTTY === true,
|
|
@@ -365,6 +377,21 @@ export async function main(argv) {
|
|
|
365
377
|
if (hasDe) {
|
|
366
378
|
const deKeys = [values.de ?? []].flat();
|
|
367
379
|
const dataDir = dataDirectoryForBu(projectRoot, credential, bu);
|
|
380
|
+
if (backupBeforeImport === true) {
|
|
381
|
+
for (const deKey of deKeys) {
|
|
382
|
+
const { path: outPath, rowCount } = await exportDataExtensionToFile(sdk, {
|
|
383
|
+
projectRoot,
|
|
384
|
+
credentialName: credential,
|
|
385
|
+
buName: bu,
|
|
386
|
+
deKey,
|
|
387
|
+
format: /** @type {'csv'|'tsv'|'json'} */ (fmt),
|
|
388
|
+
useGit: false,
|
|
389
|
+
});
|
|
390
|
+
console.error(
|
|
391
|
+
`Backup export: ${projectRelativePosix(projectRoot, outPath)} (${rowCount} rows)`,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
368
395
|
if (clear) {
|
|
369
396
|
await confirmClearBeforeImport({
|
|
370
397
|
deKeys,
|
|
@@ -400,6 +427,21 @@ export async function main(argv) {
|
|
|
400
427
|
const keysFromFiles = fileList.map(
|
|
401
428
|
(fp) => parseExportBasename(path.basename(fp)).customerKey,
|
|
402
429
|
);
|
|
430
|
+
if (backupBeforeImport === true) {
|
|
431
|
+
for (const deKey of keysFromFiles) {
|
|
432
|
+
const { path: outPath, rowCount } = await exportDataExtensionToFile(sdk, {
|
|
433
|
+
projectRoot,
|
|
434
|
+
credentialName: credential,
|
|
435
|
+
buName: bu,
|
|
436
|
+
deKey,
|
|
437
|
+
format: /** @type {'csv'|'tsv'|'json'} */ (fmt),
|
|
438
|
+
useGit: false,
|
|
439
|
+
});
|
|
440
|
+
console.error(
|
|
441
|
+
`Backup export: ${projectRelativePosix(projectRoot, outPath)} (${rowCount} rows)`,
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
403
445
|
if (clear) {
|
|
404
446
|
await confirmClearBeforeImport({
|
|
405
447
|
deKeys: keysFromFiles,
|
package/lib/cross-bu-import.mjs
CHANGED
|
@@ -73,10 +73,11 @@ async function offerPreExportBackup({ targets, deKeys, stdin: stdinStream, stdou
|
|
|
73
73
|
* @param {CredBuTarget[]} params.targets
|
|
74
74
|
* @param {'csv'|'tsv'|'json'} params.format
|
|
75
75
|
* @param {'upsert'|'insert'} params.mode
|
|
76
|
+
* @param {boolean} [params.backupBeforeImport] - true=always backup, false=never backup, undefined=TTY prompt
|
|
76
77
|
* @param {boolean} params.clearBeforeImport
|
|
77
78
|
* @param {boolean} params.acceptRiskFlag
|
|
78
79
|
* @param {boolean} params.isTTY
|
|
79
|
-
* @param {boolean} [params.useGit] -
|
|
80
|
+
* @param {boolean} [params.useGit] - accepted for API compatibility but ignored; snapshot files always use a timestamped name
|
|
80
81
|
* @param {import('./config.mjs').DebugLogger|null} [params.logger] - debug logger for API requests/responses
|
|
81
82
|
* @param {NodeJS.ReadableStream} [params.stdin] Override for testing
|
|
82
83
|
* @param {NodeJS.WritableStream} [params.stdout] Override for testing
|
|
@@ -90,10 +91,10 @@ export async function crossBuImport(params) {
|
|
|
90
91
|
targets,
|
|
91
92
|
format,
|
|
92
93
|
mode,
|
|
94
|
+
backupBeforeImport,
|
|
93
95
|
clearBeforeImport,
|
|
94
96
|
acceptRiskFlag,
|
|
95
97
|
isTTY,
|
|
96
|
-
useGit = false,
|
|
97
98
|
logger = null,
|
|
98
99
|
} = params;
|
|
99
100
|
const stdin = params.stdin;
|
|
@@ -135,29 +136,29 @@ export async function crossBuImport(params) {
|
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
// 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
|
-
}
|
|
139
|
+
const shouldBackup =
|
|
140
|
+
backupBeforeImport === true
|
|
141
|
+
? true
|
|
142
|
+
: backupBeforeImport === false
|
|
143
|
+
? false
|
|
144
|
+
: isTTY
|
|
145
|
+
? await offerPreExportBackup({ targets, deKeys, stdin, stdout })
|
|
146
|
+
: false;
|
|
147
|
+
if (shouldBackup) {
|
|
148
|
+
for (const { credential, bu } of targets) {
|
|
149
|
+
const { mid, authCred } = resolveCredentialAndMid(mcdevrc, mcdevAuth, credential, bu);
|
|
150
|
+
const tgtSdk = new SDK(buildSdkAuthObject(authCred, mid), buildSdkOptions(logger));
|
|
151
|
+
for (const deKey of deKeys) {
|
|
152
|
+
const { path: outPath, rowCount } = await exportDataExtensionToFile(tgtSdk, {
|
|
153
|
+
projectRoot,
|
|
154
|
+
credentialName: credential,
|
|
155
|
+
buName: bu,
|
|
156
|
+
deKey,
|
|
157
|
+
format,
|
|
158
|
+
useGit: false,
|
|
159
|
+
});
|
|
160
|
+
const rel = projectRelativePosix(projectRoot, outPath);
|
|
161
|
+
console.error(`Backup export: ${rel} (${rowCount} rows)`);
|
|
161
162
|
}
|
|
162
163
|
}
|
|
163
164
|
}
|
|
@@ -205,7 +206,7 @@ export async function crossBuImport(params) {
|
|
|
205
206
|
const dir = dataDirectoryForBu(projectRoot, credential, bu);
|
|
206
207
|
await fs.mkdir(dir, { recursive: true });
|
|
207
208
|
const ts = filesystemSafeTimestamp();
|
|
208
|
-
const basename = buildExportBasename(deKey, ts, format,
|
|
209
|
+
const basename = buildExportBasename(deKey, ts, format, false);
|
|
209
210
|
const snapshotPath = path.join(dir, basename);
|
|
210
211
|
await fs.writeFile(snapshotPath, serializeRows(rows, format, false), 'utf8');
|
|
211
212
|
const snapRel = projectRelativePosix(projectRoot, snapshotPath);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sfmc-dataloader",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.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 .",
|