Repository: Accenture/sfmc-devtools
Pull request #500 (2022-11-01 19:40:25)

4.1.4
release


Patch:
diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml
index 170ea85c..8e428c3a 100644
--- a/.github/ISSUE_TEMPLATE/bug.yml
+++ b/.github/ISSUE_TEMPLATE/bug.yml
@@ -39,6 +39,7 @@ body:
           label: Version
           description: What version of our software are you running? (mcdev --version)
           options:
+              - 4.1.4
               - 4.1.3
               - 4.1.2
               - 4.1.1
diff --git a/.github/workflows/code-analysis.yml b/.github/workflows/code-analysis.yml
index ad939b57..384d2164 100644
--- a/.github/workflows/code-analysis.yml
+++ b/.github/workflows/code-analysis.yml
@@ -34,9 +34,9 @@ jobs:
 
         steps:
             - name: Checkout repository
-              uses: actions/checkout@v2
+              uses: actions/checkout@v3
 
-            - uses: actions/setup-node@v2
+            - uses: actions/setup-node@v3
               with:
                   node-version: 16
                   registry-url: https://registry.npmjs.org/
diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml
index d19b38e8..09f91bf5 100644
--- a/.github/workflows/npm-publish.yml
+++ b/.github/workflows/npm-publish.yml
@@ -11,8 +11,8 @@ jobs:
     build:
         runs-on: ubuntu-latest
         steps:
-            - uses: actions/checkout@v2
-            - uses: actions/setup-node@v1
+            - uses: actions/checkout@v3
+            - uses: actions/setup-node@v3
               with:
                   node-version: 16
             - run: npm ci
@@ -22,8 +22,8 @@ jobs:
         needs: build
         runs-on: ubuntu-latest
         steps:
-            - uses: actions/checkout@v2
-            - uses: actions/setup-node@v1
+            - uses: actions/checkout@v3
+            - uses: actions/setup-node@v3
               with:
                   node-version: 16
                   registry-url: https://registry.npmjs.org/
diff --git a/README.md b/README.md
index f9fb6091..08c40147 100644
--- a/README.md
+++ b/README.md
@@ -123,7 +123,7 @@ If you experience issues installing Accenture SFMC DevTools, please check out th
 1. Install Accenture SFMC DevTools by running `npm install -g mcdev` (prefix with `sudo` on MacOS)
    - If you get an error, please see the below troubleshooting section.
 
-When completed run `mcdev --version` and it will show you which version you installed (e.g. `4.1.3`).
+When completed run `mcdev --version` and it will show you which version you installed (e.g. `4.1.4`).
 
 > **_Side note for proud nerds_:**
 >
@@ -277,10 +277,10 @@ _Note: Regardless of which tag or branch you install_
 **Install specific version (using a version tag on npm):**
 
 ```bash
-npm install -g mcdev@4.1.3
+npm install -g mcdev@4.1.4
 ```
 
