mcdev 8.3.0 → 8.4.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 (82) hide show
  1. package/.github/ISSUE_TEMPLATE/bug.yml +2 -0
  2. package/.github/workflows/close_issues_on_merge.yml +4 -0
  3. package/.github/workflows/coverage-base-update.yml +2 -2
  4. package/.github/workflows/coverage-develop-branch.yml +1 -1
  5. package/.github/workflows/coverage-main-branch.yml +1 -1
  6. package/.github/workflows/coverage.yml +2 -2
  7. package/.github/workflows/npm-publish.yml +0 -2
  8. package/@types/lib/index.d.ts.map +1 -1
  9. package/@types/lib/metadataTypes/DataFilter.d.ts +19 -9
  10. package/@types/lib/metadataTypes/DataFilter.d.ts.map +1 -1
  11. package/@types/lib/metadataTypes/DataFilterHidden.d.ts +1 -0
  12. package/@types/lib/metadataTypes/DataFilterHidden.d.ts.map +1 -1
  13. package/@types/lib/metadataTypes/FileLocation.d.ts +74 -0
  14. package/@types/lib/metadataTypes/FileLocation.d.ts.map +1 -1
  15. package/@types/lib/metadataTypes/Journey.d.ts +6 -0
  16. package/@types/lib/metadataTypes/Journey.d.ts.map +1 -1
  17. package/@types/lib/metadataTypes/MetadataType.d.ts +6 -0
  18. package/@types/lib/metadataTypes/MetadataType.d.ts.map +1 -1
  19. package/@types/lib/metadataTypes/Script.d.ts +14 -0
  20. package/@types/lib/metadataTypes/Script.d.ts.map +1 -1
  21. package/@types/lib/metadataTypes/definitions/DataFilter.definition.d.ts +1 -0
  22. package/@types/lib/metadataTypes/definitions/DataFilterHidden.definition.d.ts +1 -0
  23. package/@types/lib/metadataTypes/definitions/FileLocation.definition.d.ts +62 -4
  24. package/@types/lib/metadataTypes/definitions/Journey.definition.d.ts +6 -0
  25. package/@types/lib/metadataTypes/definitions/Script.definition.d.ts +10 -0
  26. package/lib/cli.js +6 -0
  27. package/lib/index.js +1 -0
  28. package/lib/metadataTypes/DataFilter.js +82 -27
  29. package/lib/metadataTypes/Event.js +2 -2
  30. package/lib/metadataTypes/FileLocation.js +204 -4
  31. package/lib/metadataTypes/Journey.js +72 -45
  32. package/lib/metadataTypes/MetadataType.js +14 -0
  33. package/lib/metadataTypes/Script.js +16 -0
  34. package/lib/metadataTypes/definitions/DataFilter.definition.js +1 -0
  35. package/lib/metadataTypes/definitions/DataFilterHidden.definition.js +1 -0
  36. package/lib/metadataTypes/definitions/FileLocation.definition.js +52 -7
  37. package/lib/metadataTypes/definitions/Journey.definition.js +6 -0
  38. package/lib/metadataTypes/definitions/Script.definition.js +6 -0
  39. package/package.json +11 -11
  40. package/test/general.test.js +11 -11
  41. package/test/mockRoot/.mcdevrc.json +1 -1
  42. package/test/mockRoot/deploy/testInstance/testBU/asset/block/{testExisting_asset_htmlblock-matchName-fail.asset-block-meta.json → testExisting_asset_html-matchNamFail.asset-block-meta.json} +1 -1
  43. package/test/mockRoot/deploy/testInstance/testBU/asset/block/{testExisting_asset_htmlblock-matchName.asset-block-meta.json → testExisting_asset_html-matchName.asset-block-meta.json} +1 -1
  44. package/test/mockRoot/deploy/testInstance/testBU/asset/block/{testExisting_asset_htmlblock-matchName-create.asset-block-meta.json → testExisting_asset_html-matchNameAdd.asset-block-meta.json} +3 -3
  45. package/test/mockRoot/deploy/testInstance/testBU/fileLocation/ExactTarget Enhanced FTP.fileLocation-meta.json +5 -0
  46. package/test/mockRoot/deploy/testInstance/testBU/fileLocation/testExisting_fileLocation_aws.fileLocation-meta.json +14 -0
  47. package/test/mockRoot/deploy/testInstance/testBU/fileLocation/testExisting_fileLocation_exsftp.fileLocation-meta.json +12 -0
  48. package/test/resourceFactory.js +9 -2
  49. package/test/resources/9999999/asset/v1/content/assets/1295064/patch-response.json +1 -1
  50. package/test/resources/9999999/automation/v1/ftplocations/get-response.json +26 -1
  51. package/test/resources/9999999/automation/v1/scripts/39f6a488-20eb-4ba0-b0b9-023725b574e4/get-response.json +10 -0
  52. package/test/resources/9999999/automation/v1/scripts/39f6a488-20eb-4ba0-b0b9-023725b574e4/patch-response.json +2 -2
  53. package/test/resources/9999999/data/v1/filetransferlocation/Salesforce%20Objects%20%26%20Reports/get-response.json +4 -0
  54. package/test/resources/9999999/data/v1/filetransferlocation/testExisting_fileLocation_aws/patch-response.json +18 -0
  55. package/test/resources/9999999/data/v1/filetransferlocation/testExisting_fileLocation_azure/delete-response.json +4 -0
  56. package/test/resources/9999999/data/v1/filetransferlocation/testExisting_fileLocation_azure/get-response.json +18 -0
  57. package/test/resources/9999999/data/v1/filetransferlocation/testExisting_fileLocation_exsftp/patch-response.json +16 -0
  58. package/test/resources/9999999/data/v1/filetransferlocations/get-response.json +59 -0
  59. package/test/resources/9999999/fileLocation/build-expected.json +14 -0
  60. package/test/resources/9999999/fileLocation/get-aws-expected.json +14 -0
  61. package/test/resources/9999999/fileLocation/get-azure-expected.json +14 -0
  62. package/test/resources/9999999/fileLocation/get-eftp-expected.json +5 -0
  63. package/test/resources/9999999/fileLocation/get-exsftp-expected.json +12 -0
  64. package/test/resources/9999999/fileLocation/get-gcp-expected.json +10 -0
  65. package/test/resources/9999999/fileLocation/get-sor-expected.json +5 -0
  66. package/test/resources/9999999/fileLocation/patch-aws-expected.json +14 -0
  67. package/test/resources/9999999/fileLocation/patch-exsftp-expected.json +12 -0
  68. package/test/resources/9999999/fileLocation/template-expected.json +14 -0
  69. package/test/resources/9999999/interaction/v1/interactions/get-response-status=Published.json +40 -0
  70. package/test/type.asset.test.js +7 -7
  71. package/test/type.automation.test.js +14 -14
  72. package/test/type.dataFilter.test.js +10 -2
  73. package/test/type.fileLocation.test.js +279 -0
  74. package/test/type.fileTransfer.test.js +4 -4
  75. package/test/type.filter.test.js +9 -2
  76. package/test/type.importFile.test.js +5 -5
  77. package/test/type.journey.test.js +26 -0
  78. package/test/type.query.test.js +2 -2
  79. package/test/type.script.test.js +1 -1
  80. /package/test/mockRoot/deploy/testInstance/testBU/asset/block/{testExisting_asset_htmlblock-matchName-create.asset-block-meta.html → testExisting_asset_html-matchNamFail.asset-block-meta.html} +0 -0
  81. /package/test/mockRoot/deploy/testInstance/testBU/asset/block/{testExisting_asset_htmlblock-matchName-fail.asset-block-meta.html → testExisting_asset_html-matchName.asset-block-meta.html} +0 -0
  82. /package/test/mockRoot/deploy/testInstance/testBU/asset/block/{testExisting_asset_htmlblock-matchName.asset-block-meta.html → testExisting_asset_html-matchNameAdd.asset-block-meta.html} +0 -0
