sfmc-dataloader 1.1.0 → 1.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
@@ -57,9 +57,9 @@ Import from explicit paths (DE key is recovered from the `+MCDATA+` filename):
57
57
  mcdata import MyCred/MyBU --file ./data/MyCred/MyBU/encoded%2Bkey+MCDATA+2026-04-06T12-00-00.000Z.csv
58
58
  ```
59
59
 
60
- ### Import — one source BU into multiple target BUs
60
+ ### Import — one source BU into multiple target BUs (API mode)
61
61
 
62
- Use `--from` (one source) and `--to` (repeatable targets) for a cross-BU import:
62
+ Use `--from` (one source) and `--to` (repeatable targets) for a cross-BU import where rows are fetched live from the source BU:
63
63
 
64
64
  ```bash
65
65
  mcdata import --from MyCred/Dev --to MyCred/QA --to MyCred/Prod --de Contact_DE
@@ -67,6 +67,25 @@ mcdata import --from MyCred/Dev --to MyCred/QA --to MyCred/Prod --de Contact_DE
67
67
 
68
68
  Before the import starts you will be offered the option to export the current data from each target BU as a timestamped backup. A timestamped download file is also written to each target BU's data directory so there is a traceable record of exactly what was imported.
69
69
 
70
+ ### Import — local export files into multiple target BUs (file mode)
71
+
72
+ Use `--to` (repeatable targets) and `--file` (repeatable file paths) to push previously exported data files to multiple BUs without connecting to a source BU. The DE customer key is derived from each filename automatically:
73
+
74
+ ```bash
75
+ mcdata import --to MyCred/QA --to MyCred/Prod \
76
+ --file ./data/MyCred/Dev/Contact_DE+MCDATA+2026-04-08T10-00-00.000Z.csv
77
+ ```
78
+
79
+ Multiple files can be supplied to push several DEs in one command:
80
+
81
+ ```bash
82
+ mcdata import --to MyCred/QA --to MyCred/Prod \
83
+ --file ./data/MyCred/Dev/Contact_DE+MCDATA+2026-04-08T10-00-00.000Z.csv \
84
+ --file ./data/MyCred/Dev/Order_DE+MCDATA+2026-04-08T10-00-00.000Z.csv
85
+ ```
86
+
87
+ A timestamped download file is written to each target BU's data directory, giving a traceable snapshot of exactly what was imported.
88
+
70
89
  ### Clear all rows before import
71
90
 
72
91
  **Dangerous:** removes every row in the target Data Extension(s) before uploading.
@@ -92,8 +111,8 @@ Interactive: type `YES` when prompted. In CI, add `--i-accept-clear-data-risk` a
92
111
  | `--format` | `csv` (default), `tsv`, or `json` |
93
112
  | `--api` | `async` (default) or `sync` |
94
113
  | `--mode` | `upsert` (default), `insert` and `update` require `--api sync` |
95
- | `--from <cred>/<bu>` | Export: source BU (repeatable). Import: single source BU (use with `--to`) |
96
- | `--to <cred>/<bu>` | Import: target BU (repeatable for multiple targets) |
114
+ | `--from <cred>/<bu>` | Export: source BU (repeatable). Import API mode: single source BU (use with `--to` and `--de`) |
115
+ | `--to <cred>/<bu>` | Import: target BU (repeatable). API mode: use with `--from`/`--de`. File mode: use with `--file` (no `--from` needed) |
97
116
  | `--clear-before-import` | SOAP `ClearData` before REST import |
98
117
  | `--i-accept-clear-data-risk` | Non-interactive consent for clear |
99
118
 
package/lib/cli.mjs CHANGED
@@ -26,6 +26,7 @@ Usage:
26
26
  mcdata export --from <cred>/<bu> [--from <cred>/<bu> ...] --de <key> [--de <key> ...] [options]
27
27
  mcdata import <credential>/<bu> (--de <key> ... | --file <path> ...) [options]
28
28
  mcdata import --from <cred>/<bu> --to <cred>/<bu> [--to <cred>/<bu> ...] --de <key> ... [options]
29
+ mcdata import --to <cred>/<bu> [--to <cred>/<bu> ...] --file <path> [--file <path> ...] [options]
29
30
 
30
31
  Options:
31
32
  -p, --project <dir> mcdev project root (default: cwd)
@@ -40,8 +41,9 @@ Import options:
40
41
 
41
42
  Multi-BU options:
42
43
  --from <cred>/<bu> Export: source BU (repeatable for multiple sources)
43
- Import: single source BU (use with --to)
44
+ Import (API mode): single source BU (use with --to and --de)
44
45
  --to <cred>/<bu> Import: target BU (repeatable for multiple targets)
46
+ Import (file mode): use with --file only (no --from needed)
45
47
 
46
48
  Notes:
47
49
  Exports are written under ./data/<credential>/<bu>/ with "+MCDATA+" in the filename.
