Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/odd-clouds-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'flowtestai': minor
---

Allow multiple kv params in multipart form data request type
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 6 additions & 8 deletions packages/flowtest-cli/bin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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') {
Expand Down
34 changes: 23 additions & 11 deletions packages/flowtest-cli/graph/compute/requestnode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -113,19 +117,16 @@ 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;
}
}

const options = {
method: restMethod,
url: finalUrl,
headers: {
'Content-type': contentType,
'content-type': contentType,
},
data: requestData,
};
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/flowtest-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/flowtest-electron/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
31 changes: 24 additions & 7 deletions packages/flowtest-electron/src/ipc/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 11 additions & 2 deletions src/components/molecules/flow/graph/Graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,6 +21,7 @@ class Graph {
this.auth = undefined;
this.envVariables = initialEnvVars;
this.caller = caller;
this.collectionPath = collectionPath;
}

#checkTimeout() {
Expand Down Expand Up @@ -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) {
Expand All @@ -146,6 +154,7 @@ class Graph {
this.envVariables,
this.logger,
node.type,
this.collectionPath,
);
result = await cNode.evaluate();
this.envVariables = result.envVars;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
23 changes: 9 additions & 14 deletions src/components/molecules/flow/graph/compute/requestnode.js
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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 = '<BASE64_ENCODED_FILE_DATA>';
}

if (res.error) {
this.logger.add(LogLevel.ERROR, 'HTTP request failed', {
type: 'requestNode',
Expand Down Expand Up @@ -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;
Expand All @@ -94,19 +92,16 @@ 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;
}
}

const options = {
method: restMethod,
url: finalUrl,
headers: {
'Content-type': contentType,
'content-type': contentType,
},
data: requestData,
};
Expand All @@ -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);
});
}
}
Expand Down
15 changes: 8 additions & 7 deletions src/components/molecules/flow/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down Expand Up @@ -243,7 +243,7 @@ const Flow = ({ tab, collectionId }) => {
>
<Background variant='dots' gap={12} size={1} />
<Controls
className='flex shadow-none border-cyan-900'
className='flex border-cyan-900 shadow-none'
onFitView={() => setViewport(reactFlowInstance.getViewport())}
></Controls>
<Button
Expand All @@ -257,10 +257,10 @@ const Flow = ({ tab, collectionId }) => {
try {
let envVariables = {};

const activeEnv = useCollectionStore
.getState()
.collections.find((c) => c.id === collectionId)
?.environments.find((e) => e.name === useTabStore.getState().selectedEnv);
const activeCollection = useCollectionStore.getState().collections.find((c) => c.id === collectionId);
const activeEnv = activeCollection?.environments.find(
(e) => e.name === useTabStore.getState().selectedEnv,
);
if (activeEnv) {
envVariables = cloneDeep(activeEnv.variables);
}
Expand All @@ -273,6 +273,7 @@ const Flow = ({ tab, collectionId }) => {
envVariables,
logger,
'main',
activeCollection.pathname,
);
const result = await g.run();
const time = Date.now() - startTime;
Expand Down
Loading