mcdev 4.2.1 → 4.3.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.
Files changed (98) hide show
  1. package/.github/ISSUE_TEMPLATE/bug.yml +3 -0
  2. package/.github/PULL_REQUEST_TEMPLATE.md +1 -2
  3. package/.github/pr-labeler.yml +3 -0
  4. package/.github/workflows/close_issues_on_merge.yml +18 -0
  5. package/.github/workflows/pr-labeler.yml +19 -0
  6. package/LICENSE +1 -1
  7. package/README.md +1 -1
  8. package/docs/dist/documentation.md +702 -284
  9. package/lib/Deployer.js +21 -15
  10. package/lib/Retriever.js +41 -34
  11. package/lib/cli.js +36 -6
  12. package/lib/index.js +56 -10
  13. package/lib/metadataTypes/AccountUser.js +17 -23
  14. package/lib/metadataTypes/Asset.js +36 -48
  15. package/lib/metadataTypes/AttributeGroup.js +1 -2
  16. package/lib/metadataTypes/Automation.js +75 -37
  17. package/lib/metadataTypes/Campaign.js +4 -3
  18. package/lib/metadataTypes/ContentArea.js +2 -3
  19. package/lib/metadataTypes/DataExtension.js +56 -47
  20. package/lib/metadataTypes/DataExtensionField.js +9 -12
  21. package/lib/metadataTypes/DataExtensionTemplate.js +2 -3
  22. package/lib/metadataTypes/DataExtract.js +1 -2
  23. package/lib/metadataTypes/DataExtractType.js +1 -2
  24. package/lib/metadataTypes/Discovery.js +3 -4
  25. package/lib/metadataTypes/Email.js +20 -6
  26. package/lib/metadataTypes/EmailSendDefinition.js +40 -39
  27. package/lib/metadataTypes/EventDefinition.js +29 -2
  28. package/lib/metadataTypes/FileTransfer.js +1 -2
  29. package/lib/metadataTypes/Filter.js +1 -2
  30. package/lib/metadataTypes/Folder.js +12 -14
  31. package/lib/metadataTypes/FtpLocation.js +1 -2
  32. package/lib/metadataTypes/ImportFile.js +1 -2
  33. package/lib/metadataTypes/Interaction.js +743 -12
  34. package/lib/metadataTypes/List.js +36 -33
  35. package/lib/metadataTypes/MetadataType.js +170 -124
  36. package/lib/metadataTypes/MobileCode.js +1 -2
  37. package/lib/metadataTypes/MobileKeyword.js +1 -2
  38. package/lib/metadataTypes/Query.js +15 -6
  39. package/lib/metadataTypes/Role.js +10 -11
  40. package/lib/metadataTypes/Script.js +2 -5
  41. package/lib/metadataTypes/SetDefinition.js +1 -2
  42. package/lib/metadataTypes/TransactionalMessage.js +25 -32
  43. package/lib/metadataTypes/TransactionalSMS.js +3 -4
  44. package/lib/metadataTypes/TriggeredSendDefinition.js +232 -56
  45. package/lib/metadataTypes/definitions/Asset.definition.js +1 -1
  46. package/lib/metadataTypes/definitions/Automation.definition.js +1 -1
  47. package/lib/metadataTypes/definitions/DataExtension.definition.js +10 -1
  48. package/lib/metadataTypes/definitions/Email.definition.js +1 -1
  49. package/lib/metadataTypes/definitions/EmailSendDefinition.definition.js +1 -1
  50. package/lib/metadataTypes/definitions/EventDefinition.definition.js +40 -1
  51. package/lib/metadataTypes/definitions/Folder.definition.js +31 -0
  52. package/lib/metadataTypes/definitions/ImportFile.definition.js +1 -1
  53. package/lib/metadataTypes/definitions/Interaction.definition.js +47 -26
  54. package/lib/metadataTypes/definitions/List.definition.js +1 -1
  55. package/lib/metadataTypes/definitions/Query.definition.js +1 -1
  56. package/lib/metadataTypes/definitions/Script.definition.js +1 -1
  57. package/lib/metadataTypes/definitions/TransactionalEmail.definition.js +4 -3
  58. package/lib/metadataTypes/definitions/TransactionalPush.definition.js +4 -3
  59. package/lib/metadataTypes/definitions/TransactionalSMS.definition.js +4 -3
  60. package/lib/metadataTypes/definitions/TriggeredSendDefinition.definition.js +10 -2
  61. package/lib/util/auth.js +15 -2
  62. package/lib/util/cli.js +4 -1
  63. package/lib/util/file.js +7 -3
  64. package/lib/util/init.js +62 -0
  65. package/lib/util/util.js +173 -11
  66. package/package.json +23 -9
  67. package/test/dataExtension.test.js +10 -10
  68. package/test/interaction.test.js +123 -0
  69. package/test/mockRoot/.mcdevrc.json +1 -1
  70. package/test/mockRoot/deploy/testInstance/testBU/interaction/testExisting_interaction.interaction-meta.json +266 -0
  71. package/test/mockRoot/deploy/testInstance/testBU/interaction/testNew_interaction.interaction-meta.json +266 -0
  72. package/test/mockRoot/deploy/testInstance/testBU/transactionalEmail/testExisting_temail.transactionalEmail-meta.json +0 -3
  73. package/test/query.test.js +8 -8
  74. package/test/resourceFactory.js +30 -14
  75. package/test/resources/1111111/dataExtension/retrieve-response.xml +26 -0
  76. package/test/resources/9999999/data/v1/customobjectdata/key/childBU_dataextension_test/rowset/get-response.json +13 -0
  77. package/test/resources/9999999/dataFolder/retrieve-response.xml +22 -0
  78. package/test/resources/9999999/eventDefinition/get-expected.json +34 -0
  79. package/test/resources/9999999/interaction/build-expected.json +260 -0
  80. package/test/resources/9999999/interaction/get-expected.json +264 -0
  81. package/test/resources/9999999/interaction/post-expected.json +264 -0
  82. package/test/resources/9999999/interaction/put-expected.json +264 -0
  83. package/test/resources/9999999/interaction/template-expected.json +260 -0
  84. package/test/resources/9999999/interaction/v1/EventDefinitions/get-response.json +43 -0
  85. package/test/resources/9999999/interaction/v1/interactions/get-response.json +222 -3
  86. package/test/resources/9999999/interaction/v1/interactions/key_0b76dccf-594c-b6dc-1acf-10c4493dcb84/get-response.json +219 -0
  87. package/test/resources/9999999/interaction/v1/interactions/key_testExisting_interaction/get-response.json +280 -0
  88. package/test/resources/9999999/interaction/v1/interactions/post-response.json +280 -0
  89. package/test/resources/9999999/interaction/v1/interactions/put-response.json +280 -0
  90. package/test/resources/9999999/messaging/v1/email/definitions/post-response.json +1 -1
  91. package/test/resources/9999999/query/post-expected.sql +1 -1
  92. package/test/resources/9999999/transactionalEmail/post-expected.json +1 -1
  93. package/test/resources/9999999/triggeredSendDefinition/retrieve-response.xml +68 -0
  94. package/test/transactionalEmail.test.js +7 -7
  95. package/test/transactionalPush.test.js +7 -7
  96. package/test/transactionalSMS.test.js +7 -7
  97. package/test/utils.js +50 -0
  98. package/types/mcdev.d.js +1 -0
