mcdev 3.1.1 → 4.0.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 (135) hide show
  1. package/.eslintrc.json +67 -7
  2. package/.github/ISSUE_TEMPLATE/bug.yml +5 -1
  3. package/.github/ISSUE_TEMPLATE/task.md +1 -1
  4. package/.github/PULL_REQUEST_TEMPLATE.md +5 -3
  5. package/.github/dependabot.yml +14 -0
  6. package/.github/workflows/code-analysis.yml +57 -0
  7. package/.husky/commit-msg +10 -0
  8. package/.husky/post-checkout +5 -0
  9. package/.husky/pre-commit +2 -1
  10. package/.prettierrc +8 -0
  11. package/.vscode/settings.json +1 -1
  12. package/LICENSE +2 -2
  13. package/README.md +134 -45
  14. package/boilerplate/config.json +5 -11
  15. package/boilerplate/files/.prettierrc +8 -0
  16. package/boilerplate/files/.vscode/extensions.json +0 -1
  17. package/boilerplate/files/.vscode/settings.json +28 -2
  18. package/boilerplate/files/README.md +2 -2
  19. package/boilerplate/forcedUpdates.json +10 -0
  20. package/boilerplate/npm-dependencies.json +5 -5
  21. package/docs/dist/documentation.md +2795 -1724
  22. package/jsconfig.json +1 -1
  23. package/lib/Builder.js +166 -75
  24. package/lib/Deployer.js +244 -96
  25. package/lib/MetadataTypeDefinitions.js +2 -0
  26. package/lib/MetadataTypeInfo.js +2 -0
  27. package/lib/Retriever.js +61 -84
  28. package/lib/cli.js +133 -25
  29. package/lib/index.js +242 -563
  30. package/lib/metadataTypes/AccountUser.js +101 -95
  31. package/lib/metadataTypes/Asset.js +677 -248
  32. package/lib/metadataTypes/AttributeGroup.js +23 -12
  33. package/lib/metadataTypes/Automation.js +456 -357
  34. package/lib/metadataTypes/Campaign.js +33 -93
  35. package/lib/metadataTypes/ContentArea.js +31 -11
  36. package/lib/metadataTypes/DataExtension.js +391 -376
  37. package/lib/metadataTypes/DataExtensionField.js +131 -54
  38. package/lib/metadataTypes/DataExtensionTemplate.js +22 -4
  39. package/lib/metadataTypes/DataExtract.js +67 -50
  40. package/lib/metadataTypes/DataExtractType.js +14 -8
  41. package/lib/metadataTypes/Discovery.js +21 -16
  42. package/lib/metadataTypes/Email.js +32 -12
  43. package/lib/metadataTypes/EmailSendDefinition.js +85 -80
  44. package/lib/metadataTypes/EventDefinition.js +69 -47
  45. package/lib/metadataTypes/FileTransfer.js +78 -54
  46. package/lib/metadataTypes/Filter.js +11 -4
  47. package/lib/metadataTypes/Folder.js +149 -117
  48. package/lib/metadataTypes/FtpLocation.js +14 -8
  49. package/lib/metadataTypes/ImportFile.js +69 -69
  50. package/lib/metadataTypes/Interaction.js +19 -4
  51. package/lib/metadataTypes/List.js +54 -13
  52. package/lib/metadataTypes/MetadataType.js +687 -479
  53. package/lib/metadataTypes/MobileCode.js +46 -0
  54. package/lib/metadataTypes/MobileKeyword.js +114 -0
  55. package/lib/metadataTypes/Query.js +204 -103
  56. package/lib/metadataTypes/Role.js +76 -61
  57. package/lib/metadataTypes/Script.js +146 -82
  58. package/lib/metadataTypes/SetDefinition.js +20 -8
  59. package/lib/metadataTypes/TriggeredSendDefinition.js +78 -58
  60. package/lib/metadataTypes/definitions/Asset.definition.js +21 -10
  61. package/lib/metadataTypes/definitions/AttributeGroup.definition.js +12 -0
  62. package/lib/metadataTypes/definitions/Automation.definition.js +10 -5
  63. package/lib/metadataTypes/definitions/Campaign.definition.js +44 -1
  64. package/lib/metadataTypes/definitions/DataExtension.definition.js +4 -0
  65. package/lib/metadataTypes/definitions/DataExtensionTemplate.definition.js +6 -0
  66. package/lib/metadataTypes/definitions/DataExtract.definition.js +18 -14
  67. package/lib/metadataTypes/definitions/Discovery.definition.js +12 -0
  68. package/lib/metadataTypes/definitions/EmailSendDefinition.definition.js +4 -0
  69. package/lib/metadataTypes/definitions/EventDefinition.definition.js +22 -0
  70. package/lib/metadataTypes/definitions/FileTransfer.definition.js +4 -0
  71. package/lib/metadataTypes/definitions/Filter.definition.js +4 -0
  72. package/lib/metadataTypes/definitions/Folder.definition.js +6 -0
  73. package/lib/metadataTypes/definitions/FtpLocation.definition.js +4 -0
  74. package/lib/metadataTypes/definitions/ImportFile.definition.js +10 -5
  75. package/lib/metadataTypes/definitions/Interaction.definition.js +4 -0
  76. package/lib/metadataTypes/definitions/MobileCode.definition.js +163 -0
  77. package/lib/metadataTypes/definitions/MobileKeyword.definition.js +253 -0
  78. package/lib/metadataTypes/definitions/Query.definition.js +4 -0
  79. package/lib/metadataTypes/definitions/Role.definition.js +5 -0
  80. package/lib/metadataTypes/definitions/Script.definition.js +4 -0
  81. package/lib/metadataTypes/definitions/SetDefinition.definition.js +28 -0
  82. package/lib/metadataTypes/definitions/TriggeredSendDefinition.definition.js +4 -0
  83. package/lib/retrieveChangelog.js +7 -6
  84. package/lib/util/auth.js +117 -0
  85. package/lib/util/businessUnit.js +55 -66
  86. package/lib/util/cache.js +194 -0
  87. package/lib/util/cli.js +90 -116
  88. package/lib/util/config.js +302 -0
  89. package/lib/util/devops.js +240 -50
  90. package/lib/util/file.js +120 -191
  91. package/lib/util/init.config.js +195 -69
  92. package/lib/util/init.git.js +45 -50
  93. package/lib/util/init.js +72 -59
  94. package/lib/util/init.npm.js +48 -39
  95. package/lib/util/util.js +280 -564
  96. package/package.json +44 -33
  97. package/test/dataExtension.test.js +152 -0
  98. package/test/mockRoot/.mcdev-auth.json +8 -0
  99. package/test/mockRoot/.mcdevrc.json +67 -0
  100. package/test/mockRoot/deploy/testInstance/testBU/dataExtension/childBU_dataextension_test.dataExtension-meta.json +39 -0
  101. package/test/mockRoot/deploy/testInstance/testBU/dataExtension/testDataExtension.dataExtension-meta.json +23 -0
  102. package/test/mockRoot/deploy/testInstance/testBU/query/testExistingQuery.query-meta.json +11 -0
  103. package/test/mockRoot/deploy/testInstance/testBU/query/testExistingQuery.query-meta.sql +4 -0
  104. package/test/mockRoot/deploy/testInstance/testBU/query/testQuery.query-meta.json +11 -0
  105. package/test/mockRoot/deploy/testInstance/testBU/query/testQuery.query-meta.sql +4 -0
  106. package/test/query.test.js +149 -0
  107. package/test/resourceFactory.js +142 -0
  108. package/test/resources/1111111/dataFolder/retrieve-response.xml +43 -0
  109. package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dat/patch-response.json +18 -0
  110. package/test/resources/9999999/automation/v1/queries/get-response.json +24 -0
  111. package/test/resources/9999999/automation/v1/queries/post-response.json +18 -0
  112. package/test/resources/9999999/dataExtension/build-expected.json +51 -0
  113. package/test/resources/9999999/dataExtension/create-expected.json +23 -0
  114. package/test/resources/9999999/dataExtension/create-response.xml +54 -0
  115. package/test/resources/9999999/dataExtension/retrieve-expected.json +51 -0
  116. package/test/resources/9999999/dataExtension/retrieve-response.xml +47 -0
  117. package/test/resources/9999999/dataExtension/template-expected.json +51 -0
  118. package/test/resources/9999999/dataExtension/update-expected.json +55 -0
  119. package/test/resources/9999999/dataExtension/update-response.xml +52 -0
  120. package/test/resources/9999999/dataExtensionField/retrieve-response.xml +93 -0
  121. package/test/resources/9999999/dataExtensionTemplate/retrieve-response.xml +303 -0
  122. package/test/resources/9999999/dataFolder/retrieve-response.xml +65 -0
  123. package/test/resources/9999999/query/build-expected.json +8 -0
  124. package/test/resources/9999999/query/get-expected.json +11 -0
  125. package/test/resources/9999999/query/patch-expected.json +11 -0
  126. package/test/resources/9999999/query/post-expected.json +11 -0
  127. package/test/resources/9999999/query/template-expected.json +8 -0
  128. package/test/resources/auth.json +32 -0
  129. package/test/resources/rest404-response.json +5 -0
  130. package/test/resources/retrieve-response.xml +21 -0
  131. package/test/utils.js +107 -0
  132. package/types/mcdev.d.js +301 -0
  133. package/CHANGELOG.md +0 -126
  134. package/PULL_REQUEST_TEMPLATE.md +0 -19
  135. package/test/util/file.js +0 -51
