mcdev 4.2.0 → 4.3.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 (96) hide show
  1. package/.github/ISSUE_TEMPLATE/bug.yml +2 -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 +703 -283
  9. package/lib/Deployer.js +21 -15
  10. package/lib/Retriever.js +23 -19
  11. package/lib/cli.js +36 -6
  12. package/lib/index.js +57 -11
  13. package/lib/metadataTypes/AccountUser.js +17 -23
  14. package/lib/metadataTypes/Asset.js +28 -21
  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 +5 -8
  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 +678 -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 +2 -1
  58. package/lib/metadataTypes/definitions/TransactionalPush.definition.js +2 -1
  59. package/lib/metadataTypes/definitions/TransactionalSMS.definition.js +2 -1
  60. package/lib/metadataTypes/definitions/TriggeredSendDefinition.definition.js +10 -2
  61. package/lib/util/auth.js +10 -2
  62. package/lib/util/cli.js +4 -1
  63. package/lib/util/config.js +3 -2
  64. package/lib/util/file.js +7 -3
  65. package/lib/util/init.js +65 -3
  66. package/lib/util/util.js +131 -11
  67. package/package.json +22 -9
  68. package/test/dataExtension.test.js +10 -10
  69. package/test/interaction.test.js +123 -0
  70. package/test/mockRoot/.mcdevrc.json +1 -1
  71. package/test/mockRoot/deploy/testInstance/testBU/interaction/testExisting_interaction.interaction-meta.json +266 -0
  72. package/test/mockRoot/deploy/testInstance/testBU/interaction/testNew_interaction.interaction-meta.json +266 -0
  73. package/test/mockRoot/deploy/testInstance/testBU/transactionalEmail/testExisting_temail.transactionalEmail-meta.json +0 -3
  74. package/test/query.test.js +8 -8
  75. package/test/resourceFactory.js +12 -7
  76. package/test/resources/1111111/dataExtension/retrieve-response.xml +26 -0
  77. package/test/resources/9999999/data/v1/customobjectdata/key/childBU_dataextension_test/rowset/get-response.json +13 -0
  78. package/test/resources/9999999/dataFolder/retrieve-response.xml +22 -0
  79. package/test/resources/9999999/eventDefinition/get-expected.json +34 -0
  80. package/test/resources/9999999/interaction/build-expected.json +260 -0
  81. package/test/resources/9999999/interaction/get-expected.json +264 -0
  82. package/test/resources/9999999/interaction/post-expected.json +264 -0
  83. package/test/resources/9999999/interaction/put-expected.json +264 -0
  84. package/test/resources/9999999/interaction/template-expected.json +260 -0
  85. package/test/resources/9999999/interaction/v1/EventDefinitions/get-response.json +43 -0
  86. package/test/resources/9999999/interaction/v1/interactions/get-response.json +222 -3
  87. package/test/resources/9999999/interaction/v1/interactions/post-response.json +280 -0
  88. package/test/resources/9999999/interaction/v1/interactions/put-response.json +280 -0
  89. package/test/resources/9999999/messaging/v1/email/definitions/post-response.json +1 -1
  90. package/test/resources/9999999/query/post-expected.sql +1 -1
  91. package/test/resources/9999999/transactionalEmail/post-expected.json +1 -1
  92. package/test/transactionalEmail.test.js +7 -7
  93. package/test/transactionalPush.test.js +7 -7
  94. package/test/transactionalSMS.test.js +7 -7
  95. package/test/utils.js +50 -0
  96. package/types/mcdev.d.js +1 -0
@@ -2,34 +2,700 @@
2
2
 
3
3
  const TYPE = require('../../types/mcdev.d');
4
4
  const MetadataType = require('./MetadataType');
5
+ const TransactionalEmail = require('./TransactionalEmail');
6
+ const Util = require('../util/util');
7
+ const cache = require('../util/cache');
8
+ const File = require('../util/file');
5
9
 
6
10
  /**
7
- * Script MetadataType
11
+ * Interaction MetadataType
12
+ * ! BETA RELEASE of journey support (v4.3.0); it so far only resolves a limited amount of dependencies and will likely break during cross-BU deployments!
13
+ * id: A unique id of the journey assigned by the journey’s API during its creation
14
+ * key: A unique id of the journey within the MID. Can be generated by the developer
15
+ * definitionId: A unique UUID provided by Salesforce Marketing Cloud. Each version of a journey has a unique DefinitionID while the Id and Key remain the same. Version 1 will have id == definitionId
8
16
  *
9
17
  * @augments MetadataType
10
18
  */