@@ -196,7 +198,43 @@ export async function main(argv) {
196
198
  const clear = values['clear-before-import'];
197
199
  const acceptRisk = values['i-accept-clear-data-risk'];
198
200
 
199
- // ── Cross-BU import: --from + --to ──────────────────────────────────
201
+ // ── File-to-multi-BU import: --to + --file (no --from) ─────────────
202
+ // Rows are read from local export files; no source BU auth needed.
203
+ if (hasTo && !hasFrom && values.file?.length > 0) {
204
+ if (hasPositional) {
205
+ console.error('Cannot mix a positional <credential>/<bu> with --to/--file. Use one or the other.');
206
+ return 1;
207
+ }
208
+ if (values.de?.length > 0) {
209
+ console.error('Cannot mix --de with --file in multi-target import. Use --file only.');
210
+ return 1;
211
+ }
212
+ const filePaths = values.file;
213
+ let targets;
214
+ try {
215
+ targets = toFlags.map(parseCredBu);
216
+ } catch (e) {
217
+ console.error(e.message);
218
+ return 1;
219
+ }
220
+ const { mcdevrc, mcdevAuth } = loadMcdevProject(projectRoot);
221
+ await crossBuImport({
222
+ projectRoot,
223
+ mcdevrc,
224
+ mcdevAuth,
225
+ filePaths,
226
+ targets,
227
+ format: /** @type {'csv'|'tsv'|'json'} */ (fmt),
228
+ api: /** @type {'async'|'sync'} */ (api),
229
+ mode: /** @type {'upsert'|'insert'|'update'} */ (mode),
230
+ clearBeforeImport: clear,
231
+ acceptRiskFlag: acceptRisk,
232
+ isTTY: process.stdin.isTTY === true,
233
+ });
234
+ return 0;
235
+ }
236
+
237
+ // ── Cross-BU import (API mode): --from + --to + --de ────────────────
200
238
  if (hasFrom || hasTo) {
201
239
  if (hasPositional) {
202
240
  console.error('Cannot mix a positional <credential>/<bu> with --from/--to. Use one or the other.');
@@ -215,7 +253,7 @@ export async function main(argv) {
215
253
  return 1;
216
254
  }
217
255
  if (values.file?.length > 0) {
218
- console.error('--file cannot be combined with --from/--to. Use --de instead.');
256
+ console.error('--file cannot be combined with --from/--to/--de. For file-based multi-target import use --to + --file (without --from).');
219
257
  return 1;
220
258
  }
221
259
  const deKeys = [].concat(values.de ?? []);
@@ -7,10 +7,11 @@ import { resolveCredentialAndMid, buildSdkAuthObject } from './config.mjs';
7
7
  import { fetchAllRowObjects, serializeRows } from './export-de.mjs';
8
8
  import { exportDataExtensionToFile } from './export-de.mjs';
9
9
  import { importRowsForDe } from './import-de.mjs';
10
+ import { readRowsFromFile } from './read-rows.mjs';
10
11
  import { clearDataExtensionRows } from './clear-de.mjs';
11
12
  import { confirmClearBeforeImport } from './confirm-clear.mjs';
12
13
  import { dataDirectoryForBu } from './paths.mjs';
13
- import { buildExportBasename, filesystemSafeTimestamp } from './filename.mjs';
14
+ import { buildExportBasename, filesystemSafeTimestamp, parseExportBasename } from './filename.mjs';
14
15
 
15
16
  /**
16
17
  * @typedef {{ credential: string, bu: string }} CredBuTarget
@@ -51,6 +52,16 @@ async function offerPreExportBackup({ targets, deKeys, stdin: stdinStream, stdou
51
52
  * Imports Data Extension rows from a single source BU into one or more target
52
53
  * BUs.
53
54
  *
55
+ * Two source modes are supported:
56
+ *
57
+ * **API mode** (default): rows are fetched live from the source BU via the
58
+ * SFMC REST API. Requires `sourceCred`, `sourceBu`, and `deKeys`.
59
+ *
60
+ * **File mode**: rows are read from local export files (e.g. previously
61
+ * created by `mcdata export`). Requires `filePaths`; `deKeys` and
62
+ * `sourceCred`/`sourceBu` must be omitted. The DE customer key is derived
63
+ * from each filename via `parseExportBasename`.
64
+ *
54
65
  * Before the import each target BU:
55
66
  * 1. Optionally exports its current DE data as a timestamped backup (TTY only).
56
67
  * 2. Optionally clears all existing rows (with danger warning covering every target).
@@ -62,10 +73,11 @@ async function offerPreExportBackup({ targets, deKeys, stdin: stdinStream, stdou
62
73
  * @param {string} params.projectRoot
63
74
  * @param {import('./config.mjs').Mcdevrc} params.mcdevrc
64
75
  * @param {Record<string, import('./config.mjs').AuthCredential>} params.mcdevAuth
65
- * @param {string} params.sourceCred
66
- * @param {string} params.sourceBu
76
+ * @param {string} [params.sourceCred] - API mode only
77
+ * @param {string} [params.sourceBu] - API mode only
78
+ * @param {string[]} [params.deKeys] - API mode only
79
+ * @param {string[]} [params.filePaths] - File mode only; mutually exclusive with sourceCred/sourceBu/deKeys
67
80
  * @param {CredBuTarget[]} params.targets
68
- * @param {string[]} params.deKeys
69
81
  * @param {'csv'|'tsv'|'json'} params.format
70
82
  * @param {'async'|'sync'} params.api
71
83
  * @param {'upsert'|'insert'|'update'} params.mode
@@ -81,10 +93,7 @@ export async function crossBuImport(params) {
81
93
  projectRoot,
82
94
  mcdevrc,
83
95
  mcdevAuth,
84
- sourceCred,
85
- sourceBu,
86
96
  targets,
87
- deKeys,
88
97
  format,
89
98
  api,
90
99
  mode,
@@ -95,14 +104,37 @@ export async function crossBuImport(params) {
95
104
  const stdin = params.stdin;
96
105
  const stdout = params.stdout;
97
106
 
98
- // Validate all BU configurations upfront before making any API calls
99
- const { mid: srcMid, authCred: srcAuth } = resolveCredentialAndMid(mcdevrc, mcdevAuth, sourceCred, sourceBu);
107
+ // Determine source mode
108
+ const filePaths = params.filePaths ?? null;
109
+ const isFileBased = filePaths !== null && filePaths.length > 0;
110
+
111
+ // Derive DE keys: from explicit list (API mode) or from filenames (file mode)
112
+ const deKeys = isFileBased
113
+ ? filePaths.map((fp) => parseExportBasename(path.basename(fp)).customerKey)
114
+ : (params.deKeys ?? []);
115
+
116
+ // Build a lookup map from deKey → filePath for file mode
117
+ /** @type {Map<string, string>} */
118
+ const fileByDeKey = new Map();
119
+ if (isFileBased) {
120
+ for (const fp of filePaths) {
121
+ fileByDeKey.set(parseExportBasename(path.basename(fp)).customerKey, fp);
122
+ }
123
+ }
124
+
125
+ // Validate all target BU configurations upfront
100
126
  for (const { credential, bu } of targets) {
101
127
  resolveCredentialAndMid(mcdevrc, mcdevAuth, credential, bu);
102
128
  }
103
129
 
104
- // Connect to source BU
105
- const srcSdk = new SDK(buildSdkAuthObject(srcAuth, srcMid), { requestAttempts: 3 });
130
+ // Connect to source BU (API mode only)
131
+ let srcSdk = null;
132
+ if (!isFileBased) {
133
+ const { mid: srcMid, authCred: srcAuth } = resolveCredentialAndMid(
134
+ mcdevrc, mcdevAuth, params.sourceCred, params.sourceBu
135
+ );
136
+ srcSdk = new SDK(buildSdkAuthObject(srcAuth, srcMid), { requestAttempts: 3 });
137
+ }
106
138
 
107
139
  // Optional pre-import backup of target BU data
108
140
  if (isTTY) {
@@ -130,9 +162,11 @@ export async function crossBuImport(params) {
130
162
  await confirmClearBeforeImport({ deKeys, targets, acceptRiskFlag, isTTY, stdin, stdout });
131
163
  }
132
164
 
133
- // Fetch rows once per DE from the source, then fan out to every target
165
+ // Load rows once per DE then fan out to every target
134
166
  for (const deKey of deKeys) {
135
- const rows = await fetchAllRowObjects(srcSdk, deKey);
167
+ const rows = isFileBased
168
+ ? await readRowsFromFile(fileByDeKey.get(deKey), format)
169
+ : await fetchAllRowObjects(srcSdk, deKey);
136
170
 
137
171
  // Clear targets before import (rows already confirmed above)
138
172
  if (clearBeforeImport) {
@@ -154,9 +188,9 @@ export async function crossBuImport(params) {
154
188
  await fs.mkdir(dir, { recursive: true });
155
189
  const ts = filesystemSafeTimestamp();
156
190
  const basename = buildExportBasename(deKey, ts, format);
157
- const filePath = path.join(dir, basename);
158
- await fs.writeFile(filePath, serializeRows(rows, format, false), 'utf8');
159
- console.error(`Download stored: ${filePath}`);
191
+ const snapshotPath = path.join(dir, basename);
192
+ await fs.writeFile(snapshotPath, serializeRows(rows, format, false), 'utf8');
193
+ console.error(`Download stored: ${snapshotPath}`);
160
194
 
161
195
  await importRowsForDe(tgtSdk, { deKey, rows, api, mode });
162
196
  console.error(`Imported -> ${credential}/${bu} DE ${deKey}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sfmc-dataloader",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "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",