mcdev 3.0.0 → 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.
Files changed (50) hide show
  1. package/.eslintrc.json +1 -1
  2. package/.github/ISSUE_TEMPLATE/bug.yml +72 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  4. package/.github/ISSUE_TEMPLATE/task.md +10 -0
  5. package/.github/PULL_REQUEST_TEMPLATE.md +11 -0
  6. package/.github/workflows/npm-publish.yml +33 -0
  7. package/.issuetracker +11 -3
  8. package/.vscode/extensions.json +1 -2
  9. package/.vscode/settings.json +19 -4
  10. package/CHANGELOG.md +98 -0
  11. package/README.md +247 -142
  12. package/boilerplate/config.json +3 -2
  13. package/docs/dist/considerations.md +66 -0
  14. package/docs/dist/documentation.md +5794 -0
  15. package/lib/Deployer.js +4 -1
  16. package/lib/MetadataTypeDefinitions.js +1 -0
  17. package/lib/MetadataTypeInfo.js +1 -0
  18. package/lib/Retriever.js +32 -17
  19. package/lib/cli.js +295 -0
  20. package/lib/index.js +774 -1019
  21. package/lib/metadataTypes/AccountUser.js +389 -0
  22. package/lib/metadataTypes/Asset.js +140 -116
  23. package/lib/metadataTypes/Automation.js +119 -54
  24. package/lib/metadataTypes/DataExtension.js +172 -131
  25. package/lib/metadataTypes/DataExtensionField.js +134 -4
  26. package/lib/metadataTypes/Folder.js +66 -69
  27. package/lib/metadataTypes/ImportFile.js +4 -6
  28. package/lib/metadataTypes/MetadataType.js +168 -80
  29. package/lib/metadataTypes/Query.js +54 -25
  30. package/lib/metadataTypes/Role.js +13 -8
  31. package/lib/metadataTypes/Script.js +43 -24
  32. package/lib/metadataTypes/definitions/AccountUser.definition.js +227 -0
  33. package/lib/metadataTypes/definitions/Asset.definition.js +1 -0
  34. package/lib/metadataTypes/definitions/Campaign.definition.js +1 -1
  35. package/lib/metadataTypes/definitions/DataExtension.definition.js +1 -1
  36. package/lib/metadataTypes/definitions/DataExtensionField.definition.js +1 -1
  37. package/lib/metadataTypes/definitions/Folder.definition.js +1 -1
  38. package/lib/metadataTypes/definitions/ImportFile.definition.js +2 -1
  39. package/lib/metadataTypes/definitions/Script.definition.js +5 -5
  40. package/lib/retrieveChangelog.js +96 -0
  41. package/lib/util/cli.js +4 -6
  42. package/lib/util/init.config.js +3 -0
  43. package/lib/util/init.git.js +2 -1
  44. package/lib/util/util.js +35 -18
  45. package/package.json +20 -24
  46. package/test/util/file.js +51 -0
  47. package/img/README.md/troubleshoot-nodejs-postinstall.jpg +0 -0
  48. package/postinstall.js +0 -41
  49. package/test/deployer.js +0 -16
  50. package/test/util.js +0 -26