@@ -146,6 +146,16 @@ declare namespace _default {
146
146
  let template_12: boolean;
147
147
  export { template_12 as template };
148
148
  }
149
+ namespace parentCategoryId {
150
+ let isCreateable_13: boolean;
151
+ export { isCreateable_13 as isCreateable };
152
+ let isUpdateable_13: boolean;
153
+ export { isUpdateable_13 as isUpdateable };
154
+ let retrieving_13: boolean;
155
+ export { retrieving_13 as retrieving };
156
+ let template_13: boolean;
157
+ export { template_13 as template };
158
+ }
149
159
  namespace r__folder_Path {
150
160
  let skipValidation: boolean;
151
161
  }
package/lib/cli.js CHANGED
@@ -73,6 +73,12 @@ yargs(hideBin(process.argv))
73
73
  type: 'boolean',
74
74
  group: 'Options for retrieve:',
75
75
  describe: 'deletes the relevant retrieve folder before retrieving',
76
+ })
77
+ .option('onlyPublished', {
78
+ type: 'boolean',
79
+ alias: 'op',
80
+ group: 'Options for retrieve:',
81
+ describe: 'only retrieve the version of the metadata that was published',
76
82
  }),
77
83
  (argv) => {
78
84
  Mcdev.setOptions(argv);
package/lib/index.js CHANGED
@@ -97,6 +97,7 @@ class Mcdev {
97
97
  'ignoreSfFields',
98
98
  'json',
99
99
  'keySuffix',
100
+ 'onlyPublished',
100
101
  'like',
101
102
  'matchName',
102
103
  'noLogColors',
@@ -20,6 +20,8 @@ import File from '../util/file.js';
20
20
  * @typedef {import('../../types/mcdev.d.js').SoapRequestParams} SoapRequestParams
21
21
  * @typedef {import('../../types/mcdev.d.js').TemplateMap} TemplateMap
22
22
  * @typedef {import('../../types/mcdev.d.js').DataFilterItem} DataFilterItem
23
+ * @typedef {import('../../types/mcdev.d.js').DataExtensionFieldMap} DataExtensionFieldMap
24
+ * @typedef {import('../../types/mcdev.d.js').DataExtensionFieldItem} DataExtensionFieldItem
23
25
  * @typedef {import('../../types/mcdev.d.js').DataFilterMap} DataFilterMap
24
26
  * @typedef {import('../../types/mcdev.d.js').MultiMetadataTypeMap} MultiMetadataTypeMap
25
27
  * @typedef {import('../../types/mcdev.d.js').FilterConditionSet} FilterConditionSet
@@ -108,8 +110,10 @@ class DataFilter extends MetadataType {
108
110
  */
109
111
  static async _getFilterFolderIds(recached = false) {
110
112
  const fromCache =
111
- this.cache.folderFilter || cache.getCache().folder
112
- ? Object.values(this.cache.folderFilter || cache.getCache().folder)
113
+ this.cache[this.buObject.mid]?.folderFilter || cache.getCache().folder
114
+ ? Object.values(
115
+ this.cache[this.buObject.mid]?.folderFilter || cache.getCache().folder
116
+ )
113
117
  .filter((item) => item.ContentType === 'filterdefinition')
114
118
  .filter(
115
119
  (item) =>
@@ -133,7 +137,10 @@ class DataFilter extends MetadataType {
133
137
  Folder.client = this.client;
134
138
  Folder.buObject = this.buObject;
135
139
  Folder.properties = this.properties;
136
- this.cache.folderFilter = (await Folder.retrieveForCache(null, subTypeArr)).metadata;
140
+ this.cache[this.buObject.mid] ||= {};
141
+ this.cache[this.buObject.mid].folderFilter = (
142
+ await Folder.retrieveForCache(null, subTypeArr)
143
+ ).metadata;
137
144
  return this._getFilterFolderIds(true);
138
145
  }
139
146
  /**
@@ -143,9 +150,9 @@ class DataFilter extends MetadataType {
143
150
  */
144
151
  static async _getMeasureFolderIds() {
145
152
  const fromCache =
146
- this.cache.folderMeasure?.[this.buObject.mid] || cache.getCache().folder
153
+ this.cache[this.buObject.mid]?.folderMeasure || cache.getCache().folder
147
154
  ? Object.values(
148
- this.cache.folderMeasure?.[this.buObject.mid] || cache.getCache().folder
155
+ this.cache[this.buObject.mid]?.folderMeasure || cache.getCache().folder
149
156
  )
150
157
  .filter((item) => item.ContentType === 'measure')
151
158
  .map((item) => item.ID)
@@ -161,8 +168,9 @@ class DataFilter extends MetadataType {
161
168
  Folder.client = this.client;
162
169
  Folder.buObject = this.buObject;
163
170
  Folder.properties = this.properties;
164
- this.cache.folderMeasure ||= {};
165
- this.cache.folderMeasure[this.buObject.mid] = (
171
+ this.cache[this.buObject.mid] ||= {};
172
+ this.cache[this.buObject.mid].folderMeasure ||= {};
173
+ this.cache[this.buObject.mid].folderMeasure = (
166
174
  await Folder.retrieveForCache(null, subTypeArr)
167
175
  ).metadata;
168
176
  return this._getMeasureFolderIds();
@@ -201,12 +209,18 @@ class DataFilter extends MetadataType {
201
209
  : Object.values(metadataTypeMap)
202
210
  .filter((item) => item.r__source_dataExtension_key)
203
211
  .map((item) => item.r__source_dataExtension_key)
212
+ .filter(
213
+ (deKey) =>
214
+ !this.cache[this.buObject.mid]?.dataExtensionField ||
215
+ !this.cache[this.buObject.mid]?.dataExtensionField[deKey]
216
+ )
204
217
  .filter(Boolean);
205
218
  if (deKeys.length) {
219
+ const deKeysUnique = [...new Set(deKeys)];
206
220
  Util.logger.info(' - Caching dependent Metadata: dataExtensionField');
207
221
  // only proceed with the download if we have dataExtension keys
208
222
  const fieldOptions = {};
209
- for (const deKey of deKeys) {
223
+ for (const deKey of deKeysUnique) {
210
224
  fieldOptions.filter = fieldOptions.filter
211
225
  ? {
212
226
  leftOperand: {
@@ -226,9 +240,38 @@ class DataFilter extends MetadataType {
226
240
  DataExtensionField.buObject = this.buObject;
227
241
  DataExtensionField.client = this.client;
228
242
  DataExtensionField.properties = this.properties;
229
- this.dataExtensionFieldCache = (
230
- await DataExtensionField.retrieveForCacheDE(fieldOptions, ['Name', 'ObjectID'])
231
- ).metadata;
243
+ this.saveDataExtensionFieldCacheToMap(
244
+ (await DataExtensionField.retrieveForCacheDE(fieldOptions, ['Name', 'ObjectID']))
245
+ .metadata
246
+ );
247
+ }
248
+ }
249
+ /**
250
+ * helper for {@link DataFilter._cacheDeFields}
251
+ *
252
+ * @param {DataExtensionFieldMap} deFieldCache -
253
+ */
254
+ static saveDataExtensionFieldCacheToMap(deFieldCache) {
255
+ this.cache[this.buObject.mid] ||= {};
256
+ this.cache[this.buObject.mid].dataExtensionField ||= {};
257
+
258
+ if (!deFieldCache) {
259
+ return;
260
+ }
261
+ for (const field of Object.values(deFieldCache)) {
262
+ if (!field?.DataExtension?.CustomerKey) {
263
+ continue;
264
+ }
265
+ if (
266
+ !this.cache[this.buObject.mid].dataExtensionField[field.DataExtension.CustomerKey]
267
+ ) {
268
+ /** @type {Object.<string, DataExtensionFieldItem[]>} */
269
+ this.cache[this.buObject.mid].dataExtensionField[field.DataExtension.CustomerKey] =
270
+ [];
271
+ }
272
+ this.cache[this.buObject.mid].dataExtensionField[field.DataExtension.CustomerKey].push(
273
+ field
274
+ );
232
275
  }
233
276
  }
234
277
  /**
@@ -237,7 +280,7 @@ class DataFilter extends MetadataType {
237
280
  * @param {DataFilterMap} metadataTypeMap -
238
281
  */
239
282
  static async _cacheContactAttributes(metadataTypeMap) {
240
- if (this.cache.contactAttributes?.[this.buObject.mid]) {
283
+ if (this.cache[this.buObject.mid]?.contactAttributes) {
241
284
  return;
242
285
  }
243
286
  const subscriberFilters = Object.values(metadataTypeMap)
@@ -248,8 +291,8 @@ class DataFilter extends MetadataType {
248
291
  const response = await this.client.rest.get('/email/v1/Contacts/Attributes/');
249
292
  const keyFieldBackup = this.definition.keyField;
250
293
  this.definition.keyField = 'id';
251
- this.cache.contactAttributes ||= {};
252
- this.cache.contactAttributes[this.buObject.mid] = this.parseResponseBody(response);
294
+ this.cache[this.buObject.mid] ||= {};
295
+ this.cache[this.buObject.mid].contactAttributes = this.parseResponseBody(response);
253
296
  this.definition.keyField = keyFieldBackup;
254
297
  }
255
298
  }
@@ -259,7 +302,7 @@ class DataFilter extends MetadataType {
259
302
  * @param {DataFilterMap} metadataTypeMap -
260
303
  */
261
304
  static async _cacheMeasures(metadataTypeMap) {
262
- if (this.cache.measures?.[this.buObject.mid]) {
305
+ if (this.cache?.[this.buObject.mid]?.measures) {
263
306
  return;
264
307
  }
265
308
  const subscriberFilters = Object.values(metadataTypeMap)
@@ -281,8 +324,8 @@ class DataFilter extends MetadataType {
281
324
 
282
325
  const keyFieldBackup = this.definition.keyField;
283
326
  this.definition.keyField = 'measureID';
284
- this.cache.measures ||= {};
285
- this.cache.measures[this.buObject.mid] = this.parseResponseBody(response);
327
+ this.cache[this.buObject.mid] ||= {};
328
+ this.cache[this.buObject.mid].measures = this.parseResponseBody(response);
286
329
  this.definition.keyField = keyFieldBackup;
287
330
  }
288
331
  }
@@ -399,7 +442,7 @@ class DataFilter extends MetadataType {
399
442
  *
400
443
  * @param {DataFilterItem} metadata -
401
444
  * @param {'postRetrieve'|'preDeploy'} mode -
402
- * @param {object[]} [fieldCache] -
445
+ * @param {DataExtensionFieldItem[]} [fieldCache] -
403
446
  * @param {FilterConditionSet} [filter] -
404
447
  * @returns {void}
405
448
  */
@@ -408,7 +451,9 @@ class DataFilter extends MetadataType {
408
451
  return this._resolveFields(
409
452
  metadata,
410
453
  mode,
411
- Object.values(this.dataExtensionFieldCache),
454
+ this.cache[this.buObject.mid].dataExtensionField[
455
+ metadata.r__source_dataExtension_key
456
+ ],
412
457
  metadata.c__filterDefinition?.ConditionSet
413
458
  );
414
459
  }
@@ -418,11 +463,11 @@ class DataFilter extends MetadataType {
418
463
  : [filter.Condition];
419
464
  if (mode === 'postRetrieve') {
420
465
  for (const condition of conditionsArr) {
421
- this._postRetrieve_resolveFieldIdsCondition(condition, fieldCache);
466
+ this._postRetrieve_resolveFieldIdsCondition(metadata, condition, fieldCache);
422
467
  }
423
468
  } else if (mode === 'preDeploy') {
424
469
  for (const condition of conditionsArr) {
425
- this._preDeploy_resolveFieldNamesCondition(condition, fieldCache);
470
+ this._preDeploy_resolveFieldNamesCondition(metadata, condition, fieldCache);
426
471
  }
427
472
  }
428
473
  }
@@ -438,16 +483,21 @@ class DataFilter extends MetadataType {
438
483
  /**
439
484
  * helper for {@link _resolveFields}
440
485
  *
486
+ * @param {DataFilterItem} metadata -
441
487
  * @param {FilterCondition} condition -
442
- * @param {object[]} fieldCache -
488
+ * @param {DataExtensionFieldItem[]} fieldCache -
443
489
  * @returns {void}
444
490
  */
445
- static _postRetrieve_resolveFieldIdsCondition(condition, fieldCache) {
491
+ static _postRetrieve_resolveFieldIdsCondition(metadata, condition, fieldCache) {
446
492
  condition.r__dataExtensionField_name = fieldCache.find(
447
493
  (field) => field.ObjectID === condition['@_ID']
448
494
  )?.Name;
449
495
  if (condition.r__dataExtensionField_name) {
450
496
  delete condition['@_ID'];
497
+ } else {
498
+ Util.logger.warn(
499
+ ` - ${this.definition.type} ${metadata[this.definition.keyField]}: could not resolve dataExtensionField with ID ${condition['@_ID']} in condition`
500
+ );
451
501
  }
452
502
  if (['IsEmpty', 'IsNotEmpty'].includes(condition['@_Operator'])) {
453
503
  delete condition.Value;
@@ -456,16 +506,21 @@ class DataFilter extends MetadataType {
456
506
  /**
457
507
  * helper for {@link _resolveFields}
458
508
  *
509
+ * @param {DataFilterItem} metadata -
459
510
  * @param {FilterCondition} condition -
460
- * @param {object[]} fieldCache -
511
+ * @param {DataExtensionFieldItem[]} fieldCache -
461
512
  * @returns {void}
462
513
  */
463
- static _preDeploy_resolveFieldNamesCondition(condition, fieldCache) {
514
+ static _preDeploy_resolveFieldNamesCondition(metadata, condition, fieldCache) {
464
515
  condition['@_ID'] = fieldCache.find(
465
516
  (field) => field.Name === condition.r__dataExtensionField_name
466
517
  )?.ObjectID;
467
518
  if (condition['@_ID']) {
468
519
  delete condition.r__dataExtensionField_name;
520
+ } else {
521
+ throw new Error(
522
+ ` - ${this.definition.type} ${metadata[this.definition.keyField]}: could not resolve dataExtensionField with Name ${condition.r__dataExtensionField_name} in condition`
523
+ );
469
524
  }
470
525
  if (['IsEmpty', 'IsNotEmpty'].includes(condition['@_Operator'])) {
471
526
  condition.Value ||= '';
@@ -489,8 +544,8 @@ class DataFilter extends MetadataType {
489
544
  metadata.c__filterDefinition?.ConditionSet
490
545
  );
491
546
  }
492
- const contactAttributes = this.cache.contactAttributes[this.buObject.mid];
493
- const measures = this.cache.measures[this.buObject.mid];
547
+ const contactAttributes = this.cache[this.buObject.mid]?.contactAttributes;
548
+ const measures = this.cache[this.buObject.mid]?.measures;
494
549
  const conditionsArr = Array.isArray(filter.Condition)
495
550
  ? filter.Condition
496
551
  : [filter.Condition];
@@ -1067,7 +1067,7 @@ class Event extends MetadataType {
1067
1067
  if (errors.length > 1) {
1068
1068
  errors.unshift(``);
1069
1069
  }
1070
- throw new Error(errors.join('\n · ')); // eslint-disable-line unicorn/error-message
1070
+ throw new Error(errors.join('\n · '));
1071
1071
  }
1072
1072
  if (warnings?.length) {
1073
1073
  // add a line break
@@ -1220,7 +1220,7 @@ class Event extends MetadataType {
1220
1220
  if (errors.length > 1) {
1221
1221
  errors.unshift(``);
1222
1222
  }
1223
- throw new Error(errors.join('\n · ')); // eslint-disable-line unicorn/error-message
1223
+ throw new Error(errors.join('\n · '));
1224
1224
  }
1225
1225
  }
1226
1226
 
@@ -22,6 +22,7 @@ import { Util } from '../util/util.js';
22
22
  * @augments MetadataType
23
23
  */
24
24
  class FileLocation extends MetadataType {
25
+ static cache = {};
25
26
  /**
26
27
  * Retrieves Metadata of FileLocation
27
28
  * Endpoint /automation/v1/ftplocations/ return all FileLocations
@@ -32,8 +33,32 @@ class FileLocation extends MetadataType {
32
33
  * @param {string} [key] customer key of single item to retrieve
33
34
  * @returns {Promise.<MetadataTypeMapObj>} Promise
34
35
  */
35
- static retrieve(retrieveDir, _, __, key) {
36
- return super.retrieveREST(retrieveDir, '/automation/v1/ftplocations/', null, key);
36
+ static async retrieve(retrieveDir, _, __, key) {
37
+ try {
38
+ const dataItems = await super.retrieveREST(
39
+ null,
40
+ '/data/v1/filetransferlocation' + (key ? '/' + encodeURIComponent(key) : 's'),
41
+ null,
42
+ key
43
+ );
44
+ this.cache[this.buObject.mid] ||= {};
45
+ this.cache[this.buObject.mid].dataItems = dataItems.metadata;
46
+ } catch (ex) {
47
+ if (ex.code === 'ERR_BAD_REQUEST') {
48
+ // if retrieve-by-key comes up empty, the data-endpoint returns a code 400
49
+ Util.logger.debug(ex.message);
50
+ } else {
51
+ Util.logger.warn(ex.message);
52
+ }
53
+ }
54
+ const items = await super.retrieveREST(
55
+ retrieveDir,
56
+ '/automation/v1/ftplocations/',
57
+ null,
58
+ key
59
+ );
60
+
61
+ return items;
37
62
  }
38
63
 
39
64
  /**
@@ -42,9 +67,156 @@ class FileLocation extends MetadataType {
42
67
  * @returns {Promise.<MetadataTypeMapObj>} Promise
43
68
  */
44
69
  static async retrieveForCache() {
45
- return super.retrieveREST(null, '/automation/v1/ftplocations/');
70
+ return this.retrieve(null);
71
+ }
72
+
73
+ /**
74
+ * Creates a single item
75
+ *
76
+ * @param {MetadataTypeItem} metadata a single item
77
+ * @returns {Promise.<MetadataTypeItem>} Promise
78
+ */
79
+ static create(metadata) {
80
+ return this.createREST(metadata, '/data/v1/filetransferlocation');
81
+ }
82
+ /**
83
+ * Updates a single item
84
+ *
85
+ * @param {MetadataTypeItem} metadata a single item
86
+ * @returns {Promise.<MetadataTypeItem>} Promise
87
+ */
88
+ static update(metadata) {
89
+ return this.updateREST(
90
+ metadata,
91
+ '/data/v1/filetransferlocation/' +
92
+ encodeURIComponent(metadata[this.definition.keyField])
93
+ );
46
94
  }
47
95
 
96
+ /**
97
+ * helper for {@link MetadataType.parseResponseBody} that creates a custom key field for this type based on mobileCode and keyword
98
+ *
99
+ * @param {MetadataTypeItem} metadata single item
100
+ */
101
+ static createCustomKeyField(metadata) {
102
+ if (metadata.fileTransferLocation) {
103
+ const fileTransferLocation = metadata.fileTransferLocation;
104
+ for (const key of Object.keys(metadata)) {
105
+ delete metadata[key];
106
+ }
107
+ Object.assign(metadata, fileTransferLocation);
108
+ } else {
109
+ if (!metadata.customerKey && this.cache[this.buObject.mid]?.dataItems) {
110
+ const nameMatch = Object.values(this.cache[this.buObject.mid].dataItems).find(
111
+ (item) => item.name === metadata.name
112
+ );
113
+ if (nameMatch) {
114
+ Object.assign(metadata, nameMatch);
115
+ }
116
+ }
117
+ if (!this.definition.locationTypeIdMappingDeployable[metadata.locationTypeId]) {
118
+ // old file location types are only returned by the automation-endpoint which does not return customerKey field - but also these are not updatable and hence we can improvise here
119
+
120
+ metadata.customerKey ||= metadata.name;
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Creates a single metadata entry via REST
127
+ *
128
+ * @param {MetadataTypeItem} metadataEntry a single metadata Entry
129
+ * @param {string} uri rest endpoint for POST
130
+ * @param {boolean} [handleOutside] if the API reponse is irregular this allows you to handle it outside of this generic method
131
+ * @returns {Promise.<object> | null} Promise of API response or null in case of an error
132
+ */
133
+ static async createREST(metadataEntry, uri, handleOutside) {
134
+ this.removeNotCreateableFields(metadataEntry);
135
+ const createPayload = { fileTransferLocation: metadataEntry };
136
+ try {
137
+ // set to empty object in case API returned nothing to be able to update it in helper classes
138
+ let response = (await this.client.rest.post(uri, createPayload)) || {};
139
+ response = await this.postCreateTasks(metadataEntry, response);
140
+ if (!handleOutside) {
141
+ Util.logger.info(
142
+ ` - created ${Util.getTypeKeyName(this.definition, metadataEntry)}`
143
+ );
144
+ }
145
+ return response;
146
+ } catch (ex) {
147
+ const parsedErrors = this.getErrorsREST(ex);
148
+ Util.logger.error(
149
+ ` ☇ error creating ${Util.getTypeKeyName(this.definition, metadataEntry)}:`
150
+ );
151
+ if (parsedErrors.length) {
152
+ for (const msg of parsedErrors) {
153
+ Util.logger.error(' • ' + msg);
154
+ }
155
+ } else if (ex?.message) {
156
+ Util.logger.debug(ex.message);
157
+ }
158
+ return null;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * helper for {@link MetadataType.createREST}
164
+ *
165
+ * @param {MetadataTypeItem} _ a single metadata Entry
166
+ * @param {object} apiResponse varies depending on the API call
167
+ * @returns {Promise.<object>} apiResponse, potentially modified
168
+ */
169
+ static async postCreateTasks(_, apiResponse) {
170
+ return apiResponse?.fileTransferLocation || apiResponse;
171
+ }
172
+
173
+ /**
174
+ * Updates a single metadata entry via REST
175
+ *
176
+ * @param {MetadataTypeItem} metadataEntry a single metadata Entry
177
+ * @param {string} uri rest endpoint for PATCH
178
+ * @param {'patch'|'post'|'put'} [httpMethod] defaults to 'patch'; some update requests require PUT instead of PATCH
179
+ * @param {boolean} [handleOutside] if the API reponse is irregular this allows you to handle it outside of this generic method
180
+ * @returns {Promise.<object> | null} Promise of API response or null in case of an error
181
+ */
182
+ static async updateREST(metadataEntry, uri, httpMethod = 'patch', handleOutside) {
183
+ this.removeNotUpdateableFields(metadataEntry);
184
+ const updatePayload = { fileTransferLocation: metadataEntry };
185
+ try {
186
+ // set to empty object in case API returned nothing to be able to update it in helper classes
187
+ let response = (await this.client.rest[httpMethod](uri, updatePayload)) || {};
188
+ await this._postChangeKeyTasks(metadataEntry);
189
+ this.getErrorsREST(response);
190
+ response = await this.postUpdateTasks(metadataEntry, response);
191
+ // some times, e.g. automation dont return a key in their update response and hence we need to fall back to name
192
+ if (!handleOutside) {
193
+ Util.logger.info(
194
+ ` - updated ${Util.getTypeKeyName(this.definition, metadataEntry)}`
195
+ );
196
+ }
197
+ return response;
198
+ } catch (ex) {
199
+ const parsedErrors = this.getErrorsREST(ex);
200
+ Util.logger.error(
201
+ ` ☇ error updating ${Util.getTypeKeyName(this.definition, metadataEntry)}:`
202
+ );
203
+ for (const msg of parsedErrors) {
204
+ Util.logger.error(' • ' + msg);
205
+ }
206
+ return null;
207
+ }
208
+ }
209
+
210
+ /**
211
+ * helper for {@link MetadataType.updateREST} and {@link MetadataType.updateSOAP}
212
+ *
213
+ * @param {MetadataTypeItem} _ a single metadata Entry
214
+ * @param {object} apiResponse varies depending on the API call
215
+ * @returns {Promise.<object>} apiResponse, potentially modified
216
+ */
217
+ static postUpdateTasks(_, apiResponse) {
218
+ return apiResponse?.fileTransferLocation || apiResponse;
219
+ }
48
220
  /**
49
221
  * prepares a import definition for deployment
50
222
  *
@@ -54,6 +226,14 @@ class FileLocation extends MetadataType {
54
226
  static async preDeployTasks(metadata) {
55
227
  if (metadata.c__locationType) {
56
228
  metadata.locationTypeId = this.definition.locationTypeMapping[metadata.c__locationType];
229
+ if (this.definition.locationTypeMappingDeployable[metadata.c__locationType]) {
230
+ metadata.locationType =
231
+ this.definition.locationTypeMappingDeployable[metadata.c__locationType];
232
+ } else {
233
+ throw new Error(
234
+ `Only FileLocations of types ${Object.keys(this.definition.locationTypeMappingDeployable).join(', ')} can be deployed via mcdev.`
235
+ );
236
+ }
57
237
  }
58
238
  return metadata;
59
239
  }
@@ -65,7 +245,7 @@ class FileLocation extends MetadataType {
65
245
  * @returns {MetadataTypeItem} parsed metadata
66
246
  */
67
247
  static postRetrieveTasks(metadata) {
68
- if (metadata.locationTypeId) {
248
+ if (metadata.locationTypeId !== undefined) {
69
249
  try {
70
250
  metadata.c__locationType = Util.inverseGet(
71
251
  this.definition.locationTypeMapping,
@@ -78,9 +258,29 @@ class FileLocation extends MetadataType {
78
258
  metadata.locationTypeId
79
259
  );
80
260
  }
261
+ } else if (metadata.locationType) {
262
+ // assuming create/update of new types
263
+ metadata.c__locationType = Util.inverseGet(
264
+ this.definition.locationTypeMappingDeployable,
265
+ metadata.locationType
266
+ );
81
267
  }
82
268
  return metadata;
83
269
  }
270
+
271
+ /**
272
+ * Delete a metadata item from the specified business unit
273
+ *
274
+ * @param {string} key Identifier of item
275
+ * @returns {Promise.<boolean>} deletion success flag
276
+ */
277
+ static async deleteByKey(key) {
278
+ return super.deleteByKeyREST(
279
+ '/data/v1/filetransferlocation/' + encodeURIComponent(key),
280
+ key,
281
+ 400
282
+ );
283
+ }
84
284
  }
85
285
 
86
286
  // Assign definition to static attributes