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 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
@@ -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,
@@ -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] - stable snapshot basename (no timestamp)
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
- 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
- }
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, useGit);
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.1.0",
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/**/*.test.js",
47
+ "test": "node --test test/*.test.js",
48
48
  "lint": "eslint .",
49
49
  "lint:fix": "eslint --fix .",
50
50
  "format": "prettier --write .",