sfmc-dataloader 1.1.0 → 2.0.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 +36 -19
- package/lib/cli.mjs +67 -33
- package/lib/cross-bu-import.mjs +63 -36
- package/lib/export-de.mjs +12 -31
- package/lib/file-resolve.mjs +9 -5
- package/lib/filename.mjs +34 -20
- package/lib/import-de.mjs +11 -13
- package/lib/import-routes.mjs +10 -42
- package/lib/index.mjs +2 -1
- package/lib/multi-bu-export.mjs +7 -3
- package/lib/paths.mjs +16 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -24,7 +24,9 @@ Run from your mcdev project root (where `.mcdevrc.json` lives).
|
|
|
24
24
|
mcdata export MyCred/MyBU --de MyDE_CustomerKey --format csv
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
Writes to `./data/MyCred/MyBU/<encodedKey
|
|
27
|
+
Writes to `./data/MyCred/MyBU/<encodedKey>.mcdata.<timestamp>.csv` (TSV/JSON with `--format`). Timestamps use the same filesystem-safe ISO format as before.
|
|
28
|
+
|
|
29
|
+
Use **`--git`** for a stable name without a timestamp: `<encodedKey>.mcdata.csv` (useful for version control).
|
|
28
30
|
|
|
29
31
|
### Export — multiple BUs at once
|
|
30
32
|
|
|
@@ -34,49 +36,62 @@ Use `--from` (repeatable) instead of the positional argument to export the same
|
|
|
34
36
|
mcdata export --from MyCred/Dev --from MyCred/QA --de Contact_DE --de Order_DE
|
|
35
37
|
```
|
|
36
38
|
|
|
37
|
-
Creates one
|
|
38
|
-
|
|
39
|
-
```
|
|
40
|
-
./data/MyCred/Dev/Contact_DE+MCDATA+<timestamp>.csv
|
|
41
|
-
./data/MyCred/Dev/Order_DE+MCDATA+<timestamp>.csv
|
|
42
|
-
./data/MyCred/QA/Contact_DE+MCDATA+<timestamp>.csv
|
|
43
|
-
./data/MyCred/QA/Order_DE+MCDATA+<timestamp>.csv
|
|
44
|
-
```
|
|
39
|
+
Creates one file per BU/DE combination using the same `.mcdata.` naming rules.
|
|
45
40
|
|
|
46
41
|
### Import — single BU
|
|
47
42
|
|
|
48
43
|
```bash
|
|
49
|
-
mcdata import MyCred/MyBU --de MyDE_CustomerKey --format csv --
|
|
44
|
+
mcdata import MyCred/MyBU --de MyDE_CustomerKey --format csv --mode upsert
|
|
50
45
|
```
|
|
51
46
|
|
|
47
|
+
Imports use the **asynchronous** bulk row API only: `POST` for `--mode insert`, `PUT` for `--mode upsert` (same endpoint path).
|
|
48
|
+
|
|
52
49
|
Resolves the latest matching export file under `./data/MyCred/MyBU/` for that DE key.
|
|
53
50
|
|
|
54
|
-
Import from explicit paths (DE key is
|
|
51
|
+
Import from explicit paths (the DE key is taken from the `.mcdata.` basename):
|
|
55
52
|
|
|
56
53
|
```bash
|
|
57
|
-
mcdata import MyCred/MyBU --file ./data/MyCred/MyBU/encoded%2Bkey
|
|
54
|
+
mcdata import MyCred/MyBU --file ./data/MyCred/MyBU/encoded%2Bkey.mcdata.2026-04-06T12-00-00.000Z.csv
|
|
58
55
|
```
|
|
59
56
|
|
|
60
|
-
|
|
57
|
+
#### Upsert vs insert
|
|
58
|
+
|
|
59
|
+
- **Upsert** follows the platform’s 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.
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
### Import — one source BU into multiple target BUs (API mode)
|
|
63
|
+
|
|
64
|
+
Use `--from` (one source) and `--to` (repeatable targets) for a cross-BU import where rows are fetched live from the source BU:
|
|
63
65
|
|
|
64
66
|
```bash
|
|
65
67
|
mcdata import --from MyCred/Dev --to MyCred/QA --to MyCred/Prod --de Contact_DE
|
|
66
68
|
```
|
|
67
69
|
|
|
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
|
|
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.
|
|
71
|
+
|
|
72
|
+
### Import — local export files into multiple target BUs (file mode)
|
|
73
|
+
|
|
74
|
+
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:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
mcdata import --to MyCred/QA --to MyCred/Prod \
|
|
78
|
+
--file ./data/MyCred/Dev/Contact_DE.mcdata.2026-04-08T10-00-00.000Z.csv
|
|
79
|
+
```
|
|
80
|
+
|
|
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.
|
|
69
82
|
|
|
70
83
|
### Clear all rows before import
|
|
71
84
|
|
|
72
85
|
**Dangerous:** removes every row in the target Data Extension(s) before uploading.
|
|
73
86
|
|
|
74
87
|
Single-BU:
|
|
88
|
+
|
|
75
89
|
```bash
|
|
76
90
|
mcdata import MyCred/MyBU --de MyKey --clear-before-import
|
|
77
91
|
```
|
|
78
92
|
|
|
79
93
|
Cross-BU (warning lists every affected BU):
|
|
94
|
+
|
|
80
95
|
```bash
|
|
81
96
|
mcdata import --from MyCred/Dev --to MyCred/QA --to MyCred/Prod \
|
|
82
97
|
--de Contact_DE --clear-before-import
|
|
@@ -90,13 +105,15 @@ Interactive: type `YES` when prompted. In CI, add `--i-accept-clear-data-risk` a
|
|
|
90
105
|
|--------|-------------|
|
|
91
106
|
| `-p, --project` | Project root (default: cwd) |
|
|
92
107
|
| `--format` | `csv` (default), `tsv`, or `json` |
|
|
93
|
-
| `--
|
|
94
|
-
| `--mode` | `upsert` (default)
|
|
95
|
-
| `--from <cred>/<bu>` | Export: source BU (repeatable). Import: single source BU (use with `--to`) |
|
|
96
|
-
| `--to <cred>/<bu>` | Import: target BU (repeatable
|
|
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) |
|
|
97
112
|
| `--clear-before-import` | SOAP `ClearData` before REST import |
|
|
98
113
|
| `--i-accept-clear-data-risk` | Non-interactive consent for clear |
|
|
99
114
|
|
|
115
|
+
Log lines use paths **relative** to the project root (POSIX-style, `./…`) and include **row counts** where applicable.
|
|
116
|
+
|
|
100
117
|
## License
|
|
101
118
|
|
|
102
119
|
MIT — Author: Jörn Berkefeld
|
package/lib/cli.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
resolveCredentialAndMid,
|
|
9
9
|
buildSdkAuthObject,
|
|
10
10
|
} from './config.mjs';
|
|
11
|
-
import { dataDirectoryForBu } from './paths.mjs';
|
|
11
|
+
import { dataDirectoryForBu, projectRelativePosix } from './paths.mjs';
|
|
12
12
|
import { exportDataExtensionToFile } from './export-de.mjs';
|
|
13
13
|
import { findImportCandidates, pickLatestByMtime } from './file-resolve.mjs';
|
|
14
14
|
import { parseExportBasename } from './filename.mjs';
|
|
@@ -26,28 +26,30 @@ 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)
|
|
32
33
|
--format <csv|tsv|json> File format (default: csv)
|
|
33
34
|
--json-pretty Pretty-print JSON on export
|
|
35
|
+
--git Stable filenames: <key>.mcdata.<ext> (no timestamp)
|
|
34
36
|
|
|
35
37
|
Import options:
|
|
36
|
-
--
|
|
37
|
-
--mode <upsert|insert|update> (default: upsert; insert/update require --api sync)
|
|
38
|
+
--mode <upsert|insert> Row write mode (default: upsert; async REST bulk API)
|
|
38
39
|
--clear-before-import SOAP ClearData before import (destructive; see below)
|
|
39
40
|
--i-accept-clear-data-risk Non-interactive acknowledgement for --clear-before-import
|
|
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
|
-
Exports are written under ./data/<credential>/<bu>/
|
|
49
|
+
Exports are written under ./data/<credential>/<bu>/ using ".mcdata." in the filename.
|
|
48
50
|
Import with --de resolves the latest matching file in that folder (by mtime).
|
|
49
|
-
Import with --file parses the DE key from the basename (
|
|
50
|
-
Cross-BU import stores a
|
|
51
|
+
Import with --file parses the DE key from the basename (.mcdata. format).
|
|
52
|
+
Cross-BU import stores a download file in each target BU's data directory.
|
|
51
53
|
|
|
52
54
|
Clear data warning:
|
|
53
55
|
--clear-before-import deletes ALL existing rows in the target DE(s) before upload.
|
|
@@ -72,10 +74,10 @@ export async function main(argv) {
|
|
|
72
74
|
format: { type: 'string' },
|
|
73
75
|
de: { type: 'string', multiple: true },
|
|
74
76
|
file: { type: 'string', multiple: true },
|
|
75
|
-
api: { type: 'string' },
|
|
76
|
-
mode: { type: 'string' },
|
|
77
77
|
from: { type: 'string', multiple: true },
|
|
78
78
|
to: { type: 'string', multiple: true },
|
|
79
|
+
git: { type: 'boolean', default: false },
|
|
80
|
+
mode: { type: 'string' },
|
|
79
81
|
'clear-before-import': { type: 'boolean', default: false },
|
|
80
82
|
'i-accept-clear-data-risk': { type: 'boolean', default: false },
|
|
81
83
|
'json-pretty': { type: 'boolean', default: false },
|
|
@@ -109,6 +111,7 @@ export async function main(argv) {
|
|
|
109
111
|
return 1;
|
|
110
112
|
}
|
|
111
113
|
|
|
114
|
+
const useGit = values.git === true;
|
|
112
115
|
const fromFlags = values.from ?? [];
|
|
113
116
|
const toFlags = values.to ?? [];
|
|
114
117
|
const hasFrom = fromFlags.length > 0;
|
|
@@ -140,7 +143,6 @@ export async function main(argv) {
|
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
if (hasFrom) {
|
|
143
|
-
// Multi-BU export
|
|
144
146
|
let sources;
|
|
145
147
|
try {
|
|
146
148
|
sources = fromFlags.map(parseCredBu);
|
|
@@ -157,46 +159,78 @@ export async function main(argv) {
|
|
|
157
159
|
deKeys: des,
|
|
158
160
|
format: /** @type {'csv'|'tsv'|'json'} */ (fmt),
|
|
159
161
|
jsonPretty: values['json-pretty'],
|
|
162
|
+
useGit,
|
|
160
163
|
});
|
|
161
164
|
return 0;
|
|
162
165
|
}
|
|
163
166
|
|
|
164
|
-
// Single-BU export (original behavior)
|
|
165
167
|
const { credential, bu } = parseCredBu(credBuRaw);
|
|
166
168
|
const { mcdevrc, mcdevAuth } = loadMcdevProject(projectRoot);
|
|
167
169
|
const { mid, authCred } = resolveCredentialAndMid(mcdevrc, mcdevAuth, credential, bu);
|
|
168
170
|
const sdk = new SDK(buildSdkAuthObject(authCred, mid), { requestAttempts: 3 });
|
|
169
171
|
for (const deKey of des) {
|
|
170
|
-
const out = await exportDataExtensionToFile(sdk, {
|
|
172
|
+
const { path: out, rowCount } = await exportDataExtensionToFile(sdk, {
|
|
171
173
|
projectRoot,
|
|
172
174
|
credentialName: credential,
|
|
173
175
|
buName: bu,
|
|
174
176
|
deKey,
|
|
175
177
|
format: /** @type {'csv'|'tsv'|'json'} */ (fmt),
|
|
176
178
|
jsonPretty: values['json-pretty'],
|
|
179
|
+
useGit,
|
|
177
180
|
});
|
|
178
|
-
|
|
181
|
+
const rel = projectRelativePosix(projectRoot, out);
|
|
182
|
+
console.error(`Exported: ${rel} (${rowCount} rows)`);
|
|
179
183
|
}
|
|
180
184
|
return 0;
|
|
181
185
|
}
|
|
182
186
|
|
|
183
187
|
// ── import ──────────────────────────────────────────────────────────────
|
|
184
188
|
if (sub === 'import') {
|
|
185
|
-
const api = values.api ?? 'async';
|
|
186
189
|
const mode = values.mode ?? 'upsert';
|
|
187
|
-
if (!['
|
|
188
|
-
console.error(`Invalid --
|
|
189
|
-
return 1;
|
|
190
|
-
}
|
|
191
|
-
if (!['upsert', 'insert', 'update'].includes(mode)) {
|
|
192
|
-
console.error(`Invalid --mode: ${mode}`);
|
|
190
|
+
if (!['upsert', 'insert'].includes(mode)) {
|
|
191
|
+
console.error(`Invalid --mode: ${mode} (use upsert or insert)`);
|
|
193
192
|
return 1;
|
|
194
193
|
}
|
|
195
194
|
|
|
196
195
|
const clear = values['clear-before-import'];
|
|
197
196
|
const acceptRisk = values['i-accept-clear-data-risk'];
|
|
198
197
|
|
|
199
|
-
// ──
|
|
198
|
+
// ── File-to-multi-BU import: --to + --file (no --from) ─────────────
|
|
199
|
+
if (hasTo && !hasFrom && values.file?.length > 0) {
|
|
200
|
+
if (hasPositional) {
|
|
201
|
+
console.error('Cannot mix a positional <credential>/<bu> with --to/--file. Use one or the other.');
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
204
|
+
if (values.de?.length > 0) {
|
|
205
|
+
console.error('Cannot mix --de with --file in multi-target import. Use --file only.');
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
const filePaths = values.file;
|
|
209
|
+
let targets;
|
|
210
|
+
try {
|
|
211
|
+
targets = toFlags.map(parseCredBu);
|
|
212
|
+
} catch (e) {
|
|
213
|
+
console.error(e.message);
|
|
214
|
+
return 1;
|
|
215
|
+
}
|
|
216
|
+
const { mcdevrc, mcdevAuth } = loadMcdevProject(projectRoot);
|
|
217
|
+
await crossBuImport({
|
|
218
|
+
projectRoot,
|
|
219
|
+
mcdevrc,
|
|
220
|
+
mcdevAuth,
|
|
221
|
+
filePaths,
|
|
222
|
+
targets,
|
|
223
|
+
format: /** @type {'csv'|'tsv'|'json'} */ (fmt),
|
|
224
|
+
mode: /** @type {'upsert'|'insert'} */ (mode),
|
|
225
|
+
clearBeforeImport: clear,
|
|
226
|
+
acceptRiskFlag: acceptRisk,
|
|
227
|
+
isTTY: process.stdin.isTTY === true,
|
|
228
|
+
useGit,
|
|
229
|
+
});
|
|
230
|
+
return 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── Cross-BU import (API mode): --from + --to + --de ────────────────
|
|
200
234
|
if (hasFrom || hasTo) {
|
|
201
235
|
if (hasPositional) {
|
|
202
236
|
console.error('Cannot mix a positional <credential>/<bu> with --from/--to. Use one or the other.');
|
|
@@ -215,7 +249,7 @@ export async function main(argv) {
|
|
|
215
249
|
return 1;
|
|
216
250
|
}
|
|
217
251
|
if (values.file?.length > 0) {
|
|
218
|
-
console.error('--file cannot be combined with --from/--to.
|
|
252
|
+
console.error('--file cannot be combined with --from/--to/--de. For file-based multi-target import use --to + --file (without --from).');
|
|
219
253
|
return 1;
|
|
220
254
|
}
|
|
221
255
|
const deKeys = [].concat(values.de ?? []);
|
|
@@ -242,11 +276,11 @@ export async function main(argv) {
|
|
|
242
276
|
targets,
|
|
243
277
|
deKeys,
|
|
244
278
|
format: /** @type {'csv'|'tsv'|'json'} */ (fmt),
|
|
245
|
-
|
|
246
|
-
mode: /** @type {'upsert'|'insert'|'update'} */ (mode),
|
|
279
|
+
mode: /** @type {'upsert'|'insert'} */ (mode),
|
|
247
280
|
clearBeforeImport: clear,
|
|
248
281
|
acceptRiskFlag: acceptRisk,
|
|
249
282
|
isTTY: process.stdin.isTTY === true,
|
|
283
|
+
useGit,
|
|
250
284
|
});
|
|
251
285
|
return 0;
|
|
252
286
|
}
|
|
@@ -286,19 +320,19 @@ export async function main(argv) {
|
|
|
286
320
|
for (const deKey of deKeys) {
|
|
287
321
|
const candidates = await findImportCandidates(dataDir, deKey, fmt);
|
|
288
322
|
if (candidates.length === 0) {
|
|
289
|
-
console.error(`No ${fmt} file found for DE "${deKey}" under ${dataDir}`);
|
|
323
|
+
console.error(`No ${fmt} file found for DE "${deKey}" under ${projectRelativePosix(projectRoot, dataDir)}`);
|
|
290
324
|
return 1;
|
|
291
325
|
}
|
|
292
326
|
const filePath =
|
|
293
327
|
candidates.length === 1 ? candidates[0] : await pickLatestByMtime(candidates);
|
|
294
|
-
await importFromFile(sdk, {
|
|
328
|
+
const n = await importFromFile(sdk, {
|
|
295
329
|
filePath,
|
|
296
330
|
deKey,
|
|
297
331
|
format: /** @type {'csv'|'tsv'|'json'} */ (fmt),
|
|
298
|
-
|
|
299
|
-
mode: /** @type {'upsert'|'insert'|'update'} */ (mode),
|
|
332
|
+
mode: /** @type {'upsert'|'insert'} */ (mode),
|
|
300
333
|
});
|
|
301
|
-
|
|
334
|
+
const rel = projectRelativePosix(projectRoot, filePath);
|
|
335
|
+
console.error(`Imported: ${rel} (${n} rows) -> DE ${deKey}`);
|
|
302
336
|
}
|
|
303
337
|
return 0;
|
|
304
338
|
}
|
|
@@ -318,14 +352,14 @@ export async function main(argv) {
|
|
|
318
352
|
for (const filePath of fileList) {
|
|
319
353
|
const base = path.basename(filePath);
|
|
320
354
|
const { customerKey } = parseExportBasename(base);
|
|
321
|
-
await importFromFile(sdk, {
|
|
355
|
+
const n = await importFromFile(sdk, {
|
|
322
356
|
filePath,
|
|
323
357
|
deKey: customerKey,
|
|
324
358
|
format: /** @type {'csv'|'tsv'|'json'} */ (fmt),
|
|
325
|
-
|
|
326
|
-
mode: /** @type {'upsert'|'insert'|'update'} */ (mode),
|
|
359
|
+
mode: /** @type {'upsert'|'insert'} */ (mode),
|
|
327
360
|
});
|
|
328
|
-
|
|
361
|
+
const rel = projectRelativePosix(projectRoot, filePath);
|
|
362
|
+
console.error(`Imported: ${rel} (${n} rows)`);
|
|
329
363
|
}
|
|
330
364
|
return 0;
|
|
331
365
|
}
|
package/lib/cross-bu-import.mjs
CHANGED
|
@@ -4,13 +4,13 @@ import readline from 'node:readline/promises';
|
|
|
4
4
|
import { stdin as input, stdout as output } from 'node:process';
|
|
5
5
|
import SDK from 'sfmc-sdk';
|
|
6
6
|
import { resolveCredentialAndMid, buildSdkAuthObject } from './config.mjs';
|
|
7
|
-
import { fetchAllRowObjects, serializeRows } from './export-de.mjs';
|
|
8
|
-
import { exportDataExtensionToFile } from './export-de.mjs';
|
|
7
|
+
import { fetchAllRowObjects, serializeRows, exportDataExtensionToFile } from './export-de.mjs';
|
|
9
8
|
import { importRowsForDe } from './import-de.mjs';
|
|
9
|
+
import { readRowsFromFile } from './read-rows.mjs';
|
|
10
10
|
import { clearDataExtensionRows } from './clear-de.mjs';
|
|
11
11
|
import { confirmClearBeforeImport } from './confirm-clear.mjs';
|
|
12
|
-
import { dataDirectoryForBu } from './paths.mjs';
|
|
13
|
-
import { buildExportBasename, filesystemSafeTimestamp } from './filename.mjs';
|
|
12
|
+
import { dataDirectoryForBu, projectRelativePosix } from './paths.mjs';
|
|
13
|
+
import { buildExportBasename, filesystemSafeTimestamp, parseExportBasename } from './filename.mjs';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* @typedef {{ credential: string, bu: string }} CredBuTarget
|
|
@@ -51,27 +51,31 @@ async function offerPreExportBackup({ targets, deKeys, stdin: stdinStream, stdou
|
|
|
51
51
|
* Imports Data Extension rows from a single source BU into one or more target
|
|
52
52
|
* BUs.
|
|
53
53
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
54
|
+
* Two source modes are supported:
|
|
55
|
+
*
|
|
56
|
+
* **API mode** (default): rows are fetched live from the source BU via the
|
|
57
|
+
* SFMC REST API. Requires `sourceCred`, `sourceBu`, and `deKeys`.
|
|
58
|
+
*
|
|
59
|
+
* **File mode**: rows are read from local export files (e.g. previously
|
|
60
|
+
* created by `mcdata export`). Requires `filePaths`; `deKeys` and
|
|
61
|
+
* `sourceCred`/`sourceBu` must be omitted. The DE customer key is derived
|
|
62
|
+
* from each filename via `parseExportBasename`.
|
|
60
63
|
*
|
|
61
64
|
* @param {object} params
|
|
62
65
|
* @param {string} params.projectRoot
|
|
63
66
|
* @param {import('./config.mjs').Mcdevrc} params.mcdevrc
|
|
64
67
|
* @param {Record<string, import('./config.mjs').AuthCredential>} params.mcdevAuth
|
|
65
|
-
* @param {string} params.sourceCred
|
|
66
|
-
* @param {string} params.sourceBu
|
|
68
|
+
* @param {string} [params.sourceCred] - API mode only
|
|
69
|
+
* @param {string} [params.sourceBu] - API mode only
|
|
70
|
+
* @param {string[]} [params.deKeys] - API mode only
|
|
71
|
+
* @param {string[]} [params.filePaths] - File mode only; mutually exclusive with sourceCred/sourceBu/deKeys
|
|
67
72
|
* @param {CredBuTarget[]} params.targets
|
|
68
|
-
* @param {string[]} params.deKeys
|
|
69
73
|
* @param {'csv'|'tsv'|'json'} params.format
|
|
70
|
-
* @param {'
|
|
71
|
-
* @param {'upsert'|'insert'|'update'} params.mode
|
|
74
|
+
* @param {'upsert'|'insert'} params.mode
|
|
72
75
|
* @param {boolean} params.clearBeforeImport
|
|
73
76
|
* @param {boolean} params.acceptRiskFlag
|
|
74
77
|
* @param {boolean} params.isTTY
|
|
78
|
+
* @param {boolean} [params.useGit] - stable snapshot basename (no timestamp)
|
|
75
79
|
* @param {NodeJS.ReadableStream} [params.stdin] Override for testing
|
|
76
80
|
* @param {NodeJS.WritableStream} [params.stdout] Override for testing
|
|
77
81
|
* @returns {Promise<void>}
|
|
@@ -81,28 +85,48 @@ export async function crossBuImport(params) {
|
|
|
81
85
|
projectRoot,
|
|
82
86
|
mcdevrc,
|
|
83
87
|
mcdevAuth,
|
|
84
|
-
sourceCred,
|
|
85
|
-
sourceBu,
|
|
86
88
|
targets,
|
|
87
|
-
deKeys,
|
|
88
89
|
format,
|
|
89
|
-
api,
|
|
90
90
|
mode,
|
|
91
91
|
clearBeforeImport,
|
|
92
92
|
acceptRiskFlag,
|
|
93
93
|
isTTY,
|
|
94
|
+
useGit = false,
|
|
94
95
|
} = params;
|
|
95
96
|
const stdin = params.stdin;
|
|
96
97
|
const stdout = params.stdout;
|
|
97
98
|
|
|
98
|
-
//
|
|
99
|
-
const
|
|
99
|
+
// Determine source mode
|
|
100
|
+
const filePaths = params.filePaths ?? null;
|
|
101
|
+
const isFileBased = filePaths !== null && filePaths.length > 0;
|
|
102
|
+
|
|
103
|
+
// Derive DE keys: from explicit list (API mode) or from filenames (file mode)
|
|
104
|
+
const deKeys = isFileBased
|
|
105
|
+
? filePaths.map((fp) => parseExportBasename(path.basename(fp)).customerKey)
|
|
106
|
+
: (params.deKeys ?? []);
|
|
107
|
+
|
|
108
|
+
// Build a lookup map from deKey → filePath for file mode
|
|
109
|
+
/** @type {Map<string, string>} */
|
|
110
|
+
const fileByDeKey = new Map();
|
|
111
|
+
if (isFileBased) {
|
|
112
|
+
for (const fp of filePaths) {
|
|
113
|
+
fileByDeKey.set(parseExportBasename(path.basename(fp)).customerKey, fp);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Validate all target BU configurations upfront
|
|
100
118
|
for (const { credential, bu } of targets) {
|
|
101
119
|
resolveCredentialAndMid(mcdevrc, mcdevAuth, credential, bu);
|
|
102
120
|
}
|
|
103
121
|
|
|
104
|
-
// Connect to source BU
|
|
105
|
-
|
|
122
|
+
// Connect to source BU (API mode only)
|
|
123
|
+
let srcSdk = null;
|
|
124
|
+
if (!isFileBased) {
|
|
125
|
+
const { mid: srcMid, authCred: srcAuth } = resolveCredentialAndMid(
|
|
126
|
+
mcdevrc, mcdevAuth, params.sourceCred, params.sourceBu
|
|
127
|
+
);
|
|
128
|
+
srcSdk = new SDK(buildSdkAuthObject(srcAuth, srcMid), { requestAttempts: 3 });
|
|
129
|
+
}
|
|
106
130
|
|
|
107
131
|
// Optional pre-import backup of target BU data
|
|
108
132
|
if (isTTY) {
|
|
@@ -112,14 +136,16 @@ export async function crossBuImport(params) {
|
|
|
112
136
|
const { mid, authCred } = resolveCredentialAndMid(mcdevrc, mcdevAuth, credential, bu);
|
|
113
137
|
const tgtSdk = new SDK(buildSdkAuthObject(authCred, mid), { requestAttempts: 3 });
|
|
114
138
|
for (const deKey of deKeys) {
|
|
115
|
-
const outPath = await exportDataExtensionToFile(tgtSdk, {
|
|
139
|
+
const { path: outPath, rowCount } = await exportDataExtensionToFile(tgtSdk, {
|
|
116
140
|
projectRoot,
|
|
117
141
|
credentialName: credential,
|
|
118
142
|
buName: bu,
|
|
119
143
|
deKey,
|
|
120
144
|
format,
|
|
145
|
+
useGit: false,
|
|
121
146
|
});
|
|
122
|
-
|
|
147
|
+
const rel = projectRelativePosix(projectRoot, outPath);
|
|
148
|
+
console.error(`Backup export: ${rel} (${rowCount} rows)`);
|
|
123
149
|
}
|
|
124
150
|
}
|
|
125
151
|
}
|
|
@@ -130,9 +156,11 @@ export async function crossBuImport(params) {
|
|
|
130
156
|
await confirmClearBeforeImport({ deKeys, targets, acceptRiskFlag, isTTY, stdin, stdout });
|
|
131
157
|
}
|
|
132
158
|
|
|
133
|
-
//
|
|
159
|
+
// Load rows once per DE then fan out to every target
|
|
134
160
|
for (const deKey of deKeys) {
|
|
135
|
-
const rows =
|
|
161
|
+
const rows = isFileBased
|
|
162
|
+
? await readRowsFromFile(fileByDeKey.get(deKey), format)
|
|
163
|
+
: await fetchAllRowObjects(srcSdk, deKey);
|
|
136
164
|
|
|
137
165
|
// Clear targets before import (rows already confirmed above)
|
|
138
166
|
if (clearBeforeImport) {
|
|
@@ -147,19 +175,18 @@ export async function crossBuImport(params) {
|
|
|
147
175
|
const { mid, authCred } = resolveCredentialAndMid(mcdevrc, mcdevAuth, credential, bu);
|
|
148
176
|
const tgtSdk = new SDK(buildSdkAuthObject(authCred, mid), { requestAttempts: 3 });
|
|
149
177
|
|
|
150
|
-
// Write a
|
|
151
|
-
// This mirrors what mcdev retrieve / mcdata export produces, giving a
|
|
152
|
-
// traceable snapshot of exactly what was imported.
|
|
178
|
+
// Write a snapshot file in the target BU's data directory.
|
|
153
179
|
const dir = dataDirectoryForBu(projectRoot, credential, bu);
|
|
154
180
|
await fs.mkdir(dir, { recursive: true });
|
|
155
181
|
const ts = filesystemSafeTimestamp();
|
|
156
|
-
const basename = buildExportBasename(deKey, ts, format);
|
|
157
|
-
const
|
|
158
|
-
await fs.writeFile(
|
|
159
|
-
|
|
182
|
+
const basename = buildExportBasename(deKey, ts, format, useGit);
|
|
183
|
+
const snapshotPath = path.join(dir, basename);
|
|
184
|
+
await fs.writeFile(snapshotPath, serializeRows(rows, format, false), 'utf8');
|
|
185
|
+
const snapRel = projectRelativePosix(projectRoot, snapshotPath);
|
|
186
|
+
console.error(`Download stored: ${snapRel} (${rows.length} rows)`);
|
|
160
187
|
|
|
161
|
-
await importRowsForDe(tgtSdk, { deKey, rows,
|
|
162
|
-
console.error(`Imported
|
|
188
|
+
const imported = await importRowsForDe(tgtSdk, { deKey, rows, mode });
|
|
189
|
+
console.error(`Imported: ${credential}/${bu} DE ${deKey} (${imported} rows)`);
|
|
163
190
|
}
|
|
164
191
|
}
|
|
165
192
|
}
|
package/lib/export-de.mjs
CHANGED
|
@@ -6,37 +6,17 @@ import { buildExportBasename, filesystemSafeTimestamp } from './filename.mjs';
|
|
|
6
6
|
import { dataDirectoryForBu } from './paths.mjs';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* @param {{ rest: {
|
|
9
|
+
* @param {{ rest: { getBulk: (path: string, pageSize?: number) => Promise<any> } }} sdk
|
|
10
10
|
* @param {string} deKey
|
|
11
11
|
* @returns {Promise<object[]>}
|
|
12
12
|
*/
|
|
13
13
|
export async function fetchAllRowObjects(sdk, deKey) {
|
|
14
|
-
const
|
|
15
|
-
|
|
14
|
+
const basePath = rowsetGetPath(deKey);
|
|
15
|
+
const data = await sdk.rest.getBulk(basePath, 2500);
|
|
16
|
+
const items = data.items ?? [];
|
|
16
17
|
const rows = [];
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const qs = new URLSearchParams({
|
|
20
|
-
page: String(page),
|
|
21
|
-
pageSize: String(pageSize),
|
|
22
|
-
});
|
|
23
|
-
const urlPath = `${rowsetGetPath(deKey)}?${qs.toString()}`;
|
|
24
|
-
const data = await sdk.rest.get(urlPath);
|
|
25
|
-
const items = data.items ?? [];
|
|
26
|
-
for (const item of items) {
|
|
27
|
-
rows.push({ ...item.keys, ...item.values });
|
|
28
|
-
}
|
|
29
|
-
if (items.length === 0) {
|
|
30
|
-
hasMore = false;
|
|
31
|
-
} else if (data.hasMoreRows === false) {
|
|
32
|
-
hasMore = false;
|
|
33
|
-
} else if (data.hasMoreRows === true) {
|
|
34
|
-
hasMore = true;
|
|
35
|
-
page++;
|
|
36
|
-
} else {
|
|
37
|
-
hasMore = items.length === pageSize;
|
|
38
|
-
page++;
|
|
39
|
-
}
|
|
18
|
+
for (const item of items) {
|
|
19
|
+
rows.push({ ...item.keys, ...item.values });
|
|
40
20
|
}
|
|
41
21
|
return rows;
|
|
42
22
|
}
|
|
@@ -62,7 +42,7 @@ export function serializeRows(rows, format, jsonPretty) {
|
|
|
62
42
|
}
|
|
63
43
|
|
|
64
44
|
/**
|
|
65
|
-
* @param {{ rest: {
|
|
45
|
+
* @param {{ rest: { getBulk: (path: string, pageSize?: number) => Promise<any> } }} sdk
|
|
66
46
|
* @param {object} params
|
|
67
47
|
* @param {string} params.projectRoot
|
|
68
48
|
* @param {string} params.credentialName
|
|
@@ -70,17 +50,18 @@ export function serializeRows(rows, format, jsonPretty) {
|
|
|
70
50
|
* @param {string} params.deKey
|
|
71
51
|
* @param {'csv'|'tsv'|'json'} params.format
|
|
72
52
|
* @param {boolean} [params.jsonPretty]
|
|
73
|
-
* @
|
|
53
|
+
* @param {boolean} [params.useGit]
|
|
54
|
+
* @returns {Promise<{ path: string, rowCount: number }>}
|
|
74
55
|
*/
|
|
75
56
|
export async function exportDataExtensionToFile(sdk, params) {
|
|
76
|
-
const { projectRoot, credentialName, buName, deKey, format, jsonPretty = false } = params;
|
|
57
|
+
const { projectRoot, credentialName, buName, deKey, format, jsonPretty = false, useGit = false } = params;
|
|
77
58
|
const rows = await fetchAllRowObjects(sdk, deKey);
|
|
78
59
|
const dir = dataDirectoryForBu(projectRoot, credentialName, buName);
|
|
79
60
|
await fs.mkdir(dir, { recursive: true });
|
|
80
61
|
const ts = filesystemSafeTimestamp();
|
|
81
|
-
const basename = buildExportBasename(deKey, ts, format);
|
|
62
|
+
const basename = buildExportBasename(deKey, ts, format, useGit);
|
|
82
63
|
const outPath = path.join(dir, basename);
|
|
83
64
|
const body = serializeRows(rows, format, jsonPretty);
|
|
84
65
|
await fs.writeFile(outPath, body, 'utf8');
|
|
85
|
-
return outPath;
|
|
66
|
+
return { path: outPath, rowCount: rows.length };
|
|
86
67
|
}
|
package/lib/file-resolve.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { parseExportBasename } from './filename.mjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Find export files under data dir matching
|
|
6
|
+
* Find export files under data dir matching the DE customer key and extension.
|
|
7
7
|
*
|
|
8
8
|
* @param {string} dataDir
|
|
9
9
|
* @param {string} customerKey
|
|
@@ -11,7 +11,6 @@ import { filterIllegalFilenames, MCDATA_SENTINEL } from './filename.mjs';
|
|
|
11
11
|
* @returns {Promise<string[]>} full paths
|
|
12
12
|
*/
|
|
13
13
|
export async function findImportCandidates(dataDir, customerKey, format) {
|
|
14
|
-
const prefix = filterIllegalFilenames(customerKey) + MCDATA_SENTINEL;
|
|
15
14
|
let entries;
|
|
16
15
|
try {
|
|
17
16
|
entries = await fs.readdir(dataDir, { withFileTypes: true });
|
|
@@ -28,8 +27,13 @@ export async function findImportCandidates(dataDir, customerKey, format) {
|
|
|
28
27
|
if (!name.endsWith(`.${ext}`)) {
|
|
29
28
|
continue;
|
|
30
29
|
}
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
try {
|
|
31
|
+
const { customerKey: parsedKey } = parseExportBasename(name);
|
|
32
|
+
if (parsedKey === customerKey) {
|
|
33
|
+
matches.push(path.join(dataDir, name));
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
continue;
|
|
33
37
|
}
|
|
34
38
|
}
|
|
35
39
|
return matches;
|
package/lib/filename.mjs
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* @see https://github.com/Accenture/sfmc-devtools (lib/util/file.js)
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
/** Literal segment between encoded DE key and timestamp (or extension in --git mode). */
|
|
8
|
+
export const MCDATA_SEGMENT = '.mcdata.';
|
|
9
|
+
|
|
7
10
|
/**
|
|
8
11
|
* @param {string} filename
|
|
9
12
|
* @returns {string}
|
|
@@ -35,17 +38,19 @@ export function reverseFilterIllegalFilenames(filename) {
|
|
|
35
38
|
return decodeURIComponent(filename).split('_STAR_').join('*');
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
/** Sentinel between encoded DE key and timestamp in export basenames; cannot appear in the key segment after encoding. */
|
|
39
|
-
export const MCDATA_SENTINEL = '+MCDATA+';
|
|
40
|
-
|
|
41
41
|
/**
|
|
42
42
|
* @param {string} customerKey
|
|
43
|
-
* @param {string} safeTs - filesystem-safe UTC timestamp
|
|
43
|
+
* @param {string} safeTs - filesystem-safe UTC timestamp (ignored when useGit is true)
|
|
44
44
|
* @param {'csv'|'tsv'|'json'} ext
|
|
45
|
+
* @param {boolean} [useGit] - stable `key.mcdata.ext` without timestamp
|
|
45
46
|
* @returns {string} basename without directory
|
|
46
47
|
*/
|
|
47
|
-
export function buildExportBasename(customerKey, safeTs, ext) {
|
|
48
|
-
|
|
48
|
+
export function buildExportBasename(customerKey, safeTs, ext, useGit = false) {
|
|
49
|
+
const enc = filterIllegalFilenames(customerKey);
|
|
50
|
+
if (useGit) {
|
|
51
|
+
return `${enc}.mcdata.${ext}`;
|
|
52
|
+
}
|
|
53
|
+
return `${enc}${MCDATA_SEGMENT}${safeTs}.${ext}`;
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
/**
|
|
@@ -57,24 +62,33 @@ export function filesystemSafeTimestamp(d = new Date()) {
|
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
/**
|
|
60
|
-
* @param {string} basename - e.g. `encodedKey
|
|
65
|
+
* @param {string} basename - e.g. `encodedKey.mcdata.2026-04-06T15-00-00.000Z.csv` or `encodedKey.mcdata.csv`
|
|
61
66
|
* @returns {{ customerKey: string, timestampPart: string, ext: string }}
|
|
62
67
|
*/
|
|
63
68
|
export function parseExportBasename(basename) {
|
|
64
69
|
const lastDot = basename.lastIndexOf('.');
|
|
65
70
|
const stem = lastDot === -1 ? basename : basename.slice(0, lastDot);
|
|
66
|
-
const ext = lastDot === -1 ? '' : basename.slice(lastDot + 1);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
);
|
|
71
|
+
const ext = lastDot === -1 ? '' : basename.slice(lastDot + 1).toLowerCase();
|
|
72
|
+
|
|
73
|
+
const idx = stem.indexOf(MCDATA_SEGMENT);
|
|
74
|
+
if (idx !== -1) {
|
|
75
|
+
const encodedKey = stem.slice(0, idx);
|
|
76
|
+
const timestampPart = stem.slice(idx + MCDATA_SEGMENT.length);
|
|
77
|
+
return {
|
|
78
|
+
customerKey: reverseFilterIllegalFilenames(encodedKey),
|
|
79
|
+
timestampPart,
|
|
80
|
+
ext,
|
|
81
|
+
};
|
|
72
82
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
|
|
84
|
+
if (stem.endsWith('.mcdata')) {
|
|
85
|
+
const encodedKey = stem.slice(0, -'.mcdata'.length);
|
|
86
|
+
return {
|
|
87
|
+
customerKey: reverseFilterIllegalFilenames(encodedKey),
|
|
88
|
+
timestampPart: '',
|
|
89
|
+
ext,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
throw new Error(`Filename must contain ".mcdata." or end with ".mcdata" before the extension: ${basename}`);
|
|
80
94
|
}
|
package/lib/import-de.mjs
CHANGED
|
@@ -8,23 +8,23 @@ import { readRowsFromFile } from './read-rows.mjs';
|
|
|
8
8
|
* @param {object} params
|
|
9
9
|
* @param {string} params.deKey
|
|
10
10
|
* @param {object[]} params.rows
|
|
11
|
-
* @param {'
|
|
12
|
-
* @
|
|
13
|
-
* @returns {Promise<void>}
|
|
11
|
+
* @param {'upsert'|'insert'} params.mode
|
|
12
|
+
* @returns {Promise<number>} number of rows imported
|
|
14
13
|
*/
|
|
15
14
|
export async function importRowsForDe(sdk, params) {
|
|
16
|
-
const { deKey, rows,
|
|
17
|
-
const route = resolveImportRoute(
|
|
15
|
+
const { deKey, rows, mode } = params;
|
|
16
|
+
const route = resolveImportRoute(mode);
|
|
18
17
|
const chunks = chunkItemsForPayload(rows);
|
|
19
18
|
for (const chunk of chunks) {
|
|
20
|
-
const
|
|
19
|
+
const p = route.path(deKey);
|
|
21
20
|
const body = { items: chunk };
|
|
22
21
|
await withRetry429(() =>
|
|
23
22
|
route.method === 'PUT'
|
|
24
|
-
? sdk.rest.put(
|
|
25
|
-
: sdk.rest.post(
|
|
23
|
+
? sdk.rest.put(p, body)
|
|
24
|
+
: sdk.rest.post(p, body)
|
|
26
25
|
);
|
|
27
26
|
}
|
|
27
|
+
return rows.length;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
@@ -33,16 +33,14 @@ export async function importRowsForDe(sdk, params) {
|
|
|
33
33
|
* @param {string} params.filePath
|
|
34
34
|
* @param {string} params.deKey - target DE customer key for API
|
|
35
35
|
* @param {'csv'|'tsv'|'json'} params.format
|
|
36
|
-
* @param {'
|
|
37
|
-
* @
|
|
38
|
-
* @returns {Promise<void>}
|
|
36
|
+
* @param {'upsert'|'insert'} params.mode
|
|
37
|
+
* @returns {Promise<number>} number of rows imported
|
|
39
38
|
*/
|
|
40
39
|
export async function importFromFile(sdk, params) {
|
|
41
40
|
const rows = await readRowsFromFile(params.filePath, params.format);
|
|
42
|
-
|
|
41
|
+
return importRowsForDe(sdk, {
|
|
43
42
|
deKey: params.deKey,
|
|
44
43
|
rows,
|
|
45
|
-
api: params.api,
|
|
46
44
|
mode: params.mode,
|
|
47
45
|
});
|
|
48
46
|
}
|
package/lib/import-routes.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* REST paths for Data Extension row
|
|
2
|
+
* REST paths for Data Extension row operations (relative to REST base URL).
|
|
3
3
|
* Confirm against current Salesforce reference hubs when adjusting behavior.
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -12,56 +12,24 @@ export function rowsetGetPath(deKey) {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Async bulk upsert (
|
|
15
|
+
* Async bulk row writes: POST insert, PUT upsert (same URL).
|
|
16
16
|
* @param {string} deKey
|
|
17
17
|
* @returns {string}
|
|
18
18
|
*/
|
|
19
|
-
export function
|
|
19
|
+
export function asyncDataExtensionRowsPath(deKey) {
|
|
20
20
|
return `/data/v1/async/dataextensions/key:${encodeURIComponent(deKey)}/rows`;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
* @param {string} deKey
|
|
26
|
-
* @returns {string}
|
|
27
|
-
*/
|
|
28
|
-
export function syncUpsertPath(deKey) {
|
|
29
|
-
return `/data/v1/customobjectdata/key/${encodeURIComponent(deKey)}/rows`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Synchronous insert row set (POST).
|
|
34
|
-
* @param {string} deKey
|
|
35
|
-
* @returns {string}
|
|
36
|
-
*/
|
|
37
|
-
export function syncInsertPath(deKey) {
|
|
38
|
-
return `/data/v1/customobjectdata/key/${encodeURIComponent(deKey)}/rows`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* @param {'async'|'sync'} api
|
|
43
|
-
* @param {'upsert'|'insert'|'update'} mode
|
|
24
|
+
* @param {'upsert'|'insert'} mode
|
|
44
25
|
* @returns {{ method: 'PUT'|'POST', path: (de: string) => string }}
|
|
45
26
|
*/
|
|
46
|
-
export function resolveImportRoute(
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
throw new Error(
|
|
50
|
-
`Import mode "${mode}" is not supported with --api async (use --api sync or --mode upsert).`
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
return { method: 'PUT', path: asyncUpsertPath };
|
|
27
|
+
export function resolveImportRoute(mode) {
|
|
28
|
+
if (mode === 'upsert') {
|
|
29
|
+
return { method: 'PUT', path: asyncDataExtensionRowsPath };
|
|
54
30
|
}
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
return { method: 'PUT', path: syncUpsertPath };
|
|
58
|
-
}
|
|
59
|
-
if (mode === 'insert') {
|
|
60
|
-
return { method: 'POST', path: syncInsertPath };
|
|
61
|
-
}
|
|
62
|
-
if (mode === 'update') {
|
|
63
|
-
return { method: 'PUT', path: syncUpsertPath };
|
|
64
|
-
}
|
|
31
|
+
if (mode === 'insert') {
|
|
32
|
+
return { method: 'POST', path: asyncDataExtensionRowsPath };
|
|
65
33
|
}
|
|
66
|
-
throw new Error(`Unsupported
|
|
34
|
+
throw new Error(`Unsupported import mode "${mode}" (use upsert or insert).`);
|
|
67
35
|
}
|
package/lib/index.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { main } from './cli.mjs';
|
|
2
2
|
export { filterIllegalFilenames, reverseFilterIllegalFilenames, parseExportBasename } from './filename.mjs';
|
|
3
3
|
export { chunkItemsForPayload, DEFAULT_MAX_BODY_BYTES, MAX_OBJECTS_PER_BATCH } from './batch.mjs';
|
|
4
|
-
export { resolveImportRoute, rowsetGetPath,
|
|
4
|
+
export { resolveImportRoute, rowsetGetPath, asyncDataExtensionRowsPath } from './import-routes.mjs';
|
|
5
5
|
export { loadMcdevProject, parseCredBu, resolveCredentialAndMid, buildSdkAuthObject } from './config.mjs';
|
|
6
6
|
export { multiBuExport } from './multi-bu-export.mjs';
|
|
7
7
|
export { crossBuImport } from './cross-bu-import.mjs';
|
|
8
|
+
export { projectRelativePosix } from './paths.mjs';
|
package/lib/multi-bu-export.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import SDK from 'sfmc-sdk';
|
|
2
2
|
import { resolveCredentialAndMid, buildSdkAuthObject } from './config.mjs';
|
|
3
3
|
import { exportDataExtensionToFile } from './export-de.mjs';
|
|
4
|
+
import { projectRelativePosix } from './paths.mjs';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @typedef {{ credential: string, bu: string }} CredBuSource
|
|
@@ -19,24 +20,27 @@ import { exportDataExtensionToFile } from './export-de.mjs';
|
|
|
19
20
|
* @param {string[]} params.deKeys
|
|
20
21
|
* @param {'csv'|'tsv'|'json'} params.format
|
|
21
22
|
* @param {boolean} [params.jsonPretty]
|
|
23
|
+
* @param {boolean} [params.useGit]
|
|
22
24
|
* @returns {Promise<string[]>} Paths of all written files
|
|
23
25
|
*/
|
|
24
|
-
export async function multiBuExport({ projectRoot, mcdevrc, mcdevAuth, sources, deKeys, format, jsonPretty = false }) {
|
|
26
|
+
export async function multiBuExport({ projectRoot, mcdevrc, mcdevAuth, sources, deKeys, format, jsonPretty = false, useGit = false }) {
|
|
25
27
|
/** @type {string[]} */
|
|
26
28
|
const exported = [];
|
|
27
29
|
for (const { credential, bu } of sources) {
|
|
28
30
|
const { mid, authCred } = resolveCredentialAndMid(mcdevrc, mcdevAuth, credential, bu);
|
|
29
31
|
const sdk = new SDK(buildSdkAuthObject(authCred, mid), { requestAttempts: 3 });
|
|
30
32
|
for (const deKey of deKeys) {
|
|
31
|
-
const outPath = await exportDataExtensionToFile(sdk, {
|
|
33
|
+
const { path: outPath, rowCount } = await exportDataExtensionToFile(sdk, {
|
|
32
34
|
projectRoot,
|
|
33
35
|
credentialName: credential,
|
|
34
36
|
buName: bu,
|
|
35
37
|
deKey,
|
|
36
38
|
format,
|
|
37
39
|
jsonPretty,
|
|
40
|
+
useGit,
|
|
38
41
|
});
|
|
39
|
-
|
|
42
|
+
const rel = projectRelativePosix(projectRoot, outPath);
|
|
43
|
+
console.error(`Exported: ${rel} (${rowCount} rows)`);
|
|
40
44
|
exported.push(outPath);
|
|
41
45
|
}
|
|
42
46
|
}
|
package/lib/paths.mjs
CHANGED
|
@@ -9,3 +9,19 @@ import path from 'node:path';
|
|
|
9
9
|
export function dataDirectoryForBu(projectRoot, credentialName, buName) {
|
|
10
10
|
return path.join(projectRoot, 'data', credentialName, buName);
|
|
11
11
|
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Path relative to mcdev project root for logs (POSIX-style, `./`-prefixed when needed).
|
|
15
|
+
*
|
|
16
|
+
* @param {string} projectRoot
|
|
17
|
+
* @param {string} absolutePath
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
export function projectRelativePosix(projectRoot, absolutePath) {
|
|
21
|
+
const rel = path.relative(path.resolve(projectRoot), path.resolve(absolutePath));
|
|
22
|
+
const norm = rel.split(path.sep).join('/');
|
|
23
|
+
if (norm === '' || norm === '.') {
|
|
24
|
+
return './';
|
|
25
|
+
}
|
|
26
|
+
return norm.startsWith('.') ? norm : `./${norm}`;
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sfmc-dataloader",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "CLI (mcdata) to export and import Marketing Cloud Data Extension rows using mcdev project config and sfmc-sdk",
|
|
3
|
+
"version": "2.0.0",
|
|
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",
|
|
7
7
|
"repository": {
|