mcdev 3.0.3 → 3.1.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/.eslintrc.json +1 -1
- package/.github/ISSUE_TEMPLATE/bug.yml +72 -0
- package/.issuetracker +11 -3
- package/.vscode/settings.json +3 -3
- package/CHANGELOG.md +53 -0
- package/README.md +245 -141
- package/boilerplate/config.json +3 -2
- package/docs/dist/documentation.md +783 -322
- 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 +295 -0
- package/lib/index.js +774 -1019
- package/lib/metadataTypes/AccountUser.js +389 -0
- package/lib/metadataTypes/Asset.js +8 -7
- package/lib/metadataTypes/Automation.js +115 -52
- package/lib/metadataTypes/DataExtension.js +125 -89
- package/lib/metadataTypes/DataExtensionField.js +134 -4
- package/lib/metadataTypes/ImportFile.js +4 -6
- package/lib/metadataTypes/MetadataType.js +19 -5
- package/lib/metadataTypes/Query.js +2 -3
- package/lib/metadataTypes/Role.js +13 -8
- 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 +17 -21
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -30
- package/img/README.md/troubleshoot-nodejs-postinstall.jpg +0 -0
- package/postinstall.js +0 -41
|
@@ -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,10 +857,12 @@ 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.
|
|
@@ -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
|
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const Util = require('../util/util');
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* @typedef {Object} DataExtensionFieldItem
|
|
5
7
|
* @property {string} [ObjectID] id
|
|
6
|
-
* @property {string} [CustomerKey] key
|
|
8
|
+
* @property {string} [CustomerKey] key in format [DEkey].[FieldName]
|
|
7
9
|
* @property {Object} [DataExtension] -
|
|
8
10
|
* @property {string} DataExtension.CustomerKey key of DE
|
|
9
|
-
* @property {string} Name name
|
|
10
|
-
* @property {string}
|
|
11
|
+
* @property {string} Name name of field
|
|
12
|
+
* @property {string} [Name_new] custom attribute that is only used when trying to rename a field from Name to Name_new
|
|
13
|
+
* @property {string} DefaultValue empty string for not set
|
|
11
14
|
* @property {'true'|'false'} IsRequired -
|
|
12
15
|
* @property {'true'|'false'} IsPrimaryKey -
|
|
13
16
|
* @property {string} Ordinal 1, 2, 3, ...
|
|
14
|
-
* @property {'Text'|'Date'|'
|
|
17
|
+
* @property {'Text'|'Number'|'Date'|'Boolean'|'Decimal'|'EmailAddress'|'Phone'|'Locale'} FieldType can only be set on create
|
|
18
|
+
* @property {string} Scale the number of places after the decimal that the field can hold; example: "0","1", ...
|
|
15
19
|
*
|
|
16
20
|
* @typedef {Object.<string, DataExtensionFieldItem>} DataExtensionFieldMap
|
|
17
21
|
*/
|
|
@@ -91,9 +95,135 @@ class DataExtensionField extends MetadataType {
|
|
|
91
95
|
delete metadata.CustomerKey;
|
|
92
96
|
delete metadata.DataExtension;
|
|
93
97
|
delete metadata.Ordinal;
|
|
98
|
+
if (metadata.FieldType !== 'Decimal') {
|
|
99
|
+
// remove scale - it's only used for "Decimal" to define the digits behind the decimal
|
|
100
|
+
delete metadata.Scale;
|
|
101
|
+
}
|
|
94
102
|
}
|
|
95
103
|
return metadata;
|
|
96
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Mofifies passed deployColumns for update by mapping ObjectID to their target column's values.
|
|
107
|
+
* 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
|
|
108
|
+
*
|
|
109
|
+
* @param {DataExtensionFieldItem[]} deployColumns Columns of data extension that will be deployed
|
|
110
|
+
* @param {string} deKey external/customer key of Data Extension
|
|
111
|
+
* @returns {Object<string,DataExtensionFieldItem>} existing fields by their original name to allow re-adding FieldType after update
|
|
112
|
+
*/
|
|
113
|
+
static async prepareDeployColumnsOnUpdate(deployColumns, deKey) {
|
|
114
|
+
// retrieve existing fields to enable updating them
|
|
115
|
+
const response = await this.retrieveForCache(
|
|
116
|
+
{
|
|
117
|
+
filter: {
|
|
118
|
+
leftOperand: 'DataExtension.CustomerKey',
|
|
119
|
+
operator: 'equals',
|
|
120
|
+
rightOperand: deKey,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
['Name', 'ObjectID']
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const fieldsObj = response.metadata;
|
|
127
|
+
|
|
128
|
+
// ensure fields can be updated properly by their adding ObjectId based on Name-matching
|
|
129
|
+
/** @type {Object<string,DataExtensionFieldItem>} */
|
|
130
|
+
const existingFieldByName = {};
|
|
131
|
+
|
|
132
|
+
Object.keys(fieldsObj).forEach((key) => {
|
|
133
|
+
existingFieldByName[fieldsObj[key].Name] = fieldsObj[key];
|
|
134
|
+
});
|
|
135
|
+
for (let i = deployColumns.length - 1; i >= 0; i--) {
|
|
136
|
+
const item = deployColumns[i];
|
|
137
|
+
const itemOld = existingFieldByName[item.Name];
|
|
138
|
+
if (itemOld) {
|
|
139
|
+
// field is getting updated ---
|
|
140
|
+
|
|
141
|
+
// Updating to a new FieldType will result in an error; warn & afterwards remove it
|
|
142
|
+
if (itemOld.FieldType !== item.FieldType) {
|
|
143
|
+
Util.logger.warn(
|
|
144
|
+
`- The Field Type of an existing field cannot be changed. Keeping the original value for [${deKey}].[${item.Name}]: '${itemOld.FieldType}'`
|
|
145
|
+
);
|
|
146
|
+
item.FieldType = itemOld.FieldType;
|
|
147
|
+
}
|
|
148
|
+
if (item.FieldType !== 'Decimal') {
|
|
149
|
+
// remove scale - it's only used for "Decimal" to define the digits behind the decimal
|
|
150
|
+
delete item.Scale;
|
|
151
|
+
}
|
|
152
|
+
delete item.FieldType;
|
|
153
|
+
|
|
154
|
+
if (itemOld.MaxLength > item.MaxLength) {
|
|
155
|
+
Util.logger.warn(
|
|
156
|
+
`- The length of an existing field cannot be decreased. Keeping the original value for [${deKey}].[${item.Name}]: '${itemOld.MaxLength}'`
|
|
157
|
+
);
|
|
158
|
+
item.MaxLength = itemOld.MaxLength;
|
|
159
|
+
}
|
|
160
|
+
if (Util.isFalse(itemOld.IsRequired) && Util.isTrue(item.IsRequired)) {
|
|
161
|
+
Util.logger.warn(
|
|
162
|
+
`- A field cannot be changed to be required on update after it was created to allow nulls: [${deKey}].[${item.Name}]`
|
|
163
|
+
);
|
|
164
|
+
item.IsRequired = itemOld.IsRequired;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// enable renaming
|
|
168
|
+
if (item.Name_new) {
|
|
169
|
+
item.Name = item.Name_new;
|
|
170
|
+
delete item.Name_new;
|
|
171
|
+
Util.logger.warn(
|
|
172
|
+
`Found 'Name_new' value '${item.Name_new}' for ${deKey}.${item.Name} - trying to rename.`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// check if any changes were found
|
|
177
|
+
let changeFound = false;
|
|
178
|
+
Object.keys(item).forEach((key) => {
|
|
179
|
+
if (item[key] !== itemOld[key]) {
|
|
180
|
+
changeFound = true;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
if (!changeFound) {
|
|
184
|
+
deployColumns.splice(i, 1);
|
|
185
|
+
Util.logger.verbose(`no change - removed field [${deKey}].[${item.Name}]`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// set the ObjectId for clear identification during update
|
|
190
|
+
item.ObjectID = itemOld.ObjectID;
|
|
191
|
+
} else {
|
|
192
|
+
// field is getting added ---
|
|
193
|
+
if (Util.isTrue(item.IsRequired) && item.DefaultValue === '') {
|
|
194
|
+
Util.logger.warn(
|
|
195
|
+
`- Adding new fields to an existing table requires that these fields are either not-required (nullable) or have a default value set. Changing [${deKey}].[${item.Name}] to be not-required`
|
|
196
|
+
);
|
|
197
|
+
item.IsRequired = 'false';
|
|
198
|
+
}
|
|
199
|
+
if (item.Name_new) {
|
|
200
|
+
Util.logger.warn(
|
|
201
|
+
`Found 'Name_new' value '${item.Name_new}' for ${deKey}.${item.Name} but could not find a corresponding DE field on the server - adding new field instead of updating.`
|
|
202
|
+
);
|
|
203
|
+
delete item.Name_new;
|
|
204
|
+
}
|
|
205
|
+
// Field doesn't exist in target, therefore Remove ObjectID if present
|
|
206
|
+
delete item.ObjectID;
|
|
207
|
+
}
|
|
208
|
+
if (!Util.isTrue(item.IsRequired) && !Util.isFalse(item.IsRequired)) {
|
|
209
|
+
Util.logger.error(
|
|
210
|
+
`- Invalid value for 'IsRequired' of [${deKey}].[${item.Name}]. Found '${item.IsRequired}' instead of 'true'/'false'. Removing field from deploy!`
|
|
211
|
+
);
|
|
212
|
+
deployColumns.splice(i, 1);
|
|
213
|
+
}
|
|
214
|
+
if (!Util.isTrue(item.IsPrimaryKey) && !Util.isFalse(item.IsPrimaryKey)) {
|
|
215
|
+
Util.logger.error(
|
|
216
|
+
`- Invalid value for 'IsPrimaryKey' of [${deKey}].[${item.Name}]. Found '${item.IsPrimaryKey}' instead of 'true'/'false'. Removing field from deploy!`
|
|
217
|
+
);
|
|
218
|
+
deployColumns.splice(i, 1);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
Util.logger.debug(
|
|
222
|
+
`${deployColumns.length} Fields added/updated for [${deKey}]: ` +
|
|
223
|
+
deployColumns.map((item) => item.Name).join(', ')
|
|
224
|
+
);
|
|
225
|
+
return deployColumns.length ? existingFieldByName : null;
|
|
226
|
+
}
|
|
97
227
|
}
|
|
98
228
|
|
|
99
229
|
// Assign definition to static attributes
|
|
@@ -147,12 +147,10 @@ class ImportFile extends MetadataType {
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
// When the destinationObjectTypeId is 584 is refers to Mobile Connect which is not supported as an Import Type
|
|
150
|
-
metadata.destinationObjectTypeId =
|
|
151
|
-
metadata.c__destinationType
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
metadata.c__subscriberImportType
|
|
155
|
-
];
|
|
150
|
+
metadata.destinationObjectTypeId =
|
|
151
|
+
this.definition.destinationObjectTypeMapping[metadata.c__destinationType];
|
|
152
|
+
metadata.subscriberImportTypeId =
|
|
153
|
+
this.definition.subscriberImportTypeMapping[metadata.c__subscriberImportType];
|
|
156
154
|
metadata.updateTypeId = this.definition.updateTypeMapping[metadata.c__dataAction];
|
|
157
155
|
return metadata;
|
|
158
156
|
}
|
|
@@ -204,6 +204,16 @@ class MetadataType {
|
|
|
204
204
|
const metadata = {};
|
|
205
205
|
return { metadata: null, type: this.definition.type };
|
|
206
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Gets metadata from Marketing Cloud
|
|
209
|
+
* @param {string[]} [additionalFields] Returns specified fields even if their retrieve definition is not set to true
|
|
210
|
+
* @param {Util.BuObject} buObject properties for auth
|
|
211
|
+
* @param {string} [subType] optionally limit to a single subtype
|
|
212
|
+
* @returns {Promise<{metadata:MetadataTypeMap,type:string}>} metadata
|
|
213
|
+
*/
|
|
214
|
+
static retrieveChangelog(additionalFields, buObject, subType) {
|
|
215
|
+
return this.retrieve(null, additionalFields, buObject, subType);
|
|
216
|
+
}
|
|
207
217
|
|
|
208
218
|
/**
|
|
209
219
|
* Gets metadata cache with limited fields and does not store value to disk
|
|
@@ -304,9 +314,10 @@ class MetadataType {
|
|
|
304
314
|
});
|
|
305
315
|
} else if (this.cache[this.definition.type][normalizedKey]) {
|
|
306
316
|
// normal way of processing update files
|
|
307
|
-
metadata[metadataKey][this.definition.idField] =
|
|
308
|
-
this.definition.type
|
|
309
|
-
|
|
317
|
+
metadata[metadataKey][this.definition.idField] =
|
|
318
|
+
this.cache[this.definition.type][normalizedKey][
|
|
319
|
+
this.definition.idField
|
|
320
|
+
];
|
|
310
321
|
metadataToUpdate.push({
|
|
311
322
|
before: this.cache[this.definition.type][normalizedKey],
|
|
312
323
|
after: metadata[metadataKey],
|
|
@@ -474,8 +485,12 @@ class MetadataType {
|
|
|
474
485
|
async () => (response = await this.client.RestClient.patch(options))
|
|
475
486
|
);
|
|
476
487
|
this.checkForErrors(response);
|
|
488
|
+
// some times, e.g. automation dont return a key in their update response and hence we need to fall back to name
|
|
477
489
|
Util.logger.info(
|
|
478
|
-
`- updated ${this.definition.type}: ${
|
|
490
|
+
`- updated ${this.definition.type}: ${
|
|
491
|
+
metadataEntry[this.definition.keyField] ||
|
|
492
|
+
metadataEntry[this.definition.nameField]
|
|
493
|
+
}`
|
|
479
494
|
);
|
|
480
495
|
return response;
|
|
481
496
|
} catch (ex) {
|
|
@@ -565,7 +580,6 @@ class MetadataType {
|
|
|
565
580
|
Util.logger.info(
|
|
566
581
|
`Downloaded: ${this.definition.type} (${Object.keys(savedMetadata).length})`
|
|
567
582
|
);
|
|
568
|
-
|
|
569
583
|
if (
|
|
570
584
|
buObject &&
|
|
571
585
|
this.properties.metaDataTypes.documentOnRetrieve.includes(this.definition.type)
|
|
@@ -144,9 +144,8 @@ class Query extends MetadataType {
|
|
|
144
144
|
} catch (ex) {
|
|
145
145
|
throw new Error(`Query '${metadata.key}': ${ex.message}`);
|
|
146
146
|
}
|
|
147
|
-
metadata.targetUpdateTypeId =
|
|
148
|
-
metadata.targetUpdateTypeName
|
|
149
|
-
];
|
|
147
|
+
metadata.targetUpdateTypeId =
|
|
148
|
+
this.definition.targetUpdateTypeMapping[metadata.targetUpdateTypeName];
|
|
150
149
|
return metadata;
|
|
151
150
|
}
|
|
152
151
|
|
|
@@ -107,14 +107,19 @@ class Role extends MetadataType {
|
|
|
107
107
|
return;
|
|
108
108
|
}
|
|
109
109
|
if (!metadata) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
110
|
+
try {
|
|
111
|
+
metadata = this.readBUMetadataForType(
|
|
112
|
+
File.normalizePath([
|
|
113
|
+
this.properties.directories.retrieve,
|
|
114
|
+
buObject.credential,
|
|
115
|
+
Util.parentBuName,
|
|
116
|
+
]),
|
|
117
|
+
true
|
|
118
|
+
).role;
|
|
119
|
+
} catch (ex) {
|
|
120
|
+
Util.logger.error(ex.message);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
118
123
|
}
|
|
119
124
|
const directory = this.properties.directories.roles;
|
|
120
125
|
|