mcdev 3.1.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/.eslintrc.json +67 -7
  2. package/.github/ISSUE_TEMPLATE/bug.yml +5 -1
  3. package/.github/ISSUE_TEMPLATE/task.md +1 -1
  4. package/.github/PULL_REQUEST_TEMPLATE.md +5 -3
  5. package/.github/dependabot.yml +14 -0
  6. package/.github/workflows/code-analysis.yml +57 -0
  7. package/.husky/commit-msg +10 -0
  8. package/.husky/post-checkout +5 -0
  9. package/.husky/pre-commit +2 -1
  10. package/.prettierrc +8 -0
  11. package/.vscode/settings.json +1 -1
  12. package/LICENSE +2 -2
  13. package/README.md +134 -45
  14. package/boilerplate/config.json +5 -11
  15. package/boilerplate/files/.prettierrc +8 -0
  16. package/boilerplate/files/.vscode/extensions.json +0 -1
  17. package/boilerplate/files/.vscode/settings.json +28 -2
  18. package/boilerplate/files/README.md +2 -2
  19. package/boilerplate/forcedUpdates.json +10 -0
  20. package/boilerplate/npm-dependencies.json +5 -5
  21. package/docs/dist/documentation.md +2795 -1724
  22. package/jsconfig.json +1 -1
  23. package/lib/Builder.js +166 -75
  24. package/lib/Deployer.js +244 -96
  25. package/lib/MetadataTypeDefinitions.js +2 -0
  26. package/lib/MetadataTypeInfo.js +2 -0
  27. package/lib/Retriever.js +61 -84
  28. package/lib/cli.js +133 -25
  29. package/lib/index.js +242 -563
  30. package/lib/metadataTypes/AccountUser.js +101 -95
  31. package/lib/metadataTypes/Asset.js +677 -248
  32. package/lib/metadataTypes/AttributeGroup.js +23 -12
  33. package/lib/metadataTypes/Automation.js +456 -357
  34. package/lib/metadataTypes/Campaign.js +33 -93
  35. package/lib/metadataTypes/ContentArea.js +31 -11
  36. package/lib/metadataTypes/DataExtension.js +391 -376
  37. package/lib/metadataTypes/DataExtensionField.js +131 -54
  38. package/lib/metadataTypes/DataExtensionTemplate.js +22 -4
  39. package/lib/metadataTypes/DataExtract.js +67 -50
  40. package/lib/metadataTypes/DataExtractType.js +14 -8
  41. package/lib/metadataTypes/Discovery.js +21 -16
  42. package/lib/metadataTypes/Email.js +32 -12
  43. package/lib/metadataTypes/EmailSendDefinition.js +85 -80
  44. package/lib/metadataTypes/EventDefinition.js +69 -47
  45. package/lib/metadataTypes/FileTransfer.js +78 -54
  46. package/lib/metadataTypes/Filter.js +11 -4
  47. package/lib/metadataTypes/Folder.js +149 -117
  48. package/lib/metadataTypes/FtpLocation.js +14 -8
  49. package/lib/metadataTypes/ImportFile.js +69 -69
  50. package/lib/metadataTypes/Interaction.js +19 -4
  51. package/lib/metadataTypes/List.js +54 -13
  52. package/lib/metadataTypes/MetadataType.js +687 -479
  53. package/lib/metadataTypes/MobileCode.js +46 -0
  54. package/lib/metadataTypes/MobileKeyword.js +114 -0
  55. package/lib/metadataTypes/Query.js +204 -103
  56. package/lib/metadataTypes/Role.js +76 -61
  57. package/lib/metadataTypes/Script.js +146 -82
  58. package/lib/metadataTypes/SetDefinition.js +20 -8
  59. package/lib/metadataTypes/TriggeredSendDefinition.js +78 -58
  60. package/lib/metadataTypes/definitions/Asset.definition.js +21 -10
  61. package/lib/metadataTypes/definitions/AttributeGroup.definition.js +12 -0
  62. package/lib/metadataTypes/definitions/Automation.definition.js +10 -5
  63. package/lib/metadataTypes/definitions/Campaign.definition.js +44 -1
  64. package/lib/metadataTypes/definitions/DataExtension.definition.js +4 -0
  65. package/lib/metadataTypes/definitions/DataExtensionTemplate.definition.js +6 -0
  66. package/lib/metadataTypes/definitions/DataExtract.definition.js +18 -14
  67. package/lib/metadataTypes/definitions/Discovery.definition.js +12 -0
  68. package/lib/metadataTypes/definitions/EmailSendDefinition.definition.js +4 -0
  69. package/lib/metadataTypes/definitions/EventDefinition.definition.js +22 -0
  70. package/lib/metadataTypes/definitions/FileTransfer.definition.js +4 -0
  71. package/lib/metadataTypes/definitions/Filter.definition.js +4 -0
  72. package/lib/metadataTypes/definitions/Folder.definition.js +6 -0
  73. package/lib/metadataTypes/definitions/FtpLocation.definition.js +4 -0
  74. package/lib/metadataTypes/definitions/ImportFile.definition.js +10 -5
  75. package/lib/metadataTypes/definitions/Interaction.definition.js +4 -0
  76. package/lib/metadataTypes/definitions/MobileCode.definition.js +163 -0
  77. package/lib/metadataTypes/definitions/MobileKeyword.definition.js +253 -0
  78. package/lib/metadataTypes/definitions/Query.definition.js +4 -0
  79. package/lib/metadataTypes/definitions/Role.definition.js +5 -0
  80. package/lib/metadataTypes/definitions/Script.definition.js +4 -0
  81. package/lib/metadataTypes/definitions/SetDefinition.definition.js +28 -0
  82. package/lib/metadataTypes/definitions/TriggeredSendDefinition.definition.js +4 -0
  83. package/lib/retrieveChangelog.js +7 -6
  84. package/lib/util/auth.js +117 -0
  85. package/lib/util/businessUnit.js +55 -66
  86. package/lib/util/cache.js +194 -0
  87. package/lib/util/cli.js +90 -116
  88. package/lib/util/config.js +302 -0
  89. package/lib/util/devops.js +240 -50
  90. package/lib/util/file.js +120 -191
  91. package/lib/util/init.config.js +195 -69
  92. package/lib/util/init.git.js +45 -50
  93. package/lib/util/init.js +72 -59
  94. package/lib/util/init.npm.js +48 -39
  95. package/lib/util/util.js +280 -564
  96. package/package.json +44 -33
  97. package/test/dataExtension.test.js +152 -0
  98. package/test/mockRoot/.mcdev-auth.json +8 -0
  99. package/test/mockRoot/.mcdevrc.json +67 -0
  100. package/test/mockRoot/deploy/testInstance/testBU/dataExtension/childBU_dataextension_test.dataExtension-meta.json +39 -0
  101. package/test/mockRoot/deploy/testInstance/testBU/dataExtension/testDataExtension.dataExtension-meta.json +23 -0
  102. package/test/mockRoot/deploy/testInstance/testBU/query/testExistingQuery.query-meta.json +11 -0
  103. package/test/mockRoot/deploy/testInstance/testBU/query/testExistingQuery.query-meta.sql +4 -0
  104. package/test/mockRoot/deploy/testInstance/testBU/query/testQuery.query-meta.json +11 -0
  105. package/test/mockRoot/deploy/testInstance/testBU/query/testQuery.query-meta.sql +4 -0
  106. package/test/query.test.js +149 -0
  107. package/test/resourceFactory.js +142 -0
  108. package/test/resources/1111111/dataFolder/retrieve-response.xml +43 -0
  109. package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dat/patch-response.json +18 -0
  110. package/test/resources/9999999/automation/v1/queries/get-response.json +24 -0
  111. package/test/resources/9999999/automation/v1/queries/post-response.json +18 -0
  112. package/test/resources/9999999/dataExtension/build-expected.json +51 -0
  113. package/test/resources/9999999/dataExtension/create-expected.json +23 -0
  114. package/test/resources/9999999/dataExtension/create-response.xml +54 -0
  115. package/test/resources/9999999/dataExtension/retrieve-expected.json +51 -0
  116. package/test/resources/9999999/dataExtension/retrieve-response.xml +47 -0
  117. package/test/resources/9999999/dataExtension/template-expected.json +51 -0
  118. package/test/resources/9999999/dataExtension/update-expected.json +55 -0
  119. package/test/resources/9999999/dataExtension/update-response.xml +52 -0
  120. package/test/resources/9999999/dataExtensionField/retrieve-response.xml +93 -0
  121. package/test/resources/9999999/dataExtensionTemplate/retrieve-response.xml +303 -0
  122. package/test/resources/9999999/dataFolder/retrieve-response.xml +65 -0
  123. package/test/resources/9999999/query/build-expected.json +8 -0
  124. package/test/resources/9999999/query/get-expected.json +11 -0
  125. package/test/resources/9999999/query/patch-expected.json +11 -0
  126. package/test/resources/9999999/query/post-expected.json +11 -0
  127. package/test/resources/9999999/query/template-expected.json +8 -0
  128. package/test/resources/auth.json +32 -0
  129. package/test/resources/rest404-response.json +5 -0
  130. package/test/resources/retrieve-response.xml +21 -0
  131. package/test/utils.js +107 -0
  132. package/types/mcdev.d.js +301 -0
  133. package/CHANGELOG.md +0 -126
  134. package/PULL_REQUEST_TEMPLATE.md +0 -19
  135. package/test/util/file.js +0 -51
