sfmc-dataloader 2.6.1 → 2.7.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/lib/async-status.mjs +11 -10
- package/lib/batch.mjs +2 -2
- package/lib/cli.mjs +178 -86
- package/lib/config.mjs +15 -9
- package/lib/cross-bu-import.mjs +133 -61
- package/lib/export-de.mjs +196 -16
- package/lib/file-resolve.mjs +71 -0
- package/lib/filename.mjs +25 -5
- package/lib/import-de.mjs +160 -37
- package/lib/init-project.mjs +2 -1
- package/lib/log.mjs +56 -0
- package/lib/multi-bu-export.mjs +8 -3
- package/lib/read-rows.mjs +156 -27
- package/lib/row-count.mjs +2 -1
- package/package.json +2 -2
package/lib/async-status.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { asyncRequestResultsPath, asyncRequestStatusPath } from './import-routes.mjs';
|
|
2
|
+
import { log } from './log.mjs';
|
|
2
3
|
|
|
3
4
|
const POLL_INTERVAL_MS = 5000;
|
|
4
5
|
const PENDING_STATUSES = new Set(['Pending', 'Executing']);
|
|
@@ -25,11 +26,11 @@ export async function pollAsyncImportCompletion(sdk, requestIds) {
|
|
|
25
26
|
|
|
26
27
|
for (const requestId of requestIds) {
|
|
27
28
|
if (!requestId) {
|
|
28
|
-
|
|
29
|
+
log.error('Async import: no requestId returned for chunk — skipping status check.');
|
|
29
30
|
continue;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
log.info(`Waiting for async import to complete (requestId: ${requestId})…`);
|
|
33
34
|
await sleep(POLL_INTERVAL_MS);
|
|
34
35
|
|
|
35
36
|
let requestStatus;
|
|
@@ -38,7 +39,7 @@ export async function pollAsyncImportCompletion(sdk, requestIds) {
|
|
|
38
39
|
try {
|
|
39
40
|
statusResult = await sdk.rest.get(asyncRequestStatusPath(requestId));
|
|
40
41
|
} catch (ex) {
|
|
41
|
-
|
|
42
|
+
log.error(
|
|
42
43
|
`Async import: status check failed for requestId ${requestId}: ${ex.message}`,
|
|
43
44
|
);
|
|
44
45
|
break;
|
|
@@ -47,7 +48,7 @@ export async function pollAsyncImportCompletion(sdk, requestIds) {
|
|
|
47
48
|
requestStatus = statusResult?.status?.requestStatus;
|
|
48
49
|
|
|
49
50
|
if (PENDING_STATUSES.has(requestStatus)) {
|
|
50
|
-
|
|
51
|
+
log.info(
|
|
51
52
|
`Async import still in progress (requestId: ${requestId}) — retrying in 5 s…`,
|
|
52
53
|
);
|
|
53
54
|
await sleep(POLL_INTERVAL_MS);
|
|
@@ -55,16 +56,16 @@ export async function pollAsyncImportCompletion(sdk, requestIds) {
|
|
|
55
56
|
} while (PENDING_STATUSES.has(requestStatus));
|
|
56
57
|
|
|
57
58
|
if (requestStatus === 'Complete') {
|
|
58
|
-
|
|
59
|
+
log.info(`Async import completed successfully (requestId: ${requestId}).`);
|
|
59
60
|
} else if (requestStatus === 'Error') {
|
|
60
|
-
|
|
61
|
+
log.error(`Async import job failed (requestId: ${requestId}).`);
|
|
61
62
|
hasError = true;
|
|
62
63
|
|
|
63
64
|
let resultsResult;
|
|
64
65
|
try {
|
|
65
66
|
resultsResult = await sdk.rest.get(asyncRequestResultsPath(requestId));
|
|
66
67
|
} catch (ex) {
|
|
67
|
-
|
|
68
|
+
log.error(
|
|
68
69
|
`Async import: could not retrieve error details for requestId ${requestId}: ${ex.message}`,
|
|
69
70
|
);
|
|
70
71
|
continue;
|
|
@@ -72,14 +73,14 @@ export async function pollAsyncImportCompletion(sdk, requestIds) {
|
|
|
72
73
|
|
|
73
74
|
const items = resultsResult?.items ?? [];
|
|
74
75
|
if (items.length === 0) {
|
|
75
|
-
|
|
76
|
+
log.warn('Async import: no item-level error details returned by API.');
|
|
76
77
|
} else {
|
|
77
78
|
for (const item of items) {
|
|
78
|
-
|
|
79
|
+
log.error(`Import error: ${item.message}`);
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
} else if (requestStatus !== undefined) {
|
|
82
|
-
|
|
83
|
+
log.error(
|
|
83
84
|
`Async import: unexpected status "${requestStatus}" for requestId ${requestId}.`,
|
|
84
85
|
);
|
|
85
86
|
}
|
package/lib/batch.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/** Default max UTF-8 bytes per request body (Data API family; margin below 5.9 MB). */
|
|
2
2
|
export const DEFAULT_MAX_BODY_BYTES = 5_500_000;
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
export const MAX_OBJECTS_PER_BATCH =
|
|
4
|
+
/** Max rows per HTTP payload chunk (byte cap in `DEFAULT_MAX_BODY_BYTES` may split further). */
|
|
5
|
+
export const MAX_OBJECTS_PER_BATCH = 2_500;
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Split rows into chunks that respect both max row count and serialized JSON body size.
|
package/lib/cli.mjs
CHANGED
|
@@ -13,9 +13,14 @@ import {
|
|
|
13
13
|
} from './config.mjs';
|
|
14
14
|
import { dataDirectoryForBu } from './paths.mjs';
|
|
15
15
|
import { exportDataExtensionToFile } from './export-de.mjs';
|
|
16
|
-
import { findImportCandidates,
|
|
16
|
+
import { findImportCandidates, formatFromExtension, resolveImportSet } from './file-resolve.mjs';
|
|
17
17
|
import { parseExportBasename } from './filename.mjs';
|
|
18
|
-
import {
|
|
18
|
+
import { MAX_OBJECTS_PER_BATCH } from './batch.mjs';
|
|
19
|
+
import {
|
|
20
|
+
assertNonEmptyImportRowCount,
|
|
21
|
+
importRowsStreamingForDe,
|
|
22
|
+
warnIfImportCountUnexpected,
|
|
23
|
+
} from './import-de.mjs';
|
|
19
24
|
import { pollAsyncImportCompletion } from './async-status.mjs';
|
|
20
25
|
import { clearDataExtensionRows } from './clear-de.mjs';
|
|
21
26
|
import { confirmClearBeforeImport } from './confirm-clear.mjs';
|
|
@@ -24,6 +29,24 @@ import { multiBuExport } from './multi-bu-export.mjs';
|
|
|
24
29
|
import { crossBuImport } from './cross-bu-import.mjs';
|
|
25
30
|
import { initDebugLogger } from './debug-logger.mjs';
|
|
26
31
|
import { runMcdataInit } from './init-project.mjs';
|
|
32
|
+
import { countDataRowsFromImportPaths, streamRowsFromImportPaths } from './read-rows.mjs';
|
|
33
|
+
import { log, setDebugLogger, formatTime } from './log.mjs';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string|undefined} raw
|
|
37
|
+
* @param {string} flagName
|
|
38
|
+
* @returns {number|undefined}
|
|
39
|
+
*/
|
|
40
|
+
function parseOptionalPositiveInt(raw, flagName) {
|
|
41
|
+
if (raw === undefined || raw === '') {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const n = Number(raw);
|
|
45
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
46
|
+
throw new Error(`Invalid ${flagName}: ${raw} (expect positive integer)`);
|
|
47
|
+
}
|
|
48
|
+
return Math.floor(n);
|
|
49
|
+
}
|
|
27
50
|
|
|
28
51
|
/** @returns {string} semver from this package's package.json */
|
|
29
52
|
function readCliPackageVersion() {
|
|
@@ -53,6 +76,7 @@ Options:
|
|
|
53
76
|
--format <csv|tsv|json> Export file format (default: csv); ignored for imports
|
|
54
77
|
--json-pretty Pretty-print JSON on export
|
|
55
78
|
--git Stable filenames: <key>.mcdata.<ext> (no timestamp)
|
|
79
|
+
--max-rows-per-file <n> Split exports into multiple files after N data rows (optional)
|
|
56
80
|
--debug Write API requests/responses to ./logs/data/*.log
|
|
57
81
|
|
|
58
82
|
Init options:
|
|
@@ -82,7 +106,7 @@ Config files:
|
|
|
82
106
|
|
|
83
107
|
Notes:
|
|
84
108
|
Exports are written under ./data/<credential>/<bu>/ using ".mcdata." in the filename.
|
|
85
|
-
Import with --de resolves the latest matching
|
|
109
|
+
Import with --de resolves the latest matching export (csv/tsv/json), including multi-part partN files.
|
|
86
110
|
Import with --file parses the DE key from the basename (.mcdata. format).
|
|
87
111
|
Import format is auto-detected from file extension (.csv, .tsv, .json).
|
|
88
112
|
Cross-BU import stores a download file in each target BU's data directory.
|
|
@@ -128,12 +152,13 @@ export async function main(argv) {
|
|
|
128
152
|
'auth-url': { type: 'string' },
|
|
129
153
|
'enterprise-id': { type: 'string' },
|
|
130
154
|
yes: { type: 'boolean', short: 'y', default: false },
|
|
155
|
+
'max-rows-per-file': { type: 'string' },
|
|
131
156
|
},
|
|
132
157
|
});
|
|
133
158
|
values = parsed.values;
|
|
134
159
|
positionals = parsed.positionals;
|
|
135
160
|
} catch (ex) {
|
|
136
|
-
|
|
161
|
+
log.error(ex.message);
|
|
137
162
|
printHelp();
|
|
138
163
|
return 1;
|
|
139
164
|
}
|
|
@@ -172,7 +197,7 @@ export async function main(argv) {
|
|
|
172
197
|
|
|
173
198
|
const fmt = values.format ?? 'csv';
|
|
174
199
|
if (!['csv', 'tsv', 'json'].includes(fmt)) {
|
|
175
|
-
|
|
200
|
+
log.error(`Invalid --format: ${fmt}`);
|
|
176
201
|
return 1;
|
|
177
202
|
}
|
|
178
203
|
|
|
@@ -195,31 +220,33 @@ export async function main(argv) {
|
|
|
195
220
|
? initDebugLogger(projectRoot, readCliPackageVersion(), argv)
|
|
196
221
|
: null;
|
|
197
222
|
if (logger) {
|
|
198
|
-
|
|
223
|
+
setDebugLogger(logger);
|
|
224
|
+
// Written directly to stdout — must not appear inside the file it announces
|
|
225
|
+
console.log(`${formatTime()} info: Debug log: "${path.resolve(logger.logPath)}"`);
|
|
199
226
|
}
|
|
200
227
|
|
|
201
228
|
// ── export ──────────────────────────────────────────────────────────────
|
|
202
229
|
if (sub === 'export') {
|
|
203
230
|
if (hasTo) {
|
|
204
|
-
|
|
231
|
+
log.error('--to is not valid for export. Did you mean import?');
|
|
205
232
|
return 1;
|
|
206
233
|
}
|
|
207
234
|
|
|
208
235
|
const des = [values.de ?? []].flat();
|
|
209
236
|
if (des.length === 0) {
|
|
210
|
-
|
|
237
|
+
log.error('export requires at least one --de <customerKey>');
|
|
211
238
|
return 1;
|
|
212
239
|
}
|
|
213
240
|
|
|
214
241
|
if (hasFrom && hasPositional) {
|
|
215
|
-
|
|
242
|
+
log.error(
|
|
216
243
|
'Cannot mix a positional <credential>/<bu> with --from. Use one or the other.',
|
|
217
244
|
);
|
|
218
245
|
return 1;
|
|
219
246
|
}
|
|
220
247
|
|
|
221
248
|
if (!hasFrom && !hasPositional) {
|
|
222
|
-
|
|
249
|
+
log.error(
|
|
223
250
|
'export requires either a positional <credential>/<bu> or at least one --from <cred>/<bu>.',
|
|
224
251
|
);
|
|
225
252
|
printHelp();
|
|
@@ -231,10 +258,20 @@ export async function main(argv) {
|
|
|
231
258
|
try {
|
|
232
259
|
sources = fromFlags.map(parseCredBu);
|
|
233
260
|
} catch (ex) {
|
|
234
|
-
|
|
261
|
+
log.error(ex.message);
|
|
235
262
|
return 1;
|
|
236
263
|
}
|
|
237
264
|
const { mcdevrc, mcdevAuth } = loadProjectConfig(projectRoot);
|
|
265
|
+
let maxRowsPerFile;
|
|
266
|
+
try {
|
|
267
|
+
maxRowsPerFile = parseOptionalPositiveInt(
|
|
268
|
+
values['max-rows-per-file'],
|
|
269
|
+
'--max-rows-per-file',
|
|
270
|
+
);
|
|
271
|
+
} catch (ex) {
|
|
272
|
+
log.error(ex.message);
|
|
273
|
+
return 1;
|
|
274
|
+
}
|
|
238
275
|
await multiBuExport({
|
|
239
276
|
projectRoot,
|
|
240
277
|
mcdevrc,
|
|
@@ -244,17 +281,29 @@ export async function main(argv) {
|
|
|
244
281
|
format: /** @type {'csv'|'tsv'|'json'} */ (fmt),
|
|
245
282
|
jsonPretty: values['json-pretty'],
|
|
246
283
|
useGit,
|
|
284
|
+
maxRowsPerFile,
|
|
247
285
|
logger,
|
|
248
286
|
});
|
|
249
287
|
return 0;
|
|
250
288
|
}
|
|
251
289
|
|
|
290
|
+
let maxRowsPerFile;
|
|
291
|
+
try {
|
|
292
|
+
maxRowsPerFile = parseOptionalPositiveInt(
|
|
293
|
+
values['max-rows-per-file'],
|
|
294
|
+
'--max-rows-per-file',
|
|
295
|
+
);
|
|
296
|
+
} catch (ex) {
|
|
297
|
+
log.error(ex.message);
|
|
298
|
+
return 1;
|
|
299
|
+
}
|
|
300
|
+
|
|
252
301
|
const { credential, bu } = parseCredBu(credBuRaw);
|
|
253
302
|
const { mcdevrc, mcdevAuth } = loadProjectConfig(projectRoot);
|
|
254
303
|
const { mid, authCred } = resolveCredentialAndMid(mcdevrc, mcdevAuth, credential, bu);
|
|
255
304
|
const sdk = new SDK(buildSdkAuthObject(authCred, mid), buildSdkOptions(logger));
|
|
256
305
|
for (const deKey of des) {
|
|
257
|
-
const {
|
|
306
|
+
const { paths: outPaths, rowCount } = await exportDataExtensionToFile(sdk, {
|
|
258
307
|
projectRoot,
|
|
259
308
|
credentialName: credential,
|
|
260
309
|
buName: bu,
|
|
@@ -262,8 +311,10 @@ export async function main(argv) {
|
|
|
262
311
|
format: /** @type {'csv'|'tsv'|'json'} */ (fmt),
|
|
263
312
|
jsonPretty: values['json-pretty'],
|
|
264
313
|
useGit,
|
|
314
|
+
maxRowsPerFile,
|
|
265
315
|
});
|
|
266
|
-
|
|
316
|
+
const label = outPaths.map((p) => `"${path.resolve(p)}"`).join(', ');
|
|
317
|
+
log.info(`Exported: ${label} (${rowCount} rows)`);
|
|
267
318
|
}
|
|
268
319
|
return 0;
|
|
269
320
|
}
|
|
@@ -272,7 +323,7 @@ export async function main(argv) {
|
|
|
272
323
|
if (sub === 'import') {
|
|
273
324
|
const mode = values.mode ?? 'upsert';
|
|
274
325
|
if (!['upsert', 'insert'].includes(mode)) {
|
|
275
|
-
|
|
326
|
+
log.error(`Invalid --mode: ${mode} (use upsert or insert)`);
|
|
276
327
|
return 1;
|
|
277
328
|
}
|
|
278
329
|
|
|
@@ -282,15 +333,13 @@ export async function main(argv) {
|
|
|
282
333
|
// ── File-to-multi-BU import: --to + --file (no --from) ─────────────
|
|
283
334
|
if (hasTo && !hasFrom && values.file?.length > 0) {
|
|
284
335
|
if (hasPositional) {
|
|
285
|
-
|
|
336
|
+
log.error(
|
|
286
337
|
'Cannot mix a positional <credential>/<bu> with --to/--file. Use one or the other.',
|
|
287
338
|
);
|
|
288
339
|
return 1;
|
|
289
340
|
}
|
|
290
341
|
if (values.de?.length > 0) {
|
|
291
|
-
|
|
292
|
-
'Cannot mix --de with --file in multi-target import. Use --file only.',
|
|
293
|
-
);
|
|
342
|
+
log.error('Cannot mix --de with --file in multi-target import. Use --file only.');
|
|
294
343
|
return 1;
|
|
295
344
|
}
|
|
296
345
|
const filePaths = values.file;
|
|
@@ -298,7 +347,7 @@ export async function main(argv) {
|
|
|
298
347
|
try {
|
|
299
348
|
targets = toFlags.map(parseCredBu);
|
|
300
349
|
} catch (ex) {
|
|
301
|
-
|
|
350
|
+
log.error(ex.message);
|
|
302
351
|
return 1;
|
|
303
352
|
}
|
|
304
353
|
const { mcdevrc, mcdevAuth } = loadProjectConfig(projectRoot);
|
|
@@ -323,38 +372,36 @@ export async function main(argv) {
|
|
|
323
372
|
// ── Cross-BU import (API mode): --from + --to + --de ────────────────
|
|
324
373
|
if (hasFrom || hasTo) {
|
|
325
374
|
if (hasPositional) {
|
|
326
|
-
|
|
375
|
+
log.error(
|
|
327
376
|
'Cannot mix a positional <credential>/<bu> with --from/--to. Use one or the other.',
|
|
328
377
|
);
|
|
329
378
|
return 1;
|
|
330
379
|
}
|
|
331
380
|
if (!hasFrom) {
|
|
332
|
-
|
|
333
|
-
'--to requires --from <cred>/<bu> to specify the source Business Unit.',
|
|
334
|
-
);
|
|
381
|
+
log.error('--to requires --from <cred>/<bu> to specify the source Business Unit.');
|
|
335
382
|
return 1;
|
|
336
383
|
}
|
|
337
384
|
if (!hasTo) {
|
|
338
|
-
|
|
385
|
+
log.error(
|
|
339
386
|
'--from requires at least one --to <cred>/<bu> to specify target Business Unit(s).',
|
|
340
387
|
);
|
|
341
388
|
return 1;
|
|
342
389
|
}
|
|
343
390
|
if (fromFlags.length > 1) {
|
|
344
|
-
|
|
391
|
+
log.error(
|
|
345
392
|
'import accepts exactly one --from <cred>/<bu> (use multiple --to for multiple targets).',
|
|
346
393
|
);
|
|
347
394
|
return 1;
|
|
348
395
|
}
|
|
349
396
|
if (values.file?.length > 0) {
|
|
350
|
-
|
|
397
|
+
log.error(
|
|
351
398
|
'--file cannot be combined with --from/--to/--de. For file-based multi-target import use --to + --file (without --from).',
|
|
352
399
|
);
|
|
353
400
|
return 1;
|
|
354
401
|
}
|
|
355
402
|
const deKeys = [values.de ?? []].flat();
|
|
356
403
|
if (deKeys.length === 0) {
|
|
357
|
-
|
|
404
|
+
log.error('Cross-BU import requires at least one --de <customerKey>.');
|
|
358
405
|
return 1;
|
|
359
406
|
}
|
|
360
407
|
let sourceParsed;
|
|
@@ -363,7 +410,7 @@ export async function main(argv) {
|
|
|
363
410
|
sourceParsed = parseCredBu(fromFlags[0]);
|
|
364
411
|
targets = toFlags.map(parseCredBu);
|
|
365
412
|
} catch (ex) {
|
|
366
|
-
|
|
413
|
+
log.error(ex.message);
|
|
367
414
|
return 1;
|
|
368
415
|
}
|
|
369
416
|
const { mcdevrc, mcdevAuth } = loadProjectConfig(projectRoot);
|
|
@@ -389,7 +436,7 @@ export async function main(argv) {
|
|
|
389
436
|
|
|
390
437
|
// ── Single-BU import (original behavior) ────────────────────────────
|
|
391
438
|
if (!hasPositional) {
|
|
392
|
-
|
|
439
|
+
log.error(
|
|
393
440
|
'import requires either a positional <credential>/<bu> or --from/--to flags.',
|
|
394
441
|
);
|
|
395
442
|
printHelp();
|
|
@@ -399,7 +446,7 @@ export async function main(argv) {
|
|
|
399
446
|
const hasDe = values.de?.length > 0;
|
|
400
447
|
const hasFile = values.file?.length > 0;
|
|
401
448
|
if (hasDe === hasFile) {
|
|
402
|
-
|
|
449
|
+
log.error(
|
|
403
450
|
'import requires exactly one of: repeated --de <key> OR repeated --file <path>',
|
|
404
451
|
);
|
|
405
452
|
return 1;
|
|
@@ -415,7 +462,7 @@ export async function main(argv) {
|
|
|
415
462
|
const dataDir = dataDirectoryForBu(projectRoot, credential, bu);
|
|
416
463
|
if (backupBeforeImport === true) {
|
|
417
464
|
for (const deKey of deKeys) {
|
|
418
|
-
const {
|
|
465
|
+
const { paths: outPaths, rowCount } = await exportDataExtensionToFile(sdk, {
|
|
419
466
|
projectRoot,
|
|
420
467
|
credentialName: credential,
|
|
421
468
|
buName: bu,
|
|
@@ -423,7 +470,8 @@ export async function main(argv) {
|
|
|
423
470
|
format: /** @type {'csv'|'tsv'|'json'} */ (fmt),
|
|
424
471
|
useGit: false,
|
|
425
472
|
});
|
|
426
|
-
|
|
473
|
+
const label = outPaths.map((p) => `"${path.resolve(p)}"`).join(', ');
|
|
474
|
+
log.info(`Backup export: ${label} (${rowCount} rows)`);
|
|
427
475
|
}
|
|
428
476
|
}
|
|
429
477
|
if (clear) {
|
|
@@ -436,37 +484,49 @@ export async function main(argv) {
|
|
|
436
484
|
let anyError = false;
|
|
437
485
|
for (const deKey of deKeys) {
|
|
438
486
|
const candidates = await findImportCandidates(dataDir, deKey);
|
|
439
|
-
|
|
440
|
-
|
|
487
|
+
const { paths: importPaths } = await resolveImportSet(candidates);
|
|
488
|
+
if (importPaths.length === 0) {
|
|
489
|
+
log.error(
|
|
441
490
|
`No import file (csv/tsv/json) found for DE "${deKey}" under "${path.resolve(dataDir)}"`,
|
|
442
491
|
);
|
|
443
492
|
return 1;
|
|
444
493
|
}
|
|
445
|
-
const
|
|
446
|
-
|
|
494
|
+
const detectedFormat = formatFromExtension(importPaths[0]);
|
|
495
|
+
if (!detectedFormat) {
|
|
496
|
+
log.error(
|
|
497
|
+
`Cannot determine format for "${importPaths[0]}". Use .csv, .tsv, or .json.`,
|
|
498
|
+
);
|
|
499
|
+
return 1;
|
|
500
|
+
}
|
|
447
501
|
|
|
448
502
|
const countBefore = await getDeRowCount(sdk, deKey);
|
|
449
|
-
|
|
503
|
+
log.info(
|
|
450
504
|
`Row count before import: ${countBefore ?? '(unavailable)'} (DE "${deKey}")`,
|
|
451
505
|
);
|
|
452
506
|
|
|
507
|
+
let clearedSingleDe = false;
|
|
453
508
|
if (clear) {
|
|
454
509
|
if (countBefore === 0) {
|
|
455
|
-
|
|
456
|
-
`Skipping clear-data for DE "${deKey}" — DE is already empty.`,
|
|
457
|
-
);
|
|
510
|
+
log.info(`Skipping clear-data for DE "${deKey}" — DE is already empty.`);
|
|
458
511
|
} else {
|
|
459
512
|
await clearDataExtensionRows(sdk.soap, deKey);
|
|
460
|
-
|
|
513
|
+
clearedSingleDe = true;
|
|
514
|
+
log.warn(`Cleared data: DE "${deKey}"`);
|
|
461
515
|
}
|
|
462
516
|
}
|
|
463
517
|
|
|
464
|
-
const
|
|
465
|
-
|
|
518
|
+
const rowCount = await countDataRowsFromImportPaths(importPaths, detectedFormat);
|
|
519
|
+
assertNonEmptyImportRowCount(rowCount, importPaths.join(', '));
|
|
520
|
+
const totalMemoryBatches = Math.max(1, Math.ceil(rowCount / MAX_OBJECTS_PER_BATCH));
|
|
521
|
+
const rowSource = streamRowsFromImportPaths(importPaths, detectedFormat);
|
|
522
|
+
const { count: n, requestIds } = await importRowsStreamingForDe(sdk, {
|
|
466
523
|
deKey,
|
|
524
|
+
rowSource,
|
|
467
525
|
mode: /** @type {'upsert'|'insert'} */ (mode),
|
|
526
|
+
totalMemoryBatches,
|
|
468
527
|
});
|
|
469
|
-
|
|
528
|
+
const srcLabel = importPaths.map((p) => `"${path.resolve(p)}"`).join(', ');
|
|
529
|
+
log.info(`Imported: ${srcLabel} (${n} rows) -> DE ${deKey}`);
|
|
470
530
|
|
|
471
531
|
const importHadError = await pollAsyncImportCompletion(sdk, requestIds);
|
|
472
532
|
if (importHadError) {
|
|
@@ -474,32 +534,35 @@ export async function main(argv) {
|
|
|
474
534
|
}
|
|
475
535
|
|
|
476
536
|
const countAfter = await getDeRowCount(sdk, deKey);
|
|
477
|
-
|
|
537
|
+
log.info(
|
|
478
538
|
`Row count after import: ${countAfter ?? '(unavailable)'} (DE "${deKey}")`,
|
|
479
539
|
);
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
`Import result for DE "${deKey}" looks unexpected: expected at least ${expected} rows, got ${countAfter}.`,
|
|
489
|
-
);
|
|
490
|
-
}
|
|
491
|
-
}
|
|
540
|
+
warnIfImportCountUnexpected({
|
|
541
|
+
countBefore,
|
|
542
|
+
cleared: clearedSingleDe,
|
|
543
|
+
countAfter,
|
|
544
|
+
imported: n,
|
|
545
|
+
mode: /** @type {'upsert'|'insert'} */ (mode),
|
|
546
|
+
label: `DE "${deKey}"`,
|
|
547
|
+
});
|
|
492
548
|
}
|
|
493
549
|
return anyError ? 1 : 0;
|
|
494
550
|
}
|
|
495
551
|
|
|
496
552
|
const fileList = values.file ?? [];
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
);
|
|
553
|
+
/** @type {string[]} */
|
|
554
|
+
const keysFromFiles = [];
|
|
555
|
+
const seenKeys = new Set();
|
|
556
|
+
for (const fp of fileList) {
|
|
557
|
+
const k = parseExportBasename(path.basename(fp)).customerKey;
|
|
558
|
+
if (!seenKeys.has(k)) {
|
|
559
|
+
seenKeys.add(k);
|
|
560
|
+
keysFromFiles.push(k);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
500
563
|
if (backupBeforeImport === true) {
|
|
501
564
|
for (const deKey of keysFromFiles) {
|
|
502
|
-
const {
|
|
565
|
+
const { paths: outPaths, rowCount } = await exportDataExtensionToFile(sdk, {
|
|
503
566
|
projectRoot,
|
|
504
567
|
credentialName: credential,
|
|
505
568
|
buName: bu,
|
|
@@ -507,7 +570,8 @@ export async function main(argv) {
|
|
|
507
570
|
format: /** @type {'csv'|'tsv'|'json'} */ (fmt),
|
|
508
571
|
useGit: false,
|
|
509
572
|
});
|
|
510
|
-
|
|
573
|
+
const label = outPaths.map((p) => `"${path.resolve(p)}"`).join(', ');
|
|
574
|
+
log.info(`Backup export: ${label} (${rowCount} rows)`);
|
|
511
575
|
}
|
|
512
576
|
}
|
|
513
577
|
if (clear) {
|
|
@@ -517,33 +581,64 @@ export async function main(argv) {
|
|
|
517
581
|
isTTY: process.stdin.isTTY === true,
|
|
518
582
|
});
|
|
519
583
|
}
|
|
584
|
+
/** @type {Map<string, string[]>} */
|
|
585
|
+
const pathsByDeKey = new Map();
|
|
586
|
+
for (const fp of fileList) {
|
|
587
|
+
const { customerKey } = parseExportBasename(path.basename(fp));
|
|
588
|
+
const list = pathsByDeKey.get(customerKey);
|
|
589
|
+
if (list) {
|
|
590
|
+
list.push(fp);
|
|
591
|
+
} else {
|
|
592
|
+
pathsByDeKey.set(customerKey, [fp]);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
520
596
|
let anyFileError = false;
|
|
521
|
-
for (const
|
|
522
|
-
const
|
|
523
|
-
const {
|
|
597
|
+
for (const customerKey of keysFromFiles) {
|
|
598
|
+
const groupPaths = pathsByDeKey.get(customerKey) ?? [];
|
|
599
|
+
const { paths: importPaths } = await resolveImportSet(groupPaths);
|
|
600
|
+
if (importPaths.length === 0) {
|
|
601
|
+
log.error(`No resolvable import files for DE "${customerKey}".`);
|
|
602
|
+
anyFileError = true;
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
const detectedFormat = formatFromExtension(importPaths[0]);
|
|
606
|
+
if (!detectedFormat) {
|
|
607
|
+
log.error(
|
|
608
|
+
`Cannot determine format for "${importPaths[0]}". Use .csv, .tsv, or .json.`,
|
|
609
|
+
);
|
|
610
|
+
anyFileError = true;
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
524
613
|
|
|
525
614
|
const countBefore = await getDeRowCount(sdk, customerKey);
|
|
526
|
-
|
|
615
|
+
log.info(
|
|
527
616
|
`Row count before import: ${countBefore ?? '(unavailable)'} (DE "${customerKey}")`,
|
|
528
617
|
);
|
|
529
618
|
|
|
619
|
+
let clearedMultiFileDe = false;
|
|
530
620
|
if (clear) {
|
|
531
621
|
if (countBefore === 0) {
|
|
532
|
-
|
|
533
|
-
`Skipping clear-data for DE "${customerKey}" — DE is already empty.`,
|
|
534
|
-
);
|
|
622
|
+
log.info(`Skipping clear-data for DE "${customerKey}" — DE is already empty.`);
|
|
535
623
|
} else {
|
|
536
624
|
await clearDataExtensionRows(sdk.soap, customerKey);
|
|
537
|
-
|
|
625
|
+
clearedMultiFileDe = true;
|
|
626
|
+
log.warn(`Cleared data: DE "${customerKey}"`);
|
|
538
627
|
}
|
|
539
628
|
}
|
|
540
629
|
|
|
541
|
-
const
|
|
542
|
-
|
|
630
|
+
const rowCount = await countDataRowsFromImportPaths(importPaths, detectedFormat);
|
|
631
|
+
assertNonEmptyImportRowCount(rowCount, importPaths.join(', '));
|
|
632
|
+
const totalMemoryBatches = Math.max(1, Math.ceil(rowCount / MAX_OBJECTS_PER_BATCH));
|
|
633
|
+
const rowSource = streamRowsFromImportPaths(importPaths, detectedFormat);
|
|
634
|
+
const { count: n, requestIds } = await importRowsStreamingForDe(sdk, {
|
|
543
635
|
deKey: customerKey,
|
|
636
|
+
rowSource,
|
|
544
637
|
mode: /** @type {'upsert'|'insert'} */ (mode),
|
|
638
|
+
totalMemoryBatches,
|
|
545
639
|
});
|
|
546
|
-
|
|
640
|
+
const srcLabel = importPaths.map((p) => `"${path.resolve(p)}"`).join(', ');
|
|
641
|
+
log.info(`Imported: ${srcLabel} (${n} rows)`);
|
|
547
642
|
|
|
548
643
|
const importHadError = await pollAsyncImportCompletion(sdk, requestIds);
|
|
549
644
|
if (importHadError) {
|
|
@@ -551,25 +646,22 @@ export async function main(argv) {
|
|
|
551
646
|
}
|
|
552
647
|
|
|
553
648
|
const countAfter = await getDeRowCount(sdk, customerKey);
|
|
554
|
-
|
|
649
|
+
log.info(
|
|
555
650
|
`Row count after import: ${countAfter ?? '(unavailable)'} (DE "${customerKey}")`,
|
|
556
651
|
);
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
652
|
+
warnIfImportCountUnexpected({
|
|
653
|
+
countBefore,
|
|
654
|
+
cleared: clearedMultiFileDe,
|
|
655
|
+
countAfter,
|
|
656
|
+
imported: n,
|
|
657
|
+
mode: /** @type {'upsert'|'insert'} */ (mode),
|
|
658
|
+
label: `DE "${customerKey}"`,
|
|
659
|
+
});
|
|
568
660
|
}
|
|
569
661
|
return anyFileError ? 1 : 0;
|
|
570
662
|
}
|
|
571
663
|
|
|
572
|
-
|
|
664
|
+
log.error(`Unknown command: ${sub}`);
|
|
573
665
|
printHelp();
|
|
574
666
|
return 1;
|
|
575
667
|
}
|