mcdev 3.0.0 → 3.1.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 (50) hide show
  1. package/.eslintrc.json +1 -1
  2. package/.github/ISSUE_TEMPLATE/bug.yml +72 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  4. package/.github/ISSUE_TEMPLATE/task.md +10 -0
  5. package/.github/PULL_REQUEST_TEMPLATE.md +11 -0
  6. package/.github/workflows/npm-publish.yml +33 -0
  7. package/.issuetracker +11 -3
  8. package/.vscode/extensions.json +1 -2
  9. package/.vscode/settings.json +19 -4
  10. package/CHANGELOG.md +98 -0
  11. package/README.md +247 -142
  12. package/boilerplate/config.json +3 -2
  13. package/docs/dist/considerations.md +66 -0
  14. package/docs/dist/documentation.md +5794 -0
  15. package/lib/Deployer.js +4 -1
  16. package/lib/MetadataTypeDefinitions.js +1 -0
  17. package/lib/MetadataTypeInfo.js +1 -0
  18. package/lib/Retriever.js +32 -17
  19. package/lib/cli.js +295 -0
  20. package/lib/index.js +774 -1019
  21. package/lib/metadataTypes/AccountUser.js +389 -0
  22. package/lib/metadataTypes/Asset.js +140 -116
  23. package/lib/metadataTypes/Automation.js +119 -54
  24. package/lib/metadataTypes/DataExtension.js +172 -131
  25. package/lib/metadataTypes/DataExtensionField.js +134 -4
  26. package/lib/metadataTypes/Folder.js +66 -69
  27. package/lib/metadataTypes/ImportFile.js +4 -6
  28. package/lib/metadataTypes/MetadataType.js +168 -80
  29. package/lib/metadataTypes/Query.js +54 -25
  30. package/lib/metadataTypes/Role.js +13 -8
  31. package/lib/metadataTypes/Script.js +43 -24
  32. package/lib/metadataTypes/definitions/AccountUser.definition.js +227 -0
  33. package/lib/metadataTypes/definitions/Asset.definition.js +1 -0
  34. package/lib/metadataTypes/definitions/Campaign.definition.js +1 -1
  35. package/lib/metadataTypes/definitions/DataExtension.definition.js +1 -1
  36. package/lib/metadataTypes/definitions/DataExtensionField.definition.js +1 -1
  37. package/lib/metadataTypes/definitions/Folder.definition.js +1 -1
  38. package/lib/metadataTypes/definitions/ImportFile.definition.js +2 -1
  39. package/lib/metadataTypes/definitions/Script.definition.js +5 -5
  40. package/lib/retrieveChangelog.js +96 -0
  41. package/lib/util/cli.js +4 -6
  42. package/lib/util/init.config.js +3 -0
  43. package/lib/util/init.git.js +2 -1
  44. package/lib/util/util.js +35 -18
  45. package/package.json +20 -24
  46. package/test/util/file.js +51 -0
  47. package/img/README.md/troubleshoot-nodejs-postinstall.jpg +0 -0
  48. package/postinstall.js +0 -41
  49. package/test/deployer.js +0 -16
  50. package/test/util.js +0 -26
