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
package/lib/util/util.js CHANGED
@@ -1,32 +1,12 @@
1
1
  'use strict';
2
2
 
3
- /**
4
- * @ignore @typedef {import('sfmc-fuelsdk-node')} ET_Client
5
- *
6
- * @typedef {Object} BuObject
7
- * @property {String} clientId installed package client id
8
- * @property {String} clientSecret installed package client secret
9
- * @property {String} tenant subdomain part of Authentication Base Uri
10
- * @property {String} [eid] Enterprise ID = MID of the parent BU
11
- * @property {String} [mid] MID of the BU to work with
12
- * @property {String} [businessUnit] name of the BU to interact with
13
- * @property {String} [credential] name of the credential to interact with
14
- */
15
- /**
16
- * @typedef {Object.<string,string>} TemplateMap
17
- */
18
-
19
- /** @type ET_Client */
20
- const ET_Client = require('sfmc-fuelsdk-node');
21
- const fs = require('fs-extra'); // ! do not switch to util/file.js to avoid circular dependency
3
+ const TYPE = require('../../types/mcdev.d');
22
4
  const MetadataDefinitions = require('./../MetadataTypeDefinitions');
23
5
  const packageJsonMcdev = require('../../package.json');
24
- const path = require('path');
6
+ const process = require('node:process');
25
7
  const toposort = require('toposort');
26
8
  const winston = require('winston');
27
- const inquirer = require('inquirer');
28
- const child_process = require('child_process');
29
- const semver = require('semver');
9
+ const child_process = require('node:child_process');
30
10
 
