Repository: Accenture/sfmc-devtools (js)

Issue:
[FEATURE] add `--execute` option to `deploy` method
similar to the --refresh option (only much easier), this could run execute for supported types right after deploy.

especially together with the upcoming vscode extension that could be a powerful helper, chaining deploy() and execute() commands together.

Pull request:
feature/982: added option to execute after deploy
# PR details

## What changes did you make? (Give an overview)

- closes #982

Added possibility to execute query after deploy

## Is there anything you'd like reviewers to focus on?

...

## Checklist

- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- N/A test scripts updated
- [x] Wiki updated (prepared as comment here)


Patch:
diff --git a/.eslintrc.json b/.eslintrc.json
index 355ff183..3636f6eb 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -84,6 +84,7 @@
                 }
             }
         ],
+        "jsdoc/require-param-type": "error",
         "jsdoc/tag-lines": ["warn", "any", { "startLines": 1 }],
         "spaced-comment": ["warn", "always", { "block": { "exceptions": ["*"], "balanced": true } }]
     },
diff --git a/docs/dist/documentation.md b/docs/dist/documentation.md
index c937ec61..44cddb11 100644
--- a/docs/dist/documentation.md
+++ b/docs/dist/documentation.md
@@ -4593,6 +4593,7 @@ Query MetadataType
     * [.checkForErrors(ex)](#Query.checkForErrors) ⇒ <code>Array.&lt;string&gt;</code> \| <code>void</code>
     * [.deleteByKey(customerKey)](#Query.deleteByKey) ⇒ <code>boolean</code>
     * [.postDeleteTasks(customerKey)](#Query.postDeleteTasks) ⇒ <code>void</code>
+    * [.postDeployTasks(upsertResults)](#Query.postDeployTasks)
 
 <a name="Query.retrieve"></a>
 
@@ -4791,6 +4792,17 @@ clean up after deleting a metadata item
 | --- | --- | --- |
 | customerKey | <code>string</code> | Identifier of metadata item |
 
+<a name="Query.postDeployTasks"></a>
+
+### Query.postDeployTasks(upsertResults)
+Gets executed after deployment of metadata type
+
+**Kind**: static method of [<code>Query</code>](#Query)  
+
+| Param | Type | Description |
+| --- | --- | --- |
+| upsertResults | <code>TYPE.MetadataTypeMap</code> | metadata mapped by their keyField as returned by update/create |
+
 <a name="Role"></a>
 
 ## Role ⇐ [<code>MetadataType</code>](#MetadataType)
diff --git a/lib/cli.js b/lib/cli.js
index 4bd97940..d6098899 100644
--- a/lib/cli.js
+++ b/lib/cli.js
@@ -58,7 +58,7 @@ yargs
                     describe: 'metadata key that shall be exclusively uploaded',
                 })
                 .group(
-                    ['changeKeyField', 'changeKeyValue', 'fromRetrieve', 'refresh'],
+                    ['changeKeyField', 'changeKeyValue', 'fromRetrieve', 'refresh', 'execute'],
                     'Options for deploy:'
                 )
                 .option('changeKeyField', {
@@ -79,6 +79,10 @@ yargs
                     type: 'boolean',
                     describe:
                         'optional for asset-message: runs refresh command for related triggeredSends after deploy',
+                })
+                .option('execute', {
+                    type: 'boolean',
+                    describe: 'optional for query: runs execute after deploy',
                 });
         },
         handler: (argv) => {
diff --git a/lib/index.js b/lib/index.js
index 3589f02b..968be4e7 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -58,6 +58,7 @@ class Mcdev {
             'fromRetrieve',
             'json',
             'refresh',
+            'execute',
             'skipInteraction',
             'noLogFile',
         ];
@@ -729,12 +730,10 @@ class Mcdev {
             }
         }
         if (businessUnit === '*') {
-            Util.logger.info(
-                '\n               :: Executing the entity on all BUs for all credentials'
-            );
+            Util.logger.info(':: Executing the entity on all BUs for all credentials');
             let counter_credTotal = 0;
             for (const cred in properties.credentials) {
-                Util.logger.info(`\n               :: Executing the entity on all BUs for ${cred}`);
+                Util.logger.info(`:: Executing the entity on all BUs for ${cred}`);
 
                 for (const bu in properties.credentials[cred].businessUnits) {
                     if (await this._executeBU(cred, bu, selectedTypesArr, keys)) {
@@ -745,13 +744,9 @@ class Mcdev {
                     Util.startLogger(true);
                 }
                 counter_credTotal += counter_credBu;
-                Util.logger.info(
-                    `\n               :: Executed the entity on ${counter_credBu} BUs for ${cred}\n`
-                );
+                Util.logger.info(`:: Executed the entity on ${counter_credBu} BUs for ${cred}\n`);
             }
-            Util.logger.info(
-                `\n               :: Executed the entity on ${counter_credTotal} BUs in total\n`
-            );
+            Util.logger.info(`:: Executed the entity on ${counter_credTotal} BUs in total\n`);
         } else {
             let [cred, bu] = businessUnit ? businessUnit.split('/') : [null, null];
             // to allow all-BU via user selection we need to run this here already
diff --git a/lib/metadataTypes/MetadataType.js b/lib/metadataTypes/MetadataType.js
index 584b7fa6..a5e55bcf 100644
--- a/lib/metadataTypes/MetadataType.js
+++ b/lib/metadataTypes/MetadataType.js
@@ -1079,7 +1079,7 @@ class MetadataType {
         try {
             const response = await this.client.rest.post(uri, {}); // payload is empty for this request
             if (response === 'OK') {
-                Util.logger.info(`Executed ${this.definition.type}: ${key}`);
+                Util.logger.info(` - executed ${this.definition.type}: ${key}`);
             } else {
                 throw new Error(response);
             }
diff --git a/lib/metadataTypes/Query.js b/lib/metadataTypes/Query.js
index e9149613..f33d9a9d 100644
--- a/lib/metadataTypes/Query.js
+++ b/lib/metadataTypes/Query.js
@@ -65,7 +65,7 @@ class Query extends MetadataType {
                 objectId = await this._getObjectIdForSingleRetrieve(key);
                 if (!objectId) {
                     Util.logger.info(`Skipping ${key} - did not find an item with such key`);
-                    break;
+                    continue;
                 }
             }
             results.push(
@@ -452,6 +452,17 @@ class Query extends MetadataType {
         // delete local copy: retrieve/cred/bu/.../...-meta.sql
         await super.postDeleteTasks(customerKey, [`${this.definition.type}-meta.sql`]);
     }
+    /**
+     * Gets executed after deployment of metadata type
+     *
+     * @param {TYPE.MetadataTypeMap} upsertResults metadata mapped by their keyField as returned by update/create
+     */
+    static async postDeployTasks(upsertResults) {
+        if (Util.OPTIONS.execute) {
+            Util.logger.info(`Executing: ${this.definition.type}`);
+            await this.execute(Object.keys(upsertResults));
+        }
+    }
 }
 
 // Assign definition & cache to static attributes


Test patch:
diff --git a/test/resourceFactory.js b/test/resourceFactory.js
index 5cdd5550..e9176ab6 100644
--- a/test/resourceFactory.js
+++ b/test/resourceFactory.js
@@ -54,11 +54,22 @@ exports.loadSOAPRecords = async (mcdevAction, type, mid, filter) => {
     });
 };
 exports.filterToPath = (filter) => {
-    if (filter && filter.Property && filter.SimpleOperator && filter.Value) {
-        return `-${filter.Property}${filter.SimpleOperator.replace('equals', '=')}${filter.Value}`;
+    if (filter) {
+        return '-' + this._filterToPath(filter);
     }
     return '';
 };
+exports._filterToPath = (filter) => {
+    if (filter.Property && filter.SimpleOperator && filter.Value) {
+        return `${filter.Property}${filter.SimpleOperator.replace('equals', '=')}${filter.Value}`;
+    } else if (filter.LeftOperand && filter.LogicalOperator && filter.RightOperand) {
+        return (
+            this._filterToPath(filter.LeftOperand) +
+            filter.LogicalOperator +
+            this._filterToPath(filter.RightOperand)
+        );
+    }
+};
 /**
  * based on request, respond with different soap data
  *
@@ -197,7 +208,7 @@ exports.handleRESTRequest = async (config) => {
 
             return [
                 404,
-                fs.readFile(path.join('test', 'resources', 'rest404-response.json'), {
+                await fs.readFile(path.join('test', 'resources', 'rest404-response.json'), {
                     encoding: 'utf8',
                 }),
             ];
diff --git a/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dae/actions/start/post-response.txt b/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dae/actions/start/post-response.txt
new file mode 100644
index 00000000..a0aba931
--- /dev/null
+++ b/test/resources/9999999/automation/v1/queries/549f0568-607c-4940-afef-437965094dae/actions/start/post-response.txt
@@ -0,0 +1,1 @@
+OK
\ No newline at end of file
diff --git a/test/resources/9999999/queryDefinition/retrieve-response.xml b/test/resources/9999999/queryDefinition/retrieve-CustomerKey=testExisting_queryANDStatus=Active-response.xml
similarity index 100%
rename from test/resources/9999999/queryDefinition/retrieve-response.xml
rename to test/resources/9999999/queryDefinition/retrieve-CustomerKey=testExisting_queryANDStatus=Active-response.xml
diff --git a/test/resources/9999999/queryDefinition/retrieve-CustomerKey=testNew_queryANDStatus=Active-response.xml b/test/resources/9999999/queryDefinition/retrieve-CustomerKey=testNew_queryANDStatus=Active-response.xml
new file mode 100644
index 00000000..ad6aad84
--- /dev/null
+++ b/test/resources/9999999/queryDefinition/retrieve-CustomerKey=testNew_queryANDStatus=Active-response.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+    xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing"
+    xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
+    xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
+    <soap:Header>
+        <wsa:Action>RetrieveResponse</wsa:Action>
+        <wsa:MessageID>urn:uuid:7ef0345e-b559-4fc4-8986-47e54e1a8a58</wsa:MessageID>
+        <wsa:RelatesTo>urn:uuid:b2e814a6-517c-4882-9bbb-238bfce951ce</wsa:RelatesTo>
+        <wsa:To>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:To>
+        <wsse:Security>
+            <wsu:Timestamp wsu:Id="Timestamp-dfc4ae59-8642-4432-b505-554669b47186">
+                <wsu:Created>2023-04-11T16:33:48Z</wsu:Created>
+                <wsu:Expires>2023-04-11T16:38:48Z</wsu:Expires>
+            </wsu:Timestamp>
+        </wsse:Security>
+    </soap:Header>
+    <soap:Body>
+        <RetrieveResponseMsg xmlns="http://exacttarget.com/wsdl/partnerAPI">
+            <OverallStatus>OK</OverallStatus>
+            <RequestID>e8eb2988-2f43-4243-a6b0-6ab6b841a6ab</RequestID>
+            <Results xsi:type="QueryDefinition">
+                <PartnerKey xsi:nil="true" />
+                <ObjectID>549f0568-607c-4940-afef-437965094dae</ObjectID>
+            </Results>
+        </RetrieveResponseMsg>
+    </soap:Body>
+</soap:Envelope>
\ No newline at end of file
diff --git a/test/type.query.test.js b/test/type.query.test.js
index edaf7216..e174cfdd 100644
--- a/test/type.query.test.js
+++ b/test/type.query.test.js
@@ -124,6 +124,33 @@ describe('type: query', () => {
             return;
         });
         it('Should change the key during update with --changeKeyValue');
+        it('Should deploy and execute with --execute', async () => {
+            handler.setOptions({ execute: true });
+            // WHEN
+            await handler.deploy('testInstance/testBU', ['query']);
+            // THEN
+            assert.equal(
+                process.exitCode,
+                false,
+                'deploy with --execute should not have thrown an error'
+            );
+            // confirm updated item
+            assert.deepEqual(
+                await testUtils.getActualJson('testExisting_query', 'query'),
+                await testUtils.getExpectedJson('9999999', 'query', 'patch'),
+                'returned metadata was not equal expected for insert query'
+            );
+            expect(file(testUtils.getActualFile('testExisting_query', 'query', 'sql'))).to.equal(
+                file(testUtils.getExpectedFile('9999999', 'query', 'patch', 'sql'))
+            );
+            // check number of API calls
+            assert.equal(
+                testUtils.getAPIHistoryLength(),
+                12,
+                'Unexpected number of requests made. Run testUtils.logAPIHistoryDebug() to see the requests'
+            );
+            return;
+        });
     });
     describe('Templating ================', () => {
         it('Should create a query template via retrieveAsTemplate and build it', async () => {