sfmc-dataloader 1.2.0 → 2.0.1
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 -25
- package/lib/cli.mjs +44 -33
- package/lib/cross-bu-import.mjs +15 -22
- 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,29 +36,29 @@ 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
|
|
|
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
|
+
|
|
60
62
|
### Import — one source BU into multiple target BUs (API mode)
|
|
61
63
|
|
|
62
64
|
Use `--from` (one source) and `--to` (repeatable targets) for a cross-BU import where rows are fetched live from the source BU:
|
|
@@ -65,7 +67,7 @@ Use `--from` (one source) and `--to` (repeatable targets) for a cross-BU import
|
|
|
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.
|
|
69
71
|
|
|
70
72
|
### Import — local export files into multiple target BUs (file mode)
|
|
71
73
|
|
|
@@ -73,29 +75,23 @@ Use `--to` (repeatable targets) and `--file` (repeatable file paths) to push pre
|
|
|
73
75
|
|
|
74
76
|
```bash
|
|
75
77
|
mcdata import --to MyCred/QA --to MyCred/Prod \
|
|
76
|
-
--file ./data/MyCred/Dev/Contact_DE
|
|
78
|
+
--file ./data/MyCred/Dev/Contact_DE.mcdata.2026-04-08T10-00-00.000Z.csv
|
|
77
79
|
```
|
|
78
80
|
|
|
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.
|
|
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.
|
|
88
82
|
|
|
89
83
|
### Clear all rows before import
|
|
90
84
|
|
|
91
85
|
**Dangerous:** removes every row in the target Data Extension(s) before uploading.
|
|
92
86
|
|
|
93
87
|
Single-BU:
|
|
88
|
+
|
|
94
89
|
```bash
|
|
95
90
|
mcdata import MyCred/MyBU --de MyKey --clear-before-import
|
|
96
91
|
```
|
|
97
92
|
|
|
98
93
|
Cross-BU (warning lists every affected BU):
|
|
94
|
+
|
|
99
95
|
```bash
|
|
100
96
|
mcdata import --from MyCred/Dev --to MyCred/QA --to MyCred/Prod \
|
|
101
97
|
--de Contact_DE --clear-before-import
|
|
@@ -109,13 +105,15 @@ Interactive: type `YES` when prompted. In CI, add `--i-accept-clear-data-risk` a
|
|
|
109
105
|
|--------|-------------|
|
|
110
106
|
| `-p, --project` | Project root (default: cwd) |
|
|
111
107
|
| `--format` | `csv` (default), `tsv`, or `json` |
|
|
112
|
-
| `--
|
|
113
|
-
| `--mode` | `upsert` (default)
|
|
108
|
+
| `--git` | Stable export filenames: `<key>.mcdata.<ext>` (no timestamp segment) |
|
|
109
|
+
| `--mode` | `upsert` (default) or `insert` — async bulk REST API only |
|
|
114
110
|
| `--from <cred>/<bu>` | Export: source BU (repeatable). Import API mode: single source BU (use with `--to` and `--de`) |
|
|
115
111
|
| `--to <cred>/<bu>` | Import: target BU (repeatable). API mode: use with `--from`/`--de`. File mode: use with `--file` (no `--from` needed) |
|
|
116
112
|
| `--clear-before-import` | SOAP `ClearData` before REST import |
|
|
117
113
|
| `--i-accept-clear-data-risk` | Non-interactive consent for clear |
|
|
118
114
|
|
|
115
|
+
Log lines use paths **relative** to the project root (POSIX-style, `./…`) and include **row counts** where applicable.
|
|
116
|
+
|
|
119
117
|
## License
|
|
120
118
|
|
|
121
119
|
MIT — Author: Jörn Berkefeld
|
package/lib/cli.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
1
2
|
import { parseArgs } from 'node:util';
|
|
2
3
|
import process from 'node:process';
|
|
3
4
|
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
4
6
|
import SDK from 'sfmc-sdk';
|
|
5
7
|
import {
|
|
6
8
|
loadMcdevProject,
|
|
@@ -8,7 +10,7 @@ import {
|
|
|
8
10
|
resolveCredentialAndMid,
|
|
9
11
|
buildSdkAuthObject,
|
|
10
12
|
} from './config.mjs';
|
|
11
|
-
import { dataDirectoryForBu } from './paths.mjs';
|
|
13
|
+
import { dataDirectoryForBu, projectRelativePosix } from './paths.mjs';
|
|
12
14
|
import { exportDataExtensionToFile } from './export-de.mjs';
|
|
13
15
|
import { findImportCandidates, pickLatestByMtime } from './file-resolve.mjs';
|
|
14
16
|
import { parseExportBasename } from './filename.mjs';
|
|
@@ -18,6 +20,13 @@ import { confirmClearBeforeImport } from './confirm-clear.mjs';
|
|
|
18
20
|
import { multiBuExport } from './multi-bu-export.mjs';
|
|
19
21
|
import { crossBuImport } from './cross-bu-import.mjs';
|
|
20
22
|
|
|
23
|
+
/** @returns {string} semver from this package's package.json */
|
|
24
|
+
function readCliPackageVersion() {
|
|
25
|
+
const pkgPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
|
|
26
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
27
|
+
return typeof pkg.version === 'string' ? pkg.version : '';
|
|
28
|
+
}
|
|
29
|
+
|
|
21
30
|
function printHelp() {
|
|
22
31
|
console.log(`mcdata — SFMC Data Extension export/import (mcdev project)
|
|
23
32
|
|
|
@@ -29,13 +38,14 @@ Usage:
|
|
|
29
38
|
mcdata import --to <cred>/<bu> [--to <cred>/<bu> ...] --file <path> [--file <path> ...] [options]
|
|
30
39
|
|
|
31
40
|
Options:
|
|
41
|
+
--version Print version and exit
|
|
32
42
|
-p, --project <dir> mcdev project root (default: cwd)
|
|
33
43
|
--format <csv|tsv|json> File format (default: csv)
|
|
34
44
|
--json-pretty Pretty-print JSON on export
|
|
45
|
+
--git Stable filenames: <key>.mcdata.<ext> (no timestamp)
|
|
35
46
|
|
|
36
47
|
Import options:
|
|
37
|
-
--
|
|
38
|
-
--mode <upsert|insert|update> (default: upsert; insert/update require --api sync)
|
|
48
|
+
--mode <upsert|insert> Row write mode (default: upsert; async REST bulk API)
|
|
39
49
|
--clear-before-import SOAP ClearData before import (destructive; see below)
|
|
40
50
|
--i-accept-clear-data-risk Non-interactive acknowledgement for --clear-before-import
|
|
41
51
|
|
|
@@ -46,10 +56,10 @@ Multi-BU options:
|
|
|
46
56
|
Import (file mode): use with --file only (no --from needed)
|
|
47
57
|
|
|
48
58
|
Notes:
|
|
49
|
-
Exports are written under ./data/<credential>/<bu>/
|
|
59
|
+
Exports are written under ./data/<credential>/<bu>/ using ".mcdata." in the filename.
|
|
50
60
|
Import with --de resolves the latest matching file in that folder (by mtime).
|
|
51
|
-
Import with --file parses the DE key from the basename (
|
|
52
|
-
Cross-BU import stores a
|
|
61
|
+
Import with --file parses the DE key from the basename (.mcdata. format).
|
|
62
|
+
Cross-BU import stores a download file in each target BU's data directory.
|
|
53
63
|
|
|
54
64
|
Clear data warning:
|
|
55
65
|
--clear-before-import deletes ALL existing rows in the target DE(s) before upload.
|
|
@@ -74,14 +84,15 @@ export async function main(argv) {
|
|
|
74
84
|
format: { type: 'string' },
|
|
75
85
|
de: { type: 'string', multiple: true },
|
|
76
86
|
file: { type: 'string', multiple: true },
|
|
77
|
-
api: { type: 'string' },
|
|
78
|
-
mode: { type: 'string' },
|
|
79
87
|
from: { type: 'string', multiple: true },
|
|
80
88
|
to: { type: 'string', multiple: true },
|
|
89
|
+
git: { type: 'boolean', default: false },
|
|
90
|
+
mode: { type: 'string' },
|
|
81
91
|
'clear-before-import': { type: 'boolean', default: false },
|
|
82
92
|
'i-accept-clear-data-risk': { type: 'boolean', default: false },
|
|
83
93
|
'json-pretty': { type: 'boolean', default: false },
|
|
84
94
|
help: { type: 'boolean', short: 'h', default: false },
|
|
95
|
+
version: { type: 'boolean', default: false },
|
|
85
96
|
},
|
|
86
97
|
});
|
|
87
98
|
values = parsed.values;
|
|
@@ -96,6 +107,10 @@ export async function main(argv) {
|
|
|
96
107
|
printHelp();
|
|
97
108
|
return 0;
|
|
98
109
|
}
|
|
110
|
+
if (values.version) {
|
|
111
|
+
console.log(readCliPackageVersion());
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
99
114
|
if (positionals.length === 0) {
|
|
100
115
|
printHelp();
|
|
101
116
|
return 1;
|
|
@@ -111,6 +126,7 @@ export async function main(argv) {
|
|
|
111
126
|
return 1;
|
|
112
127
|
}
|
|
113
128
|
|
|
129
|
+
const useGit = values.git === true;
|
|
114
130
|
const fromFlags = values.from ?? [];
|
|
115
131
|
const toFlags = values.to ?? [];
|
|
116
132
|
const hasFrom = fromFlags.length > 0;
|
|
@@ -142,7 +158,6 @@ export async function main(argv) {
|
|
|
142
158
|
}
|
|
143
159
|
|
|
144
160
|
if (hasFrom) {
|
|
145
|
-
// Multi-BU export
|
|
146
161
|
let sources;
|
|
147
162
|
try {
|
|
148
163
|
sources = fromFlags.map(parseCredBu);
|
|
@@ -159,39 +174,36 @@ export async function main(argv) {
|
|
|
159
174
|
deKeys: des,
|
|
160
175
|
format: /** @type {'csv'|'tsv'|'json'} */ (fmt),
|
|
161
176
|
jsonPretty: values['json-pretty'],
|
|
177
|
+
useGit,
|
|
162
178
|
});
|
|
163
179
|
return 0;
|
|
164
180
|
}
|
|
165
181
|
|
|
166
|
-
// Single-BU export (original behavior)
|
|
167
182
|
const { credential, bu } = parseCredBu(credBuRaw);
|
|
168
183
|
const { mcdevrc, mcdevAuth } = loadMcdevProject(projectRoot);
|
|
169
184
|
const { mid, authCred } = resolveCredentialAndMid(mcdevrc, mcdevAuth, credential, bu);
|
|
170
185
|
const sdk = new SDK(buildSdkAuthObject(authCred, mid), { requestAttempts: 3 });
|
|
171
186
|
for (const deKey of des) {
|
|
172
|
-
const out = await exportDataExtensionToFile(sdk, {
|
|
187
|
+
const { path: out, rowCount } = await exportDataExtensionToFile(sdk, {
|
|
173
188
|
projectRoot,
|
|
174
189
|
credentialName: credential,
|
|
175
190
|
buName: bu,
|
|
176
191
|
deKey,
|
|
177
192
|
format: /** @type {'csv'|'tsv'|'json'} */ (fmt),
|
|
178
193
|
jsonPretty: values['json-pretty'],
|
|
194
|
+
useGit,
|
|
179
195
|
});
|
|
180
|
-
|
|
196
|
+
const rel = projectRelativePosix(projectRoot, out);
|
|
197
|
+
console.error(`Exported: ${rel} (${rowCount} rows)`);
|
|
181
198
|
}
|
|
182
199
|
return 0;
|
|
183
200
|
}
|
|
184
201
|
|
|
185
202
|
// ── import ──────────────────────────────────────────────────────────────
|
|
186
203
|
if (sub === 'import') {
|
|
187
|
-
const api = values.api ?? 'async';
|
|
188
204
|
const mode = values.mode ?? 'upsert';
|
|
189
|
-
if (!['
|
|
190
|
-
console.error(`Invalid --
|
|
191
|
-
return 1;
|
|
192
|
-
}
|
|
193
|
-
if (!['upsert', 'insert', 'update'].includes(mode)) {
|
|
194
|
-
console.error(`Invalid --mode: ${mode}`);
|
|
205
|
+
if (!['upsert', 'insert'].includes(mode)) {
|
|
206
|
+
console.error(`Invalid --mode: ${mode} (use upsert or insert)`);
|
|
195
207
|
return 1;
|
|
196
208
|
}
|
|
197
209
|
|
|
@@ -199,7 +211,6 @@ export async function main(argv) {
|
|
|
199
211
|
const acceptRisk = values['i-accept-clear-data-risk'];
|
|
200
212
|
|
|
201
213
|
// ── File-to-multi-BU import: --to + --file (no --from) ─────────────
|
|
202
|
-
// Rows are read from local export files; no source BU auth needed.
|
|
203
214
|
if (hasTo && !hasFrom && values.file?.length > 0) {
|
|
204
215
|
if (hasPositional) {
|
|
205
216
|
console.error('Cannot mix a positional <credential>/<bu> with --to/--file. Use one or the other.');
|
|
@@ -225,11 +236,11 @@ export async function main(argv) {
|
|
|
225
236
|
filePaths,
|
|
226
237
|
targets,
|
|
227
238
|
format: /** @type {'csv'|'tsv'|'json'} */ (fmt),
|
|
228
|
-
|
|
229
|
-
mode: /** @type {'upsert'|'insert'|'update'} */ (mode),
|
|
239
|
+
mode: /** @type {'upsert'|'insert'} */ (mode),
|
|
230
240
|
clearBeforeImport: clear,
|
|
231
241
|
acceptRiskFlag: acceptRisk,
|
|
232
242
|
isTTY: process.stdin.isTTY === true,
|
|
243
|
+
useGit,
|
|
233
244
|
});
|
|
234
245
|
return 0;
|
|
235
246
|
}
|
|
@@ -280,11 +291,11 @@ export async function main(argv) {
|
|
|
280
291
|
targets,
|
|
281
292
|
deKeys,
|
|
282
293
|
format: /** @type {'csv'|'tsv'|'json'} */ (fmt),
|
|
283
|
-
|
|
284
|
-
mode: /** @type {'upsert'|'insert'|'update'} */ (mode),
|
|
294
|
+
mode: /** @type {'upsert'|'insert'} */ (mode),
|
|
285
295
|
clearBeforeImport: clear,
|
|
286
296
|
acceptRiskFlag: acceptRisk,
|
|
287
297
|
isTTY: process.stdin.isTTY === true,
|
|
298
|
+
useGit,
|
|
288
299
|
});
|
|
289
300
|
return 0;
|
|
290
301
|
}
|
|
@@ -324,19 +335,19 @@ export async function main(argv) {
|
|
|
324
335
|
for (const deKey of deKeys) {
|
|
325
336
|
const candidates = await findImportCandidates(dataDir, deKey, fmt);
|
|
326
337
|
if (candidates.length === 0) {
|
|
327
|
-
console.error(`No ${fmt} file found for DE "${deKey}" under ${dataDir}`);
|
|
338
|
+
console.error(`No ${fmt} file found for DE "${deKey}" under ${projectRelativePosix(projectRoot, dataDir)}`);
|
|
328
339
|
return 1;
|
|
329
340
|
}
|
|
330
341
|
const filePath =
|
|
331
342
|
candidates.length === 1 ? candidates[0] : await pickLatestByMtime(candidates);
|
|
332
|
-
await importFromFile(sdk, {
|
|
343
|
+
const n = await importFromFile(sdk, {
|
|
333
344
|
filePath,
|
|
334
345
|
deKey,
|
|
335
346
|
format: /** @type {'csv'|'tsv'|'json'} */ (fmt),
|
|
336
|
-
|
|
337
|
-
mode: /** @type {'upsert'|'insert'|'update'} */ (mode),
|
|
347
|
+
mode: /** @type {'upsert'|'insert'} */ (mode),
|
|
338
348
|
});
|
|
339
|
-
|
|
349
|
+
const rel = projectRelativePosix(projectRoot, filePath);
|
|
350
|
+
console.error(`Imported: ${rel} (${n} rows) -> DE ${deKey}`);
|
|
340
351
|
}
|
|
341
352
|
return 0;
|
|
342
353
|
}
|
|
@@ -356,14 +367,14 @@ export async function main(argv) {
|
|
|
356
367
|
for (const filePath of fileList) {
|
|
357
368
|
const base = path.basename(filePath);
|
|
358
369
|
const { customerKey } = parseExportBasename(base);
|
|
359
|
-
await importFromFile(sdk, {
|
|
370
|
+
const n = await importFromFile(sdk, {
|
|
360
371
|
filePath,
|
|
361
372
|
deKey: customerKey,
|
|
362
373
|
format: /** @type {'csv'|'tsv'|'json'} */ (fmt),
|
|
363
|
-
|
|
364
|
-
mode: /** @type {'upsert'|'insert'|'update'} */ (mode),
|
|
374
|
+
mode: /** @type {'upsert'|'insert'} */ (mode),
|
|
365
375
|
});
|
|
366
|
-
|
|
376
|
+
const rel = projectRelativePosix(projectRoot, filePath);
|
|
377
|
+
console.error(`Imported: ${rel} (${n} rows)`);
|
|
367
378
|
}
|
|
368
379
|
return 0;
|
|
369
380
|
}
|
package/lib/cross-bu-import.mjs
CHANGED
|
@@ -4,13 +4,12 @@ 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';
|
|
10
9
|
import { readRowsFromFile } from './read-rows.mjs';
|
|
11
10
|
import { clearDataExtensionRows } from './clear-de.mjs';
|
|
12
11
|
import { confirmClearBeforeImport } from './confirm-clear.mjs';
|
|
13
|
-
import { dataDirectoryForBu } from './paths.mjs';
|
|
12
|
+
import { dataDirectoryForBu, projectRelativePosix } from './paths.mjs';
|
|
14
13
|
import { buildExportBasename, filesystemSafeTimestamp, parseExportBasename } from './filename.mjs';
|
|
15
14
|
|
|
16
15
|
/**
|
|
@@ -62,13 +61,6 @@ async function offerPreExportBackup({ targets, deKeys, stdin: stdinStream, stdou
|
|
|
62
61
|
* `sourceCred`/`sourceBu` must be omitted. The DE customer key is derived
|
|
63
62
|
* from each filename via `parseExportBasename`.
|
|
64
63
|
*
|
|
65
|
-
* Before the import each target BU:
|
|
66
|
-
* 1. Optionally exports its current DE data as a timestamped backup (TTY only).
|
|
67
|
-
* 2. Optionally clears all existing rows (with danger warning covering every target).
|
|
68
|
-
* 3. Receives the source rows written to a timestamped "download" file in its
|
|
69
|
-
* own `./data/<credential>/<bu>/` directory (mirroring mcdev retrieve).
|
|
70
|
-
* 4. Has the rows imported via the REST API.
|
|
71
|
-
*
|
|
72
64
|
* @param {object} params
|
|
73
65
|
* @param {string} params.projectRoot
|
|
74
66
|
* @param {import('./config.mjs').Mcdevrc} params.mcdevrc
|
|
@@ -79,11 +71,11 @@ async function offerPreExportBackup({ targets, deKeys, stdin: stdinStream, stdou
|
|
|
79
71
|
* @param {string[]} [params.filePaths] - File mode only; mutually exclusive with sourceCred/sourceBu/deKeys
|
|
80
72
|
* @param {CredBuTarget[]} params.targets
|
|
81
73
|
* @param {'csv'|'tsv'|'json'} params.format
|
|
82
|
-
* @param {'
|
|
83
|
-
* @param {'upsert'|'insert'|'update'} params.mode
|
|
74
|
+
* @param {'upsert'|'insert'} params.mode
|
|
84
75
|
* @param {boolean} params.clearBeforeImport
|
|
85
76
|
* @param {boolean} params.acceptRiskFlag
|
|
86
77
|
* @param {boolean} params.isTTY
|
|
78
|
+
* @param {boolean} [params.useGit] - stable snapshot basename (no timestamp)
|
|
87
79
|
* @param {NodeJS.ReadableStream} [params.stdin] Override for testing
|
|
88
80
|
* @param {NodeJS.WritableStream} [params.stdout] Override for testing
|
|
89
81
|
* @returns {Promise<void>}
|
|
@@ -95,11 +87,11 @@ export async function crossBuImport(params) {
|
|
|
95
87
|
mcdevAuth,
|
|
96
88
|
targets,
|
|
97
89
|
format,
|
|
98
|
-
api,
|
|
99
90
|
mode,
|
|
100
91
|
clearBeforeImport,
|
|
101
92
|
acceptRiskFlag,
|
|
102
93
|
isTTY,
|
|
94
|
+
useGit = false,
|
|
103
95
|
} = params;
|
|
104
96
|
const stdin = params.stdin;
|
|
105
97
|
const stdout = params.stdout;
|
|
@@ -144,14 +136,16 @@ export async function crossBuImport(params) {
|
|
|
144
136
|
const { mid, authCred } = resolveCredentialAndMid(mcdevrc, mcdevAuth, credential, bu);
|
|
145
137
|
const tgtSdk = new SDK(buildSdkAuthObject(authCred, mid), { requestAttempts: 3 });
|
|
146
138
|
for (const deKey of deKeys) {
|
|
147
|
-
const outPath = await exportDataExtensionToFile(tgtSdk, {
|
|
139
|
+
const { path: outPath, rowCount } = await exportDataExtensionToFile(tgtSdk, {
|
|
148
140
|
projectRoot,
|
|
149
141
|
credentialName: credential,
|
|
150
142
|
buName: bu,
|
|
151
143
|
deKey,
|
|
152
144
|
format,
|
|
145
|
+
useGit: false,
|
|
153
146
|
});
|
|
154
|
-
|
|
147
|
+
const rel = projectRelativePosix(projectRoot, outPath);
|
|
148
|
+
console.error(`Backup export: ${rel} (${rowCount} rows)`);
|
|
155
149
|
}
|
|
156
150
|
}
|
|
157
151
|
}
|
|
@@ -181,19 +175,18 @@ export async function crossBuImport(params) {
|
|
|
181
175
|
const { mid, authCred } = resolveCredentialAndMid(mcdevrc, mcdevAuth, credential, bu);
|
|
182
176
|
const tgtSdk = new SDK(buildSdkAuthObject(authCred, mid), { requestAttempts: 3 });
|
|
183
177
|
|
|
184
|
-
// Write a
|
|
185
|
-
// This mirrors what mcdev retrieve / mcdata export produces, giving a
|
|
186
|
-
// traceable snapshot of exactly what was imported.
|
|
178
|
+
// Write a snapshot file in the target BU's data directory.
|
|
187
179
|
const dir = dataDirectoryForBu(projectRoot, credential, bu);
|
|
188
180
|
await fs.mkdir(dir, { recursive: true });
|
|
189
181
|
const ts = filesystemSafeTimestamp();
|
|
190
|
-
const basename = buildExportBasename(deKey, ts, format);
|
|
182
|
+
const basename = buildExportBasename(deKey, ts, format, useGit);
|
|
191
183
|
const snapshotPath = path.join(dir, basename);
|
|
192
184
|
await fs.writeFile(snapshotPath, serializeRows(rows, format, false), 'utf8');
|
|
193
|
-
|
|
185
|
+
const snapRel = projectRelativePosix(projectRoot, snapshotPath);
|
|
186
|
+
console.error(`Download stored: ${snapRel} (${rows.length} rows)`);
|
|
194
187
|
|
|
195
|
-
await importRowsForDe(tgtSdk, { deKey, rows,
|
|
196
|
-
console.error(`Imported
|
|
188
|
+
const imported = await importRowsForDe(tgtSdk, { deKey, rows, mode });
|
|
189
|
+
console.error(`Imported: ${credential}/${bu} DE ${deKey} (${imported} rows)`);
|
|
197
190
|
}
|
|
198
191
|
}
|
|
199
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.1",
|
|
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": {
|