@@ -1,10 +1,13 @@
1
1
  'use strict';
2
2
 
3
+ const TYPE = require('../../types/mcdev.d');
3
4
  const Cli = require('./cli');
4
5
  const File = require('./file');
6
+ const config = require('./config');
5
7
  const Util = require('./util');
6
8
  const inquirer = require('inquirer');
7
- const path = require('path');
9
+ const path = require('node:path');
10
+ const semver = require('semver');
8
11
 
9
12
  /**
10
13
  * CLI helper class
@@ -13,8 +16,9 @@ const path = require('path');
13
16
  const Init = {
14
17
  /**
15
18
  * helper method for this.upgradeProject that upgrades project config if needed
16
- * @param {Object} properties config file's json
17
- * @returns {Promise<Boolean>} returns true if worked without errors
19
+ *
20
+ * @param {TYPE.Mcdevrc} properties config file's json
21
+ * @returns {Promise.<boolean>} returns true if worked without errors
18
22
  */
19
23
  async fixMcdevConfig(properties) {
20
24
  if (!properties) {
@@ -26,10 +30,10 @@ const Init = {
26
30
 
27
31
  const upgradeMsgs = [`Upgrading existing ${Util.configFileName}:`];
28
32
 
29
- const missingFields = Util.checkProperties(properties, true);
33
+ const missingFields = await config.checkProperties(properties, true);
34
+ const defaultProps = await config.getDefaultProperties();
30
35
  if (missingFields.length) {
31
- const defaultProps = Util.getDefaultProperties();
32
- missingFields.forEach((fieldName) => {
36
+ for (const fieldName of missingFields) {
33
37
  switch (fieldName) {
34
38
  case 'marketList':
35
39
  if (properties.marketBulk) {
@@ -41,18 +45,35 @@ const Init = {
41
45
  this._updateLeaf(properties, defaultProps, fieldName);
42
46
  }
43
47
  break;
44
- case 'directories.dataExtension':
48
+ case 'directories.docs':
49
+ if (properties.directories.badKeys) {
50
+ delete properties.directories.badKeys;
51
+ upgradeMsgs.push(`- ✋ removed 'directories.badKeys'`);
52
+ }
53
+ if (properties.directories.dataExtension) {
54
+ File.removeSync(properties.directories.dataExtension);
55
+ delete properties.directories.dataExtension;
56
+ upgradeMsgs.push(`- ✋ removed 'directories.dataExtension'`);
57
+ }
58
+ if (properties.directories.deltaPackage) {
59
+ delete properties.directories.deltaPackage;
60
+ upgradeMsgs.push(`- ✋ removed 'directories.deltaPackage'`);
61
+ }
45
62
  if (properties.directories.dataextension) {
46
- upgradeMsgs.push(
47
- `- ✔️ converted 'directories.dataextension' to '${fieldName}'`
48
- );
49
- properties.directories.dataExtension =
50
- properties.directories.dataextension;
51
63
  delete properties.directories.dataextension;
52
- } else {
53
- upgradeMsgs.push(`- ✔️ added '${fieldName}'`);
54
- this._updateLeaf(properties, defaultProps, fieldName);
64
+ upgradeMsgs.push(`- removed 'directories.dataextension'`);
65
+ }
66
+ if (properties.directories.roles) {
67
+ delete properties.directories.roles;
68
+ upgradeMsgs.push(`- ✋ removed 'directories.roles'`);
55
69
  }
70
+ if (properties.directories.users) {
71
+ delete properties.directories.users;
72
+ upgradeMsgs.push(`- ✋ removed 'directories.users'`);
73
+ }
74
+
75
+ this._updateLeaf(properties, defaultProps, fieldName);
76
+ upgradeMsgs.push(`- ✔️ added '${fieldName}'`);
56
77
  break;
57
78
  case 'metaDataTypes.documentOnRetrieve':
58
79
  if (!properties.options.documentOnRetrieve) {
@@ -90,19 +111,22 @@ const Init = {
90
111
  break;
91
112
  case 'version':
92
113
  // do nothing other than ensure we re-save the config (with the new version)
93
- delete properties.catalystFullVersion;
94
- delete properties.catalystVersion;
95
-
96
114
  upgradeMsgs.push(`- ✔️ version updated`);
97
115
  break;
98
116
  default:
99
117
  this._updateLeaf(properties, defaultProps, fieldName);
100
118
  upgradeMsgs.push(`- ✔️ added '${fieldName}'`);
101
119
  }
102
- });
120
+ }
103
121
  updateConfigNeeded = true;
104
122
  }
105
123
 
124
+ // ensure we document dataExtensions and automations on retrieve as they should now be in the retrieve folder
125
+ this._updateLeaf(properties, defaultProps, 'metaDataTypes.documentOnRetrieve');
126
+ upgradeMsgs.push(
127
+ `- ✔️ updated 'metaDataTypes.documentOnRetrieve' to include all available types`
128
+ );
129
+
106
130
  // check if metaDataTypes.retrieve is set to default values and if not, launch selectTypes
107
131
  const defaultRetrieveArr = Util.getRetrieveTypeChoices();
108
132
  let reselectDefaultRetrieve = false;
@@ -118,6 +142,18 @@ const Init = {
118
142
  updateConfigNeeded = true;
119
143
  }
120
144
 
145
+ // move to version 4 uses integers for MIDs
146
+ for (const cred in properties.credentials) {
147
+ properties.credentials[cred].eid = Number.parseInt(properties.credentials[cred].eid);
148
+ for (const bu in properties.credentials[cred].businessUnits) {
149
+ properties.credentials[cred].businessUnits[bu] = Number.parseInt(
150
+ properties.credentials[cred].businessUnits[bu]
151
+ );
152
+ }
153
+ updateConfigNeeded = true;
154
+ upgradeMsgs.push(`- ✔️ updated Business Unit format (${cred})`);
155
+ }
156
+
121
157
  // update config
122
158
  if (updateConfigNeeded) {
123
159
  for (const msg of upgradeMsgs) {
@@ -131,16 +167,16 @@ const Init = {
131
167
  Util.logger.warn('');
132
168
  if (toBeAddedTypes.length) {
133
169
  Util.logger.warn('Adding types:');
134
- toBeAddedTypes.forEach((type) => {
170
+ for (const type of toBeAddedTypes) {
135
171
  Util.logger.warn(` - ${type}`);
136
- });
172
+ }
137
173
  Util.logger.warn('');
138
174
  }
139
175
  if (toBeRemovedTypes.length) {
140
176
  Util.logger.warn('Removing types:');
141
- toBeRemovedTypes.forEach((type) => {
177
+ for (const type of toBeRemovedTypes) {
142
178
  Util.logger.warn(` - ${type}`);
143
- });
179
+ }
144
180
  Util.logger.warn('');
145
181
  }
146
182
  await Cli.selectTypes(properties, defaultRetrieveArr);
@@ -154,21 +190,19 @@ const Init = {
154
190
 
155
191
  return true;
156
192
  },
193
+
157
194
  /**
158
195
  * handles creation/update of all config file from the boilerplate
159
- * @returns {Promise<Boolean>} status of config file creation
196
+ *
197
+ * @param {string} versionBeforeUpgrade 'x.y.z'
198
+ * @returns {Promise.<boolean>} status of config file creation
160
199
  */
161
- async createIdeConfigFiles() {
200
+ async createIdeConfigFiles(versionBeforeUpgrade) {
162
201
  Util.logger.info('Checking configuration files (existing files will not be changed):');
163
202
  const creationLog = [];
164
-
165
- if (!File.existsSync('deploy/')) {
166
- File.mkdirpSync('deploy/');
167
- }
168
-
169
- if (!File.existsSync('src/cloudPages')) {
170
- File.mkdirpSync('src/cloudPages');
171
- }
203
+ await File.ensureDir('deploy/');
204
+ await File.ensureDir('src/cloudPages');
205
+ const relevantForcedUpdates = await this._getForcedUpdateList(versionBeforeUpgrade);
172
206
 
173
207
  // copy in .gitignore (cant be retrieved via npm install directly)
174
208
  const gitignoreFileName = path.resolve(
@@ -176,32 +210,39 @@ const Init = {
176
210
  Util.boilerplateDirectory,
177
211
  'gitignore-template'
178
212
  );
179
- if (!File.existsSync(gitignoreFileName)) {
213
+ if (!(await File.pathExists(gitignoreFileName))) {
180
214
  Util.logger.debug(`Dependency file not found in ${gitignoreFileName}`);
181
215
  return false;
182
216
  } else {
183
- const fileContent = File.readFileSync(gitignoreFileName, 'utf8');
217
+ const fileContent = await File.readFile(gitignoreFileName, 'utf8');
184
218
  creationLog.push(
185
- await this._createIdeConfigFile(['.' + path.sep, '', '.gitignore'], fileContent)
219
+ await this._createIdeConfigFile(
220
+ ['.' + path.sep, '', '.gitignore'],
221
+ relevantForcedUpdates,
222
+ fileContent
223
+ )
186
224
  );
187
225
  }
188
226
 
189
227
  // load file list from boilerplate dir and initiate copy process
190
228
  const boilerPlateFilesPath = path.resolve(__dirname, Util.boilerplateDirectory, 'files');
191
- const directories = File.readDirectoriesSync(boilerPlateFilesPath, 10, false);
192
-
229
+ // ! do not switch to readDirectories before merging the two custom methods. Their logic is different!
230
+ const directories = await File.readDirectoriesSync(boilerPlateFilesPath, 10, false);
193
231
  for (const subdir of directories) {
194
232
  // walk thru the root of our boilerplate-files directory and all sub folders
195
233
  const curDir = path.join(boilerPlateFilesPath, subdir);
196
- for (const file of File.readdirSync(curDir)) {
234
+ for (const file of await File.readdir(curDir)) {
197
235
  // read all files in these directories
198
- if (!File.lstatSync(path.join(curDir, file)).isDirectory()) {
236
+ if (!(await File.lstat(path.join(curDir, file))).isDirectory()) {
199
237
  // filter entries that are actually folders
200
238
  const fileArr = file.split('.');
201
239
  const ext = '.' + fileArr.pop();
202
240
  // awaiting the result here due to interactive optional overwrite
203
241
  creationLog.push(
204
- await this._createIdeConfigFile([subdir + path.sep, fileArr.join('.'), ext])
242
+ await this._createIdeConfigFile(
243
+ [subdir + path.sep, fileArr.join('.'), ext],
244
+ relevantForcedUpdates
245
+ )
205
246
  );
206
247
  }
207
248
  }
@@ -220,9 +261,10 @@ const Init = {
220
261
  },
221
262
  /**
222
263
  * recursive helper for _fixMcdevConfig that adds missing settings
223
- * @param {Object} propertiersCur current sub-object of project settings
224
- * @param {Object} defaultPropsCur current sub-object of default settings
225
- * @param {String} fieldName dot-concatenated object-path that needs adding
264
+ *
265
+ * @param {object} propertiersCur current sub-object of project settings
266
+ * @param {object} defaultPropsCur current sub-object of default settings
267
+ * @param {string} fieldName dot-concatenated object-path that needs adding
226
268
  * @returns {void}
227
269
  */
228
270
  _updateLeaf(propertiersCur, defaultPropsCur, fieldName) {
@@ -242,13 +284,45 @@ const Init = {
242
284
  propertiersCur[fieldName] = defaultPropsCur[fieldName];
243
285
  }
244
286
  },
287
+ /**
288
+ * returns list of files that need to be updated
289
+ *
290
+ * @param {string} projectVersion version found in config file of the current project
291
+ * @returns {Promise.<string[]>} relevant files with path that need to be updated
292
+ */
293
+ async _getForcedUpdateList(projectVersion) {
294
+ // list of files that absolutely need to get overwritten, no questions asked, when upgrading from a version lower than the given.
295
+ let forceIdeConfigUpdate;
296
+ const relevantForcedUpdates = [];
297
+ if (await File.pathExists(Util.configFileName)) {
298
+ forceIdeConfigUpdate = File.readJsonSync(
299
+ path.resolve(__dirname, Util.boilerplateDirectory, 'forcedUpdates.json')
300
+ );
301
+ // return all if no project version was found or only changes from "newer" versions otherwise
302
+ for (const element of forceIdeConfigUpdate) {
303
+ if (!projectVersion || semver.gt(element.version, projectVersion)) {
304
+ relevantForcedUpdates.push(
305
+ // adapt it for local file systems
306
+ ...element.files.map((item) => path.normalize(item))
307
+ );
308
+ } else {
309
+ continue;
310
+ }
311
+ }
312
+ }
313
+
314
+ return relevantForcedUpdates;
315
+ },
245
316
  /**
246
317
  * handles creation/update of one config file from the boilerplate at a time
247
- * @param {String[]} fileNameArr 0: path, 1: filename, 2: extension with dot
248
- * @param {String} [fileContent] in case we cannot copy files 1:1 this can be used to pass in content
249
- * @returns {Promise<Boolean>} install successful or error occured
318
+ *
319
+ * @param {string[]} fileNameArr 0: path, 1: filename, 2: extension with dot
320
+ * @param {string[]} relevantForcedUpdates if fileNameArr is in this list we require an override
321
+ * @param {string} [boilerplateFileContent] in case we cannot copy files 1:1 this can be used to pass in content
322
+ * @returns {Promise.<boolean>} install successful or error occured
250
323
  */
251
- async _createIdeConfigFile(fileNameArr, fileContent) {
324
+ async _createIdeConfigFile(fileNameArr, relevantForcedUpdates, boilerplateFileContent) {
325
+ let update = false;
252
326
  const fileName = fileNameArr.join('');
253
327
  const boilerplateFileName = path.resolve(
254
328
  __dirname,
@@ -256,41 +330,93 @@ const Init = {
256
330
  'files',
257
331
  fileName
258
332
  );
259
- if (File.existsSync(fileName)) {
260
- Util.logger.info(`- ✋ ${fileName} already existing`);
261
- const questions = [
262
- {
263
- type: 'confirm',
264
- name: 'overrideFile',
265
- message: 'Would you like to override it?',
266
- default: false,
267
- },
268
- ];
269
- const responses = await new Promise((resolve) => {
270
- inquirer.prompt(questions).then((answers) => {
271
- resolve(answers);
272
- });
273
- });
274
- if (!responses.overrideFile) {
333
+ boilerplateFileContent =
334
+ boilerplateFileContent || (await File.readFile(boilerplateFileName, 'utf8'));
335
+
336
+ if (await File.pathExists(fileName)) {
337
+ const existingFileContent = await File.readFile(fileName, 'utf8');
338
+ if (existingFileContent === boilerplateFileContent) {
339
+ Util.logger.info(`- ✔️ ${fileName} found. No update needed`);
275
340
  return true;
276
341
  }
342
+ if (relevantForcedUpdates.includes(path.normalize(fileName))) {
343
+ Util.logger.info(
344
+ `- ✋ ${fileName} found but an update is required. Commencing with override:`
345
+ );
346
+ } else {
347
+ Util.logger.info(
348
+ `- ✋ ${fileName} found with differences to the new standard version. We recommend updating it.`
349
+ );
350
+ if (!Util.skipInteraction) {
351
+ const responses = await inquirer.prompt([
352
+ {
353
+ type: 'confirm',
354
+ name: 'overrideFile',
355
+ message: 'Would you like to update (override) it?',
356
+ default: true,
357
+ },
358
+ ]);
359
+ if (!responses.overrideFile) {
360
+ // skip override without error
361
+ return true;
362
+ }
363
+ }
364
+ }
365
+ update = true;
366
+
367
+ // ensure our update is not leading to data loss in case config files were not versioned correctly by the user
368
+ await File.rename(fileName, fileName + '.BAK');
277
369
  }
278
- fileContent = fileContent || File.readFileSync(boilerplateFileName, 'utf8');
279
370
  const saveStatus = await File.writeToFile(
280
371
  fileNameArr[0],
281
372
  fileNameArr[1],
282
- fileNameArr[2].substr(1),
283
- fileContent
373
+ fileNameArr[2].slice(1),
374
+ boilerplateFileContent
284
375
  );
285
376
 
286
377
  if (saveStatus) {
287
- Util.logger.info(`- ✔️ ${fileName} created`);
378
+ Util.logger.info(
379
+ `- ✔️ ${fileName} ${
380
+ update
381
+ ? `updated (we created a backup of the old file under ${fileName + '.BAK'})`
382
+ : 'created'
383
+ }`
384
+ );
288
385
  return true;
289
386
  } else {
290
- Util.logger.warn(`- ❌ ${fileName} creation failed`);
387
+ Util.logger.warn(`- ❌ ${fileName} ${update ? 'update' : 'creation'} failed`);
291
388
  return false;
292
389
  }
293
390
  },
391
+ /**
392
+ * helper method for this.upgradeProject that upgrades project config if needed
393
+ *
394
+ * @returns {Promise.<boolean>} returns true if worked without errors
395
+ */
396
+ async upgradeAuthFile() {
397
+ if (await File.pathExists(Util.authFileName)) {
398
+ const existingAuth = await File.readJSON(Util.authFileName);
399
+ // if has credentials key then is old format
400
+ if (existingAuth.credentials) {
401
+ const newAuth = {};
402
+ for (const cred in existingAuth.credentials) {
403
+ newAuth[cred] = {
404
+ client_id: existingAuth.credentials[cred].clientId,
405
+ client_secret: existingAuth.credentials[cred].clientSecret,
406
+ auth_url: `https://${existingAuth.credentials[cred].tenant}.auth.marketingcloudapis.com/`,
407
+ account_id: Number.parseInt(existingAuth.credentials[cred].eid),
408
+ };
409
+ }
410
+ await File.writeJSONToFile(
411
+ './',
412
+ Util.authFileName.replace(/(.json)+$/, ''),
413
+ newAuth
414
+ );
415
+ Util.logger.info(`- ✔️ upgraded credential file`);
416
+ }
417
+ }
418
+ return true;
419
+ },
294
420
  };
295
421
 
296
422
  module.exports = Init;
@@ -1,10 +1,10 @@
1
1
  'use strict';
2
-
2
+ const TYPE = require('../../types/mcdev.d');
3
3
  const File = require('./file');
4
4
  const inquirer = require('inquirer');
5
5
  const Util = require('./util');
6
6
  const commandExists = require('command-exists');
7
- const git = require('simple-git/promise')();
7
+ const git = require('simple-git')();
8
8
 
9
9
  /**
10
10
  * CLI helper class
@@ -13,9 +13,9 @@ const git = require('simple-git/promise')();
13
13
  const Init = {
14
14
  /**
15
15
  * check if git repo exists and otherwise create one
16
- * @param {Object} [skipInteraction] signals what to insert automatically for things usually asked via wizard
17
- * @param {String} skipInteraction.gitRemoteUrl URL of Git remote server
18
- * @returns {Promise<{status:String, repoName:String}>} success flag
16
+ *
17
+ * @param {TYPE.skipInteraction} [skipInteraction] signals what to insert automatically for things usually asked via wizard
18
+ * @returns {Promise.<{status: string, repoName: string}>} success flag
19
19
  */
20
20
  async initGitRepo(skipInteraction) {
21
21
  const result = { status: null, repoName: null };
@@ -23,13 +23,13 @@ const Init = {
23
23
  if (!commandExists.sync('git')) {
24
24
  Util.logger.error('Git installation not found.');
25
25
  Util.logger.error(
26
- 'Please follow our tutorial on installing Git: https://go.accenture.com/mcdevdocs'
26
+ 'Please follow our tutorial on installing Git: https://github.com/Accenture/sfmc-devtools#212-install-the-git-command-line'
27
27
  );
28
28
  result.status = 'error';
29
29
  return result;
30
30
  }
31
31
  // 3. test if in git repo
32
- const gitRepoFoundInCWD = File.existsSync('.git');
32
+ const gitRepoFoundInCWD = await File.pathExists('.git');
33
33
  let newRepoInitialized = null;
34
34
  if (gitRepoFoundInCWD) {
35
35
  Util.logger.info(`✔️ Git repository found`);
@@ -37,7 +37,7 @@ const Init = {
37
37
  } else {
38
38
  Util.logger.warn('No Git repository found. Initializing git:');
39
39
  Util.execSync('git', ['init']);
40
- if (File.existsSync('.git')) {
40
+ if (await File.pathExists('.git')) {
41
41
  newRepoInitialized = true;
42
42
  } else {
43
43
  Util.logger.error(
@@ -50,7 +50,7 @@ const Init = {
50
50
  Util.logger.info('Ensuring long file paths are not causing issues with git:');
51
51
  try {
52
52
  Util.execSync('git', ['config', '--local', 'core.longpaths', 'true']);
53
- } catch (ex) {
53
+ } catch {
54
54
  Util.logger.warn(
55
55
  `Updating your git config failed. We recommend running the above command manually yourself to avoid issues.`
56
56
  );
@@ -58,7 +58,7 @@ const Init = {
58
58
  Util.logger.info('Ensuring checkout (git pull) as-is and commit Unix-style line endings:');
59
59
  try {
60
60
  Util.execSync('git', ['config', '--local', 'core.autocrlf', 'input']);
61
- } catch (ex) {
61
+ } catch {
62
62
  Util.logger.warn(
63
63
  `Updating your git config failed. We recommend running the above command manually yourself to avoid issues.`
64
64
  );
@@ -77,7 +77,8 @@ const Init = {
77
77
  },
78
78
  /**
79
79
  * offer to push the new repo straight to the server
80
- * @param {Boolean|Object} [skipInteraction] signals what to insert automatically for things usually asked via wizard
80
+ *
81
+ * @param {boolean | TYPE.skipInteraction} [skipInteraction] signals what to insert automatically for things usually asked via wizard
81
82
  * @returns {void}
82
83
  */
83
84
  async gitPush(skipInteraction) {
@@ -101,19 +102,14 @@ const Init = {
101
102
  );
102
103
  let responses;
103
104
  if (!skipInteraction) {
104
- const questions = [
105
+ responses = await inquirer.prompt([
105
106
  {
106
107
  type: 'confirm',
107
108
  name: 'gitPush',
108
109
  message: `Would you like to 'push' your backup to the remote Git repo?`,
109
110
  default: true,
110
111
  },
111
- ];
112
- responses = await new Promise((resolve) => {
113
- inquirer.prompt(questions).then((answers) => {
114
- resolve(answers);
115
- });
116
- });
112
+ ]);
117
113
  }
118
114
  if (skipInteraction || responses.gitPush) {
119
115
  Util.execSync('git', ['push', '-u', 'origin', 'master']);
@@ -127,34 +123,30 @@ const Init = {
127
123
  },
128
124
  /**
129
125
  * offers to add the git remote origin
130
- * @param {Object} [skipInteraction] signals what to insert automatically for things usually asked via wizard
131
- * @param {String} skipInteraction.gitRemoteUrl URL of Git remote server
132
- * @returns {String} repo name (optionally)
126
+ *
127
+ * @param {TYPE.skipInteraction} [skipInteraction] signals what to insert automatically for things usually asked via wizard
128
+ * @returns {string} repo name (optionally)
133
129
  */
134
130
  async _addGitRemote(skipInteraction) {
135
131
  // #1 ask if the user wants to do it now
136
132
  let responses;
137
133
  if (!skipInteraction) {
138
- const questions = [
134
+ responses = await inquirer.prompt([
139
135
  {
140
136
  type: 'confirm',
141
137
  name: 'gitOriginKnown',
142
138
  message: `Do you know the remote/clone URL of your Git repo (starts with ssh:// or http:// and ends on '.git')?`,
143
139
  default: true,
144
140
  },
145
- ];
146
- responses = await new Promise((resolve) => {
147
- inquirer.prompt(questions).then((answers) => {
148
- resolve(answers);
149
- });
150
- });
141
+ ]);
151
142
  }
152
143
  if (skipInteraction || responses.gitOriginKnown) {
153
144
  // #2 if yes, guide the user to input the right url
145
+ /* eslint-disable unicorn/prefer-ternary */
154
146
  if (skipInteraction) {
155
147
  responses = skipInteraction;
156
148
  } else {
157
- const questions = [
149
+ responses = await inquirer.prompt([
158
150
  {
159
151
  type: 'input',
160
152
  name: 'gitRemoteUrl',
@@ -173,13 +165,10 @@ const Init = {
173
165
  }
174
166
  },
175
167
  },
176
- ];
177
- responses = await new Promise((resolve) => {
178
- inquirer.prompt(questions).then((answers) => {
179
- resolve(answers);
180
- });
181
- });
168
+ ]);
182
169
  }
170
+ /* eslint-enable unicorn/prefer-ternary */
171
+
183
172
  responses.gitRemoteUrl = responses.gitRemoteUrl.trim();
184
173
  Util.execSync('git', ['remote', 'add', 'origin', responses.gitRemoteUrl]);
185
174
  return responses.gitRemoteUrl.split('/').pop().split('.')[0];
@@ -187,7 +176,8 @@ const Init = {
187
176
  },
188
177
  /**
189
178
  * checks global config and ask to config the user info and then store it locally
190
- * @param {Object|Boolean} [skipInteraction] signals what to insert automatically for things usually asked via wizard
179
+ *
180
+ * @param {boolean | TYPE.skipInteraction} [skipInteraction] signals what to insert automatically for things usually asked via wizard
191
181
  * @returns {void}
192
182
  */
193
183
  async _updateGitConfigUser(skipInteraction) {
@@ -196,19 +186,20 @@ const Init = {
196
186
  `Please confirm your Git user name & email. It should be in the format 'FirstName LastName' and 'your.email@accenture.com'. The current (potentially wrong) values are provided as default. If correct, confirm with ENTER, otherwise please update:`
197
187
  );
198
188
  let responses;
189
+ /* eslint-disable unicorn/prefer-ternary */
199
190
  if (skipInteraction) {
200
191
  responses = {
201
192
  name: gitUser['user.name'],
202
193
  email: gitUser['user.email'],
203
194
  };
204
195
  } else {
205
- const questions = [
196
+ responses = await inquirer.prompt([
206
197
  {
207
198
  type: 'input',
208
199
  name: 'name',
209
200
  message: 'Git user.name',
210
201
  default: gitUser['user.name'] || null,
211
- // eslint-disable-next-line require-jsdoc
202
+ // eslint-disable-next-line jsdoc/require-jsdoc
212
203
  validate: function (value) {
213
204
  if (
214
205
  !value ||
@@ -226,7 +217,7 @@ const Init = {
226
217
  name: 'email',
227
218
  message: 'Git user.email',
228
219
  default: gitUser['user.email'] || null,
229
- // eslint-disable-next-line require-jsdoc
220
+ // eslint-disable-next-line jsdoc/require-jsdoc
230
221
  validate: function (value) {
231
222
  value = value.trim();
232
223
  const regex =
@@ -237,24 +228,28 @@ const Init = {
237
228
  return true;
238
229
  },
239
230
  },
240
- ];
241
- responses = await new Promise((resolve) => {
242
- inquirer.prompt(questions).then((answers) => {
243
- resolve(answers);
244
- });
245
- });
231
+ ]);
246
232
  }
233
+ /* eslint-enable unicorn/prefer-ternary */
234
+
247
235
  if (responses.name && responses.email) {
248
236
  // name can contain spaces - wrap it in quotes
249
237
  const name = `"${responses.name.trim()}"`;
250
238
  const email = responses.email.trim();
251
- Util.execSync('git', ['config', '--local', 'user.name', name]);
252
- Util.execSync('git', ['config', '--local', 'user.email', email]);
239
+ try {
240
+ Util.execSync('git', ['config', '--local', 'user.name', name]);
241
+ Util.execSync('git', ['config', '--local', 'user.email', email]);
242
+ } catch (ex) {
243
+ // if project folder is not a git folder then using --local will lead to a fatal error
244
+ Util.logger.warn('- Could not update git user name and email');
245
+ Util.logger.debug(ex.message);
246
+ }
253
247
  }
254
248
  },
255
249
  /**
256
250
  * retrieves the global user.name and user.email values
257
- * @returns {Promise<{'user.name': String, 'user.email': String}>} user.name and user.email
251
+ *
252
+ * @returns {Promise.<{'user.name': string, 'user.email': string}>} user.name and user.email
258
253
  */
259
254
  async _getGitConfigUser() {
260
255
  const gitConfigs = await git.listConfig();
@@ -262,14 +257,14 @@ const Init = {
262
257
  delete gitConfigs.values['.git/config'];
263
258
  const result = {};
264
259
 
265
- Object.keys(gitConfigs.values).forEach((file) => {
260
+ for (const file of Object.keys(gitConfigs.values)) {
266
261
  if (gitConfigs.values[file]['user.name']) {
267
262
  result['user.name'] = gitConfigs.values[file]['user.name'];
268
263
  }
269
264
  if (gitConfigs.values[file]['user.email']) {
270
265
  result['user.email'] = gitConfigs.values[file]['user.email'];
271
266
  }
272
- });
267
+ }
273
268
  if (!result['user.name'] || !result['user.email']) {
274
269
  return null;
275
270
  }