31
11
  /**
32
12
  * Util that contains logger and simple util methods
@@ -37,119 +17,165 @@ const Util = {
37
17
  configFileName: '.mcdevrc.json',
38
18
  parentBuName: '_ParentBU_',
39
19
  standardizedSplitChar: '/',
40
- expectedAuthScope: [
41
- 'accounts_read',
42
- 'accounts_write',
43
- 'approvals_read',
44
- 'approvals_write',
45
- 'audiences_read',
46
- 'audiences_write',
47
- 'automations_execute',
48
- 'automations_read',
49
- 'automations_write',
50
- 'calendar_read',
51
- 'calendar_write',
52
- 'campaign_read',
53
- 'campaign_write',
54
- 'data_extensions_read',
55
- 'data_extensions_write',
56
- 'documents_and_images_read',
57
- 'documents_and_images_write',
58
- 'email_read',
59
- 'email_send',
60
- 'email_write',
61
- 'event_notification_callback_create',
62
- 'event_notification_callback_delete',
63
- 'event_notification_callback_read',
64
- 'event_notification_callback_update',
65
- 'event_notification_subscription_create',
66
- 'event_notification_subscription_delete',
67
- 'event_notification_subscription_read',
68
- 'event_notification_subscription_update',
69
- 'file_locations_read',
70
- 'file_locations_write',
71
- 'journeys_execute',
72
- 'journeys_read',
73
- 'journeys_write',
74
- 'key_manage_revoke',
75
- 'key_manage_rotate',
76
- 'key_manage_view',
77
- 'list_and_subscribers_read',
78
- 'list_and_subscribers_write',
79
- 'marketing_cloud_connect_read',
80
- 'marketing_cloud_connect_send',
81
- 'marketing_cloud_connect_write',
82
- 'offline',
83
- 'ott_channels_read',
84
- 'ott_channels_write',
85
- 'ott_chat_messaging_read',
86
- 'ott_chat_messaging_send',
87
- 'push_read',
88
- 'push_send',
89
- 'push_write',
90
- 'saved_content_read',
91
- 'saved_content_write',
92
- 'sms_read',
93
- 'sms_send',
94
- 'sms_write',
95
- 'social_post',
96
- 'social_publish',
97
- 'social_read',
98
- 'social_write',
99
- 'tags_read',
100
- 'tags_write',
101
- 'tracking_events_read',
102
- 'tracking_events_write',
103
- 'users_read',
104
- 'users_write',
105
- 'web_publish',
106
- 'web_read',
107
- 'web_write',
108
- 'webhooks_read',
109
- 'webhooks_write',
110
- 'workflows_read',
111
- 'workflows_write',
112
- ],
20
+ /** @type {TYPE.skipInteraction} */
21
+ skipInteraction: false,
22
+ packageJsonMcdev: packageJsonMcdev,
23
+
24
+ /**
25
+ * helper that allows filtering an object by its keys
26
+ *
27
+ * @param {Object.<string,*>} originalObj object that you want to filter
28
+ * @param {string[]} [whitelistArr] positive filter. if not provided, returns originalObj without filter
29
+ * @returns {Object.<string,*>} filtered object that only contains keys you provided
30
+ */
31
+ filterObjByKeys(originalObj, whitelistArr) {
32
+ if (!whitelistArr || !Array.isArray(whitelistArr)) {
33
+ return originalObj;
34
+ }
35
+ return Object.keys(originalObj)
36
+ .filter((key) => whitelistArr.includes(key))
37
+ .reduce((obj, key) => {
38
+ obj[key] = originalObj[key];
39
+ return obj;
40
+ }, {});
41
+ },
42
+ /**
43
+ * extended Array.includes method that allows check if an array-element starts with a certain string
44
+ *
45
+ * @param {string[]} arr your array of strigns
46
+ * @param {string} search the string you are looking for
47
+ * @returns {boolean} found / not found
48
+ */
49
+ includesStartsWith(arr, search) {
50
+ return this.includesStartsWithIndex(arr, search) >= 0;
51
+ },
52
+ /**
53
+ * extended Array.includes method that allows check if an array-element starts with a certain string
54
+ *
55
+ * @param {string[]} arr your array of strigns
56
+ * @param {string} search the string you are looking for
57
+ * @returns {number} array index 0..n or -1 of not found
58
+ */
59
+ includesStartsWithIndex(arr, search) {
60
+ return Array.isArray(arr) ? arr.findIndex((el) => el.startsWith(search)) : -1;
61
+ },
62
+ /**
63
+ * check if a market name exists in current mcdev config
64
+ *
65
+ * @param {string} market market localizations
66
+ * @param {TYPE.Mcdevrc} properties local mcdev config
67
+ * @returns {boolean} found market or not
68
+ */
69
+ checkMarket(market, properties) {
70
+ if (properties.markets[market]) {
71
+ return true;
72
+ } else {
73
+ Util.logger.error(`Could not find the market '${market}' in your configuration file.`);
74
+ const marketArr = Object.values(properties.markets);
75
+
76
+ if (marketArr.length) {
77
+ Util.logger.info('Available markets are: ' + marketArr.join(', '));
78
+ }
79
+ return false;
80
+ }
81
+ },
82
+ /**
83
+ * ensure provided MarketList exists and it's content including markets and BUs checks out
84
+ *
85
+ * @param {string} mlName name of marketList
86
+ * @param {TYPE.Mcdevrc} properties General configuration to be used in retrieve
87
+ * @returns {void} throws errors if problems were found
88
+ */
89
+ verifyMarketList(mlName, properties) {
90
+ if (!properties.marketList[mlName]) {
91
+ // ML does not exist
92
+ throw new Error(`Market List ${mlName} is not defined`);
93
+ } else {
94
+ // ML exists, check if it is properly set up
95
+
96
+ // check if BUs in marketList are valid
97
+ let buCounter = 0;
98
+ for (const businessUnit in properties.marketList[mlName]) {
99
+ if (businessUnit !== 'description') {
100
+ buCounter++;
101
+ const [cred, bu] = businessUnit ? businessUnit.split('/') : [null, null];
102
+ if (
103
+ !properties.credentials[cred] ||
104
+ !properties.credentials[cred].businessUnits[bu]
105
+ ) {
106
+ throw new Error(`'${businessUnit}' in Market ${mlName} is not defined.`);
107
+ }
108
+ // check if markets are valid
109
+ let marketArr = properties.marketList[mlName][businessUnit];
110
+ if ('string' === typeof marketArr) {
111
+ marketArr = [marketArr];
112
+ }
113
+ for (const market of marketArr) {
114
+ if (!properties.markets[market]) {
115
+ throw new Error(`Market '${market}' is not defined.`);
116
+ } else {
117
+ // * markets can be empty or include variables. Nothing we can test here
118
+ }
119
+ }
120
+ }
121
+ }
122
+ if (!buCounter) {
123
+ throw new Error(`No BUs defined in marketList ${mlName}`);
124
+ }
125
+ }
126
+ },
127
+ /**
128
+ * used to ensure the program tells surrounding software that an unrecoverable error occured
129
+ *
130
+ * @returns {void}
131
+ */
132
+ signalFatalError() {
133
+ process.exitCode = 1;
134
+ },
113
135
  /**
114
136
  * SFMC accepts multiple true values for Boolean attributes for which we are checking here
137
+ *
115
138
  * @param {*} attrValue value
116
139
  * @returns {boolean} attribute value == true ? true : false
117
140
  */
118
141
  isTrue(attrValue) {
119
- return ['true', 'TRUE', 'True', '1', 1, 'Y'].includes(attrValue);
142
+ return ['true', 'TRUE', 'True', '1', 1, 'Y', true].includes(attrValue);
120
143
  },
121
144
  /**
122
145
  * SFMC accepts multiple false values for Boolean attributes for which we are checking here
146
+ *
123
147
  * @param {*} attrValue value
124
148
  * @returns {boolean} attribute value == false ? true : false
125
149
  */
126
150
  isFalse(attrValue) {
127
- return ['false', 'FALSE', 'False', '0', 0, 'N'].includes(attrValue);
151
+ return ['false', 'FALSE', 'False', '0', 0, 'N', false].includes(attrValue);
128
152
  },