@@ -1,39 +1,30 @@
1
1
  'use strict';
2
2
 
3
3
  const MetadataType = require('./MetadataType');
4
+ const TYPE = require('../../types/mcdev.d');
4
5
  const Util = require('../util/util');
5
6
  const File = require('../util/file');
6
- const bluebird = require('bluebird');
7
+ const pLimit = require('p-limit');
7
8
  const cliProgress = require('cli-progress');
8
- const Mustache = require('mustache');
9
-
10
- /**
11
- * @typedef {Object.<string, any>} AssetItem
12
- *
13
- * @typedef {Object.<string, AssetItem>} AssetMap
14
- *
15
- * @typedef {'archive'|'asset'|'audio'|'block'|'code'|'document'|'image'|'message'|'other'|'rawimage'|'template'|'textfile'|'video'} AssetSubType
16
- *
17
- * @typedef {Object} CodeExtractItem
18
- * @property {AssetItem} json metadata of one item w/o code
19
- * @property {MetadataType.CodeExtract[]} codeArr list of code snippets in this item
20
- * @property {string[]} subFolder mostly set to null, otherwise list of subfolders
21
- */
9
+ const cache = require('../util/cache');
22
10
 
23
11
  /**
24
12
  * FileTransfer MetadataType
13
+ *
25
14
  * @augments MetadataType
26
15
  */