@@ -0,0 +1,389 @@
1
+ 'use strict';
2
+
3
+ const MetadataType = require('./MetadataType');
4
+ const Util = require('../util/util');
5
+ const File = require('../util/file');
6
+
7
+ /**
8
+ * MessageSendActivity MetadataType
9
+ * @augments MetadataType
10
+ */
11
+ class AccountUser extends MetadataType {
12
+ /**
13
+ * Retrieves SOAP based metadata of metadata type into local filesystem. executes callback with retrieved metadata
14
+ * @param {String} retrieveDir Directory where retrieved metadata directory will be saved
15
+ * @param {String[]} _ Returns specified fields even if their retrieve definition is not set to true
16
+ * @param {Object} buObject properties for auth
17
+ * @returns {Promise<Object>} Promise of metadata
18
+ */
19
+ static async retrieve(retrieveDir, _, buObject) {
20
+ if (buObject.eid !== buObject.mid) {
21
+ Util.logger.info('Skipping User retrieval on non-parent BU');
22
+ return;
23
+ }
24
+ Util.logger.info('- Caching dependent Metadata: AccountUserAccount');
25
+
26
+ // get BUs that each users have access to
27
+ const optionsBUs = {};
28
+ let resultsBatch;
29
+ await Util.retryOnError(`Retrying ${this.definition.type}`, async () => {
30
+ resultsBatch = await new Promise((resolve, reject) => {
31
+ this.client.SoapClient.retrieve(
32
+ 'AccountUserAccount',
33
+ [
34
+ 'AccountUser.AccountUserID',
35
+ 'AccountUser.UserID',
36
+ 'Account.ID',
37
+ 'Account.Name',
38
+ ],
39
+ optionsBUs,
40
+ (ex, response) => (ex ? reject(ex) : resolve(response.body.Results))
41
+ );
42
+ });
43
+ });
44
+ this.userIdBuMap = {};
45
+ resultsBatch.forEach((item) => {
46
+ this.userIdBuMap[item.AccountUser.AccountUserID] =
47
+ this.userIdBuMap[item.AccountUser.AccountUserID] || [];
48
+ this.userIdBuMap[item.AccountUser.AccountUserID].push({
49
+ ID: item.Account.ID,
50
+ Name: item.Account.Name,
51
+ });
52
+ });
53
+ // get actual user details
54
+ const options = {
55
+ queryAllAccounts: true,
56
+
57
+ filter: {
58
+ leftOperand: {
59
+ // normal users
60
+ leftOperand: 'Email',
61
+ operator: 'like',
62
+ rightOperand: '@',
63
+ },
64
+ operator: 'OR',
65
+ rightOperand: {
66
+ // installed packages
67
+ leftOperand: {
68
+ leftOperand: 'Name',
69
+ operator: 'like',
70
+ rightOperand: ' app user', // ! will not work if the name was too long as "app user" might be cut off
71
+ },
72
+ operator: 'AND',
73
+ rightOperand: {
74
+ // this is used to filter out system generated installed packages. in our testing, at least those installed packages created in the last few years have hat set this to false while additional (hidden) installed packages have it set to true.
75
+ leftOperand: 'MustChangePassword',
76
+ operator: 'equals',
77
+ rightOperand: 'false',
78
+ },
79
+ },
80
+ },
81
+ };
82
+
83
+ return super.retrieveSOAPgeneric(retrieveDir, buObject, options);
84
+ }
85
+ /**
86
+ *
87
+ * @param {string} date first date
88
+ * @param {string} date2 second date
89
+ * @returns {number} time difference
90
+ */
91
+ static timeSinceDate(date) {
92
+ const interval = 'days';
93
+ const second = 1000,
94
+ minute = second * 60,
95
+ hour = minute * 60,
96
+ day = hour * 24,
97
+ week = day * 7;
98
+ date = new Date(date);
99
+ const now = new Date();
100
+ const timediff = now - date;
101
+ if (isNaN(timediff)) {
102
+ return NaN;
103
+ }
104
+ let result;
105
+ switch (interval) {
106
+ case 'years':
107
+ result = now.getFullYear() - date.getFullYear();
108
+ break;
109
+ case 'months':
110
+ result =
111
+ now.getFullYear() * 12 +
112
+ now.getMonth() -
113
+ (date.getFullYear() * 12 + date.getMonth());
114
+ break;
115
+ case 'weeks':
116
+ result = Math.floor(timediff / week);
117
+ break;
118
+ case 'days':
119
+ result = Math.floor(timediff / day);
120
+ break;
121
+ case 'hours':
122
+ result = Math.floor(timediff / hour);
123
+ break;
124
+ case 'minutes':
125
+ result = Math.floor(timediff / minute);
126
+ break;
127
+ case 'seconds':
128
+ result = Math.floor(timediff / second);
129
+ break;
130
+ default:
131
+ return undefined;
132
+ }
133
+ return result + ' ' + interval;
134
+ }
135
+ /**
136
+ * helper to print bu names
137
+ * @param {Util.BuObject} buObject needed for eid
138
+ * @param {string} buObject.eid needed to check for parent bu
139
+ * @param {numeric} id bu id
140
+ * @returns {string} "bu name (bu id)""
141
+ */
142
+ static getBuName(buObject, id) {
143
+ let name;
144
+ if (buObject.eid == id) {
145
+ name = '_ParentBU_';
146
+ } else {
147
+ name = this.buIdName[id];
148
+ }
149
+ return `<nobr>${name} (${id})</nobr>`;
150
+ }
151
+ /**
152
+ * Creates markdown documentation of all roles
153
+ * @param {Util.BuObject} buObject properties for auth
154
+ * @param {Object} [metadata] user list
155
+ * @returns {Promise<void>} -
156
+ */
157
+ static async document(buObject, metadata) {
158
+ if (buObject.eid !== buObject.mid) {
159
+ Util.logger.error(
160
+ `Users can only be retrieved & documented for the ${Util.parentBuName}`
161
+ );
162
+ return;
163
+ }
164
+ if (!metadata) {
165
+ // load users from disk if document was called directly and not part of a retrieve
166
+ try {
167
+ metadata = this.readBUMetadataForType(
168
+ File.normalizePath([
169
+ this.properties.directories.retrieve,
170
+ buObject.credential,
171
+ Util.parentBuName,
172
+ ]),
173
+ true
174
+ ).accountUser;
175
+ } catch (ex) {
176
+ Util.logger.error(ex.message);
177
+ return;
178
+ }
179
+ }
180
+ // init map of BU Ids > BU Name
181
+ this.buIdName = {};
182
+
183
+ // initialize permission object
184
+ this.allPermissions = {};
185
+ const users = [];
186
+ // traverse all permissions recursively and write them into allPermissions object once it has reached the end
187
+ for (const id in metadata) {
188
+ const user = metadata[id];
189
+ // TODO resolve user permissions to something readable
190
+ let userPermissions = '';
191
+ if (user.UserPermissions) {
192
+ if (!user.UserPermissions.length) {
193
+ // 1 single user permission found, normalize it
194
+ user.UserPermissions = [user.UserPermissions];
195
+ }
196
+ userPermissions = user.UserPermissions.map((item) => item.ID * 1)
197
+ .sort(function (a, b) {
198
+ return a < b ? -1 : a > b ? 1 : 0;
199
+ })
200
+ .join(', ');
201
+ }
202
+ // user roles
203
+ // TODO think about what to do with "individual role" entries
204
+ let roles = '';
205
+ if (user.Roles) {
206
+ roles =
207
+ '<nobr>' +
208
+ user.Roles.map((item) => item.Name)
209
+ .sort(function (a, b) {
210
+ return a < b ? -1 : a > b ? 1 : 0;
211
+ })
212
+ .join(',</nobr><br> <nobr>') +
213
+ '</nobr>';
214
+ }
215
+ let associatedBus = '';
216
+ if (user.AssociatedBusinessUnits__c) {
217
+ associatedBus = user.AssociatedBusinessUnits__c.map((item) => {
218
+ this.buIdName[item.ID] = item.Name;
219
+ return this.getBuName(buObject, item.ID);
220
+ })
221
+ .sort(function (a, b) {
222
+ return a < b ? -1 : a > b ? 1 : 0;
223
+ })
224
+ .join(',<br> ');
225
+ }
226
+ const defaultBUName = this.getBuName(buObject, user.DefaultBusinessUnit);
227
+ users.push({
228
+ TYPE: user.type__c,
229
+ UserID: user.UserID,
230
+ AccountUserID: user.AccountUserID,
231
+ CustomerKey: user.CustomerKey,
232
+ Name: user.Name,
233
+ Email: user.Email,
234
+ NotificationEmailAddress: user.NotificationEmailAddress,
235
+ ActiveFlag: user.ActiveFlag === 'true' ? '✓' : '-',
236
+ IsAPIUser: user.IsAPIUser === 'true' ? '✓' : '-',
237
+ MustChangePassword: user.MustChangePassword === 'true' ? '✓' : '-',
238
+ DefaultBusinessUnit: defaultBUName,
239
+ AssociatedBusinessUnits__c: associatedBus,
240
+ Roles: roles,
241
+ UserPermissions: userPermissions,
242
+ LastSuccessfulLogin: this.timeSinceDate(user.LastSuccessfulLogin),
243
+ CreatedDate: user.CreatedDate.split('T').join(' '),
244
+ ModifiedDate: user.ModifiedDate.split('T').join(' '),
245
+ });
246
+ }
247
+ users.sort(function (a, b) {
248
+ return a.Name < b.Name ? -1 : a.Name > b.Name ? 1 : 0;
249
+ });
250
+ const columnsToPrint = [
251
+ ['Name', 'Name'],
252
+ ['Last successful Login', 'LastSuccessfulLogin'],
253
+ ['Active', 'ActiveFlag'],
254
+ ['API User', 'IsAPIUser'],
255
+ ['Must change PW', 'MustChangePassword'],
256
+ ['Default BU', 'DefaultBusinessUnit'],
257
+ ['BU Access', 'AssociatedBusinessUnits__c'],
258
+ ['Roles', 'Roles'],
259
+ ['User Permissions', 'UserPermissions'],
260
+ ['Login', 'UserID'],
261
+ ['ID', 'AccountUserID'],
262
+ ['Key', 'CustomerKey'],
263
+ ['E-Mail', 'Email'],
264
+ ['Notification E-Mail', 'NotificationEmailAddress'],
265
+ ['Modified Date', 'ModifiedDate'],
266
+ ['Created Date', 'CreatedDate'],
267
+ ];
268
+ let output = `# User Overview - ${buObject.credential}`;
269
+ output += this._generateDocMd(
270
+ users.filter((user) => user.TYPE === 'User' && user.ActiveFlag === '✓'),
271
+ 'User',
272
+ columnsToPrint
273
+ );
274
+ output += this._generateDocMd(
275
+ users.filter((user) => user.TYPE === 'User' && user.ActiveFlag === '-'),
276
+ 'Inactivated User',
277
+ columnsToPrint
278
+ );
279
+ output += this._generateDocMd(
280
+ users.filter((user) => user.TYPE === 'Installed Package'),
281
+ 'Installed Package',
282
+ columnsToPrint
283
+ );
284
+ const docPath = File.normalizePath([this.properties.directories.users]);
285
+
286
+ try {
287
+ const filename = buObject.credential;
288
+ // ensure docs/roles folder is existing (depends on setup in .mcdevrc.json)
289
+ if (!File.existsSync(docPath)) {
290
+ File.mkdirpSync(docPath);
291
+ }
292
+ // write to disk
293
+ await File.writeToFile(docPath, filename + '.accountUser', 'md', output);
294
+ Util.logger.info(`Created ${docPath}${filename}.accountUser.md`);
295
+ if (['html', 'both'].includes(this.properties.options.documentType)) {
296
+ Util.logger.warn(
297
+ 'HTML-based documentation of accountUser currently not supported.'
298
+ );
299
+ }
300
+ } catch (ex) {
301
+ Util.logger.error(`AccountUser.document():: error | `, ex.message);
302
+ }
303
+ }
304
+ /**
305
+ * Experimental: Only working for DataExtensions:
306
+ * Saves json content to a html table in the local file system. Will create the parent directory if it does not exist.
307
+ * The json's first level of keys must represent the rows and the secend level the columns
308
+ * @private
309
+ * @param {DataExtensionItem} json dataextension
310
+ * @param {Array} tabled prepped array for output in tabular format
311
+ * @returns {string} file content
312
+ */
313
+ /**
314
+ *
315
+ * @param {Object[]} users list of users and installed package
316
+ * @param {'Installed Package'|'User'} type choose what sub type to print
317
+ * @param {Array[]} columnsToPrint helper array
318
+ * @param {Object} buObject properties for auth
319
+ * @returns {string} markdown
320
+ */
321
+ static _generateDocMd(users, type, columnsToPrint) {
322
+ let output = `\n\n## ${type}s (${users.length})\n\n`;
323
+ let tableSeparator = '';
324
+ columnsToPrint.forEach((column) => {
325
+ output += `| ${column[0]} `;
326
+ tableSeparator += '| --- ';
327
+ });
328
+ output += `|\n${tableSeparator}|\n`;
329
+ users.forEach((user) => {
330
+ columnsToPrint.forEach((column) => {
331
+ output += `| ${user[column[1]]} `;
332
+ });
333
+ output += `|\n`;
334
+ });
335
+ return output;
336
+ }
337
+
338
+ /**
339
+ * manages post retrieve steps
340
+ * @param {Object} metadata a single query
341
+ * @returns {Object[]} Array with one metadata object and one query string
342
+ */
343
+ static postRetrieveTasks(metadata) {
344
+ return this.parseMetadata(metadata);
345
+ }
346
+ /**
347
+ * parses retrieved Metadata before saving
348
+ * @param {Object} metadata a single query activity definition
349
+ * @returns {Array} Array with one metadata object and one sql string
350
+ */
351
+ static parseMetadata(metadata) {
352
+ metadata.type__c = 'Installed Package';
353
+ if (metadata.Email.includes('@') && !metadata.Name.endsWith('app user')) {
354
+ metadata.type__c = 'User';
355
+ }
356
+
357
+ if (this.userIdBuMap[metadata.ID]) {
358
+ metadata.AssociatedBusinessUnits__c = this.userIdBuMap[metadata.ID];
359
+ } else {
360
+ metadata.AssociatedBusinessUnits__c = [];
361
+ }
362
+
363
+ let roles;
364
+ if (metadata.Roles.Role) {
365
+ // normalize to always use array
366
+ if (!metadata.Roles.Role.length) {
367
+ metadata.Roles.Role = [metadata.Roles.Role];
368
+ }
369
+ // convert complex object into basic set of info
370
+ roles = metadata.Roles.Role.map((item) => ({
371
+ Name: item.Name,
372
+ CustomerKey: item.CustomerKey,
373
+ })).sort(function (a, b) {
374
+ return a.Name < b.Name ? -1 : a.Name > b.Name ? 1 : 0;
375
+ });
376
+ } else {
377
+ // set to empty array
378
+ roles = [];
379
+ }
380
+ metadata.Roles = roles;
381
+
382
+ return metadata;
383
+ }
384
+ }
385
+
386
+ // Assign definition to static attributes
387
+ AccountUser.definition = require('../MetadataTypeDefinitions').accountUser;
388
+
389
+ module.exports = AccountUser;