mcdev 8.2.1 → 8.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/.github/ISSUE_TEMPLATE/bug.yml +2 -0
  2. package/.github/workflows/code-test.yml +1 -1
  3. package/.github/workflows/coverage-base-update.yml +1 -1
  4. package/.github/workflows/coverage-develop-branch.yml +1 -1
  5. package/.github/workflows/coverage-main-branch.yml +1 -1
  6. package/.github/workflows/coverage.yml +1 -1
  7. package/.github/workflows/npm-publish.yml +7 -5
  8. package/@types/lib/MetadataTypeDefinitions.d.ts +4 -0
  9. package/@types/lib/MetadataTypeDefinitions.d.ts.map +1 -1
  10. package/@types/lib/MetadataTypeInfo.d.ts +4 -0
  11. package/@types/lib/MetadataTypeInfo.d.ts.map +1 -1
  12. package/@types/lib/Retriever.d.ts.map +1 -1
  13. package/@types/lib/index.d.ts +20 -9
  14. package/@types/lib/index.d.ts.map +1 -1
  15. package/@types/lib/metadataTypes/Asset.d.ts +3 -3
  16. package/@types/lib/metadataTypes/Asset.d.ts.map +1 -1
  17. package/@types/lib/metadataTypes/Automation.d.ts +7 -7
  18. package/@types/lib/metadataTypes/Automation.d.ts.map +1 -1
  19. package/@types/lib/metadataTypes/DataExtension.d.ts +9 -9
  20. package/@types/lib/metadataTypes/DataExtension.d.ts.map +1 -1
  21. package/@types/lib/metadataTypes/DataExtensionField.d.ts +7 -0
  22. package/@types/lib/metadataTypes/DataExtensionField.d.ts.map +1 -1
  23. package/@types/lib/metadataTypes/DataFilter.d.ts +335 -0
  24. package/@types/lib/metadataTypes/DataFilter.d.ts.map +1 -0
  25. package/@types/lib/metadataTypes/DataFilterHidden.d.ts +164 -0
  26. package/@types/lib/metadataTypes/DataFilterHidden.d.ts.map +1 -0
  27. package/@types/lib/metadataTypes/Filter.d.ts +181 -6
  28. package/@types/lib/metadataTypes/Filter.d.ts.map +1 -1
  29. package/@types/lib/metadataTypes/FilterDefinition.d.ts +318 -0
  30. package/@types/lib/metadataTypes/FilterDefinition.d.ts.map +1 -0
  31. package/@types/lib/metadataTypes/FilterDefinitionHidden.d.ts +156 -0
  32. package/@types/lib/metadataTypes/FilterDefinitionHidden.d.ts.map +1 -0
  33. package/@types/lib/metadataTypes/ImportFile.d.ts.map +1 -1
  34. package/@types/lib/metadataTypes/Journey.d.ts +18 -0
  35. package/@types/lib/metadataTypes/Journey.d.ts.map +1 -1
  36. package/@types/lib/metadataTypes/MetadataType.d.ts +6 -0
  37. package/@types/lib/metadataTypes/MetadataType.d.ts.map +1 -1
  38. package/@types/lib/metadataTypes/MobileKeyword.d.ts +2 -2
  39. package/@types/lib/metadataTypes/MobileKeyword.d.ts.map +1 -1
  40. package/@types/lib/metadataTypes/User.d.ts +1 -1
  41. package/@types/lib/metadataTypes/User.d.ts.map +1 -1
  42. package/@types/lib/metadataTypes/definitions/DataFilter.definition.d.ts +230 -0
  43. package/@types/lib/metadataTypes/definitions/DataFilter.definition.d.ts.map +1 -0
  44. package/@types/lib/metadataTypes/definitions/DataFilterHidden.definition.d.ts +230 -0
  45. package/@types/lib/metadataTypes/definitions/DataFilterHidden.definition.d.ts.map +1 -0
  46. package/@types/lib/metadataTypes/definitions/Filter.definition.d.ts +203 -31
  47. package/@types/lib/metadataTypes/definitions/FilterDefinition.definition.d.ts +218 -0
  48. package/@types/lib/metadataTypes/definitions/FilterDefinition.definition.d.ts.map +1 -0
  49. package/@types/lib/metadataTypes/definitions/FilterDefinitionHidden.definition.d.ts +218 -0
  50. package/@types/lib/metadataTypes/definitions/FilterDefinitionHidden.definition.d.ts.map +1 -0
  51. package/@types/lib/metadataTypes/definitions/Journey.definition.d.ts +18 -0
  52. package/@types/lib/util/devops.d.ts +1 -0
  53. package/@types/lib/util/devops.d.ts.map +1 -1
  54. package/@types/lib/util/replaceContentBlockReference.d.ts +3 -3
  55. package/@types/lib/util/replaceContentBlockReference.d.ts.map +1 -1
  56. package/@types/lib/util/util.d.ts.map +1 -1
  57. package/@types/lib/util/validations.d.ts.map +1 -1
  58. package/@types/types/mcdev.d.d.ts +296 -0
  59. package/@types/types/mcdev.d.d.ts.map +1 -1
  60. package/lib/MetadataTypeDefinitions.js +4 -0
  61. package/lib/MetadataTypeInfo.js +4 -0
  62. package/lib/Retriever.js +3 -0
  63. package/lib/index.js +87 -3
  64. package/lib/metadataTypes/DataExtensionField.js +21 -0
  65. package/lib/metadataTypes/DataFilter.js +694 -0
  66. package/lib/metadataTypes/DataFilterHidden.js +18 -0
  67. package/lib/metadataTypes/Filter.js +315 -1
  68. package/lib/metadataTypes/ImportFile.js +11 -1
  69. package/lib/metadataTypes/Journey.js +111 -7
  70. package/lib/metadataTypes/MetadataType.js +14 -0
  71. package/lib/metadataTypes/definitions/Automation.definition.js +1 -0
  72. package/lib/metadataTypes/definitions/DataFilter.definition.js +168 -0
  73. package/lib/metadataTypes/definitions/DataFilterHidden.definition.js +169 -0
  74. package/lib/metadataTypes/definitions/Filter.definition.js +136 -22
  75. package/lib/metadataTypes/definitions/Journey.definition.js +20 -1
  76. package/lib/util/devops.js +14 -4
  77. package/lib/util/util.js +7 -2
  78. package/lib/util/validations.js +7 -1
  79. package/package.json +10 -10
  80. package/test/general.test.js +17 -19
  81. package/test/mockRoot/.mcdevrc.json +4 -1
  82. package/test/mockRoot/deploy/testInstance/testBU/asset/block/{testExisting_asset_htmlblock-matchName-fail.asset-block-meta.json → testExisting_asset_html-matchNamFail.asset-block-meta.json} +1 -1
  83. package/test/mockRoot/deploy/testInstance/testBU/asset/block/{testExisting_asset_htmlblock-matchName.asset-block-meta.json → testExisting_asset_html-matchName.asset-block-meta.json} +1 -1
  84. package/test/mockRoot/deploy/testInstance/testBU/asset/block/{testExisting_asset_htmlblock-matchName-create.asset-block-meta.json → testExisting_asset_html-matchNameAdd.asset-block-meta.json} +3 -3
  85. package/test/mockRoot/deploy/testInstance/testBU/dataFilter/testExisting_dataFilter.dataFilter-meta.json +19 -0
  86. package/test/mockRoot/deploy/testInstance/testBU/dataFilter/testNew_dataFilter.dataFilter-meta.json +19 -0
  87. package/test/mockRoot/deploy/testInstance/testBU/filter/testExisting_filter.filter-meta.json +10 -0
  88. package/test/mockRoot/deploy/testInstance/testBU/filter/testNew_filter.filter-meta.json +11 -0
  89. package/test/mockRoot/deploy/testInstance/testBU/journey/testExisting_journey_updatecontact.journey-meta.json +108 -0
  90. package/test/resources/9999999/asset/v1/content/assets/1295064/patch-response.json +1 -1
  91. package/test/resources/9999999/automation/v1/filters/a0f1a1bc-4ea1-44b3-8fe1-ce40ef35c1c0/patch-response.json +18 -0
  92. package/test/resources/9999999/automation/v1/filters/f018f237-f7ef-40b0-afc8-39ea2e5dcca4/delete-response.txt +0 -0
  93. package/test/resources/9999999/automation/v1/filters/f018f237-f7ef-40b0-afc8-39ea2e5dcca4/get-response.json +15 -0
  94. package/test/resources/9999999/automation/v1/filters/f018f237-f7ef-40b0-afc8-39ea2e5dcca4/patch-response.json +15 -0
  95. package/test/resources/9999999/automation/v1/filters/get-response.json +22 -0
  96. package/test/resources/9999999/dataFilter/build-expected.json +19 -0
  97. package/test/resources/9999999/dataFilter/get-expected.json +23 -0
  98. package/test/resources/9999999/dataFilter/patch-expected.json +20 -0
  99. package/test/resources/9999999/dataFilter/post-expected.json +20 -0
  100. package/test/resources/9999999/dataFilter/template-expected.json +19 -0
  101. package/test/resources/9999999/dataFolder/retrieve-ContentTypeINasset,asset-sha,automatio,cloudpage,dataexten,filteract,filterdef,hidden,journey,list,mysubs,publicati,queryacti,salesforc,shared_da,shared_da,shared_sa,ssjsactiv,synchroni,useriniti-response.xml +765 -0
  102. package/test/resources/9999999/dataFolder/retrieve-ContentTypeINfilteractivity,hidden-response.xml +139 -0
  103. package/test/resources/9999999/dataFolder/retrieve-ContentTypeINfilterdefinition,hidden-response.xml +116 -0
  104. package/test/resources/9999999/email/v1/filters/filterdefinition/10ef27dd-4be8-4bf6-970a-8acf8e281e55/delete-response.txt +1 -0
  105. package/test/resources/9999999/email/v1/filters/filterdefinition/10ef27dd-4be8-4bf6-970a-8acf8e281e55/get-response.json +19 -0
  106. package/test/resources/9999999/email/v1/filters/filterdefinition/10ef27dd-4be8-4bf6-970a-8acf8e281e55/patch-response.json +15 -0
  107. package/test/resources/9999999/email/v1/filters/filterdefinition/category/5318/get-response.json +27 -0
  108. package/test/resources/9999999/email/v1/filters/filterdefinition/category/8502/get-response.json +7 -0
  109. package/test/resources/9999999/email/v1/filters/filterdefinition/category/8503/get-response.json +7 -0
  110. package/test/resources/9999999/email/v1/filters/filterdefinition/post-response.json +15 -0
  111. package/test/resources/9999999/filter/build-expected.json +10 -0
  112. package/test/resources/9999999/filter/get-expected.json +11 -0
  113. package/test/resources/9999999/filter/patch-expected.json +11 -0
  114. package/test/resources/9999999/filter/post-expected.json +11 -0
  115. package/test/resources/9999999/filter/template-expected.json +10 -0
  116. package/test/resources/9999999/filterActivity/create-response.xml +46 -0
  117. package/test/resources/9999999/filterActivity/retrieve-CustomerKey=testExisting_filter-response.xml +30 -0
  118. package/test/resources/9999999/filterDefinition/retrieve-CustomerKey=testExisting_dataFilter-response.xml +30 -0
  119. package/test/resources/9999999/interaction/v1/interactions/get-response.json +45 -3
  120. package/test/resources/9999999/interaction/v1/interactions/key_testExisting_journey_updatecontact/get-response.json +119 -0
  121. package/test/resources/9999999/interaction/v1/interactions/key_testExisting_journey_updatecontact/put-response.json +106 -0
  122. package/test/resources/9999999/journey/get-updatecontact-expected.json +108 -0
  123. package/test/resources/9999999/journey/put-updatecontact-expected.json +108 -0
  124. package/test/type.asset.test.js +7 -7
  125. package/test/type.automation.test.js +14 -14
  126. package/test/type.dataFilter.test.js +174 -0
  127. package/test/type.filter.test.js +170 -0
  128. package/test/type.journey.test.js +59 -7
  129. package/test/type.query.test.js +2 -2
  130. package/test/utils.js +8 -0
  131. package/types/mcdev.d.js +101 -0
  132. /package/test/mockRoot/deploy/testInstance/testBU/asset/block/{testExisting_asset_htmlblock-matchName-create.asset-block-meta.html → testExisting_asset_html-matchNamFail.asset-block-meta.html} +0 -0
  133. /package/test/mockRoot/deploy/testInstance/testBU/asset/block/{testExisting_asset_htmlblock-matchName-fail.asset-block-meta.html → testExisting_asset_html-matchName.asset-block-meta.html} +0 -0
  134. /package/test/mockRoot/deploy/testInstance/testBU/asset/block/{testExisting_asset_htmlblock-matchName.asset-block-meta.html → testExisting_asset_html-matchNameAdd.asset-block-meta.html} +0 -0
