mcdev 3.1.3 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/.eslintrc.json +67 -7
  2. package/.github/ISSUE_TEMPLATE/bug.yml +2 -1
  3. package/.github/PULL_REQUEST_TEMPLATE.md +5 -3
  4. package/.github/dependabot.yml +14 -0
  5. package/.github/workflows/code-analysis.yml +57 -0
  6. package/.husky/commit-msg +10 -0
  7. package/.husky/post-checkout +5 -0
  8. package/.husky/pre-commit +2 -1
  9. package/.prettierrc +8 -0
  10. package/.vscode/settings.json +1 -1
  11. package/LICENSE +2 -2
  12. package/README.md +134 -45
  13. package/boilerplate/config.json +5 -11
  14. package/boilerplate/files/.prettierrc +8 -0
  15. package/boilerplate/files/.vscode/extensions.json +0 -1
  16. package/boilerplate/files/.vscode/settings.json +30 -2
  17. package/boilerplate/files/README.md +2 -2
  18. package/boilerplate/forcedUpdates.json +10 -0
  19. package/boilerplate/npm-dependencies.json +5 -5
  20. package/docs/dist/documentation.md +2807 -1730
  21. package/jsconfig.json +1 -1
  22. package/lib/Builder.js +171 -74
  23. package/lib/Deployer.js +244 -96
  24. package/lib/MetadataTypeDefinitions.js +2 -0
  25. package/lib/MetadataTypeInfo.js +2 -0
  26. package/lib/Retriever.js +61 -84
  27. package/lib/cli.js +116 -11
  28. package/lib/index.js +241 -561
  29. package/lib/metadataTypes/AccountUser.js +117 -103
  30. package/lib/metadataTypes/Asset.js +705 -255
  31. package/lib/metadataTypes/AttributeGroup.js +23 -12
  32. package/lib/metadataTypes/Automation.js +489 -392
  33. package/lib/metadataTypes/Campaign.js +33 -93
  34. package/lib/metadataTypes/ContentArea.js +31 -11
  35. package/lib/metadataTypes/DataExtension.js +387 -372
  36. package/lib/metadataTypes/DataExtensionField.js +131 -54
  37. package/lib/metadataTypes/DataExtensionTemplate.js +22 -4
  38. package/lib/metadataTypes/DataExtract.js +61 -48
  39. package/lib/metadataTypes/DataExtractType.js +14 -8
  40. package/lib/metadataTypes/Discovery.js +21 -16
  41. package/lib/metadataTypes/Email.js +32 -12
  42. package/lib/metadataTypes/EmailSendDefinition.js +85 -80
  43. package/lib/metadataTypes/EventDefinition.js +61 -43
  44. package/lib/metadataTypes/FileTransfer.js +72 -52
  45. package/lib/metadataTypes/Filter.js +11 -4
  46. package/lib/metadataTypes/Folder.js +149 -117
  47. package/lib/metadataTypes/FtpLocation.js +14 -8
  48. package/lib/metadataTypes/ImportFile.js +61 -64
  49. package/lib/metadataTypes/Interaction.js +19 -4
  50. package/lib/metadataTypes/List.js +54 -13
  51. package/lib/metadataTypes/MetadataType.js +664 -454
  52. package/lib/metadataTypes/MobileCode.js +46 -0
  53. package/lib/metadataTypes/MobileKeyword.js +114 -0
  54. package/lib/metadataTypes/Query.js +206 -105
  55. package/lib/metadataTypes/Role.js +76 -61
  56. package/lib/metadataTypes/Script.js +147 -83
  57. package/lib/metadataTypes/SetDefinition.js +20 -8
  58. package/lib/metadataTypes/TriggeredSendDefinition.js +78 -58
  59. package/lib/metadataTypes/definitions/Asset.definition.js +21 -10
  60. package/lib/metadataTypes/definitions/AttributeGroup.definition.js +12 -0
  61. package/lib/metadataTypes/definitions/Automation.definition.js +10 -5
  62. package/lib/metadataTypes/definitions/Campaign.definition.js +44 -1
  63. package/lib/metadataTypes/definitions/DataExtension.definition.js +4 -0
  64. package/lib/metadataTypes/definitions/DataExtensionTemplate.definition.js +6 -0
  65. package/lib/metadataTypes/definitions/DataExtract.definition.js +18 -14
  66. package/lib/metadataTypes/definitions/Discovery.definition.js +12 -0
  67. package/lib/metadataTypes/definitions/EmailSendDefinition.definition.js +4 -0
  68. package/lib/metadataTypes/definitions/EventDefinition.definition.js +22 -0
  69. package/lib/metadataTypes/definitions/FileTransfer.definition.js +4 -0
  70. package/lib/metadataTypes/definitions/Filter.definition.js +4 -0
  71. package/lib/metadataTypes/definitions/Folder.definition.js +6 -0
  72. package/lib/metadataTypes/definitions/FtpLocation.definition.js +4 -0
  73. package/lib/metadataTypes/definitions/ImportFile.definition.js +10 -5
  74. package/lib/metadataTypes/definitions/Interaction.definition.js +4 -0
  75. package/lib/metadataTypes/definitions/MobileCode.definition.js +163 -0
  76. package/lib/metadataTypes/definitions/MobileKeyword.definition.js +253 -0
  77. package/lib/metadataTypes/definitions/Query.definition.js +4 -0
  78. package/lib/metadataTypes/definitions/Role.definition.js +5 -0
  79. package/lib/metadataTypes/definitions/Script.definition.js +4 -0
  80. package/lib/metadataTypes/definitions/SetDefinition.definition.js +28 -0
  81. package/lib/metadataTypes/definitions/TriggeredSendDefinition.definition.js +4 -0
  82. package/lib/retrieveChangelog.js +7 -6
  83. package/lib/util/auth.js +117 -0
  84. package/lib/util/businessUnit.js +55 -66
  85. package/lib/util/cache.js +194 -0
  86. package/lib/util/cli.js +90 -116
  87. package/lib/util/config.js +302 -0
  88. package/lib/util/devops.js +250 -50
  89. package/lib/util/file.js +141 -201
  90. package/lib/util/init.config.js +208 -75
  91. package/lib/util/init.git.js +45 -50
  92. package/lib/util/init.js +72 -59
  93. package/lib/util/init.npm.js +48 -39
  94. package/lib/util/util.js +280 -564
  95. package/package.json +45 -34
  96. package/test/dataExtension.test.js +152 -0
  97. package/test/mockRoot/.mcdev-auth.json +8 -0
  98. package/test/mockRoot/.mcdevrc.json +67 -0
  99. package/test/mockRoot/deploy/testInstance/testBU/dataExtension/childBU_dataextension_test.dataExtension-meta.json +39 -0
  100. package/test/mockRoot/deploy/testInstance/testBU/dataExtension/testDataExtension.dataExtension-meta.json +23 -0
  101. package/test/mockRoot/deploy/testInstance/testBU/query/testExistingQuery.query-meta.json +11 -0
  102. package/test/mockRoot/deploy/testInstance/testBU/query/testExistingQuery.query-meta.sql +4 -0
  103. package/test/mockRoot/deploy/testInstance/testBU/query/testQuery.query-meta.json +11 -0
  104. package/test/mockRoot/deploy/testInstance/testBU/query/testQuery.query-meta.sql +4 -0
  105. package/test/query.test.js +149 -0
  106. package/test/resourceFactory.js +142 -0
  107. package/test/resources/1111111/dataFolder/retrieve-response.xml +43 -0
  108. package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dat/patch-response.json +18 -0
  109. package/test/resources/9999999/automation/v1/queries/get-response.json +24 -0
  110. package/test/resources/9999999/automation/v1/queries/post-response.json +18 -0
  111. package/test/resources/9999999/dataExtension/build-expected.json +51 -0
  112. package/test/resources/9999999/dataExtension/create-expected.json +23 -0
  113. package/test/resources/9999999/dataExtension/create-response.xml +54 -0
  114. package/test/resources/9999999/dataExtension/retrieve-expected.json +51 -0
  115. package/test/resources/9999999/dataExtension/retrieve-response.xml +47 -0
  116. package/test/resources/9999999/dataExtension/template-expected.json +51 -0
  117. package/test/resources/9999999/dataExtension/update-expected.json +55 -0
  118. package/test/resources/9999999/dataExtension/update-response.xml +52 -0
  119. package/test/resources/9999999/dataExtensionField/retrieve-response.xml +93 -0
  120. package/test/resources/9999999/dataExtensionTemplate/retrieve-response.xml +303 -0
  121. package/test/resources/9999999/dataFolder/retrieve-response.xml +65 -0
  122. package/test/resources/9999999/query/build-expected.json +8 -0
  123. package/test/resources/9999999/query/get-expected.json +11 -0
  124. package/test/resources/9999999/query/patch-expected.json +11 -0
  125. package/test/resources/9999999/query/post-expected.json +11 -0
  126. package/test/resources/9999999/query/template-expected.json +8 -0
  127. package/test/resources/auth.json +32 -0
  128. package/test/resources/rest404-response.json +5 -0
  129. package/test/resources/retrieve-response.xml +21 -0
  130. package/test/utils.js +107 -0
  131. package/types/mcdev.d.js +301 -0
  132. package/CHANGELOG.md +0 -126
  133. package/PULL_REQUEST_TEMPLATE.md +0 -19
  134. 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,70 @@ 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
