diff --git a/.changeset/odd-clouds-decide.md b/.changeset/odd-clouds-decide.md new file mode 100644 index 0000000..f2122d2 --- /dev/null +++ b/.changeset/odd-clouds-decide.md @@ -0,0 +1,5 @@ +--- +'flowtestai': minor +--- + +Allow multiple kv params in multipart form data request type diff --git a/package.json b/package.json index ec996ff..a21499b 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "date-fns": "^3.6.0", "eslint-import-resolver-alias": "^1.1.2", "eslint-plugin-import": "^2.29.1", + "form-data": "^4.0.0", "immer": "^10.0.4", "lodash": "^4.17.21", "mousetrap": "^1.6.5", diff --git a/packages/flowtest-cli/bin/index.js b/packages/flowtest-cli/bin/index.js index 50aeb53..5827bf6 100755 --- a/packages/flowtest-cli/bin/index.js +++ b/packages/flowtest-cli/bin/index.js @@ -68,7 +68,7 @@ const argv = yargs(hideBin(process.argv)) try { const flowData = serialize(JSON.parse(content)); // output json output to a file - //console.log(chalk.green(JSON.stringify(flowData))); + const logger = new GraphLogger(); const startTime = Date.now(); const g = new Graph( @@ -80,13 +80,11 @@ const argv = yargs(hideBin(process.argv)) logger, ); console.log(chalk.yellow('Running Flow \n')); - if (flowData.nodes.find((n) => n.type === 'flowNode')) { - console.log( - chalk.blue( - '[Note] This flow contains nested flows so run it from parent directory of collection. Ignore if already doing that. \n', - ), - ); - } + console.log( + chalk.blue( + 'Right now CLI commands must be run from root directory of collection. We will gradually add support to run commands from anywhere inside the collection. \n', + ), + ); const result = await g.run(); console.log('\n'); if (result.status === 'Success') { diff --git a/packages/flowtest-cli/graph/compute/requestnode.js b/packages/flowtest-cli/graph/compute/requestnode.js index 9c4e50c..59bbf53 100644 --- a/packages/flowtest-cli/graph/compute/requestnode.js +++ b/packages/flowtest-cli/graph/compute/requestnode.js @@ -3,6 +3,10 @@ const Node = require('./node'); const axios = require('axios'); const chalk = require('chalk'); const { LogLevel } = require('../GraphLogger'); +const FormData = require('form-data'); +const { extend, cloneDeep } = require('lodash'); +const fs = require('fs'); +const path = require('path'); const newAbortSignal = () => { const abortController = new AbortController(); @@ -113,11 +117,8 @@ class requestNode extends Node { : JSON.parse('{}'); } else if (this.nodeData.requestBody.type === 'form-data') { contentType = 'multipart/form-data'; - requestData = { - key: computeVariables(this.nodeData.requestBody.body.key, variablesDict), - value: this.nodeData.requestBody.body.value, - name: this.nodeData.requestBody.body.name, - }; + const params = cloneDeep(this.nodeData.requestBody.body); + requestData = params; } } @@ -125,7 +126,7 @@ class requestNode extends Node { method: restMethod, url: finalUrl, headers: { - 'Content-type': contentType, + 'content-type': contentType, }, data: requestData, }; @@ -141,12 +142,23 @@ class requestNode extends Node { async runHttpRequest(request) { try { - if (request.headers['Content-type'] === 'multipart/form-data') { - const requestData = new FormData(); - const file = await convertBase64ToBlob(request.data.value); - requestData.append(request.data.key, file, request.data.name); + if (request.headers['content-type'] === 'multipart/form-data') { + const formData = new FormData(); + const params = request.data; + await params.map(async (param, index) => { + if (param.type === 'text') { + formData.append(param.key, param.value); + } + + if (param.type === 'file') { + let trimmedFilePath = param.value.trim(); + + formData.append(param.key, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath)); + } + }); - request.data = requestData; + request.data = formData; + extend(request.headers, formData.getHeaders()); } // assuming 'application/json' type diff --git a/packages/flowtest-cli/package.json b/packages/flowtest-cli/package.json index d1664b1..65dd835 100644 --- a/packages/flowtest-cli/package.json +++ b/packages/flowtest-cli/package.json @@ -17,6 +17,7 @@ "boxen": "^7.1.1", "chalk": "^3.0.0", "dotenv": "^16.4.5", + "form-data": "^4.0.0", "fs": "^0.0.1-security", "lodash": "^4.17.21", "omelette": "^0.4.17", diff --git a/packages/flowtest-electron/package.json b/packages/flowtest-electron/package.json index d264caf..f49b10d 100644 --- a/packages/flowtest-electron/package.json +++ b/packages/flowtest-electron/package.json @@ -43,6 +43,7 @@ "dotenv": "^16.4.5", "electron-store": "^8.1.0", "flatted": "^3.3.1", + "form-data": "^4.0.0", "fs": "^0.0.1-security", "json-refs": "^3.0.15", "langchain": "^0.1.28", diff --git a/packages/flowtest-electron/src/ipc/collection.js b/packages/flowtest-electron/src/ipc/collection.js index 8e68af5..a67094b 100644 --- a/packages/flowtest-electron/src/ipc/collection.js +++ b/packages/flowtest-electron/src/ipc/collection.js @@ -18,6 +18,8 @@ const FlowtestAI = require('../ai/flowtestai'); const { stringify, parse } = require('flatted'); const { deserialize, serialize } = require('../utils/flowparser/parser'); const { axiosClient } = require('./axiosClient'); +const FormData = require('form-data'); +const { extend } = require('lodash'); const collectionStore = new Collections(); const flowTestAI = new FlowtestAI(); @@ -283,23 +285,38 @@ const registerRendererEventHandlers = (mainWindow, watcher) => { } }); - ipcMain.handle('renderer:run-http-request', async (event, request) => { + ipcMain.handle('renderer:run-http-request', async (event, request, collectionPath) => { try { - if (request.headers['Content-type'] === 'multipart/form-data') { - const requestData = new FormData(); - const file = await convertBase64ToBlob(request.data.value); - requestData.append(request.data.key, file, request.data.name); + if (request.headers['content-type'] === 'multipart/form-data') { + const formData = new FormData(); + const params = request.data; + await params.map(async (param, index) => { + if (param.type === 'text') { + formData.append(param.key, param.value); + } + + if (param.type === 'file') { + let trimmedFilePath = param.value.trim(); + + if (!path.isAbsolute(trimmedFilePath)) { + trimmedFilePath = path.join(collectionPath, trimmedFilePath); + } - request.data = requestData; + formData.append(param.key, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath)); + } + }); + + request.data = formData; + extend(request.headers, formData.getHeaders()); } - // assuming 'application/json' type const options = { ...request, signal: newAbortSignal(), }; const result = await axios(options); + return { status: result.status, statusText: result.statusText, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10a5ca0..114b27b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: eslint-plugin-import: specifier: ^2.29.1 version: 2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@4.9.5))(eslint@8.57.0) + form-data: + specifier: ^4.0.0 + version: 4.0.0 immer: specifier: ^10.0.4 version: 10.1.1 @@ -201,6 +204,9 @@ importers: dotenv: specifier: ^16.4.5 version: 16.4.5 + form-data: + specifier: ^4.0.0 + version: 4.0.0 fs: specifier: ^0.0.1-security version: 0.0.1-security @@ -267,6 +273,9 @@ importers: flatted: specifier: ^3.3.1 version: 3.3.1 + form-data: + specifier: ^4.0.0 + version: 4.0.0 fs: specifier: ^0.0.1-security version: 0.0.1-security diff --git a/src/components/molecules/flow/graph/Graph.js b/src/components/molecules/flow/graph/Graph.js index 82bfa41..d655981 100644 --- a/src/components/molecules/flow/graph/Graph.js +++ b/src/components/molecules/flow/graph/Graph.js @@ -11,7 +11,7 @@ import setVarNode from './compute/setvarnode'; import { LogLevel } from './GraphLogger'; class Graph { - constructor(nodes, edges, startTime, initialEnvVars, logger, caller) { + constructor(nodes, edges, startTime, initialEnvVars, logger, caller, collectionPath) { this.nodes = nodes; this.edges = edges; this.logger = logger; @@ -21,6 +21,7 @@ class Graph { this.auth = undefined; this.envVariables = initialEnvVars; this.caller = caller; + this.collectionPath = collectionPath; } #checkTimeout() { @@ -125,7 +126,14 @@ class Graph { } if (node.type === 'requestNode') { - const rNode = new requestNode(node.data, prevNodeOutputData, this.envVariables, this.auth, this.logger); + const rNode = new requestNode( + node.data, + prevNodeOutputData, + this.envVariables, + this.auth, + this.logger, + this.collectionPath, + ); result = await rNode.evaluate(); // add post response variables if any if (result.postRespVars) { @@ -146,6 +154,7 @@ class Graph { this.envVariables, this.logger, node.type, + this.collectionPath, ); result = await cNode.evaluate(); this.envVariables = result.envVars; diff --git a/src/components/molecules/flow/graph/compute/nestedflownode.js b/src/components/molecules/flow/graph/compute/nestedflownode.js index f0f9db3..ff13776 100644 --- a/src/components/molecules/flow/graph/compute/nestedflownode.js +++ b/src/components/molecules/flow/graph/compute/nestedflownode.js @@ -2,9 +2,9 @@ import Graph1 from '../Graph'; import Node from './node'; class nestedFlowNode extends Node { - constructor(nodes, edges, startTime, initialEnvVars, logger, caller) { + constructor(nodes, edges, startTime, initialEnvVars, logger, caller, collectionPath) { super('flowNode'); - this.internalGraph = new Graph1(nodes, edges, startTime, initialEnvVars, logger, caller); + this.internalGraph = new Graph1(nodes, edges, startTime, initialEnvVars, logger, caller, collectionPath); } async evaluate() { diff --git a/src/components/molecules/flow/graph/compute/requestnode.js b/src/components/molecules/flow/graph/compute/requestnode.js index 332465a..fd19446 100644 --- a/src/components/molecules/flow/graph/compute/requestnode.js +++ b/src/components/molecules/flow/graph/compute/requestnode.js @@ -1,15 +1,17 @@ +import { cloneDeep } from 'lodash'; import { computeNodeVariables, computeVariables } from '../compute/utils'; import { LogLevel } from '../GraphLogger'; import Node from './node'; class requestNode extends Node { - constructor(nodeData, prevNodeOutputData, envVariables, auth, logger) { + constructor(nodeData, prevNodeOutputData, envVariables, auth, logger, collectionPath) { super('requestNode'); this.nodeData = nodeData; this.prevNodeOutputData = prevNodeOutputData; this.envVariables = envVariables; this.auth = auth; this.logger = logger; + this.collectionPath = collectionPath; } async evaluate() { @@ -28,14 +30,10 @@ class requestNode extends Node { console.debug('Evaluated Url: ', finalUrl); // step 3 - const options = this.formulateRequest(finalUrl, variablesDict); + const options = await this.formulateRequest(finalUrl, variablesDict); const res = await this.runHttpRequest(options); - if (this.nodeData?.requestBody?.type === 'form-data') { - options.data.value = ''; - } - if (res.error) { this.logger.add(LogLevel.ERROR, 'HTTP request failed', { type: 'requestNode', @@ -81,7 +79,7 @@ class requestNode extends Node { } } - formulateRequest(finalUrl, variablesDict) { + async formulateRequest(finalUrl, variablesDict) { let restMethod = this.nodeData.requestType.toLowerCase(); let contentType = 'application/json'; let requestData = undefined; @@ -94,11 +92,8 @@ class requestNode extends Node { : JSON.parse('{}'); } else if (this.nodeData.requestBody.type === 'form-data') { contentType = 'multipart/form-data'; - requestData = { - key: computeVariables(this.nodeData.requestBody.body.key, variablesDict), - value: this.nodeData.requestBody.body.value, - name: this.nodeData.requestBody.body.name, - }; + const params = cloneDeep(this.nodeData.requestBody.body); + requestData = params; } } @@ -106,7 +101,7 @@ class requestNode extends Node { method: restMethod, url: finalUrl, headers: { - 'Content-type': contentType, + 'content-type': contentType, }, data: requestData, }; @@ -124,7 +119,7 @@ class requestNode extends Node { const { ipcRenderer } = window; return new Promise((resolve, reject) => { - ipcRenderer.invoke('renderer:run-http-request', request).then(resolve).catch(reject); + ipcRenderer.invoke('renderer:run-http-request', request, this.collectionPath).then(resolve).catch(reject); }); } } diff --git a/src/components/molecules/flow/index.js b/src/components/molecules/flow/index.js index a9c081a..1d69ecb 100644 --- a/src/components/molecules/flow/index.js +++ b/src/components/molecules/flow/index.js @@ -196,8 +196,8 @@ const Flow = ({ tab, collectionId }) => { const onGraphComplete = async (status, time, logs) => { const response = await uploadGraphRunLogs(tab.name, status, time, logs); - console.log(response); - setLogs(tab.id, logs, response); + //console.log(response); + setLogs(tab.id, status, logs, response); if (status == 'Success') { toast.success(`FlowTest Run Success!`); } else if (status == 'Failed') { @@ -243,7 +243,7 @@ const Flow = ({ tab, collectionId }) => { > setViewport(reactFlowInstance.getViewport())} > -
- {nodeData.requestBody.body.name != '' ? nodeData.requestBody.body.name : 'Choose a file to upload'} +
+
+
+
Add Param
+ { + const currentParams = nodeData.requestBody.body; + const updatedParams = currentParams.concat([{ key: '', value: '', type }]); + setRequestNodeBody(nodeId, 'form-data', updatedParams); + }} + />
+ {renderFormData(nodeData.requestBody.body)}
diff --git a/src/components/molecules/sideSheets/FlowLogs.js b/src/components/molecules/sideSheets/FlowLogs.js index f2a0dae..3b532a0 100644 --- a/src/components/molecules/sideSheets/FlowLogs.js +++ b/src/components/molecules/sideSheets/FlowLogs.js @@ -1,5 +1,11 @@ import React, { useState } from 'react'; -import { ShieldCheckIcon, BarsArrowUpIcon, ExclamationTriangleIcon } from '@heroicons/react/24/outline'; +import { + ShieldCheckIcon, + BarsArrowUpIcon, + ExclamationTriangleIcon, + XCircleIcon, + CheckCircleIcon, +} from '@heroicons/react/24/outline'; import { JsonView, collapseAllNested, defaultStyles } from 'react-json-view-lite'; import { LogLevel } from '../flow/graph/GraphLogger'; import { ClockIcon } from '@heroicons/react/20/solid'; @@ -87,7 +93,16 @@ const FlowLogs = ({ logsData }) => {
{renderFlowScan(logsData.run.scan)}
-

Logs

+

+
+ Logs + {logsData.run.status === 'Success' ? ( + + ) : ( + + )} +
+

{logsData.run.logs.map((log, index) => { if (log.logLevel === LogLevel.INFO) { diff --git a/src/stores/TabStore.js b/src/stores/TabStore.js index 6ead9a6..ba68b0b 100644 --- a/src/stores/TabStore.js +++ b/src/stores/TabStore.js @@ -99,12 +99,13 @@ export const useTabStore = create((set, get) => ({ }), ); }, - updateFlowTestLogs: (tabId, logs, flowScan) => { + updateFlowTestLogs: (tabId, status, logs, flowScan) => { set( produce((state) => { const existingTab = state.tabs.find((t) => t.id === tabId); if (existingTab) { existingTab.run = { + status, scan: flowScan, logs, };