mcdev 3.0.3 → 3.1.3
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/.eslintrc.json +1 -1
- package/.github/ISSUE_TEMPLATE/bug.yml +75 -0
- package/.github/ISSUE_TEMPLATE/task.md +1 -1
- package/.issuetracker +11 -3
- package/.vscode/settings.json +3 -3
- package/CHANGELOG.md +66 -0
- package/README.md +245 -141
- package/boilerplate/config.json +3 -2
- package/docs/dist/documentation.md +799 -338
- package/lib/Deployer.js +4 -1
- package/lib/MetadataTypeDefinitions.js +1 -0
- package/lib/MetadataTypeInfo.js +1 -0
- package/lib/Retriever.js +30 -14
- package/lib/cli.js +298 -0
- package/lib/index.js +773 -1019
- package/lib/metadataTypes/AccountUser.js +389 -0
- package/lib/metadataTypes/Asset.js +8 -7
- package/lib/metadataTypes/Automation.js +121 -56
- package/lib/metadataTypes/DataExtension.js +133 -97
- package/lib/metadataTypes/DataExtensionField.js +134 -4
- package/lib/metadataTypes/DataExtract.js +9 -5
- package/lib/metadataTypes/EventDefinition.js +9 -5
- package/lib/metadataTypes/FileTransfer.js +9 -5
- package/lib/metadataTypes/ImportFile.js +13 -12
- package/lib/metadataTypes/MetadataType.js +41 -33
- package/lib/metadataTypes/Query.js +2 -3
- package/lib/metadataTypes/Role.js +13 -8
- package/lib/metadataTypes/Script.js +2 -2
- package/lib/metadataTypes/definitions/AccountUser.definition.js +227 -0
- package/lib/metadataTypes/definitions/Asset.definition.js +1 -0
- package/lib/metadataTypes/definitions/DataExtension.definition.js +1 -1
- package/lib/metadataTypes/definitions/ImportFile.definition.js +2 -1
- package/lib/metadataTypes/definitions/Script.definition.js +5 -5
- package/lib/retrieveChangelog.js +96 -0
- package/lib/util/cli.js +4 -6
- package/lib/util/init.git.js +2 -1
- package/lib/util/util.js +17 -0
- package/package.json +18 -22
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -30
- package/img/README.md/troubleshoot-nodejs-postinstall.jpg +0 -0
- package/postinstall.js +0 -41
|
@@ -92,14 +92,10 @@ class Automation extends MetadataType {
|
|
|
92
92
|
* @returns {Promise<{metadata:AutomationMap,type:string}>} Promise of metadata
|
|
93
93
|
*/
|
|
94
94
|
static async retrieve(retrieveDir) {
|
|
95
|
-
const results = await new Promise((resolve) => {
|
|
96
|
-
this.client.SoapClient.retrieve('Program', ['ObjectID'], (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
} else {
|
|
100
|
-
resolve(response.body.Results);
|
|
101
|
-
}
|
|
102
|
-
});
|
|
95
|
+
const results = await new Promise((resolve, reject) => {
|
|
96
|
+
this.client.SoapClient.retrieve('Program', ['ObjectID'], (ex, response) =>
|
|
97
|
+
ex ? reject(ex) : resolve(response.body.Results)
|
|
98
|
+
);
|
|
103
99
|
});
|
|
104
100
|
const details = (
|
|
105
101
|
await Promise.all(
|
|
@@ -116,6 +112,60 @@ class Automation extends MetadataType {
|
|
|
116
112
|
Util.logger.info(
|
|
117
113
|
`Downloaded: ${this.definition.type} (${Object.keys(savedMetadata).length})`
|
|
118
114
|
);
|
|
115
|
+
return { metadata: savedMetadata, type: this.definition.type };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Retrieves Metadata of Automation
|
|
119
|
+
* @returns {Promise<{metadata:AutomationMap,type:string}>} Promise of metadata
|
|
120
|
+
*/
|
|
121
|
+
static async retrieveChangelog() {
|
|
122
|
+
const results = await new Promise((resolve, reject) => {
|
|
123
|
+
this.client.SoapClient.retrieve(
|
|
124
|
+
'Program',
|
|
125
|
+
['ObjectID'],
|
|
126
|
+
|
|
127
|
+
(ex, response) => (ex ? reject(ex) : resolve(response.body.Results))
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
const details = [];
|
|
131
|
+
(
|
|
132
|
+
await Promise.all(
|
|
133
|
+
results.map(async (a) => {
|
|
134
|
+
const options = {
|
|
135
|
+
filter: {
|
|
136
|
+
leftOperand: 'ProgramID',
|
|
137
|
+
operator: 'equals',
|
|
138
|
+
rightOperand: a.ObjectID,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
this.client.SoapClient.retrieve(
|
|
144
|
+
'Automation',
|
|
145
|
+
[
|
|
146
|
+
'ProgramID',
|
|
147
|
+
'Name',
|
|
148
|
+
'CustomerKey',
|
|
149
|
+
'LastSaveDate',
|
|
150
|
+
'LastSavedBy',
|
|
151
|
+
'CreatedBy',
|
|
152
|
+
'CreatedDate',
|
|
153
|
+
],
|
|
154
|
+
options,
|
|
155
|
+
(ex, response) => (ex ? reject(ex) : resolve(response.body.Results))
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
})
|
|
159
|
+
)
|
|
160
|
+
).forEach((item) => {
|
|
161
|
+
details.push(...item);
|
|
162
|
+
});
|
|
163
|
+
details.map((item) => {
|
|
164
|
+
item.key = item.CustomerKey;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const parsed = this.parseResponseBody({ items: details });
|
|
168
|
+
|
|
119
169
|
return { metadata: parsed, type: this.definition.type };
|
|
120
170
|
}
|
|
121
171
|
|
|
@@ -152,10 +202,10 @@ class Automation extends MetadataType {
|
|
|
152
202
|
* Retrieve a specific Automation Definition by Name
|
|
153
203
|
* @param {string} templateDir Directory where retrieved metadata directory will be saved
|
|
154
204
|
* @param {string} name name of the metadata file
|
|
155
|
-
* @param {Util.TemplateMap}
|
|
205
|
+
* @param {Util.TemplateMap} templateVariables variables to be replaced in the metadata
|
|
156
206
|
* @returns {Promise<{metadata:AutomationMap,type:string}>} Promise of metadata
|
|
157
207
|
*/
|
|
158
|
-
static async retrieveAsTemplate(templateDir, name,
|
|
208
|
+
static async retrieveAsTemplate(templateDir, name, templateVariables) {
|
|
159
209
|
const results = await new Promise((resolve) => {
|
|
160
210
|
this.client.SoapClient.retrieve(
|
|
161
211
|
'Program',
|
|
@@ -189,12 +239,14 @@ class Automation extends MetadataType {
|
|
|
189
239
|
})
|
|
190
240
|
).body;
|
|
191
241
|
let val = null;
|
|
242
|
+
let originalKey;
|
|
192
243
|
// if parsing fails, we should just save what we get
|
|
193
244
|
try {
|
|
194
245
|
const parsedDetails = this.parseMetadata(details);
|
|
246
|
+
originalKey = parsedDetails[this.definition.keyField];
|
|
195
247
|
if (parsedDetails !== null) {
|
|
196
248
|
val = JSON.parse(
|
|
197
|
-
Util.replaceByObject(JSON.stringify(parsedDetails),
|
|
249
|
+
Util.replaceByObject(JSON.stringify(parsedDetails), templateVariables)
|
|
198
250
|
);
|
|
199
251
|
}
|
|
200
252
|
} catch (ex) {
|
|
@@ -209,7 +261,7 @@ class Automation extends MetadataType {
|
|
|
209
261
|
this.keepTemplateFields(val);
|
|
210
262
|
File.writeJSONToFile(
|
|
211
263
|
[templateDir, this.definition.type].join('/'),
|
|
212
|
-
|
|
264
|
+
originalKey + '.' + this.definition.type + '-meta',
|
|
213
265
|
val
|
|
214
266
|
);
|
|
215
267
|
Util.logger.info(
|
|
@@ -315,9 +367,8 @@ class Automation extends MetadataType {
|
|
|
315
367
|
delete metadata.schedule.scheduledTime;
|
|
316
368
|
delete metadata.schedule.scheduledStatus;
|
|
317
369
|
if (this.definition.timeZoneMapping[metadata.schedule.timezoneName]) {
|
|
318
|
-
metadata.schedule.timezoneId =
|
|
319
|
-
metadata.schedule.timezoneName
|
|
320
|
-
];
|
|
370
|
+
metadata.schedule.timezoneId =
|
|
371
|
+
this.definition.timeZoneMapping[metadata.schedule.timezoneName];
|
|
321
372
|
} else {
|
|
322
373
|
Util.logger.error(
|
|
323
374
|
`Could not find timezone ${metadata.schedule.timezoneName} in definition.timeZoneMapping`
|
|
@@ -435,44 +486,56 @@ class Automation extends MetadataType {
|
|
|
435
486
|
}
|
|
436
487
|
if (schedule !== null) {
|
|
437
488
|
try {
|
|
438
|
-
await
|
|
439
|
-
this.
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
(response
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
489
|
+
await Util.retryOnError(
|
|
490
|
+
`Retrying ${this.definition.type}`,
|
|
491
|
+
async () => {
|
|
492
|
+
await new Promise((resolve, reject) => {
|
|
493
|
+
this.client.SoapClient.schedule(
|
|
494
|
+
'Automation',
|
|
495
|
+
schedule,
|
|
496
|
+
{
|
|
497
|
+
Interaction: {
|
|
498
|
+
ObjectID: metadata[key].id,
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
'start',
|
|
502
|
+
null,
|
|
503
|
+
(error, response) => {
|
|
504
|
+
if (
|
|
505
|
+
error ||
|
|
506
|
+
(response.body.Results &&
|
|
507
|
+
response.body.Results[0] &&
|
|
508
|
+
response.body.Results[0].StatusCode &&
|
|
509
|
+
response.body.Results[0].StatusCode ===
|
|
510
|
+
'Error')
|
|
511
|
+
) {
|
|
512
|
+
reject(
|
|
513
|
+
error ||
|
|
514
|
+
response.body.Results[0].StatusMessage
|
|
515
|
+
);
|
|
516
|
+
} else {
|
|
517
|
+
resolve(response.body.Results);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
);
|
|
521
|
+
});
|
|
522
|
+
const intervalString =
|
|
523
|
+
(schedule._interval > 1 ? `${schedule._interval} ` : '') +
|
|
524
|
+
(schedule.RecurrenceType === 'Daily'
|
|
525
|
+
? 'Day'
|
|
526
|
+
: schedule.RecurrenceType.slice(0, -2) +
|
|
527
|
+
(schedule._interval > 1 ? 's' : ''));
|
|
528
|
+
Util.logger.warn(
|
|
529
|
+
`Automation '${
|
|
530
|
+
originalMetadata[key].name
|
|
531
|
+
}' deployed Active: runs every ${intervalString} starting ${
|
|
532
|
+
schedule._StartDateTime
|
|
533
|
+
.split('T')
|
|
534
|
+
.join(' ')
|
|
535
|
+
.split('.')[0]
|
|
536
|
+
} ${schedule._timezoneString}`
|
|
537
|
+
);
|
|
538
|
+
}
|
|
476
539
|
);
|
|
477
540
|
} catch (ex) {
|
|
478
541
|
Util.logger.error(
|
|
@@ -684,9 +747,8 @@ class Automation extends MetadataType {
|
|
|
684
747
|
}
|
|
685
748
|
|
|
686
749
|
if (this.definition.timeZoneMapping[scheduleObject.timezoneName]) {
|
|
687
|
-
scheduleObject.timezoneId =
|
|
688
|
-
scheduleObject.timezoneName
|
|
689
|
-
];
|
|
750
|
+
scheduleObject.timezoneId =
|
|
751
|
+
this.definition.timeZoneMapping[scheduleObject.timezoneName];
|
|
690
752
|
} else {
|
|
691
753
|
Util.logger.error(
|
|
692
754
|
`Could not find timezone ${scheduleObject.timezoneName} in definition.timeZoneMapping`
|
|
@@ -780,6 +842,9 @@ class Automation extends MetadataType {
|
|
|
780
842
|
// Assign definition to static attributes
|
|
781
843
|
Automation.definition = Definitions.automation;
|
|
782
844
|
Automation.cache = {};
|
|
845
|
+
/**
|
|
846
|
+
* @type {Util.ET_Client}
|
|
847
|
+
*/
|
|
783
848
|
Automation.client = undefined;
|
|
784
849
|
|
|
785
850
|
module.exports = Automation;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const jsonToTable = require('json-to-table');
|
|
4
4
|
const MetadataType = require('./MetadataType');
|
|
5
5
|
const DataExtensionField = require('./DataExtensionField');
|
|
6
|
+
const Folder = require('./Folder');
|
|
6
7
|
const Util = require('../util/util');
|
|
7
8
|
const File = require('../util/file');
|
|
8
9
|
|
|
@@ -43,7 +44,7 @@ class DataExtension extends MetadataType {
|
|
|
43
44
|
*/
|
|
44
45
|
static async upsert(desToDeploy, _, buObject) {
|
|
45
46
|
Util.logger.info('- Retrieve target metadata for comparison with deploy metadata');
|
|
46
|
-
const results = await this.retrieveForCache(buObject);
|
|
47
|
+
const results = await this.retrieveForCache(buObject, null, true);
|
|
47
48
|
const targetMetadata = results.metadata;
|
|
48
49
|
Util.logger.info('- Retrieved target metadata');
|
|
49
50
|
/** @type {Promise[]} */
|
|
@@ -89,21 +90,7 @@ class DataExtension extends MetadataType {
|
|
|
89
90
|
// skip rest of handling for this DE
|
|
90
91
|
continue;
|
|
91
92
|
}
|
|
92
|
-
// Error if SendableSubscriberField.Name = '_SubscriberKey' even though it is retrieved like that
|
|
93
|
-
// Therefore map it to 'Subscriber Key'. Retrieving afterward still results in '_SubscriberKey'
|
|
94
|
-
if (
|
|
95
|
-
desToDeploy[dataExtension].SendableSubscriberField &&
|
|
96
|
-
desToDeploy[dataExtension].SendableSubscriberField.Name === '_SubscriberKey'
|
|
97
|
-
) {
|
|
98
|
-
desToDeploy[dataExtension].SendableSubscriberField.Name = 'Subscriber Key';
|
|
99
|
-
}
|
|
100
93
|
if (targetMetadata[dataExtension]) {
|
|
101
|
-
// Update dataExtension + Columns if they already exist; Create them if not
|
|
102
|
-
// Modify columns for update call
|
|
103
|
-
DataExtension.prepareDeployColumnsOnUpdate(
|
|
104
|
-
desToDeploy[dataExtension].Fields,
|
|
105
|
-
targetMetadata[dataExtension].Fields
|
|
106
|
-
);
|
|
107
94
|
// data extension already exists in target and needs to be updated
|
|
108
95
|
deUpdatePromises.push(DataExtension.update(desToDeploy[dataExtension]));
|
|
109
96
|
} else {
|
|
@@ -113,7 +100,7 @@ class DataExtension extends MetadataType {
|
|
|
113
100
|
}
|
|
114
101
|
if (deUpdatePromises.length) {
|
|
115
102
|
Util.logger.info(
|
|
116
|
-
'Please note that Data Retention Policies can only be set during creation, not during update.'
|
|
103
|
+
'- Please note that Data Retention Policies can only be set during creation, not during update.'
|
|
117
104
|
);
|
|
118
105
|
}
|
|
119
106
|
|
|
@@ -123,6 +110,7 @@ class DataExtension extends MetadataType {
|
|
|
123
110
|
const updateResults = (await Promise.allSettled(deUpdatePromises)).filter(
|
|
124
111
|
this._filterUpsertResults
|
|
125
112
|
);
|
|
113
|
+
|
|
126
114
|
const successfulResults = [...createResults, ...updateResults];
|
|
127
115
|
|
|
128
116
|
Util.metadataLogger(
|
|
@@ -160,6 +148,10 @@ class DataExtension extends MetadataType {
|
|
|
160
148
|
// promise rejects, whole request failed
|
|
161
149
|
Util.logger.error('- error upserting dataExtension: ' + res.reason);
|
|
162
150
|
return false;
|
|
151
|
+
} else if (res.value == undefined || Object.keys(res.value).length === 0) {
|
|
152
|
+
// in case of returning empty result handle gracefully
|
|
153
|
+
// TODO: consider if SOAP handler for this should really return empty object
|
|
154
|
+
return false;
|
|
163
155
|
} else if (res.value.results) {
|
|
164
156
|
Util.logger.error(
|
|
165
157
|
'- error upserting dataExtension: ' +
|
|
@@ -177,39 +169,6 @@ class DataExtension extends MetadataType {
|
|
|
177
169
|
}
|
|
178
170
|
}
|
|
179
171
|
|
|
180
|
-
/**
|
|
181
|
-
* Mofifies passed deployColumns for update by mapping ObjectID to their target column's values.
|
|
182
|
-
* Removes FieldType field if its the same in deploy and target column, because it results in an error even if its of the same type
|
|
183
|
-
*
|
|
184
|
-
* @param {DataExtensionField.DataExtensionFieldItem[]} deployColumns Columns of data extension that will be deployed
|
|
185
|
-
* @param {DataExtensionField.DataExtensionFieldItem[]} targetColumns Columns of data extension that currently exists in target
|
|
186
|
-
* @returns {void}
|
|
187
|
-
*/
|
|
188
|
-
static prepareDeployColumnsOnUpdate(deployColumns, targetColumns) {
|
|
189
|
-
// Map data extension column ObjectIDs to their target, because they are environment specific
|
|
190
|
-
for (const column in deployColumns) {
|
|
191
|
-
const deployColumn = deployColumns[column];
|
|
192
|
-
// Check if column exists in target
|
|
193
|
-
if (targetColumns[column]) {
|
|
194
|
-
// Map ObjectID to value of target, because DataExtensionField updates are based on ObjectID
|
|
195
|
-
deployColumn.ObjectID = targetColumns[column].ObjectID;
|
|
196
|
-
// Remove FieldType if it is the same in target, because an error occurs if a FieldTypes gets passed on a field update (even if its the same type)
|
|
197
|
-
if (targetColumns[column].FieldType === deployColumn.FieldType) {
|
|
198
|
-
delete deployColumn.FieldType;
|
|
199
|
-
} else {
|
|
200
|
-
// Updating to a new FieldType will result in an error
|
|
201
|
-
Util.logger.warn(
|
|
202
|
-
'DataExtension.prepareDeployColumnsOnUpdate:: Cannot update FieldType of field: ' +
|
|
203
|
-
deployColumn.CustomerKey
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
} else {
|
|
207
|
-
// Field doesn't exist in target, therefore Remove ObjectID if present
|
|
208
|
-
delete deployColumn.ObjectID;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
172
|
/**
|
|
214
173
|
* Create a single dataExtension. Also creates their columns in 'dataExtension.columns'
|
|
215
174
|
* @param {DataExtensionItem} metadata single metadata entry
|
|
@@ -256,33 +215,17 @@ class DataExtension extends MetadataType {
|
|
|
256
215
|
* @returns {Promise} Promise
|
|
257
216
|
*/
|
|
258
217
|
static async update(metadata) {
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
Object.keys(fieldsObj).forEach((key) => {
|
|
271
|
-
existingFieldObj[fieldsObj[key].Name] = fieldsObj[key].ObjectID;
|
|
272
|
-
});
|
|
273
|
-
metadata.Fields.map((item) => {
|
|
274
|
-
if (existingFieldObj[item.Name]) {
|
|
275
|
-
// field is getting updated ---
|
|
276
|
-
// remove FieldType as it cannot be updated mostly and will lead to an API error
|
|
277
|
-
delete item.FieldType;
|
|
278
|
-
|
|
279
|
-
// set the ObjectId for clear identification
|
|
280
|
-
item.ObjectID = existingFieldObj[item.Name];
|
|
281
|
-
} else {
|
|
282
|
-
// field is getting added ---
|
|
283
|
-
delete item.ObjectID; // make sure this is gone or it will cause issues on new fields!
|
|
284
|
-
}
|
|
285
|
-
});
|
|
218
|
+
// Update dataExtension + Columns if they already exist; Create them if not
|
|
219
|
+
// Modify columns for update call
|
|
220
|
+
DataExtensionField.cache = this.metadata;
|
|
221
|
+
DataExtensionField.client = this.client;
|
|
222
|
+
DataExtensionField.properties = this.properties;
|
|
223
|
+
DataExtension.oldFields = DataExtension.oldFields || {};
|
|
224
|
+
DataExtension.oldFields[metadata.CustomerKey] =
|
|
225
|
+
await DataExtensionField.prepareDeployColumnsOnUpdate(
|
|
226
|
+
metadata.Fields,
|
|
227
|
+
metadata.CustomerKey
|
|
228
|
+
);
|
|
286
229
|
|
|
287
230
|
// convert simple array into object.Array.object format to cope with how the XML body in the SOAP call needs to look like:
|
|
288
231
|
// <Fields>
|
|
@@ -295,25 +238,70 @@ class DataExtension extends MetadataType {
|
|
|
295
238
|
metadata.Fields = { Field: metadata.Fields };
|
|
296
239
|
return super.updateSOAP(metadata);
|
|
297
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Gets executed after deployment of metadata type
|
|
243
|
+
* @param {DataExtensionMap} upsertedMetadata metadata mapped by their keyField
|
|
244
|
+
* @returns {void}
|
|
245
|
+
*/
|
|
246
|
+
static postDeployTasks(upsertedMetadata) {
|
|
247
|
+
if (!DataExtension.oldFields) {
|
|
248
|
+
// only run postDeploy if we are in update mode
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
// somewhat of a workardoun but it ensures we get the field list from the server rather than whatever we might have in cache got returned during update/add. This ensures a complete and correctly ordered field list
|
|
252
|
+
for (const key in upsertedMetadata) {
|
|
253
|
+
const item = upsertedMetadata[key];
|
|
254
|
+
const isUpdate =
|
|
255
|
+
this.cache &&
|
|
256
|
+
this.cache.dataExtension &&
|
|
257
|
+
this.cache.dataExtension[item.CustomerKey];
|
|
258
|
+
if (isUpdate) {
|
|
259
|
+
const cachedVersion = this.cache.dataExtension[item.CustomerKey];
|
|
260
|
+
// restore retention values that are typically not returned by the update call
|
|
261
|
+
item.RowBasedRetention = cachedVersion.RowBasedRetention;
|
|
262
|
+
item.ResetRetentionPeriodOnImport = cachedVersion.ResetRetentionPeriodOnImport;
|
|
263
|
+
item.DeleteAtEndOfRetentionPeriod = cachedVersion.DeleteAtEndOfRetentionPeriod;
|
|
264
|
+
item.RetainUntil = cachedVersion.RetainUntil;
|
|
265
|
+
|
|
266
|
+
// ensure we have th
|
|
267
|
+
const existingFields = DataExtension.oldFields[item[this.definition.nameField]];
|
|
268
|
+
if (item.Fields !== '' && existingFields) {
|
|
269
|
+
// TODO should be replaced by a manual sort using existingFields
|
|
270
|
+
// ! this is inefficient because it triggers a new download of the fields during the saveResults() step
|
|
271
|
+
item.Fields.length = 0;
|
|
272
|
+
}
|
|
273
|
+
// sort Fields entry to the end of the object for saving in .json
|
|
274
|
+
const fieldsBackup = item.Fields;
|
|
275
|
+
delete item.Fields;
|
|
276
|
+
item.Fields = fieldsBackup;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
298
280
|
|
|
299
281
|
/**
|
|
300
282
|
* Retrieves dataExtension metadata. Afterwards starts retrieval of dataExtensionColumn metadata retrieval
|
|
301
283
|
* @param {string} retrieveDir Directory where retrieved metadata directory will be saved
|
|
302
284
|
* @param {string[]} [additionalFields] Returns specified fields even if their retrieve definition is not set to true
|
|
303
285
|
* @param {Util.BuObject} buObject properties for auth
|
|
286
|
+
* @param {void} [_] -
|
|
287
|
+
* @param {boolean} [isDeploy] used to signal that fields shall be retrieve in caching mode
|
|
304
288
|
* @returns {Promise<{metadata:DataExtensionMap,type:string}>} Promise of item map
|
|
305
289
|
*/
|
|
306
|
-
static async retrieve(retrieveDir, additionalFields, buObject) {
|
|
290
|
+
static async retrieve(retrieveDir, additionalFields, buObject, _, isDeploy) {
|
|
307
291
|
let metadata = await this._retrieveAll(additionalFields);
|
|
308
292
|
// in case of cache dont get fields
|
|
309
|
-
if (metadata && retrieveDir) {
|
|
293
|
+
if (isDeploy || (metadata && retrieveDir)) {
|
|
310
294
|
// get fields from API
|
|
311
|
-
const fieldsObj = await this._retrieveFields(additionalFields);
|
|
295
|
+
const fieldsObj = await this._retrieveFields(null, additionalFields);
|
|
312
296
|
const fieldKeys = Object.keys(fieldsObj);
|
|
313
297
|
// add fields to corresponding DE
|
|
314
298
|
fieldKeys.forEach((key) => {
|
|
315
299
|
const field = fieldsObj[key];
|
|
316
|
-
metadata[field
|
|
300
|
+
if (metadata[field?.DataExtension?.CustomerKey]) {
|
|
301
|
+
metadata[field.DataExtension.CustomerKey].Fields.push(field);
|
|
302
|
+
} else {
|
|
303
|
+
Util.logger.warn(`Issue retrieving data extension fields. key='${key}'`);
|
|
304
|
+
}
|
|
317
305
|
});
|
|
318
306
|
|
|
319
307
|
// sort fields by Ordinal value (API returns field unsorted)
|
|
@@ -336,9 +324,7 @@ class DataExtension extends MetadataType {
|
|
|
336
324
|
clientSecret: this.properties.credentials[buObject.credential].clientSecret,
|
|
337
325
|
tenant: this.properties.credentials[buObject.credential].tenant,
|
|
338
326
|
eid: this.properties.credentials[buObject.credential].eid,
|
|
339
|
-
mid: this.properties.credentials[buObject.credential].
|
|
340
|
-
Util.parentBuName
|
|
341
|
-
],
|
|
327
|
+
mid: this.properties.credentials[buObject.credential].eid,
|
|
342
328
|
businessUnit: Util.parentBuName,
|
|
343
329
|
credential: buObject.credential,
|
|
344
330
|
};
|
|
@@ -351,6 +337,16 @@ class DataExtension extends MetadataType {
|
|
|
351
337
|
}
|
|
352
338
|
const metadataParentBu = await this._retrieveAll(additionalFields);
|
|
353
339
|
|
|
340
|
+
// get shared folders to match our shared / synched Data Extensions
|
|
341
|
+
Util.logger.info('- Caching dependent Metadata: folder (shared via _ParentBU_)');
|
|
342
|
+
Folder.cache = {};
|
|
343
|
+
Folder.client = this.client;
|
|
344
|
+
Folder.properties = this.properties;
|
|
345
|
+
const result = await Folder.retrieveForCache(buObjectParentBu);
|
|
346
|
+
const parentCache = {
|
|
347
|
+
folder: result.metadata,
|
|
348
|
+
};
|
|
349
|
+
|
|
354
350
|
// get the types and clean out non-shared ones
|
|
355
351
|
const folderTypesFromParent = require('../MetadataTypeDefinitions').folder
|
|
356
352
|
.folderTypesFromParent;
|
|
@@ -358,22 +354,30 @@ class DataExtension extends MetadataType {
|
|
|
358
354
|
try {
|
|
359
355
|
// get the data extension type from the folder
|
|
360
356
|
const folderContentType = Util.getFromCache(
|
|
361
|
-
|
|
357
|
+
parentCache,
|
|
362
358
|
'folder',
|
|
363
359
|
metadataParentBu[metadataEntry].CategoryID,
|
|
364
360
|
'ID',
|
|
365
361
|
'ContentType'
|
|
366
362
|
);
|
|
367
363
|
if (!folderTypesFromParent.includes(folderContentType)) {
|
|
364
|
+
Util.logger.verbose(
|
|
365
|
+
`removing ${metadataEntry} because r__folder_ContentType '${folderContentType}' identifies this DE as not being shared`
|
|
366
|
+
);
|
|
368
367
|
delete metadataParentBu[metadataEntry];
|
|
369
368
|
}
|
|
370
369
|
} catch (ex) {
|
|
370
|
+
Util.logger.debug(
|
|
371
|
+
`removing ${metadataEntry} because of error while retrieving r__folder_ContentType: ${ex.message}`
|
|
372
|
+
);
|
|
371
373
|
delete metadataParentBu[metadataEntry];
|
|
372
374
|
}
|
|
373
375
|
}
|
|
374
376
|
|
|
375
377
|
// revert client to current default
|
|
376
378
|
this.client = clientBackup;
|
|
379
|
+
Folder.client = clientBackup;
|
|
380
|
+
Folder.cache = this.cache;
|
|
377
381
|
|
|
378
382
|
// make sure to overwrite parent bu DEs with local ones
|
|
379
383
|
metadata = { ...metadataParentBu, ...metadata };
|
|
@@ -390,6 +394,15 @@ class DataExtension extends MetadataType {
|
|
|
390
394
|
}
|
|
391
395
|
return { metadata: metadata, type: 'dataExtension' };
|
|
392
396
|
}
|
|
397
|
+
/**
|
|
398
|
+
* Retrieves dataExtension metadata. Afterwards starts retrieval of dataExtensionColumn metadata retrieval
|
|
399
|
+
* @param {string[]} [additionalFields] Returns specified fields even if their retrieve definition is not set to true
|
|
400
|
+
* @returns {Promise<{metadata:DataExtensionMap,type:string}>} Promise of item map
|
|
401
|
+
*/
|
|
402
|
+
static async retrieveChangelog(additionalFields) {
|
|
403
|
+
const metadata = await this._retrieveAll(additionalFields);
|
|
404
|
+
return { metadata: metadata, type: 'dataExtension' };
|
|
405
|
+
}
|
|
393
406
|
/**
|
|
394
407
|
* manages post retrieve steps
|
|
395
408
|
* @param {DataExtensionItem} metadata a single dataExtension
|
|
@@ -410,17 +423,25 @@ class DataExtension extends MetadataType {
|
|
|
410
423
|
'Ensure that Queries that write into this DE are updated with the new key before deployment.';
|
|
411
424
|
this.overrideKeyWithName(metadata, warningMsg);
|
|
412
425
|
}
|
|
426
|
+
// Error during deploy if SendableSubscriberField.Name = '_SubscriberKey' even though it is retrieved like that
|
|
427
|
+
// Therefore map it to 'Subscriber Key'. Retrieving afterward still results in '_SubscriberKey'
|
|
428
|
+
if (
|
|
429
|
+
metadata.SendableSubscriberField &&
|
|
430
|
+
metadata.SendableSubscriberField.Name === '_SubscriberKey'
|
|
431
|
+
) {
|
|
432
|
+
metadata.SendableSubscriberField.Name = 'Subscriber Key';
|
|
433
|
+
}
|
|
413
434
|
return this._parseMetadata(JSON.parse(JSON.stringify(metadata)));
|
|
414
435
|
}
|
|
415
436
|
|
|
416
437
|
/**
|
|
417
438
|
* Helper to retrieve Data Extension Fields
|
|
418
439
|
* @private
|
|
419
|
-
* @param {string[]} [additionalFields] Returns specified fields even if their retrieve definition is not set to true
|
|
420
440
|
* @param {Object} [options] options (e.g. continueRequest)
|
|
441
|
+
* @param {string[]} [additionalFields] Returns specified fields even if their retrieve definition is not set to true
|
|
421
442
|
* @returns {Promise<DataExtensionField.DataExtensionFieldMap>} Promise of items
|
|
422
443
|
*/
|
|
423
|
-
static async _retrieveFields(
|
|
444
|
+
static async _retrieveFields(options, additionalFields) {
|
|
424
445
|
if (!options) {
|
|
425
446
|
// dont print this during updates or templating which retrieves fields DE-by-DE
|
|
426
447
|
Util.logger.info('- Caching dependent Metadata: dataExtensionField');
|
|
@@ -447,7 +468,7 @@ class DataExtension extends MetadataType {
|
|
|
447
468
|
rightOperand: customerKey,
|
|
448
469
|
},
|
|
449
470
|
};
|
|
450
|
-
const fieldsObj = await this._retrieveFields(
|
|
471
|
+
const fieldsObj = await this._retrieveFields(fieldOptions);
|
|
451
472
|
|
|
452
473
|
DataExtensionField.cache = this.metadata;
|
|
453
474
|
DataExtensionField.client = this.client;
|
|
@@ -524,6 +545,16 @@ class DataExtension extends MetadataType {
|
|
|
524
545
|
// contenttype
|
|
525
546
|
delete metadata.r__folder_ContentType;
|
|
526
547
|
|
|
548
|
+
// Error if SendableSubscriberField.Name = '_SubscriberKey' even though it is retrieved like that
|
|
549
|
+
// Therefore map it to 'Subscriber Key'. Retrieving afterward still results in '_SubscriberKey'
|
|
550
|
+
// TODO remove from preDeploy with release of version 4, keep until then to help with migration of old metadata
|
|
551
|
+
if (
|
|
552
|
+
metadata.SendableSubscriberField &&
|
|
553
|
+
metadata.SendableSubscriberField.Name === '_SubscriberKey'
|
|
554
|
+
) {
|
|
555
|
+
metadata.SendableSubscriberField.Name = 'Subscriber Key';
|
|
556
|
+
}
|
|
557
|
+
|
|
527
558
|
return metadata;
|
|
528
559
|
}
|
|
529
560
|
|
|
@@ -826,19 +857,21 @@ class DataExtension extends MetadataType {
|
|
|
826
857
|
/**
|
|
827
858
|
* Retrieves folder metadata into local filesystem. Also creates a uniquePath attribute for each folder.
|
|
828
859
|
* @param {Object} buObject properties for auth
|
|
860
|
+
* @param {void} [_] -
|
|
861
|
+
* @param {boolean} [isDeploy] used to signal that fields shall be retrieve in caching mode
|
|
829
862
|
* @returns {Promise} Promise
|
|
830
863
|
*/
|
|
831
|
-
static async retrieveForCache(buObject) {
|
|
832
|
-
return this.retrieve(null, ['ObjectID', 'CustomerKey', 'Name'], buObject);
|
|
864
|
+
static async retrieveForCache(buObject, _, isDeploy) {
|
|
865
|
+
return this.retrieve(null, ['ObjectID', 'CustomerKey', 'Name'], buObject, null, isDeploy);
|
|
833
866
|
}
|
|
834
867
|
/**
|
|
835
868
|
* Retrieves dataExtension metadata in template format.
|
|
836
869
|
* @param {string} templateDir Directory where retrieved metadata directory will be saved
|
|
837
870
|
* @param {string} name name of the metadata item
|
|
838
|
-
* @param {Util.TemplateMap}
|
|
871
|
+
* @param {Util.TemplateMap} templateVariables variables to be replaced in the metadata
|
|
839
872
|
* @returns {Promise<{metadata:DataExtensionMap,type:string}>} Promise of items
|
|
840
873
|
*/
|
|
841
|
-
static async retrieveAsTemplate(templateDir, name,
|
|
874
|
+
static async retrieveAsTemplate(templateDir, name, templateVariables) {
|
|
842
875
|
const options = {
|
|
843
876
|
filter: {
|
|
844
877
|
leftOperand: 'Name',
|
|
@@ -863,20 +896,20 @@ class DataExtension extends MetadataType {
|
|
|
863
896
|
// API returns field unsorted
|
|
864
897
|
metadata[key].Fields.sort((a, b) => a.Ordinal - b.Ordinal);
|
|
865
898
|
|
|
899
|
+
const originalKey = key;
|
|
866
900
|
const metadataCleaned = JSON.parse(
|
|
867
|
-
JSON.stringify(
|
|
901
|
+
JSON.stringify(
|
|
902
|
+
await this.postRetrieveTasks(metadata[key], null, !!templateVariables)
|
|
903
|
+
)
|
|
868
904
|
);
|
|
869
905
|
|
|
870
906
|
this.keepTemplateFields(metadataCleaned);
|
|
871
907
|
const metadataTemplated = JSON.parse(
|
|
872
|
-
Util.replaceByObject(JSON.stringify(metadataCleaned),
|
|
908
|
+
Util.replaceByObject(JSON.stringify(metadataCleaned), templateVariables)
|
|
873
909
|
);
|
|
874
910
|
File.writeJSONToFile(
|
|
875
911
|
[templateDir, this.definition.type].join('/'),
|
|
876
|
-
|
|
877
|
-
'.' +
|
|
878
|
-
this.definition.type +
|
|
879
|
-
'-meta',
|
|
912
|
+
originalKey + '.' + this.definition.type + '-meta',
|
|
880
913
|
metadataTemplated
|
|
881
914
|
);
|
|
882
915
|
} catch (ex) {
|
|
@@ -993,6 +1026,9 @@ class DataExtension extends MetadataType {
|
|
|
993
1026
|
|
|
994
1027
|
// Assign definition to static attributes
|
|
995
1028
|
DataExtension.definition = require('../MetadataTypeDefinitions').dataExtension;
|
|
1029
|
+
/**
|
|
1030
|
+
* @type {Util.ET_Client}
|
|
1031
|
+
*/
|
|
996
1032
|
DataExtension.client = undefined;
|
|
997
1033
|
DataExtension.cache = {};
|
|
998
1034
|
|