11
19
  class Interaction extends MetadataType {
12
20
  /**
13
21
  * Retrieves Metadata of Interaction
14
- * Endpoint /interaction/v1/interactions?extras=all&pageSize=50000 return 50000 Scripts with all details.
15
22
  *
16
23
  * @param {string} retrieveDir Directory where retrieved metadata directory will be saved
17
24
  * @param {void} [_] unused parameter
18
25
  * @param {void} [__] unused parameter
19
- * @param {void} [___] unused parameter
20
26
  * @param {string} [key] customer key of single item to retrieve
21
27
  * @returns {Promise.<TYPE.MetadataTypeMapObj>} Promise
22
28
  */
23
- static retrieve(retrieveDir, _, __, ___, key) {
24
- return super.retrieveREST(
25
- retrieveDir,
26
- `/interaction/v1/interactions${
27
- key ? '/key:' + encodeURIComponent(key) : ''
28
- }?extras=all`,
29
- null,
30
- null,
31
- key
29
+ static async retrieve(retrieveDir, _, __, key) {
30
+ if (retrieveDir) {
31
+ // only print this during retrieve, not during retrieveForCache
32
+ Util.logBeta(this.definition.type);
33
+ }
34
+
35
+ let singleKey = '';
36
+ let mode = 'key';
37
+ if (key) {
38
+ /* eslint-disable unicorn/prefer-ternary */
39
+
40
+ if (key.startsWith('id:') || key.startsWith('%23')) {
41
+ // ! allow selecting journeys by ID because that's what users see in the URL
42
+ // if the key started with %23 assume an ID was copied from the URL but the user forgot to prefix it with id:
43
+
44
+ // remove id: or %23
45
+ singleKey = key.slice(3);
46
+ if (singleKey.startsWith('%23')) {
47
+ // in the journey URL the Id is prefixed with an HTML-encoded "#" which could accidentally be copied by users
48
+ // despite the slicing above, this still needs testing here because users might have prefixed the ID with id: but did not know to remove the #23
49
+ singleKey = singleKey.slice(3);
50
+ }
51
+ if (singleKey.includes('/')) {
52
+ // in the journey URL the version is appended after the ID, separated by a forward-slash. Needs to be removed from the ID for the retrieve as we always aim to retrieve the latest version only
53
+ singleKey = singleKey.split('/')[0];
54
+ }
55
+ mode = 'id';
56
+ } else {
57
+ // assume actual key was provided
58
+ singleKey = 'key:' + encodeURIComponent(key);
59
+ }
60
+ /* eslint-enable unicorn/prefer-ternary */
61
+ }
62
+ // full details for retrieve, only base data for caching; reduces caching time from minutes to seconds
63
+ const extras = retrieveDir ? 'all' : '';
64
+
65
+ try {
66
+ return await super.retrieveREST(
67
+ retrieveDir,
68
+ `/interaction/v1/interactions/${singleKey}?extras=${extras}`,
69
+ null,
70
+ null,
71
+ key
72
+ );
73
+ } catch (ex) {
74
+ // if the interaction does not exist, the API returns an error code which would otherwise bring execution to a hold
75
+ if (
76
+ [
77
+ 'Interaction matching key not found.',
78
+ 'Must provide a valid ID or Key parameter',
79
+ ].includes(ex.message)
80
+ ) {
81
+ Util.logger.info(
82
+ `Downloaded: ${this.definition.type} (0)${Util.getKeysString(
83
+ mode === 'id' ? singleKey : key,
84
+ mode === 'id'
85
+ )}`
86
+ );
87
+ } else {
88
+ throw ex;
89
+ }
90
+ }
91
+ }
92
+ /**
93
+ * Delete a metadata item from the specified business unit
94
+ *
95
+ * @param {string} key Identifier of item
96
+ * @returns {Promise.<boolean>} deletion success status
97
+ */
98
+ static async deleteByKey(key) {
99
+ let version;
100
+ let singleKey = '';
101
+ /* eslint-disable unicorn/prefer-ternary */
102
+ if (key.startsWith('id:') || key.startsWith('%23')) {
103
+ // ! allow selecting journeys by ID because that's what users see in the URL
104
+ // if the key started with %23 assume an ID was copied from the URL but the user forgot to prefix it with id:
105
+
106
+ // remove id: or %23
107
+ singleKey = key.slice(3);
108
+ if (singleKey.startsWith('%23')) {
109
+ // in the journey URL the Id is prefixed with an HTML-encoded "#" which could accidentally be copied by users
110
+ // despite the slicing above, this still needs testing here because users might have prefixed the ID with id: but did not know to remove the #23
111
+ singleKey = singleKey.slice(3);
112
+ }
113
+ if (singleKey.includes('/')) {
114
+ // in the journey URL the version is appended after the ID, separated by a forward-slash.
115
+ [singleKey, version] = singleKey.split('/');
116
+ }
117
+ } else {
118
+ if (key.includes('/')) {
119
+ // in the journey URL the version is appended after the ID, separated by a forward-slash.
120
+ [key, version] = key.split('/');
121
+ }
122
+
123
+ // delete by key with specified version does not work, therefore we need to get the ID first
124
+ const response = await this.client.rest.get(
125
+ `/interaction/v1/interactions/key:${encodeURIComponent(key)}?extras=`
126
+ );
127
+ const results = this.parseResponseBody(response, key);
128
+ singleKey = results[key].id;
129
+ Util.logger.debug(`Deleting interaction ${key} via its ID ${singleKey}`);
130
+ }
131
+ if (!/^\d+$/.test(version)) {
132
+ throw new TypeError(
133
+ 'Version is required for deleting interactions to avoid accidental deletion of the wrong item. Please append it at the end of the key or id, separated by forward-slash. Example for deleting version 4: ' +
134
+ key +
135
+ '/4'
136
+ );
137
+ }
138
+ Util.logger.warn(
139
+ `Deleting Interactions via this command breaks following retrieve-by-key/id requests until you've deployed/created a new draft version! You can get still get the latest available version of your journey by retrieving all interactions on this BU.`
32
140
  );
141
+ /* eslint-enable unicorn/prefer-ternary */
142
+ return super.deleteByKeyREST(
143
+ '/interaction/v1/interactions/' + singleKey + `?versionNumber=${version}`,
144
+ key,
145
+ false
146
+ );
147
+ }
148
+ /**
149
+ * Deploys metadata - merely kept here to be able to print {@link Util.logBeta} once per deploy
150
+ *
151
+ * @param {TYPE.MetadataTypeMap} metadata metadata mapped by their keyField
152
+ * @param {string} deployDir directory where deploy metadata are saved
153
+ * @param {string} retrieveDir directory where metadata after deploy should be saved
154
+ * @returns {Promise.<TYPE.MetadataTypeMap>} Promise of keyField => metadata map
155
+ */
156
+ static async deploy(metadata, deployDir, retrieveDir) {
157
+ Util.logBeta(this.definition.type);
158
+ return super.deploy(metadata, deployDir, retrieveDir);
159
+ }
160
+
161
+ /**
162
+ * Updates a single item
163
+ *
164
+ * @param {TYPE.MetadataTypeItem} metadata a single item
165
+ * @returns {Promise} Promise
166
+ */
167
+ static update(metadata) {
168
+ return super.updateREST(metadata, '/interaction/v1/interactions/', true);
169
+ }
170
+
171
+ /**
172
+ * Creates a single item
173
+ *
174
+ * @param {TYPE.MetadataTypeItem} metadata a single item
175
+ * @returns {Promise} Promise
176
+ */
177
+ static create(metadata) {
178
+ return super.createREST(metadata, '/interaction/v1/interactions/');
179
+ }
180
+ /**
181
+ * Helper for writing Metadata to disk, used for Retrieve and deploy
182
+ *
183
+ * @param {TYPE.MetadataTypeMap} results metadata results from deploy
184
+ * @param {string} retrieveDir directory where metadata should be stored after deploy/retrieve
185
+ * @param {string} [overrideType] for use when there is a subtype (such as folder-queries)
186
+ * @param {TYPE.TemplateMap} [templateVariables] variables to be replaced in the metadata
187
+ * @returns {Promise.<TYPE.MetadataTypeMap>} Promise of saved metadata
188
+ */
189
+ static async saveResults(results, retrieveDir, overrideType, templateVariables) {
190
+ if (Object.keys(results).length) {
191
+ // only execute the following if records were found
192
+ await this._postRetrieveTasksBulk(results);
193
+ }
194
+ return super.saveResults(results, retrieveDir, overrideType, templateVariables);
195
+ }
196
+
197
+ /**
198
+ * helper for Interaction's {@link saveResults}. Gets executed after retreive of metadata type and
199
+ *
200
+ * @param {TYPE.MetadataTypeMap} metadataMap key=customer key, value=metadata
201
+ */
202
+ static async _postRetrieveTasksBulk(metadataMap) {
203
+ let needTransactionalEmail = false;
204
+ for (const key in metadataMap) {
205
+ if (metadataMap[key].definitionType == 'Transactional') {
206
+ needTransactionalEmail = true;
207
+ break;
208
+ }
209
+ }
210
+ if (needTransactionalEmail && !cache.getCache()?.transactionalEmail) {
211
+ // ! interaction and transactionalEmail both link to each other. caching transactionalEmail here "manually", assuming that it's quicker than the other way round
212
+ Util.logger.info(' - Caching dependent Metadata: transactionalEmail');
213
+ TransactionalEmail.buObject = this.buObject;
214
+ TransactionalEmail.client = this.client;
215
+ TransactionalEmail.properties = this.properties;
216
+ const result = await TransactionalEmail.retrieveForCache();
217
+ cache.setMetadata('transactionalEmail', result.metadata);
218
+ }
219
+ }
220
+
221
+ /**
222
+ * manages post retrieve steps
223
+ * ! BETA RELEASE of journey support (v4.3.0); it so far only resolves a limited amount of dependencies and will likely break during cross-BU deployments!
224
+ *
225
+ * @param {TYPE.MetadataTypeItem} metadata a single query
226
+ * @returns {TYPE.MetadataTypeItem} Array with one metadata object and one query string
227
+ */
228
+ static postRetrieveTasks(metadata) {
229
+ // folder
230
+ super.setFolderPath(metadata);
231
+
232
+ switch (metadata.definitionType) {
233
+ case 'Multistep': {
234
+ // Multi-Step Journey
235
+ // ~~~ TRIGGERS ~~~~
236
+ // eventDefinition / definitionType==='Multistep' && channel==='' && triggers[].type === 'EmailAudience'|'APIEvent'
237
+ if (
238
+ metadata.triggers?.length > 0 &&
239
+ metadata.triggers[0].metaData?.eventDefinitionKey
240
+ ) {
241
+ // trigger found; there can only be one entry in this array
242
+ try {
243
+ const edId = cache.searchForField(
244
+ 'eventDefinition',
245
+ metadata.triggers[0].metaData.eventDefinitionKey,
246
+ 'eventDefinitionKey',
247
+ 'id'
248
+ );
249
+ if (metadata.triggers[0].metaData.eventDefinitionId !== edId) {
250
+ throw new Error(
251
+ `eventDefinitionId not matching Id found on eventDefinition with key in eventDefinitionKey`
252
+ );
253
+ }
254
+ delete metadata.triggers[0].metaData.eventDefinitionId;
255
+ } catch (ex) {
256
+ Util.logger.warn(
257
+ ` - ${this.definition.type} ${metadata[this.definition.nameField]} (${
258
+ metadata[this.definition.keyField]
259
+ }): ${ex.message}.`
260
+ );
261
+ }
262
+ }
263
+
264
+ // ~~~ ACTIVITIES ~~~~
265
+
266
+ // triggeredSend + email+asset / activities[].type === 'EMAILV2'
267
+ // TODO email / asset
268
+ for (const item of metadata.activities) {
269
+ // check if all triggeredSends are there
270
+ try {
271
+ if (item.configurationArguments?.triggeredSendKey) {
272
+ // triggeredSendKey is not always set but triggeredSendId is
273
+ cache.searchForField(
274
+ 'triggeredSendDefinition',
275
+ item.configurationArguments.triggeredSendKey,
276
+ 'CustomerKey',
277
+ 'CustomerKey'
278
+ );
279
+ delete item.configurationArguments.triggeredSendId;
280
+ } else if (item.configurationArguments?.triggeredSendId) {
281
+ // triggeredSendKey is not always set but triggeredSendId is
282
+ item.configurationArguments.triggeredSendKey = cache.searchForField(
283
+ 'triggeredSendDefinition',
284
+ item.configurationArguments.triggeredSendId,
285
+ 'ObjectID',
286
+ 'CustomerKey'
287
+ );
288
+ delete item.configurationArguments.triggeredSendId;
289
+ }
290
+ } catch (ex) {
291
+ Util.logger.warn(
292
+ ` - ${this.definition.type} '${metadata[this.definition.nameField]}' (${
293
+ metadata[this.definition.keyField]
294
+ }): Could not find triggeredSendDefinition (${ex.message})`
295
+ );
296
+ }
297
+ }
298
+
299
+ // TODO: Filters / activities[].type === 'MULTICRITERIADECISION'
300
+ // - activities[].arguments.filterResult
301
+ // - activities[].arguments.configurationArguments.criteria
302
+
303
+ // TODO: wait activity / activities[].type === 'WAIT'
304
+
305
+ // TODO: journey template id? / metaData.templateId
306
+ break;
307
+ }
308
+ case 'Quicksend': {
309
+ // Single Send Journey
310
+ Util.logger.warn(
311
+ ` - ${this.definition.type} ${metadata[this.definition.nameField]} (${
312
+ metadata[this.definition.keyField]
313
+ }): definitionType Quicksend is not fully supported yet.`
314
+ );
315
+ // ~~~ TRIGGERS ~~~~
316
+ // eventDefinition && triggers[].type === 'ContactAudience'
317
+ if (
318
+ metadata.triggers?.length > 0 &&
319
+ metadata.triggers[0].metaData?.eventDefinitionKey
320
+ ) {
321
+ // trigger found; there can only be one entry in this array
322
+ try {
323
+ const edId = cache.searchForField(
324
+ 'eventDefinition',
325
+ metadata.triggers[0].metaData.eventDefinitionKey,
326
+ 'eventDefinitionKey',
327
+ 'id'
328
+ );
329
+ if (metadata.triggers[0].metaData.eventDefinitionId !== edId) {
330
+ throw new Error(
331
+ ` - ${this.definition.type} ${
332
+ metadata[this.definition.nameField]
333
+ } (${
334
+ metadata[this.definition.keyField]
335
+ }): eventDefinitionId not matching Id found on eventDefinition with key in eventDefinitionKey`
336
+ );
337
+ }
338
+ delete metadata.triggers[0].metaData.eventDefinitionId;
339
+ } catch (ex) {
340
+ Util.logger.warn(
341
+ ` - ${this.definition.type} ${metadata[this.definition.nameField]} (${
342
+ metadata[this.definition.keyField]
343
+ }): ${ex.message}.`
344
+ );
345
+ }
346
+ }
347
+
348
+ // ~~~ ACTIVITIES ~~~~
349
+ try {
350
+ // TODO channel=='email'
351
+ // TODO channel=='sms'
352
+ // TODO channel=='push' / activities[].type === 'PUSHNOTIFICATIONACTIVITY'
353
+ } catch (ex) {
354
+ Util.logger.warn(
355
+ ` - ${this.definition.type} ${metadata[this.definition.nameField]} (${
356
+ metadata[this.definition.keyField]
357
+ }): ${ex.message}.`
358
+ );
359
+ }
360
+ break;
361
+ }
362
+ case 'Transactional': {
363
+ // Transactional Send Journey
364
+ // ~~~ TRIGGERS ~~~~
365
+ // ! journeys so far only supports transactional EMAIL messages. SMS and Push do not create their own journey.
366
+ // ! transactional (email) journeys only have a dummy trigger without real content.
367
+ // transactionalEmail / definitionType==='Transactional' && channel==='email' && triggers[].type === 'transactional-api'
368
+ // --> nothing to do here
369
+
370
+ // ~~~ ACTIVITIES ~~~~
371
+ // ! transactional (email) journeys only have one activity (type=EMAILV2) which links back to the transactionalEmail ()
372
+ switch (metadata.channel) {
373
+ case 'email': {
374
+ if (
375
+ metadata.activities?.length > 0 &&
376
+ metadata.activities[0].configurationArguments?.triggeredSendKey
377
+ ) {
378
+ // trigger found; there can only be one entry in this array
379
+ try {
380
+ const tEmailId = cache.searchForField(
381
+ 'transactionalEmail',
382
+ metadata.activities[0].configurationArguments?.triggeredSendKey,
383
+ 'definitionKey',
384
+ 'definitionId'
385
+ );
386
+ if (
387
+ tEmailId !=
388
+ metadata.activities[0].configurationArguments?.triggeredSendId
389
+ ) {
390
+ throw new Error(
391
+ ` - ${this.definition.type} ${
392
+ metadata[this.definition.nameField]
393
+ } (${
394
+ metadata[this.definition.keyField]
395
+ }): transactionalEmailId not matching Id found on transactionalEmail with key in transactionalEmailKey`
396
+ );
397
+ }
398
+ if (
399
+ metadata.activities[0].metaData?.highThroughput
400
+ ?.definitionKey &&
401
+ metadata.activities[0].metaData?.highThroughput
402
+ ?.definitionKey !=
403
+ metadata.activities[0].configurationArguments
404
+ ?.triggeredSendKey
405
+ ) {
406
+ throw new Error(
407
+ ` - ${this.definition.type} ${
408
+ metadata[this.definition.nameField]
409
+ } (${
410
+ metadata[this.definition.keyField]
411
+ }): metaData.highThroughput.definitionKey not matching key in configurationArguments.transactionalEmailKey`
412
+ );
413
+ }
414
+ } catch (ex) {
415
+ Util.logger.warn(
416
+ ` - ${this.definition.type} ${
417
+ metadata[this.definition.nameField]
418
+ } (${metadata[this.definition.keyField]}): ${ex.message}.`
419
+ );
420
+ }
421
+ }
422
+
423
+ break;
424
+ }
425
+ default: {
426
+ // it is expected that we'll see 'sms' and 'push' here in the future
427
+ Util.logger.warn(
428
+ ` - ${this.definition.type} ${metadata[this.definition.nameField]} (${
429
+ metadata[this.definition.keyField]
430
+ }): channel ${
431
+ metadata.channel
432
+ } is not supported yet. Please open a ticket at https://github.com/Accenture/sfmc-devtools/issues/new/choose to request it`
433
+ );
434
+ }
435
+ }
436
+
437
+ break;
438
+ }
439
+ default: {
440
+ Util.logger.warn(
441
+ ` - ${this.definition.type} ${metadata[this.definition.nameField]} (${
442
+ metadata[this.definition.keyField]
443
+ }): definitionType ${
444
+ metadata.definitionType
445
+ } is not supported yet. Please open a ticket at https://github.com/Accenture/sfmc-devtools/issues/new/choose to request it`
446
+ );
447
+ }
448
+ }
449
+
450
+ return metadata;
451
+ }
452
+ /**
453
+ * prepares a TSD for deployment
454
+ * ! BETA RELEASE of journey support (v4.3.0); it so far only resolves a limited amount of dependencies and will likely break during cross-BU deployments!
455
+ *
456
+ * @param {TYPE.MetadataTypeItem} metadata of a single TSD
457
+ * @returns {TYPE.MetadataTypeItem} metadata object
458
+ */
459
+ static async preDeployTasks(metadata) {
460
+ if (metadata.status !== 'Draft') {
461
+ metadata.status !== 'Draft';
462
+ }
463
+
464
+ // folder
465
+ super.setFolderId(metadata);
466
+
467
+ switch (metadata.definitionType) {
468
+ case 'Multistep': {
469
+ // Multi-Step Journey
470
+ // ~~~ TRIGGERS ~~~~
471
+
472
+ // eventDefinition / definitionType==='Multistep' && channel==='' && triggers[].type === 'EmailAudience'|'APIEvent'
473
+ if (
474
+ metadata.triggers?.length > 0 &&
475
+ metadata.triggers[0].metaData?.eventDefinitionKey
476
+ ) {
477
+ // trigger found; there can only be one entry in this array
478
+ metadata.triggers[0].metaData.eventDefinitionId = cache.searchForField(
479
+ 'eventDefinition',
480
+ metadata.triggers[0].metaData.eventDefinitionKey,
481
+ 'eventDefinitionKey',
482
+ 'id'
483
+ );
484
+ }
485
+
486
+ // transactionalEmail / definitionType==='Transactional' && channel==='email' && triggers[].type === 'transactional-api'
487
+
488
+ // ~~~ ACTIVITIES ~~~~
489
+
490
+ // triggeredSend + email+asset / activities[].type === 'EMAILV2'
491
+ // TODO email / asset
492
+ for (const item of metadata.activities) {
493
+ // check if all triggeredSends are there
494
+ if (!item.configurationArguments?.triggeredSendKey) {
495
+ continue;
496
+ }
497
+ // triggeredSendKey is not always set but triggeredSendId is
498
+ item.configurationArguments.triggeredSendId = cache.searchForField(
499
+ 'triggeredSendDefinition',
500
+ item.configurationArguments.triggeredSendKey,
501
+ 'CustomerKey',
502
+ 'ObjectID'
503
+ );
504
+ }
505
+
506
+ // TODO: Filters / activities[].type === 'MULTICRITERIADECISION'
507
+ // - activities[].arguments.filterResult
508
+ // - activities[].arguments.configurationArguments.criteria
509
+
510
+ // TODO: wait activity / activities[].type === 'WAIT'
511
+
512
+ // TODO: journey template id? / metaData.templateId
513
+ break;
514
+ }
515
+ case 'Quicksend': {
516
+ // Single Send Journey
517
+ Util.logger.warn(
518
+ ` - ${this.definition.type} ${metadata[this.definition.nameField]} (${
519
+ metadata[this.definition.keyField]
520
+ }): definitionType Quicksend is not supported yet and might fail to deploy.`
521
+ );
522
+ // ~~~ TRIGGERS ~~~~
523
+ // eventDefinition && triggers[].type === 'ContactAudience'
524
+ if (
525
+ metadata.triggers?.length > 0 &&
526
+ metadata.triggers[0].metaData?.eventDefinitionKey
527
+ ) {
528
+ // trigger found; there can only be one entry in this array
529
+ try {
530
+ metadata.triggers[0].metaData.eventDefinitionId = cache.searchForField(
531
+ 'eventDefinition',
532
+ metadata.triggers[0].metaData?.eventDefinitionKey,
533
+ 'eventDefinitionKey',
534
+ 'id'
535
+ );
536
+ } catch (ex) {
537
+ Util.logger.warn(
538
+ ` - ${this.definition.type} ${metadata[this.definition.nameField]} (${
539
+ metadata[this.definition.keyField]
540
+ }): ${ex.message}.`
541
+ );
542
+ }
543
+ }
544
+
545
+ // ~~~ ACTIVITIES ~~~~
546
+ try {
547
+ // TODO channel=='email'
548
+ // TODO channel=='sms'
549
+ // TODO channel=='push' / activities[].type === 'PUSHNOTIFICATIONACTIVITY'
550
+ } catch (ex) {
551
+ Util.logger.warn(
552
+ ` - ${this.definition.type} ${metadata[this.definition.nameField]} (${
553
+ metadata[this.definition.keyField]
554
+ }): ${ex.message}.`
555
+ );
556
+ }
557
+ break;
558
+ }
559
+ case 'Transactional': {
560
+ // Transactional Send Journey
561
+ // ~~~ TRIGGERS ~~~~
562
+ // ! journeys so far transactional EMAIL messages. SMS and Push do not create their own journey.
563
+ // ! transactional (email) journeys only have a dummy trigger without real content.
564
+
565
+ // transactionalEmail / definitionType==='Transactional' && channel==='email' && triggers[].type === 'transactional-api'
566
+ // --> nothing to do here
567
+
568
+ // ~~~ ACTIVITIES ~~~~
569
+ // ! transactional (email) journeys only have one activity (type=EMAILV2) which links back to the transactionalEmail ()
570
+ switch (metadata.channel) {
571
+ case 'email': {
572
+ if (
573
+ metadata.activities?.length > 0 &&
574
+ metadata.activities[0].configurationArguments?.triggeredSendKey
575
+ ) {
576
+ // trigger found; there can only be one entry in this array
577
+ metadata.activities[0].configurationArguments.triggeredSendId =
578
+ cache.searchForField(
579
+ 'transactionalEmail',
580
+ metadata.activities[0].configurationArguments?.triggeredSendKey,
581
+ 'definitionKey',
582
+ 'definitionId'
583
+ );
584
+ if (
585
+ metadata.activities[0].metaData?.highThroughput?.definitionKey &&
586
+ metadata.activities[0].metaData?.highThroughput?.definitionKey !=
587
+ metadata.activities[0].configurationArguments?.triggeredSendKey
588
+ ) {
589
+ throw new Error(
590
+ ` - ${this.definition.type} ${
591
+ metadata[this.definition.nameField]
592
+ } (${
593
+ metadata[this.definition.keyField]
594
+ }): metaData.highThroughput.definitionKey not matching key in configurationArguments.transactionalEmailKey`
595
+ );
596
+ }
597
+ }
598
+
599
+ break;
600
+ }
601
+ default: {
602
+ // it is expected that we'll see 'sms' and 'push' here in the future
603
+ throw new Error(
604
+ ` - ${this.definition.type} ${metadata[this.definition.nameField]} (${
605
+ metadata[this.definition.keyField]
606
+ }): channel ${
607
+ metadata.channel
608
+ } is not supported yet. Please open a ticket at https://github.com/Accenture/sfmc-devtools/issues/new/choose to request it`
609
+ );
610
+ }
611
+ }
612
+
613
+ break;
614
+ }
615
+ default: {
616
+ throw new Error(
617
+ ` - ${this.definition.type} ${metadata[this.definition.nameField]} (${
618
+ metadata[this.definition.keyField]
619
+ }): definitionType ${
620
+ metadata.definitionType
621
+ } is not supported yet. Please open a ticket at https://github.com/Accenture/sfmc-devtools/issues/new/choose to request it`
622
+ );
623
+ }
624
+ }
625
+ return metadata;
626
+ }
627
+
628
+ /**
629
+ *
630
+ * @param {TYPE.MetadataTypeItem} metadata single metadata itme
631
+ * @param {string} metadataKey key of item we are looking at
632
+ * @param {boolean} hasError error flag from previous code
633
+ * @param {TYPE.MetadataTypeItemDiff[]} metadataToUpdate list of items to update
634
+ * @param {TYPE.MetadataTypeItem[]} metadataToCreate list of items to create
635
+ */
636
+ static createOrUpdate(metadata, metadataKey, hasError, metadataToUpdate, metadataToCreate) {
637
+ const normalizedKey = File.reverseFilterIllegalFilenames(
638
+ metadata[metadataKey][this.definition.keyField]
639
+ );
640
+ // Update if it already exists; Create it if not
641
+ if (Util.logger.level === 'debug' && metadata[metadataKey][this.definition.idField]) {
642
+ // TODO: re-evaluate in future releases if & when we managed to solve folder dependencies once and for all
643
+ // only used if resource is excluded from cache and we still want to update it
644
+ // needed e.g. to rewire lost folders
645
+ Util.logger.warn(
646
+ ' - Hotfix for non-cachable resource found in deploy folder. Trying update:'
647
+ );
648
+ Util.logger.warn(JSON.stringify(metadata[metadataKey]));
649
+ if (hasError) {
650
+ metadataToUpdate.push(null);
651
+ } else {
652
+ metadataToUpdate.push({
653
+ before: {},
654
+ after: metadata[metadataKey],
655
+ });
656
+ }
657
+ } else {
658
+ const cachedVersion = cache.getByKey(this.definition.type, normalizedKey);
659
+ if (cachedVersion && cachedVersion.status === 'Draft') {
660
+ // normal way of processing update files
661
+ if (!this.hasChanged(cachedVersion, metadata[metadataKey])) {
662
+ hasError = true;
663
+ }
664
+
665
+ if (hasError) {
666
+ // do this in case something went wrong during pre-deploy steps to ensure the total counter is correct
667
+ metadataToUpdate.push(null);
668
+ } else {
669
+ // add ObjectId to allow actual update
670
+ metadata[metadataKey][this.definition.idField] =
671
+ cachedVersion[this.definition.idField];
672
+ // add ObjectId to allow actual update
673
+ metadata[metadataKey].version = cachedVersion.version;
674
+
675
+ metadataToUpdate.push({
676
+ before: cachedVersion,
677
+ after: metadata[metadataKey],
678
+ });
679
+ }
680
+ } else {
681
+ if (hasError) {
682
+ // do this in case something went wrong during pre-deploy steps to ensure the total counter is correct
683
+ metadataToCreate.push(null);
684
+ } else {
685
+ if (cachedVersion) {
686
+ Util.logger.info(
687
+ ` - Found ${this.definition.type} ${
688
+ metadata[metadataKey][this.definition.nameField]
689
+ } (${
690
+ metadata[metadataKey][this.definition.keyField]
691
+ }) on BU, but it is not in Draft status. Will create new version.`
692
+ );
693
+ }
694
+
695
+ metadataToCreate.push(metadata[metadataKey]);
696
+ }
697
+ }
698
+ }
33
699
  }
34
700
  }
35
701