@@ -13,16 +13,16 @@ class Folder extends MetadataType {
13
13
  /**
14
14
  * Retrieves metadata of metadata type into local filesystem. executes callback with retrieved metadata
15
15
  * @param {String} retrieveDir Directory where retrieved metadata directory will be saved
16
- * @param {String[]} [overrideFields] Returns specified fields even if their retrieve definition is not set to true
16
+ * @param {String[]} [additionalFields] Returns specified fields even if their retrieve definition is not set to true
17
17
  * @param {Object} buObject properties for auth
18
18
  * @returns {Promise} Promise
19
19
  */
20
- static async retrieve(retrieveDir, overrideFields, buObject) {
21
- const queryAllFolders = await this.retrieveHelper(overrideFields, true);
20
+ static async retrieve(retrieveDir, additionalFields, buObject) {
21
+ const queryAllFolders = await this.retrieveHelper(additionalFields, true);
22
22
  // if this is the parent, no need to query twice as QueryAllAccounts works.
23
23
 
24
24
  if (buObject.eid !== buObject.mid) {
25
- queryAllFolders.push(...(await this.retrieveHelper(overrideFields, false)));
25
+ queryAllFolders.push(...(await this.retrieveHelper(additionalFields, false)));
26
26
  }
27
27
  const sortPairs = toposort(queryAllFolders.map((a) => [a.ParentFolder.ID, a.ID]));
28
28
  const idMap = {};
@@ -258,39 +258,36 @@ class Folder extends MetadataType {
258
258
  * @param {Object} metadata metadata of the folder
259
259
  * @returns {Promise} Promise
260
260
  */
261
- static create(metadata) {
262
- const tempCustomerKey = metadata.CustomerKey;
261
+ static async create(metadata) {
262
+ if (metadata.Parent && metadata.Parent.ID && metadata.Parent.ID === 0) {
263
+ Util.logger.error(
264
+ `${this.definition.type}-${metadata.ContentType}.create:: Cannot create Root Folder: ${metadata.Name}`
265
+ );
266
+ return {};
267
+ }
263
268
  const path = metadata.Path;
264
- this.removeNotCreateableFields(metadata);
265
- return new Promise((resolve) => {
266
- if (metadata.Parent && metadata.Parent.ID && metadata.Parent.ID === 0) {
269
+ try {
270
+ const response = await super.createSOAP(metadata, 'DataFolder', true);
271
+ if (response) {
272
+ response.body.Results[0].Object = metadata;
273
+ response.body.Results[0].Object.ID = response.body.Results[0].NewID;
274
+ response.body.Results[0].Object.CustomerKey = metadata.CustomerKey;
275
+ delete response.body.Results[0].Object.$;
276
+
277
+ Util.logger.info(`- created folder: ${path}`);
278
+ return response;
279
+ }
280
+ } catch (ex) {
281
+ if (ex && ex.results) {
267
282
  Util.logger.error(
268
- `${this.definition.type}-${metadata.ContentType}.create:: Cannot create Root Folder: ${metadata.Name}`
283
+ `${this.definition.type}-${metadata.ContentType}.create:: error creating: '${path}'. ${ex.results[0].StatusMessage}`
284
+ );
285
+ } else if (ex) {
286
+ Util.logger.error(
287
+ `${this.definition.type}-${metadata.ContentType}.create:: error creating: '${path}'. ${ex.message}`
269
288
  );
270
- resolve({});
271
- } else {
272
- this.client.SoapClient.create('DataFolder', metadata, null, (ex, response) => {
273
- if (ex && ex.results) {
274
- Util.logger.error(
275
- `${this.definition.type}-${metadata.ContentType}.create:: error creating: ${path}. ${ex.results[0].StatusMessage}`
276
- );
277
- resolve();
278
- } else if (ex) {
279
- Util.logger.error(
280
- `${this.definition.type}-${metadata.ContentType}.create:: error creating: ${path}. ${ex.message}`
281
- );
282
- resolve();
283
- } else {
284
- response.body.Results[0].Object = metadata;
285
- response.body.Results[0].Object.ID = response.body.Results[0].NewID;
286
- response.body.Results[0].Object.CustomerKey = tempCustomerKey;
287
- delete response.body.Results[0].Object.$;
288
- Util.logger.info(`- created folder: ${path}`);
289
- resolve(response);
290
- }
291
- });
292
289
  }
293
- });
290
+ }
294
291
  }
295
292
 
296
293
  /**
@@ -299,30 +296,27 @@ class Folder extends MetadataType {
299
296
  * @returns {Promise} Promise
300
297
  */
301
298
  static async update(metadata) {
302
- return await new Promise((resolve) => {
303
- const tempCustomerKey = metadata.CustomerKey;
304
- const path = metadata.Path;
305
- this.removeNotUpdateableFields(metadata);
306
- this.client.SoapClient.update('DataFolder', metadata, null, (ex, response) => {
307
- if (ex && ex.results) {
308
- Util.logger.error(
309
- `${this.definition.type}-${metadata.ContentType}.update:: error updating: '${path}'. ${ex.results[0].StatusMessage}`
310
- );
311
- resolve();
312
- } else if (ex) {
313
- Util.logger.error(
314
- `${this.definition.type}-${metadata.ContentType}.update:: error updating: '${path}'. ${ex.message}`
315
- );
316
- resolve();
317
- } else {
318
- response.body.Results[0].Object = metadata;
319
- response.body.Results[0].Object.CustomerKey = tempCustomerKey;
320
- delete response.body.Results[0].Object.$;
321
- Util.logger.info(`- updated folder: ${path}`);
322
- resolve(response);
323
- }
324
- });
325
- });
299
+ const path = metadata.Path;
300
+ try {
301
+ const response = await super.updateSOAP(metadata, 'DataFolder', true);
302
+ if (response) {
303
+ response.body.Results[0].Object = metadata;
304
+ response.body.Results[0].Object.CustomerKey = metadata.CustomerKey;
305
+ delete response.body.Results[0].Object.$;
306
+ Util.logger.info(`- updated folder: ${path}`);
307
+ return response;
308
+ }
309
+ } catch (ex) {
310
+ if (ex && ex.results) {
311
+ Util.logger.error(
312
+ `${this.definition.type}-${metadata.ContentType}.update:: error updating: '${path}'. ${ex.results[0].StatusMessage}`
313
+ );
314
+ } else if (ex) {
315
+ Util.logger.error(
316
+ `${this.definition.type}-${metadata.ContentType}.update:: error updating: '${path}'. ${ex.message}`
317
+ );
318
+ }
319
+ }
326
320
  }
327
321
 
328
322
  /**
@@ -451,25 +445,28 @@ class Folder extends MetadataType {
451
445
 
452
446
  /**
453
447
  * Helper to retrieve the folders as promise
454
- * @param {String[]} [overrideFields] Returns specified fields even if their retrieve definition is not set to true
448
+ * @param {String[]} [additionalFields] Returns specified fields even if their retrieve definition is not set to true
455
449
  * @param {Boolean} [queryAllAccounts] which queryAllAccounts setting to use
456
450
  * @returns {Promise<Object>} soap object
457
451
  */
458
- static async retrieveHelper(overrideFields, queryAllAccounts) {
452
+ static async retrieveHelper(additionalFields, queryAllAccounts) {
459
453
  const options = { queryAllAccounts: !!queryAllAccounts };
460
454
  let status = 'MoreDataAvailable';
461
455
  const Results = [];
462
456
  do {
463
- const response = await new Promise((resolve, reject) => {
464
- // filtered out path as we need them to be stored locally, but do not want to try and retrieve
465
- this.client.SoapClient.retrieve(
466
- 'DataFolder',
467
- this.getFieldNamesToRetrieve(overrideFields).filter(
468
- (field) => !field.includes('Path')
469
- ),
470
- options,
471
- (ex, response) => (ex ? reject(ex) : resolve(response))
472
- );
457
+ let response;
458
+ await Util.retryOnError(`Retrying ${this.definition.type}`, async () => {
459
+ response = await new Promise((resolve, reject) => {
460
+ // filtered out path as we need them to be stored locally, but do not want to try and retrieve
461
+ this.client.SoapClient.retrieve(
462
+ 'DataFolder',
463
+ this.getFieldNamesToRetrieve(additionalFields).filter(
464
+ (field) => !field.includes('Path')
465
+ ),
466
+ options,
467
+ (ex, response) => (ex ? reject(ex) : resolve(response))
468
+ );
469
+ });
473
470
  });
474
471
  // merge results with existing
475
472
  Results.push(...response.body.Results);
@@ -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 = this.definition.destinationObjectTypeMapping[
151
- metadata.c__destinationType
152
- ];
153
- metadata.subscriberImportTypeId = this.definition.subscriberImportTypeMapping[
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
  }
@@ -9,6 +9,19 @@
9
9
 
10
10
  /**
11
11
  * @typedef {Object.<string, any>} MetadataTypeItem
12
+ *
13
+ * @typedef {Object} CodeExtractItem
14
+ * @property {MetadataTypeItem} json metadata of one item w/o code
15
+ * @property {CodeExtract[]} codeArr list of code snippets in this item
16
+ * @property {string[]} subFolder mostly set to null, otherwise list of subfolders
17
+ *
18
+ * @typedef {Object} CodeExtract
19
+ * @property {string[]} subFolder mostly set to null, otherwise subfolders path split into elements
20
+ * @property {string} fileName name of file w/o extension
21
+ * @property {string} fileExt file extension
22
+ * @property {string} content file content
23
+ * @property {'base64'} [encoding] optional for binary files
24
+
12
25
  *
13
26
  * @typedef {Object.<string, MetadataTypeItem>} MetadataTypeMap
14
27
  *
@@ -181,16 +194,26 @@ class MetadataType {
181
194
  /**
182
195
  * Gets metadata from Marketing Cloud
183
196
  * @param {string} retrieveDir Directory where retrieved metadata directory will be saved
184
- * @param {string[]} [overrideFields] Returns specified fields even if their retrieve definition is not set to true
197
+ * @param {string[]} [additionalFields] Returns specified fields even if their retrieve definition is not set to true
185
198
  * @param {Util.BuObject} buObject properties for auth
186
199
  * @param {string} [subType] optionally limit to a single subtype
187
200
  * @returns {Promise<{metadata:MetadataTypeMap,type:string}>} metadata
188
201
  */
189
- static retrieve(retrieveDir, overrideFields, buObject, subType) {
202
+ static retrieve(retrieveDir, additionalFields, buObject, subType) {
190
203
  Util.metadataLogger('error', this.definition.type, 'retrieve', `Not Supported`);
191
204
  const metadata = {};
192
205
  return { metadata: null, type: this.definition.type };
193
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
+ }
194
217
 
195
218
  /**
196
219
  * Gets metadata cache with limited fields and does not store value to disk
@@ -210,8 +233,9 @@ class MetadataType {
210
233
  * @returns {Promise<{metadata:MetadataTypeMap,type:string}>} metadata
211
234
  */
212
235
  static retrieveAsTemplate(templateDir, name, templateVariables, subType) {
236
+ Util.logger.error('retrieveAsTemplate is not supported yet for ' + this.definition.type);
213
237
  Util.metadataLogger(
214
- 'error',
238
+ 'debug',
215
239
  this.definition.type,
216
240
  'retrieveAsTemplate',
217
241
  `(${templateDir}, ${name}, ${templateVariables}) no templating function`
@@ -290,9 +314,10 @@ class MetadataType {
290
314
  });
291
315
  } else if (this.cache[this.definition.type][normalizedKey]) {
292
316
  // normal way of processing update files
293
- metadata[metadataKey][this.definition.idField] = this.cache[
294
- this.definition.type
295
- ][normalizedKey][this.definition.idField];
317
+ metadata[metadataKey][this.definition.idField] =
318
+ this.cache[this.definition.type][normalizedKey][
319
+ this.definition.idField
320
+ ];
296
321
  metadataToUpdate.push({
297
322
  before: this.cache[this.definition.type][normalizedKey],
298
323
  after: metadata[metadataKey],
@@ -389,31 +414,54 @@ class MetadataType {
389
414
  /**
390
415
  * Creates a single metadata entry via fuel-soap (generic lib not wrapper)
391
416
  * @param {MetadataTypeItem} metadataEntry single metadata entry
417
+ * @param {string} [overrideType] can be used if the API type differs from the otherwise used type identifier
418
+ * @param {boolean} [handleOutside] if the API reponse is irregular this allows you to handle it outside of this generic method
392
419
  * @returns {Promise} Promise
393
420
  */
394
- static async createSOAP(metadataEntry) {
421
+ static async createSOAP(metadataEntry, overrideType, handleOutside) {
395
422
  try {
396
- const res = await new Promise((resolve, reject) => {
397
- this.removeNotCreateableFields(metadataEntry);
398
- this.client.SoapClient.create(
399
- this.definition.type.charAt(0).toUpperCase() + this.definition.type.slice(1),
400
- metadataEntry,
401
- null,
402
- (error, response) => (error ? reject(error) : resolve(response))
403
- );
404
- });
405
- Util.logger.info(
406
- `- created ${this.definition.type}: ${metadataEntry[this.definition.keyField]}`
423
+ this.removeNotCreateableFields(metadataEntry);
424
+ let response;
425
+ await Util.retryOnError(
426
+ `Retrying to create ${this.definition.type}: ${
427
+ metadataEntry[this.definition.nameField]
428
+ }`,
429
+ async () =>
430
+ (response = await new Promise((resolve, reject) => {
431
+ this.client.SoapClient.create(
432
+ overrideType ||
433
+ this.definition.type.charAt(0).toUpperCase() +
434
+ this.definition.type.slice(1),
435
+ metadataEntry,
436
+ null,
437
+ (error, response) => (error ? reject(error) : resolve(response))
438
+ );
439
+ }))
407
440
  );
408
- return res;
441
+ if (!handleOutside) {
442
+ Util.logger.info(
443
+ `- created ${this.definition.type}: ${metadataEntry[this.definition.keyField]}`
444
+ );
445
+ }
446
+ return response;
409
447
  } catch (ex) {
410
- Util.logger.error(
411
- `- error creating ${this.definition.type}: ${
412
- metadataEntry[this.definition.keyField]
413
- } (${ex.message})`
414
- );
448
+ if (!handleOutside) {
449
+ let errorMsg;
450
+ if (ex.results && ex.results.length) {
451
+ errorMsg = `${ex.results[0].StatusMessage} (Code ${ex.results[0].ErrorCode})`;
452
+ } else {
453
+ errorMsg = ex.message;
454
+ }
455
+ Util.logger.error(
456
+ `- error creating ${this.definition.type} '${
457
+ metadataEntry[this.definition.keyField]
458
+ }': ${errorMsg}`
459
+ );
460
+ } else {
461
+ throw ex;
462
+ }
415
463
 
416
- return null;
464
+ return {};
417
465
  }
418
466
  }
419
467
 
@@ -437,8 +485,12 @@ class MetadataType {
437
485
  async () => (response = await this.client.RestClient.patch(options))
438
486
  );
439
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
440
489
  Util.logger.info(
441
- `- updated ${this.definition.type}: ${metadataEntry[this.definition.keyField]}`
490
+ `- updated ${this.definition.type}: ${
491
+ metadataEntry[this.definition.keyField] ||
492
+ metadataEntry[this.definition.nameField]
493
+ }`
442
494
  );
443
495
  return response;
444
496
  } catch (ex) {
@@ -454,31 +506,54 @@ class MetadataType {
454
506
  /**
455
507
  * Updates a single metadata entry via fuel-soap (generic lib not wrapper)
456
508
  * @param {MetadataTypeItem} metadataEntry single metadata entry
509
+ * @param {string} [overrideType] can be used if the API type differs from the otherwise used type identifier
510
+ * @param {boolean} [handleOutside] if the API reponse is irregular this allows you to handle it outside of this generic method
457
511
  * @returns {Promise} Promise
458
512
  */
459
- static async updateSOAP(metadataEntry) {
513
+ static async updateSOAP(metadataEntry, overrideType, handleOutside) {
514
+ let response;
460
515
  try {
461
- const res = await new Promise((resolve, reject) => {
462
- this.removeNotUpdateableFields(metadataEntry);
463
- this.client.SoapClient.update(
464
- this.definition.type.charAt(0).toUpperCase() + this.definition.type.slice(1),
465
- metadataEntry,
466
- null,
467
- (error, response) => (error ? reject(error) : resolve(response))
468
- );
469
- });
470
- Util.logger.info(
471
- `- updated ${this.definition.type}: ${metadataEntry[this.definition.keyField]}`
516
+ this.removeNotUpdateableFields(metadataEntry);
517
+ await Util.retryOnError(
518
+ `Retrying to update ${this.definition.type}: ${
519
+ metadataEntry[this.definition.nameField]
520
+ }`,
521
+ async () =>
522
+ (response = await new Promise((resolve, reject) => {
523
+ this.client.SoapClient.update(
524
+ overrideType ||
525
+ this.definition.type.charAt(0).toUpperCase() +
526
+ this.definition.type.slice(1),
527
+ metadataEntry,
528
+ null,
529
+ (error, response) => (error ? reject(error) : resolve(response))
530
+ );
531
+ }))
472
532
  );
473
- return res;
533
+ if (!handleOutside) {
534
+ Util.logger.info(
535
+ `- updated ${this.definition.type}: ${metadataEntry[this.definition.keyField]}`
536
+ );
537
+ }
538
+ return response;
474
539
  } catch (ex) {
475
- Util.logger.error(
476
- `- error updating ${this.definition.type}: ${
477
- metadataEntry[this.definition.keyField]
478
- } (${ex.message})`
479
- );
540
+ if (!handleOutside) {
541
+ let errorMsg;
542
+ if (ex.results && ex.results.length) {
543
+ errorMsg = `${ex.results[0].StatusMessage} (Code ${ex.results[0].ErrorCode})`;
544
+ } else {
545
+ errorMsg = ex.message;
546
+ }
547
+ Util.logger.error(
548
+ `- error updating ${this.definition.type} '${
549
+ metadataEntry[this.definition.keyField]
550
+ }': ${errorMsg}`
551
+ );
552
+ } else {
553
+ throw ex;
554
+ }
480
555
 
481
- return null;
556
+ return {};
482
557
  }
483
558
  }
484
559
  /**
@@ -487,18 +562,24 @@ class MetadataType {
487
562
  * @param {Util.BuObject} buObject properties for auth
488
563
  * @param {Object} [options] required for the specific request (filter for example)
489
564
  * @param {string[]} [additionalFields] Returns specified fields even if their retrieve definition is not set to true
565
+ * @param {string} [overrideType] can be used if the API type differs from the otherwise used type identifier
490
566
  * @returns {Promise<{metadata:MetadataTypeMap,type:string}>} Promise of item map
491
567
  */
492
- static async retrieveSOAPgeneric(retrieveDir, buObject, options, additionalFields) {
568
+ static async retrieveSOAPgeneric(
569
+ retrieveDir,
570
+ buObject,
571
+ options,
572
+ additionalFields,
573
+ overrideType
574
+ ) {
493
575
  const fields = this.getFieldNamesToRetrieve(additionalFields);
494
576
 
495
- const metadata = await this.retrieveSOAPBody(fields, options);
577
+ const metadata = await this.retrieveSOAPBody(fields, options, overrideType);
496
578
  if (retrieveDir) {
497
579
  const savedMetadata = await this.saveResults(metadata, retrieveDir, null);
498
580
  Util.logger.info(
499
581
  `Downloaded: ${this.definition.type} (${Object.keys(savedMetadata).length})`
500
582
  );
501
-
502
583
  if (
503
584
  buObject &&
504
585
  this.properties.metaDataTypes.documentOnRetrieve.includes(this.definition.type)
@@ -522,19 +603,27 @@ class MetadataType {
522
603
  options = options || {};
523
604
  let metadata = {};
524
605
  do {
525
- const resultsBatch = await new Promise((resolve) => {
526
- this.client.SoapClient.retrieve(
527
- type || this.definition.type,
528
- fields,
529
- options || {},
530
- (error, response) => {
531
- if (error) {
532
- throw new Error(error);
533
- } else {
534
- resolve(response.body);
606
+ let resultsBatch;
607
+ await Util.retryOnError(`Retrying ${this.definition.type}`, async () => {
608
+ resultsBatch = await new Promise((resolve, reject) => {
609
+ this.client.SoapClient.retrieve(
610
+ type || this.definition.type,
611
+ fields,
612
+ options || {},
613
+ (error, response) => {
614
+ if (error) {
615
+ Util.logger.debug(`SOAP.retrieve Error: ${error.message}`);
616
+ reject(error);
617
+ }
618
+ if (response) {
619
+ resolve(response.body);
620
+ } else {
621
+ // fallback, lets make sure surrounding methods know we got an empty result back
622
+ resolve({});
623
+ }
535
624
  }
536
- }
537
- );
625
+ );
626
+ });
538
627
  });
539
628
  status = resultsBatch.OverallStatus;
540
629
  if (status === 'MoreDataAvailable') {
@@ -572,7 +661,10 @@ class MetadataType {
572
661
  let results = {};
573
662
  do {
574
663
  options.uri = this.paginate(options.uri, lastPage);
575
- const response = await this.client.RestClient.get(options);
664
+ let response;
665
+ await Util.retryOnError(`Retrying ${this.definition.type}`, async () => {
666
+ response = await this.client.RestClient.get(options);
667
+ });
576
668
  const metadata = this.parseResponseBody(response.body);
577
669
  results = Object.assign(results, metadata);
578
670
  if (
@@ -621,28 +713,24 @@ class MetadataType {
621
713
  /**
622
714
  * Builds map of metadata entries mapped to their keyfields
623
715
  * @param {Object} body json of response body
624
- * @param {string} [bodyIteratorField] overrides bodyIteratorField of definition
625
- * @param {string} [keyField] overrides keyField of definition
626
716
  * @returns {Promise<MetadataTypeMap>} keyField => metadata map
627
717
  */
628
- static parseResponseBody(body, bodyIteratorField, keyField) {
629
- if (!bodyIteratorField) {
630
- bodyIteratorField = this.definition.bodyIteratorField;
631
- }
632
- if (!keyField) {
633
- keyField = this.definition.keyField;
634
- }
718
+ static parseResponseBody(body) {
719
+ const bodyIteratorField = this.definition.bodyIteratorField;
720
+ const keyField = this.definition.keyField;
635
721
  const metadataStructure = {};
636
- // in some cases data is just an array
637
- if (Array.isArray(bodyIteratorField)) {
638
- for (const item of body) {
639
- const key = item[keyField];
640
- metadataStructure[key] = item;
641
- }
642
- } else {
643
- for (const item of body[bodyIteratorField]) {
644
- const key = item[keyField];
645
- metadataStructure[key] = item;
722
+ if (body !== null) {
723
+ // in some cases data is just an array
724
+ if (Array.isArray(bodyIteratorField)) {
725
+ for (const item of body) {
726
+ const key = item[keyField];
727
+ metadataStructure[key] = item;
728
+ }
729
+ } else if (body[bodyIteratorField]) {
730
+ for (const item of body[bodyIteratorField]) {
731
+ const key = item[keyField];
732
+ metadataStructure[key] = item;
733
+ }
646
734
  }
647
735
  }
648
736
  return metadataStructure;