+ }
382
+ case 'verbose': {
383
+ obj = { verbose: true };
384
+ break;
385
+ }
386
+ case 'debug': {
387
+ obj = { debug: true };
388
+ break;
389
+ }
390
+ case 'error': {
391
+ obj = { silent: true };
392
+ }
393
+ }
394
+ Util.setLoggingLevel(obj);
395
+ }
396
+
397
+ if (failed.length) {
398
+ Util.logger.warn(
399
+ ` - Failed to download ${failed.length} extended file${
400
+ failed.length > 1 ? 's' : ''
401
+ }:`
402
+ );
403
+ for (const fail of failed) {
404
+ Util.logger.warn(
405
+ ` - "${fail.item.name}" (${fail.item.customerKey}) in ${fail.item.r__folder_Path}: ${fail.error.message}`
406
+ );
407
+ Util.logger.debug(`-- Error: ${fail.error.message}`);
408
+ Util.logger.debug(`-- AssetType: ${fail.item.assetType.name}`);
409
+ Util.logger.debug(`-- fileProperties: ${JSON.stringify(fail.item.fileProperties)}`);
410
+ }
411
+ Util.logger.info(
412
+ ' - You will still find a JSON file for each of these in the download directory.'
413
+ );
414
+ }
415
+ }
416
+
344
417
  /**
345
418
  * Some metadata types store their actual content as a separate file, e.g. images
346
419
  * 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)
420
+ *
421
+ * @param {TYPE.AssetItem} metadata a single asset
422
+ * @param {TYPE.AssetSubType} subType group of similar assets to put in a folder (ie. images)
349
423
  * @param {string} retrieveDir target directory for saving assets
350
- * @returns {Promise<void>} -
424
+ * @returns {Promise.<void>} -
351
425
  */
352
426
  static async _retrieveExtendedFile(metadata, subType, retrieveDir) {
353
- const file = await this.client.RestClient.get({
354
- uri: 'asset/v1/content/assets/' + metadata.id + '/file',
355
- });
427
+ const file = await this.client.rest.get('asset/v1/content/assets/' + metadata.id + '/file');
356
428
 
357
429
  // to handle uploaded files that bear the same name, SFMC engineers decided to add a number after the fileName
358
430
  // however, their solution was not following standards: fileName="header.png (4) " and then extension="png (4) "
@@ -362,7 +434,7 @@ class Asset extends MetadataType {
362
434
  [retrieveDir, this.definition.type, subType],
363
435
  metadata.customerKey,
364
436
  fileExt,
365
- file.body,
437
+ file,
366
438
  'base64'
367
439
  );
368
440
  }
@@ -370,10 +442,11 @@ class Asset extends MetadataType {
370
442
  * helper for this.preDeployTasks()
371
443
  * Some metadata types store their actual content as a separate file, e.g. images
372
444
  * 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)
445
+ *
446
+ * @param {TYPE.AssetItem} metadata a single asset
447
+ * @param {TYPE.AssetSubType} subType group of similar assets to put in a folder (ie. images)
375
448
  * @param {string} deployDir directory of deploy files
376
- * @returns {Promise<void>} -
449
+ * @returns {Promise.<void>} -
377
450
  */
