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 +23 -4
- package/lib/cli.mjs +41 -3
- package/lib/cross-bu-import.mjs +50 -16
- package/package.json +1 -1
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
|
|
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
|
-
// ──
|
|
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.
|
|
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 ?? []);
|
package/lib/cross-bu-import.mjs
CHANGED
|
@@ -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
|
-
//
|
|
99
|
-
const
|
|
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
|
-
|
|
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
|
-
//
|
|
165
|
+
// Load rows once per DE then fan out to every target
|
|
134
166
|
for (const deKey of deKeys) {
|
|
135
|
-
const rows =
|
|
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
|
|
158
|
-
await fs.writeFile(
|
|
159
|
-
console.error(`Download stored: ${
|
|
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.
|
|
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",
|