Repository: Accenture/sfmc-devtools (js)

Issue:
[FEATURE] get name and key for content block id
because the search functionality in SFMC does not really allow you to simply find the one item that an id belongs to, lets add a method for it

Pull request description:
Feature/1238: add more info to resolveId


# PR details

## What changes did you make? (Give an overview)

- closes #1238

## Further details (optional)

add { folder:string, isShared:boolean } and ContentBlockByName snippet to resolveId response

## Checklist

- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] test scripts updated
- [ ] Wiki updated (if applicable)


Patch:
diff --git a/lib/metadataTypes/Asset.js b/lib/metadataTypes/Asset.js
index 287eb396..71e689d1 100644
--- a/lib/metadataTypes/Asset.js
+++ b/lib/metadataTypes/Asset.js
@@ -7,6 +7,7 @@ import pLimit from 'p-limit';
 import cliProgress from 'cli-progress';
 import cache from '../util/cache.js';
 import TriggeredSend from './TriggeredSend.js';
+import Folder from './Folder.js';
 
 /**
  * @typedef {import('../../types/mcdev.d.js').BuObject} BuObject
@@ -1719,14 +1720,16 @@ class Asset extends MetadataType {
      * get name & key for provided id
      *
      * @param {string} id Identifier of metadata
-     * @returns {Promise.<{key:string, name:string, path:string, mid:number, error:string}>} key, name and path of metadata; null if not found
+     * @returns {Promise.<{key:string, name:string, path:string, folder:string, mid:number, error:string, isShared:boolean}>} key, name and path of metadata; null if not found
      */
     static async resolveId(id) {
         let response;
         const json = {
             key: '',
+            isShared: false,
             name: '',
             path: '',
+            folder: '',
             mid: 0,
             error: '',
         };
@@ -1740,19 +1743,43 @@ class Asset extends MetadataType {
         if (response?.id) {
             const item = response;
             const subType = this.#getMainSubtype(item.assetType.name);
-            // find path for code of content block, fall back to json if not found; undefined if not even the json exists
-            const pathBase1 = `./retrieve/${this.buObject.credential}/${this.buObject.businessUnit}/${this.definition.type}/${subType}/${item[this.definition.keyField]}.${this.definition.type}-${subType}-meta.`;
-            const pathBase2 = `./retrieve/${this.buObject.credential}/${this.buObject.businessUnit}/${this.definition.type}/${subType}/${item[this.definition.keyField]}/${item[this.definition.keyField]}.${this.definition.type}-${subType}-meta.`;
-            const paths = [];
-            for (const ext of ['html', 'ssjs', 'amp', 'json']) {
-                paths.push(pathBase1 + ext, pathBase2 + ext);
-            }
-            const path = paths.find((p) => File.pathExistsSync(p));
 
             // prep response object
             json.key = item[this.definition.keyField];
             json.name = item[this.definition.nameField];
             json.mid = item.memberId;
+            json.isShared = item.memberId != this.buObject.mid;
+
+            // find path for code of content block, fall back to json if not found; undefined if not even the json exists
+            let path = json.isShared ? null : this.#getPath(subType, item);
+
+            if (!json.isShared && !path) {
+                // if not shared, we should have the file on disk; attempt download
+                if (!cache.getCache()?.folder) {
+                    // folders not cached yet but required to fill json.path
+                    Folder.client = this.client;
+                    Folder.buObject = this.buObject;
+                    Folder.properties = this.properties;
+                    const result = await Folder.retrieveForCache(null, ['asset', 'asset-shared']);
+                    if (!cache.getCache()) {
+                        cache.initCache(this.buObject);
+                    }
+                    cache.setMetadata('folder', result.metadata);
+                }
+
+                await this.retrieve(
+                    File.normalizePath([
+                        this.properties.directories.retrieve,
+                        this.buObject.credential,
+                        this.buObject.businessUnit,
+                    ]),
+                    null,
+                    [subType],
+                    json.key
+                );
+                // try again
+                path = this.#getPath(subType, item);
+            }
 
             if (path) {
                 json.path = path;
@@ -1761,6 +1788,11 @@ class Asset extends MetadataType {
                 json.error = 'file not found on local disk';
                 delete json.path;
             }
+            const fileContent = this.#getJson(subType, item);
+            if (fileContent?.r__folder_Path) {
+                json.folder = fileContent.r__folder_Path;
+            }
+
             if (Util.OPTIONS.json) {
                 // for automated processing by VSCode extension, optionally print the json
                 console.log(JSON.stringify(json, null, 2)); // eslint-disable-line no-console
@@ -1768,8 +1800,21 @@ class Asset extends MetadataType {
                 Util.logger.info(
                     ` - ${this.definition.type}-${subType} found: ${item[this.definition.keyField]} (${item[this.definition.nameField]})`
                 );
+                if (json.isShared) {
+                    Util.logger.warn(' - Shared from MID ' + json.mid);
+                }
+                if (json.folder) {
+                    Util.logger.info(
+                        ` - ContentBlockByName("${json.folder.split('/').join('\\') + '\\' + json.name}")`
+                    );
+                    if (json.folder.includes('&amp;') || json.name.includes('&amp;')) {
+                        Util.logger.warn(
+                            ` - ContentBlockByName will fail because your path or name contains the & or &amp; symbol. Please replace it with 'and' or remove it.`
+                        );
+                    }
+                }
                 Util.logger.info(
-                    ' - link: ' +
+                    ' - local link: ' +
                         (path ||
                             `404. Try running mcdev r ${this.buObject.credential}/${this.buObject.businessUnit} ${this.definition.type}-${subType} ${item[this.definition.keyField]}`)
                 );
@@ -1781,6 +1826,8 @@ class Asset extends MetadataType {
             delete json.mid;
             delete json.name;
             delete json.path;
+            delete json.folder;
+            delete json.isShared;
             if (Util.OPTIONS.json) {
                 console.log(JSON.stringify(json, null, 2)); // eslint-disable-line no-console
                 return json;
@@ -1789,6 +1836,67 @@ class Asset extends MetadataType {
             return json;
         }
     }
+
+    /**
+     * helper for {@link Asset.resolveId} that finds the path to the asset's code
+     *
+     * @param {string} subType asset subtype
+     * @param {object} item api response for metadata
+     * @returns {string} path to the asset's code
+     */
+    static #getPath(subType, item) {
+        const mid = item.memberId;
+        const buName =
+            mid === this.buObject.mid
+                ? this.buObject.businessUnit
+                : Object.keys(
+                      this.properties.credentials[this.buObject.credential].businessUnits
+                  ).find(
+                      (buName) =>
+                          this.properties.credentials[this.buObject.credential].businessUnits[
+                              buName
+                          ] == mid
+                  );
+        const pathBase1 = `./retrieve/${this.buObject.credential}/${buName}/${this.definition.type}/${subType}/${item[this.definition.keyField]}.${this.definition.type}-${subType}-meta.`;
+        const pathBase2 = `./retrieve/${this.buObject.credential}/${buName}/${this.definition.type}/${subType}/${item[this.definition.keyField]}/${item[this.definition.keyField]}.${this.definition.type}-${subType}-meta.`;
+        const paths = [];
+        for (const ext of ['html', 'ssjs', 'amp', 'json']) {
+            paths.push(pathBase1 + ext, pathBase2 + ext);
+        }
+        const path = paths.find((p) => File.pathExistsSync(p));
+        return path;
+    }
+    /**
+     * helper for {@link Asset.resolveId} that loads the JSON file for the asset
+     *
+     * @param {string} subType asset subtype
+     * @param {object} item api response for metadata
+     * @returns {object} JS object of the asset we loaded from disk
+     */
+    static #getJson(subType, item) {
+        const mid = item.memberId;
+        const buName =
+            mid === this.buObject.mid
+                ? this.buObject.businessUnit
+                : Object.keys(
+                      this.properties.credentials[this.buObject.credential].businessUnits
+                  ).find(
+                      (buName) =>
+                          this.properties.credentials[this.buObject.credential].businessUnits[
+                              buName
+                          ] == mid
+                  );
+        const pathBase1 = `./retrieve/${this.buObject.credential}/${buName}/${this.definition.type}/${subType}/${item[this.definition.keyField]}.${this.definition.type}-${subType}-meta.`;
+        const pathBase2 = `./retrieve/${this.buObject.credential}/${buName}/${this.definition.type}/${subType}/${item[this.definition.keyField]}/${item[this.definition.keyField]}.${this.definition.type}-${subType}-meta.`;
+        const paths = [];
+        paths.push(pathBase1 + 'json', pathBase2 + 'json');
+        const path = paths.find((p) => File.pathExistsSync(p));
+
+        const pathArr = path.split('/');
+        const fileName = pathArr.pop().slice(0, -5);
+        const fileContent = File.readJSONFile(pathArr.join('/'), fileName, true, false);
+        return fileContent;
+    }
 }
 
 // Assign definition to static attributes


Test patch:
diff --git a/test/resources/9999999/asset/resolveId-1295064-noPath-expected.json b/test/resources/9999999/asset/resolveId-1295064-noPath-expected.json
index 2f9ef8a2..436b54e3 100644
--- a/test/resources/9999999/asset/resolveId-1295064-noPath-expected.json
+++ b/test/resources/9999999/asset/resolveId-1295064-noPath-expected.json
@@ -1,6 +1,8 @@
 {
+    "folder": "Content Builder",
+    "isShared": false,
     "key": "mcdev-issue-1157",
     "mid": 9999999,
     "name": "mcdev-issue-1157-Mcdev-strips-content",
-    "error": "file not found on local disk"
+    "path": "./retrieve/testInstance/testBU/asset/block/mcdev-issue-1157.asset-block-meta.html"
 }
diff --git a/test/resources/9999999/asset/resolveId-1295064-withPath-expected.json b/test/resources/9999999/asset/resolveId-1295064-withPath-expected.json
index 9a05a481..436b54e3 100644
--- a/test/resources/9999999/asset/resolveId-1295064-withPath-expected.json
+++ b/test/resources/9999999/asset/resolveId-1295064-withPath-expected.json
@@ -1,4 +1,6 @@
 {
+    "folder": "Content Builder",
+    "isShared": false,
     "key": "mcdev-issue-1157",
     "mid": 9999999,
     "name": "mcdev-issue-1157-Mcdev-strips-content",