@@ -5,6 +5,15 @@ const MetadataType = require('./MetadataType');
5
5
  const File = require('../util/file');
6
6
  const cache = require('../util/cache');
7
7
  const Mustache = require('mustache');
8
+ /**
9
+ * ensure that Mustache does not escape any characters
10
+ *
11
+ * @param {string} text -
12
+ * @returns {string} text
13
+ */
14
+ Mustache.escape = function (text) {
15
+ return text;
16
+ };
8
17
 
9
18
  /**
10
19
  * Query MetadataType
@@ -18,11 +27,10 @@ class Query extends MetadataType {
18
27
  * @param {string} retrieveDir Directory where retrieved metadata directory will be saved
19
28
  * @param {void} [_] unused parameter
20
29
  * @param {void} [__] unused parameter
21
- * @param {void} [___] unused parameter
22
30
  * @param {string} [key] customer key of single item to retrieve
23
31
  * @returns {Promise.<{metadata: TYPE.QueryMap, type: string}>} Promise of metadata
24
32
  */
25
- static async retrieve(retrieveDir, _, __, ___, key) {
33
+ static async retrieve(retrieveDir, _, __, key) {
26
34
  await File.initPrettier('sql');
27
35
  const objectId = key ? await this._getObjectIdForSingleRetrieve(key) : null;
28
36
  return super.retrieveREST(
@@ -129,8 +137,9 @@ class Query extends MetadataType {
129
137
  'CustomerKey',
130
138
  'CustomerKey'
131
139
  );
132
- metadata.categoryId = cache.searchForField('folder', metadata.r__folder_Path, 'Path', 'ID');
133
- delete metadata.r__folder_Path;
140
+ // folder
141
+ super.setFolderId(metadata);
142
+
134
143
  metadata.targetUpdateTypeId =
135
144
  this.definition.targetUpdateTypeMapping[metadata.targetUpdateTypeName];
136
145
  return metadata;
@@ -156,7 +165,7 @@ class Query extends MetadataType {
156
165
  .join('}}}');
157
166
 
158
167
  // replace template variables with their values
159
- return Mustache.render(code, templateVariables);
168
+ return Mustache.render(code, templateVariables, {}, ['{{{', '}}}']);
160
169
  }
161
170
  /**
162
171
  * helper for {@link MetadataType.buildDefinition}
@@ -327,7 +336,7 @@ class Query extends MetadataType {
327
336
  * Standardizes a check for multiple messages but adds query specific filters to error texts
328
337
  *
329
338
  * @param {object} ex response payload from REST API
330
- * @returns {string[]} formatted Error Message
339
+ * @returns {string[] | void} formatted Error Message
331
340
  */
332
341
  static checkForErrors(ex) {
333
342
  const errors = super.checkForErrors(ex);
@@ -16,13 +16,12 @@ class Role extends MetadataType {
16
16
  *
17
17
  * @param {string} retrieveDir Directory where retrieved metadata directory will be saved
18
18
  * @param {string[]} _ Returns specified fields even if their retrieve definition is not set to true
19
- * @param {TYPE.BuObject} buObject properties for auth
20
19
  * @param {void} [___] unused parameter
21
20
  * @param {string} [key] customer key of single item to retrieve
22
21
  * @returns {Promise.<TYPE.MetadataTypeMapObj>} Metadata store object
23
22
  */
24
- static async retrieve(retrieveDir, _, buObject, ___, key) {
25
- if (retrieveDir && buObject.eid !== buObject.mid) {
23
+ static async retrieve(retrieveDir, _, ___, key) {
24
+ if (retrieveDir && this.buObject.eid !== this.buObject.mid) {
26
25
  // don't run for BUs other than Parent BU
27
26
  // this check does not work during caching
28
27
  Util.logger.info(' - Skipping Role retrieval on non-parent BU');
@@ -67,10 +66,11 @@ class Role extends MetadataType {
67
66
  if (retrieveDir) {
68
67
  const savedMetadata = await super.saveResults(parsed, retrieveDir, null);
69
68
  Util.logger.info(
70
- `Downloaded: ${this.definition.type} (${Object.keys(savedMetadata).length})`
69
+ `Downloaded: ${this.definition.type} (${Object.keys(savedMetadata).length})` +
70
+ Util.getKeysString(key)
71
71
  );
72
72
  if (this.properties.metaDataTypes.documentOnRetrieve.includes(this.definition.type)) {
73
- await this.document(buObject, savedMetadata);
73
+ await this.document(savedMetadata);
74
74
  }
75
75
  }
76
76
  return { metadata: parsed, type: this.definition.type };
@@ -113,12 +113,11 @@ class Role extends MetadataType {
113
113
  /**
114
114
  * Creates markdown documentation of all roles
115
115
  *
116
- * @param {TYPE.BuObject} buObject properties for auth
117
116
  * @param {TYPE.MetadataTypeMap} [metadata] role definitions
118
117
  * @returns {Promise.<void>} -
119
118
  */
120
- static async document(buObject, metadata) {
121
- if (buObject.eid !== buObject.mid) {
119
+ static async document(metadata) {
120
+ if (this.buObject.eid !== this.buObject.mid) {
122
121
  Util.logger.error(
123
122
  `Roles can only be retrieved & documented for the ${Util.parentBuName}`
124
123
  );
@@ -129,7 +128,7 @@ class Role extends MetadataType {
129
128
  metadata = this.readBUMetadataForType(
130
129
  File.normalizePath([
131
130
  this.properties.directories.retrieve,
132
- buObject.credential,
131
+ this.buObject.credential,
133
132
  Util.parentBuName,
134
133
  ]),
135
134
  true
@@ -159,7 +158,7 @@ class Role extends MetadataType {
159
158
  }
160
159
  }
161
160
  // Create output markdown
162
- let output = `# Permission Overview - ${buObject.credential}\n\n`;
161
+ let output = `# Permission Overview - ${this.buObject.credential}\n\n`;
163
162
  output += `> **Legend**
164
163
  >
165
164
  > <hr>
@@ -211,7 +210,7 @@ class Role extends MetadataType {
211
210
  output += '\n';
212
211
  }
213
212
  try {
214
- const filename = buObject.credential;
213
+ const filename = this.buObject.credential;
215
214
  // write to disk
216
215
  await File.writeToFile(directory, filename + '.roles', 'md', output);
217
216
  Util.logger.info(`Created ${File.normalizePath([directory, filename])}.roles.md`);
@@ -4,7 +4,6 @@ const TYPE = require('../../types/mcdev.d');
4
4
  const MetadataType = require('./MetadataType');
5
5
  const Util = require('../util/util');
6
6
  const File = require('../util/file');
7
- const cache = require('../util/cache');
8
7
 
9
8
  /**
10
9
  * Script MetadataType
@@ -19,11 +18,10 @@ class Script extends MetadataType {
19
18
  * @param {string} retrieveDir Directory where retrieved metadata directory will be saved
20
19
  * @param {void} [_] unused parameter
21
20
  * @param {void} [__] unused parameter
22
- * @param {void} [___] unused parameter
23
21
  * @param {string} [key] customer key of single item to retrieve
24
22
  * @returns {Promise.<{metadata: TYPE.ScriptMap, type: string}>} Promise
25
23
  */
26
- static async retrieve(retrieveDir, _, __, ___, key) {
24
+ static async retrieve(retrieveDir, _, __, key) {
27
25
  await File.initPrettier('ssjs');
28
26
  return super.retrieveREST(retrieveDir, '/automation/v1/scripts/', null, null, key);
29
27
  }
@@ -128,8 +126,7 @@ class Script extends MetadataType {
128
126
  */
129
127
  static async preDeployTasks(metadata, dir) {
130
128
  // folder
131
- metadata.categoryId = cache.searchForField('folder', metadata.r__folder_Path, 'Path', 'ID');
132
- delete metadata.r__folder_Path;
129
+ super.setFolderId(metadata);
133
130
 
134
131
  // code
135
132
  metadata.script = await this._mergeCode(metadata, dir);
@@ -15,11 +15,10 @@ class SetDefinition extends MetadataType {
15
15
  * @param {string} retrieveDir Directory where retrieved metadata directory will be saved
16
16
  * @param {void} [_] unused parameter
17
17
  * @param {void} [__] unused parameter
18
- * @param {void} [___] unused parameter
19
18
  * @param {string} [key] customer key of single item to retrieve
20
19
  * @returns {Promise.<TYPE.MetadataTypeMapObj>} Promise
21
20
  */
22
- static retrieve(retrieveDir, _, __, ___, key) {
21
+ static retrieve(retrieveDir, _, __, key) {
23
22
  return super.retrieveREST(
24
23
  retrieveDir,
25
24
  '/hub/v1/contacts/schema/setDefinitions',
@@ -19,11 +19,10 @@ class TransactionalMessage extends MetadataType {
19
19
  * @param {string} retrieveDir Directory where retrieved metadata directory will be saved
20
20
  * @param {void} [_] unused parameter
21
21
  * @param {void} [__] unused parameter
22
- * @param {void} [___] unused parameter
23
22
  * @param {string} [key] customer key of single item to retrieve
24
23
  * @returns {Promise.<TYPE.MetadataTypeMapObj>} Promise of metadata
25
24
  */
26
- static async retrieve(retrieveDir, _, __, ___, key) {
25
+ static async retrieve(retrieveDir, _, __, key) {
27
26
  let keyList;
28
27
  const baseUri = '/messaging/v1/' + this.subType + '/definitions/';
29
28
  if (key) {
@@ -31,20 +30,16 @@ class TransactionalMessage extends MetadataType {
31
30
  keyList = [key];
32
31
  } else {
33
32
  // Retrieve all
34
- const response = this.definition.restPagination
35
- ? await this.client.rest.getBulk(baseUri)
36
- : await this.client.rest.get(baseUri);
37
- const parsed = this.parseResponseBody(response);
38
- keyList = Object.keys(parsed).filter((item) => parsed[item].status !== 'Deleted');
39
- const filteredCount = Object.keys(parsed).length - keyList.length;
40
- if (filteredCount) {
41
- Util.logger.info(
42
- ` - Filtered ${this.definition.type} with status 'deleted': ${filteredCount} (downloaded but not saved to disk)`
43
- );
44
- }
33
+ // * keep deleted items for caching (and to decide on update vs create)
34
+ const parsed = (
35
+ await this.retrieveREST(
36
+ null,
37
+ baseUri + (retrieveDir ? '?$filter=status%20neq%20deleted' : '')
38
+ )
39
+ ).metadata;
40
+ keyList = Object.keys(parsed);
45
41
  }
46
-
47
- // get all sms with additional details not given by the list endpoint
42
+ // get all transactionalX items with additional details not given by the list endpoint
48
43
  const details = (
49
44
  await Promise.all(
50
45
  keyList.map(async (key) => {
@@ -57,20 +52,17 @@ class TransactionalMessage extends MetadataType {
57
52
  )
58
53
  ).filter(Boolean);
59
54
  const parsed = this.parseResponseBody({ definitions: details });
55
+ let savedMetadata;
56
+ if (retrieveDir) {
57
+ // * retrieveDir is mandatory in this method as it is not used for caching (there is a seperate method for that)
58
+ savedMetadata = await this.saveResults(parsed, retrieveDir, null, null);
59
+ Util.logger.info(
60
+ `Downloaded: ${this.definition.type} (${Object.keys(savedMetadata).length})` +
61
+ Util.getKeysString(key)
62
+ );
63
+ }
60
64
 
61
- // * retrieveDir is mandatory in this method as it is not used for caching (there is a seperate method for that)
62
- const savedMetadata = await this.saveResults(parsed, retrieveDir, null, null);
63
- // defined colors for optionally printing the keys we filtered by
64
- const color = {
65
- reset: '\x1B[0m',
66
- dim: '\x1B[2m',
67
- };
68
- Util.logger.info(
69
- `Downloaded: ${this.definition.type} (${Object.keys(savedMetadata).length})` +
70
- (key === null ? '' : ` ${color.dim}(Key: ${key})${color.reset}`)
71
- );
72
-
73
- return { metadata: savedMetadata, type: this.definition.type };
65
+ return { metadata: savedMetadata || parsed, type: this.definition.type };
74
66
  }
75
67
 
76
68
  /**
@@ -79,7 +71,10 @@ class TransactionalMessage extends MetadataType {
79
71
  * @returns {Promise.<TYPE.MetadataTypeMapObj>} Promise of metadata
80
72
  */
81
73
  static retrieveForCache() {
82
- return super.retrieveREST(null, '/messaging/v1/' + this.subType + '/definitions/');
74
+ // the call to /messaging/v1/email/definitions/ does not return definitionId
75
+ // definitionId is required for resolving dependencies on interactions.
76
+ // we should therefore use the already defined retrieve method
77
+ return this.retrieve();
83
78
  }
84
79
  /**
85
80
  * Updates a single item
@@ -106,13 +101,11 @@ class TransactionalMessage extends MetadataType {
106
101
  /**
107
102
  * Delete a metadata item from the specified business unit
108
103
  *
109
- * @param {TYPE.BuObject} buObject references credentials
110
104
  * @param {string} key Identifier of item
111
105
  * @returns {Promise.<boolean>} deletion success status
112
106
  */
113
- static deleteByKey(buObject, key) {
107
+ static deleteByKey(key) {
114
108
  return super.deleteByKeyREST(
115
- buObject,
116
109
  '/messaging/v1/' + this.subType + '/definitions/' + key,
117
110
  key,
118
111
  false
@@ -17,16 +17,15 @@ class TransactionalSMS extends TransactionalMessage {
17
17
  /**
18
18
  * clean up after deleting a metadata item
19
19
  *
20
- * @param {TYPE.BuObject} buObject references credentials
21
20
  * @param {string} customerKey Identifier of metadata item
22
21
  * @returns {void}
23
22
  */
24
- static async postDeleteTasks(buObject, customerKey) {
23
+ static async postDeleteTasks(customerKey) {
25
24
  // delete local copy: retrieve/cred/bu/type/...json
26
25
  const fileName = File.normalizePath([
27
26
  this.properties.directories.retrieve,
28
- buObject.credential,
29
- buObject.businessUnit,
27
+ this.buObject.credential,
28
+ this.buObject.businessUnit,
30
29
  this.definition.type,
31
30
  `${customerKey}.${this.definition.type}-meta.`,
32
31
  ]);
@@ -5,6 +5,12 @@ const MetadataType = require('./MetadataType');
5
5
  const Util = require('../util/util');
6
6
  const cache = require('../util/cache');
7
7
 
8
+ const cacheTypes = {
9
+ asset: require('./Asset'),
10
+ folder: require('./Folder'),
11
+ list: require('./List'),
12
+ };
13
+
8
14
  /**
9
15
  * MessageSendActivity MetadataType
10
16
  *
@@ -17,26 +23,16 @@ class TriggeredSendDefinition extends MetadataType {
17
23
  * @param {string} retrieveDir Directory where retrieved metadata directory will be saved
18
24
  * @param {void} [_] unused parameter
19
25
  * @param {void} [__] unused parameter
20
- * @param {void} [___] unused parameter
21
26
  * @param {string} [key] customer key of single item to retrieve
22
27
  * @returns {Promise.<TYPE.MetadataTypeMapObj>} Promise of metadata
23
28
  */
24
- static retrieve(retrieveDir, _, __, ___, key) {
29
+ static retrieve(retrieveDir, _, __, key) {
25
30
  /** @type {TYPE.SoapRequestParams} */
26
31
  let requestParams = {
27
32
  filter: {
28
- leftOperand: {
29
- // somehow that parameter controls visible (non deleted?) email send activities
30
- leftOperand: 'IsPlatformObject',
31
- operator: 'equals',
32
- rightOperand: false,
33
- },
34
- operator: 'AND',
35
- rightOperand: {
36
- leftOperand: 'TriggeredSendStatus',
37
- operator: 'IN',
38
- rightOperand: ['New', 'Active', 'Inactive'], // New, Active=Running, Inactive=Paused, (Deleted)
39
- },
33
+ leftOperand: 'TriggeredSendStatus',
34
+ operator: 'IN',
35
+ rightOperand: ['New', 'Active', 'Inactive', 'Moved', 'Canceled'], // New, Active=Running, Inactive=Paused, (Deleted)
40
36
  },
41
37
  };
42
38
  if (key) {
@@ -54,7 +50,7 @@ class TriggeredSendDefinition extends MetadataType {
54
50
  };
55
51
  }
56
52
 
57
- return super.retrieveSOAP(retrieveDir, null, requestParams);
53
+ return super.retrieveSOAP(retrieveDir, requestParams);
58
54
  }
59
55
 
60
56
  /**
@@ -71,73 +67,75 @@ class TriggeredSendDefinition extends MetadataType {
71
67
  * Updates a single TSD.
72
68
  *
73
69
  * @param {TYPE.MetadataTypeItem} metadata single metadata entry
70
+ * @param {boolean} [handleOutside] if the API reponse is irregular this allows you to handle it outside of this generic method
74
71
  * @returns {Promise} Promise
75
72
  */
76
- static update(metadata) {
73
+ static update(metadata, handleOutside) {
77
74
  // * in case of update and active definition, we need to pause first.
78
- // * this should be done manually to not accidentally purge production queues
79
- return super.updateSOAP(metadata);
75
+ // * this should be done manually to not accidentally pause production queues without restarting them
76
+ return super.updateSOAP(metadata, null, handleOutside);
80
77
  }
81
78
 
82
79
  /**
83
80
  * Delete a metadata item from the specified business unit
84
81
  *
85
- * @param {TYPE.BuObject} buObject references credentials
86
82
  * @param {string} customerKey Identifier of data extension
87
83
  * @returns {Promise.<boolean>} deletion success status
88
84
  */
89
- static deleteByKey(buObject, customerKey) {
90
- return super.deleteByKeySOAP(buObject, customerKey, false);
85
+ static deleteByKey(customerKey) {
86
+ return super.deleteByKeySOAP(customerKey, false);
91
87
  }
92
88
 
93
89
  /**
94
- * checks if the current metadata entry should be saved on retrieve or not
90
+ * manages post retrieve steps
95
91
  *
96
- * @static
97
- * @param {object} metadataEntry metadata entry
98
- * @returns {boolean} if false, do not save
99
- * @memberof MetadataType
92
+ * @param {TYPE.MetadataTypeItem} metadata a single query
93
+ * @returns {TYPE.MetadataTypeItem} Array with one metadata object and one query string
100
94
  */
101
- static isFiltered(metadataEntry) {
95
+ static postRetrieveTasks(metadata) {
96
+ return this.parseMetadata(metadata);
97
+ }
98
+ /**
99
+ * generic script that retrieves the folder path from cache and updates the given metadata with it after retrieve
100
+ *
101
+ * @param {TYPE.MetadataTypeItem} metadata a single script activity definition
102
+ */
103
+ static setFolderPath(metadata) {
102
104
  try {
103
- // get folder path to be able to filter journey-created TSDs
104
- const folderPath = cache.searchForField(
105
+ metadata.r__folder_Path = cache.searchForField(
105
106
  'folder',
106
- metadataEntry.CategoryID,
107
+ metadata[this.definition.folderIdField],
107
108
  'ID',
108
109
  'Path'
109
110
  );
110
-
111
- if (folderPath?.startsWith('Journey Builder Sends/')) {
112
- // filter out any triggered sends that were auto-created by journeys
113
- return true;
114
- }
115
- } catch {
116
- // handle it in parseMetadata()
111
+ delete metadata[this.definition.folderIdField];
112
+ } catch (ex) {
113
+ Util.logger.verbose(
114
+ ` - skipping ${this.definition.type} '${metadata[this.definition.nameField]}' (${
115
+ metadata[this.definition.keyField]
116
+ }): Could not find folder (${ex.message})`
117
+ );
118
+ throw ex;
117
119
  }
118
- return false;
119
- }
120
-
121
- /**
122
- * manages post retrieve steps
123
- *
124
- * @param {TYPE.MetadataTypeItem} metadata a single query
125
- * @returns {TYPE.MetadataTypeItem} Array with one metadata object and one query string
126
- */
127
- static postRetrieveTasks(metadata) {
128
- return this.parseMetadata(metadata);
129
120
  }
130
121
  /**
131
122
  * parses retrieved Metadata before saving
132
123
  *
133
124
  * @param {TYPE.MetadataTypeItem} metadata a single query activity definition
134
- * @returns {TYPE.MetadataTypeItem} Array with one metadata object and one sql string
125
+ * @returns {TYPE.MetadataTypeItem | void} Array with one metadata object and one sql string
135
126
  */
136
127
  static parseMetadata(metadata) {
137
128
  // remove IsPlatformObject, always has to be 'false'
138
129
  delete metadata.IsPlatformObject;
139
130
  // folder
140
- super.setFolderPath(metadata);
131
+ try {
132
+ this.setFolderPath(metadata);
133
+ } catch {
134
+ Util.logger.verbose(
135
+ ` - skipping ${this.definition.typeName} '${metadata.Name}'/'${metadata.CustomerKey}': Could not find folder.`
136
+ );
137
+ return;
138
+ }
141
139
 
142
140
  // email
143
141
  try {
@@ -164,9 +162,10 @@ class TriggeredSendDefinition extends MetadataType {
164
162
  metadata.r__assetMessage_Key = contentBuilderEmailKey;
165
163
  delete metadata.Email;
166
164
  } catch {
167
- Util.logger.warn(
168
- ` - ${this.definition.typeName} '${metadata.Name}'/'${metadata.CustomerKey}': Could not find email with ID ${metadata.Email.ID} in Classic nor in Content Builder.`
165
+ Util.logger.verbose(
166
+ ` - skipping ${this.definition.typeName} '${metadata.Name}'/'${metadata.CustomerKey}': Could not find email with ID ${metadata.Email.ID} in Classic nor in Content Builder.`
169
167
  );
168
+ return;
170
169
  }
171
170
  }
172
171
  // List (optional)
@@ -175,9 +174,10 @@ class TriggeredSendDefinition extends MetadataType {
175
174
  metadata.r__list_PathName = cache.getListPathName(metadata.List.ID, 'ID');
176
175
  delete metadata.List;
177
176
  } catch (ex) {
178
- Util.logger.warn(
179
- ` - ${this.definition.typeName} '${metadata.Name}'/'${metadata.CustomerKey}': ${ex.message}`
177
+ Util.logger.verbose(
178
+ ` - skipping ${this.definition.typeName} '${metadata.Name}'/'${metadata.CustomerKey}': ${ex.message}`
180
179
  );
180
+ return;
181
181
  }
182
182
  }
183
183
 
@@ -202,8 +202,7 @@ class TriggeredSendDefinition extends MetadataType {
202
202
  // re-add IsPlatformObject, required for visibility
203
203
  metadata.IsPlatformObject = false;
204
204
  // folder
205
- metadata.CategoryID = cache.searchForField('folder', metadata.r__folder_Path, 'Path', 'ID');
206
- delete metadata.r__folder_Path;
205
+ super.setFolderId(metadata);
207
206
  // email
208
207
  if (metadata.r__email_Name) {
209
208
  // classic
@@ -243,6 +242,183 @@ class TriggeredSendDefinition extends MetadataType {
243
242
 
244
243
  return metadata;
245
244
  }
245
+ /**
246
+ * TSD-specific refresh method that finds active TSDs and refreshes them
247
+ *
248
+ * @param {string[]} [keyArr] metadata keys
249
+ * @returns {Promise.<void>} -
250
+ */
251
+ static async refresh(keyArr) {
252
+ console.time('Time'); // eslint-disable-line no-console
253
+ let checkKey = true;
254
+ if (!keyArr) {
255
+ keyArr = await this._findRefreshableItems();
256
+ checkKey = false;
257
+ }
258
+ // then executes pause, publish, start on them.
259
+ const refreshList = [];
260
+ Util.logger.info(`Refreshing ${keyArr.length} ${this.definition.typeName}...`);
261
+ Util.logger.debug(`Refreshing keys: ${keyArr.join(', ')}`);
262
+ for (const key of keyArr) {
263
+ refreshList.push(this._refreshItem(key, checkKey));
264
+ }
265
+ const successCounter = (await Promise.all(refreshList)).filter(Boolean).length;
266
+ Util.logger.info(`Refreshed ${successCounter} of ${keyArr.length}`);
267
+ console.timeEnd('Time'); // eslint-disable-line no-console
268
+ }
269
+
270
+ /**
271
+ * helper for {@link refresh} that finds active TSDs on the server and filters it by the same rules that {@link retrieve} is using to avoid refreshing TSDs with broken dependencies
272
+ *
273
+ * @returns {Promise.<string[]>} keyArr
274
+ */
275
+ static async _findRefreshableItems() {
276
+ Util.logger.info('Finding refreshable items...');
277
+ // cache dependencies to test for broken links
278
+ // skip deprecated classic emails here, assuming they cannot be updated and hence are not relevant for {@link refresh}
279
+ const requiredCache = {
280
+ folder: [
281
+ 'list',
282
+ 'mysubs',
283
+ 'suppression_list',
284
+ 'publication',
285
+ 'contextual_suppression_list',
286
+ 'triggered_send',
287
+ 'triggered_send_journeybuilder',
288
+ ],
289
+ asset: ['message'],
290
+ list: null,
291
+ };
292
+ for (const [type, subTypeArr] of Object.entries(requiredCache)) {
293
+ if (!cache.getCache()?.[type]) {
294
+ Util.logger.info(` - Caching dependent Metadata: ${type}`);
295
+ Util.logSubtypes(subTypeArr);
296
+ cacheTypes[type].client = this.client;
297
+ cacheTypes[type].buObject = this.buObject;
298
+ cacheTypes[type].properties = this.properties;
299
+
300
+ const result = await cacheTypes[type].retrieveForCache(null, subTypeArr);
301
+ cache.setMetadata(type, result.metadata);
302
+ }
303
+ }
304
+ // cache ACTIVE triggeredSends from the server
305
+ /** @type {TYPE.SoapRequestParams} */
306
+ const requestParams = {
307
+ filter: {
308
+ leftOperand: 'TriggeredSendStatus',
309
+ operator: 'IN',
310
+ rightOperand: ['dummy', 'Active'], // using equals does not work for this field for an unknown reason and IN requires at least 2 values, hence the 'dummy' entry
311
+ },
312
+ };
313
+ const metadata = (await super.retrieveSOAP(null, requestParams)).metadata;
314
+ const keyArr = Object.keys(metadata).filter((key) => {
315
+ const test = this.postRetrieveTasks(metadata[key]);
316
+ return test?.CustomerKey || false;
317
+ });
318
+ Util.logger.info(`Found ${keyArr.length} refreshable items.`);
319
+ return keyArr;
320
+ }
321
+
322
+ /**
323
+ * helper for {@link refresh} that pauses, publishes and starts a triggered send
324
+ *
325
+ * @param {string} key external key of triggered send item
326
+ * @param {boolean} checkKey whether to check if key exists on the server
327
+ * @returns {Promise.<boolean>} true if refresh was successful
328
+ */
329
+ static async _refreshItem(key, checkKey) {
330
+ const item = {};
331
+ let test;
332
+ item[this.definition.keyField] = key;
333
+ // check triggeredSend-key exists on the server AND its status==ACTIVE
334
+ if (checkKey) {
335
+ /** @type {TYPE.SoapRequestParams} */
336
+ const requestParams = {
337
+ filter: {
338
+ leftOperand: 'CustomerKey',
339
+ operator: 'equals',
340
+ rightOperand: key,
341
+ },
342
+ };
343
+ try {
344
+ test = (
345
+ await super.retrieveSOAP(null, requestParams, [
346
+ 'CustomerKey',
347
+ 'TriggeredSendStatus',
348
+ ])
349
+ )?.metadata;
350
+ } catch (ex) {
351
+ const errorMsg = super.getSOAPErrorMsg(ex);
352
+ Util.logger.error(` ☇ skipping ${this.definition.typeName}: ${key} - ${errorMsg}}`);
353
+ return false;
354
+ }
355
+ if (!test[key]) {
356
+ Util.logger.error(
357
+ ` ☇ skipping ${this.definition.typeName}: ${key} - not found on server`
358
+ );
359
+ return false;
360
+ }
361
+ if (test[key].TriggeredSendStatus !== 'Active') {
362
+ Util.logger.error(
363
+ ` ☇ skipping ${this.definition.typeName}: ${key} - refresh only needed for running entries (TriggeredSendStatus=Active)`
364
+ );
365
+ return false;
366
+ }
367
+ }
368
+
369
+ // pause
370
+ try {
371
+ item.TriggeredSendStatus = 'Inactive';
372
+ test = await this.update(item, true);
373
+ if (test.OverallStatus !== 'OK') {
374
+ throw new Error(test.Results[0].StatusMessage);
375
+ }
376
+ delete item.TriggeredSendStatus;
377
+ Util.logger.info(` - paused ${this.definition.typeName}: ${key}`);
378
+ } catch (ex) {
379
+ const errorMsg = super.getSOAPErrorMsg(ex);
380
+
381
+ Util.logger.error(
382
+ ` - failed to pause ${this.definition.typeName}: ${key} - ${errorMsg}`
383
+ );
384
+ return false;
385
+ }
386
+
387
+ // publish
388
+ try {
389
+ item.RefreshContent = 'true';
390
+ test = await this.update(item, true);
391
+ if (test.OverallStatus !== 'OK') {
392
+ throw new Error(test.Results[0].StatusMessage);
393
+ }
394
+ delete item.RefreshContent;
395
+ Util.logger.info(` - published ${this.definition.typeName}: ${key}`);
396
+ } catch (ex) {
397
+ const errorMsg = super.getSOAPErrorMsg(ex);
398
+ Util.logger.error(
399
+ ` - failed to publish ${this.definition.typeName}: ${key} - ${errorMsg}`
400
+ );
401
+ return false;
402
+ }
403
+
404
+ // start
405
+ try {
406
+ item.TriggeredSendStatus = 'Active';
407
+ test = await this.update(item, true);
408
+ if (test.OverallStatus !== 'OK') {
409
+ throw new Error(test.Results[0].StatusMessage);
410
+ }
411
+ delete item.RefreshContent;
412
+ Util.logger.info(` - started ${this.definition.typeName}: ${key}`);
413
+ } catch (ex) {
414
+ const errorMsg = super.getSOAPErrorMsg(ex);
415
+ Util.logger.error(
416
+ ` - failed to publish ${this.definition.typeName}: ${key} - ${errorMsg}`
417
+ );
418
+ return false;
419
+ }
420
+ return true;
421
+ }
246
422
  }
247
423
 
248
424
  // Assign definition to static attributes