-**Warning**: When you used the above method to install Accenture SFMC DevTools for a specific version or tag, trying to [update Accenture SFMC DevTools](#updating-mcdev) might not download the most recently published official version but instead stay on the version or branch you previously selected (in the above examples: develop, 4.1.3)!
+**Warning**: When you used the above method to install Accenture SFMC DevTools for a specific version or tag, trying to [update Accenture SFMC DevTools](#updating-mcdev) might not download the most recently published official version but instead stay on the version or branch you previously selected (in the above examples: develop, 4.1.4)!
 
 > **Note**: The version is currently _not_ updated on the developer branch until a new release is published. Hence, you will not see a change if you run `mcdev --version`.
 
@@ -1493,7 +1493,7 @@ Assuming you cloned Accenture SFMC DevTools into `C:\repos\sfmc-devtools\` (or `
 
 This should tell npm to create a symlink to your cloned local directoty, allowing you to see updates you make in your mcdev repo instantly.
 
-To test your new **global** developer setup, run `mcdev --version` in CLI which should return the current version (e.g. `4.1.3`). Then, go into your mcdev repo and update the version with the suffix `-dev`, e.g. to `4.1.3-dev` and then run `mcdev --version` again to verify that your change propagates instantly.
+To test your new **global** developer setup, run `mcdev --version` in CLI which should return the current version (e.g. `4.1.4`). Then, go into your mcdev repo and update the version with the suffix `-dev`, e.g. to `4.1.4-dev` and then run `mcdev --version` again to verify that your change propagates instantly.
 
 > **Not recommended:** Alternatively, you can install it locally only by opening a terminal in your project directory and executing `npm install --save-dev "C:\repos\sfmc-devtools"`
 > To run the local version you need to prepend "npx" before your commands, e.g. `npx mcdev --version`
@@ -1531,7 +1531,7 @@ The following explains how you _could_ install it locally for certain edge cases
 4. Afterwards, install Accenture SFMC DevTools by running `npm install --save-dev mcdev`
    - If you get an error, please see the below troubleshooting section.
 
-When completed run `mcdev --version` and it will show you which version you installed (e.g. `4.1.3`).
+When completed run `mcdev --version` and it will show you which version you installed (e.g. `4.1.4`).
 
 ### 9.3. NPM Scripts
 
diff --git a/docs/dist/documentation.md b/docs/dist/documentation.md
index b59c07d2..e5d46ef3 100644
--- a/docs/dist/documentation.md
+++ b/docs/dist/documentation.md
@@ -836,6 +836,7 @@ FileTransfer MetadataType
     * [.buildDefinitionForNested(templateDir, targetDir, metadata, templateVariables, templateName)](#Asset.buildDefinitionForNested) ⇒ <code>Promise.&lt;void&gt;</code>
     * [.buildTemplateForNested(templateDir, targetDir, metadata, templateVariables, templateName)](#Asset.buildTemplateForNested) ⇒ <code>Promise.&lt;void&gt;</code>
     * [._buildForNested(templateDir, targetDir, metadata, templateVariables, templateName, mode)](#Asset._buildForNested) ⇒ <code>Promise.&lt;void&gt;</code>
+    * [.setFolderPath(metadata)](#Asset.setFolderPath)
     * [.parseMetadata(metadata)](#Asset.parseMetadata) ⇒ <code>TYPE.CodeExtractItem</code>
     * [._mergeCode(metadata, deployDir, subType, [templateName], [fileListOnly])](#Asset._mergeCode) ⇒ <code>Promise.&lt;Array.&lt;TYPE.CodeExtract&gt;&gt;</code>
     * [._mergeCode_slots(prefix, metadataSlots, readDirArr, subtypeExtension, subDirArr, fileList, customerKey, [templateName], [fileListOnly])](#Asset._mergeCode_slots) ⇒ <code>Promise.&lt;void&gt;</code>
@@ -1072,6 +1073,17 @@ handles extracted code if any are found for complex types
 | templateName | <code>string</code> | name of the template to be built |
 | mode | <code>&#x27;definition&#x27;</code> \| <code>&#x27;template&#x27;</code> | defines what we use this helper for |
 
+<a name="Asset.setFolderPath"></a>
+
+### Asset.setFolderPath(metadata)
+generic script that retrieves the folder path from cache and updates the given metadata with it after retrieve
+
+**Kind**: static method of [<code>Asset</code>](#Asset)  
+
+| Param | Type | Description |
+| --- | --- | --- |
+| metadata | <code>TYPE.MetadataTypeItem</code> | a single script activity definition |
+
 <a name="Asset.parseMetadata"></a>
 
 ### Asset.parseMetadata(metadata) ⇒ <code>TYPE.CodeExtractItem</code>
@@ -1254,6 +1266,7 @@ Automation MetadataType
     * [.preDeployTasks(metadata)](#Automation.preDeployTasks) ⇒ <code>Promise.&lt;TYPE.AutomationItem&gt;</code>
     * [.validateDeployMetadata(metadata)](#Automation.validateDeployMetadata) ⇒ <code>boolean</code>
     * [.postDeployTasks(metadata, originalMetadata)](#Automation.postDeployTasks) ⇒ <code>Promise.&lt;void&gt;</code>
+    * [.setFolderPath(metadata)](#Automation.setFolderPath)
     * [.parseMetadata(metadata)](#Automation.parseMetadata) ⇒ <code>TYPE.AutomationItem</code>
     * [._buildSchedule(scheduleObject)](#Automation._buildSchedule) ⇒ <code>TYPE.AutomationScheduleSoap</code>
     * [._calcTime(offsetServer, dateInput, [offsetInput])](#Automation._calcTime) ⇒ <code>string</code>
@@ -1393,6 +1406,17 @@ Gets executed after deployment of metadata type
 | metadata | <code>TYPE.AutomationMap</code> | metadata mapped by their keyField |
 | originalMetadata | <code>TYPE.AutomationMap</code> | metadata to be updated (contains additioanl fields) |
 
+<a name="Automation.setFolderPath"></a>
+
+### Automation.setFolderPath(metadata)
+generic script that retrieves the folder path from cache and updates the given metadata with it after retrieve
+
+**Kind**: static method of [<code>Automation</code>](#Automation)  
+
+| Param | Type | Description |
+| --- | --- | --- |
+| metadata | <code>TYPE.MetadataTypeItem</code> | a single script activity definition |
+
 <a name="Automation.parseMetadata"></a>
 
 ### Automation.parseMetadata(metadata) ⇒ <code>TYPE.AutomationItem</code>
@@ -1511,6 +1535,7 @@ ContentArea MetadataType
 * [ContentArea](#ContentArea) ⇐ [<code>MetadataType</code>](#MetadataType)
     * [.retrieve(retrieveDir, [_], [__], [___], [key])](#ContentArea.retrieve) ⇒ <code>Promise.&lt;TYPE.MetadataTypeMapObj&gt;</code>
     * [.postRetrieveTasks(metadata)](#ContentArea.postRetrieveTasks) ⇒ <code>TYPE.MetadataTypeItem</code>
+    * [.setFolderPath(metadata)](#ContentArea.setFolderPath)
     * [.parseMetadata(metadata)](#ContentArea.parseMetadata) ⇒ <code>TYPE.MetadataTypeItem</code>
 
 <a name="ContentArea.retrieve"></a>
@@ -1541,6 +1566,17 @@ manages post retrieve steps
 | --- | --- | --- |
 | metadata | <code>TYPE.MetadataTypeItem</code> | a single item |
 
+<a name="ContentArea.setFolderPath"></a>
+
+### ContentArea.setFolderPath(metadata)
+generic script that retrieves the folder path from cache and updates the given metadata with it after retrieve
+
+**Kind**: static method of [<code>ContentArea</code>](#ContentArea)  
+
+| Param | Type | Description |
+| --- | --- | --- |
+| metadata | <code>TYPE.MetadataTypeItem</code> | a single script activity definition |
+
 <a name="ContentArea.parseMetadata"></a>
 
 ### ContentArea.parseMetadata(metadata) ⇒ <code>TYPE.MetadataTypeItem</code>
@@ -1576,6 +1612,7 @@ DataExtension MetadataType
     * [.postDeleteTasks(buObject, customerKey)](#DataExtension.postDeleteTasks) ⇒ <code>void</code>
     * [.retrieveForCache(buObject)](#DataExtension.retrieveForCache) ⇒ <code>Promise.&lt;{metadata: TYPE.DataExtensionMap, type: string}&gt;</code>
     * [.retrieveAsTemplate(templateDir, name, templateVariables)](#DataExtension.retrieveAsTemplate) ⇒ <code>Promise.&lt;{metadata: TYPE.DataExtensionMap, type: string}&gt;</code>
+    * [.setFolderPath(metadata)](#DataExtension.setFolderPath)
     * [.getFilesToCommit(keyArr)](#DataExtension.getFilesToCommit) ⇒ <code>Array.&lt;string&gt;</code>
 
 <a name="DataExtension.upsert"></a>
@@ -1758,6 +1795,18 @@ Retrieves dataExtension metadata in template format.
 | name | <code>string</code> | name of the metadata item |
 | templateVariables | <code>TYPE.TemplateMap</code> | variables to be replaced in the metadata |
 
+<a name="DataExtension.setFolderPath"></a>
+
+### DataExtension.setFolderPath(metadata)
+dataExtension logic that retrieves the folder path from cache and updates the given metadata with it after retrieve
+it also sets the content type which is basically the subtype
+
+**Kind**: static method of [<code>DataExtension</code>](#DataExtension)  
+
+| Param | Type | Description |
+| --- | --- | --- |
+| metadata | <code>TYPE.MetadataTypeItem</code> | a single script activity definition |
+
 <a name="DataExtension.getFilesToCommit"></a>
 
 ### DataExtension.getFilesToCommit(keyArr) ⇒ <code>Array.&lt;string&gt;</code>
@@ -2946,6 +2995,7 @@ Provides default functionality that can be overwritten by child metadata type cl
     * [.deploy(metadata, deployDir, retrieveDir, buObject)](#MetadataType.deploy) ⇒ <code>Promise.&lt;TYPE.MetadataTypeMap&gt;</code>
     * [.postDeployTasks(metadata, originalMetadata)](#MetadataType.postDeployTasks) ⇒ <code>void</code>
     * [.postRetrieveTasks(metadata, targetDir, [isTemplating])](#MetadataType.postRetrieveTasks) ⇒ <code>TYPE.MetadataTypeItem</code>
+    * [.setFolderPath(metadata)](#MetadataType.setFolderPath)
     * [.retrieve(retrieveDir, [additionalFields], buObject, [subType], [key])](#MetadataType.retrieve) ⇒ <code>Promise.&lt;TYPE.MetadataTypeMapObj&gt;</code>
     * [.retrieveChangelog([buObject], [additionalFields], [subType])](#MetadataType.retrieveChangelog) ⇒ <code>Promise.&lt;TYPE.MetadataTypeMapObj&gt;</code>
     * [.retrieveForCache(buObject, [subType])](#MetadataType.retrieveForCache) ⇒ <code>Promise.&lt;TYPE.MetadataTypeMapObj&gt;</code>
@@ -3069,6 +3119,17 @@ Gets executed after retreive of metadata type
 | targetDir | <code>string</code> | folder where retrieves should be saved |
 | [isTemplating] | <code>boolean</code> | signals that we are retrieving templates |
 
+<a name="MetadataType.setFolderPath"></a>
+
+### MetadataType.setFolderPath(metadata)
+generic script that retrieves the folder path from cache and updates the given metadata with it after retrieve
+
+**Kind**: static method of [<code>MetadataType</code>](#MetadataType)  
+
+| Param | Type | Description |
+| --- | --- | --- |
+| metadata | <code>TYPE.MetadataTypeItem</code> | a single script activity definition |
+
 <a name="MetadataType.retrieve"></a>
 
 ### MetadataType.retrieve(retrieveDir, [additionalFields], buObject, [subType], [key]) ⇒ <code>Promise.&lt;TYPE.MetadataTypeMapObj&gt;</code>
diff --git a/lib/Retriever.js b/lib/Retriever.js
index 2b555786..f70bc1a6 100644
--- a/lib/Retriever.js
+++ b/lib/Retriever.js
@@ -74,8 +74,8 @@ class Retriever {
             try {
                 let result;
                 if (!metadataTypes.includes(type) && !metadataTypes.includes(metadataType)) {
-                    if (changelogOnly) {
-                        // no extra caching needed for list view
+                    if (changelogOnly && type !== 'folder') {
+                        // no extra caching needed for list view except for folders
                         continue;
                     }
                     Util.logger.info(`Caching dependent Metadata: ${metadataType}`);
@@ -115,6 +115,12 @@ class Retriever {
                                   )
                               )
                           ));
+                    if (changelogOnly) {
+                        // add folder to changelog
+                        for (const key of Object.keys(result.metadata)) {
+                            MetadataTypeInfo[type].setFolderPath(result.metadata[key]);
+                        }
+                    }
                 }
                 if (result) {
                     if (Array.isArray(result)) {
diff --git a/lib/metadataTypes/Asset.js b/lib/metadataTypes/Asset.js
index 0e3c08b3..82d4d211 100644
--- a/lib/metadataTypes/Asset.js
+++ b/lib/metadataTypes/Asset.js
@@ -755,15 +755,12 @@ class Asset extends MetadataType {
             }
         }
     }
-
     /**
-     * parses retrieved Metadata before saving
+     * generic script that retrieves the folder path from cache and updates the given metadata with it after retrieve
      *
-     * @param {TYPE.AssetItem} metadata a single asset definition
-     * @returns {TYPE.CodeExtractItem} parsed metadata definition
+     * @param {TYPE.MetadataTypeItem} metadata a single script activity definition
      */
-    static parseMetadata(metadata) {
-        // folder
+    static setFolderPath(metadata) {
         try {
             metadata.r__folder_Path = cache.searchForField(
                 'folder',
@@ -773,12 +770,23 @@ class Asset extends MetadataType {
             );
             delete metadata.category;
         } catch (ex) {
-            // ! if we don't catch this error here we end up saving the actual asset but not its corresponding JSON
-            Util.logger.debug(ex.message);
             Util.logger.warn(
-                ` - Could not find folder with ID ${metadata.category.id} for '${metadata.name}' (${metadata.customerKey})`
+                ` - ${this.definition.type} '${metadata[this.definition.nameField]}' (${
+                    metadata[this.definition.keyField]
+                }): Could not find folder (${ex.message})`
             );
         }
+    }
+
+    /**
+     * parses retrieved Metadata before saving
+     *
+     * @param {TYPE.AssetItem} metadata a single asset definition
+     * @returns {TYPE.CodeExtractItem} parsed metadata definition
+     */
+    static parseMetadata(metadata) {
+        // folder
+        this.setFolderPath(metadata);
         // extract HTML for selected subtypes and convert payload for easier processing in MetadataType.saveResults()
         metadata = this._extractCode(metadata);
         return metadata;
diff --git a/lib/metadataTypes/Automation.js b/lib/metadataTypes/Automation.js
index cf51644f..c352cb2c 100644
--- a/lib/metadataTypes/Automation.js
+++ b/lib/metadataTypes/Automation.js
@@ -73,6 +73,7 @@ class Automation extends MetadataType {
                               'ProgramID',
                               'Name',
                               'CustomerKey',
+                              'CategoryID',
                               'LastSaveDate',
                               'LastSavedBy',
                               'CreatedBy',
@@ -457,23 +458,23 @@ class Automation extends MetadataType {
             }
         }
     }
-
     /**
-     * parses retrieved Metadata before saving
+     * generic script that retrieves the folder path from cache and updates the given metadata with it after retrieve
      *
-     * @param {TYPE.AutomationItem} metadata a single automation definition
-     * @returns {TYPE.AutomationItem} parsed item
+     * @param {TYPE.MetadataTypeItem} metadata a single script activity definition
      */
-    static parseMetadata(metadata) {
-        // automations are often skipped due to lack of support.
+    static setFolderPath(metadata) {
+        const folderIdField = metadata[this.definition.folderIdField]
+            ? this.definition.folderIdField
+            : 'CategoryID';
         try {
             metadata.r__folder_Path = cache.searchForField(
                 'folder',
-                metadata.categoryId,
+                metadata[folderIdField],
                 'ID',
                 'Path'
             );
-            delete metadata.categoryId;
+            delete metadata[folderIdField];
             if (metadata.r__folder_Path !== 'my automations') {
                 Util.logger.verbose(
                     `- automation '${
@@ -484,13 +485,23 @@ class Automation extends MetadataType {
                 );
             }
         } catch (ex) {
-            // * don't exit on missing folder for automation
             Util.logger.warn(
-                ` - ${this.definition.typeName} '${metadata[this.definition.nameField]}': ${
-                    ex.message
-                }`
+                ` - ${this.definition.type} '${metadata[this.definition.nameField]}' (${
+                    metadata[this.definition.keyField]
+                }): Could not find folder (${ex.message})`
             );
         }
+    }
+
+    /**
+     * parses retrieved Metadata before saving
+     *
+     * @param {TYPE.AutomationItem} metadata a single automation definition
+     * @returns {TYPE.AutomationItem} parsed item
+     */
+    static parseMetadata(metadata) {
+        this.setFolderPath(metadata);
+        // automations are often skipped due to lack of support.
         try {
             if (metadata.type === 'scheduled' && metadata.schedule?.startDate) {
                 // Starting Source == 'Schedule'
diff --git a/lib/metadataTypes/ContentArea.js b/lib/metadataTypes/ContentArea.js
index 8e85f9b2..3b634cfe 100644
--- a/lib/metadataTypes/ContentArea.js
+++ b/lib/metadataTypes/ContentArea.js
@@ -49,27 +49,40 @@ class ContentArea extends MetadataType {
         return this.parseMetadata(metadata);
     }
     /**
-     * parses retrieved Metadata before saving
+     * generic script that retrieves the folder path from cache and updates the given metadata with it after retrieve
      *
-     * @param {TYPE.MetadataTypeItem} metadata a single item
-     * @returns {TYPE.MetadataTypeItem} parsed item
+     * @param {TYPE.MetadataTypeItem} metadata a single script activity definition
      */
-    static parseMetadata(metadata) {
-        // folder
+    static setFolderPath(metadata) {
         try {
             metadata.r__folder_Path = cache.searchForField(
                 'folder',
-                metadata.CategoryID,
+                metadata[this.definition.folderIdField],
                 'ID',
                 'Path'
             );
+            delete metadata[this.definition.folderIdField];
         } catch (ex) {
-            Util.logger.debug(`Classic Content Area '${metadata.CustomerKey}': ${ex.message}`);
+            Util.logger.debug(
+                ` - ${this.definition.type} '${metadata[this.definition.nameField]}' (${
+                    metadata[this.definition.keyField]
+                }): Could not find folder (${ex.message})`
+            );
             // classic content blocks that reside in the main folder are
             // saved with CategoryID=0, instead of to the actual ID of
             // their parent root folder.
             metadata.r__folder_Path = 'my contents';
         }
+    }
+    /**
+     * parses retrieved Metadata before saving
+     *
+     * @param {TYPE.MetadataTypeItem} metadata a single item
+     * @returns {TYPE.MetadataTypeItem} parsed item
+     */
+    static parseMetadata(metadata) {
+        // folder
+        this.setFolderPath(metadata);
 
         // extract code
         const codeArr = [
diff --git a/lib/metadataTypes/DataExtension.js b/lib/metadataTypes/DataExtension.js
index 87895f21..e4e3fda9 100644
--- a/lib/metadataTypes/DataExtension.js
+++ b/lib/metadataTypes/DataExtension.js
@@ -914,22 +914,20 @@ class DataExtension extends MetadataType {
 
         return { metadata: metadata[customerKey], type: 'dataExtension' };
     }
-
     /**
-     * parses retrieved Metadata before saving
+     * dataExtension logic that retrieves the folder path from cache and updates the given metadata with it after retrieve
+     * it also sets the content type which is basically the subtype
      *
-     * @private
-     * @param {TYPE.DataExtensionItem} metadata a single dataExtension definition
-     * @returns {TYPE.DataExtensionItem} a single dataExtension definition
+     * @param {TYPE.MetadataTypeItem} metadata a single script activity definition
      */
-    static _parseMetadata(metadata) {
+    static setFolderPath(metadata) {
         let error = false;
         let verbose = false;
         // data extension type (from folder)
         try {
             metadata.r__folder_ContentType = cache.searchForField(
                 'folder',
-                metadata.CategoryID,
+                metadata[this.definition.folderIdField],
                 'ID',
                 'ContentType'
             );
@@ -939,22 +937,26 @@ class DataExtension extends MetadataType {
                 metadata.r__folder_ContentType = 'synchronizeddataextension';
             } else {
                 error = true;
-                Util.logger.warn(` - dataExtension '${metadata.Name}': ${ex.message}`);
+                Util.logger.warn(
+                    ` - ${this.definition.type} '${metadata[this.definition.nameField]}' (${
+                        metadata[this.definition.keyField]
+                    }): Could not find folder (${ex.message})`
+                );
             }
         }
         // folder
         try {
             metadata.r__folder_Path = cache.searchForField(
                 'folder',
-                metadata.CategoryID,
+                metadata[this.definition.folderIdField],
                 'ID',
                 'Path'
             );
-            delete metadata.CategoryID;
+            delete metadata[this.definition.folderIdField];
         } catch (ex) {
             if (/(_Salesforce)(_\d\d?\d?)?$/.test(metadata.Name)) {
                 metadata.r__folder_Path = 'Synchronized Data Extensions';
-                delete metadata.CategoryID;
+                delete metadata[this.definition.folderIdField];
 
                 if (!verbose) {
                     Util.logger.verbose(
@@ -962,9 +964,25 @@ class DataExtension extends MetadataType {
                     );
                 }
             } else if (!error) {
-                Util.logger.error(`Data Extension '${metadata.Name}': ${ex.message}`);
+                Util.logger.error(
+                    ` - ${this.definition.type} '${metadata[this.definition.nameField]}' (${
+                        metadata[this.definition.keyField]
+                    }): ${ex.message}`
+                );
             }
         }
+    }
+
+    /**
+     * parses retrieved Metadata before saving
+     *
+     * @private
+     * @param {TYPE.DataExtensionItem} metadata a single dataExtension definition
+     * @returns {TYPE.DataExtensionItem} a single dataExtension definition
+     */
+    static _parseMetadata(metadata) {
+        this.setFolderPath(metadata);
+
         // DataExtensionTemplate
         if (metadata.Template?.CustomerKey) {
             try {
diff --git a/lib/metadataTypes/Email.js b/lib/metadataTypes/Email.js
index fc242ff4..feccbeaf 100644
--- a/lib/metadataTypes/Email.js
+++ b/lib/metadataTypes/Email.js
@@ -3,7 +3,6 @@
 const TYPE = require('../../types/mcdev.d');
 const MetadataType = require('./MetadataType');
 const Util = require('../util/util');
-const cache = require('../util/cache');
 
 /**
  * Email MetadataType
@@ -56,19 +55,7 @@ class Email extends MetadataType {
      */
     static parseMetadata(metadata) {
         // folder
-        try {
-            metadata.r__folder_Path = cache.searchForField(
-                'folder',
-                metadata.CategoryID,
-                'ID',
-                'Path'
-            );
-            delete metadata.CategoryID;
-        } catch (ex) {
-            Util.logger.warn(
-                ` - Classic E-Mail '${metadata.Name}'/'${metadata.CustomerKey}': ${ex.message}`
-            );
-        }
+        super.setFolderPath(metadata);
 
         // extract code
         const codeArr = [
diff --git a/lib/metadataTypes/EmailSendDefinition.js b/lib/metadataTypes/EmailSendDefinition.js
index 2dec7f53..dec09e70 100644
--- a/lib/metadataTypes/EmailSendDefinition.js
+++ b/lib/metadataTypes/EmailSendDefinition.js
@@ -182,19 +182,8 @@ class EmailSendDefinition extends MetadataType {
         // remove IsPlatformObject, always has to be 'false'
         delete metadata.IsPlatformObject;
         // folder
-        try {
-            metadata.r__folder_Path = cache.searchForField(
-                'folder',
-                metadata.CategoryID,
-                'ID',
-                'Path'
-            );
-            delete metadata.CategoryID;
-        } catch (ex) {
-            Util.logger.warn(
-                ` - ${this.definition.typeName} '${metadata.Name}'/'${metadata.CustomerKey}': ${ex.message}`
-            );
-        }
+        super.setFolderPath(metadata);
+
         // email
         try {
             // classic
diff --git a/lib/metadataTypes/MetadataType.js b/lib/metadataTypes/MetadataType.js
index 9abe6aa7..5d0fabdd 100644
--- a/lib/metadataTypes/MetadataType.js
+++ b/lib/metadataTypes/MetadataType.js
@@ -141,6 +141,32 @@ class MetadataType {
     static postRetrieveTasks(metadata, targetDir, isTemplating) {
         return JSON.parse(JSON.stringify(metadata));
     }
+    /**
+     * generic script that retrieves the folder path from cache and updates the given metadata with it after retrieve
+     *
+     * @param {TYPE.MetadataTypeItem} metadata a single script activity definition
+     */
+    static setFolderPath(metadata) {
+        if (!this.definition.folderIdField) {
+            return;
+        }
+        try {
+            metadata.r__folder_Path = cache.searchForField(
+                'folder',
+                metadata[this.definition.folderIdField],
+                'ID',
+                'Path'
+            );
+            delete metadata[this.definition.folderIdField];
+        } catch (ex) {
+            Util.logger.warn(
+                ` - ${this.definition.type} '${metadata[this.definition.nameField]}' (${
+                    metadata[this.definition.keyField]
+                }): Could not find folder (${ex.message})`
+            );
+        }
+    }
+
     /**
      * Gets metadata from Marketing Cloud
      *
diff --git a/lib/metadataTypes/Query.js b/lib/metadataTypes/Query.js
index 0d8ff3fc..48f917f0 100644
--- a/lib/metadataTypes/Query.js
+++ b/lib/metadataTypes/Query.js
@@ -2,7 +2,6 @@
 
 const TYPE = require('../../types/mcdev.d');
 const MetadataType = require('./MetadataType');
-const Util = require('../util/util');
 const File = require('../util/file');
 const cache = require('../util/cache');
 const Mustache = require('mustache');
@@ -287,17 +286,7 @@ class Query extends MetadataType {
      */
     static parseMetadata(metadata) {
         // folder
-        try {
-            metadata.r__folder_Path = cache.searchForField(
-                'folder',
-                metadata.categoryId,
-                'ID',
-                'Path'
-            );
-            delete metadata.categoryId;
-        } catch (ex) {
-            Util.logger.warn(` - Query '${metadata.key}': ${ex.message}`);
-        }
+        super.setFolderPath(metadata);
 
         // extract SQL
         const codeArr = [
diff --git a/lib/metadataTypes/Script.js b/lib/metadataTypes/Script.js
index dec9acad..34959d91 100644
--- a/lib/metadataTypes/Script.js
+++ b/lib/metadataTypes/Script.js
@@ -261,17 +261,7 @@ class Script extends MetadataType {
      */
     static parseMetadata(metadata) {
         // folder
-        try {
-            metadata.r__folder_Path = cache.searchForField(
-                'folder',
-                metadata.categoryId,
-                'ID',
-                'Path'
-            );
-            delete metadata.categoryId;
-        } catch (ex) {
-            Util.logger.warn(` - Script '${metadata.key}': ${ex.message}`);
-        }
+        super.setFolderPath(metadata);
 
         // extract SSJS
         const codeArr = [];
diff --git a/lib/metadataTypes/TriggeredSendDefinition.js b/lib/metadataTypes/TriggeredSendDefinition.js
index 74bcda70..9e718d5b 100644
--- a/lib/metadataTypes/TriggeredSendDefinition.js
+++ b/lib/metadataTypes/TriggeredSendDefinition.js
@@ -137,19 +137,8 @@ class TriggeredSendDefinition extends MetadataType {
         // remove IsPlatformObject, always has to be 'false'
         delete metadata.IsPlatformObject;
         // folder
-        try {
-            metadata.r__folder_Path = cache.searchForField(
-                'folder',
-                metadata.CategoryID,
-                'ID',
-                'Path'
-            );
-            delete metadata.CategoryID;
-        } catch (ex) {
-            Util.logger.warn(
-                ` - Triggered Send '${metadata.Name}'/'${metadata.CustomerKey}': ${ex.message}`
-            );
-        }
+        super.setFolderPath(metadata);
+
         // email
         try {
             // classic
diff --git a/lib/metadataTypes/definitions/Automation.definition.js b/lib/metadataTypes/definitions/Automation.definition.js
index 7ea3ac71..b380c960 100644
--- a/lib/metadataTypes/definitions/Automation.definition.js
+++ b/lib/metadataTypes/definitions/Automation.definition.js
@@ -40,6 +40,7 @@ module.exports = {
     idField: 'id',
     keyField: 'key',
     nameField: 'name',
+    folderIdField: 'categoryId',
     createdDateField: 'createdDate',
     createdNameField: 'createdByName',
     lastmodDateField: 'lastSavedDate',
diff --git a/lib/metadataTypes/definitions/ContentArea.definition.js b/lib/metadataTypes/definitions/ContentArea.definition.js
index 2c5fdbff..a0b9c61b 100644
--- a/lib/metadataTypes/definitions/ContentArea.definition.js
+++ b/lib/metadataTypes/definitions/ContentArea.definition.js
@@ -6,6 +6,7 @@ module.exports = {
     keepId: true,
     keyField: 'CustomerKey',
     nameField: 'Name',
+    folderIdField: 'CategoryID',
     restPagination: null,
     type: 'contentArea',
     typeDescription:
diff --git a/lib/metadataTypes/definitions/DataExtension.definition.js b/lib/metadataTypes/definitions/DataExtension.definition.js
index eafe649e..1eda1ab0 100644
--- a/lib/metadataTypes/definitions/DataExtension.definition.js
+++ b/lib/metadataTypes/definitions/DataExtension.definition.js
@@ -80,6 +80,7 @@ module.exports = {
     idField: 'ObjectID',
     keyField: 'CustomerKey',
     nameField: 'Name',
+    folderIdField: 'CategoryID',
     createdDateField: 'CreatedDate',
     createdNameField: null,
     lastmodDateField: 'ModifiedDate',
diff --git a/lib/metadataTypes/definitions/Email.definition.js b/lib/metadataTypes/definitions/Email.definition.js
index eda5da37..56aaa2d1 100644
--- a/lib/metadataTypes/definitions/Email.definition.js
+++ b/lib/metadataTypes/definitions/Email.definition.js
@@ -6,6 +6,7 @@ module.exports = {
     keepId: true,
     keyField: 'CustomerKey',
     nameField: 'Name',
+    folderIdField: 'CategoryID',
     restPagination: null,
     type: 'email',
     typeDescription:
diff --git a/lib/metadataTypes/definitions/EmailSendDefinition.definition.js b/lib/metadataTypes/definitions/EmailSendDefinition.definition.js
index 9e7739ed..b44f94f4 100644
--- a/lib/metadataTypes/definitions/EmailSendDefinition.definition.js
+++ b/lib/metadataTypes/definitions/EmailSendDefinition.definition.js
@@ -7,6 +7,7 @@ module.exports = {
     keepId: true,
     keyField: 'CustomerKey',
     nameField: 'Name',
+    folderIdField: 'CategoryID',
     createdDateField: 'CreatedDate',
     createdNameField: null,
     lastmodDateField: 'ModifiedDate',
diff --git a/lib/metadataTypes/definitions/Query.definition.js b/lib/metadataTypes/definitions/Query.definition.js
index 592fd8ea..f5c57f0d 100644
--- a/lib/metadataTypes/definitions/Query.definition.js
+++ b/lib/metadataTypes/definitions/Query.definition.js
@@ -9,6 +9,7 @@ module.exports = {
     idField: 'queryDefinitionId',
     keyField: 'key',
     nameField: 'name',
+    folderIdField: 'categoryId',
     createdDateField: 'createdDate',
     createdNameField: null,
     lastmodDateField: 'modifiedDate',
diff --git a/lib/metadataTypes/definitions/Script.definition.js b/lib/metadataTypes/definitions/Script.definition.js
index 11cbff83..f2c82473 100644
--- a/lib/metadataTypes/definitions/Script.definition.js
+++ b/lib/metadataTypes/definitions/Script.definition.js
@@ -6,6 +6,7 @@ module.exports = {
     idField: 'ssjsActivityId',
     keyField: 'key',
     nameField: 'name',
+    folderIdField: 'categoryId',
     createdDateField: 'createdDate',
     createdNameField: 'createdBy',
     lastmodDateField: 'modifiedDate',
diff --git a/lib/metadataTypes/definitions/TriggeredSendDefinition.definition.js b/lib/metadataTypes/definitions/TriggeredSendDefinition.definition.js
index 2050ed5c..da48afec 100644
--- a/lib/metadataTypes/definitions/TriggeredSendDefinition.definition.js
+++ b/lib/metadataTypes/definitions/TriggeredSendDefinition.definition.js
@@ -7,6 +7,7 @@ module.exports = {
     keepId: true,
     keyField: 'CustomerKey',
     nameField: 'Name',
+    folderIdField: 'CategoryID',
     createdDateField: 'CreatedDate',
     createdNameField: null,
     lastmodDateField: 'ModifiedDate',
diff --git a/package-lock.json b/package-lock.json
index c68c032c..0facfba2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
     "name": "mcdev",
-    "version": "4.1.3",
+    "version": "4.1.4",
     "lockfileVersion": 2,
     "requires": true,
     "packages": {
         "": {
             "name": "mcdev",
-            "version": "4.1.3",
+            "version": "4.1.4",
             "license": "MIT",
             "dependencies": {
                 "cli-progress": "3.11.2",
@@ -39,7 +39,7 @@
                 "eslint": "8.26.0",
                 "eslint-config-prettier": "8.5.0",
                 "eslint-config-ssjs": "1.1.11",
-                "eslint-plugin-jsdoc": "39.3.25",
+                "eslint-plugin-jsdoc": "39.4.0",
                 "eslint-plugin-mocha": "10.1.0",
                 "eslint-plugin-prettier": "4.2.1",
                 "eslint-plugin-unicorn": "44.0.2",
@@ -2725,9 +2725,9 @@
             }
         },
         "node_modules/eslint-plugin-jsdoc": {
-            "version": "39.3.25",
-            "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-39.3.25.tgz",
-            "integrity": "sha512-7JiFOOaipz7Z7lNQ9sMJ6cdvclmVUwNYtFWGS3a0k0uEFcdZPPD64WOfENuyNHpl86C0AKIEPgOpZby5kd+pew==",
+            "version": "39.4.0",
+            "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-39.4.0.tgz",
+            "integrity": "sha512-2FXAmh8lpO22aqZgu6NLB6berVnYgwbZx95BmjIIQjbRNwLIAwY0dtvSJDUZVW5nflv58c1gSWQjIJ8yLZ/+fw==",
             "dev": true,
             "dependencies": {
                 "@es-joy/jsdoccomment": "~0.33.4",
@@ -10791,9 +10791,9 @@
             "requires": {}
         },
         "eslint-plugin-jsdoc": {
-            "version": "39.3.25",
-            "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-39.3.25.tgz",
-            "integrity": "sha512-7JiFOOaipz7Z7lNQ9sMJ6cdvclmVUwNYtFWGS3a0k0uEFcdZPPD64WOfENuyNHpl86C0AKIEPgOpZby5kd+pew==",
+            "version": "39.4.0",
+            "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-39.4.0.tgz",
+            "integrity": "sha512-2FXAmh8lpO22aqZgu6NLB6berVnYgwbZx95BmjIIQjbRNwLIAwY0dtvSJDUZVW5nflv58c1gSWQjIJ8yLZ/+fw==",
             "dev": true,
             "requires": {
                 "@es-joy/jsdoccomment": "~0.33.4",
diff --git a/package.json b/package.json
index de19bc13..071dce06 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
     "name": "mcdev",
-    "version": "4.1.3",
+    "version": "4.1.4",
     "description": "Accenture Salesforce Marketing Cloud DevTools",
     "author": "joern.berkefeld, douglas.midgley, robert.zimmermann, maciej.barnas",
     "license": "MIT",
@@ -67,7 +67,7 @@
         "eslint": "8.26.0",
         "eslint-config-prettier": "8.5.0",
         "eslint-config-ssjs": "1.1.11",
-        "eslint-plugin-jsdoc": "39.3.25",
+        "eslint-plugin-jsdoc": "39.4.0",
         "eslint-plugin-mocha": "10.1.0",
         "eslint-plugin-prettier": "4.2.1",
         "eslint-plugin-unicorn": "44.0.2",


Test patch:
diff --git a/test/mockRoot/.mcdevrc.json b/test/mockRoot/.mcdevrc.json
index 0462fc28..0c7c8150 100644
--- a/test/mockRoot/.mcdevrc.json
+++ b/test/mockRoot/.mcdevrc.json
@@ -63,5 +63,5 @@
             "triggeredSendDefinition"
         ]
     },
-    "version": "4.1.3"
+    "version": "4.1.4"
 }