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