Explorar o código

feat: a Respond to Webhook node, so the HTTP triggers can back a real API

Also activate the workflow in verify-node.py's http case path, since
the webhook controller rejects inactive workflows with a 400 before
they ever reach the node under test.
fszontagh hai 1 mes
pai
achega
77a9d5273d
Modificáronse 3 ficheiros con 144 adicións e 0 borrados
  1. 120 0
      nodes/core/respond-to-webhook.js
  2. 1 0
      scripts/verify-node.py
  3. 23 0
      tests/nodes/respond-to-webhook.json

+ 120 - 0
nodes/core/respond-to-webhook.js

@@ -0,0 +1,120 @@
+/**
+ * @node respond-to-webhook
+ * @name Respond to Webhook
+ * @category core
+ * @version 1.0.0
+ * @description Set the status, headers and body a webhook-triggered workflow returns to its caller
+ * @icon reply
+ */
+
+const configSchema = {
+    type: 'object',
+    properties: {
+        status: {
+            type: 'number',
+            title: 'Status Code',
+            description: 'HTTP status to return, such as 200, 201, 400 or 404',
+            default: 200
+        },
+        bodySource: {
+            type: 'string',
+            title: 'Body From',
+            description: 'json sends the text below parsed as JSON, text sends it as it is, and input sends a value taken from the incoming data',
+            enum: ['json', 'text', 'input'],
+            default: 'json'
+        },
+        body: {
+            type: 'string',
+            title: 'Body',
+            description: 'The response body, for the json and text sources. Supports {{variable}} interpolation',
+            format: 'textarea'
+        },
+        bodyField: {
+            type: 'string',
+            title: 'Body Field',
+            description: 'Path to the value to send, for the input source, such as data.result',
+            default: 'data'
+        },
+        contentType: {
+            type: 'string',
+            title: 'Content Type',
+            description: 'Content-Type header. Leave empty to send application/json',
+            default: 'application/json'
+        },
+        headers: {
+            type: 'object',
+            title: 'Extra Headers',
+            description: 'Additional response headers',
+            additionalProperties: { type: 'string' }
+        }
+    }
+};
+
+const inputSchema = {
+    type: 'object',
+    properties: {
+        data: { type: 'any' }
+    }
+};
+
+const outputSchema = {
+    type: 'object',
+    properties: {
+        status: { type: 'number', description: 'Status this node asked for' },
+        respondedWith: { type: 'string', description: 'Content type sent' }
+    }
+};
+
+async function execute(config, input, context) {
+    const status = config.status === undefined || config.status === null || config.status === ''
+        ? 200
+        : Number(config.status);
+    if (isNaN(status) || status < 100 || status > 599) {
+        throw new Error('Respond to Webhook: "' + config.status +
+            '" is not an HTTP status code. Use a number from 100 to 599');
+    }
+
+    const source = config.bodySource || 'json';
+    const contentType = config.contentType || 'application/json';
+
+    let body;
+    if (source === 'input') {
+        body = smartbotic.utils.getFieldValue(input, config.bodyField || 'data');
+    } else if (source === 'text') {
+        body = config.body === undefined || config.body === null ? '' : String(config.body);
+    } else {
+        const raw = config.body === undefined || config.body === null ? '' : String(config.body);
+        if (raw.length === 0) {
+            body = {};
+        } else {
+            try {
+                body = JSON.parse(raw);
+            } catch (e) {
+                throw new Error('Respond to Webhook: the body is not valid JSON: ' + e.message +
+                    '. Use the text source to send it as it is');
+            }
+        }
+    }
+
+    const headers = {};
+    if (config.headers && typeof config.headers === 'object') {
+        for (const name of Object.keys(config.headers)) {
+            headers[name] = String(config.headers[name]);
+        }
+    }
+    headers['Content-Type'] = contentType;
+
+    smartbotic.log.info('Respond to Webhook: ' + status + ' as ' + contentType);
+
+    return {
+        status: status,
+        respondedWith: contentType,
+        _webhookResponse: {
+            status: status,
+            headers: headers,
+            body: body
+        }
+    };
+}
+
+module.exports = { configSchema, inputSchema, outputSchema, execute };

+ 1 - 0
scripts/verify-node.py

@@ -141,6 +141,7 @@ def main():
 
     try:
         if "http" in case:
+            call("POST", f"/workflows/{workflow_id}/activate", token, {})
             failures = run_http_case(case, token, workflow_id)
             print("\nFAIL" if failures else "\nPASS")
             for f in failures:

+ 23 - 0
tests/nodes/respond-to-webhook.json

@@ -0,0 +1,23 @@
+{
+  "name": "verify-respond-to-webhook",
+  "nodes": [
+    {"id": "n1", "name": "Webhook", "type": "post-trigger", "position": {"x": 0, "y": 0}, "config": {}},
+    {"id": "n2", "name": "Respond", "type": "respond-to-webhook", "position": {"x": 0, "y": 100},
+     "config": {"status": 201, "bodySource": "json", "body": "{\"created\": true, \"id\": \"abc\"}",
+                "contentType": "application/json", "headers": {"X-Smartbotic-Test": "tier2"}}},
+    {"id": "n3", "name": "After", "type": "code", "position": {"x": 0, "y": 200},
+     "config": {"code": "return { ranAfterResponding: true };"}}
+  ],
+  "connections": [
+    {"sourceNodeId": "n1", "sourceOutput": "main", "targetNodeId": "n2", "targetInput": "data"},
+    {"sourceNodeId": "n2", "sourceOutput": "main", "targetNodeId": "n3", "targetInput": "data"}
+  ],
+  "http": {
+    "method": "POST",
+    "path": "/webhook/{workflowId}",
+    "body": {"hello": "world"},
+    "expectStatus": 201,
+    "expectHeaders": {"Content-Type": "application/json", "X-Smartbotic-Test": "tier2"},
+    "expectBodyContains": ["\"created\":true", "\"id\":\"abc\""]
+  }
+}