27
16
  class Asset extends MetadataType {
28
17
  /**
29
18
  * Retrieves Metadata of Asset
19
+ *
30
20
  * @param {string} retrieveDir Directory where retrieved metadata directory will be saved
31
21
  * @param {void} _ -
32
22
  * @param {void} __ -
33
- * @param {AssetSubType} [selectedSubType] optionally limit to a single subtype
34
- * @returns {Promise<{metadata:AssetMap,type:string}>} Promise
23
+ * @param {TYPE.AssetSubType} [selectedSubType] optionally limit to a single subtype
24
+ * @param {string} [key] customer key
25
+ * @returns {Promise.<{metadata: TYPE.AssetMap, type: string}>} Promise
35
26
  */
36
- static async retrieve(retrieveDir, _, __, selectedSubType) {
27
+ static async retrieve(retrieveDir, _, __, selectedSubType, key) {
37
28
  const items = [];
38
29
  const subTypes = selectedSubType ? [selectedSubType] : this._getSubTypes();
39
30
  await File.initPrettier();
@@ -45,7 +36,10 @@ class Asset extends MetadataType {
45
36
  ...(await this.requestSubType(
46
37
  subType,
47
38
  this.definition.extendedSubTypes[subType],
48
- retrieveDir
39
+ retrieveDir,
40
+ null,
41
+ null,
42
+ key
49
43
  ))
50
44
  );
51
45
  }
@@ -60,21 +54,23 @@ class Asset extends MetadataType {
60
54
 
61
55
  /**
62
56
  * Retrieves asset metadata for caching
57
+ *
63
58
  * @param {void} _ -
64
59
  * @param {string} [selectedSubType] optionally limit to a single subtype
65
- * @returns {Promise<{metadata:AssetMap,type:string}>} Promise
60
+ * @returns {Promise.<{metadata: TYPE.AssetMap, type: string}>} Promise
66
61
  */
67
62
  static retrieveForCache(_, selectedSubType) {
68
63
  return this.retrieve(null, null, null, selectedSubType);
69
64
  }
70
65
 
71
66
  /**
72
- * Retrieves asset metadata for caching
67
+ * Retrieves asset metadata for templating
68
+ *
73
69
  * @param {string} templateDir Directory where retrieved metadata directory will be saved
74
70
  * @param {string} name name of the metadata file
75
- * @param {Util.TemplateMap} templateVariables variables to be replaced in the metadata
76
- * @param {AssetSubType} [selectedSubType] optionally limit to a single subtype
77
- * @returns {Promise<{metadata:AssetMap,type:string}>} Promise
71
+ * @param {TYPE.TemplateMap} templateVariables variables to be replaced in the metadata
72
+ * @param {TYPE.AssetSubType} [selectedSubType] optionally limit to a single subtype
73
+ * @returns {Promise.<{metadata: TYPE.AssetItem, type: string}>} Promise
78
74
  */
79
75
  static async retrieveAsTemplate(templateDir, name, templateVariables, selectedSubType) {
80
76
  const items = [];
@@ -100,17 +96,19 @@ class Asset extends MetadataType {
100
96
  }
101
97
  Util.logger.info(`Downloaded: ${this.definition.type} (${Object.keys(metadata).length})`);
102
98
 
103
- return { metadata: metadata, type: this.definition.type };
99
+ return { metadata: Object.values(metadata)[0], type: this.definition.type };
104
100
  }
105
101
  /**
106
102
  * helper for retrieve + retrieveAsTemplate
103
+ *
107
104
  * @private
108
- * @returns {AssetSubType[]} subtype array
105
+ * @returns {TYPE.AssetSubType[]} subtype array
109
106
  */
110
107
  static _getSubTypes() {
111
108
  const selectedSubTypeArr = this.properties.metaDataTypes.retrieve.filter((type) =>
112
109
  type.startsWith('asset-')
113
110
  );
111
+ /* eslint-disable unicorn/prefer-ternary */
114
112
  if (
115
113
  this.properties.metaDataTypes.retrieve.includes('asset') ||
116
114
  !selectedSubTypeArr.length
@@ -120,11 +118,13 @@ class Asset extends MetadataType {
120
118
  } else {
121
119
  return selectedSubTypeArr.map((type) => type.replace('asset-', ''));
122
120
  }
121
+ /* eslint-enable unicorn/prefer-ternary */
123
122
  }
124
123
 
125
124
  /**
126
125
  * Creates a single asset
127
- * @param {AssetItem} metadata a single asset
126
+ *
127
+ * @param {TYPE.AssetItem} metadata a single asset
128
128
  * @returns {Promise} Promise
129
129
  */
130
130
  static create(metadata) {
@@ -134,7 +134,8 @@ class Asset extends MetadataType {
134
134
 
135
135
  /**
136
136
  * Updates a single asset
137
- * @param {AssetItem} metadata a single asset
137
+ *
138
+ * @param {TYPE.AssetItem} metadata a single asset
138
139
  * @returns {Promise} Promise
139
140
  */
140
141
  static update(metadata) {
@@ -143,11 +144,13 @@ class Asset extends MetadataType {
143
144
  }
144
145
  /**
145
146
  * Retrieves Metadata of a specific asset type
146
- * @param {AssetSubType} subType group of similar assets to put in a folder (ie. images)
147
- * @param {AssetSubType[]} subTypeArray list of all asset types within this subtype
147
+ *
148
+ * @param {TYPE.AssetSubType} subType group of similar assets to put in a folder (ie. images)
149
+ * @param {TYPE.AssetSubType[]} subTypeArray list of all asset types within this subtype
148
150
  * @param {string} [retrieveDir] target directory for saving assets
149
151
  * @param {string} [templateName] name of the metadata file
150
- * @param {Util.TemplateMap} [templateVariables] variables to be replaced in the metadata
152
+ * @param {TYPE.TemplateMap} [templateVariables] variables to be replaced in the metadata
153
+ * @param {string} key customer key to filter by
151
154
  * @returns {Promise} Promise
152
155
  */
153
156
  static async requestSubType(
@@ -155,29 +158,29 @@ class Asset extends MetadataType {
155
158
  subTypeArray,
156
159
  retrieveDir,
157
160
  templateName,
158
- templateVariables
161
+ templateVariables,
162
+ key
159
163
  ) {
160
164
  if (retrieveDir) {
161
165
  Util.logger.info(`- Retrieving Subtype: ${subType}`);
162
166
  } else {
163
- Util.logger.info(`- Caching Subtype: ${subType}`);
167
+ Util.logger.info(` - Caching Subtype: ${subType}`);
164
168
  }
165
- const subtypeIds = subTypeArray?.map(subTypeItemName => Asset.definition.typeMapping[subTypeItemName]);
166
- const uri = 'asset/v1/content/assets/';
167
- const options = {
168
- uri: uri + 'query',
169
- headers: {},
170
- json: {
171
- page: {
172
- page: 1,
173
- pageSize: 50,
174
- },
175
- query: null,
176
- fields: [],
169
+ const subtypeIds = subTypeArray?.map(
170
+ (subTypeItemName) => Asset.definition.typeMapping[subTypeItemName]
171
+ );
172
+ const uri = 'asset/v1/content/assets/query';
173
+ const payload = {
174
+ page: {
175
+ page: 1,
176
+ pageSize: 50,
177
177
  },
178
+ query: null,
179
+ fields: ['category', 'createdDate', 'createdBy', 'modifiedDate', 'modifiedBy'], // get folder to allow duplicate name check against cache
178
180
  };
181
+
179
182
  if (templateName) {
180
- options.json.query = {
183
+ payload.query = {
181
184
  leftOperand: {
182
185
  property: 'assetType.id',
183
186
  simpleOperator: 'in',
@@ -185,22 +188,36 @@ class Asset extends MetadataType {
185
188
  },
186
189
  logicalOperator: 'AND',
187
190
  rightOperand: {
188
- property: 'name',
191
+ property: this.definition.nameField,
189
192
  simpleOperator: 'equal',
190
193
  value: templateName,
191
194
  },
192
195
  };
196
+ } else if (key) {
197
+ payload.query = {
198
+ leftOperand: {
199
+ property: 'assetType.id',
200
+ simpleOperator: 'in',
201
+ value: subtypeIds,
202
+ },
203
+ logicalOperator: 'AND',
204
+ rightOperand: {
205
+ property: this.definition.keyField,
206
+ simpleOperator: 'equal',
207
+ value: key,
208
+ },
209
+ };
193
210
  } else {
194
- options.json.query = {
211
+ payload.query = {
195
212
  property: 'assetType.id',
196
213
  simpleOperator: 'in',
197
214
  value: subtypeIds,
198
215
  };
199
- options.json.sort = [{ property: 'id', direction: 'ASC' }];
216
+ payload.sort = [{ property: 'id', direction: 'ASC' }];
200
217
  }
201
218
  // for caching we do not need these fields
202
219
  if (retrieveDir) {
203
- options.json.fields = [
220
+ payload.fields = [
204
221
  'fileProperties',
205
222
  'status',
206
223
  'category',
@@ -217,21 +234,18 @@ class Asset extends MetadataType {
217
234
  let lastPage = 0;
218
235
  let items = [];
219
236
  do {
220
- options.json.page.page = lastPage + 1;
221
- let response;
222
- await Util.retryOnError(`Retrying Subtype: ${subType}`, async () => {
223
- response = await this.client.RestClient.post(options);
224
- });
225
- if (response && response.body && response.body.items && response.body.items.length) {
237
+ payload.page.page = lastPage + 1;
238
+ const response = await this.client.rest.post(uri, payload);
239
+ if (response?.items?.length) {
226
240
  // sometimes the api will return a payload without items
227
241
  // --> ensure we only add proper items-arrays here
228
- items = items.concat(response.body.items);
242
+ items = items.concat(response.items);
229
243
  }
230
244
  // check if any more records
231
- if (response.body.message && response.body.message.includes('all shards failed')) {
245
+ if (response?.message?.includes('all shards failed')) {
232
246
  // When running certain filters, there is a limit of 10k on ElastiCache.
233
247
  // Since we sort by ID, we can get the last ID then run new requests from there
234
- options.json.query = {
248
+ payload.query = {
235
249
  leftOperand: {
236
250
  property: 'assetType.id',
237
251
  simpleOperator: 'in',
@@ -246,94 +260,100 @@ class Asset extends MetadataType {
246
260
  };
247
261
  lastPage = 0;
248
262
  moreResults = true;
249
- } else if (response.body.page * response.body.pageSize < response.body.count) {
263
+ } else if (response.page * response.pageSize < response.count) {
250
264
  moreResults = true;
251
- lastPage = Number(response.body.page);
265
+ lastPage = Number(response.page);
252
266
  } else {
253
267
  moreResults = false;
254
268
  }
255
269
  } while (moreResults);
270
+
256
271
  // only when we save results do we need the complete metadata or files. caching can skip these
257
272
  if (retrieveDir && items.length > 0) {
258
273
  // we have to wait on execution or it potentially causes memory reference issues when changing between BUs
259
274
  await this.requestAndSaveExtended(items, subType, retrieveDir, templateVariables);
275
+ Util.logger.debug(`Downloaded asset-${subType}: ${items.length}`);
276
+ } else if (retrieveDir && !items.length) {
277
+ Util.logger.info(` Downloaded asset-${subType}: ${items.length}`);
260
278
  }
261
279
  return items;
262
280
  }
263
281
  /**
264
282
  * Retrieves extended metadata (files or extended content) of asset
283
+ *
265
284
  * @param {Array} items array of items to retrieve
266
- * @param {AssetSubType} subType group of similar assets to put in a folder (ie. images)
285
+ * @param {TYPE.AssetSubType} subType group of similar assets to put in a folder (ie. images)
267
286
  * @param {string} retrieveDir target directory for saving assets
268
- * @param {Util.TemplateMap} [templateVariables] variables to be replaced in the metadata
287
+ * @param {TYPE.TemplateMap} [templateVariables] variables to be replaced in the metadata
269
288
  * @returns {Promise} Promise
270
289
  */
271
290
  static async requestAndSaveExtended(items, subType, retrieveDir, templateVariables) {
291
+ // disable CLI logs other than error while retrieving subtype
292
+ const loggerLevelBak = Util.logger.level;
293
+ if (loggerLevelBak !== 'error') {
294
+ // disable logging to cli other than Errors
295
+ Util.setLoggingLevel({ silent: true });
296
+ }
272
297
  const extendedBar = new cliProgress.SingleBar(
273
- { format: 'Processing [{bar}] {percentage}% | {value}/{total}' },
298
+ {
299
+ format:
300
+ ' Downloaded [{bar}] {percentage}% | {value}/{total} | asset-' +
301
+ subType,
302
+ },
274
303
  cliProgress.Presets.shades_classic
275
304
  );
276
305
 
277
306
  const completed = [];
307
+ const failed = [];
278
308
  // put in do loop to manage issues with connection timeout
279
309
  do {
280
310
  // use promise execution limiting to avoid rate limits on api, but speed up execution
281
311
  // start the progress bar with a total value of 200 and start value of 0
282
312
  extendedBar.start(items.length - completed.length, 0);
283
313
  try {
284
- const promiseMap = await bluebird.map(
285
- items,
286
- async (item) => {
287
- await Util.retryOnError(
288
- `Retrying asset-${subType} ${item[this.definition.nameField]} (${
289
- item[this.definition.keyField]
290
- })`,
291
- async () => {
292
- const metadata = {};
293
- // this is a file so extended is at another endpoint
294
- if (
295
- item.fileProperties &&
296
- item.fileProperties.extension &&
297
- !completed.includes(item.id)
298
- ) {
299
- metadata[item.customerKey] = item;
300
- if (templateVariables) {
301
- // do this here already because otherwise the extended file could be saved with wrong fileName
302
- const warningMsg =
303
- 'Ensure that Code that might be loading this via ContentBlockByKey is updated with the new key before deployment.';
304
- this.overrideKeyWithName(item, warningMsg);
305
- }
314
+ const rateLimit = pLimit(5);
315
+
316
+ const promiseMap = await Promise.all(
317
+ items.map((item) =>
318
+ rateLimit(async () => {
319
+ const metadata = {};
320
+ // this is a file so extended is at another endpoint
321
+ if (item?.fileProperties?.extension && !completed.includes(item.id)) {
322
+ try {
306
323
  // retrieving the extended file does not need to be awaited
307
324
  await this._retrieveExtendedFile(item, subType, retrieveDir);
325
+ } catch (ex) {
326
+ failed.push({ item: item, error: ex });
308
327
  }
309
- // this is a complex type which stores data in the asset itself
310
- else if (!completed.includes(item.id)) {
311
- const extendedItem = await this.client.RestClient.get({
312
- uri: 'asset/v1/content/assets/' + item.id,
313
- });
314
- metadata[item.customerKey] = extendedItem.body;
315
- }
316
- completed.push(item.id);
317
- await this.saveResults(
318
- metadata,
319
- retrieveDir,
320
- 'asset-' + subType,
321
- templateVariables
328
+ metadata[item.customerKey] = item;
329
+ }
330
+ // this is a complex type which stores data in the asset itself
331
+ else if (!completed.includes(item.id)) {
332
+ const extendedItem = await this.client.rest.get(
333
+ 'asset/v1/content/assets/' + item.id
322
334
  );
323
- // update the current value in your application..
324
- extendedBar.increment();
325
- },
326
- true
327
- );
328
- },
329
- { concurrency: 5 }
335
+ metadata[item.customerKey] = extendedItem;
336
+ }
337
+ completed.push(item.id);
338
+ await this.saveResults(
339
+ metadata,
340
+ retrieveDir,
341
+ 'asset-' + subType,
342
+ templateVariables
343
+ );
344
+ // update the current value in your application..
345
+ extendedBar.increment();
346
+ })
347
+ )
330
348
  );
331
349
 
332
350
  // stop the progress bar
333
351
  extendedBar.stop();
352
+ Asset._resetLogLevel(loggerLevelBak, failed);
334
353
  return promiseMap;
335
354
  } catch (ex) {
336
355
  extendedBar.stop();
356
+ Asset._resetLogLevel(loggerLevelBak, failed);
337
357
  // timeouts should be retried, others can be retried
338
358
  if (ex.code !== 'ETIMEDOUT') {
339
359
  throw ex;
@@ -341,18 +361,66 @@ class Asset extends MetadataType {
341
361
  }
342
362
  } while (completed.length === items.length);
343
363
  }
364
+
365
+ /**
366
+ * helper that reset the log level and prints errors
367
+ *
368
+ * @private
369
+ * @param {'info'|'verbose'|'debug'|'error'} loggerLevelBak original logger level
370
+ * @param {object[]} failed array of failed items
371
+ */
372
+ static _resetLogLevel(loggerLevelBak, failed) {
373
+ // re-enable CLI logs
374
+ if (loggerLevelBak !== 'error') {
375
+ // reset logging level
376
+ let obj;
377
+ switch (loggerLevelBak) {
378
+ case 'info':
379
+ obj = {};
380
+ break;
381
+ case 'verbose':
382
+ obj = { verbose: true };
383
+ break;
384
+ case 'debug':
385
+ obj = { debug: true };
386
+ break;
387
+ case 'error':
388
+ obj = { silent: true };
389
+ }
390
+ Util.setLoggingLevel(obj);
391
+ }
392
+
393
+ if (failed.length) {
394
+ Util.logger.warn(
395
+ ` - Failed to download ${failed.length} extended file${
396
+ failed.length > 1 ? 's' : ''
397
+ }:`
398
+ );
399
+ for (const fail of failed) {
400
+ Util.logger.warn(
401
+ ` - "${fail.item.name}" (${fail.item.customerKey}) in ${fail.item.r__folder_Path}: ${fail.error.message}`
402
+ );
403
+ Util.logger.debug(`-- Error: ${fail.error.message}`);
404
+ Util.logger.debug(`-- AssetType: ${fail.item.assetType.name}`);
405
+ Util.logger.debug(`-- fileProperties: ${JSON.stringify(fail.item.fileProperties)}`);
406
+ }
407
+ Util.logger.info(
408
+ ' - You will still find a JSON file for each of these in the download directory.'
409
+ );
410
+ }
411
+ }
412
+
344
413
  /**
345
414
  * Some metadata types store their actual content as a separate file, e.g. images
346
415
  * This method retrieves these and saves them alongside the metadata json
347
- * @param {AssetItem} metadata a single asset
348
- * @param {AssetSubType} subType group of similar assets to put in a folder (ie. images)
416
+ *
417
+ * @param {TYPE.AssetItem} metadata a single asset
418
+ * @param {TYPE.AssetSubType} subType group of similar assets to put in a folder (ie. images)
349
419
  * @param {string} retrieveDir target directory for saving assets
350
- * @returns {Promise<void>} -
420
+ * @returns {Promise.<void>} -
351
421
  */
352
422
  static async _retrieveExtendedFile(metadata, subType, retrieveDir) {
353
- const file = await this.client.RestClient.get({
354
- uri: 'asset/v1/content/assets/' + metadata.id + '/file',
355
- });
423
+ const file = await this.client.rest.get('asset/v1/content/assets/' + metadata.id + '/file');
356
424
 
357
425
  // to handle uploaded files that bear the same name, SFMC engineers decided to add a number after the fileName
358
426
  // however, their solution was not following standards: fileName="header.png (4) " and then extension="png (4) "
@@ -362,7 +430,7 @@ class Asset extends MetadataType {
362
430
  [retrieveDir, this.definition.type, subType],
363
431
  metadata.customerKey,
364
432
  fileExt,
365
- file.body,
433
+ file,
366
434
  'base64'
367
435
  );
368
436
  }
@@ -370,10 +438,11 @@ class Asset extends MetadataType {
370
438
  * helper for this.preDeployTasks()
371
439
  * Some metadata types store their actual content as a separate file, e.g. images
372
440
  * This method reads these from the local FS stores them in the metadata object allowing to deploy it
373
- * @param {AssetItem} metadata a single asset
374
- * @param {AssetSubType} subType group of similar assets to put in a folder (ie. images)
441
+ *
442
+ * @param {TYPE.AssetItem} metadata a single asset
443
+ * @param {TYPE.AssetSubType} subType group of similar assets to put in a folder (ie. images)
375
444
  * @param {string} deployDir directory of deploy files
376
- * @returns {Promise<void>} -
445
+ * @returns {Promise.<void>} -
377
446
  */
378
447
  static async _readExtendedFileFromFS(metadata, subType, deployDir) {
379
448
  if (metadata.fileProperties && metadata.fileProperties.extension) {
@@ -381,7 +450,7 @@ class Asset extends MetadataType {
381
450
  // however, their solution was not following standards: fileName="header.png (4) " and then extension="png (4) "
382
451
  const fileExt = metadata.fileProperties.extension.split(' ')[0];
383
452
 
384
- metadata.file = await File.readFile(
453
+ metadata.file = await File.readFilteredFilename(
385
454
  [deployDir, this.definition.type, subType],
386
455
  metadata.customerKey,
387
456
  fileExt,
@@ -391,36 +460,25 @@ class Asset extends MetadataType {
391
460
  }
392
461
  /**
393
462
  * manages post retrieve steps
394
- * @param {AssetItem} metadata a single asset
395
- * @param {string} [_] unused
396
- * @param {Boolean} isTemplating signals that we are retrieving templates
397
- * @returns {CodeExtractItem} metadata
463
+ *
464
+ * @param {TYPE.AssetItem} metadata a single asset
465
+ * @returns {TYPE.CodeExtractItem} metadata
398
466
  */
399
- static postRetrieveTasks(metadata, _, isTemplating) {
400
- // if retrieving template, replace the name with customer key if that wasn't already the case
401
- if (isTemplating) {
402
- const warningMsg =
403
- 'Ensure that Code that might be loading this via ContentBlockByKey is updated with the new key before deployment.';
404
- this.overrideKeyWithName(metadata, warningMsg);
405
- }
467
+ static postRetrieveTasks(metadata) {
406
468
  return this.parseMetadata(metadata);
407
469
  }
408
470
 
409
471
  /**
410
472
  * prepares an asset definition for deployment
411
- * @param {AssetItem} metadata a single asset
473
+ *
474
+ * @param {TYPE.AssetItem} metadata a single asset
412
475
  * @param {string} deployDir directory of deploy files
413
- * @returns {Promise<AssetItem>} Promise
476
+ * @param {TYPE.BuObject} buObject buObject properties for auth
477
+ * @returns {Promise.<TYPE.AssetItem>} Promise
414
478
  */
415
- static async preDeployTasks(metadata, deployDir) {
479
+ static async preDeployTasks(metadata, deployDir, buObject) {
416
480
  // additonalattributes fail where the value is "" so we need to remove them from deploy
417
- if (
418
- metadata.data &&
419
- metadata.data.email &&
420
- metadata.data.email &&
421
- metadata.data.email.attributes &&
422
- metadata.data.email.attributes.length > 0
423
- ) {
481
+ if (metadata?.data?.email?.attributes?.length > 0) {
424
482
  metadata.data.email.attributes = metadata.data.email.attributes.filter(
425
483
  (attr) => attr.value
426
484
  );
@@ -428,7 +486,7 @@ class Asset extends MetadataType {
428
486
 
429
487
  // folder
430
488
  metadata.category = {
431
- id: Util.getFromCache(this.cache, 'folder', metadata.r__folder_Path, 'Path', 'ID'),
489
+ id: cache.searchForField('folder', metadata.r__folder_Path, 'Path', 'ID'),
432
490
  };
433
491
  delete metadata.r__folder_Path;
434
492
 
@@ -436,7 +494,7 @@ class Asset extends MetadataType {
436
494
  metadata.assetType.id = this.definition.typeMapping[metadata.assetType.name];
437
495
 
438
496
  // define asset's subtype
439
- const subType = this.getSubtype(metadata);
497
+ const subType = this._getSubtype(metadata);
440
498
 
441
499
  // #1 get text extracts back into the JSON
442
500
  await this._mergeCode(metadata, deployDir, subType);
@@ -444,51 +502,190 @@ class Asset extends MetadataType {
444
502
  // #2 get file from local disk and insert as base64
445
503
  await this._readExtendedFileFromFS(metadata, subType, deployDir);
446
504
 
505
+ // only execute #3 if we are deploying / copying from one BU to another, not while using mcdev as a developer tool
506
+ if (
507
+ buObject.mid &&
508
+ metadata.memberId !== buObject.mid &&
509
+ !metadata[this.definition.keyField].startsWith(buObject.mid)
510
+ ) {
511
+ // #3 make sure customer key is unique by prefixing it with target MID (unless we are deploying to the same MID)
512
+ // check if this prefixed with the source MID
513
+ const suffix = '-' + buObject.mid;
514
+ // for customer key max is 36 chars
515
+ metadata[this.definition.keyField] =
516
+ metadata[this.definition.keyField].slice(0, Math.max(0, 36 - suffix.length)) +
517
+ suffix;
518
+ }
519
+ // #4 make sure the name is unique
520
+ const assetCache = cache.getCache()[this.definition.type];
521
+ const namesInFolder = Object.keys(assetCache)
522
+ .filter((key) => assetCache[key].category.id === metadata.category.id)
523
+ .map((key) => ({
524
+ type: this._getMainSubtype(assetCache[key].assetType.name),
525
+ key: key,
526
+ name: assetCache[key].name,
527
+ }));
528
+ // if the name is already in the folder for a different key, add a number to the end
529
+ metadata[this.definition.nameField] = this._findUniqueName(
530
+ metadata[this.definition.keyField],
531
+ metadata[this.definition.nameField],
532
+ this._getMainSubtype(metadata.assetType.name),
533
+ namesInFolder
534
+ );
447
535
  return metadata;
448
536
  }
537
+ /**
538
+ * find the subType matching the extendedSubType
539
+ *
540
+ * @param {string} extendedSubType webpage, htmlblock, etc
541
+ * @returns {string} subType: block, message, other, etc
542
+ */
543
+ static _getMainSubtype(extendedSubType) {
544
+ return Object.keys(this.definition.extendedSubTypes).find((subType) =>
545
+ this.definition.extendedSubTypes[subType].includes(extendedSubType)
546
+ );
547
+ }
548
+ /**
549
+ * helper to find a new unique name during asset creation
550
+ *
551
+ * @private
552
+ * @param {string} key key of the asset
553
+ * @param {string} name name of the asset
554
+ * @param {string} type assetType-name
555
+ * @param {string[]} namesInFolder names of the assets in the same folder
556
+ * @returns {string} new name
557
+ */
558
+ static _findUniqueName(key, name, type, namesInFolder) {
559
+ let newName = name;
560
+ let suffix;
561
+ let i = 1;
562
+ while (
563
+ namesInFolder.find(
564
+ (item) => item.name === newName && item.type === type && item.key !== key
565
+ )
566
+ ) {
567
+ suffix = ' (' + i + ')';
568
+ // for customer key max is 100 chars
569
+ newName = name.slice(0, Math.max(0, 100 - suffix.length)) + suffix;
570
+ i++;
571
+ }
572
+ return newName;
573
+ }
449
574
  /**
450
575
  * determines the subtype of the current asset
451
- * @param {AssetItem} metadata a single asset
452
- * @returns {AssetSubType} subtype
576
+ *
577
+ * @private
578
+ * @param {TYPE.AssetItem} metadata a single asset
579
+ * @returns {TYPE.AssetSubType} subtype
453
580
  */
454
- static getSubtype(metadata) {
581
+ static _getSubtype(metadata) {
455
582
  for (const sub in this.definition.extendedSubTypes) {
456
583
  if (this.definition.extendedSubTypes[sub].includes(metadata.assetType.name)) {
457
584
  return sub;
458
585
  }
459
586
  }
460
587
  }
461
-
462
588
  /**
463
589
  * helper for buildDefinition
464
590
  * handles extracted code if any are found for complex types
591
+ *
465
592
  * @param {string} templateDir Directory where metadata templates are stored
466
593
  * @param {string} targetDir Directory where built definitions will be saved
467
- * @param {AssetItem} metadata main JSON file that was read from file system
468
- * @param {Util.TemplateMap} variables variables to be replaced in the metadata
594
+ * @param {TYPE.AssetItem} metadata main JSON file that was read from file system
595
+ * @param {TYPE.TemplateMap} templateVariables variables to be replaced in the metadata
469
596
  * @param {string} templateName name of the template to be built
470
- * @returns {Promise<void>} Promise
597
+ * @returns {Promise.<void>} -
471
598
  */
472
- static async buildDefinitionForExtracts(
599
+ static buildDefinitionForNested(
473
600
  templateDir,
474
601
  targetDir,
475
602
  metadata,
476
- variables,
603
+ templateVariables,
477
604
  templateName
478
605
  ) {
479
- // clone metadata to ensure the main file is not modified by what we do in here
606
+ return this._buildForNested(
607
+ templateDir,
608
+ targetDir,
609
+ metadata,
610
+ templateVariables,
611
+ templateName,
612
+ 'definition'
613
+ );
614
+ }
615
+ /**
616
+ * helper for buildTemplate
617
+ * handles extracted code if any are found for complex types
618
+ *
619
+ * @example assets of type codesnippetblock will result in 1 json and 1 amp/html file. both files need to be run through templating
620
+ * @param {string} templateDir Directory where metadata templates are stored
621
+ * @param {string|string[]} targetDir (List of) Directory where built definitions will be saved
622
+ * @param {TYPE.AssetItem} metadata main JSON file that was read from file system
623
+ * @param {TYPE.TemplateMap} templateVariables variables to be replaced in the metadata
624
+ * @param {string} templateName name of the template to be built
625
+ * @returns {Promise.<void>} -
626
+ */
627
+ static buildTemplateForNested(
628
+ templateDir,
629
+ targetDir,
630
+ metadata,
631
+ templateVariables,
632
+ templateName
633
+ ) {
634
+ return this._buildForNested(
635
+ templateDir,
636
+ targetDir,
637
+ metadata,
638
+ templateVariables,
639
+ templateName,
640
+ 'template'
641
+ );
642
+ }
643
+
644
+ /**
645
+ * helper for buildDefinition
646
+ * handles extracted code if any are found for complex types
647
+ *
648
+ * @param {string} templateDir Directory where metadata templates are stored
649
+ * @param {string} targetDir Directory where built definitions will be saved
650
+ * @param {TYPE.AssetItem} metadata main JSON file that was read from file system
651
+ * @param {TYPE.TemplateMap} templateVariables variables to be replaced in the metadata
652
+ * @param {string} templateName name of the template to be built
653
+ * @param {'definition'|'template'} mode defines what we use this helper for
654
+ * @returns {Promise.<void>} -
655
+ */
656
+ static async _buildForNested(
657
+ templateDir,
658
+ targetDir,
659
+ metadata,
660
+ templateVariables,
661
+ templateName,
662
+ mode
663
+ ) {
664
+ // * because asset's _mergeCode() is overwriting 'metadata', clone it to ensure the main file is not modified by what we do in here
480
665
  metadata = JSON.parse(JSON.stringify(metadata));
481
666
 
482
667
  // #1 text extracts
483
668
  // define asset's subtype
484
- const subType = this.getSubtype(metadata);
669
+ const subType = this._getSubtype(metadata);
485
670
  // get HTML from filesystem
486
671
  const fileList = await this._mergeCode(metadata, templateDir, subType, templateName);
487
672
  // replace template variables with their values
488
673
  for (const extractedFile of fileList) {
489
674
  try {
490
- extractedFile.content = Mustache.render(extractedFile.content, variables);
491
- } catch (ex) {
675
+ if (mode === 'definition') {
676
+ // replace template variables with their values
677
+ extractedFile.content = this.applyTemplateValues(
678
+ extractedFile.content,
679
+ templateVariables
680
+ );
681
+ } else if (mode === 'template') {
682
+ // replace template values with corresponding variable names
683
+ extractedFile.content = this.applyTemplateNames(
684
+ extractedFile.content,
685
+ templateVariables
686
+ );
687
+ }
688
+ } catch {
492
689
  throw new Error(
493
690
  `${this.definition.type}:: Error applying template variables on ${
494
691
  metadata[this.definition.keyField]
@@ -498,12 +695,12 @@ class Asset extends MetadataType {
498
695
  }
499
696
 
500
697
  // #2 binary extracts
501
- if (metadata.fileProperties && metadata.fileProperties.extension) {
698
+ if (metadata?.fileProperties?.extension) {
502
699
  // to handle uploaded files that bear the same name, SFMC engineers decided to add a number after the fileName
503
700
  // however, their solution was not following standards: fileName="header.png (4) " and then extension="png (4) "
504
701
  const fileExt = metadata.fileProperties.extension.split(' ')[0];
505
702
 
506
- const filecontent = await File.readFile(
703
+ const filecontent = await File.readFilteredFilename(
507
704
  [templateDir, this.definition.type, subType],
508
705
  metadata.customerKey,
509
706
  fileExt,
@@ -535,14 +732,14 @@ class Asset extends MetadataType {
535
732
 
536
733
  /**
537
734
  * parses retrieved Metadata before saving
538
- * @param {AssetItem} metadata a single asset definition
539
- * @returns {CodeExtractItem} parsed metadata definition
735
+ *
736
+ * @param {TYPE.AssetItem} metadata a single asset definition
737
+ * @returns {TYPE.CodeExtractItem} parsed metadata definition
540
738
  */
541
739
  static parseMetadata(metadata) {
542
740
  // folder
543
741
  try {
544
- metadata.r__folder_Path = Util.getFromCache(
545
- this.cache,
742
+ metadata.r__folder_Path = cache.searchForField(
546
743
  'folder',
547
744
  metadata.category.id,
548
745
  'ID',
@@ -553,7 +750,7 @@ class Asset extends MetadataType {
553
750
  // ! if we don't catch this error here we end up saving the actual asset but not its corresponding JSON
554
751
  Util.logger.debug(ex.message);
555
752
  Util.logger.warn(
556
- `Could not find folder with ID ${metadata.category.id} for '${metadata.name}' (${metadata.customerKey})`
753
+ ` - Could not find folder with ID ${metadata.category.id} for '${metadata.name}' (${metadata.customerKey})`
557
754
  );
558
755
  }
559
756
  // extract HTML for selected subtypes and convert payload for easier processing in MetadataType.saveResults()
@@ -562,42 +759,41 @@ class Asset extends MetadataType {
562
759
  }
563
760
  /**
564
761
  * helper for this.preDeployTasks() that loads extracted code content back into JSON
565
- * @param {AssetItem} metadata a single asset definition
762
+ *
763
+ * @param {TYPE.AssetItem} metadata a single asset definition
566
764
  * @param {string} deployDir directory of deploy files
567
- * @param {AssetSubType} subType asset-subtype name
765
+ * @param {TYPE.AssetSubType} subType asset-subtype name
568
766
  * @param {string} [templateName] name of the template used to built defintion (prior applying templating)
569
- * @returns {Promise<MetadataType.CodeExtract[]>} fileList for templating (disregarded during deployment)
767
+ * @param {boolean} [fileListOnly] does not read file contents nor update metadata if true
768
+ * @returns {Promise.<TYPE.CodeExtract[]>} fileList for templating (disregarded during deployment)
570
769
  */
571
- static async _mergeCode(metadata, deployDir, subType, templateName) {
770
+ static async _mergeCode(metadata, deployDir, subType, templateName, fileListOnly = false) {
572
771
  const subtypeExtension = `.${this.definition.type}-${subType}-meta`;
573
772
  const fileList = [];
574
773
  let subDirArr;
575
774
  let readDirArr;
576
-
577
775
  switch (metadata.assetType.name) {
578
- case 'webpage': // asset
579
776
  case 'templatebasedemail': // message
580
777
  case 'htmlemail': // message
581
778
  // this complex type always creates its own subdir per asset
582
779
  subDirArr = [this.definition.type, subType];
583
- readDirArr = [
584
- deployDir,
585
- ...subDirArr,
586
- templateName ? templateName : metadata.customerKey,
587
- ];
780
+ readDirArr = [deployDir, ...subDirArr, templateName || metadata.customerKey];
588
781
 
589
782
  // metadata.views.html.content (mandatory)
783
+ // the main content can be empty (=not set up yet) hence check if we did extract sth or else readFile() will print error msgs
590
784
  if (
591
- File.existsSync(
785
+ (await File.pathExists(
592
786
  File.normalizePath([...readDirArr, `index${subtypeExtension}.html`])
593
- )
787
+ )) &&
788
+ metadata.views.html
594
789
  ) {
595
- // the main content can be empty (=not set up yet) hence check if we did extract sth or else readFile() will print error msgs
596
- metadata.views.html.content = await File.readFile(
597
- readDirArr,
598
- 'index' + subtypeExtension,
599
- 'html'
600
- );
790
+ if (!fileListOnly) {
791
+ metadata.views.html.content = await File.readFilteredFilename(
792
+ readDirArr,
793
+ 'index' + subtypeExtension,
794
+ 'html'
795
+ );
796
+ }
601
797
 
602
798
  if (templateName) {
603
799
  // to use this method in templating, store a copy of the info in fileList
@@ -611,15 +807,17 @@ class Asset extends MetadataType {
611
807
  }
612
808
 
613
809
  // metadata.views.html.slots.<>.blocks.<>.content (optional)
614
- if (metadata.views && metadata.views.html && metadata.views.html.slots) {
810
+ if (metadata?.views?.html?.slots) {
615
811
  await this._mergeCode_slots(
812
+ 'views.html.slots',
616
813
  metadata.views.html.slots,
617
814
  readDirArr,
618
815
  subtypeExtension,
619
816
  subDirArr,
620
817
  fileList,
621
818
  metadata.customerKey,
622
- templateName
819
+ templateName,
820
+ fileListOnly
623
821
  );
624
822
  }
625
823
  break;
@@ -628,22 +826,23 @@ class Asset extends MetadataType {
628
826
  subDirArr = [this.definition.type, subType];
629
827
  readDirArr = [deployDir, ...subDirArr];
630
828
  if (
631
- File.existsSync(
829
+ await File.pathExists(
632
830
  File.normalizePath([
633
831
  ...readDirArr,
634
832
  `${
635
- templateName ? templateName : metadata.customerKey
833
+ templateName || metadata.customerKey // TODO check why this could be templateName
636
834
  }${subtypeExtension}.html`,
637
835
  ])
638
836
  )
639
837
  ) {
640
838
  // the main content can be empty (=not set up yet) hence check if we did extract sth or else readFile() will print error msgs
641
- metadata.views.text.content = await File.readFile(
642
- readDirArr,
643
- (templateName ? templateName : metadata.customerKey) + subtypeExtension,
644
- 'html'
645
- );
646
-
839
+ if (!fileListOnly) {
840
+ metadata.views.text.content = await File.readFilteredFilename(
841
+ readDirArr,
842
+ (templateName || metadata.customerKey) + subtypeExtension,
843
+ 'html'
844
+ );
845
+ }
647
846
  if (templateName) {
648
847
  // to use this method in templating, store a copy of the info in fileList
649
848
  fileList.push({
@@ -655,63 +854,157 @@ class Asset extends MetadataType {
655
854
  }
656
855
  }
657
856
  break;
658
- case 'freeformblock': // block
659
- case 'htmlblock': // block
660
- case 'textblock': // block
661
- case 'smartcaptureblock': // other
662
- case 'codesnippetblock': // other
663
- // metadata.content
857
+ case 'webpage': // asset
858
+ // this complex type always creates its own subdir per asset
664
859
  subDirArr = [this.definition.type, subType];
665
- readDirArr = [deployDir, ...subDirArr];
860
+ readDirArr = [deployDir, ...subDirArr, templateName || metadata.customerKey];
861
+
862
+ // metadata.views.html.slots.<>.blocks.<>.content (optional) (pre & post 20222)
863
+ if (metadata?.views?.html?.slots) {
864
+ await this._mergeCode_slots(
865
+ 'views.html.slots',
866
+ metadata.views.html.slots,
867
+ readDirArr,
868
+ subtypeExtension,
869
+ subDirArr,
870
+ fileList,
871
+ metadata.customerKey,
872
+ templateName,
873
+ fileListOnly
874
+ );
875
+ }
876
+
877
+ // +++ old webpages / pre-2022 +++
878
+ // metadata.views.html.content (mandatory)
666
879
  if (
667
- File.existsSync(
880
+ (await File.pathExists(
668
881
  File.normalizePath([
669
882
  ...readDirArr,
670
- `${
671
- templateName ? templateName : metadata.customerKey
672
- }${subtypeExtension}.html`,
883
+ `views.html.content${subtypeExtension}.html`,
673
884
  ])
885
+ )) && // the main content can be empty (=not set up yet) hence check if we did extract sth or else readFile() will print error msgs
886
+ metadata.views?.html
887
+ ) {
888
+ if (!fileListOnly) {
889
+ metadata.views.html.content = await File.readFilteredFilename(
890
+ readDirArr,
891
+ 'views.html.content' + subtypeExtension,
892
+ 'html'
893
+ );
894
+ }
895
+ if (templateName) {
896
+ // to use this method in templating, store a copy of the info in fileList
897
+ fileList.push({
898
+ subFolder: [...subDirArr, metadata.customerKey],
899
+ fileName: 'views.html.content' + subtypeExtension,
900
+ fileExt: 'html',
901
+ content: metadata.views.html.content,
902
+ });
903
+ }
904
+ }
905
+
906
+ // +++ new webpages / 2022+ +++
907
+ // metadata.content
908
+ if (
909
+ await File.pathExists(
910
+ File.normalizePath([...readDirArr, `content${subtypeExtension}.html`])
674
911
  )
675
912
  ) {
676
913
  // the main content can be empty (=not set up yet) hence check if we did extract sth or else readFile() will print error msgs
677
- metadata.content = await File.readFile(
678
- readDirArr,
679
- (templateName ? templateName : metadata.customerKey) + subtypeExtension,
680
- 'html'
681
- );
914
+ if (!fileListOnly) {
915
+ metadata.content = await File.readFilteredFilename(
916
+ readDirArr,
917
+ 'content' + subtypeExtension,
918
+ 'html'
919
+ );
920
+ }
682
921
  if (templateName) {
683
922
  // to use this method in templating, store a copy of the info in fileList
684
923
  fileList.push({
685
- subFolder: subDirArr,
686
- fileName: metadata.customerKey + subtypeExtension,
924
+ subFolder: [...subDirArr, metadata.customerKey],
925
+ fileName: 'content' + subtypeExtension,
687
926
  fileExt: 'html',
688
- content: metadata.content,
927
+ content: metadata.views.html.content,
689
928
  });
690
929
  }
691
930
  }
931
+
932
+ break;
933
+ case 'buttonblock': // block - Button Block
934
+ case 'freeformblock': // block
935
+ case 'htmlblock': // block
936
+ case 'icemailformblock': // block - Interactive Content Email Form
937
+ case 'imageblock': // block - Image Block
938
+ case 'textblock': // block
939
+ case 'smartcaptureblock': // other
940
+ case 'codesnippetblock': // other
941
+ // metadata.content
942
+ subDirArr = [this.definition.type, subType];
943
+ readDirArr = [deployDir, ...subDirArr];
944
+ const fileExtArr = ['html']; // eslint-disable-line no-case-declarations
945
+ if (metadata.assetType.name === 'codesnippetblock') {
946
+ // extracted code snippets should end on the right extension
947
+ // we are making a few assumptions during retrieve to pick the right one
948
+ fileExtArr.push('amp', 'sjss');
949
+ }
950
+ for (const ext of fileExtArr) {
951
+ if (
952
+ await File.pathExists(
953
+ File.normalizePath([
954
+ ...readDirArr,
955
+ `${templateName || metadata.customerKey}${subtypeExtension}.${ext}`,
956
+ ])
957
+ )
958
+ ) {
959
+ // the main content can be empty (=not set up yet) hence check if we did extract sth or else readFile() will print error msgs
960
+ if (!fileListOnly) {
961
+ metadata.content = await File.readFilteredFilename(
962
+ readDirArr,
963
+ (templateName || metadata.customerKey) + subtypeExtension,
964
+ ext
965
+ );
966
+ }
967
+ if (templateName) {
968
+ // to use this method in templating, store a copy of the info in fileList
969
+ fileList.push({
970
+ subFolder: subDirArr,
971
+ fileName: metadata.customerKey + subtypeExtension,
972
+ fileExt: ext,
973
+ content: metadata.content,
974
+ });
975
+ }
976
+ // break loop when found
977
+ break;
978
+ }
979
+ }
692
980
  break;
693
981
  }
694
982
  return fileList;
695
983
  }
696
984
  /**
697
985
  * helper for this.preDeployTasks() that loads extracted code content back into JSON
698
- * @param {Object} metadataSlots metadata.views.html.slots or deeper slots.<>.blocks.<>.slots
986
+ *
987
+ * @param {string} prefix usually the customerkey
988
+ * @param {object} metadataSlots metadata.views.html.slots or deeper slots.<>.blocks.<>.slots
699
989
  * @param {string[]} readDirArr directory of deploy files
700
990
  * @param {string} subtypeExtension asset-subtype name ending on -meta
701
991
  * @param {string[]} subDirArr directory of files w/o leading deploy dir
702
- * @param {Object[]} fileList directory of files w/o leading deploy dir
992
+ * @param {object[]} fileList directory of files w/o leading deploy dir
703
993
  * @param {string} customerKey external key of template (could have been changed if used during templating)
704
994
  * @param {string} [templateName] name of the template used to built defintion (prior applying templating)
705
- * @returns {Promise<void>} -
995
+ * @param {boolean} [fileListOnly] does not read file contents nor update metadata if true
996
+ * @returns {Promise.<void>} -
706
997
  */
707
998
  static async _mergeCode_slots(
999
+ prefix,
708
1000
  metadataSlots,
709
1001
  readDirArr,
710
1002
  subtypeExtension,
711
1003
  subDirArr,
712
1004
  fileList,
713
1005
  customerKey,
714
- templateName
1006
+ templateName,
1007
+ fileListOnly = false
715
1008
  ) {
716
1009
  for (const slot in metadataSlots) {
717
1010
  if (Object.prototype.hasOwnProperty.call(metadataSlots, slot)) {
@@ -720,9 +1013,9 @@ class Asset extends MetadataType {
720
1013
  if (slotObj.blocks) {
721
1014
  for (const block in slotObj.blocks) {
722
1015
  if (Object.prototype.hasOwnProperty.call(slotObj.blocks, block)) {
723
- const fileName = `${slot}-${block}${subtypeExtension}`;
1016
+ const fileName = `${prefix}.[${slot}-${block}]${subtypeExtension}`;
724
1017
  if (
725
- File.existsSync(
1018
+ await File.pathExists(
726
1019
  File.normalizePath([
727
1020
  ...readDirArr,
728
1021
  'blocks',
@@ -732,11 +1025,13 @@ class Asset extends MetadataType {
732
1025
  ) {
733
1026
  // the main content can be empty (=not set up yet) hence check if we did extract sth or else readFile() will print error msgs
734
1027
  // if an extracted block was found, save it back into JSON
735
- slotObj.blocks[block].content = await File.readFile(
736
- [...readDirArr, 'blocks'],
737
- fileName,
738
- 'html'
739
- );
1028
+ if (!fileListOnly) {
1029
+ slotObj.blocks[block].content = await File.readFilteredFilename(
1030
+ [...readDirArr, 'blocks'],
1031
+ fileName,
1032
+ 'html'
1033
+ );
1034
+ }
740
1035
  if (templateName) {
741
1036
  // to use this method in templating, store a copy of the info in fileList
742
1037
  fileList.push({
@@ -750,6 +1045,7 @@ class Asset extends MetadataType {
750
1045
  if (slotObj.blocks[block].slots) {
751
1046
  // * recursion: each block can have slots of its own
752
1047
  await this._mergeCode_slots(
1048
+ `${prefix}.[${slot}-${block}]`,
753
1049
  slotObj.blocks[block].slots,
754
1050
  readDirArr,
755
1051
  subtypeExtension,
@@ -768,17 +1064,18 @@ class Asset extends MetadataType {
768
1064
  /**
769
1065
  * helper for this.parseMetadata() that finds code content in JSON and extracts it
770
1066
  * to allow saving that separately and formatted
771
- * @param {AssetItem} metadata a single asset definition
772
- * @returns {CodeExtractItem} { json: metadata, codeArr: object[], subFolder: string[] }
1067
+ *
1068
+ * @param {TYPE.AssetItem} metadata a single asset definition
1069
+ * @returns {TYPE.CodeExtractItem} { json: metadata, codeArr: object[], subFolder: string[] }
773
1070
  */
774
1071
  static _extractCode(metadata) {
775
1072
  const codeArr = [];
1073
+ let subType;
776
1074
  switch (metadata.assetType.name) {
777
- case 'webpage': // asset
778
1075
  case 'templatebasedemail': // message
779
1076
  case 'htmlemail': // message
780
1077
  // metadata.views.html.content (mandatory)
781
- if (metadata.views.html.content && metadata.views.html.content.length) {
1078
+ if (metadata.views?.html?.content?.length) {
782
1079
  codeArr.push({
783
1080
  subFolder: null,
784
1081
  fileName: 'index',
@@ -789,14 +1086,14 @@ class Asset extends MetadataType {
789
1086
  }
790
1087
 
791
1088
  // metadata.views.html.slots.<>.blocks.<>.content (optional)
792
- if (metadata.views && metadata.views.html && metadata.views.html.slots) {
793
- this._extractCode_slots(metadata.views.html.slots, codeArr);
1089
+ if (metadata.views?.html?.slots) {
1090
+ this._extractCode_slots('views.html.slots', metadata.views.html.slots, codeArr);
794
1091
  }
795
1092
 
796
1093
  return { json: metadata, codeArr: codeArr, subFolder: [metadata.customerKey] };
797
1094
  case 'textonlyemail': // message
798
1095
  // metadata.views.text.content
799
- if (metadata.views.text.content && metadata.views.text.content.length) {
1096
+ if (metadata.views?.text?.content?.length) {
800
1097
  codeArr.push({
801
1098
  subFolder: null,
802
1099
  fileName: metadata.customerKey,
@@ -806,32 +1103,80 @@ class Asset extends MetadataType {
806
1103
  delete metadata.views.text.content;
807
1104
  }
808
1105
  return { json: metadata, codeArr: codeArr, subFolder: null };
1106
+ case 'webpage': // asset
1107
+ // metadata.views.html.content (pre & post 20222)
1108
+ if (metadata.views?.html?.content?.length) {
1109
+ codeArr.push({
1110
+ subFolder: null,
1111
+ fileName: 'views.html.content',
1112
+ fileExt: 'html',
1113
+ content: metadata.views.html.content,
1114
+ });
1115
+ delete metadata.views.html.content;
1116
+ }
1117
+
1118
+ // +++ old webpages / pre-2022 +++
1119
+ // metadata.views.html.slots.<>.blocks.<>.content (optional)
1120
+ if (metadata.views?.html?.slots) {
1121
+ this._extractCode_slots('views.html.slots', metadata.views.html.slots, codeArr);
1122
+ }
1123
+
1124
+ // +++ new webpages / 2022+ +++
1125
+ // metadata.content
1126
+ if (metadata?.content?.length) {
1127
+ codeArr.push({
1128
+ subFolder: null,
1129
+ fileName: 'content',
1130
+ fileExt: 'html',
1131
+ content: metadata.content,
1132
+ });
1133
+ delete metadata.content;
1134
+ }
1135
+ return { json: metadata, codeArr: codeArr, subFolder: [metadata.customerKey] };
1136
+ case 'buttonblock': // block - Button Block
809
1137
  case 'freeformblock': // block
810
1138
  case 'htmlblock': // block
1139
+ case 'icemailformblock': // block - Interactive Content Email Form
1140
+ case 'imageblock': // block - Image Block
811
1141
  case 'textblock': // block
812
1142
  case 'smartcaptureblock': // other
813
1143
  case 'codesnippetblock': // other
814
1144
  // metadata.content
815
- if (metadata.content && metadata.content.length) {
1145
+ let fileExt = 'html'; // eslint-disable-line no-case-declarations
1146
+ if (
1147
+ metadata.assetType.name === 'codesnippetblock' && // extracted code snippets should end on the right extension
1148
+ // we are making a few assumptions during retrieve to pick the right one
1149
+ metadata?.content?.includes('%%[')
1150
+ ) {
1151
+ fileExt = 'amp';
1152
+ }
1153
+ if (metadata?.content?.length) {
816
1154
  codeArr.push({
817
1155
  subFolder: null,
818
1156
  fileName: metadata.customerKey,
819
- fileExt: 'html',
1157
+ fileExt: fileExt,
820
1158
  content: metadata.content,
821
1159
  });
822
1160
  delete metadata.content;
823
1161
  }
824
1162
  return { json: metadata, codeArr: codeArr, subFolder: null };
825
1163
  default:
1164
+ subType = this._getSubtype(metadata);
1165
+ if (!this.definition.binarySubtypes.includes(subType)) {
1166
+ Util.logger.debug(
1167
+ 'not processed metadata.assetType.name: ' + metadata.assetType.name
1168
+ );
1169
+ }
826
1170
  return { json: metadata, codeArr: codeArr, subFolder: null };
827
1171
  }
828
1172
  }
829
1173
  /**
830
- * @param {Object} metadataSlots metadata.views.html.slots or deeper slots.<>.blocks.<>.slots
831
- * @param {Object[]} codeArr to be extended array for extracted code
1174
+ * @param {string} prefix usually the customerkey
1175
+ * @param {object} metadataSlots metadata.views.html.slots or deeper slots.<>.blocks.<>.slots
1176
+ * @param {object[]} codeArr to be extended array for extracted code
832
1177
  * @returns {void}
833
1178
  */
834
- static _extractCode_slots(metadataSlots, codeArr) {
1179
+ static _extractCode_slots(prefix, metadataSlots, codeArr) {
835
1180
  for (const slot in metadataSlots) {
836
1181
  if (Object.prototype.hasOwnProperty.call(metadataSlots, slot)) {
837
1182
  const slotObj = metadataSlots[slot];
@@ -843,7 +1188,7 @@ class Asset extends MetadataType {
843
1188
  const code = slotObj.blocks[block].content;
844
1189
  codeArr.push({
845
1190
  subFolder: ['blocks'],
846
- fileName: `${slot}-${block}`,
1191
+ fileName: `${prefix}.[${slot}-${block}]`,
847
1192
  fileExt: 'html',
848
1193
  content: code,
849
1194
  });
@@ -851,7 +1196,11 @@ class Asset extends MetadataType {
851
1196
  }
852
1197
  if (slotObj.blocks[block].slots) {
853
1198
  // * recursion: each block can have slots of its own
854
- this._extractCode_slots(slotObj.blocks[block].slots, codeArr);
1199
+ this._extractCode_slots(
1200
+ `${prefix}.[${slot}-${block}]`,
1201
+ slotObj.blocks[block].slots,
1202
+ codeArr
1203
+ );
855
1204
  }
856
1205
  }
857
1206
  }
@@ -860,18 +1209,28 @@ class Asset extends MetadataType {
860
1209
  }
861
1210
  /**
862
1211
  * Returns file contents mapped to their fileName without '.json' ending
1212
+ *
863
1213
  * @param {string} dir directory that contains '.json' files to be read
864
- * @returns {Object} fileName => fileContent map
1214
+ * @param {void} [_] unused parameter
1215
+ * @param {string[]} selectedSubType asset, message, ...
1216
+ * @returns {TYPE.MetadataTypeMap} fileName => fileContent map
865
1217
  */
866
- static getJsonFromFS(dir) {
1218
+ static getJsonFromFS(dir, _, selectedSubType) {
867
1219
  const fileName2FileContent = {};
868
1220
  try {
869
1221
  for (const subtype of this.definition.subTypes) {
1222
+ if (
1223
+ selectedSubType &&
1224
+ !selectedSubType.includes('asset-' + subtype) &&
1225
+ !selectedSubType.includes('asset')
1226
+ ) {
1227
+ continue;
1228
+ }
870
1229
  const currentdir = File.normalizePath([dir, subtype]);
871
- if (File.existsSync(currentdir)) {
1230
+ if (File.pathExistsSync(currentdir)) {
872
1231
  const files = File.readdirSync(currentdir, { withFileTypes: true });
873
1232
 
874
- files.forEach((dirent) => {
1233
+ for (const dirent of files) {
875
1234
  try {
876
1235
  let thisDir = currentdir;
877
1236
  let fileName = dirent.name;
@@ -882,12 +1241,12 @@ class Asset extends MetadataType {
882
1241
  const subfolderFiles = File.readdirSync(
883
1242
  File.normalizePath([currentdir, dirent.name])
884
1243
  );
885
- subfolderFiles.forEach((subFileName) => {
1244
+ for (const subFileName of subfolderFiles) {
886
1245
  if (subFileName.endsWith('-meta.json')) {
887
1246
  fileName = subFileName;
888
1247
  thisDir = File.normalizePath([currentdir, dirent.name]);
889
1248
  }
890
- });
1249
+ }
891
1250
  }
892
1251
  if (fileName.endsWith('-meta.json')) {
893
1252
  const fileContent = File.readJSONFile(
@@ -907,7 +1266,7 @@ class Asset extends MetadataType {
907
1266
  // by catching this in the loop we gracefully handle the issue and move on to the next file
908
1267
  Util.metadataLogger('debug', this.definition.type, 'getJsonFromFS', ex);
909
1268
  }
910
- });
1269
+ }
911
1270
  }
912
1271
  }
913
1272
  } catch (ex) {
@@ -919,22 +1278,23 @@ class Asset extends MetadataType {
919
1278
  }
920
1279
  /**
921
1280
  * check template directory for complex types that open subfolders for their subtypes
1281
+ *
922
1282
  * @param {string} templateDir Directory where metadata templates are stored
923
1283
  * @param {string} templateName name of the metadata file
924
- * @returns {AssetSubType} subtype name
1284
+ * @returns {Promise.<TYPE.AssetSubType>} subtype name
925
1285
  */
926
- static findSubType(templateDir, templateName) {
1286
+ static async findSubType(templateDir, templateName) {
927
1287
  const typeDirArr = [this.definition.type];
928
1288
  let subType;
929
1289
  for (const st of this.definition.subTypes) {
930
1290
  const fileNameFull = templateName + '.' + this.definition.type + `-${st}-meta.json`;
931
1291
  if (
932
- File.existsSync(
1292
+ (await File.pathExists(
933
1293
  File.normalizePath([templateDir, ...typeDirArr, st, fileNameFull])
934
- ) ||
935
- File.existsSync(
1294
+ )) ||
1295
+ (await File.pathExists(
936
1296
  File.normalizePath([templateDir, ...typeDirArr, st, templateName, fileNameFull])
937
- )
1297
+ ))
938
1298
  ) {
939
1299
  subType = st;
940
1300
  break;
@@ -952,19 +1312,88 @@ class Asset extends MetadataType {
952
1312
  }
953
1313
  /**
954
1314
  * optional method used for some types to try a different folder structure
1315
+ *
955
1316
  * @param {string} templateDir Directory where metadata templates are stored
956
1317
  * @param {string[]} typeDirArr current subdir for this type
957
1318
  * @param {string} templateName name of the metadata template
958
1319
  * @param {string} fileName name of the metadata template file w/o extension
959
- * @returns {AssetItem} metadata
1320
+ * @returns {TYPE.AssetItem} metadata
960
1321
  */
961
1322
  static async readSecondaryFolder(templateDir, typeDirArr, templateName, fileName) {
962
1323
  // handles subtypes that create 1 folder per asset -> currently causes the below File.ReadFile to error out
963
1324
  typeDirArr.push(templateName);
964
- return await File.readFile([templateDir, ...typeDirArr], fileName, 'json');
1325
+ return await File.readFilteredFilename([templateDir, ...typeDirArr], fileName, 'json');
1326
+ }
1327
+ /**
1328
+ * should return only the json for all but asset, query and script that are saved as multiple files
1329
+ * additionally, the documentation for dataExtension and automation should be returned
1330
+ *
1331
+ * @param {string[]} keyArr customerkey of the metadata
1332
+ * @returns {string[]} list of all files that need to be committed in a flat array ['path/file1.ext', 'path/file2.ext']
1333
+ */
1334
+ static async getFilesToCommit(keyArr) {
1335
+ const basePath = File.normalizePath([
1336
+ this.properties.directories.retrieve,
1337
+ this.buObject.credential,
1338
+ this.buObject.businessUnit,
1339
+ ]);
1340
+
1341
+ const fileList = (
1342
+ await Promise.all(
1343
+ keyArr.map(async (key) => {
1344
+ let subType;
1345
+ let filePath;
1346
+ let fileName;
1347
+ for (const st of this.definition.subTypes) {
1348
+ fileName = `${key}.${this.definition.type}-${st}-meta.json`;
1349
+ if (
1350
+ await File.pathExists(
1351
+ File.normalizePath([basePath, this.definition.type, st, fileName])
1352
+ )
1353
+ ) {
1354
+ subType = st;
1355
+ filePath = [basePath, this.definition.type, st];
1356
+ break;
1357
+ } else if (
1358
+ await File.pathExists(
1359
+ File.normalizePath([
1360
+ basePath,
1361
+ this.definition.type,
1362
+ st,
1363
+ key,
1364
+ fileName,
1365
+ ])
1366
+ )
1367
+ ) {
1368
+ subType = st;
1369
+ filePath = [basePath, this.definition.type, st, key];
1370
+ break;
1371
+ }
1372
+ }
1373
+ if (await File.pathExists(File.normalizePath([...filePath, fileName]))) {
1374
+ const metadata = File.readJSONFile(filePath, fileName, true, false);
1375
+ const fileListNested = (
1376
+ await this._mergeCode(metadata, basePath, subType, metadata.customerKey)
1377
+ ).map((item) =>
1378
+ File.normalizePath([
1379
+ basePath,
1380
+ ...item.subFolder,
1381
+ `${item.fileName}.${item.fileExt}`,
1382
+ ])
1383
+ );
1384
+
1385
+ return [File.normalizePath([...filePath, fileName]), ...fileListNested];
1386
+ } else {
1387
+ return [];
1388
+ }
1389
+ })
1390
+ )
1391
+ ).flat();
1392
+ return fileList;
965
1393
  }
966
1394
  }
967
1395
 
968
1396
  // Assign definition to static attributes
969
1397
  Asset.definition = require('../MetadataTypeDefinitions').asset;
1398
+
970
1399
  module.exports = Asset;