From e7e9a9714bdc3a9327413d65c0958e57de3672ce Mon Sep 17 00:00:00 2001 From: jreyesr Date: Tue, 21 May 2024 21:40:34 -0500 Subject: [PATCH] Add a way to read outputs from the response headers, status code and req time, in addition to the response body --- components/BatchDialog.js | 5 +++- components/OutputField.js | 37 ++++++++++++++++++++----- components/OutputFieldsChooser.js | 12 ++++++--- utils.js | 45 +++++++++++++++++++++++-------- 4 files changed, 77 insertions(+), 22 deletions(-) diff --git a/components/BatchDialog.js b/components/BatchDialog.js index 06470dd..52c7651 100644 --- a/components/BatchDialog.js +++ b/components/BatchDialog.js @@ -45,7 +45,10 @@ export default function BatchDialog({context, request}) { writeFile(csvPath, outString); }, [csvData, csvHeaders, csvPath]); - const canRun = csvData.length > 0 && outputConfig.every(x => x.name && x.jsonPath); + // Valid output configs: + // * Must contain a name, AND + // * Must contain a jsonPath, OR must be statusCode or reqTime (which don't need a jsonPath) + const canRun = csvData.length > 0 && outputConfig.every(x => x.name && (x.jsonPath || ["statusCode", "reqTime"].includes(x.context))); const onRun = async () => { setSent(0); diff --git a/components/OutputField.js b/components/OutputField.js index b046ad3..34c2963 100644 --- a/components/OutputField.js +++ b/components/OutputField.js @@ -1,14 +1,24 @@ import React, { useCallback } from 'react'; import ActionButton from './ActionButton'; -export default function OutputField({options, name, jsonPath, onChange, onDelete}) { +export default function OutputField({options, name, context, jsonPath, onChange, onDelete}) { const onChangeName = useCallback((e) => { - onChange(e.target.value, jsonPath) - }, [jsonPath, onChange]); + onChange(e.target.value, context, jsonPath) + }, [context, jsonPath, onChange]); + + const onChangeContext = useCallback((e) => { + onChange(name, e.target.value, jsonPath) + }, [name, jsonPath, onChange]) const onChangeJsonPath = useCallback((e) => { - onChange(name, e.target.value) - }, [name, onChange]); + onChange(name, context, e.target.value) + }, [name, context, onChange]); + + const placeholder = { + body: "$.store.books[*].author", + headers: "X-Some-Header" + }[context]; + const shouldShowValueField = ["body", "headers"].includes(context); return
- + + ⟸ + + + + +
} \ No newline at end of file diff --git a/components/OutputFieldsChooser.js b/components/OutputFieldsChooser.js index 5dfd452..de768d8 100644 --- a/components/OutputFieldsChooser.js +++ b/components/OutputFieldsChooser.js @@ -8,15 +8,15 @@ export default function OutputFieldsChooser({colNames, onChange}) { const [outputs, setOutputs] = useState([]); const addNew = useCallback(() => { - const newVal = outputs.concat([{name: "", jsonPath: ""}]); + const newVal = outputs.concat([{name: "", context: "body", jsonPath: ""}]); setOutputs(newVal); onChange(newVal); }, [outputs, setOutputs, onChange]); - const updateField = useCallback((i) => (newName, newJsonPath) => { + const updateField = useCallback((i) => (newName, newContext, newJsonPath) => { // Poor man's deep copy, since I'm not sure if you should modify React state in place const cloned = JSON.parse(JSON.stringify(outputs)) - cloned[i] = {name: newName, jsonPath: newJsonPath}; + cloned[i] = {name: newName, context: newContext, jsonPath: newJsonPath}; setOutputs(cloned); onChange(cloned); }, [outputs, setOutputs, onChange]); @@ -30,7 +30,11 @@ export default function OutputFieldsChooser({colNames, onChange}) { return {outputs.map((o, i) => - + )} diff --git a/utils.js b/utils.js index cabb8c2..f11b001 100644 --- a/utils.js +++ b/utils.js @@ -74,19 +74,42 @@ export async function makeRequest(context, request, i, row, delay, outputConfig, return } - // Check that the Content-Type header is sensible, otherwise error out - if(!response.contentType.includes("json")) { - context.app.alert("Error!", `The response has invalid Content-Type "${response.contentType}", needs "application/json"! Alternatively, delete all Outputs and try again.`) - return // There's no point in attempting to parse the response, just jump to the next request + let responseData = {}; + // If any outputConfigs refer to the response body, we must parse it + if(outputConfig.some(x => x.context === "body")) { + // Check that the Content-Type header is sensible, otherwise error out + if(!response.contentType.includes("json")) { + context.app.alert("Error!", `The response has invalid Content-Type "${response.contentType}", needs "application/json"! Alternatively, delete all Outputs and try again.`) + return // There's no point in attempting to parse the response, just jump to the next request + } + + console.debug("parsing response data") + responseData = JSON.parse(readResponseFromFile(response.bodyPath)) } - - console.debug("parsing response data") - // Read the response data, then apply JSONPath expressions on it and update the CSV data - const responseData = JSON.parse(readResponseFromFile(response.bodyPath)) console.debug(responseData) - for(const {name, jsonPath} of outputConfig) { - let out = applyJsonPath(jsonPath, responseData) ?? null - console.debug(name, "+", jsonPath, "=>", out) + + // WEIRD: Labeled statement! https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/label + writerForLoop: + for(const {name, jsonPath, context: ctx} of outputConfig) { + let out; + switch(ctx) { + case "body": + out = applyJsonPath(jsonPath, responseData) ?? null + break + case "headers": + out = response.headers.find(h => h.name === jsonPath.toLowerCase()).value + break + case "statusCode": + out = response.statusCode.toString() + break + case "reqTime": + out = response.elapsedTime.toString() + break + default: + console.error("Unknown outputConfig:", "name", name, "jsonPath", jsonPath, "context", ctx) + continue writerForLoop // Skip to next outputConfig + } + console.debug(name, "+", jsonPath, "@", ctx, "=>", out) setCsvData(csvData => { let newData = [...csvData] // Make a copy of the old data so we can mutate it normally