129
-
130
153
  /**
131
- * defines how the properties.json should look like
132
- * used for creating a template and for checking if variables are set
154
+ * helper for retrieve, retrieveAsTemplate and deploy
133
155
  *
134
- * @returns {object} default properties
156
+ * @param {TYPE.SupportedMetadataTypes} selectedType type or type-subtype
157
+ * @returns {boolean} type ok or not
135
158
  */
136
- getDefaultProperties: function () {
137
- const configFileName = path.resolve(__dirname, this.boilerplateDirectory, 'config.json');
138
- if (!fs.existsSync(configFileName)) {
139
- this.logger.debug(`Default config file file not found in ${configFileName}`);
140
- return false;
159
+ _isValidType(selectedType) {
160
+ const [type, subType] = selectedType ? selectedType.split('-') : [];
161
+ if (type && !MetadataDefinitions[type]) {
162
+ Util.logger.error(`:: '${type}' is not a valid metadata type`);
163
+ return;
164
+ } else if (
165
+ type &&
166
+ subType &&
167
+ (!MetadataDefinitions[type] || !MetadataDefinitions[type].subTypes.includes(subType))
168
+ ) {
169
+ Util.logger.error(`:: '${selectedType}' is not a valid metadata type`);
170
+ return;
141
171
  }
142
- const defaultProperties = fs.readJsonSync(configFileName);
143
- // set default name for parent BU
144
- defaultProperties.credentials.default.businessUnits[this.parentBuName] = '000000000';
145
- // set default retrieve values
146
- defaultProperties.metaDataTypes.retrieve = this.getRetrieveTypeChoices();
147
-
148
- return defaultProperties;
172
+ return true;
149
173
  },
174
+
150
175
  /**
151
176
  * helper for getDefaultProperties()
152
- * @returns {string[]} type choices
177
+ *
178
+ * @returns {TYPE.SupportedMetadataTypes[]} type choices
153
179
  */
154
180
  getRetrieveTypeChoices() {
155
181
  const typeChoices = [];
@@ -177,234 +203,23 @@ const Util = {
177
203
 
178
204
  return typeChoices;
179
205
  },
180
- /**
181
- * check if the config file is correctly formatted and has values
182
- *
183
- * @param {object} properties javascript object in .mcdevrc.json
184
- * @param {boolean} [silent] set to true for internal use w/o cli output
185
- * @returns {boolean|String[]} file structure ok OR list of fields to be fixed
186
- */
187
- checkProperties: function (properties, silent) {
188
- if (!fs.existsSync(Util.configFileName) || !properties) {
189
- Util.logger.error(`\nCould not find ${Util.configFileName} in ${process.cwd()}.`);
190
- Util.logger.error(`Run 'mcdev init' to initialize your project.\n`);
191
- return false;
192
- }
193
- if (!fs.existsSync(Util.authFileName) || !properties) {
194
- Util.logger.error(`\nCould not find ${Util.authFileName} in ${process.cwd()}.`);
195
- Util.logger.error(`Run 'mcdev init' to initialize your project.\n`);
196
- return false;
197
- }
198
-
199
- // check if user is running older mcdev version than whats saved to the config
200
- if (properties.version && semver.gt(properties.version, packageJsonMcdev.version)) {
201
- // dont run this for Catalyst to MC DevTools migration
202
- Util.logger.error(
203
- `Your Accenture SFMC DevTools version ${packageJsonMcdev.version} is lower than your project's config version ${properties.version}`
204
- );
205
- const questions = [
206
- {
207
- type: 'confirm',
208
- name: 'runUpgradeNow',
209
- message: `Do you want to run 'npm update -g mcdev' now? This may take a few minutes.`,
210
- default: true,
211
- },
212
- ];
213
- inquirer.prompt(questions).then((responses) => {
214
- if (responses.runUpgradeNow) {
215
- // use _execSync here to avoid a circular dependency
216
- this.execSync('npm', ['update', '-g', 'mcdev']);
217
- }
218
- });
219
- return false;
220
- }
221
-
222
- // check config properties
223
- const defaultProps = this.getDefaultProperties();
224
- const errorMsgs = [];
225
- const solutionSet = new Set();
226
- const missingFields = [];
227
- for (const key in defaultProps) {
228
- if (Object.prototype.hasOwnProperty.call(defaultProps, key)) {
229
- if (!Object.prototype.hasOwnProperty.call(properties, key)) {
230
- errorMsgs.push(`${key}{} missing`);
231
- solutionSet.add(
232
- `Run 'mcdev upgrade' to fix missing or changed configuration options`
233
- );
234
- missingFields.push(key);
235
- } else {
236
- if (!silent && key === 'credentials') {
237
- if (!Object.keys(properties.credentials)) {
238
- errorMsgs.push(`no Credential defined`);
239
- } else {
240
- for (const cred in properties.credentials) {
241
- if (cred.includes('/') || cred.includes('\\')) {
242
- errorMsgs.push(
243
- `Credential names may not includes slashes: ${cred}`
244
- );
245
- solutionSet.add('Apply manual fix in your config.');
246
- }
247
- if (
248
- !properties.credentials[cred].clientId ||
249
- properties.credentials[cred].clientId === '--- update me ---'
250
- ) {
251
- errorMsgs.push(`invalid ClientId on ${cred}`);
252
- solutionSet.add(`Run 'mcdev init ${cred}'`);
253
- }
254
- if (
255
- !properties.credentials[cred].clientSecret ||
256
- properties.credentials[cred].clientSecret ===
257
- '--- update me ---'
258
- ) {
259
- errorMsgs.push(`invalid ClientSecret on ${cred}`);
260
- solutionSet.add(`Run 'mcdev init ${cred}'`);
261
- }
262
- if (
263
- !properties.credentials[cred].tenant ||
264
- properties.credentials[cred].tenant === '--- update me ---'
265
- ) {
266
- errorMsgs.push(`invalid tenant on ${cred}`);
267
- solutionSet.add(`Run 'mcdev init ${cred}'`);
268
- }
269
- if (
270
- !properties.credentials[cred].eid ||
271
- properties.credentials[cred].eid === '000000000'
272
- ) {
273
- errorMsgs.push(`invalid eid on ${cred}`);
274
- solutionSet.add(`Run 'mcdev init ${cred}'`);
275
- }
276
- let i = 0;
277
- for (const buName in properties.credentials[cred].businessUnits) {
278
- if (buName.includes('/') || buName.includes('\\')) {
279
- errorMsgs.push(
280
- `Business Unit names may not includes slashes: ${cred}: ${buName}`
281
- );
282
- solutionSet.add(`Run 'mcdev reloadBUs ${cred}'`);
283
- }
284
- if (
285
- Object.prototype.hasOwnProperty.call(
286
- properties.credentials[cred].businessUnits,
287
- buName
288
- ) &&
289
- properties.credentials[cred].businessUnits[buName] !==
290
- '000000000'
291
- ) {
292
- i++;
293
- }
294
- }
295
- if (!i) {
296
- errorMsgs.push(`no Business Units defined`);
297
- solutionSet.add(`Run 'mcdev reloadBUs ${cred}'`);
298
- }
299
- }
300
- }
301
- } else if (['directories', 'metaDataTypes', 'options'].includes(key)) {
302
- for (const subkey in defaultProps[key]) {
303
- if (
304
- Object.prototype.hasOwnProperty.call(defaultProps[key], subkey) &&
305
- !Object.prototype.hasOwnProperty.call(properties[key], subkey)
306
- ) {
307
- errorMsgs.push(
308
- `${key}.${subkey} missing. Default value (${
309
- Array.isArray(defaultProps[key][subkey])
310
- ? 'Array'
311
- : typeof defaultProps[key][subkey]
312
- }): ${defaultProps[key][subkey]}`
313
- );
314
- solutionSet.add(
315
- `Run 'mcdev upgrade' to fix missing or changed configuration options`
316
- );
317
- missingFields.push(`${key}.${subkey}`);
318
- } else if (subkey === 'deployment') {
319
- for (const dkey in defaultProps[key][subkey]) {
320
- if (
321
- Object.prototype.hasOwnProperty.call(
322
- defaultProps[key][subkey],
323
- dkey
324
- ) &&
325
- !Object.prototype.hasOwnProperty.call(
326
- properties[key][subkey],
327
- dkey
328
- )
329
- ) {
330
- errorMsgs.push(
331
- `${key}.${subkey} missing. Default value (${
332
- Array.isArray(defaultProps[key][subkey][dkey])
333
- ? 'Array'
334
- : typeof defaultProps[key][subkey][dkey]
335
- }): ${defaultProps[key][subkey][dkey]}`
336
- );
337
- solutionSet.add(
338
- `Run 'mcdev upgrade' to fix missing or changed configuration options`
339
- );
340
- missingFields.push(`${key}.${subkey}.${dkey}`);
341
- }
342
- }
343
- }
344
- }
345
- }
346
- }
347
- }
348
- }
349
- // check if project config version is outdated compared to user's mcdev version
350
- if (!properties.version || semver.gt(packageJsonMcdev.version, properties.version)) {
351
- errorMsgs.push(
352
- `Your project's config version ${properties.version} is lower than your Accenture SFMC DevTools version ${packageJsonMcdev.version}`
353
- );
354
- solutionSet.add(`Run 'mcdev upgrade' to ensure optimal performance`);
355
- missingFields.push('version');
356
- }
357
- if (silent) {
358
- return missingFields;
359
- } else {
360
- if (errorMsgs.length) {
361
- const errorMsgOutput = [
362
- `Found problems in your ./${Util.configFileName} that you need to fix first:`,
363
- ];
364
- for (const msg of errorMsgs) {
365
- errorMsgOutput.push(' - ' + msg);
366
- }
367
- Util.logger.error(errorMsgOutput.join('\n'));
368
- Util.logger.info(
369
- [
370
- 'Here is what you can do to fix these issues:',
371
- ...Array.from(solutionSet),
372
- ].join('\n- ')
373
- );
374
- const questions = [
375
- {
376
- type: 'confirm',
377
- name: 'runUpgradeNow',
378
- message: `Do you want to run 'mcdev upgrade' now?`,
379
- default: true,
380
- },
381
- ];
382
- inquirer.prompt(questions).then((responses) => {
383
- if (responses.runUpgradeNow) {
384
- // use _execSync here to avoid a circular dependency
385
- this.execSync('mcdev', ['upgrade']);
386
- }
387
- });
388
206
 
389
- return false;
390
- } else {
391
- return true;
392
- }
393
- }
394
- },
395
207
  loggerTransports: null,
396
208
  /**
397
209
  * Logger that creates timestamped log file in 'logs/' directory
210
+ *
211
+ * @type {TYPE.Logger}
398
212
  */
399
213
  logger: null,
400
214
  restartLogger: startLogger,
401
215
  /**
402
216
  * Logger helper for Metadata functions
403
- * @param {String} level of log (error, info, warn)
404
- * @param {String} type of metadata being referenced
405
- * @param {String} method name which log was called from
217
+ *
218
+ * @param {string} level of log (error, info, warn)
219
+ * @param {string} type of metadata being referenced
220
+ * @param {string} method name which log was called from
406
221
  * @param {*} payload generic object which details the error
407
- * @param {String} [source] key/id of metadata which relates to error
222
+ * @param {string} [source] key/id of metadata which relates to error
408
223
  * @returns {void}
409
224
  */
410
225
  metadataLogger: function (level, type, method, payload, source) {
@@ -426,9 +241,10 @@ const Util = {
426
241
  /**
427
242
  * replaces values in a JSON object string, based on a series of
428
243
  * key-value pairs (obj)
429
- * @param {String|Object} str JSON object or its stringified version, which has values to be replaced
430
- * @param {TemplateMap} obj key value object which contains keys to be replaced and values to be replaced with
431
- * @returns {String|Object} replaced version of str
244
+ *
245
+ * @param {string | object} str JSON object or its stringified version, which has values to be replaced
246
+ * @param {TYPE.TemplateMap} obj key value object which contains keys to be replaced and values to be replaced with
247
+ * @returns {string | object} replaced version of str
432
248
  */
433
249
  replaceByObject: function (str, obj) {
434
250
  let convertType = false;
@@ -445,11 +261,9 @@ const Util = {
445
261
  }
446
262
  }
447
263
 
448
- sortable.sort(function (a, b) {
449
- return b[1].length - a[1].length;
450
- });
264
+ sortable.sort((a, b) => b[1].length - a[1].length);
451
265
  for (const pair of sortable) {
452
- const escVal = pair[1].replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
266
+ const escVal = pair[1].toString().replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
453
267
  const regString = new RegExp(escVal, 'g');
454
268
  str = str.replace(regString, '{{{' + pair[0] + '}}}');
455
269
  }
@@ -460,9 +274,10 @@ const Util = {
460
274
  },
461
275
  /**
462
276
  * get key of an object based on the first matching value
463
- * @param {Object} objs object of objects to be searched
464
- * @param {String} val value to be searched for
465
- * @returns {String} key
277
+ *
278
+ * @param {object} objs object of objects to be searched
279
+ * @param {string} val value to be searched for
280
+ * @returns {string} key
466
281
  */
467
282
  inverseGet: function (objs, val) {
468
283
  for (const obj in objs) {
@@ -475,8 +290,9 @@ const Util = {
475
290
 
476
291
  /**
477
292
  * Returns Order in which metadata needs to be retrieved/deployed
478
- * @param {String[]} metadataTypes which should be retrieved/deployed
479
- * @returns {String[]} retrieve/deploy order as array
293
+ *
294
+ * @param {string[]} metadataTypes which should be retrieved/deployed
295
+ * @returns {string[]} retrieve/deploy order as array
480
296
  */
481
297
  getMetadataHierachy(metadataTypes) {
482
298
  const dependencies = [];
@@ -504,238 +320,37 @@ const Util = {
504
320
  }
505
321
  }
506
322
  // remove subtypes if main type is in the list
507
- Object.keys(subTypeDeps)
323
+ for (const type of Object.keys(subTypeDeps)
508
324
  // only look at subtype deps that are also supposed to be retrieved fully
509
- .filter((type) => metadataTypes.includes(type))
510
- // Rewrite the subtype dependecies to main types.
511
- .forEach((type) => {
512
- // convert set into array to walk its elements
513
- [...subTypeDeps[type]].forEach((subType) => {
514
- dependencies.forEach((item) => {
515
- if (item[0] === subType) {
516
- // if subtype recognized, replace with main type
517
- item[0] = type;
518
- }
519
- });
520
- });
521
- });
522
-
523
- // sort list & remove the undefined dependencies
524
- return toposort(dependencies).filter((a) => !!a);
525
- },
526
- /**
527
- * signs in with SFMC
528
- *
529
- * @param {BuObject} buObject properties for auth
530
- * @returns {Promise<ET_Client>} auth object
531
- */
532
- async getETClient(buObject) {
533
- /** @type ET_Client */
534
- const myClient = new ET_Client(buObject.clientId, buObject.clientSecret, null, {
535
- authOptions: {
536
- authVersion: 2,
537
- accountId: buObject.mid,
538
- },
539
- globalReqOptions: {},
540
- authOrigin: 'https://' + buObject.tenant + '.auth.marketingcloudapis.com',
541
- origin: null,
542
- soapOrigin: null,
543
- });
544
- try {
545
- // check credentials to allow clear log output and stop execution
546
- const test = await myClient.FuelAuthClient.getAccessToken();
547
- if (test.error) {
548
- throw new Error(test.error_description);
549
- } else if (test.scope) {
550
- // find missing rights
551
- const currentScope = test.scope.split(' ');
552
- const missingAccess = Util.expectedAuthScope.filter(
553
- (element) => !currentScope.includes(element)
554
- );
555
- const excessAccess = currentScope.filter(
556
- (element) => !Util.expectedAuthScope.includes(element)
557
- );
558
- if (excessAccess.length) {
559
- Util.logger.debug('Extra access found:' + excessAccess.join(', '));
560
- }
561
- if (missingAccess.length) {
562
- Util.logger.warn(
563
- 'Installed package has insufficient access. You might encounter malfunctions!'
564
- );
565
- Util.logger.warn('Missing scope: ' + missingAccess.join(', '));
566
- }
567
- }
568
- } catch (ex) {
569
- throw new Error(ex.message);
570
- }
571
- return myClient;
572
- },
573
-
574
- /**
575
- * standardized method for getting data from cache.
576
- *
577
- * @param {Object} cache data retrieved from sfmc instance
578
- * @param {String} metadataType metadata type ie. query
579
- * @param {String} searchValue unique identifier of metadata being looked for
580
- * @param {String} searchField field name (key in object) which contains the unique identifer
581
- * @param {String} returnField field which should be returned
582
- * @returns {String} unique user definable metadata key (usually external/customer key)
583
- */
584
- getFromCache(cache, metadataType, searchValue, searchField, returnField) {
585
- for (const key in cache[metadataType]) {
586
- if (Util.resolveObjPath(searchField, cache[metadataType][key]) == searchValue) {
587
- try {
588
- if (Util.resolveObjPath(returnField, cache[metadataType][key])) {
589
- return Util.resolveObjPath(returnField, cache[metadataType][key]);
590
- } else {
591
- throw new Error();
325
+ .filter((type) => metadataTypes.includes(type))) {
326
+ // convert set into array to walk its elements
327
+ for (const subType of subTypeDeps[type]) {
328
+ for (const item of dependencies) {
329
+ if (item[0] === subType) {
330
+ // if subtype recognized, replace with main type
331
+ item[0] = type;
592
332
  }
593
- } catch (ex) {
594
- throw new Error(
595
- `${metadataType} with ${searchField} '${searchValue}' does not have field '${returnField}'`
596
- );
597
333
  }
598
334
  }
599
335
  }
600
- throw new Error(
601
- `Missing one or more dependent metadata. ${metadataType} with ${searchField}='${searchValue}' was not found on your BU`
602
- );
336
+
337
+ // sort list & remove the undefined dependencies
338
+ return toposort(dependencies).filter((a) => !!a);
603
339
  },
340
+
604
341
  /**
605
342
  * let's you dynamically walk down an object and get a value
606
- * @param {String} path 'fieldA.fieldB.fieldC'
607
- * @param {Object} obj some parent object
343
+ *
344
+ * @param {string} path 'fieldA.fieldB.fieldC'
345
+ * @param {object} obj some parent object
608
346
  * @returns {any} value of obj.path
609
347
  */
610
348
  resolveObjPath(path, obj) {
611
- return path.split('.').reduce(function (prev, curr) {
612
- return prev ? prev[curr] : null;
613
- }, obj);
614
- },
615
- /**
616
- * standardized method for getting data from cache - adapted for special case of lists
617
- * ! keeping this in util/util.js rather than in metadataTypes/List.js to avoid potential circular dependencies
618
- *
619
- * @param {Object} cache data retrieved from sfmc instance
620
- * @param {String} listPathName folderPath/ListName combo of list
621
- * @param {String} returnField ObjectID or ID
622
- * @returns {String} unique ObjectId of list
623
- */
624
- getListObjectIdFromCache(cache, listPathName, returnField) {
625
- let folderPath = listPathName.split('/');
626
- const listName = folderPath.pop();
627
- folderPath = folderPath.join('/');
628
- for (const key in cache['list']) {
629
- if (
630
- cache['list'][key].ListName === listName &&
631
- cache['list'][key].r__folder_Path === folderPath
632
- ) {
633
- try {
634
- if (cache['list'][key][returnField]) {
635
- return cache['list'][key][returnField];
636
- } else {
637
- throw new Error();
638
- }
639
- } catch (ex) {
640
- throw new Error(
641
- `${'list'} with ListName='${listName}' and r__folder_Path='${folderPath}' does not have field '${returnField}'`
642
- );
643
- }
644
- }
645
- }
646
- throw new Error(
647
- `Missing one or more dependent metadata. list with ListName='${listName}' and r__folder_Path='${folderPath}' was not found on your BU`
648
- );
649
- },
650
-
651
- /**
652
- * standardized method for getting data from cache - adapted for special case of lists
653
- * ! keeping this in util/util.js rather than in metadataTypes/List.js to avoid potential circular dependencies
654
- *
655
- * @param {Object} cache data retrieved from sfmc instance
656
- * @param {String} searchValue unique identifier of metadata being looked for
657
- * @param {String} searchField ObjectID or ID
658
- * @returns {String} unique folderPath/ListName combo of list
659
- */
660
- getListPathNameFromCache(cache, searchValue, searchField) {
661
- const returnField1 = 'r__folder_Path';
662
- const returnField2 = 'ListName';
663
- for (const key in cache['list']) {
664
- if (cache['list'][key][searchField] === searchValue) {
665
- try {
666
- if (cache['list'][key][returnField1] && cache['list'][key][returnField2]) {
667
- return (
668
- cache['list'][key][returnField1] +
669
- '/' +
670
- cache['list'][key][returnField2]
671
- );
672
- } else {
673
- throw new Error();
674
- }
675
- } catch (ex) {
676
- throw new Error(
677
- `${'list'} with ${searchField}='${searchValue}' does not have the fields ${returnField1} and ${returnField2}`
678
- );
679
- }
680
- }
681
- }
682
- throw new Error(
683
- `Missing one or more dependent metadata. list with ${searchField}='${searchValue}' was not found on your BU`
684
- );
685
- },
686
- /**
687
- * retry on network issues
688
- * @param {String} errorMsg what to print behind "Connection error. "
689
- * @param {Function} callback what to try executing
690
- * @param {Boolean} [silentError=false] prints retry messages to log only; default=false
691
- * @param {Number} [retries=1] number of retries; default=1
692
- * @returns {Promise<void>} -
693
- */
694
- async retryOnError(errorMsg, callback, silentError, retries) {
695
- if ('undefined' === typeof retries || retries === null) {
696
- retries = 3;
697
- }
698
- try {
699
- await callback();
700
- } catch (ex) {
701
- if (
702
- retries > 0 &&
703
- ex.code &&
704
- ['ETIMEDOUT', 'EHOSTUNREACH', 'ENOTFOUND', 'ECONNRESET'].includes(ex.code)
705
- ) {
706
- retries--;
707
- Util.logger.debug(ex.stack);
708
- const msg = `Connection problem. ${errorMsg} (${retries + 1} tries left)`;
709
- if (silentError) {
710
- Util.logger.debug(msg);
711
- } else {
712
- Util.logger.warn(msg);
713
- }
714
- await this.retryOnError(errorMsg, callback, silentError, retries);
715
- } else if (
716
- ex.code &&
717
- ['ETIMEDOUT', 'EHOSTUNREACH', 'ENOTFOUND', 'ECONNRESET'].includes(ex.code)
718
- ) {
719
- Util.logger.debug(ex.stack);
720
- Util.logger.error(
721
- `"${errorMsg}" Failed due to a Connection Error (${ex.code}) - Please check your network connection and try again`
722
- );
723
- if (Util.logger.level === 'debug') {
724
- console.log(ex.stack);
725
- }
726
- throw ex;
727
- } else {
728
- Util.logger.debug(ex.stack);
729
- if (Util.logger.level === 'debug') {
730
- console.log(ex.stack);
731
- }
732
- Util.logger.error(ex.message);
733
- throw ex;
734
- }
735
- }
349
+ return path.split('.').reduce((prev, curr) => (prev ? prev[curr] : null), obj);
736
350
  },
737
351
  /**
738
352
  * helper to run other commands as if run manually by user
353
+ *
739
354
  * @param {string} cmd to be executed command
740
355
  * @param {string[]} [args] list of arguments
741
356
  * @returns {undefined}
@@ -749,9 +364,66 @@ const Util = {
749
364
  const options = { stdio: [0, 1, 2] };
750
365
  return child_process.execSync(cmd + ' ' + args.join(' '), options);
751
366
  },
367
+ /**
368
+ * standardize check to ensure only one result is returned from template search
369
+ *
370
+ * @param {TYPE.MetadataTypeItem[]} results array of metadata
371
+ * @param {string} keyToSearch the field which contains the searched value
372
+ * @param {string} searchValue the value which is being looked for
373
+ * @returns {TYPE.MetadataTypeItem} metadata to be used in building template
374
+ */
375
+ templateSearchResult(results, keyToSearch, searchValue) {
376
+ const matching = results.filter((item) => item[keyToSearch] === searchValue);
377
+
378
+ if (matching.length === 0) {
379
+ throw new Error(`No metadata found with name "${searchValue}"`);
380
+ } else if (matching.length > 1) {
381
+ throw new Error(
382
+ `Multiple metadata with name "${searchValue}" please rename to be unique to avoid issues`
383
+ );
384
+ } else {
385
+ return matching[0];
386
+ }
387
+ },
388
+ /**
389
+ * configures what is displayed in the console
390
+ *
391
+ * @param {object} argv list of command line parameters given by user
392
+ * @param {boolean} [argv.silent] only errors printed to CLI
393
+ * @param {boolean} [argv.verbose] chatty user CLI output
394
+ * @param {boolean} [argv.debug] enables developer output & features
395
+ * @returns {void}
396
+ */
397
+ setLoggingLevel(argv) {
398
+ Util.loggerTransports.console.file = 'debug';
399
+ if (argv.silent) {
400
+ // only errors printed to CLI
401
+ Util.logger.level = 'error';
402
+ Util.loggerTransports.console.level = 'error';
403
+ Util.logger.debug('CLI logger set to: silent');
404
+ } else if (argv.verbose) {
405
+ // chatty user cli logs
406
+ Util.logger.level = 'verbose';
407
+ Util.loggerTransports.console.level = 'verbose';
408
+ Util.logger.debug('CLI logger set to: verbose');
409
+ } else {
410
+ // default user cli logs
411
+ // TODO to be switched to "warn" when cli-process is integrated
412
+ Util.logger.level = 'info';
413
+ Util.loggerTransports.console.level = 'info';
414
+ Util.logger.debug('CLI logger set to: info / default');
415
+ }
416
+ if (argv.debug) {
417
+ // enables developer output & features. no change to actual logs
418
+ Util.logger.level = 'debug';
419
+ Util.loggerTransports.console.level = 'debug';
420
+ Util.logger.debug('CLI logger set to: debug');
421
+ }
422
+ },
752
423
  };
753
424
  /**
754
425
  * wrapper around our standard winston logging to console and logfile
426
+ *
755
427
  * @returns {object} initiated logger for console and file
756
428
  */
757
429
  function createNewLoggerTransport() {
@@ -781,14 +453,58 @@ function createNewLoggerTransport() {
781
453
  }
782
454
  /**
783
455
  * initiate winston logger
456
+ *
784
457
  * @returns {void}
785
458
  */
786
459
  function startLogger() {
787
460
  Util.loggerTransports = createNewLoggerTransport();
788
- Util.logger = winston.createLogger({
461
+ const myWinston = winston.createLogger({
789
462
  levels: winston.config.npm.levels,
790
463
  transports: [Util.loggerTransports.console, Util.loggerTransports.file],
791
464
  });
465
+ const winstonError = myWinston.error;
466
+ const winstonExtension = {
467
+ /**
468
+ * helper that prints better stack trace for errors
469
+ *
470
+ * @param {Error} ex the error
471
+ * @param {string} [message] optional custom message to be printed as error together with the exception's message
472
+ * @returns {void}
473
+ */
474
+ errorStack: function (ex, message) {
475
+ if (message) {
476
+ myWinston.error(message + ': ' + ex.message);
477
+ }
478
+ let stack;
479
+ /* eslint-disable unicorn/prefer-ternary */
480
+ if (
481
+ ['ETIMEDOUT', 'EHOSTUNREACH', 'ENOTFOUND', 'ECONNRESET', 'ECONNABORTED'].includes(
482
+ ex.code
483
+ )
484
+ ) {
485
+ // the stack would just return a one-liner that does not help
486
+ stack = new Error().stack; // eslint-disable-line unicorn/error-message
487
+ } else {
488
+ stack = ex.stack;
489
+ }
490
+ /* eslint-enable unicorn/prefer-ternary */
491
+ myWinston.debug(stack);
492
+ Util.signalFatalError();
493
+ },
494
+ /**
495
+ * errors should cause surrounding applications to take notice
496
+ * hence we overwrite the default error function here
497
+ *
498
+ * @param {string} msg - the message to log
499
+ * @returns {void}
500
+ */
501
+ error: function (msg) {
502
+ winstonError(msg);
503
+ Util.signalFatalError();
504
+ },
505
+ };
506
+ Util.logger = Object.assign(myWinston, winstonExtension);
507
+
792
508
  Util.logger.debug(`:: mcdev ${packageJsonMcdev.version} ::`);
793
509
  }
794
510
  startLogger();