mcdev 4.3.4 → 5.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.
- package/.coverage-comment-template.md +20 -0
- package/.coverage-comment-template.svelte +178 -0
- package/.eslintrc.json +2 -0
- package/.github/ISSUE_TEMPLATE/bug.yml +1 -0
- package/.github/workflows/code-test.yml +36 -0
- package/.github/workflows/coverage-base-update.yml +57 -0
- package/.github/workflows/coverage-develop-branch.yml +41 -0
- package/.github/workflows/coverage-main-branch.yml +41 -0
- package/.github/workflows/coverage.yml +77 -0
- package/.husky/post-checkout +1 -1
- package/.prettierrc +1 -1
- package/.vscode/extensions.json +0 -4
- package/boilerplate/config.json +1 -1
- package/boilerplate/files/.prettierrc +1 -1
- package/boilerplate/files/.vscode/extensions.json +1 -1
- package/boilerplate/forcedUpdates.json +4 -0
- package/docs/dist/documentation.md +1196 -430
- package/lib/Builder.js +6 -1
- package/lib/Deployer.js +30 -5
- package/lib/MetadataTypeDefinitions.js +8 -6
- package/lib/MetadataTypeInfo.js +8 -6
- package/lib/cli.js +54 -42
- package/lib/index.js +82 -8
- package/lib/metadataTypes/Asset.js +73 -1
- package/lib/metadataTypes/AttributeGroup.js +0 -1
- package/lib/metadataTypes/Automation.js +48 -5
- package/lib/metadataTypes/Campaign.js +20 -7
- package/lib/metadataTypes/ContentArea.js +1 -1
- package/lib/metadataTypes/DataExtension.js +221 -184
- package/lib/metadataTypes/DataExtensionField.js +12 -19
- package/lib/metadataTypes/DataExtensionTemplate.js +1 -1
- package/lib/metadataTypes/DataExtract.js +1 -1
- package/lib/metadataTypes/DataExtractType.js +1 -1
- package/lib/metadataTypes/Email.js +1 -1
- package/lib/metadataTypes/{EmailSendDefinition.js → EmailSend.js} +5 -5
- package/lib/metadataTypes/{EventDefinition.js → Event.js} +17 -35
- package/lib/metadataTypes/{FtpLocation.js → FileLocation.js} +2 -2
- package/lib/metadataTypes/FileTransfer.js +8 -7
- package/lib/metadataTypes/Filter.js +1 -1
- package/lib/metadataTypes/Folder.js +8 -3
- package/lib/metadataTypes/ImportFile.js +6 -6
- package/lib/metadataTypes/{Interaction.js → Journey.js} +311 -147
- package/lib/metadataTypes/List.js +2 -2
- package/lib/metadataTypes/MetadataType.js +318 -90
- package/lib/metadataTypes/MobileCode.js +0 -1
- package/lib/metadataTypes/MobileKeyword.js +336 -40
- package/lib/metadataTypes/MobileMessage.js +473 -0
- package/lib/metadataTypes/Query.js +114 -32
- package/lib/metadataTypes/Role.js +60 -21
- package/lib/metadataTypes/Script.js +2 -3
- package/lib/metadataTypes/SendClassification.js +40 -0
- package/lib/metadataTypes/SetDefinition.js +1 -7
- package/lib/metadataTypes/TransactionalEmail.js +2 -3
- package/lib/metadataTypes/TransactionalMessage.js +1 -2
- package/lib/metadataTypes/TransactionalSMS.js +8 -15
- package/lib/metadataTypes/{TriggeredSendDefinition.js → TriggeredSend.js} +35 -27
- package/lib/metadataTypes/User.js +1177 -0
- package/lib/metadataTypes/definitions/Asset.definition.js +1 -0
- package/lib/metadataTypes/definitions/AttributeGroup.definition.js +1 -0
- package/lib/metadataTypes/definitions/Automation.definition.js +3 -2
- package/lib/metadataTypes/definitions/Campaign.definition.js +79 -4
- package/lib/metadataTypes/definitions/ContentArea.definition.js +1 -0
- package/lib/metadataTypes/definitions/DataExtension.definition.js +2 -1
- package/lib/metadataTypes/definitions/DataExtensionField.definition.js +1 -0
- package/lib/metadataTypes/definitions/DataExtensionTemplate.definition.js +1 -0
- package/lib/metadataTypes/definitions/DataExtract.definition.js +1 -0
- package/lib/metadataTypes/definitions/DataExtractType.definition.js +1 -0
- package/lib/metadataTypes/definitions/Discovery.definition.js +1 -0
- package/lib/metadataTypes/definitions/Email.definition.js +1 -0
- package/lib/metadataTypes/definitions/{EmailSendDefinition.definition.js → EmailSend.definition.js} +4 -2
- package/lib/metadataTypes/definitions/{EventDefinition.definition.js → Event.definition.js} +2 -1
- package/lib/metadataTypes/definitions/{FtpLocation.definition.js → FileLocation.definition.js} +4 -3
- package/lib/metadataTypes/definitions/FileTransfer.definition.js +3 -2
- package/lib/metadataTypes/definitions/Filter.definition.js +1 -0
- package/lib/metadataTypes/definitions/Folder.definition.js +2 -0
- package/lib/metadataTypes/definitions/ImportFile.definition.js +4 -3
- package/lib/metadataTypes/definitions/{Interaction.definition.js → Journey.definition.js} +11 -2
- package/lib/metadataTypes/definitions/List.definition.js +1 -0
- package/lib/metadataTypes/definitions/MobileCode.definition.js +3 -1
- package/lib/metadataTypes/definitions/MobileKeyword.definition.js +27 -17
- package/lib/metadataTypes/definitions/MobileMessage.definition.js +743 -0
- package/lib/metadataTypes/definitions/Query.definition.js +3 -2
- package/lib/metadataTypes/definitions/Role.definition.js +5 -0
- package/lib/metadataTypes/definitions/Script.definition.js +1 -0
- package/lib/metadataTypes/definitions/SendClassification.definition.js +114 -0
- package/lib/metadataTypes/definitions/SetDefinition.definition.js +1 -0
- package/lib/metadataTypes/definitions/TransactionalEmail.definition.js +2 -1
- package/lib/metadataTypes/definitions/TransactionalPush.definition.js +1 -0
- package/lib/metadataTypes/definitions/TransactionalSMS.definition.js +1 -0
- package/lib/metadataTypes/definitions/{TriggeredSendDefinition.definition.js → TriggeredSend.definition.js} +5 -3
- package/lib/metadataTypes/definitions/User.definition.js +365 -0
- package/lib/retrieveChangelog.js +1 -2
- package/lib/util/auth.js +29 -9
- package/lib/util/businessUnit.js +3 -3
- package/lib/util/cli.js +55 -7
- package/lib/util/devops.js +6 -4
- package/lib/util/file.js +55 -13
- package/lib/util/init.config.js +1 -2
- package/lib/util/init.npm.js +3 -3
- package/lib/util/util.js +23 -14
- package/package.json +16 -15
- package/test/general.test.js +62 -0
- package/test/mockRoot/.mcdevrc.json +7 -5
- package/test/mockRoot/deploy/testInstance/_ParentBU_/user/testBlocked_user.user-meta.json +23 -0
- package/test/mockRoot/deploy/testInstance/_ParentBU_/user/testExisting_user.user-meta.json +31 -0
- package/test/mockRoot/deploy/testInstance/_ParentBU_/user/testNew_user.user-meta.json +27 -0
- package/test/mockRoot/deploy/testInstance/testBU/dataExtension/{childBU_dataextension_test.dataExtension-meta.json → testExisting_dataExtension.dataExtension-meta.json} +2 -2
- package/test/mockRoot/deploy/testInstance/testBU/dataExtension/{testDataExtension.dataExtension-meta.json → testNew_dataExtension.dataExtension-meta.json} +2 -2
- package/test/mockRoot/deploy/testInstance/testBU/journey/testExisting_interaction.interaction-meta.json +576 -0
- package/test/mockRoot/deploy/testInstance/testBU/mobileKeyword/testNew_keyword.mobileKeyword-meta.amp +2 -0
- package/test/mockRoot/deploy/testInstance/testBU/mobileKeyword/testNew_keyword.mobileKeyword-meta.json +10 -0
- package/test/mockRoot/deploy/testInstance/testBU/mobileKeyword/testNew_keyword_blocked.mobileKeyword-meta.amp +2 -0
- package/test/mockRoot/deploy/testInstance/testBU/mobileKeyword/testNew_keyword_blocked.mobileKeyword-meta.json +10 -0
- package/test/mockRoot/deploy/testInstance/testBU/mobileMessage/NTIzOjc4OjA.mobileMessage-meta.amp +1 -0
- package/test/mockRoot/deploy/testInstance/testBU/mobileMessage/NTIzOjc4OjA.mobileMessage-meta.json +61 -0
- package/test/mockRoot/deploy/testInstance/testBU/mobileMessage/new.mobileMessage-meta.amp +1 -0
- package/test/mockRoot/deploy/testInstance/testBU/mobileMessage/new.mobileMessage-meta.json +60 -0
- package/test/mockRoot/deploy/testInstance/testBU/query/testExistingQuery.query-meta.json +1 -1
- package/test/mockRoot/deploy/testInstance/testBU/query/testNewQuery.query-meta.json +1 -1
- package/test/mockRoot/deploy/testInstance/testBU/query/testNewQuery.query-meta.sql +1 -1
- package/test/mockRoot/deploy/testInstance/testBU/transactionalEmail/testExisting_temail.transactionalEmail-meta.json +1 -1
- package/test/mockRoot/deploy/testInstance/testBU/transactionalEmail/testNew_temail.transactionalEmail-meta.json +1 -1
- package/test/resourceFactory.js +13 -0
- package/test/resources/1111111/accountUser/configure-response.xml +70 -0
- package/test/resources/1111111/accountUser/create-response.xml +97 -0
- package/test/resources/1111111/accountUser/retrieve-response.xml +156 -0
- package/test/resources/1111111/accountUser/update-response.xml +111 -0
- package/test/resources/1111111/accountUserAccount/retrieve-response.xml +77 -0
- package/test/resources/1111111/platform/v1/setup/quickflow/data/get-response.json +455 -0
- package/test/resources/1111111/role/retrieve-response.xml +76 -0
- package/test/resources/1111111/user/build-expected.json +16 -0
- package/test/resources/1111111/user/create-expected.json +21 -0
- package/test/resources/1111111/user/retrieve-expected.json +24 -0
- package/test/resources/1111111/user/template-expected.json +16 -0
- package/test/resources/1111111/user/update-expected.json +21 -0
- package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dat/delete-response.json +1 -0
- package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dat/get-response.json +17 -0
- package/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dat/patch-response.json +3 -3
- package/test/resources/9999999/automation/v1/queries/get-response.json +21 -4
- package/test/resources/9999999/automation/v1/queries/post-response.json +4 -4
- package/test/resources/9999999/data/v1/customobjectdata/key/{childBU_dataextension_test → testExisting_dataExtension}/rowset/get-response.json +1 -1
- package/test/resources/9999999/dataExtension/build-expected.json +3 -3
- package/test/resources/9999999/dataExtension/create-expected.json +2 -2
- package/test/resources/9999999/dataExtension/create-response.xml +8 -3
- package/test/resources/9999999/dataExtension/retrieve-expected.json +3 -3
- package/test/resources/9999999/dataExtension/retrieve-response.xml +9 -4
- package/test/resources/9999999/dataExtension/template-expected.json +3 -3
- package/test/resources/9999999/dataExtension/update-expected.json +3 -3
- package/test/resources/9999999/dataExtension/update-response.xml +9 -4
- package/test/resources/9999999/dataExtensionField/retrieve-response.xml +14 -9
- package/test/resources/9999999/interaction/v1/interactions/get-response.json +312 -0
- package/test/resources/9999999/interaction/v1/interactions/key_testExisting_interaction/get-response.json +312 -0
- package/test/resources/9999999/interaction/v1/interactions/key_testExisting_interaction/put-response.json +592 -0
- package/test/resources/9999999/journey/build-expected.json +572 -0
- package/test/resources/9999999/journey/get-expected.json +576 -0
- package/test/resources/9999999/journey/put-expected.json +576 -0
- package/test/resources/9999999/journey/template-expected.json +572 -0
- package/test/resources/9999999/legacy/v1/beta/mobile/keyword/NXV4ZFMwTEFwRVczd3RaLUF5X3p5dzo4Njow/get-response.json +42 -0
- package/test/resources/9999999/legacy/v1/beta/mobile/keyword/cTVJaG5oSDJPVUNHcUh6Z3pQT2tVdzo4Njow/delete-response.json +0 -0
- package/test/resources/9999999/legacy/v1/beta/mobile/keyword/get-response.json +1 -0
- package/test/resources/9999999/legacy/v1/beta/mobile/keyword/post-response.json +3 -0
- package/test/resources/9999999/legacy/v1/beta/mobile/message/NTIzOjc4OjA/delete-response.json +0 -0
- package/test/resources/9999999/legacy/v1/beta/mobile/message/NTIzOjc4OjA/get-response.json +106 -0
- package/test/resources/9999999/legacy/v1/beta/mobile/message/NTIzOjc4OjA/post-response.json +0 -0
- package/test/resources/9999999/legacy/v1/beta/mobile/message/NTQ3Ojc4OjA/get-response.json +127 -0
- package/test/resources/9999999/legacy/v1/beta/mobile/message/get-response.json +129 -0
- package/test/resources/9999999/legacy/v1/beta/mobile/message/post-response.json +3 -0
- package/test/resources/9999999/legacy/v1/beta2/data/campaign/get-response.json +29 -0
- package/test/resources/9999999/messaging/v1/email/definitions/post-response.json +1 -1
- package/test/resources/9999999/messaging/v1/email/definitions/testExisting_temail/get-response.json +1 -1
- package/test/resources/9999999/messaging/v1/email/definitions/testExisting_temail/patch-response.json +1 -1
- package/test/resources/9999999/mobileKeyword/build-expected.amp +2 -0
- package/test/resources/9999999/mobileKeyword/build-expected.json +9 -0
- package/test/resources/9999999/mobileKeyword/get-expected.amp +2 -0
- package/test/resources/9999999/mobileKeyword/get-expected.json +15 -0
- package/test/resources/9999999/mobileKeyword/post-create-expected.amp +2 -0
- package/test/resources/9999999/mobileKeyword/post-create-expected.json +17 -0
- package/test/resources/9999999/mobileKeyword/template-expected.amp +2 -0
- package/test/resources/9999999/mobileKeyword/template-expected.json +9 -0
- package/test/resources/9999999/mobileMessage/build-expected.amp +1 -0
- package/test/resources/9999999/mobileMessage/build-expected.json +60 -0
- package/test/resources/9999999/mobileMessage/get-expected.amp +1 -0
- package/test/resources/9999999/mobileMessage/get-expected.json +61 -0
- package/test/resources/9999999/mobileMessage/post-create-expected.amp +1 -0
- package/test/resources/9999999/mobileMessage/post-create-expected.json +63 -0
- package/test/resources/9999999/mobileMessage/post-update-expected.amp +1 -0
- package/test/resources/9999999/mobileMessage/post-update-expected.json +61 -0
- package/test/resources/9999999/mobileMessage/template-expected.amp +1 -0
- package/test/resources/9999999/mobileMessage/template-expected.json +60 -0
- package/test/resources/9999999/query/build-expected.json +1 -1
- package/test/resources/9999999/query/get-expected.json +1 -1
- package/test/resources/9999999/query/get2-expected.json +11 -0
- package/test/resources/9999999/query/patch-expected.json +1 -1
- package/test/resources/9999999/query/post-expected.json +1 -1
- package/test/resources/9999999/query/template-expected.json +1 -1
- package/test/resources/9999999/queryDefinition/retrieve-response.xml +30 -0
- package/test/resources/9999999/transactionalEmail/build-expected.json +5 -5
- package/test/resources/9999999/transactionalEmail/get-expected.json +1 -1
- package/test/resources/9999999/transactionalEmail/patch-expected.json +1 -1
- package/test/resources/9999999/transactionalEmail/post-expected.json +1 -1
- package/test/resources/9999999/transactionalEmail/template-expected.json +5 -5
- package/test/resources/9999999/transactionalPush/build-expected.json +2 -2
- package/test/resources/9999999/transactionalPush/template-expected.json +2 -2
- package/test/resources/9999999/transactionalSMS/build-expected.json +3 -3
- package/test/resources/9999999/transactionalSMS/template-expected.json +3 -3
- package/test/{dataExtension.test.js → type.dataExtension.test.js} +78 -21
- package/test/{interaction.test.js → type.journey.test.js} +64 -30
- package/test/type.mobileKeyword.test.js +250 -0
- package/test/type.mobileMessage.test.js +205 -0
- package/test/{query.test.js → type.query.test.js} +102 -5
- package/test/{transactionalEmail.test.js → type.transactionalEmail.test.js} +40 -2
- package/test/{transactionalPush.test.js → type.transactionalPush.test.js} +41 -2
- package/test/{transactionalSMS.test.js → type.transactionalSMS.test.js} +73 -3
- package/test/type.user.test.js +160 -0
- package/test/utils.js +17 -5
- package/types/mcdev.d.js +48 -15
- package/.github/workflows/code-analysis.yml +0 -57
- package/lib/metadataTypes/AccountUser.js +0 -426
- package/lib/metadataTypes/definitions/AccountUser.definition.js +0 -227
- package/test/mockRoot/deploy/testInstance/testBU/interaction/testExisting_interaction.interaction-meta.json +0 -266
- package/test/resources/9999999/interaction/build-expected.json +0 -260
- package/test/resources/9999999/interaction/get-expected.json +0 -264
- package/test/resources/9999999/interaction/put-expected.json +0 -264
- package/test/resources/9999999/interaction/template-expected.json +0 -260
- package/test/resources/9999999/interaction/v1/interactions/put-response.json +0 -280
- /package/test/mockRoot/deploy/testInstance/testBU/{interaction → journey}/testNew_interaction.interaction-meta.json +0 -0
- /package/test/resources/9999999/{interaction → journey}/post-expected.json +0 -0
|
@@ -0,0 +1,1177 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const TYPE = require('../../types/mcdev.d');
|
|
4
|
+
const MetadataType = require('./MetadataType');
|
|
5
|
+
const Util = require('../util/util');
|
|
6
|
+
const File = require('../util/file');
|
|
7
|
+
const cache = require('../util/cache');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* MetadataType
|
|
11
|
+
*
|
|
12
|
+
* @augments MetadataType
|
|
13
|
+
*/
|
|
14
|
+
class User extends MetadataType {
|
|
15
|
+
/**
|
|
16
|
+
* Retrieves SOAP based metadata of metadata type into local filesystem. executes callback with retrieved metadata
|
|
17
|
+
*
|
|
18
|
+
* @param {string} retrieveDir Directory where retrieved metadata directory will be saved
|
|
19
|
+
* @param {void} _ unused parameter
|
|
20
|
+
* @param {void} [__] unused parameter
|
|
21
|
+
* @param {string} [key] customer key of single item to retrieve
|
|
22
|
+
* @returns {Promise.<TYPE.MetadataTypeMapObj>} Promise of metadata
|
|
23
|
+
*/
|
|
24
|
+
static async retrieve(retrieveDir, _, __, key) {
|
|
25
|
+
if (this.buObject.eid !== this.buObject.mid) {
|
|
26
|
+
Util.logger.info(' - Skipping User retrieval on non-parent BU');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
return this._retrieve(retrieveDir, key);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Retrieves import definition metadata for caching
|
|
33
|
+
*
|
|
34
|
+
* @returns {Promise.<TYPE.MetadataTypeMapObj>} Promise
|
|
35
|
+
*/
|
|
36
|
+
static async retrieveForCache() {
|
|
37
|
+
return this.retrieve(null);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a single item.
|
|
42
|
+
*
|
|
43
|
+
* @param {TYPE.MetadataTypeItem} metadata single metadata entry
|
|
44
|
+
* @returns {Promise} Promise
|
|
45
|
+
*/
|
|
46
|
+
static async create(metadata) {
|
|
47
|
+
if (this.buObject.eid !== this.buObject.mid) {
|
|
48
|
+
Util.logger.info(' - Skipping User creation on non-parent BU');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
return super.createSOAP(metadata);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Updates a single item.
|
|
56
|
+
*
|
|
57
|
+
* @param {TYPE.MetadataTypeItem} metadata single metadata entry
|
|
58
|
+
* @returns {Promise} Promise
|
|
59
|
+
*/
|
|
60
|
+
static async update(metadata) {
|
|
61
|
+
if (this.buObject.eid !== this.buObject.mid) {
|
|
62
|
+
Util.logger.info(' - Skipping User update on non-parent BU');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
return super.updateSOAP(metadata);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* prepares a item for deployment
|
|
69
|
+
*
|
|
70
|
+
* @param {TYPE.UserDocument} metadata of a single item
|
|
71
|
+
* @returns {TYPE.UserDocument} metadata object
|
|
72
|
+
*/
|
|
73
|
+
static async preDeployTasks(metadata) {
|
|
74
|
+
metadata.Client = {
|
|
75
|
+
ID: this.buObject.mid,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// convert roles into API compliant format
|
|
79
|
+
if (metadata.c__RoleNamesGlobal?.length) {
|
|
80
|
+
metadata.Roles = {
|
|
81
|
+
Role: metadata.c__RoleNamesGlobal
|
|
82
|
+
.filter(
|
|
83
|
+
// individual role (which are not manageable nor visible in the GUI)
|
|
84
|
+
(roleName) => !roleName.startsWith('Individual role for ')
|
|
85
|
+
)
|
|
86
|
+
.map((roleName) => {
|
|
87
|
+
let roleCache;
|
|
88
|
+
try {
|
|
89
|
+
const roleKey = cache.searchForField(
|
|
90
|
+
'role',
|
|
91
|
+
roleName,
|
|
92
|
+
'Name',
|
|
93
|
+
'CustomerKey'
|
|
94
|
+
);
|
|
95
|
+
roleCache = cache.getByKey('role', roleKey);
|
|
96
|
+
} catch {
|
|
97
|
+
// skip this role in case of errors
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (roleCache?.c__notAssignable) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Default roles starting on 'Marketing Cloud' are not assignable via API and get removed upon update. Please create/update the user manually in the GUI or remove that role.`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
ObjectID: roleCache.ObjectID,
|
|
107
|
+
Name: roleName,
|
|
108
|
+
};
|
|
109
|
+
})
|
|
110
|
+
.filter(Boolean),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
delete metadata.c__RoleNamesGlobal;
|
|
114
|
+
|
|
115
|
+
// check if DefaultBusinessUnit is listed in AssociatedBUs
|
|
116
|
+
if (!metadata.c__AssociatedBusinessUnits.includes(metadata.DefaultBusinessUnit)) {
|
|
117
|
+
metadata.c__AssociatedBusinessUnits.push(metadata.DefaultBusinessUnit);
|
|
118
|
+
Util.logger.info(
|
|
119
|
+
Util.getGrayMsg(
|
|
120
|
+
` - adding DefaultBusinessUnit to list of associated Business Units (${metadata.CustomerKey} / ${metadata.Name}): ${metadata.DefaultBusinessUnit}`
|
|
121
|
+
)
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (metadata.c__AssociatedBusinessUnits.length) {
|
|
125
|
+
// ensure we do not have duplicates in the list - could happen due to user error or due to templating
|
|
126
|
+
metadata.c__AssociatedBusinessUnits = [...new Set(metadata.c__AssociatedBusinessUnits)];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Timezone
|
|
130
|
+
if (metadata.c__TimeZoneName) {
|
|
131
|
+
// find the ID of the timezone
|
|
132
|
+
metadata.TimeZone = { Name: metadata.c__TimeZoneName };
|
|
133
|
+
metadata.TimeZone.ID = cache.searchForField(
|
|
134
|
+
'_timezone',
|
|
135
|
+
metadata.c__TimeZoneName,
|
|
136
|
+
'description',
|
|
137
|
+
'id'
|
|
138
|
+
);
|
|
139
|
+
delete metadata.c__TimeZoneName;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Locale
|
|
143
|
+
if (metadata.c__LocaleCode) {
|
|
144
|
+
// we cannot easily confirm if hte code is valid but SFMC's API will likely throw an error if not
|
|
145
|
+
metadata.Locale = { LocaleCode: metadata.c__LocaleCode };
|
|
146
|
+
delete metadata.c__LocaleCode;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// convert SSO / Federation Token into API compliant format
|
|
150
|
+
if (metadata.SsoIdentity || metadata.SsoIdentities) {
|
|
151
|
+
const ssoIdentity = {};
|
|
152
|
+
if (metadata.SsoIdentity) {
|
|
153
|
+
// assume metadata.SsoIdentity is an object
|
|
154
|
+
ssoIdentity.IsActive = metadata.SsoIdentity.IsActive;
|
|
155
|
+
ssoIdentity.FederatedId = metadata.SsoIdentity.FederatedId;
|
|
156
|
+
delete metadata.SsoIdentity;
|
|
157
|
+
} else if (Array.isArray(metadata.SsoIdentities)) {
|
|
158
|
+
// be nice and allow SsoIdentities as an alternative if its an array of objects
|
|
159
|
+
ssoIdentity.IsActive = metadata.SsoIdentities[0].IsActive;
|
|
160
|
+
ssoIdentity.FederatedId = metadata.SsoIdentities[0].FederatedId;
|
|
161
|
+
} else if (
|
|
162
|
+
Array.isArray(metadata.SsoIdentities?.SsoIdentity) &&
|
|
163
|
+
metadata.SsoIdentities?.SsoIdentity.length
|
|
164
|
+
) {
|
|
165
|
+
// API-compliant format already provided; just use it
|
|
166
|
+
ssoIdentity.IsActive = metadata.SsoIdentities.SsoIdentity[0]?.IsActive;
|
|
167
|
+
ssoIdentity.FederatedId = metadata.SsoIdentities.SsoIdentity[0]?.FederatedId;
|
|
168
|
+
} else {
|
|
169
|
+
throw new TypeError(
|
|
170
|
+
'SsoIdentity should be an object with IsActive and FederatedId properties.'
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
// if SsoIdentity is set, assume this was on purpose and bring it
|
|
174
|
+
metadata.SsoIdentities = {
|
|
175
|
+
SsoIdentity: [
|
|
176
|
+
{
|
|
177
|
+
IsActive: ssoIdentity.IsActive,
|
|
178
|
+
FederatedId: ssoIdentity.FederatedId,
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
delete metadata.c__type;
|
|
185
|
+
delete metadata.c__AccountUserID;
|
|
186
|
+
delete metadata.c__IsLocked_readOnly;
|
|
187
|
+
|
|
188
|
+
return metadata;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* helper for {@link MetadataType.upsert}
|
|
192
|
+
*
|
|
193
|
+
* @param {TYPE.MetadataTypeMap} metadata list of metadata
|
|
194
|
+
* @param {string} metadataKey key of item we are looking at
|
|
195
|
+
* @param {boolean} hasError error flag from previous code
|
|
196
|
+
* @param {TYPE.UserDocumentDiff[]} metadataToUpdate list of items to update
|
|
197
|
+
* @param {TYPE.UserDocument[]} metadataToCreate list of items to create
|
|
198
|
+
* @returns {void}
|
|
199
|
+
*/
|
|
200
|
+
static createOrUpdate(metadata, metadataKey, hasError, metadataToUpdate, metadataToCreate) {
|
|
201
|
+
const action = super.createOrUpdate(
|
|
202
|
+
metadata,
|
|
203
|
+
metadataKey,
|
|
204
|
+
hasError,
|
|
205
|
+
metadataToUpdate,
|
|
206
|
+
metadataToCreate
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
if (action === 'create') {
|
|
210
|
+
const createItem = metadataToCreate[metadataToCreate.length - 1];
|
|
211
|
+
User._setPasswordForNewUser(createItem);
|
|
212
|
+
User._prepareRoleAssignments({ after: createItem });
|
|
213
|
+
User._prepareBuAssignments(metadata[metadataKey], null, createItem);
|
|
214
|
+
} else if (action === 'update') {
|
|
215
|
+
const updateItem = metadataToUpdate[metadataToUpdate.length - 1];
|
|
216
|
+
User._prepareRoleAssignments(updateItem);
|
|
217
|
+
User._prepareBuAssignments(metadata[metadataKey], updateItem, null);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
*
|
|
223
|
+
* @private
|
|
224
|
+
* @param {TYPE.MetadataTypeItem} metadata single metadata itme
|
|
225
|
+
* @param {TYPE.UserDocumentDiff} [updateItem] item to update
|
|
226
|
+
* @param {TYPE.UserDocument} [createItem] item to create
|
|
227
|
+
*/
|
|
228
|
+
static _prepareBuAssignments(metadata, updateItem, createItem) {
|
|
229
|
+
this.userBUassignments ||= { add: {}, delete: {} };
|
|
230
|
+
if (updateItem) {
|
|
231
|
+
// remove business units that were unassigned
|
|
232
|
+
const deletedBUs = [];
|
|
233
|
+
updateItem.before.c__AssociatedBusinessUnits =
|
|
234
|
+
this.userIdBuMap[updateItem.before.ID] || [];
|
|
235
|
+
for (const oldBuAssignment of updateItem.before.c__AssociatedBusinessUnits) {
|
|
236
|
+
// check if oldRole is missing in list of new roles
|
|
237
|
+
if (!updateItem.after.c__AssociatedBusinessUnits.includes(oldBuAssignment)) {
|
|
238
|
+
deletedBUs.push(oldBuAssignment);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (deletedBUs.length > 0) {
|
|
242
|
+
this.userBUassignments['delete'][updateItem.before.AccountUserID] = deletedBUs;
|
|
243
|
+
}
|
|
244
|
+
// add business units that were newly assigned
|
|
245
|
+
const addedBUs = [];
|
|
246
|
+
for (const newBuAssignment of updateItem.after.c__AssociatedBusinessUnits) {
|
|
247
|
+
// check if oldRole is missing in list of new roles
|
|
248
|
+
if (!updateItem.before.c__AssociatedBusinessUnits.includes(newBuAssignment)) {
|
|
249
|
+
addedBUs.push(newBuAssignment);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (addedBUs.length > 0) {
|
|
253
|
+
this.userBUassignments['add'][updateItem.before.AccountUserID] = addedBUs;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// add BUs for new users
|
|
257
|
+
if (createItem) {
|
|
258
|
+
const addedBUs = createItem.c__AssociatedBusinessUnits || [];
|
|
259
|
+
if (addedBUs.length > 0) {
|
|
260
|
+
this.userBUassignments['add']['key:' + createItem.CustomerKey] = addedBUs;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
delete metadata.c__AssociatedBusinessUnits;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Gets executed after deployment of metadata type
|
|
268
|
+
*
|
|
269
|
+
* @param {TYPE.UserDocumentMap} upsertResults metadata mapped by their keyField
|
|
270
|
+
* @returns {Promise.<void>} promise
|
|
271
|
+
*/
|
|
272
|
+
static async postDeployTasks(upsertResults) {
|
|
273
|
+
if (Object.keys(upsertResults).length) {
|
|
274
|
+
await this._handleBuAssignments(upsertResults);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* create/update business unit assignments
|
|
279
|
+
*
|
|
280
|
+
* @private
|
|
281
|
+
* @param {TYPE.UserDocumentMap} upsertResults metadata mapped by their keyField
|
|
282
|
+
* @returns {void}
|
|
283
|
+
*/
|
|
284
|
+
static async _handleBuAssignments(upsertResults) {
|
|
285
|
+
/** @type {TYPE.UserConfiguration[]} */
|
|
286
|
+
const configs = [];
|
|
287
|
+
for (const action in this.userBUassignments) {
|
|
288
|
+
for (const data of Object.entries(this.userBUassignments[action])) {
|
|
289
|
+
const buIds = data[1];
|
|
290
|
+
let userId = data[0];
|
|
291
|
+
if (!userId) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
userId = userId.startsWith('key:') ? upsertResults[userId.slice(4)].ID : userId;
|
|
295
|
+
configs.push(
|
|
296
|
+
/** @type {TYPE.UserConfiguration} */ {
|
|
297
|
+
Client: { ID: this.buObject.eid },
|
|
298
|
+
ID: userId,
|
|
299
|
+
BusinessUnitAssignmentConfiguration: {
|
|
300
|
+
BusinessUnitIds: { BusinessUnitId: buIds },
|
|
301
|
+
IsDelete: action === 'delete',
|
|
302
|
+
},
|
|
303
|
+
}
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (configs.length > 0) {
|
|
308
|
+
Util.logger.info('Deploying: BU assignment changes');
|
|
309
|
+
// run update
|
|
310
|
+
const buResponse = await this.client.soap.configure('AccountUser', configs);
|
|
311
|
+
// process response
|
|
312
|
+
if (buResponse.OverallStatus === 'OK') {
|
|
313
|
+
// get userIdNameMap
|
|
314
|
+
const userIdNameMap = {};
|
|
315
|
+
for (const user of Object.values(upsertResults)) {
|
|
316
|
+
userIdNameMap[user.ID] = `${user.CustomerKey} / ${user.Name}`;
|
|
317
|
+
}
|
|
318
|
+
// log what was added / removed
|
|
319
|
+
let configureResults = buResponse.Results?.[0]?.Result;
|
|
320
|
+
if (!configureResults) {
|
|
321
|
+
Util.logger.debug(
|
|
322
|
+
'buResponse.Results?.[0]?.Result not defined: ' + JSON.stringify(buResponse)
|
|
323
|
+
);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (!Array.isArray(configureResults)) {
|
|
327
|
+
configureResults = [configureResults];
|
|
328
|
+
}
|
|
329
|
+
const userBUresults = {};
|
|
330
|
+
for (const result of configureResults) {
|
|
331
|
+
if (result.StatusCode === 'OK') {
|
|
332
|
+
/** @type {TYPE.UserConfiguration} */
|
|
333
|
+
const config = result.Object;
|
|
334
|
+
const buArr =
|
|
335
|
+
config.BusinessUnitAssignmentConfiguration.BusinessUnitIds
|
|
336
|
+
.BusinessUnitId;
|
|
337
|
+
userBUresults[config.ID] ||= {
|
|
338
|
+
add: [],
|
|
339
|
+
delete: [],
|
|
340
|
+
};
|
|
341
|
+
userBUresults[config.ID][
|
|
342
|
+
config.BusinessUnitAssignmentConfiguration.IsDelete ? 'delete' : 'add'
|
|
343
|
+
] = Array.isArray(buArr) ? buArr : [buArr];
|
|
344
|
+
} else {
|
|
345
|
+
Util.logger.debug(
|
|
346
|
+
`Unknown error occured during processing of BU assignment reponse: ${JSON.stringify(
|
|
347
|
+
result
|
|
348
|
+
)}`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
for (const [userId, buResult] of Object.entries(userBUresults)) {
|
|
353
|
+
// show CLI log
|
|
354
|
+
const msgs = [];
|
|
355
|
+
if (buResult['add']?.length) {
|
|
356
|
+
msgs.push(`MID ${buResult['add'].join(', ')} access granted`);
|
|
357
|
+
} else {
|
|
358
|
+
msgs.push('no new access granted');
|
|
359
|
+
}
|
|
360
|
+
if (buResult['delete']?.length) {
|
|
361
|
+
msgs.push(`MID ${buResult['delete'].join(', ')} access removed`);
|
|
362
|
+
} else {
|
|
363
|
+
msgs.push('no access removed');
|
|
364
|
+
}
|
|
365
|
+
Util.logger.info(` - user ${userIdNameMap[userId]}: ${msgs.join(' / ')}`);
|
|
366
|
+
// update BU map in local variable
|
|
367
|
+
if (buResult['add']?.length) {
|
|
368
|
+
this.userIdBuMap[userId] ||= [];
|
|
369
|
+
this.userIdBuMap[userId].push(
|
|
370
|
+
...buResult['add'].filter(
|
|
371
|
+
(item) => !this.userIdBuMap[userId].includes(item)
|
|
372
|
+
)
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
if (buResult['delete']?.length) {
|
|
376
|
+
this.userIdBuMap[userId] ||= [];
|
|
377
|
+
this.userIdBuMap[userId] = this.userIdBuMap[userId].filter(
|
|
378
|
+
(item) => !buResult['delete'].includes(item)
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* helper for {@link User.createOrUpdate}
|
|
388
|
+
*
|
|
389
|
+
* @private
|
|
390
|
+
* @param {TYPE.UserDocument} metadata single created user
|
|
391
|
+
* @returns {void}
|
|
392
|
+
*/
|
|
393
|
+
static _setPasswordForNewUser(metadata) {
|
|
394
|
+
// if Password is not set during CREATE, generate one
|
|
395
|
+
// avoids error "Name, Email, UserID, and Password are required fields when creating a new user. (Code 11003)"
|
|
396
|
+
if (!metadata.Password) {
|
|
397
|
+
metadata.Password = this._generatePassword();
|
|
398
|
+
Util.logger.info(
|
|
399
|
+
` - Password for ${metadata.UserID} was not given. Generated password: ${metadata.Password}`
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* helper for {@link User.createOrUpdate}
|
|
406
|
+
* It searches for roles that were removed from the user and unassigns them; it also prints a summary of added/removed roles
|
|
407
|
+
* Adding roles works automatically for roles listed on the user
|
|
408
|
+
*
|
|
409
|
+
* @private
|
|
410
|
+
* @param {TYPE.UserDocumentDiff} item updated user with before and after state
|
|
411
|
+
* @returns {void}
|
|
412
|
+
*/
|
|
413
|
+
static _prepareRoleAssignments(item) {
|
|
414
|
+
// delete global roles from user that were not in the c__RoleNamesGlobal array / Roles.Role
|
|
415
|
+
const deletedRoles = [];
|
|
416
|
+
const deletedRoleNames = [];
|
|
417
|
+
if (item.after?.Roles?.Role && !Array.isArray(item.after.Roles.Role)) {
|
|
418
|
+
item.after.Roles.Role = [item.after.Roles.Role];
|
|
419
|
+
}
|
|
420
|
+
if (item.before?.Roles?.Role) {
|
|
421
|
+
if (!Array.isArray(item.before.Roles.Role)) {
|
|
422
|
+
item.before.Roles.Role = [item.before.Roles.Role];
|
|
423
|
+
}
|
|
424
|
+
for (const oldRole of item.before.Roles.Role) {
|
|
425
|
+
// check if oldRole is missing in list of new roles --> removing role
|
|
426
|
+
let oldRoleFound = false;
|
|
427
|
+
if (item.after.Roles?.Role) {
|
|
428
|
+
for (const newRole of item.after.Roles.Role) {
|
|
429
|
+
if (newRole.ObjectID == oldRole.ObjectID) {
|
|
430
|
+
oldRoleFound = true;
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (!oldRoleFound && !oldRole.Name.startsWith('Individual role for ')) {
|
|
436
|
+
// delete role unless it is an individual role (which are not manageable nor visible in the GUI)
|
|
437
|
+
deletedRoles.push({ ObjectID: oldRole.ObjectID, Name: oldRole.Name });
|
|
438
|
+
deletedRoleNames.push(oldRole.Name);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const addedRoleNames = [];
|
|
443
|
+
if (item.after?.Roles?.Role) {
|
|
444
|
+
for (const newRole of item.after.Roles.Role) {
|
|
445
|
+
// check if newRole is missing in list of old roles --> adding role
|
|
446
|
+
let roleAlreadyExisting = false;
|
|
447
|
+
if (item.before?.Roles?.Role) {
|
|
448
|
+
for (const oldRole of item.before.Roles.Role) {
|
|
449
|
+
if (newRole.ObjectID == oldRole.ObjectID) {
|
|
450
|
+
roleAlreadyExisting = true;
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (!roleAlreadyExisting) {
|
|
456
|
+
addedRoleNames.push(newRole.Name);
|
|
457
|
+
// Note: no AssignmentConfigurations property is needed to ADD global roles
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (addedRoleNames.length) {
|
|
461
|
+
Util.logger.info(
|
|
462
|
+
Util.getGrayMsg(
|
|
463
|
+
` - adding role-assignment (${item.after.CustomerKey} / ${
|
|
464
|
+
item.after.Name
|
|
465
|
+
}): ${addedRoleNames.join(', ')}`
|
|
466
|
+
)
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (deletedRoles.length) {
|
|
471
|
+
Util.logger.info(
|
|
472
|
+
Util.getGrayMsg(
|
|
473
|
+
` - removing role-assignment (${item.after.CustomerKey} / ${
|
|
474
|
+
item.after.Name
|
|
475
|
+
}): ${deletedRoleNames.join(', ')}`
|
|
476
|
+
)
|
|
477
|
+
);
|
|
478
|
+
// add deleted roles to payload with IsDelete=true
|
|
479
|
+
if (!item.after.Roles) {
|
|
480
|
+
item.after.Roles = { Role: [] };
|
|
481
|
+
} else if (!item.after.Roles.Role) {
|
|
482
|
+
item.after.Roles.Role = [];
|
|
483
|
+
}
|
|
484
|
+
item.after.Roles.Role.push(
|
|
485
|
+
...deletedRoles.map((role) =>
|
|
486
|
+
this._getRoleObjectForDeploy(
|
|
487
|
+
role.ObjectID,
|
|
488
|
+
role.Name,
|
|
489
|
+
item.after.AccountUserID,
|
|
490
|
+
false,
|
|
491
|
+
true
|
|
492
|
+
)
|
|
493
|
+
)
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* helper for {@link User._prepareRoleAssignments}
|
|
500
|
+
*
|
|
501
|
+
* @param {string} roleId role.ObjectID
|
|
502
|
+
* @param {string} roleName role.Name
|
|
503
|
+
* @param {number} userId user.AccountUserID
|
|
504
|
+
* @param {boolean} assignmentOnly if true, only assignment configuration will be returned
|
|
505
|
+
* @param {boolean} [isRoleRemovale=false] if true, role will be removed from user; otherwise added
|
|
506
|
+
* @returns {object} format needed by API
|
|
507
|
+
*/
|
|
508
|
+
static _getRoleObjectForDeploy(
|
|
509
|
+
roleId,
|
|
510
|
+
roleName,
|
|
511
|
+
userId,
|
|
512
|
+
assignmentOnly,
|
|
513
|
+
isRoleRemovale = false
|
|
514
|
+
) {
|
|
515
|
+
const assignmentConfigurations = {
|
|
516
|
+
AssignmentConfiguration: [
|
|
517
|
+
{
|
|
518
|
+
AccountUserId: userId,
|
|
519
|
+
AssignmentConfigureType: 'RoleUser',
|
|
520
|
+
IsDelete: isRoleRemovale,
|
|
521
|
+
},
|
|
522
|
+
],
|
|
523
|
+
};
|
|
524
|
+
return assignmentOnly
|
|
525
|
+
? assignmentConfigurations
|
|
526
|
+
: {
|
|
527
|
+
ObjectID: roleId,
|
|
528
|
+
Name: roleName,
|
|
529
|
+
AssignmentConfigurations: assignmentConfigurations,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Retrieves SOAP based metadata of metadata type into local filesystem. executes callback with retrieved metadata
|
|
535
|
+
*
|
|
536
|
+
* @returns {Promise.<TYPE.MetadataTypeMapObj>} Promise of metadata
|
|
537
|
+
*/
|
|
538
|
+
static async retrieveChangelog() {
|
|
539
|
+
return this._retrieve();
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Retrieves SOAP based metadata of metadata type into local filesystem. executes callback with retrieved metadata
|
|
543
|
+
*
|
|
544
|
+
* @private
|
|
545
|
+
* @param {string} retrieveDir Directory where retrieved metadata directory will be saved
|
|
546
|
+
* @param {string} [key] customer key of single item to retrieve
|
|
547
|
+
* @returns {Promise.<TYPE.MetadataTypeMapObj>} Promise of metadata
|
|
548
|
+
*/
|
|
549
|
+
static async _retrieve(retrieveDir, key) {
|
|
550
|
+
this.userIdBuMap = {};
|
|
551
|
+
/** @type {TYPE.SoapRequestParams} */
|
|
552
|
+
const requestParams = {
|
|
553
|
+
QueryAllAccounts: true,
|
|
554
|
+
|
|
555
|
+
filter: {
|
|
556
|
+
// normal users
|
|
557
|
+
leftOperand: 'Email',
|
|
558
|
+
operator: 'like',
|
|
559
|
+
rightOperand: '@',
|
|
560
|
+
},
|
|
561
|
+
};
|
|
562
|
+
if (key) {
|
|
563
|
+
// move original filter down one level into rightOperand and add key filter into leftOperand
|
|
564
|
+
requestParams.filter = {
|
|
565
|
+
leftOperand: {
|
|
566
|
+
leftOperand: 'CustomerKey',
|
|
567
|
+
operator: 'equals',
|
|
568
|
+
rightOperand: key,
|
|
569
|
+
},
|
|
570
|
+
operator: 'AND',
|
|
571
|
+
rightOperand: requestParams.filter,
|
|
572
|
+
};
|
|
573
|
+
} else {
|
|
574
|
+
// if we are not filtering by key the following requests will take long. Warn the user
|
|
575
|
+
Util.logger.info(` - Loading ${this.definition.type}s. This might take a while...`);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// get actual user details
|
|
579
|
+
return this.retrieveSOAP(retrieveDir, requestParams, key);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Retrieves SOAP via generic fuel-soap wrapper based metadata of metadata type into local filesystem. executes callback with retrieved metadata
|
|
584
|
+
*
|
|
585
|
+
* @param {string} retrieveDir Directory where retrieved metadata directory will be saved
|
|
586
|
+
* @param {TYPE.SoapRequestParams} [requestParams] required for the specific request (filter for example)
|
|
587
|
+
* @param {string|number} [singleRetrieve] key of single item to filter by
|
|
588
|
+
* @param {string[]} [additionalFields] Returns specified fields even if their retrieve definition is not set to true
|
|
589
|
+
* @returns {Promise.<TYPE.MetadataTypeMapObj>} Promise of item map
|
|
590
|
+
*/
|
|
591
|
+
static async retrieveSOAP(retrieveDir, requestParams, singleRetrieve, additionalFields) {
|
|
592
|
+
// to avoid not retrieving roles and userPermissions for users above the 2500 records limit we need to retrieve users twice, once with ActiveFlag=true and once with ActiveFlag=false
|
|
593
|
+
const requestParamsUser = {
|
|
594
|
+
QueryAllAccounts: true,
|
|
595
|
+
filter: {
|
|
596
|
+
leftOperand: {
|
|
597
|
+
leftOperand: 'ActiveFlag',
|
|
598
|
+
operator: 'equals',
|
|
599
|
+
rightOperand: null,
|
|
600
|
+
},
|
|
601
|
+
operator: 'AND',
|
|
602
|
+
rightOperand: requestParams.filter,
|
|
603
|
+
},
|
|
604
|
+
};
|
|
605
|
+
const fields = this.getFieldNamesToRetrieve(additionalFields, !retrieveDir);
|
|
606
|
+
const soapType = this.definition.soapType || this.definition.type;
|
|
607
|
+
let resultsBulk;
|
|
608
|
+
let foundSingle = false;
|
|
609
|
+
for (const active of [true, false]) {
|
|
610
|
+
requestParamsUser.filter.leftOperand.rightOperand = active;
|
|
611
|
+
try {
|
|
612
|
+
const resultsBatch = await this.client.soap.retrieveBulk(
|
|
613
|
+
soapType,
|
|
614
|
+
fields,
|
|
615
|
+
requestParamsUser
|
|
616
|
+
);
|
|
617
|
+
if (Array.isArray(resultsBatch?.Results)) {
|
|
618
|
+
Util.logger.debug(
|
|
619
|
+
Util.getGrayMsg(
|
|
620
|
+
` - found ${resultsBatch?.Results.length} ${
|
|
621
|
+
active ? 'active' : 'inactive'
|
|
622
|
+
} ${this.definition.type}s`
|
|
623
|
+
)
|
|
624
|
+
);
|
|
625
|
+
if (resultsBulk) {
|
|
626
|
+
// once first batch is done, the follow just add to result payload
|
|
627
|
+
resultsBulk.Results.push(...resultsBatch.Results);
|
|
628
|
+
} else {
|
|
629
|
+
resultsBulk = resultsBatch;
|
|
630
|
+
}
|
|
631
|
+
if (singleRetrieve && resultsBatch?.Results.length) {
|
|
632
|
+
foundSingle = true;
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
} catch (ex) {
|
|
637
|
+
this._handleSOAPErrors(ex, 'retrieving');
|
|
638
|
+
return {};
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
if (
|
|
642
|
+
!foundSingle &&
|
|
643
|
+
!(await this._retrieveSOAP_installedPackage(
|
|
644
|
+
requestParams,
|
|
645
|
+
soapType,
|
|
646
|
+
fields,
|
|
647
|
+
resultsBulk
|
|
648
|
+
))
|
|
649
|
+
) {
|
|
650
|
+
return {};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const metadata = this.parseResponseBody(resultsBulk);
|
|
654
|
+
// get BUs that each users have access to
|
|
655
|
+
if (resultsBulk?.Results?.length > 0) {
|
|
656
|
+
if (!singleRetrieve) {
|
|
657
|
+
Util.logger.info(
|
|
658
|
+
Util.getGrayMsg(` - found ${resultsBulk?.Results.length} users`)
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
// split array resultsBulk?.Results into chunks to avoid not getting all roles
|
|
662
|
+
const chunkSize = 100;
|
|
663
|
+
this.userIdBuMap = {};
|
|
664
|
+
Util.logger.info(` - Caching dependent Metadata: Business Unit assignments`);
|
|
665
|
+
for (let i = 0; i < resultsBulk?.Results?.length; i += chunkSize) {
|
|
666
|
+
if (i > 0) {
|
|
667
|
+
Util.logger.info(
|
|
668
|
+
Util.getGrayMsg(` - Requesting next batch (retrieved BUs for ${i} users)`)
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
const chunk = resultsBulk?.Results?.slice(i, i + chunkSize);
|
|
672
|
+
const resultsBatch = (
|
|
673
|
+
await this.client.soap.retrieveBulk(
|
|
674
|
+
'AccountUserAccount',
|
|
675
|
+
['AccountUser.AccountUserID', 'Account.ID'],
|
|
676
|
+
{
|
|
677
|
+
filter: {
|
|
678
|
+
leftOperand: 'AccountUser.AccountUserID',
|
|
679
|
+
operator: chunk.length > 1 ? 'IN' : 'equals', // API does not allow IN for single item
|
|
680
|
+
rightOperand: chunk.map((item) => item.AccountUserID),
|
|
681
|
+
},
|
|
682
|
+
}
|
|
683
|
+
)
|
|
684
|
+
).Results;
|
|
685
|
+
for (const item of resultsBatch) {
|
|
686
|
+
this.userIdBuMap[item.AccountUser.AccountUserID] ||= [];
|
|
687
|
+
// push to array if not already in array
|
|
688
|
+
if (
|
|
689
|
+
!this.userIdBuMap[item.AccountUser.AccountUserID].includes(item.Account.ID)
|
|
690
|
+
) {
|
|
691
|
+
this.userIdBuMap[item.AccountUser.AccountUserID].push(item.Account.ID);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (retrieveDir) {
|
|
698
|
+
const savedMetadata = await this.saveResults(metadata, retrieveDir, null);
|
|
699
|
+
Util.logger.info(
|
|
700
|
+
`Downloaded: ${this.definition.type} (${Object.keys(savedMetadata).length})` +
|
|
701
|
+
Util.getKeysString(singleRetrieve)
|
|
702
|
+
);
|
|
703
|
+
if (!singleRetrieve) {
|
|
704
|
+
// print summary to cli
|
|
705
|
+
const counter = {
|
|
706
|
+
userActive: 0,
|
|
707
|
+
userInactive: 0,
|
|
708
|
+
installedPackage: 0,
|
|
709
|
+
};
|
|
710
|
+
for (const id in savedMetadata) {
|
|
711
|
+
const item = savedMetadata[id];
|
|
712
|
+
if (item.c__type === 'Installed Package') {
|
|
713
|
+
counter.installedPackage++;
|
|
714
|
+
} else if (item.ActiveFlag) {
|
|
715
|
+
counter.userActive++;
|
|
716
|
+
} else {
|
|
717
|
+
counter.userInactive++;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
Util.logger.info(
|
|
721
|
+
Util.getGrayMsg(
|
|
722
|
+
`Found ${counter.userActive} active users / ${counter.userInactive} inactive users / ${counter.installedPackage} installed packages`
|
|
723
|
+
)
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
if (
|
|
727
|
+
!singleRetrieve &&
|
|
728
|
+
this.buObject &&
|
|
729
|
+
this.properties.metaDataTypes.documentOnRetrieve.includes(this.definition.type)
|
|
730
|
+
) {
|
|
731
|
+
await this.document(savedMetadata);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return { metadata: metadata, type: this.definition.type };
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* helper for {@link retrieveSOAP}
|
|
739
|
+
*
|
|
740
|
+
* @private
|
|
741
|
+
* @param {TYPE.SoapRequestParams} [requestParams] required for the specific request (filter for example)
|
|
742
|
+
* @param {string} soapType e.g. AccountUser
|
|
743
|
+
* @param {string[]} fields list of fields to retrieve
|
|
744
|
+
* @param {object} resultsBulk actual return value of this method
|
|
745
|
+
* @returns {Promise.<boolean>} success flag
|
|
746
|
+
*/
|
|
747
|
+
static async _retrieveSOAP_installedPackage(requestParams, soapType, fields, resultsBulk) {
|
|
748
|
+
const requestParamsInstalledPackage = {
|
|
749
|
+
QueryAllAccounts: true,
|
|
750
|
+
|
|
751
|
+
filter: {
|
|
752
|
+
leftOperand: {
|
|
753
|
+
leftOperand: 'ActiveFlag',
|
|
754
|
+
operator: 'equals',
|
|
755
|
+
rightOperand: true, // inactive installed packages are not visible in UI and hence cannot be reactivated there. Let's not retrieve them
|
|
756
|
+
},
|
|
757
|
+
operator: 'AND',
|
|
758
|
+
rightOperand: {
|
|
759
|
+
leftOperand: {
|
|
760
|
+
// filter out normal users
|
|
761
|
+
leftOperand: 'Email',
|
|
762
|
+
operator: 'isNull',
|
|
763
|
+
},
|
|
764
|
+
operator: 'OR',
|
|
765
|
+
rightOperand: {
|
|
766
|
+
// installed packages
|
|
767
|
+
leftOperand: {
|
|
768
|
+
leftOperand: 'Name',
|
|
769
|
+
operator: 'like',
|
|
770
|
+
rightOperand: ' app user', // ! will not work if the name was too long as "app user" might be cut off
|
|
771
|
+
},
|
|
772
|
+
operator: 'AND',
|
|
773
|
+
rightOperand: {
|
|
774
|
+
// 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.
|
|
775
|
+
leftOperand: 'MustChangePassword',
|
|
776
|
+
operator: 'equals',
|
|
777
|
+
rightOperand: 'false',
|
|
778
|
+
},
|
|
779
|
+
},
|
|
780
|
+
},
|
|
781
|
+
},
|
|
782
|
+
};
|
|
783
|
+
if (requestParams?.filter?.leftOperand?.leftOperand === 'CustomerKey') {
|
|
784
|
+
requestParamsInstalledPackage.filter = {
|
|
785
|
+
leftOperand: requestParams?.filter?.leftOperand,
|
|
786
|
+
operator: 'AND',
|
|
787
|
+
rightOperand: requestParamsInstalledPackage.filter,
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
try {
|
|
791
|
+
const resultsBatch = await this.client.soap.retrieveBulk(
|
|
792
|
+
soapType,
|
|
793
|
+
fields,
|
|
794
|
+
requestParamsInstalledPackage
|
|
795
|
+
);
|
|
796
|
+
if (Array.isArray(resultsBatch?.Results)) {
|
|
797
|
+
Util.logger.debug(
|
|
798
|
+
Util.getGrayMsg(` - found ${resultsBatch?.Results.length} installed packages`)
|
|
799
|
+
);
|
|
800
|
+
if (resultsBulk) {
|
|
801
|
+
// once first batch is done, the follow just add to result payload
|
|
802
|
+
resultsBulk.Results.push(...resultsBatch.Results);
|
|
803
|
+
} else {
|
|
804
|
+
resultsBulk = resultsBatch;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
} catch (ex) {
|
|
808
|
+
this._handleSOAPErrors(ex, 'retrieving');
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
return true;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
*
|
|
816
|
+
* @private
|
|
817
|
+
* @param {string} date first date
|
|
818
|
+
* @returns {number} time difference
|
|
819
|
+
*/
|
|
820
|
+
static _timeSinceDate(date) {
|
|
821
|
+
const interval = 'days';
|
|
822
|
+
const second = 1000,
|
|
823
|
+
minute = second * 60,
|
|
824
|
+
hour = minute * 60,
|
|
825
|
+
day = hour * 24,
|
|
826
|
+
week = day * 7;
|
|
827
|
+
date = new Date(date);
|
|
828
|
+
const now = new Date();
|
|
829
|
+
const timediff = now - date;
|
|
830
|
+
if (Number.isNaN(timediff)) {
|
|
831
|
+
return Number.NaN;
|
|
832
|
+
}
|
|
833
|
+
let result;
|
|
834
|
+
switch (interval) {
|
|
835
|
+
case 'years': {
|
|
836
|
+
result = now.getFullYear() - date.getFullYear();
|
|
837
|
+
break;
|
|
838
|
+
}
|
|
839
|
+
case 'months': {
|
|
840
|
+
result =
|
|
841
|
+
now.getFullYear() * 12 +
|
|
842
|
+
now.getMonth() -
|
|
843
|
+
(date.getFullYear() * 12 + date.getMonth());
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
case 'weeks': {
|
|
847
|
+
result = Math.floor(timediff / week);
|
|
848
|
+
break;
|
|
849
|
+
}
|
|
850
|
+
case 'days': {
|
|
851
|
+
result = Math.floor(timediff / day);
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
case 'hours': {
|
|
855
|
+
result = Math.floor(timediff / hour);
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
case 'minutes': {
|
|
859
|
+
result = Math.floor(timediff / minute);
|
|
860
|
+
break;
|
|
861
|
+
}
|
|
862
|
+
case 'seconds': {
|
|
863
|
+
result = Math.floor(timediff / second);
|
|
864
|
+
break;
|
|
865
|
+
}
|
|
866
|
+
default: {
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return result + ' ' + interval;
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* helper to print bu names
|
|
874
|
+
*
|
|
875
|
+
* @private
|
|
876
|
+
* @param {number} id bu id
|
|
877
|
+
* @returns {string} "bu name (bu id)""
|
|
878
|
+
*/
|
|
879
|
+
static _getBuName(id) {
|
|
880
|
+
const name = this.buObject.eid == id ? '_ParentBU_' : this.buIdName[id];
|
|
881
|
+
return `<nobr>${name} (${id})</nobr>`;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* helper that gets BU names from config
|
|
886
|
+
*
|
|
887
|
+
* @private
|
|
888
|
+
*/
|
|
889
|
+
static _getBuNames() {
|
|
890
|
+
this.buIdName = {};
|
|
891
|
+
for (const cred in this.properties.credentials) {
|
|
892
|
+
for (const buName in this.properties.credentials[cred].businessUnits) {
|
|
893
|
+
this.buIdName[this.properties.credentials[cred].businessUnits[buName]] = buName;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* helper for {@link createOrUpdate} to generate a random initial password for new users
|
|
899
|
+
* note: possible minimum length values in SFMC are 6, 8, 10, 15 chars. Therefore we should default here to 15 chars.
|
|
900
|
+
*
|
|
901
|
+
* @private
|
|
902
|
+
* @param {number} [length] length of password; defaults to 15
|
|
903
|
+
* @returns {string} random password
|
|
904
|
+
*/
|
|
905
|
+
static _generatePassword(length = 15) {
|
|
906
|
+
const alpha = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
907
|
+
const special = '!@#$%&';
|
|
908
|
+
const numeric = '0123456789';
|
|
909
|
+
const charset = alpha + special + numeric;
|
|
910
|
+
let retVal;
|
|
911
|
+
do {
|
|
912
|
+
retVal = '';
|
|
913
|
+
for (let i = 0, n = charset.length; i < length; ++i) {
|
|
914
|
+
retVal += charset.charAt(Math.floor(Math.random() * n));
|
|
915
|
+
}
|
|
916
|
+
// check if password contains at least one of each character type or else generate a new one
|
|
917
|
+
} while (
|
|
918
|
+
!/[a-z]/.test(retVal) ||
|
|
919
|
+
!/[A-Z]/.test(retVal) ||
|
|
920
|
+
!/[0-9]/.test(retVal) ||
|
|
921
|
+
!/[!@#$%&]/.test(retVal)
|
|
922
|
+
);
|
|
923
|
+
return retVal;
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Creates markdown documentation of all roles
|
|
927
|
+
*
|
|
928
|
+
* @param {TYPE.MetadataTypeMap} [metadata] user list
|
|
929
|
+
* @returns {Promise.<void>} -
|
|
930
|
+
*/
|
|
931
|
+
static async document(metadata) {
|
|
932
|
+
if (this.buObject.eid !== this.buObject.mid) {
|
|
933
|
+
Util.logger.error(
|
|
934
|
+
`Users can only be retrieved & documented for the ${Util.parentBuName}`
|
|
935
|
+
);
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// if ran as part of retrieve/deploy with key, exit here
|
|
940
|
+
if (metadata && Object.keys(metadata).length === 1) {
|
|
941
|
+
Util.logger.debug(
|
|
942
|
+
'Only 1 user found. Skipping documentation, assuming we ran retrieve-by-key.'
|
|
943
|
+
);
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (!metadata) {
|
|
948
|
+
// load users from disk if document was called directly and not part of a retrieve
|
|
949
|
+
try {
|
|
950
|
+
metadata = this.readBUMetadataForType(
|
|
951
|
+
File.normalizePath([
|
|
952
|
+
this.properties.directories.retrieve,
|
|
953
|
+
this.buObject.credential,
|
|
954
|
+
Util.parentBuName,
|
|
955
|
+
]),
|
|
956
|
+
true
|
|
957
|
+
).user;
|
|
958
|
+
} catch (ex) {
|
|
959
|
+
Util.logger.error(ex.message);
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// init map of BU Ids > BU Name
|
|
965
|
+
this._getBuNames();
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* @type {TYPE.UserDocument[]}
|
|
969
|
+
*/
|
|
970
|
+
const users = [];
|
|
971
|
+
|
|
972
|
+
for (const id in metadata) {
|
|
973
|
+
const user = metadata[id];
|
|
974
|
+
// TODO resolve user permissions to something readable
|
|
975
|
+
// user roles
|
|
976
|
+
let roles = '';
|
|
977
|
+
if (user.c__RoleNamesGlobal) {
|
|
978
|
+
roles = '<nobr>' + user.c__RoleNamesGlobal.join(',</nobr><br> <nobr>') + '</nobr>';
|
|
979
|
+
}
|
|
980
|
+
let associatedBus = '';
|
|
981
|
+
if (user.c__AssociatedBusinessUnits) {
|
|
982
|
+
// ensure Parent BU is first in list
|
|
983
|
+
user.c__AssociatedBusinessUnits.push(user.DefaultBusinessUnit);
|
|
984
|
+
// ensure associatedBus have no duplicates
|
|
985
|
+
associatedBus = [
|
|
986
|
+
...new Set(user.c__AssociatedBusinessUnits.map((mid) => this._getBuName(mid))),
|
|
987
|
+
]
|
|
988
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
|
|
989
|
+
.join(',<br> ');
|
|
990
|
+
}
|
|
991
|
+
const defaultBUName = this._getBuName(user.DefaultBusinessUnit);
|
|
992
|
+
const LastSuccessfulLogin = user.LastSuccessfulLogin.split('.')[0];
|
|
993
|
+
users.push({
|
|
994
|
+
TYPE: user.c__type,
|
|
995
|
+
UserID: user.UserID,
|
|
996
|
+
AccountUserID: user.c__AccountUserID,
|
|
997
|
+
CustomerKey: user.CustomerKey,
|
|
998
|
+
Name: user.Name,
|
|
999
|
+
Email: user.Email,
|
|
1000
|
+
NotificationEmailAddress: user.NotificationEmailAddress,
|
|
1001
|
+
ActiveFlag: user.ActiveFlag === true ? '✓' : '-',
|
|
1002
|
+
IsLocked: user.IsLocked === true ? '✓' : '-',
|
|
1003
|
+
IsAPIUser: user.IsAPIUser === true ? '✓' : '-',
|
|
1004
|
+
MustChangePassword: user.MustChangePassword === true ? '✓' : '-',
|
|
1005
|
+
DefaultBusinessUnit: defaultBUName,
|
|
1006
|
+
AssociatedBus: associatedBus,
|
|
1007
|
+
Roles: roles,
|
|
1008
|
+
LastSuccessfulLogin: user.LastSuccessfulLogin
|
|
1009
|
+
? // on create & update, LastSuccessfulLogin often gets overwritten with the current date
|
|
1010
|
+
LastSuccessfulLogin === user.CreatedDate.split('.')[0] ||
|
|
1011
|
+
LastSuccessfulLogin === user.ModifiedDate.split('.')[0]
|
|
1012
|
+
? 'unknown'
|
|
1013
|
+
: this._timeSinceDate(user.LastSuccessfulLogin)
|
|
1014
|
+
: 'never',
|
|
1015
|
+
CreatedDate: user.CreatedDate ? user.CreatedDate.split('T').join(' ') : 'n/a',
|
|
1016
|
+
ModifiedDate: user.ModifiedDate ? user.ModifiedDate.split('T').join(' ') : 'n/a',
|
|
1017
|
+
ModifiedBy: user.Client.ModifiedBy || 'n/a',
|
|
1018
|
+
TimeZoneName: user.c__TimeZoneName.slice(1, 10),
|
|
1019
|
+
LocaleCode: user.c__LocaleCode,
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
users.sort((a, b) => (a.Name < b.Name ? -1 : a.Name > b.Name ? 1 : 0));
|
|
1023
|
+
const columnsToPrint = [
|
|
1024
|
+
['Name', 'Name'],
|
|
1025
|
+
['Last successful Login', 'LastSuccessfulLogin'],
|
|
1026
|
+
['Active', 'ActiveFlag'],
|
|
1027
|
+
['Access Locked out', 'IsLocked'],
|
|
1028
|
+
['API User', 'IsAPIUser'],
|
|
1029
|
+
['Must change PW', 'MustChangePassword'],
|
|
1030
|
+
['Default BU', 'DefaultBusinessUnit'],
|
|
1031
|
+
['BU Access', 'AssociatedBus'],
|
|
1032
|
+
['Roles', 'Roles'],
|
|
1033
|
+
['Login', 'UserID'],
|
|
1034
|
+
['ID', 'AccountUserID'],
|
|
1035
|
+
['Key', 'CustomerKey'],
|
|
1036
|
+
['E-Mail', 'Email'],
|
|
1037
|
+
['Notification E-Mail', 'NotificationEmailAddress'],
|
|
1038
|
+
['Timezone', 'TimeZoneName'],
|
|
1039
|
+
['SFMC Locale', 'LocaleCode'],
|
|
1040
|
+
['Modified Date', 'ModifiedDate'],
|
|
1041
|
+
['Modified By', 'ModifiedBy'],
|
|
1042
|
+
['Created Date', 'CreatedDate'],
|
|
1043
|
+
];
|
|
1044
|
+
let output = `# User Overview - ${this.buObject.credential}`;
|
|
1045
|
+
output += this._generateDocMd(
|
|
1046
|
+
users.filter((user) => user.TYPE === 'User' && user.ActiveFlag === '✓'),
|
|
1047
|
+
'User',
|
|
1048
|
+
columnsToPrint
|
|
1049
|
+
);
|
|
1050
|
+
output += this._generateDocMd(
|
|
1051
|
+
users.filter((user) => user.TYPE === 'User' && user.ActiveFlag === '-'),
|
|
1052
|
+
'Inactivated User',
|
|
1053
|
+
columnsToPrint
|
|
1054
|
+
);
|
|
1055
|
+
output += this._generateDocMd(
|
|
1056
|
+
users.filter((user) => user.TYPE === 'Installed Package'),
|
|
1057
|
+
'Installed Package',
|
|
1058
|
+
columnsToPrint
|
|
1059
|
+
);
|
|
1060
|
+
const docPath = File.normalizePath([this.properties.directories.docs, 'user']);
|
|
1061
|
+
|
|
1062
|
+
try {
|
|
1063
|
+
const filename = this.buObject.credential;
|
|
1064
|
+
// write to disk
|
|
1065
|
+
await File.writeToFile(docPath, filename + '.users', 'md', output);
|
|
1066
|
+
Util.logger.info(`Created ${File.normalizePath([docPath, filename])}.users.md`);
|
|
1067
|
+
if (['html', 'both'].includes(this.properties.options.documentType)) {
|
|
1068
|
+
Util.logger.warn(' - HTML-based documentation of user currently not supported.');
|
|
1069
|
+
}
|
|
1070
|
+
} catch (ex) {
|
|
1071
|
+
Util.logger.error(`user.document():: error | `, ex.message);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
*
|
|
1076
|
+
* @private
|
|
1077
|
+
* @param {object[]} users list of users and installed package
|
|
1078
|
+
* @param {'Installed Package'|'User'} type choose what sub type to print
|
|
1079
|
+
* @param {Array[]} columnsToPrint helper array
|
|
1080
|
+
* @returns {string} markdown
|
|
1081
|
+
*/
|
|
1082
|
+
static _generateDocMd(users, type, columnsToPrint) {
|
|
1083
|
+
let output = `\n\n## ${type}s (${users.length})\n\n`;
|
|
1084
|
+
let tableSeparator = '';
|
|
1085
|
+
for (const column of columnsToPrint) {
|
|
1086
|
+
output += `| ${column[0]} `;
|
|
1087
|
+
tableSeparator += '| --- ';
|
|
1088
|
+
}
|
|
1089
|
+
output += `|\n${tableSeparator}|\n`;
|
|
1090
|
+
for (const user of users) {
|
|
1091
|
+
for (const column of columnsToPrint) {
|
|
1092
|
+
output += `| ${user[column[1]]} `;
|
|
1093
|
+
}
|
|
1094
|
+
output += `|\n`;
|
|
1095
|
+
}
|
|
1096
|
+
return output;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* manages post retrieve steps
|
|
1101
|
+
*
|
|
1102
|
+
* @param {TYPE.MetadataTypeItem} metadata a single item
|
|
1103
|
+
* @returns {TYPE.MetadataTypeItem | void} a single item
|
|
1104
|
+
*/
|
|
1105
|
+
static postRetrieveTasks(metadata) {
|
|
1106
|
+
metadata.c__type = 'Installed Package';
|
|
1107
|
+
if (metadata.Email.includes('@') && !metadata.Name.endsWith('app user')) {
|
|
1108
|
+
metadata.c__type = 'User';
|
|
1109
|
+
}
|
|
1110
|
+
if (metadata.c__type === 'Installed Package' && !metadata.ActiveFlag) {
|
|
1111
|
+
// deleted installed package - we do try to filter them in the API call but sometimes they slip through in the other calls
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// rewrite AccountUserID to avoid accidental overwrites by create attempts but still allow users to search for this ID
|
|
1116
|
+
metadata.c__AccountUserID = metadata.AccountUserID;
|
|
1117
|
+
delete metadata.AccountUserID;
|
|
1118
|
+
|
|
1119
|
+
// the actual field cannot be updated. to avoid confusion, we rename it
|
|
1120
|
+
metadata.c__IsLocked_readOnly = metadata.IsLocked;
|
|
1121
|
+
delete metadata.IsLocked;
|
|
1122
|
+
if (metadata.c__IsLocked_readOnly) {
|
|
1123
|
+
// add this field in case the user is locked to offer the opportunity to unlock it via api
|
|
1124
|
+
metadata.Unlock = false;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
metadata.c__AssociatedBusinessUnits = this.userIdBuMap[metadata.ID] || [];
|
|
1128
|
+
metadata.c__AssociatedBusinessUnits.sort();
|
|
1129
|
+
|
|
1130
|
+
// make roles easily accessible
|
|
1131
|
+
let roles;
|
|
1132
|
+
if (metadata.Roles?.Role) {
|
|
1133
|
+
// normalize to always use array
|
|
1134
|
+
if (!metadata.Roles.Role.length) {
|
|
1135
|
+
metadata.Roles.Role = [metadata.Roles.Role];
|
|
1136
|
+
}
|
|
1137
|
+
// convert complex object into basic set of info
|
|
1138
|
+
// turns out, Role Names are unique and hence we can turn this into a simple array of names
|
|
1139
|
+
roles = metadata.Roles.Role.map((item) => item.Name)
|
|
1140
|
+
.filter(Boolean)
|
|
1141
|
+
.filter(
|
|
1142
|
+
// individual role (which are not manageable nor visible in the GUI)
|
|
1143
|
+
(roleName) => !roleName.startsWith('Individual role for ')
|
|
1144
|
+
)
|
|
1145
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
1146
|
+
} else {
|
|
1147
|
+
// set to empty array
|
|
1148
|
+
roles = [];
|
|
1149
|
+
}
|
|
1150
|
+
metadata.c__RoleNamesGlobal = roles;
|
|
1151
|
+
delete metadata.Roles;
|
|
1152
|
+
|
|
1153
|
+
// Timezone
|
|
1154
|
+
if (metadata.TimeZone?.ID) {
|
|
1155
|
+
metadata.c__TimeZoneName = cache.searchForField(
|
|
1156
|
+
'_timezone',
|
|
1157
|
+
metadata.TimeZone.ID,
|
|
1158
|
+
'id',
|
|
1159
|
+
'description'
|
|
1160
|
+
);
|
|
1161
|
+
delete metadata.TimeZone;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// Locale
|
|
1165
|
+
if (metadata.Locale?.LocaleCode) {
|
|
1166
|
+
metadata.c__LocaleCode = metadata.Locale.LocaleCode;
|
|
1167
|
+
delete metadata.Locale;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
return metadata;
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Assign definition to static attributes
|
|
1175
|
+
User.definition = require('../MetadataTypeDefinitions').user;
|
|
1176
|
+
|
|
1177
|
+
module.exports = User;
|