sfmc-dataloader 2.4.2 → 2.5.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 +49 -4
- package/bin/mcdata.mjs +1 -1
- package/lib/business-units.mjs +93 -0
- package/lib/cli.mjs +51 -23
- package/lib/config.mjs +72 -13
- package/lib/cross-bu-import.mjs +3 -5
- package/lib/import-de.mjs +22 -3
- package/lib/index.mjs +6 -0
- package/lib/init-project.mjs +214 -0
- package/lib/multi-bu-export.mjs +2 -3
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,12 +1,57 @@
|
|
|
1
1
|
# sfmc-dataloader
|
|
2
2
|
|
|
3
|
-
Command-line tool **`mcdata`** to export and import **Salesforce Marketing Cloud Engagement** Data Extension rows using
|
|
3
|
+
Command-line tool **`mcdata`** to export and import **Salesforce Marketing Cloud Engagement** Data Extension rows using [sfmc-sdk](https://www.npmjs.com/package/sfmc-sdk) for REST/SOAP.
|
|
4
|
+
|
|
5
|
+
Works **standalone** — no mcdev installation required — and also integrates with existing [mcdev](https://github.com/Accenture/sfmc-devtools) projects.
|
|
6
|
+
|
|
7
|
+
## Config files
|
|
8
|
+
|
|
9
|
+
| File pair | When to use |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `.mcdevrc.json` + `.mcdev-auth.json` | Existing mcdev projects — **always wins** when both pairs present |
|
|
12
|
+
| `.mcdatarc.json` + `.mcdata-auth.json` | Standalone setup; created by `mcdata init` |
|
|
13
|
+
|
|
14
|
+
**Precedence:** When `.mcdevrc.json` and `.mcdev-auth.json` both exist they are loaded; any mcdata files are ignored (a warning is printed to stderr). Both file pairs share the same logical shape (`credentials.<name>.businessUnits`), so all commands work with either layout.
|
|
15
|
+
|
|
16
|
+
## Standalone setup with `mcdata init`
|
|
17
|
+
|
|
18
|
+
If you don't have an existing mcdev project, run:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
mcdata init
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The interactive wizard collects:
|
|
25
|
+
|
|
26
|
+
1. Credential name (e.g. `MyOrg`)
|
|
27
|
+
2. Installed-package **Client ID**
|
|
28
|
+
3. Installed-package **Client Secret**
|
|
29
|
+
4. **Auth URL** (e.g. `https://<tenantsubdomain>.auth.marketingcloudapis.com/`)
|
|
30
|
+
5. **Enterprise MID** (parent account ID)
|
|
31
|
+
|
|
32
|
+
It fetches your Business Unit list via the SOAP API, writes `.mcdatarc.json` and `.mcdata-auth.json`, and adds the auth file to `.gitignore`.
|
|
33
|
+
|
|
34
|
+
### Non-interactive / CI mode
|
|
35
|
+
|
|
36
|
+
Pass all five flags to skip prompts:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
mcdata init \
|
|
40
|
+
--credential MyOrg \
|
|
41
|
+
--client-id <id> \
|
|
42
|
+
--client-secret <secret> \
|
|
43
|
+
--auth-url https://<tenantsubdomain>.auth.marketingcloudapis.com/ \
|
|
44
|
+
--enterprise-id <eid> \
|
|
45
|
+
--yes
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Use `--yes` to overwrite existing `.mcdatarc.json` / `.mcdata-auth.json` without confirmation.
|
|
4
49
|
|
|
5
50
|
## Requirements
|
|
6
51
|
|
|
7
52
|
- Node.js `^20.19.0 || ^22.13.0 || >=24` (aligned with `sfmc-sdk`)
|
|
8
|
-
-
|
|
9
|
-
-
|
|
53
|
+
- An SFMC installed package with Data Extension access
|
|
54
|
+
- **Optional:** `mcdev` `>=7` — listed in `optionalDependencies`; install globally if you also use mcdev for other metadata types
|
|
10
55
|
|
|
11
56
|
## Install
|
|
12
57
|
|
|
@@ -128,7 +173,7 @@ Interactive: type `YES` when prompted. In CI, add `--i-accept-clear-data-risk` a
|
|
|
128
173
|
| `--clear-before-import` | SOAP `ClearData` before REST import |
|
|
129
174
|
| `--i-accept-clear-data-risk` | Non-interactive consent for clear |
|
|
130
175
|
|
|
131
|
-
Log lines
|
|
176
|
+
Log lines include **row counts** and show file paths as **absolute paths in double-quotes** (e.g. `"C:\data\MyCred\DEV\Contact.mcdata.csv"`) so they are clickable in VS Code's integrated terminal.
|
|
132
177
|
|
|
133
178
|
## License
|
|
134
179
|
|
package/bin/mcdata.mjs
CHANGED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import SDK from 'sfmc-sdk';
|
|
2
|
+
import { buildSdkOptions } from './config.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Normalize a Business Unit name to a safe identifier, mirroring mcdev's convention:
|
|
6
|
+
* strip non-word/non-space characters, replace spaces with underscores, collapse consecutive underscores.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} name - Raw BU name from the API
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
export function normalizeBuName(name) {
|
|
12
|
+
return name
|
|
13
|
+
.replaceAll(/[^\w\s]/gi, '')
|
|
14
|
+
.replaceAll(/ +/g, '_')
|
|
15
|
+
.replaceAll(/__+/g, '_');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {object} BusinessUnitsResult
|
|
20
|
+
* @property {number} eid - Enterprise MID (ID of the parent BU)
|
|
21
|
+
* @property {Record<string, number>} businessUnits - Normalized BU name → MID mapping; parent BU is stored under `_ParentBU_`
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Process a SOAP BusinessUnit retrieve result into the mcdata config shape.
|
|
26
|
+
* Exposed separately so it can be tested without a live SDK instance.
|
|
27
|
+
*
|
|
28
|
+
* @param {object[]} results - `buResult.Results` array from the SOAP API
|
|
29
|
+
* @param {number} enterpriseId - Fallback EID when no parent row is found
|
|
30
|
+
* @returns {BusinessUnitsResult}
|
|
31
|
+
*/
|
|
32
|
+
export function processBusinessUnitResults(results, enterpriseId) {
|
|
33
|
+
/** @type {Record<string, number>} */
|
|
34
|
+
const businessUnits = {};
|
|
35
|
+
let eid = enterpriseId;
|
|
36
|
+
|
|
37
|
+
for (const row of results) {
|
|
38
|
+
const id = Number.parseInt(row.ID, 10);
|
|
39
|
+
const parentId = Number.parseInt(row.ParentID, 10);
|
|
40
|
+
|
|
41
|
+
if (parentId === 0) {
|
|
42
|
+
businessUnits['_ParentBU_'] = id;
|
|
43
|
+
eid = id;
|
|
44
|
+
} else {
|
|
45
|
+
businessUnits[normalizeBuName(row.Name)] = id;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { eid, businessUnits };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Fetch all Business Units for a Marketing Cloud instance via the SOAP API.
|
|
54
|
+
* Instantiates a temporary SDK client scoped to the enterprise account (`account_id: enterpriseId`)
|
|
55
|
+
* and calls `BusinessUnit` retrieve with `QueryAllAccounts: true`.
|
|
56
|
+
*
|
|
57
|
+
* @param {{ client_id: string, client_secret: string, auth_url: string }} authCred
|
|
58
|
+
* @param {number} enterpriseId - Enterprise MID (parent account ID)
|
|
59
|
+
* @returns {Promise.<BusinessUnitsResult>}
|
|
60
|
+
*/
|
|
61
|
+
export async function fetchBusinessUnits(authCred, enterpriseId) {
|
|
62
|
+
const sdk = new SDK(
|
|
63
|
+
{
|
|
64
|
+
client_id: authCred.client_id,
|
|
65
|
+
client_secret: authCred.client_secret,
|
|
66
|
+
auth_url: authCred.auth_url,
|
|
67
|
+
account_id: enterpriseId,
|
|
68
|
+
},
|
|
69
|
+
buildSdkOptions(),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
let buResult;
|
|
73
|
+
try {
|
|
74
|
+
buResult = await sdk.soap.retrieve(
|
|
75
|
+
'BusinessUnit',
|
|
76
|
+
['Name', 'ID', 'ParentName', 'ParentID', 'IsActive'],
|
|
77
|
+
{ QueryAllAccounts: true },
|
|
78
|
+
);
|
|
79
|
+
} catch (ex) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Could not retrieve Business Units - check client_id, client_secret, auth_url, and enterprise MID.`,
|
|
82
|
+
{ cause: ex },
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!buResult?.Results?.length) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
'Credentials accepted but no Business Units returned. Verify this installed package has access to the parent BU.',
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return processBusinessUnitResults(buResult.Results, enterpriseId);
|
|
93
|
+
}
|
package/lib/cli.mjs
CHANGED
|
@@ -5,13 +5,13 @@ import path from 'node:path';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import SDK from 'sfmc-sdk';
|
|
7
7
|
import {
|
|
8
|
-
|
|
8
|
+
loadProjectConfig,
|
|
9
9
|
parseCredBu,
|
|
10
10
|
resolveCredentialAndMid,
|
|
11
11
|
buildSdkAuthObject,
|
|
12
12
|
buildSdkOptions,
|
|
13
13
|
} from './config.mjs';
|
|
14
|
-
import { dataDirectoryForBu
|
|
14
|
+
import { dataDirectoryForBu } from './paths.mjs';
|
|
15
15
|
import { exportDataExtensionToFile } from './export-de.mjs';
|
|
16
16
|
import { findImportCandidates, pickLatestByMtime } from './file-resolve.mjs';
|
|
17
17
|
import { parseExportBasename } from './filename.mjs';
|
|
@@ -23,6 +23,7 @@ import { getDeRowCount } from './row-count.mjs';
|
|
|
23
23
|
import { multiBuExport } from './multi-bu-export.mjs';
|
|
24
24
|
import { crossBuImport } from './cross-bu-import.mjs';
|
|
25
25
|
import { initDebugLogger } from './debug-logger.mjs';
|
|
26
|
+
import { runMcdataInit } from './init-project.mjs';
|
|
26
27
|
|
|
27
28
|
/** @returns {string} semver from this package's package.json */
|
|
28
29
|
function readCliPackageVersion() {
|
|
@@ -36,9 +37,10 @@ function readCliPackageVersion() {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
function printHelp() {
|
|
39
|
-
console.log(`mcdata
|
|
40
|
+
console.log(`mcdata - SFMC Data Extension export/import
|
|
40
41
|
|
|
41
42
|
Usage:
|
|
43
|
+
mcdata init [options]
|
|
42
44
|
mcdata export <credential>/<bu> --de <key> [--de <key> ...] [options]
|
|
43
45
|
mcdata export --from <cred>/<bu> [--from <cred>/<bu> ...] --de <key> [--de <key> ...] [options]
|
|
44
46
|
mcdata import <credential>/<bu> (--de <key> ... | --file <path> ...) [options]
|
|
@@ -47,12 +49,20 @@ Usage:
|
|
|
47
49
|
|
|
48
50
|
Options:
|
|
49
51
|
--version Print version and exit
|
|
50
|
-
-p, --project <dir>
|
|
52
|
+
-p, --project <dir> Project root (default: cwd)
|
|
51
53
|
--format <csv|tsv|json> Export file format (default: csv); ignored for imports
|
|
52
54
|
--json-pretty Pretty-print JSON on export
|
|
53
55
|
--git Stable filenames: <key>.mcdata.<ext> (no timestamp)
|
|
54
56
|
--debug Write API requests/responses to ./logs/data/*.log
|
|
55
57
|
|
|
58
|
+
Init options:
|
|
59
|
+
--credential <name> Credential name (e.g. MyOrg)
|
|
60
|
+
--client-id <id> Installed package client ID
|
|
61
|
+
--client-secret <sec> Installed package client secret
|
|
62
|
+
--auth-url <url> Auth URL (e.g. https://<tenantsubdomain>.auth.marketingcloudapis.com/)
|
|
63
|
+
--enterprise-id <mid> Enterprise MID (parent account ID)
|
|
64
|
+
-y, --yes Overwrite existing .mcdatarc.json / .mcdata-auth.json without prompt
|
|
65
|
+
|
|
56
66
|
Import options:
|
|
57
67
|
--mode <upsert|insert> Row write mode (default: upsert; async REST bulk API)
|
|
58
68
|
--backup-before-import Export target DE data as a timestamped backup before import (no prompt)
|
|
@@ -66,6 +76,10 @@ Multi-BU options:
|
|
|
66
76
|
--to <cred>/<bu> Import: target BU (repeatable for multiple targets)
|
|
67
77
|
Import (file mode): use with --file only (no --from needed)
|
|
68
78
|
|
|
79
|
+
Config files:
|
|
80
|
+
.mcdevrc.json / .mcdev-auth.json mcdev layout (wins when both pairs present)
|
|
81
|
+
.mcdatarc.json / .mcdata-auth.json standalone mcdata layout (created by mcdata init)
|
|
82
|
+
|
|
69
83
|
Notes:
|
|
70
84
|
Exports are written under ./data/<credential>/<bu>/ using ".mcdata." in the filename.
|
|
71
85
|
Import with --de resolves the latest matching file (csv/tsv/json) in that folder (by mtime).
|
|
@@ -108,6 +122,12 @@ export async function main(argv) {
|
|
|
108
122
|
debug: { type: 'boolean', default: false },
|
|
109
123
|
help: { type: 'boolean', short: 'h', default: false },
|
|
110
124
|
version: { type: 'boolean', default: false },
|
|
125
|
+
credential: { type: 'string' },
|
|
126
|
+
'client-id': { type: 'string' },
|
|
127
|
+
'client-secret': { type: 'string' },
|
|
128
|
+
'auth-url': { type: 'string' },
|
|
129
|
+
'enterprise-id': { type: 'string' },
|
|
130
|
+
yes: { type: 'boolean', short: 'y', default: false },
|
|
111
131
|
},
|
|
112
132
|
});
|
|
113
133
|
values = parsed.values;
|
|
@@ -135,6 +155,21 @@ export async function main(argv) {
|
|
|
135
155
|
const credBuRaw = positionals[1]; // May be undefined when --from/--to flags are used
|
|
136
156
|
|
|
137
157
|
const projectRoot = path.resolve(values.project ?? process.cwd());
|
|
158
|
+
|
|
159
|
+
// ── init ─────────────────────────────────────────────────────────────────
|
|
160
|
+
if (sub === 'init') {
|
|
161
|
+
return runMcdataInit({
|
|
162
|
+
projectRoot,
|
|
163
|
+
isTTY: process.stdin.isTTY === true,
|
|
164
|
+
credential: values.credential,
|
|
165
|
+
clientId: values['client-id'],
|
|
166
|
+
clientSecret: values['client-secret'],
|
|
167
|
+
authUrl: values['auth-url'],
|
|
168
|
+
enterpriseId: values['enterprise-id'],
|
|
169
|
+
yes: values.yes,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
138
173
|
const fmt = values.format ?? 'csv';
|
|
139
174
|
if (!['csv', 'tsv', 'json'].includes(fmt)) {
|
|
140
175
|
console.error(`Invalid --format: ${fmt}`);
|
|
@@ -160,7 +195,7 @@ export async function main(argv) {
|
|
|
160
195
|
? initDebugLogger(projectRoot, readCliPackageVersion(), argv)
|
|
161
196
|
: null;
|
|
162
197
|
if (logger) {
|
|
163
|
-
console.error(`Debug log: ${
|
|
198
|
+
console.error(`Debug log: "${path.resolve(logger.logPath)}"`);
|
|
164
199
|
}
|
|
165
200
|
|
|
166
201
|
// ── export ──────────────────────────────────────────────────────────────
|
|
@@ -199,7 +234,7 @@ export async function main(argv) {
|
|
|
199
234
|
console.error(ex.message);
|
|
200
235
|
return 1;
|
|
201
236
|
}
|
|
202
|
-
const { mcdevrc, mcdevAuth } =
|
|
237
|
+
const { mcdevrc, mcdevAuth } = loadProjectConfig(projectRoot);
|
|
203
238
|
await multiBuExport({
|
|
204
239
|
projectRoot,
|
|
205
240
|
mcdevrc,
|
|
@@ -215,7 +250,7 @@ export async function main(argv) {
|
|
|
215
250
|
}
|
|
216
251
|
|
|
217
252
|
const { credential, bu } = parseCredBu(credBuRaw);
|
|
218
|
-
const { mcdevrc, mcdevAuth } =
|
|
253
|
+
const { mcdevrc, mcdevAuth } = loadProjectConfig(projectRoot);
|
|
219
254
|
const { mid, authCred } = resolveCredentialAndMid(mcdevrc, mcdevAuth, credential, bu);
|
|
220
255
|
const sdk = new SDK(buildSdkAuthObject(authCred, mid), buildSdkOptions(logger));
|
|
221
256
|
for (const deKey of des) {
|
|
@@ -228,8 +263,7 @@ export async function main(argv) {
|
|
|
228
263
|
jsonPretty: values['json-pretty'],
|
|
229
264
|
useGit,
|
|
230
265
|
});
|
|
231
|
-
|
|
232
|
-
console.error(`Exported: ${rel} (${rowCount} rows)`);
|
|
266
|
+
console.error(`Exported: "${path.resolve(out)}" (${rowCount} rows)`);
|
|
233
267
|
}
|
|
234
268
|
return 0;
|
|
235
269
|
}
|
|
@@ -267,7 +301,7 @@ export async function main(argv) {
|
|
|
267
301
|
console.error(ex.message);
|
|
268
302
|
return 1;
|
|
269
303
|
}
|
|
270
|
-
const { mcdevrc, mcdevAuth } =
|
|
304
|
+
const { mcdevrc, mcdevAuth } = loadProjectConfig(projectRoot);
|
|
271
305
|
const crossBuHadError = await crossBuImport({
|
|
272
306
|
projectRoot,
|
|
273
307
|
mcdevrc,
|
|
@@ -332,7 +366,7 @@ export async function main(argv) {
|
|
|
332
366
|
console.error(ex.message);
|
|
333
367
|
return 1;
|
|
334
368
|
}
|
|
335
|
-
const { mcdevrc, mcdevAuth } =
|
|
369
|
+
const { mcdevrc, mcdevAuth } = loadProjectConfig(projectRoot);
|
|
336
370
|
const crossBuApiHadError = await crossBuImport({
|
|
337
371
|
projectRoot,
|
|
338
372
|
mcdevrc,
|
|
@@ -372,7 +406,7 @@ export async function main(argv) {
|
|
|
372
406
|
}
|
|
373
407
|
|
|
374
408
|
const { credential, bu } = parseCredBu(credBuRaw);
|
|
375
|
-
const { mcdevrc, mcdevAuth } =
|
|
409
|
+
const { mcdevrc, mcdevAuth } = loadProjectConfig(projectRoot);
|
|
376
410
|
const { mid, authCred } = resolveCredentialAndMid(mcdevrc, mcdevAuth, credential, bu);
|
|
377
411
|
const sdk = new SDK(buildSdkAuthObject(authCred, mid), buildSdkOptions(logger));
|
|
378
412
|
|
|
@@ -389,9 +423,7 @@ export async function main(argv) {
|
|
|
389
423
|
format: /** @type {'csv'|'tsv'|'json'} */ (fmt),
|
|
390
424
|
useGit: false,
|
|
391
425
|
});
|
|
392
|
-
console.error(
|
|
393
|
-
`Backup export: ${projectRelativePosix(projectRoot, outPath)} (${rowCount} rows)`,
|
|
394
|
-
);
|
|
426
|
+
console.error(`Backup export: "${path.resolve(outPath)}" (${rowCount} rows)`);
|
|
395
427
|
}
|
|
396
428
|
}
|
|
397
429
|
if (clear) {
|
|
@@ -406,7 +438,7 @@ export async function main(argv) {
|
|
|
406
438
|
const candidates = await findImportCandidates(dataDir, deKey);
|
|
407
439
|
if (candidates.length === 0) {
|
|
408
440
|
console.error(
|
|
409
|
-
`No import file (csv/tsv/json) found for DE "${deKey}" under ${
|
|
441
|
+
`No import file (csv/tsv/json) found for DE "${deKey}" under "${path.resolve(dataDir)}"`,
|
|
410
442
|
);
|
|
411
443
|
return 1;
|
|
412
444
|
}
|
|
@@ -434,8 +466,7 @@ export async function main(argv) {
|
|
|
434
466
|
deKey,
|
|
435
467
|
mode: /** @type {'upsert'|'insert'} */ (mode),
|
|
436
468
|
});
|
|
437
|
-
|
|
438
|
-
console.error(`Imported: ${rel} (${n} rows) -> DE ${deKey}`);
|
|
469
|
+
console.error(`Imported: "${path.resolve(filePath)}" (${n} rows) -> DE ${deKey}`);
|
|
439
470
|
|
|
440
471
|
const importHadError = await pollAsyncImportCompletion(sdk, requestIds);
|
|
441
472
|
if (importHadError) {
|
|
@@ -476,9 +507,7 @@ export async function main(argv) {
|
|
|
476
507
|
format: /** @type {'csv'|'tsv'|'json'} */ (fmt),
|
|
477
508
|
useGit: false,
|
|
478
509
|
});
|
|
479
|
-
console.error(
|
|
480
|
-
`Backup export: ${projectRelativePosix(projectRoot, outPath)} (${rowCount} rows)`,
|
|
481
|
-
);
|
|
510
|
+
console.error(`Backup export: "${path.resolve(outPath)}" (${rowCount} rows)`);
|
|
482
511
|
}
|
|
483
512
|
}
|
|
484
513
|
if (clear) {
|
|
@@ -514,8 +543,7 @@ export async function main(argv) {
|
|
|
514
543
|
deKey: customerKey,
|
|
515
544
|
mode: /** @type {'upsert'|'insert'} */ (mode),
|
|
516
545
|
});
|
|
517
|
-
|
|
518
|
-
console.error(`Imported: ${rel} (${n} rows)`);
|
|
546
|
+
console.error(`Imported: "${path.resolve(filePath)}" (${n} rows)`);
|
|
519
547
|
|
|
520
548
|
const importHadError = await pollAsyncImportCompletion(sdk, requestIds);
|
|
521
549
|
if (importHadError) {
|
package/lib/config.mjs
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
+
export const FILE_MCDEV_RC = '.mcdevrc.json';
|
|
5
|
+
export const FILE_MCDEV_AUTH = '.mcdev-auth.json';
|
|
6
|
+
export const FILE_MCDATA_RC = '.mcdatarc.json';
|
|
7
|
+
export const FILE_MCDATA_AUTH = '.mcdata-auth.json';
|
|
8
|
+
|
|
9
|
+
export const WARN_MCDATA_SUPERSEDED =
|
|
10
|
+
'mcdata: Using .mcdevrc.json / .mcdev-auth.json; .mcdatarc.json / .mcdata-auth.json are ignored.';
|
|
11
|
+
|
|
4
12
|
/**
|
|
5
13
|
* @typedef {object} McdevrcCredentials
|
|
6
|
-
* @property {
|
|
14
|
+
* @property {number} [eid]
|
|
15
|
+
* @property {Record<string, number|string>} businessUnits
|
|
7
16
|
*/
|
|
8
17
|
|
|
9
18
|
/**
|
|
@@ -16,24 +25,74 @@ import path from 'node:path';
|
|
|
16
25
|
* @property {string} client_id
|
|
17
26
|
* @property {string} client_secret
|
|
18
27
|
* @property {string} auth_url
|
|
28
|
+
* @property {number} [account_id]
|
|
19
29
|
*/
|
|
20
30
|
|
|
21
31
|
/**
|
|
32
|
+
* Loads project config from either mcdev or mcdata file pairs, applying mcdev-wins precedence.
|
|
33
|
+
* If both mcdev files exist they are used and mcdata files (if also present) are ignored with a warning.
|
|
34
|
+
* If only the mcdata pair exists it is used.
|
|
35
|
+
* Partial pairs (one file without the other) or no files at all throw descriptive errors.
|
|
36
|
+
*
|
|
22
37
|
* @param {string} projectRoot
|
|
38
|
+
* @param {{ stderr?: (msg: string) => void }} [options]
|
|
23
39
|
* @returns {{ mcdevrc: Mcdevrc, mcdevAuth: Record<string, AuthCredential> }}
|
|
24
40
|
*/
|
|
25
|
-
export function
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
41
|
+
export function loadProjectConfig(projectRoot, options = {}) {
|
|
42
|
+
const err = options.stderr ?? ((msg) => console.error(msg));
|
|
43
|
+
const rcMcdev = path.join(projectRoot, FILE_MCDEV_RC);
|
|
44
|
+
const authMcdev = path.join(projectRoot, FILE_MCDEV_AUTH);
|
|
45
|
+
const rcMcdata = path.join(projectRoot, FILE_MCDATA_RC);
|
|
46
|
+
const authMcdata = path.join(projectRoot, FILE_MCDATA_AUTH);
|
|
47
|
+
|
|
48
|
+
const hasMcdevRc = fs.existsSync(rcMcdev);
|
|
49
|
+
const hasMcdevAuth = fs.existsSync(authMcdev);
|
|
50
|
+
const hasMcdataRc = fs.existsSync(rcMcdata);
|
|
51
|
+
const hasMcdataAuth = fs.existsSync(authMcdata);
|
|
52
|
+
|
|
53
|
+
const mcdevPairComplete = hasMcdevRc && hasMcdevAuth;
|
|
54
|
+
const mcdataPairComplete = hasMcdataRc && hasMcdataAuth;
|
|
55
|
+
|
|
56
|
+
if (mcdevPairComplete) {
|
|
57
|
+
if (hasMcdataRc || hasMcdataAuth) {
|
|
58
|
+
err(WARN_MCDATA_SUPERSEDED);
|
|
59
|
+
}
|
|
60
|
+
const mcdevrc = JSON.parse(fs.readFileSync(rcMcdev, 'utf8'));
|
|
61
|
+
const mcdevAuth = JSON.parse(fs.readFileSync(authMcdev, 'utf8'));
|
|
62
|
+
return { mcdevrc, mcdevAuth };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (hasMcdevRc !== hasMcdevAuth) {
|
|
66
|
+
if (hasMcdevRc && !hasMcdevAuth) {
|
|
67
|
+
throw new Error(`Missing ${authMcdev} (pair with ${FILE_MCDEV_RC})`);
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`Missing ${rcMcdev} (pair with ${FILE_MCDEV_AUTH})`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (mcdataPairComplete) {
|
|
73
|
+
const mcdevrc = JSON.parse(fs.readFileSync(rcMcdata, 'utf8'));
|
|
74
|
+
const mcdevAuth = JSON.parse(fs.readFileSync(authMcdata, 'utf8'));
|
|
75
|
+
return { mcdevrc, mcdevAuth };
|
|
30
76
|
}
|
|
31
|
-
|
|
32
|
-
|
|
77
|
+
|
|
78
|
+
if (hasMcdataRc !== hasMcdataAuth) {
|
|
79
|
+
if (hasMcdataRc && !hasMcdataAuth) {
|
|
80
|
+
throw new Error(`Missing ${authMcdata} (pair with ${FILE_MCDATA_RC})`);
|
|
81
|
+
}
|
|
82
|
+
throw new Error(`Missing ${rcMcdata} (pair with ${FILE_MCDATA_AUTH})`);
|
|
33
83
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
84
|
+
|
|
85
|
+
throw new Error(
|
|
86
|
+
`No project config found in ${projectRoot}. Add ${FILE_MCDEV_RC} + ${FILE_MCDEV_AUTH} (mcdev), or ${FILE_MCDATA_RC} + ${FILE_MCDATA_AUTH} from \`mcdata init\`. Install mcdev globally (\`npm i -g mcdev\`) if you want a full mcdev project.`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {string} projectRoot
|
|
92
|
+
* @returns {{ mcdevrc: Mcdevrc, mcdevAuth: Record<string, AuthCredential> }}
|
|
93
|
+
*/
|
|
94
|
+
export function loadMcdevProject(projectRoot) {
|
|
95
|
+
return loadProjectConfig(projectRoot);
|
|
37
96
|
}
|
|
38
97
|
|
|
39
98
|
/**
|
|
@@ -61,7 +120,7 @@ export function parseCredBu(credBu) {
|
|
|
61
120
|
export function resolveCredentialAndMid(mcdevrc, mcdevAuth, credentialName, buName) {
|
|
62
121
|
const credBlock = mcdevrc.credentials?.[credentialName];
|
|
63
122
|
if (!credBlock) {
|
|
64
|
-
throw new Error(`Unknown credential "${credentialName}" in
|
|
123
|
+
throw new Error(`Unknown credential "${credentialName}" in project credentials config`);
|
|
65
124
|
}
|
|
66
125
|
const midRaw = credBlock.businessUnits?.[buName];
|
|
67
126
|
if (midRaw === undefined || midRaw === null) {
|
|
@@ -74,7 +133,7 @@ export function resolveCredentialAndMid(mcdevrc, mcdevAuth, credentialName, buNa
|
|
|
74
133
|
const authCred = mcdevAuth[credentialName];
|
|
75
134
|
if (!authCred?.client_id || !authCred?.client_secret || !authCred?.auth_url) {
|
|
76
135
|
throw new Error(
|
|
77
|
-
`Missing auth fields for credential "${credentialName}" in
|
|
136
|
+
`Missing auth fields for credential "${credentialName}" in auth config file`,
|
|
78
137
|
);
|
|
79
138
|
}
|
|
80
139
|
return { mid, authCred };
|
package/lib/cross-bu-import.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import { pollAsyncImportCompletion } from './async-status.mjs';
|
|
|
16
16
|
import { readRowsFromFile } from './read-rows.mjs';
|
|
17
17
|
import { clearDataExtensionRows } from './clear-de.mjs';
|
|
18
18
|
import { confirmClearBeforeImport } from './confirm-clear.mjs';
|
|
19
|
-
import { dataDirectoryForBu
|
|
19
|
+
import { dataDirectoryForBu } from './paths.mjs';
|
|
20
20
|
import { buildExportBasename, filesystemSafeTimestamp, parseExportBasename } from './filename.mjs';
|
|
21
21
|
import { getDeRowCount } from './row-count.mjs';
|
|
22
22
|
|
|
@@ -164,8 +164,7 @@ export async function crossBuImport(params) {
|
|
|
164
164
|
format,
|
|
165
165
|
useGit: false,
|
|
166
166
|
});
|
|
167
|
-
|
|
168
|
-
console.error(`Backup export: ${rel} (${rowCount} rows)`);
|
|
167
|
+
console.error(`Backup export: "${path.resolve(outPath)}" (${rowCount} rows)`);
|
|
169
168
|
}
|
|
170
169
|
}
|
|
171
170
|
}
|
|
@@ -243,8 +242,7 @@ export async function crossBuImport(params) {
|
|
|
243
242
|
serializeRows(rows, format, false, snapshotColumns),
|
|
244
243
|
'utf8',
|
|
245
244
|
);
|
|
246
|
-
|
|
247
|
-
console.error(`Download stored: ${snapRel} (${rows.length} rows)`);
|
|
245
|
+
console.error(`Download stored: "${path.resolve(snapshotPath)}" (${rows.length} rows)`);
|
|
248
246
|
|
|
249
247
|
const { count: imported, requestIds } = await importRowsForDe(tgtSdk, {
|
|
250
248
|
deKey,
|
package/lib/import-de.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RestError } from 'sfmc-sdk/util';
|
|
1
2
|
import { chunkItemsForPayload } from './batch.mjs';
|
|
2
3
|
import { formatFromExtension } from './file-resolve.mjs';
|
|
3
4
|
import { resolveImportRoute } from './import-routes.mjs';
|
|
@@ -20,9 +21,27 @@ export async function importRowsForDe(sdk, params) {
|
|
|
20
21
|
for (const chunk of chunks) {
|
|
21
22
|
const p = route.path(deKey);
|
|
22
23
|
const body = { items: chunk };
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
let resp;
|
|
25
|
+
try {
|
|
26
|
+
resp = await withRetry429(() =>
|
|
27
|
+
route.method === 'PUT' ? sdk.rest.put(p, body) : sdk.rest.post(p, body),
|
|
28
|
+
);
|
|
29
|
+
} catch (ex) {
|
|
30
|
+
const msgs =
|
|
31
|
+
ex instanceof RestError &&
|
|
32
|
+
ex.response?.status === 400 &&
|
|
33
|
+
Array.isArray(ex.response?.data?.resultMessages) &&
|
|
34
|
+
ex.response.data.resultMessages.length > 0
|
|
35
|
+
? ex.response.data.resultMessages
|
|
36
|
+
: null;
|
|
37
|
+
if (msgs) {
|
|
38
|
+
const summary = msgs.map((m) => m.message ?? String(m)).join('; ');
|
|
39
|
+
throw new Error(`Import failed for DE "${deKey}" (HTTP 400): ${summary}`, {
|
|
40
|
+
cause: ex,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
throw ex;
|
|
44
|
+
}
|
|
26
45
|
requestIds.push(resp?.requestId ?? null);
|
|
27
46
|
}
|
|
28
47
|
return { count: rows.length, requestIds };
|
package/lib/index.mjs
CHANGED
|
@@ -15,6 +15,12 @@ export {
|
|
|
15
15
|
export { pollAsyncImportCompletion } from './async-status.mjs';
|
|
16
16
|
export {
|
|
17
17
|
loadMcdevProject,
|
|
18
|
+
loadProjectConfig,
|
|
19
|
+
WARN_MCDATA_SUPERSEDED,
|
|
20
|
+
FILE_MCDEV_RC,
|
|
21
|
+
FILE_MCDEV_AUTH,
|
|
22
|
+
FILE_MCDATA_RC,
|
|
23
|
+
FILE_MCDATA_AUTH,
|
|
18
24
|
parseCredBu,
|
|
19
25
|
resolveCredentialAndMid,
|
|
20
26
|
buildSdkAuthObject,
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import readline from 'node:readline/promises';
|
|
4
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
5
|
+
import { FILE_MCDEV_RC, FILE_MCDEV_AUTH, FILE_MCDATA_RC, FILE_MCDATA_AUTH } from './config.mjs';
|
|
6
|
+
import { fetchBusinessUnits } from './business-units.mjs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {object} InitOptions
|
|
10
|
+
* @property {string} projectRoot - Absolute path to the project folder
|
|
11
|
+
* @property {boolean} isTTY - Whether stdin is a TTY (interactive)
|
|
12
|
+
* @property {string} [credential] - Non-interactive: credential name
|
|
13
|
+
* @property {string} [clientId] - Non-interactive: client_id
|
|
14
|
+
* @property {string} [clientSecret] - Non-interactive: client_secret
|
|
15
|
+
* @property {string} [authUrl] - Non-interactive: auth_url
|
|
16
|
+
* @property {string} [enterpriseId] - Non-interactive: enterprise MID (string, will be parsed to int)
|
|
17
|
+
* @property {boolean} [yes] - Skip overwrite confirmation
|
|
18
|
+
* @property {Function} [_buFetcher] - Dependency injection for testing (replaces fetchBusinessUnits)
|
|
19
|
+
* @property {(question: string) => Promise.<boolean>} [_confirm] - Dependency injection for testing (replaces defaultConfirm)
|
|
20
|
+
* @property {(msg: string) => void} [stdout] - Override stdout (for testing)
|
|
21
|
+
* @property {(msg: string) => void} [stderr] - Override stderr (for testing)
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Ask a yes/no question on the terminal and return true only if the user types "y" or "yes".
|
|
26
|
+
*
|
|
27
|
+
* @param {string} question
|
|
28
|
+
* @returns {Promise.<boolean>}
|
|
29
|
+
*/
|
|
30
|
+
async function defaultConfirm(question) {
|
|
31
|
+
const rl = readline.createInterface({ input, output });
|
|
32
|
+
try {
|
|
33
|
+
const answer = (await rl.question(question)).trim().toLowerCase();
|
|
34
|
+
return answer === 'y' || answer === 'yes';
|
|
35
|
+
} finally {
|
|
36
|
+
rl.close();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Ensure .mcdata-auth.json is listed in the project's .gitignore (creates the file if absent).
|
|
42
|
+
*
|
|
43
|
+
* @param {string} projectRoot
|
|
44
|
+
*/
|
|
45
|
+
function ensureGitignore(projectRoot) {
|
|
46
|
+
const gitignorePath = path.join(projectRoot, '.gitignore');
|
|
47
|
+
const entry = '.mcdata-auth.json';
|
|
48
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
49
|
+
fs.writeFileSync(gitignorePath, `${entry}\n`, 'utf8');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const current = fs.readFileSync(gitignorePath, 'utf8');
|
|
53
|
+
const lines = current.split('\n');
|
|
54
|
+
if (!lines.some((line) => line.trim() === entry)) {
|
|
55
|
+
const appended = current.endsWith('\n')
|
|
56
|
+
? current + entry + '\n'
|
|
57
|
+
: current + '\n' + entry + '\n';
|
|
58
|
+
fs.writeFileSync(gitignorePath, appended, 'utf8');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Run the `mcdata init` flow — interactive or non-interactive.
|
|
64
|
+
*
|
|
65
|
+
* @param {InitOptions} opts
|
|
66
|
+
* @returns {Promise.<number>} exit code (0 = success, 1 = failure)
|
|
67
|
+
*/
|
|
68
|
+
export async function runMcdataInit(opts) {
|
|
69
|
+
const { projectRoot, isTTY, yes = false, _buFetcher } = opts;
|
|
70
|
+
const out = opts.stdout ?? ((msg) => console.log(msg));
|
|
71
|
+
const err = opts.stderr ?? ((msg) => console.error(msg));
|
|
72
|
+
|
|
73
|
+
// Guard: do not init over an existing mcdev project (both files must be present)
|
|
74
|
+
if (
|
|
75
|
+
fs.existsSync(path.join(projectRoot, FILE_MCDEV_RC)) &&
|
|
76
|
+
fs.existsSync(path.join(projectRoot, FILE_MCDEV_AUTH))
|
|
77
|
+
) {
|
|
78
|
+
err(
|
|
79
|
+
`This project is managed by mcdev (${FILE_MCDEV_RC} and ${FILE_MCDEV_AUTH} found).\n` +
|
|
80
|
+
`Manage your credentials by editing ${FILE_MCDEV_AUTH} directly, or run 'mcdev init' to re-initialise.`,
|
|
81
|
+
);
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Guard: confirm overwrite when mcdata files already present
|
|
86
|
+
const mcdataRcPath = path.join(projectRoot, FILE_MCDATA_RC);
|
|
87
|
+
const mcdataAuthPath = path.join(projectRoot, FILE_MCDATA_AUTH);
|
|
88
|
+
if (!yes && (fs.existsSync(mcdataRcPath) || fs.existsSync(mcdataAuthPath))) {
|
|
89
|
+
if (isTTY) {
|
|
90
|
+
const confirmFn = opts._confirm ?? defaultConfirm;
|
|
91
|
+
const confirmed = await confirmFn(
|
|
92
|
+
`${FILE_MCDATA_RC} or ${FILE_MCDATA_AUTH} already exists. Override existing configuration? [y/N] `,
|
|
93
|
+
);
|
|
94
|
+
if (!confirmed) {
|
|
95
|
+
out('Aborted.');
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
err(
|
|
100
|
+
`${FILE_MCDATA_RC} or ${FILE_MCDATA_AUTH} already exists in ${projectRoot}.\n` +
|
|
101
|
+
`Pass --yes to overwrite.`,
|
|
102
|
+
);
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Collect credentials
|
|
108
|
+
let credentialName = opts.credential;
|
|
109
|
+
let clientId = opts.clientId;
|
|
110
|
+
let clientSecret = opts.clientSecret;
|
|
111
|
+
let authUrl = opts.authUrl;
|
|
112
|
+
let enterpriseIdStr = opts.enterpriseId;
|
|
113
|
+
|
|
114
|
+
if (isTTY && (!credentialName || !clientId || !clientSecret || !authUrl || !enterpriseIdStr)) {
|
|
115
|
+
const rl = readline.createInterface({ input, output });
|
|
116
|
+
try {
|
|
117
|
+
if (!credentialName) {
|
|
118
|
+
credentialName = (await rl.question('Credential name (e.g. MyOrg): ')).trim();
|
|
119
|
+
}
|
|
120
|
+
if (!clientId) {
|
|
121
|
+
clientId = (await rl.question('Client ID: ')).trim();
|
|
122
|
+
}
|
|
123
|
+
if (!clientSecret) {
|
|
124
|
+
clientSecret = (await rl.question('Client Secret: ')).trim();
|
|
125
|
+
}
|
|
126
|
+
if (!authUrl) {
|
|
127
|
+
authUrl = (
|
|
128
|
+
await rl.question(
|
|
129
|
+
'Auth URL (e.g. https://<tenantsubdomain>.auth.marketingcloudapis.com/): ',
|
|
130
|
+
)
|
|
131
|
+
).trim();
|
|
132
|
+
}
|
|
133
|
+
if (!enterpriseIdStr) {
|
|
134
|
+
enterpriseIdStr = (await rl.question('Enterprise MID: ')).trim();
|
|
135
|
+
}
|
|
136
|
+
} finally {
|
|
137
|
+
rl.close();
|
|
138
|
+
}
|
|
139
|
+
} else if (!isTTY) {
|
|
140
|
+
// Non-interactive: all flags must be provided
|
|
141
|
+
const missing = [];
|
|
142
|
+
if (!credentialName) {
|
|
143
|
+
missing.push('--credential');
|
|
144
|
+
}
|
|
145
|
+
if (!clientId) {
|
|
146
|
+
missing.push('--client-id');
|
|
147
|
+
}
|
|
148
|
+
if (!clientSecret) {
|
|
149
|
+
missing.push('--client-secret');
|
|
150
|
+
}
|
|
151
|
+
if (!authUrl) {
|
|
152
|
+
missing.push('--auth-url');
|
|
153
|
+
}
|
|
154
|
+
if (!enterpriseIdStr) {
|
|
155
|
+
missing.push('--enterprise-id');
|
|
156
|
+
}
|
|
157
|
+
if (missing.length > 0) {
|
|
158
|
+
err(
|
|
159
|
+
`mcdata init: missing required flags in non-interactive mode: ${missing.join(', ')}`,
|
|
160
|
+
);
|
|
161
|
+
return 1;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const enterpriseId = Number.parseInt(String(enterpriseIdStr), 10);
|
|
166
|
+
if (!Number.isInteger(enterpriseId) || enterpriseId <= 0) {
|
|
167
|
+
err(`Invalid enterprise MID: ${enterpriseIdStr}`);
|
|
168
|
+
return 1;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Fetch Business Units
|
|
172
|
+
out('Fetching Business Units from Marketing Cloud...');
|
|
173
|
+
let buResult;
|
|
174
|
+
try {
|
|
175
|
+
const buFetcher = _buFetcher ?? fetchBusinessUnits;
|
|
176
|
+
buResult = await buFetcher(
|
|
177
|
+
{ client_id: clientId, client_secret: clientSecret, auth_url: authUrl },
|
|
178
|
+
enterpriseId,
|
|
179
|
+
);
|
|
180
|
+
} catch (ex) {
|
|
181
|
+
err(`Failed to fetch Business Units: ${ex.message}`);
|
|
182
|
+
return 1;
|
|
183
|
+
}
|
|
184
|
+
out(`Found ${Object.keys(buResult.businessUnits).length} Business Unit(s).`);
|
|
185
|
+
|
|
186
|
+
// Build config objects
|
|
187
|
+
const mcdataRc = {
|
|
188
|
+
credentials: {
|
|
189
|
+
[credentialName]: {
|
|
190
|
+
eid: buResult.eid,
|
|
191
|
+
businessUnits: buResult.businessUnits,
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const mcdataAuth = {
|
|
197
|
+
[credentialName]: {
|
|
198
|
+
client_id: clientId,
|
|
199
|
+
client_secret: clientSecret,
|
|
200
|
+
auth_url: authUrl,
|
|
201
|
+
account_id: buResult.eid,
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// Write files
|
|
206
|
+
fs.writeFileSync(mcdataRcPath, JSON.stringify(mcdataRc, null, 4) + '\n', 'utf8');
|
|
207
|
+
fs.writeFileSync(mcdataAuthPath, JSON.stringify(mcdataAuth, null, 4) + '\n', 'utf8');
|
|
208
|
+
ensureGitignore(projectRoot);
|
|
209
|
+
|
|
210
|
+
out(`Created ${FILE_MCDATA_RC} and ${FILE_MCDATA_AUTH} in ${projectRoot}`);
|
|
211
|
+
out('Make sure to add .mcdata-auth.json to your .gitignore (done automatically).');
|
|
212
|
+
out("Tip: To use mcdev instead, install it globally and run 'mcdev init'.");
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
package/lib/multi-bu-export.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
1
2
|
import SDK from 'sfmc-sdk';
|
|
2
3
|
import { resolveCredentialAndMid, buildSdkAuthObject, buildSdkOptions } from './config.mjs';
|
|
3
4
|
import { exportDataExtensionToFile } from './export-de.mjs';
|
|
4
|
-
import { projectRelativePosix } from './paths.mjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @typedef {{ credential: string, bu: string }} CredBuSource
|
|
@@ -50,8 +50,7 @@ export async function multiBuExport({
|
|
|
50
50
|
jsonPretty,
|
|
51
51
|
useGit,
|
|
52
52
|
});
|
|
53
|
-
|
|
54
|
-
console.error(`Exported: ${rel} (${rowCount} rows)`);
|
|
53
|
+
console.error(`Exported: "${path.resolve(outPath)}" (${rowCount} rows)`);
|
|
55
54
|
exported.push(outPath);
|
|
56
55
|
}
|
|
57
56
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sfmc-dataloader",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "SFMC Data Loader CLI (mcdata)
|
|
3
|
+
"version": "2.5.0",
|
|
4
|
+
"description": "SFMC Data Loader CLI (mcdata) — standalone export/import of Marketing Cloud Data Extension rows; optional mcdev integration",
|
|
5
5
|
"author": "Jörn Berkefeld <joern.berkefeld@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -35,9 +35,6 @@
|
|
|
35
35
|
"engines": {
|
|
36
36
|
"node": "^20.19.0 || ^22.13.0 || >=24"
|
|
37
37
|
},
|
|
38
|
-
"peerDependencies": {
|
|
39
|
-
"mcdev": ">=7"
|
|
40
|
-
},
|
|
41
38
|
"dependencies": {
|
|
42
39
|
"csv-parser": "3.2.0",
|
|
43
40
|
"csv-stringify": "6.7.0",
|
|
@@ -61,5 +58,8 @@
|
|
|
61
58
|
"globals": "^17.4.0",
|
|
62
59
|
"husky": "^9.1.7",
|
|
63
60
|
"prettier": "^3.8.1"
|
|
61
|
+
},
|
|
62
|
+
"optionalDependencies": {
|
|
63
|
+
"mcdev": ">=7"
|
|
64
64
|
}
|
|
65
65
|
}
|