@@ -0,0 +1,694 @@
1
+ 'use strict';
2
+
3
+ import MetadataType from './MetadataType.js';
4
+ import { Util } from '../util/util.js';
5
+ import cache from '../util/cache.js';
6
+ import DataExtensionField from './DataExtensionField.js';
7
+ import Folder from './Folder.js';
8
+ import { XMLBuilder, XMLParser } from 'fast-xml-parser';
9
+ import File from '../util/file.js';
10
+
11
+ /**
12
+ * @typedef {import('../../types/mcdev.d.js').BuObject} BuObject
13
+ * @typedef {import('../../types/mcdev.d.js').CodeExtract} CodeExtract
14
+ * @typedef {import('../../types/mcdev.d.js').CodeExtractItem} CodeExtractItem
15
+ * @typedef {import('../../types/mcdev.d.js').MetadataTypeItem} MetadataTypeItem
16
+ * @typedef {import('../../types/mcdev.d.js').MetadataTypeItemDiff} MetadataTypeItemDiff
17
+ * @typedef {import('../../types/mcdev.d.js').MetadataTypeItemObj} MetadataTypeItemObj
18
+ * @typedef {import('../../types/mcdev.d.js').MetadataTypeMap} MetadataTypeMap
19
+ * @typedef {import('../../types/mcdev.d.js').MetadataTypeMapObj} MetadataTypeMapObj
20
+ * @typedef {import('../../types/mcdev.d.js').SoapRequestParams} SoapRequestParams
21
+ * @typedef {import('../../types/mcdev.d.js').TemplateMap} TemplateMap
22
+ * @typedef {import('../../types/mcdev.d.js').DataFilterItem} DataFilterItem
23
+ * @typedef {import('../../types/mcdev.d.js').DataExtensionFieldMap} DataExtensionFieldMap
24
+ * @typedef {import('../../types/mcdev.d.js').DataExtensionFieldItem} DataExtensionFieldItem
25
+ * @typedef {import('../../types/mcdev.d.js').DataFilterMap} DataFilterMap
26
+ * @typedef {import('../../types/mcdev.d.js').MultiMetadataTypeMap} MultiMetadataTypeMap
27
+ * @typedef {import('../../types/mcdev.d.js').FilterConditionSet} FilterConditionSet
28
+ * @typedef {import('../../types/mcdev.d.js').FilterCondition} FilterCondition
29
+ */
30
+
31
+ /**
32
+ * DataFilter (FilterDefinition) MetadataType
33
+ *
34
+ * @augments MetadataType
35
+ */
36
+ class DataFilter extends MetadataType {
37
+ static cache = {}; // type internal cache for various things
38
+ static deIdKeyMap;
39
+ static hidden = false;
40
+ /**
41
+ * Retrieves all records and saves it to disk
42
+ *
43
+ * @param {string} retrieveDir Directory where retrieved metadata directory will be saved
44
+ * @param {string[]} [_] unused parameter
45
+ * @param {string[]} [__] unused parameter
46
+ * @param {string} [key] customer key of single item to retrieve
47
+ * @returns {Promise.<{metadata: DataFilterMap, type: string}>} Promise of items
48
+ */
49
+ static async retrieve(retrieveDir, _, __, key) {
50
+ Util.logBeta(this.definition.type);
51
+ /** @type {DataFilterMap} */
52
+ const filterDefinitionMap = {};
53
+
54
+ const objectId = key ? await this._getObjectIdForSingleRetrieve(key) : null;
55
+ if (objectId) {
56
+ const metadataMapSingle = await super.retrieveREST(
57
+ null,
58
+ '/email/v1/filters/filterdefinition/' + objectId,
59
+ null,
60
+ key
61
+ );
62
+ Object.assign(filterDefinitionMap, metadataMapSingle.metadata);
63
+ } else {
64
+ const filterFolders = await this._getFilterFolderIds();
65
+
66
+ for (const folderId of filterFolders) {
67
+ const metadataMapFolder = await super.retrieveREST(
68
+ null,
69
+ 'email/v1/filters/filterdefinition/category/' +
70
+ folderId +
71
+ '?derivedFromType=1,2,3,4&',
72
+ null,
73
+ key
74
+ );
75
+ if (Object.keys(metadataMapFolder.metadata).length) {
76
+ Object.assign(filterDefinitionMap, metadataMapFolder.metadata);
77
+ if (filterDefinitionMap[key]) {
78
+ // if key was found we can stop checking other folders
79
+ break;
80
+ }
81
+ }
82
+ }
83
+ }
84
+ // /** @type {DataFilterMap} */
85
+ // const filterDefinitionMap = metadataTypeMapObj.metadata;
86
+ for (const item of Object.values(filterDefinitionMap)) {
87
+ // description is not returned when empty
88
+ item.description ||= '';
89
+ }
90
+ if (retrieveDir) {
91
+ // custom dataExtensionField caching
92
+ await this._cacheDeFields(filterDefinitionMap, 'retrieve');
93
+ // await this._cacheContactAttributes(filterDefinitionMap);
94
+ // await this._cacheMeasures(filterDefinitionMap);
95
+
96
+ const savedMetadata = await this.saveResults(filterDefinitionMap, retrieveDir);
97
+ Util.logger.info(
98
+ `Downloaded: ${this.definition.type} (${Object.keys(savedMetadata).length})` +
99
+ Util.getKeysString(key)
100
+ );
101
+ }
102
+
103
+ return { metadata: filterDefinitionMap, type: this.definition.type };
104
+ }
105
+ /**
106
+ * helper for {@link DataFilter.retrieve}
107
+ *
108
+ * @param {boolean} [recached] indicates if this is a recursive call after cache refresh
109
+ * @returns {Promise.<number[]>} Array of folder IDs
110
+ */
111
+ static async _getFilterFolderIds(recached = false) {
112
+ const fromCache =
113
+ this.cache.folderFilter || cache.getCache().folder
114
+ ? Object.values(this.cache.folderFilter || cache.getCache().folder)
115
+ .filter((item) => item.ContentType === 'filterdefinition')
116
+ .filter(
117
+ (item) =>
118
+ (!this.hidden && item.Path.startsWith('Data Filters')) ||
119
+ (this.hidden && !item.Path.startsWith('Data Filters'))
120
+ ) // only retrieve from Data Filters folder
121
+ .map((item) => item.ID)
122
+ : [];
123
+ if (fromCache.length) {
124
+ return fromCache;
125
+ }
126
+ if (recached) {
127
+ Util.logger.debug('_getFilterFolderIds: could not find filterdefinition folders');
128
+ return [];
129
+ }
130
+
131
+ const subTypeArr = ['hidden', 'filterdefinition'];
132
+ Util.logger.info(` - Caching dependent Metadata: folder`);
133
+ Util.logSubtypes(subTypeArr);
134
+
135
+ Folder.client = this.client;
136
+ Folder.buObject = this.buObject;
137
+ Folder.properties = this.properties;
138
+ this.cache.folderFilter = (await Folder.retrieveForCache(null, subTypeArr)).metadata;
139
+ return this._getFilterFolderIds(true);
140
+ }
141
+ /**
142
+ * helper for {@link DataFilter._cacheMeasures}
143
+ *
144
+ * @returns {Promise.<number[]>} Array of folder IDs
145
+ */
146
+ static async _getMeasureFolderIds() {
147
+ const fromCache =
148
+ this.cache.folderMeasure?.[this.buObject.mid] || cache.getCache().folder
149
+ ? Object.values(
150
+ this.cache.folderMeasure?.[this.buObject.mid] || cache.getCache().folder
151
+ )
152
+ .filter((item) => item.ContentType === 'measure')
153
+ .map((item) => item.ID)
154
+ : [];
155
+ if (fromCache.length) {
156
+ return fromCache;
157
+ }
158
+
159
+ const subTypeArr = ['measure'];
160
+ Util.logger.info(` - Caching dependent Metadata: folder`);
161
+ Util.logSubtypes(subTypeArr);
162
+
163
+ Folder.client = this.client;
164
+ Folder.buObject = this.buObject;
165
+ Folder.properties = this.properties;
166
+ this.cache.folderMeasure ||= {};
167
+ this.cache.folderMeasure[this.buObject.mid] = (
168
+ await Folder.retrieveForCache(null, subTypeArr)
169
+ ).metadata;
170
+ return this._getMeasureFolderIds();
171
+ }
172
+
173
+ /**
174
+ * helper for {@link DataFilter.retrieve}. uses cached dataExtensions to resolve dataExtensionFields
175
+ *
176
+ * @param {DataFilterMap} metadataTypeMap -
177
+ * @param {'retrieve'|'deploy'} [mode] -
178
+ */
179
+ static async _cacheDeFields(metadataTypeMap, mode) {
180
+ const deKeys =
181
+ mode === 'retrieve'
182
+ ? Object.values(metadataTypeMap)
183
+ .filter((item) => item.derivedFromObjectTypeName === 'DataExtension')
184
+ .filter((item) => item.derivedFromObjectId)
185
+ .map((item) => {
186
+ try {
187
+ const deKey = cache.searchForField(
188
+ 'dataExtension',
189
+ item.derivedFromObjectId,
190
+ 'ObjectID',
191
+ 'CustomerKey'
192
+ );
193
+ if (deKey) {
194
+ this.deIdKeyMap ||= {};
195
+ this.deIdKeyMap[item.derivedFromObjectId] = deKey;
196
+ return deKey;
197
+ }
198
+ } catch {
199
+ return null;
200
+ }
201
+ })
202
+ .filter(Boolean)
203
+ : Object.values(metadataTypeMap)
204
+ .filter((item) => item.r__source_dataExtension_key)
205
+ .map((item) => item.r__source_dataExtension_key)
206
+ .filter(
207
+ (deKey) =>
208
+ !this.cache.dataExtensionField ||
209
+ !this.cache.dataExtensionField[deKey]
210
+ )
211
+ .filter(Boolean);
212
+ if (deKeys.length) {
213
+ const deKeysUnique = [...new Set(deKeys)];
214
+ Util.logger.info(' - Caching dependent Metadata: dataExtensionField');
215
+ // only proceed with the download if we have dataExtension keys
216
+ const fieldOptions = {};
217
+ for (const deKey of deKeysUnique) {
218
+ fieldOptions.filter = fieldOptions.filter
219
+ ? {
220
+ leftOperand: {
221
+ leftOperand: 'DataExtension.CustomerKey',
222
+ operator: 'equals',
223
+ rightOperand: deKey,
224
+ },
225
+ operator: 'OR',
226
+ rightOperand: fieldOptions.filter,
227
+ }
228
+ : {
229
+ leftOperand: 'DataExtension.CustomerKey',
230
+ operator: 'equals',
231
+ rightOperand: deKey,
232
+ };
233
+ }
234
+ DataExtensionField.buObject = this.buObject;
235
+ DataExtensionField.client = this.client;
236
+ DataExtensionField.properties = this.properties;
237
+ this.saveDataExtensionFieldCacheToMap(
238
+ (await DataExtensionField.retrieveForCacheDE(fieldOptions, ['Name', 'ObjectID']))
239
+ .metadata
240
+ );
241
+ }
242
+ }
243
+ /**
244
+ * helper for {@link DataFilter._cacheDeFields}
245
+ *
246
+ * @param {DataExtensionFieldMap} deFieldCache -
247
+ */
248
+ static saveDataExtensionFieldCacheToMap(deFieldCache) {
249
+ this.cache.dataExtensionField ||= {};
250
+
251
+ if (!deFieldCache) {
252
+ return;
253
+ }
254
+ for (const field of Object.values(deFieldCache)) {
255
+ if (!field?.DataExtension?.CustomerKey) {
256
+ continue;
257
+ }
258
+ if (!this.cache.dataExtensionField[field.DataExtension.CustomerKey]) {
259
+ /** @type {Object.<string, DataExtensionFieldItem[]>} */
260
+ this.cache.dataExtensionField[field.DataExtension.CustomerKey] = [];
261
+ }
262
+ this.cache.dataExtensionField[field.DataExtension.CustomerKey].push(field);
263
+ }
264
+ }
265
+ /**
266
+ * helper for {@link DataFilter.retrieve}
267
+ *
268
+ * @param {DataFilterMap} metadataTypeMap -
269
+ */
270
+ static async _cacheContactAttributes(metadataTypeMap) {
271
+ if (this.cache.contactAttributes?.[this.buObject.mid]) {
272
+ return;
273
+ }
274
+ const subscriberFilters = Object.values(metadataTypeMap)
275
+ .filter((item) => item.derivedFromObjectTypeName === 'SubscriberAttributes')
276
+ .filter((item) => item.derivedFromObjectId);
277
+ if (subscriberFilters.length) {
278
+ Util.logger.info(' - Caching dependent Metadata: contactAttributes');
279
+ const response = await this.client.rest.get('/email/v1/Contacts/Attributes/');
280
+ const keyFieldBackup = this.definition.keyField;
281
+ this.definition.keyField = 'id';
282
+ this.cache.contactAttributes ||= {};
283
+ this.cache.contactAttributes[this.buObject.mid] = this.parseResponseBody(response);
284
+ this.definition.keyField = keyFieldBackup;
285
+ }
286
+ }
287
+ /**
288
+ * helper for {@link DataFilter.retrieve}
289
+ *
290
+ * @param {DataFilterMap} metadataTypeMap -
291
+ */
292
+ static async _cacheMeasures(metadataTypeMap) {
293
+ if (this.cache.measures?.[this.buObject.mid]) {
294
+ return;
295
+ }
296
+ const subscriberFilters = Object.values(metadataTypeMap)
297
+ .filter((item) => item.derivedFromObjectTypeName === 'SubscriberAttributes')
298
+ .filter((item) => item.derivedFromObjectId);
299
+ const measureFolders = await this._getMeasureFolderIds();
300
+ if (subscriberFilters.length) {
301
+ Util.logger.info(' - Caching dependent Metadata: measure');
302
+ const response = { items: [] };
303
+ for (const folderId of measureFolders) {
304
+ const metadataMapFolder = await this.client.rest.getBulk(
305
+ 'email/v1/Measures/category/' + folderId + '/',
306
+ 250 // 250 is what the GUI is using
307
+ );
308
+ if (Object.keys(metadataMapFolder.items).length) {
309
+ response.items.push(...metadataMapFolder.items);
310
+ }
311
+ }
312
+
313
+ const keyFieldBackup = this.definition.keyField;
314
+ this.definition.keyField = 'measureID';
315
+ this.cache.measures ||= {};
316
+ this.cache.measures[this.buObject.mid] = this.parseResponseBody(response);
317
+ this.definition.keyField = keyFieldBackup;
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Retrieves all records for caching
323
+ *
324
+ * @returns {Promise.<{metadata: DataFilterMap, type: string}>} Promise of items
325
+ */
326
+ static async retrieveForCache() {
327
+ return this.retrieve(null);
328
+ }
329
+
330
+ /**
331
+ * parses retrieved Metadata before saving
332
+ *
333
+ * @param {DataFilterItem} metadata a single record
334
+ * @returns {Promise.<DataFilterItem>} parsed metadata definition
335
+ */
336
+ static async postRetrieveTasks(metadata) {
337
+ // if (metadata.derivedFromType > 4) {
338
+ // // GUI only shows types 1,2,3,4; lets mimic that here.
339
+ // // type 6 seems to be journey related. Maybe we need to change that again in the future
340
+ // return;
341
+ // }
342
+ // folder
343
+ super.setFolderPath(metadata);
344
+
345
+ // parse XML filter for further processing in JSON format
346
+ const xmlToJson = new XMLParser({
347
+ preserveOrder: false, // XML parser returns undefined if this is true
348
+ ignoreAttributes: false,
349
+ allowBooleanAttributes: true,
350
+ });
351
+ metadata.c__filterDefinition = xmlToJson.parse(
352
+ metadata.filterDefinitionXml
353
+ )?.FilterDefinition;
354
+ delete metadata.filterDefinitionXml;
355
+ switch (metadata.derivedFromType) {
356
+ case 1: {
357
+ // if (metadata.c__filterDefinition['@_Source'] === 'SubscriberAttribute') {
358
+ // if (
359
+ // metadata.derivedFromObjectId &&
360
+ // metadata.derivedFromObjectId !== '00000000-0000-0000-0000-000000000000'
361
+ // ) {
362
+ // // Lists
363
+ // try {
364
+ // metadata.r__source_list_PathName = cache.getListPathName(
365
+ // metadata.derivedFromObjectId,
366
+ // 'ObjectID'
367
+ // );
368
+ // } catch {
369
+ // Util.logger.warn(
370
+ // ` - skipping ${this.definition.type} ${metadata.key}: list ${metadata.derivedFromObjectId} not found on current or Parent BU`
371
+ // );
372
+ // }
373
+ // } else {
374
+ // // SubscriberAttributes
375
+ // // - nothing to do
376
+ // }
377
+ // }
378
+ // // SubscriberAttributes
379
+ // this._postRetrieve_resolveAttributeIds(metadata);
380
+ // delete metadata.derivedFromObjectId;
381
+ // delete metadata.derivedFromObjectName;
382
+ // delete metadata.derivedFromObjectTypeName;
383
+ // delete metadata.derivedFromType;
384
+ // delete metadata.c__filterDefinition['@_Source'];
385
+ break;
386
+ }
387
+ case 2: {
388
+ // DataExtension + XXX?
389
+ if (
390
+ metadata.c__filterDefinition['@_Source'] === 'Meta' ||
391
+ metadata.derivedFromObjectId === '00000000-0000-0000-0000-000000000000'
392
+ ) {
393
+ // TODO - weird so far not understood case of Source=Meta
394
+ // sample: <FilterDefinition Source=\"Meta\"><Include><ConditionSet Operator=\"OR\" ConditionSetName=\"Individual Filter Grouping\"><Condition ID=\"55530cec-1df4-e611-80cc-1402ec7222b4\" isParam=\"false\" isPathed=\"true\" pathAttrGroupID=\"75530cec-1df4-e611-80cc-1402ec7222b4\" Operator=\"Equal\"><Value><![CDATA[994607]]></Value></Condition><Condition ID=\"55530cec-1df4-e611-80cc-1402ec7222b4\" isParam=\"false\" isPathed=\"true\" pathAttrGroupID=\"75530cec-1df4-e611-80cc-1402ec7222b4\" Operator=\"Equal\"><Value><![CDATA[3624804]]></Value></Condition></ConditionSet></Include><Exclude></Exclude></FilterDefinition>
395
+ } else if (metadata.c__filterDefinition['@_Source'] === 'DataExtension') {
396
+ // DataExtension
397
+ try {
398
+ metadata.r__source_dataExtension_key =
399
+ this.deIdKeyMap?.[metadata.derivedFromObjectId] ||
400
+ cache.searchForField(
401
+ 'dataExtension',
402
+ metadata.derivedFromObjectId,
403
+ 'ObjectID',
404
+ 'CustomerKey'
405
+ );
406
+ delete metadata.derivedFromObjectName;
407
+ delete metadata.derivedFromObjectTypeName;
408
+ } catch {
409
+ Util.logger.debug(
410
+ ` - skipping ${this.definition.type} ${metadata.key}: dataExtension ${metadata.derivedFromObjectId} not found on BU`
411
+ );
412
+ return;
413
+ }
414
+ this._resolveFields(metadata, 'postRetrieve');
415
+
416
+ delete metadata.derivedFromObjectId;
417
+ delete metadata.derivedFromType;
418
+ delete metadata.c__filterDefinition['@_Source'];
419
+ delete metadata.c__filterDefinition['@_SourceID'];
420
+ }
421
+ break;
422
+ }
423
+ }
424
+
425
+ return metadata;
426
+ }
427
+
428
+ /**
429
+ * helper for {@link postRetrieveTasks}
430
+ *
431
+ * @param {DataFilterItem} metadata -
432
+ * @param {'postRetrieve'|'preDeploy'} mode -
433
+ * @param {DataExtensionFieldItem[]} [fieldCache] -
434
+ * @param {FilterConditionSet} [filter] -
435
+ * @returns {void}
436
+ */
437
+ static _resolveFields(metadata, mode, fieldCache, filter) {
438
+ if (!filter) {
439
+ return this._resolveFields(
440
+ metadata,
441
+ mode,
442
+ this.cache.dataExtensionField[metadata.r__source_dataExtension_key],
443
+ metadata.c__filterDefinition?.ConditionSet
444
+ );
445
+ }
446
+ if (filter.Condition) {
447
+ const conditionsArr = Array.isArray(filter.Condition)
448
+ ? filter.Condition
449
+ : [filter.Condition];
450
+ if (mode === 'postRetrieve') {
451
+ for (const condition of conditionsArr) {
452
+ this._postRetrieve_resolveFieldIdsCondition(metadata, condition, fieldCache);
453
+ }
454
+ } else if (mode === 'preDeploy') {
455
+ for (const condition of conditionsArr) {
456
+ this._preDeploy_resolveFieldNamesCondition(metadata, condition, fieldCache);
457
+ }
458
+ }
459
+ }
460
+ if (filter.ConditionSet) {
461
+ const conditionSet = Array.isArray(filter.ConditionSet)
462
+ ? filter.ConditionSet
463
+ : [filter.ConditionSet];
464
+ for (const cs of conditionSet) {
465
+ this._resolveFields(metadata, mode, fieldCache, cs);
466
+ }
467
+ }
468
+ }
469
+ /**
470
+ * helper for {@link _resolveFields}
471
+ *
472
+ * @param {DataFilterItem} metadata -
473
+ * @param {FilterCondition} condition -
474
+ * @param {DataExtensionFieldItem[]} fieldCache -
475
+ * @returns {void}
476
+ */
477
+ static _postRetrieve_resolveFieldIdsCondition(metadata, condition, fieldCache) {
478
+ condition.r__dataExtensionField_name = fieldCache.find(
479
+ (field) => field.ObjectID === condition['@_ID']
480
+ )?.Name;
481
+ if (condition.r__dataExtensionField_name) {
482
+ delete condition['@_ID'];
483
+ } else {
484
+ Util.logger.warn(
485
+ ` - ${this.definition.type} ${metadata[this.definition.keyField]}: could not resolve dataExtensionField with ID ${condition['@_ID']} in condition`
486
+ );
487
+ }
488
+ if (['IsEmpty', 'IsNotEmpty'].includes(condition['@_Operator'])) {
489
+ delete condition.Value;
490
+ }
491
+ }
492
+ /**
493
+ * helper for {@link _resolveFields}
494
+ *
495
+ * @param {DataFilterItem} metadata -
496
+ * @param {FilterCondition} condition -
497
+ * @param {DataExtensionFieldItem[]} fieldCache -
498
+ * @returns {void}
499
+ */
500
+ static _preDeploy_resolveFieldNamesCondition(metadata, condition, fieldCache) {
501
+ condition['@_ID'] = fieldCache.find(
502
+ (field) => field.Name === condition.r__dataExtensionField_name
503
+ )?.ObjectID;
504
+ if (condition['@_ID']) {
505
+ delete condition.r__dataExtensionField_name;
506
+ } else {
507
+ throw new Error(
508
+ ` - ${this.definition.type} ${metadata[this.definition.keyField]}: could not resolve dataExtensionField with Name ${condition.r__dataExtensionField_name} in condition`
509
+ );
510
+ }
511
+ if (['IsEmpty', 'IsNotEmpty'].includes(condition['@_Operator'])) {
512
+ condition.Value ||= '';
513
+ } else if (condition?.Value && typeof condition.Value !== 'object') {
514
+ // allow adding cdata
515
+ // @ts-ignore
516
+ condition.Value = { cdata: condition.Value };
517
+ }
518
+ }
519
+ /**
520
+ * helper for {@link postRetrieveTasks}
521
+ *
522
+ * @param {DataFilterItem} metadata -
523
+ * @param {object} [filter] -
524
+ * @returns {void}
525
+ */
526
+ static _postRetrieve_resolveAttributeIds(metadata, filter) {
527
+ if (!filter) {
528
+ return this._postRetrieve_resolveAttributeIds(
529
+ metadata,
530
+ metadata.c__filterDefinition?.ConditionSet
531
+ );
532
+ }
533
+ const contactAttributes = this.cache.contactAttributes[this.buObject.mid];
534
+ const measures = this.cache.measures[this.buObject.mid];
535
+ const conditionsArr = Array.isArray(filter.Condition)
536
+ ? filter.Condition
537
+ : [filter.Condition];
538
+ for (const condition of conditionsArr) {
539
+ condition['@_ID'] += '';
540
+ if (condition['@_SourceType'] === 'Measure' && measures[condition['@_ID']]) {
541
+ condition.r__measure = measures[condition['@_ID']]?.name;
542
+ delete condition['@_ID'];
543
+ } else if (
544
+ condition['@_SourceType'] !== 'Measure' &&
545
+ contactAttributes[condition['@_ID']]
546
+ ) {
547
+ condition.r__contactAttribute = contactAttributes[condition['@_ID']]?.name;
548
+ delete condition['@_ID'];
549
+ }
550
+ if (['IsEmpty', 'IsNotEmpty'].includes(condition['@_Operator'])) {
551
+ delete condition.Value;
552
+ }
553
+ }
554
+ if (filter.ConditionSet) {
555
+ this._postRetrieve_resolveAttributeIds(metadata, filter.ConditionSet);
556
+ }
557
+ }
558
+
559
+ /**
560
+ * prepares a item for deployment
561
+ *
562
+ * @param {DataFilterItem} metadata a single record
563
+ * @returns {Promise.<DataFilterItem>} Promise of updated single item
564
+ */
565
+ static async preDeployTasks(metadata) {
566
+ // folder
567
+ super.setFolderId(metadata);
568
+
569
+ // disable updates to r__source_dataExtension_key
570
+ // the API and GUI prevent changes to the source data extensions.
571
+ // to avoid confusion and accidental changes, we will reset the values from cache before deployment
572
+ const normalizedKey = File.reverseFilterIllegalFilenames(
573
+ metadata[this.definition.keyField]
574
+ );
575
+ const cachedVersion = cache.getByKey(this.definition.type, normalizedKey);
576
+
577
+ // DataExtension
578
+ if (metadata.r__source_dataExtension_key) {
579
+ metadata.derivedFromObjectId = cache.searchForField(
580
+ 'dataExtension',
581
+ metadata.r__source_dataExtension_key,
582
+ 'CustomerKey',
583
+ 'ObjectID'
584
+ );
585
+ if (
586
+ cachedVersion &&
587
+ cachedVersion.derivedFromObjectId !== metadata.derivedFromObjectId
588
+ ) {
589
+ throw new Error(
590
+ `Updating r__source_dataExtension_key is not allowed. You need to delete and re-create the dataFilter to change this.`
591
+ );
592
+ }
593
+ metadata.derivedFromObjectName = cache.searchForField(
594
+ 'dataExtension',
595
+ metadata.r__source_dataExtension_key,
596
+ 'CustomerKey',
597
+ 'Name'
598
+ );
599
+ metadata.derivedFromObjectTypeName = 'DataExtension';
600
+ metadata.derivedFromType = 2;
601
+ metadata.c__filterDefinition['@_Source'] = 'DataExtension';
602
+ metadata.c__filterDefinition['@_SourceID'] = metadata.derivedFromObjectId;
603
+ this._resolveFields(metadata, 'preDeploy');
604
+
605
+ delete metadata.r__source_dataExtension_key;
606
+ }
607
+
608
+ const jsonToXml = new XMLBuilder({
609
+ preserveOrder: false, // XML Builder returns undefined if this is true
610
+ ignoreAttributes: false,
611
+ cdataPropName: 'cdata',
612
+ });
613
+ metadata.filterDefinitionXml = jsonToXml.build({
614
+ FilterDefinition: metadata.c__filterDefinition,
615
+ });
616
+ delete metadata.c__filterDefinition;
617
+ return metadata;
618
+ }
619
+
620
+ /**
621
+ * MetadataType upsert, after retrieving from target and comparing to check if create or update operation is needed.
622
+ *
623
+ * @param {MetadataTypeMap} metadataMap metadata mapped by their keyField
624
+ * @param {string} deployDir directory where deploy metadata are saved
625
+ * @param {boolean} [runUpsertSequentially] when a type has self-dependencies creates need to run one at a time and created/changed keys need to be cached to ensure following creates/updates have thoses keys available
626
+ * @returns {Promise.<MetadataTypeMap>} keyField => metadata map
627
+ */
628
+ static async upsert(metadataMap, deployDir, runUpsertSequentially = false) {
629
+ Util.logBeta(this.definition.type);
630
+ await this._cacheDeFields(metadataMap, 'deploy');
631
+ return super.upsert(metadataMap, deployDir, runUpsertSequentially);
632
+ }
633
+ /**
634
+ * Creates a single item
635
+ *
636
+ * @param {DataFilterItem} metadata a single item
637
+ * @returns {Promise.<DataFilterItem>} Promise
638
+ */
639
+ static create(metadata) {
640
+ return super.createREST(metadata, '/email/v1/filters/filterdefinition/');
641
+ }
642
+ /**
643
+ * Updates a single item
644
+ *
645
+ * @param {DataFilterItem} metadata a single item
646
+ * @returns {Promise.<DataFilterItem>} Promise
647
+ */
648
+ static update(metadata) {
649
+ return super.updateREST(
650
+ metadata,
651
+ '/email/v1/filters/filterdefinition/' + metadata[this.definition.idField]
652
+ );
653
+ }
654
+
655
+ /**
656
+ * helper to allow us to select single metadata entries via REST
657
+ *
658
+ * @private
659
+ * @param {string} key customer key
660
+ * @returns {Promise.<string>} objectId or empty string
661
+ */
662
+ static async _getObjectIdForSingleRetrieve(key) {
663
+ const name = key.startsWith('name:') ? key.slice(5) : null;
664
+ const response = await this.client.soap.retrieve(this.definition.soapType, ['ObjectID'], {
665
+ filter: {
666
+ leftOperand: name ? 'Name' : 'CustomerKey',
667
+ operator: 'equals',
668
+ rightOperand: name || key,
669
+ },
670
+ });
671
+ return response?.Results?.length ? response.Results[0].ObjectID : null;
672
+ }
673
+
674
+ /**
675
+ * Delete a metadata item from the specified business unit
676
+ *
677
+ * @param {string} key Identifier of data extension
678
+ * @returns {Promise.<boolean>} deletion success flag
679
+ */
680
+ static async deleteByKey(key) {
681
+ // delete only works with the query's object id
682
+ const objectId = key ? await this._getObjectIdForSingleRetrieve(key) : null;
683
+ if (!objectId) {
684
+ await this.deleteNotFound(key);
685
+ return false;
686
+ }
687
+ return super.deleteByKeyREST('/email/v1/filters/filterdefinition/' + objectId, key);
688
+ }
689
+ }
690
+ // Assign definition to static attributes
691
+ import MetadataTypeDefinitions from '../MetadataTypeDefinitions.js';
692
+ DataFilter.definition = MetadataTypeDefinitions.dataFilter;
693
+
694
+ export default DataFilter;