sfmc-dataloader 2.4.0 → 2.4.2
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/cross-bu-import.mjs +29 -2
- package/lib/export-de.mjs +44 -5
- package/lib/import-de.mjs +7 -0
- package/lib/read-rows.mjs +1 -1
- package/package.json +1 -1
package/lib/cross-bu-import.mjs
CHANGED
|
@@ -4,7 +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, buildSdkOptions } from './config.mjs';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
fetchAllRowObjects,
|
|
9
|
+
fetchDataExtensionFieldNames,
|
|
10
|
+
serializeRows,
|
|
11
|
+
exportDataExtensionToFile,
|
|
12
|
+
} from './export-de.mjs';
|
|
8
13
|
import { formatFromExtension } from './file-resolve.mjs';
|
|
9
14
|
import { importRowsForDe } from './import-de.mjs';
|
|
10
15
|
import { pollAsyncImportCompletion } from './async-status.mjs';
|
|
@@ -184,10 +189,28 @@ export async function crossBuImport(params) {
|
|
|
184
189
|
);
|
|
185
190
|
}
|
|
186
191
|
rows = await readRowsFromFile(filePath, detectedFormat);
|
|
192
|
+
if (rows.length === 0) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
`Import file contains no data rows: "${filePath}". ` +
|
|
195
|
+
`The file may be empty, contain only a BOM, or contain only a header row. ` +
|
|
196
|
+
`Export the DE first to obtain a template with column names, then add rows.`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
187
199
|
} else {
|
|
188
200
|
rows = await fetchAllRowObjects(srcSdk, deKey);
|
|
189
201
|
}
|
|
190
202
|
|
|
203
|
+
let snapshotColumns = [];
|
|
204
|
+
if (rows.length === 0 && format !== 'json') {
|
|
205
|
+
try {
|
|
206
|
+
snapshotColumns = await fetchDataExtensionFieldNames(srcSdk.soap, deKey);
|
|
207
|
+
} catch (ex) {
|
|
208
|
+
console.error(
|
|
209
|
+
`Warning: could not retrieve field names for empty DE "${deKey}" (snapshot): ${ex.message}`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
191
214
|
for (const { credential, bu } of targets) {
|
|
192
215
|
const { mid, authCred } = resolveCredentialAndMid(mcdevrc, mcdevAuth, credential, bu);
|
|
193
216
|
const tgtSdk = new SDK(buildSdkAuthObject(authCred, mid), buildSdkOptions(logger));
|
|
@@ -215,7 +238,11 @@ export async function crossBuImport(params) {
|
|
|
215
238
|
const ts = filesystemSafeTimestamp();
|
|
216
239
|
const basename = buildExportBasename(deKey, ts, format, false);
|
|
217
240
|
const snapshotPath = path.join(dir, basename);
|
|
218
|
-
await fs.writeFile(
|
|
241
|
+
await fs.writeFile(
|
|
242
|
+
snapshotPath,
|
|
243
|
+
serializeRows(rows, format, false, snapshotColumns),
|
|
244
|
+
'utf8',
|
|
245
|
+
);
|
|
219
246
|
const snapRel = projectRelativePosix(projectRoot, snapshotPath);
|
|
220
247
|
console.error(`Download stored: ${snapRel} (${rows.length} rows)`);
|
|
221
248
|
|
package/lib/export-de.mjs
CHANGED
|
@@ -30,28 +30,57 @@ export async function fetchAllRowObjects(sdk, deKey) {
|
|
|
30
30
|
return rows;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Field names for a Data Extension, in ordinal order (SOAP).
|
|
35
|
+
*
|
|
36
|
+
* @param {{ retrieve: (type: string, props: string[], opts: object) => Promise.<any> }} soap
|
|
37
|
+
* @param {string} deKey - DE customer key
|
|
38
|
+
* @returns {Promise.<string[]>}
|
|
39
|
+
*/
|
|
40
|
+
export async function fetchDataExtensionFieldNames(soap, deKey) {
|
|
41
|
+
const result = await soap.retrieve('DataExtensionField', ['Name', 'Ordinal'], {
|
|
42
|
+
filter: {
|
|
43
|
+
leftOperand: 'DataExtension.CustomerKey',
|
|
44
|
+
operator: 'equals',
|
|
45
|
+
rightOperand: deKey,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
const fields = result?.Results;
|
|
49
|
+
if (!Array.isArray(fields) || fields.length === 0) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
return fields
|
|
53
|
+
.toSorted((a, b) => Number(a.Ordinal ?? 0) - Number(b.Ordinal ?? 0))
|
|
54
|
+
.map((f) => f.Name);
|
|
55
|
+
}
|
|
56
|
+
|
|
33
57
|
/**
|
|
34
58
|
* @param {object[]} rows
|
|
35
59
|
* @param {'csv'|'tsv'|'json'} format
|
|
36
60
|
* @param {boolean} jsonPretty
|
|
61
|
+
* @param {string[]} [columns] - when `rows` is empty and format is csv/tsv, emit this header row
|
|
37
62
|
* @returns {string}
|
|
38
63
|
*/
|
|
39
|
-
export function serializeRows(rows, format, jsonPretty) {
|
|
64
|
+
export function serializeRows(rows, format, jsonPretty, columns = []) {
|
|
40
65
|
if (format === 'json') {
|
|
41
66
|
const space = jsonPretty ? 2 : undefined;
|
|
42
67
|
return JSON.stringify(rows, null, space) + '\n';
|
|
43
68
|
}
|
|
44
69
|
const delimiter = format === 'tsv' ? '\t' : ',';
|
|
45
|
-
|
|
70
|
+
const options = {
|
|
46
71
|
header: true,
|
|
47
72
|
quoted: format === 'csv',
|
|
48
73
|
bom: true,
|
|
49
74
|
delimiter,
|
|
50
|
-
}
|
|
75
|
+
};
|
|
76
|
+
if (rows.length === 0 && columns.length > 0) {
|
|
77
|
+
options.columns = columns;
|
|
78
|
+
}
|
|
79
|
+
return stringify(rows, options);
|
|
51
80
|
}
|
|
52
81
|
|
|
53
82
|
/**
|
|
54
|
-
* @param {{rest: {getBulk: (path: string, pageSize?: number) => Promise.<any>}}} sdk
|
|
83
|
+
* @param {{ rest: { getBulk: (path: string, pageSize?: number) => Promise.<any> }, soap: { retrieve: Function } }} sdk
|
|
55
84
|
* @param {object} params
|
|
56
85
|
* @param {string} params.projectRoot
|
|
57
86
|
* @param {string} params.credentialName
|
|
@@ -73,12 +102,22 @@ export async function exportDataExtensionToFile(sdk, params) {
|
|
|
73
102
|
useGit = false,
|
|
74
103
|
} = params;
|
|
75
104
|
const rows = await fetchAllRowObjects(sdk, deKey);
|
|
105
|
+
let columns = [];
|
|
106
|
+
if (rows.length === 0 && format !== 'json') {
|
|
107
|
+
try {
|
|
108
|
+
columns = await fetchDataExtensionFieldNames(sdk.soap, deKey);
|
|
109
|
+
} catch (ex) {
|
|
110
|
+
console.error(
|
|
111
|
+
`Warning: could not retrieve field names for empty DE "${deKey}": ${ex.message}`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
76
115
|
const dir = dataDirectoryForBu(projectRoot, credentialName, buName);
|
|
77
116
|
await fs.mkdir(dir, { recursive: true });
|
|
78
117
|
const ts = filesystemSafeTimestamp();
|
|
79
118
|
const basename = buildExportBasename(deKey, ts, format, useGit);
|
|
80
119
|
const outPath = path.join(dir, basename);
|
|
81
|
-
const body = serializeRows(rows, format, jsonPretty);
|
|
120
|
+
const body = serializeRows(rows, format, jsonPretty, columns);
|
|
82
121
|
await fs.writeFile(outPath, body, 'utf8');
|
|
83
122
|
return { path: outPath, rowCount: rows.length };
|
|
84
123
|
}
|
package/lib/import-de.mjs
CHANGED
|
@@ -45,6 +45,13 @@ export async function importFromFile(sdk, params) {
|
|
|
45
45
|
);
|
|
46
46
|
}
|
|
47
47
|
const rows = await readRowsFromFile(params.filePath, format);
|
|
48
|
+
if (rows.length === 0) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Import file contains no data rows: "${params.filePath}". ` +
|
|
51
|
+
`The file may be empty, contain only a BOM, or contain only a header row. ` +
|
|
52
|
+
`Export the DE first to obtain a template with column names, then add rows.`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
48
55
|
return importRowsForDe(sdk, {
|
|
49
56
|
deKey: params.deKey,
|
|
50
57
|
rows,
|
package/lib/read-rows.mjs
CHANGED
|
@@ -29,7 +29,7 @@ export async function readRowsFromFile(filePath, format) {
|
|
|
29
29
|
mapHeaders: ({ header }) => {
|
|
30
30
|
let h = header;
|
|
31
31
|
// Strip BOM if present (backup in case bom:true misses it)
|
|
32
|
-
if (h.
|
|
32
|
+
if (h.startsWith('\uFEFF')) {
|
|
33
33
|
h = h.slice(1);
|
|
34
34
|
}
|
|
35
35
|
// Strip surrounding quotes if present (non-standard quoted TSV)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sfmc-dataloader",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.2",
|
|
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",
|