378
451
  static async _readExtendedFileFromFS(metadata, subType, deployDir) {
379
452
  if (metadata.fileProperties && metadata.fileProperties.extension) {
@@ -381,7 +454,7 @@ class Asset extends MetadataType {
381
454
  // however, their solution was not following standards: fileName="header.png (4) " and then extension="png (4) "
382
455
  const fileExt = metadata.fileProperties.extension.split(' ')[0];
383
456
 
384
- metadata.file = await File.readFile(
457
+ metadata.file = await File.readFilteredFilename(
385
458
  [deployDir, this.definition.type, subType],
386
459
  metadata.customerKey,
387
460
  fileExt,
@@ -391,36 +464,25 @@ class Asset extends MetadataType {
391
464
  }
392
465
  /**
393
466
  * 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
467
+ *
468
+ * @param {TYPE.AssetItem} metadata a single asset
469
+ * @returns {TYPE.CodeExtractItem} metadata
398
470
  */
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
- }
471
+ static postRetrieveTasks(metadata) {
406
472
  return this.parseMetadata(metadata);
407
473
  }
408
474
 
409
475
  /**
410
476
  * prepares an asset definition for deployment
411
- * @param {AssetItem} metadata a single asset
477
+ *
478
+ * @param {TYPE.AssetItem} metadata a single asset
412
479
  * @param {string} deployDir directory of deploy files
413
- * @returns {Promise<AssetItem>} Promise
480
+ * @param {TYPE.BuObject} buObject buObject properties for auth
481
+ * @returns {Promise.<TYPE.AssetItem>} Promise
414
482
  */
415
- static async preDeployTasks(metadata, deployDir) {
483
+ static async preDeployTasks(metadata, deployDir, buObject) {
416
484
  // 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
- ) {
485
+ if (metadata?.data?.email?.attributes?.length > 0) {
424
486
  metadata.data.email.attributes = metadata.data.email.attributes.filter(
425
487
  (attr) => attr.value
426
488
  );
@@ -428,7 +490,7 @@ class Asset extends MetadataType {
428
490
 
429
491
  // folder
430
492
  metadata.category = {
431
- id: Util.getFromCache(this.cache, 'folder', metadata.r__folder_Path, 'Path', 'ID'),
493
+ id: cache.searchForField('folder', metadata.r__folder_Path, 'Path', 'ID'),
432
494
  };
433
495
  delete metadata.r__folder_Path;
434
496
 
@@ -436,7 +498,7 @@ class Asset extends MetadataType {
436
498
  metadata.assetType.id = this.definition.typeMapping[metadata.assetType.name];
437
499
 
438
500
  // define asset's subtype
439
- const subType = this.getSubtype(metadata);
501
+ const subType = this._getSubtype(metadata);
440
502
 
441
503
  // #1 get text extracts back into the JSON
442
504
  await this._mergeCode(metadata, deployDir, subType);
@@ -444,51 +506,190 @@ class Asset extends MetadataType {
444
506
  // #2 get file from local disk and insert as base64
445
507
  await this._readExtendedFileFromFS(metadata, subType, deployDir);
446
508
 
509
+ // only execute #3 if we are deploying / copying from one BU to another, not while using mcdev as a developer tool
510
+ if (
511
+ buObject.mid &&
512
+ metadata.memberId !== buObject.mid &&
513
+ !metadata[this.definition.keyField].startsWith(buObject.mid)
514
+ ) {
515
+ // #3 make sure customer key is unique by prefixing it with target MID (unless we are deploying to the same MID)
516
+ // check if this prefixed with the source MID
517
+ const suffix = '-' + buObject.mid;
518
+ // for customer key max is 36 chars
519
+ metadata[this.definition.keyField] =
520
+ metadata[this.definition.keyField].slice(0, Math.max(0, 36 - suffix.length)) +
521
+ suffix;
522
+ }
523
+ // #4 make sure the name is unique
524
+ const assetCache = cache.getCache()[this.definition.type];
525
+ const namesInFolder = Object.keys(assetCache)
526
+ .filter((key) => assetCache[key].category.id === metadata.category.id)
527
+ .map((key) => ({
528
+ type: this._getMainSubtype(assetCache[key].assetType.name),
529
+ key: key,
530
+ name: assetCache[key].name,
531
+ }));
532
+ // if the name is already in the folder for a different key, add a number to the end
533
+ metadata[this.definition.nameField] = this._findUniqueName(
534
+ metadata[this.definition.keyField],
535
+ metadata[this.definition.nameField],
536
+ this._getMainSubtype(metadata.assetType.name),
537
+ namesInFolder
538
+ );
447
539
  return metadata;
448
540
  }
541
+ /**
542
+ * find the subType matching the extendedSubType
543
+ *
544
+ * @param {string} extendedSubType webpage, htmlblock, etc
545
+ * @returns {string} subType: block, message, other, etc
546
+ */
547
+ static _getMainSubtype(extendedSubType) {
548
+ return Object.keys(this.definition.extendedSubTypes).find((subType) =>
549
+ this.definition.extendedSubTypes[subType].includes(extendedSubType)
550
+ );
551
+ }
552
+ /**
553
+ * helper to find a new unique name during asset creation
554
+ *
555
+ * @private
556
+ * @param {string} key key of the asset
557
+ * @param {string} name name of the asset
558
+ * @param {string} type assetType-name
559
+ * @param {string[]} namesInFolder names of the assets in the same folder
560
+ * @returns {string} new name
561
+ */
562
+ static _findUniqueName(key, name, type, namesInFolder) {
563
+ let newName = name;
564
+ let suffix;
565
+ let i = 1;
566
+ while (
567
+ namesInFolder.find(
568
+ (item) => item.name === newName && item.type === type && item.key !== key
569
+ )
570
+ ) {
571
+ suffix = ' (' + i + ')';
572
+ // for customer key max is 100 chars
573
+ newName = name.slice(0, Math.max(0, 100 - suffix.length)) + suffix;
574
+ i++;
575
+ }
576
+ return newName;
577
+ }
449
578
  /**
450
579
  * determines the subtype of the current asset
451
- * @param {AssetItem} metadata a single asset
452
- * @returns {AssetSubType} subtype
580
+ *
581
+ * @private
582
+ * @param {TYPE.AssetItem} metadata a single asset
583
+ * @returns {TYPE.AssetSubType} subtype
453
584
  */
454
- static getSubtype(metadata) {
585
+ static _getSubtype(metadata) {
455
586
  for (const sub in this.definition.extendedSubTypes) {
456
587
  if (this.definition.extendedSubTypes[sub].includes(metadata.assetType.name)) {
457
588
  return sub;
458
589
  }
459
590
  }
460
591
  }
461
-
462
592
  /**
463
593
  * helper for buildDefinition
464
594
  * handles extracted code if any are found for complex types
595
+ *
465
596
  * @param {string} templateDir Directory where metadata templates are stored
466
597
  * @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
598
+ * @param {TYPE.AssetItem} metadata main JSON file that was read from file system
599
+ * @param {TYPE.TemplateMap} templateVariables variables to be replaced in the metadata
469
600
  * @param {string} templateName name of the template to be built
470
- * @returns {Promise<void>} Promise
601
+ * @returns {Promise.<void>} -
471
602
  */
472
- static async buildDefinitionForExtracts(
603
+ static buildDefinitionForNested(
473
604
  templateDir,
474
605
  targetDir,
475
606
  metadata,
476
- variables,
607
+ templateVariables,
477
608
  templateName
478
609
  ) {
479
- // clone metadata to ensure the main file is not modified by what we do in here
610
+ return this._buildForNested(
611
+ templateDir,
612
+ targetDir,
613
+ metadata,
614
+ templateVariables,
615
+ templateName,
616
+ 'definition'
617
+ );
618
+ }
619
+ /**
620
+ * helper for buildTemplate
621
+ * handles extracted code if any are found for complex types
622
+ *
623
+ * @example assets of type codesnippetblock will result in 1 json and 1 amp/html file. both files need to be run through templating
624
+ * @param {string} templateDir Directory where metadata templates are stored
625
+ * @param {string|string[]} targetDir (List of) Directory where built definitions will be saved
626
+ * @param {TYPE.AssetItem} metadata main JSON file that was read from file system
627
+ * @param {TYPE.TemplateMap} templateVariables variables to be replaced in the metadata
628
+ * @param {string} templateName name of the template to be built
629
+ * @returns {Promise.<void>} -
630
+ */
631
+ static buildTemplateForNested(
632
+ templateDir,
633
+ targetDir,
634
+ metadata,
635
+ templateVariables,
636
+ templateName
637
+ ) {
638
+ return this._buildForNested(
639
+ templateDir,
640
+ targetDir,
641
+ metadata,
642
+ templateVariables,
643
+ templateName,
644
+ 'template'
645
+ );
646
+ }
647
+
648
+ /**
649
+ * helper for buildDefinition
650
+ * handles extracted code if any are found for complex types
651
+ *
652
+ * @param {string} templateDir Directory where metadata templates are stored
653
+ * @param {string} targetDir Directory where built definitions will be saved
654
+ * @param {TYPE.AssetItem} metadata main JSON file that was read from file system
655
+ * @param {TYPE.TemplateMap} templateVariables variables to be replaced in the metadata
656
+ * @param {string} templateName name of the template to be built
657
+ * @param {'definition'|'template'} mode defines what we use this helper for
658
+ * @returns {Promise.<void>} -
659
+ */
660
+ static async _buildForNested(
661
+ templateDir,
662
+ targetDir,
663
+ metadata,
664
+ templateVariables,
665
+ templateName,
666
+ mode
667
+ ) {
668
+ // * because asset's _mergeCode() is overwriting 'metadata', clone it to ensure the main file is not modified by what we do in here
480
669
  metadata = JSON.parse(JSON.stringify(metadata));
481
670
 
482
671
  // #1 text extracts
483
672
  // define asset's subtype
484
- const subType = this.getSubtype(metadata);
673
+ const subType = this._getSubtype(metadata);
485
674
  // get HTML from filesystem
486
675
  const fileList = await this._mergeCode(metadata, templateDir, subType, templateName);
487
676
  // replace template variables with their values
488
677
  for (const extractedFile of fileList) {
489
678
  try {
490
- extractedFile.content = Mustache.render(extractedFile.content, variables);
491
- } catch (ex) {
679
+ if (mode === 'definition') {
680
+ // replace template variables with their values
681
+ extractedFile.content = this.applyTemplateValues(
682
+ extractedFile.content,
683
+ templateVariables
684
+ );
685
+ } else if (mode === 'template') {
686
+ // replace template values with corresponding variable names
687
+ extractedFile.content = this.applyTemplateNames(
688
+ extractedFile.content,
689
+ templateVariables
690
+ );
691
+ }
692
+ } catch {
492
693
  throw new Error(
493
694
  `${this.definition.type}:: Error applying template variables on ${
494
695
  metadata[this.definition.keyField]
@@ -498,12 +699,12 @@ class Asset extends MetadataType {
498
699
  }
499
700
 
500
701
  // #2 binary extracts
501
- if (metadata.fileProperties && metadata.fileProperties.extension) {
702
+ if (metadata?.fileProperties?.extension) {
502
703
  // to handle uploaded files that bear the same name, SFMC engineers decided to add a number after the fileName
503
704
  // however, their solution was not following standards: fileName="header.png (4) " and then extension="png (4) "
504
705
  const fileExt = metadata.fileProperties.extension.split(' ')[0];
505
706
 
506
- const filecontent = await File.readFile(
707
+ const filecontent = await File.readFilteredFilename(
507
708
  [templateDir, this.definition.type, subType],
508
709
  metadata.customerKey,
509
710
  fileExt,
@@ -511,7 +712,7 @@ class Asset extends MetadataType {
511
712
  );
512
713
  fileList.push({
513
714
  subFolder: [this.definition.type, subType],
514
- fileName: metadata.customerKey,
715
+ fileName: templateName,
515
716
  fileExt: fileExt,
516
717
  content: filecontent,
517
718
  encoding: 'base64',
@@ -535,14 +736,14 @@ class Asset extends MetadataType {
535
736
 
536
737
  /**
537
738
  * parses retrieved Metadata before saving
538
- * @param {AssetItem} metadata a single asset definition
539
- * @returns {CodeExtractItem} parsed metadata definition
739
+ *
740
+ * @param {TYPE.AssetItem} metadata a single asset definition
741
+ * @returns {TYPE.CodeExtractItem} parsed metadata definition
540
742
  */
541
743
  static parseMetadata(metadata) {
542
744
  // folder
543
745
  try {
544
- metadata.r__folder_Path = Util.getFromCache(
545
- this.cache,
746
+ metadata.r__folder_Path = cache.searchForField(
546
747
  'folder',
547
748
  metadata.category.id,
548
749
  'ID',
@@ -553,7 +754,7 @@ class Asset extends MetadataType {
553
754
  // ! if we don't catch this error here we end up saving the actual asset but not its corresponding JSON
554
755
  Util.logger.debug(ex.message);
555
756
  Util.logger.warn(
556
- `Could not find folder with ID ${metadata.category.id} for '${metadata.name}' (${metadata.customerKey})`
757
+ ` - Could not find folder with ID ${metadata.category.id} for '${metadata.name}' (${metadata.customerKey})`
557
758
  );
558
759
  }
559
760
  // extract HTML for selected subtypes and convert payload for easier processing in MetadataType.saveResults()
@@ -562,42 +763,42 @@ class Asset extends MetadataType {
562
763
  }
563
764
  /**
564
765
  * helper for this.preDeployTasks() that loads extracted code content back into JSON
565
- * @param {AssetItem} metadata a single asset definition
766
+ *
767
+ * @param {TYPE.AssetItem} metadata a single asset definition
566
768
  * @param {string} deployDir directory of deploy files
567
- * @param {AssetSubType} subType asset-subtype name
769
+ * @param {TYPE.AssetSubType} subType asset-subtype name
568
770
  * @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)
771
+ * @param {boolean} [fileListOnly] does not read file contents nor update metadata if true
772
+ * @returns {Promise.<TYPE.CodeExtract[]>} fileList for templating (disregarded during deployment)
570
773
  */
571
- static async _mergeCode(metadata, deployDir, subType, templateName) {
774
+ static async _mergeCode(metadata, deployDir, subType, templateName, fileListOnly = false) {
572
775
  const subtypeExtension = `.${this.definition.type}-${subType}-meta`;
573
776
  const fileList = [];
574
777
  let subDirArr;
575
778
  let readDirArr;
576
-
577
779
  switch (metadata.assetType.name) {
578
- case 'webpage': // asset
579
780
  case 'templatebasedemail': // message
580
- case 'htmlemail': // message
781
+ case 'htmlemail': {
782
+ // message
581
783
  // this complex type always creates its own subdir per asset
582
784
  subDirArr = [this.definition.type, subType];
583
- readDirArr = [
584
- deployDir,
585
- ...subDirArr,
586
- templateName ? templateName : metadata.customerKey,
587
- ];
785
+ readDirArr = [deployDir, ...subDirArr, templateName || metadata.customerKey];
588
786
 
589
787
  // metadata.views.html.content (mandatory)
788
+ // 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
789
  if (
591
- File.existsSync(
790
+ (await File.pathExists(
592
791
  File.normalizePath([...readDirArr, `index${subtypeExtension}.html`])
593
- )
792
+ )) &&
793
+ metadata.views.html
594
794
  ) {
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
- );
795
+ if (!fileListOnly) {
796
+ metadata.views.html.content = await File.readFilteredFilename(
797
+ readDirArr,
798
+ 'index' + subtypeExtension,
799
+ 'html'
800
+ );
801
+ }
601
802
 
602
803
  if (templateName) {
603
804
  // to use this method in templating, store a copy of the info in fileList
@@ -611,39 +812,44 @@ class Asset extends MetadataType {
611
812
  }
612
813
 
613
814
  // metadata.views.html.slots.<>.blocks.<>.content (optional)
614
- if (metadata.views && metadata.views.html && metadata.views.html.slots) {
815
+ if (metadata?.views?.html?.slots) {
615
816
  await this._mergeCode_slots(
817
+ 'views.html.slots',
616
818
  metadata.views.html.slots,
617
819
  readDirArr,
618
820
  subtypeExtension,
619
821
  subDirArr,
620
822
  fileList,
621
823
  metadata.customerKey,
622
- templateName
824
+ templateName,
825
+ fileListOnly
623
826
  );
624
827
  }
625
828
  break;
626
- case 'textonlyemail': // message
829
+ }
830
+ case 'textonlyemail': {
831
+ // message
627
832
  // metadata.views.text.content
628
833
  subDirArr = [this.definition.type, subType];
629
834
  readDirArr = [deployDir, ...subDirArr];
630
835
  if (
631
- File.existsSync(
836
+ await File.pathExists(
632
837
  File.normalizePath([
633
838
  ...readDirArr,
634
839
  `${
635
- templateName ? templateName : metadata.customerKey
840
+ templateName || metadata.customerKey // TODO check why this could be templateName
636
841
  }${subtypeExtension}.html`,
637
842
  ])
638
843
  )
639
844
  ) {
640
845
  // 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
-
846
+ if (!fileListOnly) {
847
+ metadata.views.text.content = await File.readFilteredFilename(
848
+ readDirArr,
849
+ (templateName || metadata.customerKey) + subtypeExtension,
850
+ 'html'
851
+ );
852
+ }
647
853
  if (templateName) {
648
854
  // to use this method in templating, store a copy of the info in fileList
649
855
  fileList.push({
@@ -655,63 +861,162 @@ class Asset extends MetadataType {
655
861
  }
656
862
  }
657
863
  break;
658
- case 'freeformblock': // block
659
- case 'htmlblock': // block
660
- case 'textblock': // block
661
- case 'smartcaptureblock': // other
662
- case 'codesnippetblock': // other
663
- // metadata.content
864
+ }
865
+ case 'webpage': {
866
+ // asset
867
+ // this complex type always creates its own subdir per asset
664
868
  subDirArr = [this.definition.type, subType];
665
- readDirArr = [deployDir, ...subDirArr];
869
+ readDirArr = [deployDir, ...subDirArr, templateName || metadata.customerKey];
870
+
871
+ // metadata.views.html.slots.<>.blocks.<>.content (optional) (pre & post 20222)
872
+ if (metadata?.views?.html?.slots) {
873
+ await this._mergeCode_slots(
874
+ 'views.html.slots',
875
+ metadata.views.html.slots,
876
+ readDirArr,
877
+ subtypeExtension,
878
+ subDirArr,
879
+ fileList,
880
+ metadata.customerKey,
881
+ templateName,
882
+ fileListOnly
883
+ );
884
+ }
885
+
886
+ // +++ old webpages / pre-2022 +++
887
+ // metadata.views.html.content (mandatory)
666
888
  if (
667
- File.existsSync(
889
+ (await File.pathExists(
668
890
  File.normalizePath([
669
891
  ...readDirArr,
670
- `${
671
- templateName ? templateName : metadata.customerKey
672
- }${subtypeExtension}.html`,
892
+ `views.html.content${subtypeExtension}.html`,
673
893
  ])
894
+ )) && // the main content can be empty (=not set up yet) hence check if we did extract sth or else readFile() will print error msgs
895
+ metadata.views?.html
896
+ ) {
897
+ if (!fileListOnly) {
898
+ metadata.views.html.content = await File.readFilteredFilename(
899
+ readDirArr,
900
+ 'views.html.content' + subtypeExtension,
901
+ 'html'
902
+ );
903
+ }
904
+ if (templateName) {
905
+ // to use this method in templating, store a copy of the info in fileList
906
+ fileList.push({
907
+ subFolder: [...subDirArr, metadata.customerKey],
908
+ fileName: 'views.html.content' + subtypeExtension,
909
+ fileExt: 'html',
910
+ content: metadata.views.html.content,
911
+ });
912
+ }
913
+ }
914
+
915
+ // +++ new webpages / 2022+ +++
916
+ // metadata.content
917
+ if (
918
+ await File.pathExists(
919
+ File.normalizePath([...readDirArr, `content${subtypeExtension}.html`])
674
920
  )
675
921
  ) {
676
922
  // 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
- );
923
+ if (!fileListOnly) {
924
+ metadata.content = await File.readFilteredFilename(
925
+ readDirArr,
926
+ 'content' + subtypeExtension,
927
+ 'html'
928
+ );
929
+ }
682
930
  if (templateName) {
683
931
  // to use this method in templating, store a copy of the info in fileList
684
932
  fileList.push({
685
- subFolder: subDirArr,
686
- fileName: metadata.customerKey + subtypeExtension,
933
+ subFolder: [...subDirArr, metadata.customerKey],
934
+ fileName: 'content' + subtypeExtension,
687
935
  fileExt: 'html',
688
- content: metadata.content,
936
+ content: metadata.views.html.content,
689
937
  });
690
938
  }
691
939
  }
940
+
692
941
  break;
942
+ }
943
+ case 'buttonblock': // block - Button Block
944
+ case 'freeformblock': // block
945
+ case 'htmlblock': // block
946
+ case 'icemailformblock': // block - Interactive Content Email Form
947
+ case 'imageblock': // block - Image Block
948
+ case 'textblock': // block
949
+ case 'smartcaptureblock': // other
950
+ case 'codesnippetblock': {
951
+ // other
952
+ // metadata.content
953
+ subDirArr = [this.definition.type, subType];
954
+ readDirArr = [deployDir, ...subDirArr];
955
+ const fileExtArr = ['html']; // eslint-disable-line no-case-declarations
956
+ if (metadata.assetType.name === 'codesnippetblock') {
957
+ // extracted code snippets should end on the right extension
958
+ // we are making a few assumptions during retrieve to pick the right one
959
+ fileExtArr.push('amp', 'sjss');
960
+ }
961
+ for (const ext of fileExtArr) {
962
+ if (
963
+ await File.pathExists(
964
+ File.normalizePath([
965
+ ...readDirArr,
966
+ `${templateName || metadata.customerKey}${subtypeExtension}.${ext}`,
967
+ ])
968
+ )
969
+ ) {
970
+ // the main content can be empty (=not set up yet) hence check if we did extract sth or else readFile() will print error msgs
971
+ if (!fileListOnly) {
972
+ metadata.content = await File.readFilteredFilename(
973
+ readDirArr,
974
+ (templateName || metadata.customerKey) + subtypeExtension,
975
+ ext
976
+ );
977
+ }
978
+ if (templateName) {
979
+ // to use this method in templating, store a copy of the info in fileList
980
+ fileList.push({
981
+ subFolder: subDirArr,
982
+ fileName: (templateName || metadata.customerKey) + subtypeExtension,
983
+ fileExt: ext,
984
+ content: metadata.content,
985
+ });
986
+ }
987
+ // break loop when found
988
+ break;
989
+ }
990
+ }
991
+ break;
992
+ }
693
993
  }
694
994
  return fileList;
695
995
  }
696
996
  /**
697
997
  * 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
998
+ *
999
+ * @param {string} prefix usually the customerkey
1000
+ * @param {object} metadataSlots metadata.views.html.slots or deeper slots.<>.blocks.<>.slots
699
1001
  * @param {string[]} readDirArr directory of deploy files
700
1002
  * @param {string} subtypeExtension asset-subtype name ending on -meta
701
1003
  * @param {string[]} subDirArr directory of files w/o leading deploy dir
702
- * @param {Object[]} fileList directory of files w/o leading deploy dir
1004
+ * @param {object[]} fileList directory of files w/o leading deploy dir
703
1005
  * @param {string} customerKey external key of template (could have been changed if used during templating)
704
1006
  * @param {string} [templateName] name of the template used to built defintion (prior applying templating)
705
- * @returns {Promise<void>} -
1007
+ * @param {boolean} [fileListOnly] does not read file contents nor update metadata if true
1008
+ * @returns {Promise.<void>} -
706
1009
  */
707
1010
  static async _mergeCode_slots(
1011
+ prefix,
708
1012
  metadataSlots,
709
1013
  readDirArr,
710
1014
  subtypeExtension,
711
1015
  subDirArr,
712
1016
  fileList,
713
1017
  customerKey,
714
- templateName
1018
+ templateName,
1019
+ fileListOnly = false
715
1020
  ) {
716
1021
  for (const slot in metadataSlots) {
717
1022
  if (Object.prototype.hasOwnProperty.call(metadataSlots, slot)) {
@@ -720,9 +1025,9 @@ class Asset extends MetadataType {
720
1025
  if (slotObj.blocks) {
721
1026
  for (const block in slotObj.blocks) {
722
1027
  if (Object.prototype.hasOwnProperty.call(slotObj.blocks, block)) {
723
- const fileName = `${slot}-${block}${subtypeExtension}`;
1028
+ const fileName = `${prefix}.[${slot}-${block}]${subtypeExtension}`;
724
1029
  if (
725
- File.existsSync(
1030
+ await File.pathExists(
726
1031
  File.normalizePath([
727
1032
  ...readDirArr,
728
1033
  'blocks',
@@ -732,11 +1037,13 @@ class Asset extends MetadataType {
732
1037
  ) {
733
1038
  // 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
1039
  // 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
- );
1040
+ if (!fileListOnly) {
1041
+ slotObj.blocks[block].content = await File.readFilteredFilename(
1042
+ [...readDirArr, 'blocks'],
1043
+ fileName,
1044
+ 'html'
1045
+ );
1046
+ }
740
1047
  if (templateName) {
741
1048
  // to use this method in templating, store a copy of the info in fileList
742
1049
  fileList.push({
@@ -750,6 +1057,7 @@ class Asset extends MetadataType {
750
1057
  if (slotObj.blocks[block].slots) {
751
1058
  // * recursion: each block can have slots of its own
752
1059
  await this._mergeCode_slots(
1060
+ `${prefix}.[${slot}-${block}]`,
753
1061
  slotObj.blocks[block].slots,
754
1062
  readDirArr,
755
1063
  subtypeExtension,
@@ -768,17 +1076,19 @@ class Asset extends MetadataType {
768
1076
  /**
769
1077
  * helper for this.parseMetadata() that finds code content in JSON and extracts it
770
1078
  * to allow saving that separately and formatted
771
- * @param {AssetItem} metadata a single asset definition
772
- * @returns {CodeExtractItem} { json: metadata, codeArr: object[], subFolder: string[] }
1079
+ *
1080
+ * @param {TYPE.AssetItem} metadata a single asset definition
1081
+ * @returns {TYPE.CodeExtractItem} { json: metadata, codeArr: object[], subFolder: string[] }
773
1082
  */
774
1083
  static _extractCode(metadata) {
775
1084
  const codeArr = [];
1085
+ let subType;
776
1086
  switch (metadata.assetType.name) {
777
- case 'webpage': // asset
778
1087
  case 'templatebasedemail': // message
779
- case 'htmlemail': // message
1088
+ case 'htmlemail': {
1089
+ // message
780
1090
  // metadata.views.html.content (mandatory)
781
- if (metadata.views.html.content && metadata.views.html.content.length) {
1091
+ if (metadata.views?.html?.content?.length) {
782
1092
  codeArr.push({
783
1093
  subFolder: null,
784
1094
  fileName: 'index',
@@ -789,14 +1099,16 @@ class Asset extends MetadataType {
789
1099
  }
790
1100
 
791
1101
  // 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);
1102
+ if (metadata.views?.html?.slots) {
1103
+ this._extractCode_slots('views.html.slots', metadata.views.html.slots, codeArr);
794
1104
  }
795
1105
 
796
1106
  return { json: metadata, codeArr: codeArr, subFolder: [metadata.customerKey] };
797
- case 'textonlyemail': // message
1107
+ }
1108
+ case 'textonlyemail': {
1109
+ // message
798
1110
  // metadata.views.text.content
799
- if (metadata.views.text.content && metadata.views.text.content.length) {
1111
+ if (metadata.views?.text?.content?.length) {
800
1112
  codeArr.push({
801
1113
  subFolder: null,
802
1114
  fileName: metadata.customerKey,
@@ -806,32 +1118,86 @@ class Asset extends MetadataType {
806
1118
  delete metadata.views.text.content;
807
1119
  }
808
1120
  return { json: metadata, codeArr: codeArr, subFolder: null };
1121
+ }
1122
+ case 'webpage': {
1123
+ // asset
1124
+ // metadata.views.html.content (pre & post 20222)
1125
+ if (metadata.views?.html?.content?.length) {
1126
+ codeArr.push({
1127
+ subFolder: null,
1128
+ fileName: 'views.html.content',
1129
+ fileExt: 'html',
1130
+ content: metadata.views.html.content,
1131
+ });
1132
+ delete metadata.views.html.content;
1133
+ }
1134
+
1135
+ // +++ old webpages / pre-2022 +++
1136
+ // metadata.views.html.slots.<>.blocks.<>.content (optional)
1137
+ if (metadata.views?.html?.slots) {
1138
+ this._extractCode_slots('views.html.slots', metadata.views.html.slots, codeArr);
1139
+ }
1140
+
1141
+ // +++ new webpages / 2022+ +++
1142
+ // metadata.content
1143
+ if (metadata?.content?.length) {
1144
+ codeArr.push({
1145
+ subFolder: null,
1146
+ fileName: 'content',
1147
+ fileExt: 'html',
1148
+ content: metadata.content,
1149
+ });
1150
+ delete metadata.content;
1151
+ }
1152
+ return { json: metadata, codeArr: codeArr, subFolder: [metadata.customerKey] };
1153
+ }
1154
+ case 'buttonblock': // block - Button Block
809
1155
  case 'freeformblock': // block
810
1156
  case 'htmlblock': // block
1157
+ case 'icemailformblock': // block - Interactive Content Email Form
1158
+ case 'imageblock': // block - Image Block
811
1159
  case 'textblock': // block
812
1160
  case 'smartcaptureblock': // other
813
- case 'codesnippetblock': // other
1161
+ case 'codesnippetblock': {
1162
+ // other
814
1163
  // metadata.content
815
- if (metadata.content && metadata.content.length) {
1164
+ let fileExt = 'html'; // eslint-disable-line no-case-declarations
1165
+ if (
1166
+ metadata.assetType.name === 'codesnippetblock' && // extracted code snippets should end on the right extension
1167
+ // we are making a few assumptions during retrieve to pick the right one
1168
+ metadata?.content?.includes('%%[')
1169
+ ) {
1170
+ fileExt = 'amp';
1171
+ }
1172
+ if (metadata?.content?.length) {
816
1173
  codeArr.push({
817
1174
  subFolder: null,
818
1175
  fileName: metadata.customerKey,
819
- fileExt: 'html',
1176
+ fileExt: fileExt,
820
1177
  content: metadata.content,
821
1178
  });
822
1179
  delete metadata.content;
823
1180
  }
824
1181
  return { json: metadata, codeArr: codeArr, subFolder: null };
825
- default:
1182
+ }
1183
+ default: {
1184
+ subType = this._getSubtype(metadata);
1185
+ if (!this.definition.binarySubtypes.includes(subType)) {
1186
+ Util.logger.debug(
1187
+ 'not processed metadata.assetType.name: ' + metadata.assetType.name
1188
+ );
1189
+ }
826
1190
  return { json: metadata, codeArr: codeArr, subFolder: null };
1191
+ }
827
1192
  }
828
1193
  }
829
1194
  /**
830
- * @param {Object} metadataSlots metadata.views.html.slots or deeper slots.<>.blocks.<>.slots
831
- * @param {Object[]} codeArr to be extended array for extracted code
1195
+ * @param {string} prefix usually the customerkey
1196
+ * @param {object} metadataSlots metadata.views.html.slots or deeper slots.<>.blocks.<>.slots
1197
+ * @param {object[]} codeArr to be extended array for extracted code
832
1198
  * @returns {void}
833
1199
  */
834
- static _extractCode_slots(metadataSlots, codeArr) {
1200
+ static _extractCode_slots(prefix, metadataSlots, codeArr) {
835
1201
  for (const slot in metadataSlots) {
836
1202
  if (Object.prototype.hasOwnProperty.call(metadataSlots, slot)) {
837
1203
  const slotObj = metadataSlots[slot];
@@ -843,7 +1209,7 @@ class Asset extends MetadataType {
843
1209
  const code = slotObj.blocks[block].content;
844
1210
  codeArr.push({
845
1211
  subFolder: ['blocks'],
846
- fileName: `${slot}-${block}`,
1212
+ fileName: `${prefix}.[${slot}-${block}]`,
847
1213
  fileExt: 'html',
848
1214
  content: code,
849
1215
  });
@@ -851,7 +1217,11 @@ class Asset extends MetadataType {
851
1217
  }
852
1218
  if (slotObj.blocks[block].slots) {
853
1219
  // * recursion: each block can have slots of its own
854
- this._extractCode_slots(slotObj.blocks[block].slots, codeArr);
1220
+ this._extractCode_slots(
1221
+ `${prefix}.[${slot}-${block}]`,
1222
+ slotObj.blocks[block].slots,
1223
+ codeArr
1224
+ );
855
1225
  }
856
1226
  }
857
1227
  }
@@ -860,18 +1230,28 @@ class Asset extends MetadataType {
860
1230
  }
861
1231
  /**
862
1232
  * Returns file contents mapped to their fileName without '.json' ending
1233
+ *
863
1234
  * @param {string} dir directory that contains '.json' files to be read
864
- * @returns {Object} fileName => fileContent map
1235
+ * @param {void} [_] unused parameter
1236
+ * @param {string[]} selectedSubType asset, message, ...
1237
+ * @returns {TYPE.MetadataTypeMap} fileName => fileContent map
865
1238
  */
866
- static getJsonFromFS(dir) {
1239
+ static getJsonFromFS(dir, _, selectedSubType) {
867
1240
  const fileName2FileContent = {};
868
1241
  try {
869
1242
  for (const subtype of this.definition.subTypes) {
1243
+ if (
1244
+ selectedSubType &&
1245
+ !selectedSubType.includes('asset-' + subtype) &&
1246
+ !selectedSubType.includes('asset')
1247
+ ) {
1248
+ continue;
1249
+ }
870
1250
  const currentdir = File.normalizePath([dir, subtype]);
871
- if (File.existsSync(currentdir)) {
1251
+ if (File.pathExistsSync(currentdir)) {
872
1252
  const files = File.readdirSync(currentdir, { withFileTypes: true });
873
1253
 
874
- files.forEach((dirent) => {
1254
+ for (const dirent of files) {
875
1255
  try {
876
1256
  let thisDir = currentdir;
877
1257
  let fileName = dirent.name;
@@ -882,12 +1262,12 @@ class Asset extends MetadataType {
882
1262
  const subfolderFiles = File.readdirSync(
883
1263
  File.normalizePath([currentdir, dirent.name])
884
1264
  );
885
- subfolderFiles.forEach((subFileName) => {
1265
+ for (const subFileName of subfolderFiles) {
886
1266
  if (subFileName.endsWith('-meta.json')) {
887
1267
  fileName = subFileName;
888
1268
  thisDir = File.normalizePath([currentdir, dirent.name]);
889
1269
  }
890
- });
1270
+ }
891
1271
  }
892
1272
  if (fileName.endsWith('-meta.json')) {
893
1273
  const fileContent = File.readJSONFile(
@@ -907,7 +1287,7 @@ class Asset extends MetadataType {
907
1287
  // by catching this in the loop we gracefully handle the issue and move on to the next file
908
1288
  Util.metadataLogger('debug', this.definition.type, 'getJsonFromFS', ex);
909
1289
  }
910
- });
1290
+ }
911
1291
  }
912
1292
  }
913
1293
  } catch (ex) {
@@ -919,22 +1299,23 @@ class Asset extends MetadataType {
919
1299
  }
920
1300
  /**
921
1301
  * check template directory for complex types that open subfolders for their subtypes
1302
+ *
922
1303
  * @param {string} templateDir Directory where metadata templates are stored
923
1304
  * @param {string} templateName name of the metadata file
924
- * @returns {AssetSubType} subtype name
1305
+ * @returns {Promise.<TYPE.AssetSubType>} subtype name
925
1306
  */
926
- static findSubType(templateDir, templateName) {
1307
+ static async findSubType(templateDir, templateName) {
927
1308
  const typeDirArr = [this.definition.type];
928
1309
  let subType;
929
1310
  for (const st of this.definition.subTypes) {
930
1311
  const fileNameFull = templateName + '.' + this.definition.type + `-${st}-meta.json`;
931
1312
  if (
932
- File.existsSync(
1313
+ (await File.pathExists(
933
1314
  File.normalizePath([templateDir, ...typeDirArr, st, fileNameFull])
934
- ) ||
935
- File.existsSync(
1315
+ )) ||
1316
+ (await File.pathExists(
936
1317
  File.normalizePath([templateDir, ...typeDirArr, st, templateName, fileNameFull])
937
- )
1318
+ ))
938
1319
  ) {
939
1320
  subType = st;
940
1321
  break;
@@ -952,19 +1333,88 @@ class Asset extends MetadataType {
952
1333
  }
953
1334
  /**
954
1335
  * optional method used for some types to try a different folder structure
1336
+ *
955
1337
  * @param {string} templateDir Directory where metadata templates are stored
956
1338
  * @param {string[]} typeDirArr current subdir for this type
957
1339
  * @param {string} templateName name of the metadata template
958
1340
  * @param {string} fileName name of the metadata template file w/o extension
959
- * @returns {AssetItem} metadata
1341
+ * @returns {TYPE.AssetItem} metadata
960
1342
  */
961
1343
  static async readSecondaryFolder(templateDir, typeDirArr, templateName, fileName) {
962
1344
  // handles subtypes that create 1 folder per asset -> currently causes the below File.ReadFile to error out
963
1345
  typeDirArr.push(templateName);
964
- return await File.readFile([templateDir, ...typeDirArr], fileName, 'json');
1346
+ return await File.readFilteredFilename([templateDir, ...typeDirArr], fileName, 'json');
1347
+ }
1348
+ /**
1349
+ * should return only the json for all but asset, query and script that are saved as multiple files
1350
+ * additionally, the documentation for dataExtension and automation should be returned
1351
+ *
1352
+ * @param {string[]} keyArr customerkey of the metadata
1353
+ * @returns {string[]} list of all files that need to be committed in a flat array ['path/file1.ext', 'path/file2.ext']
1354
+ */
1355
+ static async getFilesToCommit(keyArr) {
1356
+ const basePath = File.normalizePath([
1357
+ this.properties.directories.retrieve,
1358
+ this.buObject.credential,
1359
+ this.buObject.businessUnit,
1360
+ ]);
1361
+
1362
+ const fileList = (
1363
+ await Promise.all(
1364
+ keyArr.map(async (key) => {
1365
+ let subType;
1366
+ let filePath;
1367
+ let fileName;
1368
+ for (const st of this.definition.subTypes) {
1369
+ fileName = `${key}.${this.definition.type}-${st}-meta.json`;
1370
+ if (
1371
+ await File.pathExists(
1372
+ File.normalizePath([basePath, this.definition.type, st, fileName])
1373
+ )
1374
+ ) {
1375
+ subType = st;
1376
+ filePath = [basePath, this.definition.type, st];
1377
+ break;
1378
+ } else if (
1379
+ await File.pathExists(
1380
+ File.normalizePath([
1381
+ basePath,
1382
+ this.definition.type,
1383
+ st,
1384
+ key,
1385
+ fileName,
1386
+ ])
1387
+ )
1388
+ ) {
1389
+ subType = st;
1390
+ filePath = [basePath, this.definition.type, st, key];
1391
+ break;
1392
+ }
1393
+ }
1394
+ if (await File.pathExists(File.normalizePath([...filePath, fileName]))) {
1395
+ const metadata = File.readJSONFile(filePath, fileName, true, false);
1396
+ const fileListNested = (
1397
+ await this._mergeCode(metadata, basePath, subType, metadata.customerKey)
1398
+ ).map((item) =>
1399
+ File.normalizePath([
1400
+ basePath,
1401
+ ...item.subFolder,
1402
+ `${item.fileName}.${item.fileExt}`,
1403
+ ])
1404
+ );
1405
+
1406
+ return [File.normalizePath([...filePath, fileName]), ...fileListNested];
1407
+ } else {
1408
+ return [];
1409
+ }
1410
+ })
1411
+ )
1412
+ ).flat();
1413
+ return fileList;
965
1414
  }
966
1415
  }
967
1416
 
968
1417
  // Assign definition to static attributes
969
1418
  Asset.definition = require('../MetadataTypeDefinitions').asset;
1419
+
970
1420
  module.exports = Asset;