sfmc-dataloader 2.2.0 → 2.3.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/cli.mjs +68 -6
- package/lib/cross-bu-import.mjs +36 -14
- package/lib/index.mjs +1 -0
- package/lib/row-count.mjs +20 -0
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import { parseExportBasename } from './filename.mjs';
|
|
|
18
18
|
import { importFromFile } from './import-de.mjs';
|
|
19
19
|
import { clearDataExtensionRows } from './clear-de.mjs';
|
|
20
20
|
import { confirmClearBeforeImport } from './confirm-clear.mjs';
|
|
21
|
+
import { getDeRowCount } from './row-count.mjs';
|
|
21
22
|
import { multiBuExport } from './multi-bu-export.mjs';
|
|
22
23
|
import { crossBuImport } from './cross-bu-import.mjs';
|
|
23
24
|
import { initDebugLogger } from './debug-logger.mjs';
|
|
@@ -398,9 +399,6 @@ export async function main(argv) {
|
|
|
398
399
|
acceptRiskFlag: acceptRisk,
|
|
399
400
|
isTTY: process.stdin.isTTY === true,
|
|
400
401
|
});
|
|
401
|
-
for (const deKey of deKeys) {
|
|
402
|
-
await clearDataExtensionRows(sdk.soap, deKey);
|
|
403
|
-
}
|
|
404
402
|
}
|
|
405
403
|
for (const deKey of deKeys) {
|
|
406
404
|
const candidates = await findImportCandidates(dataDir, deKey);
|
|
@@ -412,6 +410,23 @@ export async function main(argv) {
|
|
|
412
410
|
}
|
|
413
411
|
const filePath =
|
|
414
412
|
candidates.length === 1 ? candidates[0] : await pickLatestByMtime(candidates);
|
|
413
|
+
|
|
414
|
+
const countBefore = await getDeRowCount(sdk, deKey);
|
|
415
|
+
console.error(
|
|
416
|
+
`Row count before import: ${countBefore ?? '(unavailable)'} (DE "${deKey}")`,
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
if (clear) {
|
|
420
|
+
if (countBefore === 0) {
|
|
421
|
+
console.error(
|
|
422
|
+
`Skipping clear-data for DE "${deKey}" — DE is already empty.`,
|
|
423
|
+
);
|
|
424
|
+
} else {
|
|
425
|
+
await clearDataExtensionRows(sdk.soap, deKey);
|
|
426
|
+
console.warn(`Cleared data: DE "${deKey}"`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
415
430
|
const n = await importFromFile(sdk, {
|
|
416
431
|
filePath,
|
|
417
432
|
deKey,
|
|
@@ -419,6 +434,23 @@ export async function main(argv) {
|
|
|
419
434
|
});
|
|
420
435
|
const rel = projectRelativePosix(projectRoot, filePath);
|
|
421
436
|
console.error(`Imported: ${rel} (${n} rows) -> DE ${deKey}`);
|
|
437
|
+
|
|
438
|
+
const countAfter = await getDeRowCount(sdk, deKey);
|
|
439
|
+
console.error(
|
|
440
|
+
`Row count after import: ${countAfter ?? '(unavailable)'} (DE "${deKey}")`,
|
|
441
|
+
);
|
|
442
|
+
if (countAfter === null) {
|
|
443
|
+
console.error(`Could not verify import result for DE "${deKey}".`);
|
|
444
|
+
} else if (countBefore !== null && countAfter < (countBefore ?? 0) + n) {
|
|
445
|
+
// Insert: expect countBefore + n; upsert on empty: same; upsert on non-empty: expect >= n
|
|
446
|
+
const expected =
|
|
447
|
+
mode === 'insert' || countBefore === 0 ? (countBefore ?? 0) + n : n;
|
|
448
|
+
if (countAfter < expected) {
|
|
449
|
+
console.error(
|
|
450
|
+
`Import result for DE "${deKey}" looks unexpected: expected at least ${expected} rows, got ${countAfter}. Note: the async API may not have committed all rows yet.`,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
422
454
|
}
|
|
423
455
|
return 0;
|
|
424
456
|
}
|
|
@@ -448,13 +480,27 @@ export async function main(argv) {
|
|
|
448
480
|
acceptRiskFlag: acceptRisk,
|
|
449
481
|
isTTY: process.stdin.isTTY === true,
|
|
450
482
|
});
|
|
451
|
-
for (const deKey of keysFromFiles) {
|
|
452
|
-
await clearDataExtensionRows(sdk.soap, deKey);
|
|
453
|
-
}
|
|
454
483
|
}
|
|
455
484
|
for (const filePath of fileList) {
|
|
456
485
|
const base = path.basename(filePath);
|
|
457
486
|
const { customerKey } = parseExportBasename(base);
|
|
487
|
+
|
|
488
|
+
const countBefore = await getDeRowCount(sdk, customerKey);
|
|
489
|
+
console.error(
|
|
490
|
+
`Row count before import: ${countBefore ?? '(unavailable)'} (DE "${customerKey}")`,
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
if (clear) {
|
|
494
|
+
if (countBefore === 0) {
|
|
495
|
+
console.error(
|
|
496
|
+
`Skipping clear-data for DE "${customerKey}" — DE is already empty.`,
|
|
497
|
+
);
|
|
498
|
+
} else {
|
|
499
|
+
await clearDataExtensionRows(sdk.soap, customerKey);
|
|
500
|
+
console.warn(`Cleared data: DE "${customerKey}"`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
458
504
|
const n = await importFromFile(sdk, {
|
|
459
505
|
filePath,
|
|
460
506
|
deKey: customerKey,
|
|
@@ -462,6 +508,22 @@ export async function main(argv) {
|
|
|
462
508
|
});
|
|
463
509
|
const rel = projectRelativePosix(projectRoot, filePath);
|
|
464
510
|
console.error(`Imported: ${rel} (${n} rows)`);
|
|
511
|
+
|
|
512
|
+
const countAfter = await getDeRowCount(sdk, customerKey);
|
|
513
|
+
console.error(
|
|
514
|
+
`Row count after import: ${countAfter ?? '(unavailable)'} (DE "${customerKey}")`,
|
|
515
|
+
);
|
|
516
|
+
if (countAfter === null) {
|
|
517
|
+
console.error(`Could not verify import result for DE "${customerKey}".`);
|
|
518
|
+
} else {
|
|
519
|
+
const expected =
|
|
520
|
+
mode === 'insert' || countBefore === 0 ? (countBefore ?? 0) + n : n;
|
|
521
|
+
if (countAfter < expected) {
|
|
522
|
+
console.error(
|
|
523
|
+
`Import result for DE "${customerKey}" looks unexpected: expected at least ${expected} rows, got ${countAfter}. Note: the async API may not have committed all rows yet.`,
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
465
527
|
}
|
|
466
528
|
return 0;
|
|
467
529
|
}
|
package/lib/cross-bu-import.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { clearDataExtensionRows } from './clear-de.mjs';
|
|
|
12
12
|
import { confirmClearBeforeImport } from './confirm-clear.mjs';
|
|
13
13
|
import { dataDirectoryForBu, projectRelativePosix } from './paths.mjs';
|
|
14
14
|
import { buildExportBasename, filesystemSafeTimestamp, parseExportBasename } from './filename.mjs';
|
|
15
|
+
import { getDeRowCount } from './row-count.mjs';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* @typedef {{ credential: string, bu: string }} CredBuTarget
|
|
@@ -184,24 +185,27 @@ export async function crossBuImport(params) {
|
|
|
184
185
|
rows = await fetchAllRowObjects(srcSdk, deKey);
|
|
185
186
|
}
|
|
186
187
|
|
|
187
|
-
// Clear targets before import (rows already confirmed above)
|
|
188
|
-
if (clearBeforeImport) {
|
|
189
|
-
for (const { credential, bu } of targets) {
|
|
190
|
-
const { mid, authCred } = resolveCredentialAndMid(
|
|
191
|
-
mcdevrc,
|
|
192
|
-
mcdevAuth,
|
|
193
|
-
credential,
|
|
194
|
-
bu,
|
|
195
|
-
);
|
|
196
|
-
const tgtSdk = new SDK(buildSdkAuthObject(authCred, mid), buildSdkOptions(logger));
|
|
197
|
-
await clearDataExtensionRows(tgtSdk.soap, deKey);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
188
|
for (const { credential, bu } of targets) {
|
|
202
189
|
const { mid, authCred } = resolveCredentialAndMid(mcdevrc, mcdevAuth, credential, bu);
|
|
203
190
|
const tgtSdk = new SDK(buildSdkAuthObject(authCred, mid), buildSdkOptions(logger));
|
|
204
191
|
|
|
192
|
+
const countBefore = await getDeRowCount(tgtSdk, deKey);
|
|
193
|
+
console.error(
|
|
194
|
+
`Row count before import: ${countBefore ?? '(unavailable)'} (${credential}/${bu} DE "${deKey}")`,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Clear target before import (already confirmed above); skip if DE is empty
|
|
198
|
+
if (clearBeforeImport) {
|
|
199
|
+
if (countBefore === 0) {
|
|
200
|
+
console.error(
|
|
201
|
+
`Skipping clear-data for ${credential}/${bu} DE "${deKey}" — DE is already empty.`,
|
|
202
|
+
);
|
|
203
|
+
} else {
|
|
204
|
+
await clearDataExtensionRows(tgtSdk.soap, deKey);
|
|
205
|
+
console.warn(`Cleared data: ${credential}/${bu} DE "${deKey}"`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
205
209
|
// Write a snapshot file in the target BU's data directory.
|
|
206
210
|
const dir = dataDirectoryForBu(projectRoot, credential, bu);
|
|
207
211
|
await fs.mkdir(dir, { recursive: true });
|
|
@@ -214,6 +218,24 @@ export async function crossBuImport(params) {
|
|
|
214
218
|
|
|
215
219
|
const imported = await importRowsForDe(tgtSdk, { deKey, rows, mode });
|
|
216
220
|
console.error(`Imported: ${credential}/${bu} DE ${deKey} (${imported} rows)`);
|
|
221
|
+
|
|
222
|
+
const countAfter = await getDeRowCount(tgtSdk, deKey);
|
|
223
|
+
console.error(
|
|
224
|
+
`Row count after import: ${countAfter ?? '(unavailable)'} (${credential}/${bu} DE "${deKey}")`,
|
|
225
|
+
);
|
|
226
|
+
if (countAfter === null) {
|
|
227
|
+
console.error(
|
|
228
|
+
`Could not verify import result for ${credential}/${bu} DE "${deKey}".`,
|
|
229
|
+
);
|
|
230
|
+
} else {
|
|
231
|
+
const expected =
|
|
232
|
+
mode === 'insert' || countBefore === 0 ? (countBefore ?? 0) + imported : imported;
|
|
233
|
+
if (countAfter < expected) {
|
|
234
|
+
console.error(
|
|
235
|
+
`Import result for ${credential}/${bu} DE "${deKey}" looks unexpected: expected at least ${expected} rows, got ${countAfter}. Note: the async API may not have committed all rows yet.`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
217
239
|
}
|
|
218
240
|
}
|
|
219
241
|
}
|
package/lib/index.mjs
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { rowsetGetPath } from './import-routes.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fetch the total row count for a Data Extension without downloading all rows.
|
|
5
|
+
* Uses a pagesize=1 request so only the `count` metadata is transferred.
|
|
6
|
+
* Returns `null` if the count cannot be determined (e.g. permissions error).
|
|
7
|
+
*
|
|
8
|
+
* @param {{ rest: { get: (path: string) => Promise.<any> } }} sdk
|
|
9
|
+
* @param {string} deKey - DE external key
|
|
10
|
+
* @returns {Promise.<number|null>}
|
|
11
|
+
*/
|
|
12
|
+
export async function getDeRowCount(sdk, deKey) {
|
|
13
|
+
try {
|
|
14
|
+
const result = await sdk.rest.get(`${rowsetGetPath(deKey)}?$page=1&$pagesize=1`);
|
|
15
|
+
return result?.count ?? 0;
|
|
16
|
+
} catch (ex) {
|
|
17
|
+
console.error(`Could not retrieve row count for DE "${deKey}": ${ex.message}`);
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sfmc-dataloader",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
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",
|