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