mcdev 3.0.1 → 3.1.1
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/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/.github/ISSUE_TEMPLATE/task.md +10 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +11 -0
- package/.issuetracker +11 -3
- package/.vscode/settings.json +3 -3
- package/CHANGELOG.md +103 -1
- package/README.md +245 -141
- package/boilerplate/config.json +3 -2
- package/docs/dist/documentation.md +803 -337
- package/lib/Deployer.js +4 -1
- package/lib/MetadataTypeDefinitions.js +1 -0
- package/lib/MetadataTypeInfo.js +1 -0
- package/lib/Retriever.js +32 -17
- package/lib/cli.js +295 -0
- package/lib/index.js +774 -1019
- package/lib/metadataTypes/AccountUser.js +389 -0
- package/lib/metadataTypes/Asset.js +20 -12
- package/lib/metadataTypes/Automation.js +115 -52
- package/lib/metadataTypes/DataExtension.js +159 -113
- package/lib/metadataTypes/DataExtensionField.js +134 -4
- package/lib/metadataTypes/Folder.js +66 -69
- package/lib/metadataTypes/ImportFile.js +4 -6
- package/lib/metadataTypes/MetadataType.js +136 -67
- 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/Campaign.definition.js +1 -1
- package/lib/metadataTypes/definitions/DataExtension.definition.js +1 -1
- package/lib/metadataTypes/definitions/DataExtensionField.definition.js +1 -1
- package/lib/metadataTypes/definitions/Folder.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 +31 -15
- package/package.json +19 -23
- 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[]} */
|
|
@@ -60,11 +61,20 @@ class DataExtension extends MetadataType {
|
|
|
60
61
|
}
|
|
61
62
|
if (
|
|
62
63
|
buObject.eid !== buObject.mid &&
|
|
63
|
-
desToDeploy[dataExtension].r__folder_Path.startsWith('
|
|
64
|
+
desToDeploy[dataExtension].r__folder_Path.startsWith('Shared Items')
|
|
64
65
|
) {
|
|
65
66
|
// this needs to be run before executing preDeployTasks()
|
|
66
67
|
Util.logger.warn(
|
|
67
|
-
`- Cannot Create/Update a Shared Data
|
|
68
|
+
`- Cannot Create/Update a Shared Data Extension from the Child BU - skipping ${desToDeploy[dataExtension].Name}`
|
|
69
|
+
);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (
|
|
73
|
+
desToDeploy[dataExtension].r__folder_Path.startsWith('Synchronized Data Extensions')
|
|
74
|
+
) {
|
|
75
|
+
// this needs to be run before executing preDeployTasks()
|
|
76
|
+
Util.logger.warn(
|
|
77
|
+
`- Cannot Create/Update a Synchronized Data Extension. Please use Contact Builder to maintain these - skipping ${desToDeploy[dataExtension].Name}`
|
|
68
78
|
);
|
|
69
79
|
continue;
|
|
70
80
|
}
|
|
@@ -80,21 +90,7 @@ class DataExtension extends MetadataType {
|
|
|
80
90
|
// skip rest of handling for this DE
|
|
81
91
|
continue;
|
|
82
92
|
}
|
|
83
|
-
// Error if SendableSubscriberField.Name = '_SubscriberKey' even though it is retrieved like that
|
|
84
|
-
// Therefore map it to 'Subscriber Key'. Retrieving afterward still results in '_SubscriberKey'
|
|
85
|
-
if (
|
|
86
|
-
desToDeploy[dataExtension].SendableSubscriberField &&
|
|
87
|
-
desToDeploy[dataExtension].SendableSubscriberField.Name === '_SubscriberKey'
|
|
88
|
-
) {
|
|
89
|
-
desToDeploy[dataExtension].SendableSubscriberField.Name = 'Subscriber Key';
|
|
90
|
-
}
|
|
91
93
|
if (targetMetadata[dataExtension]) {
|
|
92
|
-
// Update dataExtension + Columns if they already exist; Create them if not
|
|
93
|
-
// Modify columns for update call
|
|
94
|
-
DataExtension.prepareDeployColumnsOnUpdate(
|
|
95
|
-
desToDeploy[dataExtension].Fields,
|
|
96
|
-
targetMetadata[dataExtension].Fields
|
|
97
|
-
);
|
|
98
94
|
// data extension already exists in target and needs to be updated
|
|
99
95
|
deUpdatePromises.push(DataExtension.update(desToDeploy[dataExtension]));
|
|
100
96
|
} else {
|
|
@@ -104,7 +100,7 @@ class DataExtension extends MetadataType {
|
|
|
104
100
|
}
|
|
105
101
|
if (deUpdatePromises.length) {
|
|
106
102
|
Util.logger.info(
|
|
107
|
-
'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.'
|
|
108
104
|
);
|
|
109
105
|
}
|
|
110
106
|
|
|
@@ -114,6 +110,7 @@ class DataExtension extends MetadataType {
|
|
|
114
110
|
const updateResults = (await Promise.allSettled(deUpdatePromises)).filter(
|
|
115
111
|
this._filterUpsertResults
|
|
116
112
|
);
|
|
113
|
+
|
|
117
114
|
const successfulResults = [...createResults, ...updateResults];
|
|
118
115
|
|
|
119
116
|
Util.metadataLogger(
|
|
@@ -151,6 +148,10 @@ class DataExtension extends MetadataType {
|
|
|
151
148
|
// promise rejects, whole request failed
|
|
152
149
|
Util.logger.error('- error upserting dataExtension: ' + res.reason);
|
|
153
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;
|
|
154
155
|
} else if (res.value.results) {
|
|
155
156
|
Util.logger.error(
|
|
156
157
|
'- error upserting dataExtension: ' +
|
|
@@ -168,65 +169,24 @@ class DataExtension extends MetadataType {
|
|
|
168
169
|
}
|
|
169
170
|
}
|
|
170
171
|
|
|
171
|
-
/**
|
|
172
|
-
* Mofifies passed deployColumns for update by mapping ObjectID to their target column's values.
|
|
173
|
-
* 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
|
|
174
|
-
*
|
|
175
|
-
* @param {DataExtensionField.DataExtensionFieldItem[]} deployColumns Columns of data extension that will be deployed
|
|
176
|
-
* @param {DataExtensionField.DataExtensionFieldItem[]} targetColumns Columns of data extension that currently exists in target
|
|
177
|
-
* @returns {void}
|
|
178
|
-
*/
|
|
179
|
-
static prepareDeployColumnsOnUpdate(deployColumns, targetColumns) {
|
|
180
|
-
// Map data extension column ObjectIDs to their target, because they are environment specific
|
|
181
|
-
for (const column in deployColumns) {
|
|
182
|
-
const deployColumn = deployColumns[column];
|
|
183
|
-
// Check if column exists in target
|
|
184
|
-
if (targetColumns[column]) {
|
|
185
|
-
// Map ObjectID to value of target, because DataExtensionField updates are based on ObjectID
|
|
186
|
-
deployColumn.ObjectID = targetColumns[column].ObjectID;
|
|
187
|
-
// 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)
|
|
188
|
-
if (targetColumns[column].FieldType === deployColumn.FieldType) {
|
|
189
|
-
delete deployColumn.FieldType;
|
|
190
|
-
} else {
|
|
191
|
-
// Updating to a new FieldType will result in an error
|
|
192
|
-
Util.logger.warn(
|
|
193
|
-
'DataExtension.prepareDeployColumnsOnUpdate:: Cannot update FieldType of field: ' +
|
|
194
|
-
deployColumn.CustomerKey
|
|
195
|
-
);
|
|
196
|
-
}
|
|
197
|
-
} else {
|
|
198
|
-
// Field doesn't exist in target, therefore Remove ObjectID if present
|
|
199
|
-
delete deployColumn.ObjectID;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
172
|
/**
|
|
205
173
|
* Create a single dataExtension. Also creates their columns in 'dataExtension.columns'
|
|
206
174
|
* @param {DataExtensionItem} metadata single metadata entry
|
|
207
175
|
* @returns {Promise} Promise
|
|
208
176
|
*/
|
|
209
|
-
static create(metadata) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
Util.logger.error('- error creating dataExtension: ' + metadata.CustomerKey);
|
|
223
|
-
resolve(error);
|
|
224
|
-
} else {
|
|
225
|
-
Util.logger.info('- created dataExtension: ' + metadata.CustomerKey);
|
|
226
|
-
resolve(response);
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
});
|
|
177
|
+
static async create(metadata) {
|
|
178
|
+
this._cleanupRetentionPolicyFields(metadata);
|
|
179
|
+
|
|
180
|
+
// convert simple array into object.Array.object format to cope with how the XML body in the SOAP call needs to look like:
|
|
181
|
+
// <Fields>
|
|
182
|
+
// <Field>
|
|
183
|
+
// <CustomerKey>SubscriberKey</CustomerKey>
|
|
184
|
+
// ..
|
|
185
|
+
// </Field>
|
|
186
|
+
// </Fields>
|
|
187
|
+
metadata.Fields = { Field: metadata.Fields };
|
|
188
|
+
|
|
189
|
+
return super.createSOAP(metadata);
|
|
230
190
|
}
|
|
231
191
|
|
|
232
192
|
/**
|
|
@@ -255,35 +215,67 @@ class DataExtension extends MetadataType {
|
|
|
255
215
|
* @returns {Promise} Promise
|
|
256
216
|
*/
|
|
257
217
|
static async update(metadata) {
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
+
);
|
|
229
|
+
|
|
230
|
+
// convert simple array into object.Array.object format to cope with how the XML body in the SOAP call needs to look like:
|
|
231
|
+
// <Fields>
|
|
232
|
+
// <Field>
|
|
233
|
+
// <CustomerKey>SubscriberKey</CustomerKey>
|
|
234
|
+
// ..
|
|
235
|
+
// </Field>
|
|
236
|
+
// </Fields>
|
|
237
|
+
|
|
238
|
+
metadata.Fields = { Field: metadata.Fields };
|
|
239
|
+
return super.updateSOAP(metadata);
|
|
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;
|
|
284
272
|
}
|
|
285
|
-
|
|
286
|
-
|
|
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
|
+
}
|
|
287
279
|
}
|
|
288
280
|
|
|
289
281
|
/**
|
|
@@ -291,19 +283,25 @@ class DataExtension extends MetadataType {
|
|
|
291
283
|
* @param {string} retrieveDir Directory where retrieved metadata directory will be saved
|
|
292
284
|
* @param {string[]} [additionalFields] Returns specified fields even if their retrieve definition is not set to true
|
|
293
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
|
|
294
288
|
* @returns {Promise<{metadata:DataExtensionMap,type:string}>} Promise of item map
|
|
295
289
|
*/
|
|
296
|
-
static async retrieve(retrieveDir, additionalFields, buObject) {
|
|
290
|
+
static async retrieve(retrieveDir, additionalFields, buObject, _, isDeploy) {
|
|
297
291
|
let metadata = await this._retrieveAll(additionalFields);
|
|
298
292
|
// in case of cache dont get fields
|
|
299
|
-
if (metadata && retrieveDir) {
|
|
293
|
+
if (isDeploy || (metadata && retrieveDir)) {
|
|
300
294
|
// get fields from API
|
|
301
|
-
const fieldsObj = await this._retrieveFields(additionalFields);
|
|
295
|
+
const fieldsObj = await this._retrieveFields(null, additionalFields);
|
|
302
296
|
const fieldKeys = Object.keys(fieldsObj);
|
|
303
297
|
// add fields to corresponding DE
|
|
304
298
|
fieldKeys.forEach((key) => {
|
|
305
299
|
const field = fieldsObj[key];
|
|
306
|
-
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
|
+
}
|
|
307
305
|
});
|
|
308
306
|
|
|
309
307
|
// sort fields by Ordinal value (API returns field unsorted)
|
|
@@ -326,9 +324,7 @@ class DataExtension extends MetadataType {
|
|
|
326
324
|
clientSecret: this.properties.credentials[buObject.credential].clientSecret,
|
|
327
325
|
tenant: this.properties.credentials[buObject.credential].tenant,
|
|
328
326
|
eid: this.properties.credentials[buObject.credential].eid,
|
|
329
|
-
mid: this.properties.credentials[buObject.credential].
|
|
330
|
-
Util.parentBuName
|
|
331
|
-
],
|
|
327
|
+
mid: this.properties.credentials[buObject.credential].eid,
|
|
332
328
|
businessUnit: Util.parentBuName,
|
|
333
329
|
credential: buObject.credential,
|
|
334
330
|
};
|
|
@@ -341,6 +337,16 @@ class DataExtension extends MetadataType {
|
|
|
341
337
|
}
|
|
342
338
|
const metadataParentBu = await this._retrieveAll(additionalFields);
|
|
343
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
|
+
|
|
344
350
|
// get the types and clean out non-shared ones
|
|
345
351
|
const folderTypesFromParent = require('../MetadataTypeDefinitions').folder
|
|
346
352
|
.folderTypesFromParent;
|
|
@@ -348,22 +354,30 @@ class DataExtension extends MetadataType {
|
|
|
348
354
|
try {
|
|
349
355
|
// get the data extension type from the folder
|
|
350
356
|
const folderContentType = Util.getFromCache(
|
|
351
|
-
|
|
357
|
+
parentCache,
|
|
352
358
|
'folder',
|
|
353
359
|
metadataParentBu[metadataEntry].CategoryID,
|
|
354
360
|
'ID',
|
|
355
361
|
'ContentType'
|
|
356
362
|
);
|
|
357
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
|
+
);
|
|
358
367
|
delete metadataParentBu[metadataEntry];
|
|
359
368
|
}
|
|
360
369
|
} catch (ex) {
|
|
370
|
+
Util.logger.debug(
|
|
371
|
+
`removing ${metadataEntry} because of error while retrieving r__folder_ContentType: ${ex.message}`
|
|
372
|
+
);
|
|
361
373
|
delete metadataParentBu[metadataEntry];
|
|
362
374
|
}
|
|
363
375
|
}
|
|
364
376
|
|
|
365
377
|
// revert client to current default
|
|
366
378
|
this.client = clientBackup;
|
|
379
|
+
Folder.client = clientBackup;
|
|
380
|
+
Folder.cache = this.cache;
|
|
367
381
|
|
|
368
382
|
// make sure to overwrite parent bu DEs with local ones
|
|
369
383
|
metadata = { ...metadataParentBu, ...metadata };
|
|
@@ -380,6 +394,15 @@ class DataExtension extends MetadataType {
|
|
|
380
394
|
}
|
|
381
395
|
return { metadata: metadata, type: 'dataExtension' };
|
|
382
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
|
+
}
|
|
383
406
|
/**
|
|
384
407
|
* manages post retrieve steps
|
|
385
408
|
* @param {DataExtensionItem} metadata a single dataExtension
|
|
@@ -400,17 +423,25 @@ class DataExtension extends MetadataType {
|
|
|
400
423
|
'Ensure that Queries that write into this DE are updated with the new key before deployment.';
|
|
401
424
|
this.overrideKeyWithName(metadata, warningMsg);
|
|
402
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
|
+
}
|
|
403
434
|
return this._parseMetadata(JSON.parse(JSON.stringify(metadata)));
|
|
404
435
|
}
|
|
405
436
|
|
|
406
437
|
/**
|
|
407
438
|
* Helper to retrieve Data Extension Fields
|
|
408
439
|
* @private
|
|
409
|
-
* @param {string[]} [additionalFields] Returns specified fields even if their retrieve definition is not set to true
|
|
410
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
|
|
411
442
|
* @returns {Promise<DataExtensionField.DataExtensionFieldMap>} Promise of items
|
|
412
443
|
*/
|
|
413
|
-
static async _retrieveFields(
|
|
444
|
+
static async _retrieveFields(options, additionalFields) {
|
|
414
445
|
if (!options) {
|
|
415
446
|
// dont print this during updates or templating which retrieves fields DE-by-DE
|
|
416
447
|
Util.logger.info('- Caching dependent Metadata: dataExtensionField');
|
|
@@ -437,7 +468,7 @@ class DataExtension extends MetadataType {
|
|
|
437
468
|
rightOperand: customerKey,
|
|
438
469
|
},
|
|
439
470
|
};
|
|
440
|
-
const fieldsObj = await this._retrieveFields(
|
|
471
|
+
const fieldsObj = await this._retrieveFields(fieldOptions);
|
|
441
472
|
|
|
442
473
|
DataExtensionField.cache = this.metadata;
|
|
443
474
|
DataExtensionField.client = this.client;
|
|
@@ -514,6 +545,16 @@ class DataExtension extends MetadataType {
|
|
|
514
545
|
// contenttype
|
|
515
546
|
delete metadata.r__folder_ContentType;
|
|
516
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
|
+
|
|
517
558
|
return metadata;
|
|
518
559
|
}
|
|
519
560
|
|
|
@@ -816,10 +857,12 @@ class DataExtension extends MetadataType {
|
|
|
816
857
|
/**
|
|
817
858
|
* Retrieves folder metadata into local filesystem. Also creates a uniquePath attribute for each folder.
|
|
818
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
|
|
819
862
|
* @returns {Promise} Promise
|
|
820
863
|
*/
|
|
821
|
-
static async retrieveForCache(buObject) {
|
|
822
|
-
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);
|
|
823
866
|
}
|
|
824
867
|
/**
|
|
825
868
|
* Retrieves dataExtension metadata in template format.
|
|
@@ -983,6 +1026,9 @@ class DataExtension extends MetadataType {
|
|
|
983
1026
|
|
|
984
1027
|
// Assign definition to static attributes
|
|
985
1028
|
DataExtension.definition = require('../MetadataTypeDefinitions').dataExtension;
|
|
1029
|
+
/**
|
|
1030
|
+
* @type {Util.ET_Client}
|
|
1031
|
+
*/
|
|
986
1032
|
DataExtension.client = undefined;
|
|
987
1033
|
DataExtension.cache = {};
|
|
988
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
|