Convert request bodies when changing type (#499)

This commit is contained in:
Gregory Schier
2026-07-04 22:22:35 -07:00
committed by GitHub
parent e52853cc2d
commit eb2a2dd775
13 changed files with 1065 additions and 151 deletions
+117
View File
@@ -0,0 +1,117 @@
type GraphQLDetectionSignal = {
score: number;
requiresGraphQLDocument?: boolean;
};
export type GraphQLJsonBody = {
query: string;
variables?: string;
operationName?: string;
};
type GraphQLJsonBodyArgs = {
mimeType: string | null;
text: string;
url: string;
};
export function isGraphQLJsonBody(args: GraphQLJsonBodyArgs): boolean {
return parseGraphQLJsonBody(args) != null;
}
export function parseGraphQLJsonBody({
mimeType,
text,
url,
}: GraphQLJsonBodyArgs): GraphQLJsonBody | null {
if (mimeType !== "application/json") {
return null;
}
let parsed: unknown;
try {
parsed = JSON.parse(text);
} catch {
return null;
}
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
return null;
}
const body = parsed as Record<string, unknown>;
if (typeof body.query !== "string") {
return null;
}
if (hasExtraGraphQLEnvelopeFields(body)) {
return null;
}
const signals = getGraphQLDetectionSignals(body, url);
const score = signals.reduce((total, signal) => total + signal.score, 0);
const hasGraphQLDocument = signals.some((signal) => signal.requiresGraphQLDocument);
if (!hasGraphQLDocument || score < 4) {
return null;
}
const result: GraphQLJsonBody = { query: body.query };
if (body.variables != null) {
result.variables =
typeof body.variables === "string" ? body.variables : JSON.stringify(body.variables, null, 2);
}
if (typeof body.operationName === "string") {
result.operationName = body.operationName;
}
return result;
}
function hasExtraGraphQLEnvelopeFields(body: Record<string, unknown>): boolean {
const allowedKeys = new Set(["query", "variables", "operationName"]);
return Object.keys(body).some((key) => !allowedKeys.has(key));
}
function getGraphQLDetectionSignals(
body: Record<string, unknown>,
url: string,
): GraphQLDetectionSignal[] {
const signals: GraphQLDetectionSignal[] = [];
const query = body.query as string;
const urlPath = getUrlPath(url).toLowerCase();
if (/\b(graphql|gql)\b/.test(urlPath)) {
signals.push({ score: 2 });
}
if (/^(query|mutation|subscription|fragment)\b/.test(query.trim())) {
signals.push({ score: 3 });
} else if (/^\{[\s\S]*\}$/.test(query.trim())) {
signals.push({ score: 3, requiresGraphQLDocument: true });
}
if (/\{[\s\S]*\}/.test(query)) {
signals.push({ score: 1, requiresGraphQLDocument: true });
}
if (typeof body.operationName === "string" && body.operationName.trim() !== "") {
signals.push({ score: 1 });
}
if (
body.variables != null &&
(typeof body.variables === "object" || typeof body.variables === "string")
) {
signals.push({ score: 1 });
}
return signals;
}
function getUrlPath(url: string): string {
try {
return new URL(url).pathname;
} catch {
return url;
}
}
+20 -11
View File
@@ -8,6 +8,7 @@ import type {
Workspace,
} from "@yaakapp/api";
import { split } from "shlex";
import { parseGraphQLJsonBody } from "./graphql";
type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
@@ -464,6 +465,8 @@ function importCommand(parseEntries: string[], workspaceId: string) {
let body = {};
let bodyType: string | null = null;
const bodyAsGET = getPairValue(flagsByName, false, ["G", "get"]);
const hasDataBody = dataParameters.length > 0 && !bodyAsGET;
const hasFormBody = multipartFormDataFromRaw != null || formDataParams.length > 0;
if (multipartFormDataFromRaw) {
// Handle multipart form data parsed from --data-raw (Chrome DevTools format)
@@ -491,15 +494,21 @@ function importCommand(parseEntries: string[], workspaceId: string) {
enabled: true,
});
} else if (dataParameters.length > 0) {
bodyType =
mimeType === "application/json" || mimeType === "text/xml" || mimeType === "text/plain"
? mimeType
: "other";
body = {
text: dataParameters
.map(({ name, value }) => (name && value ? `${name}=${value}` : name || value))
.join("&"),
};
const text = dataParameters
.map(({ name, value }) => (name && value ? `${name}=${value}` : name || value))
.join("&");
const graphqlBody = parseGraphQLJsonBody({ mimeType, text, url });
if (graphqlBody != null) {
bodyType = "graphql";
body = graphqlBody;
} else if (mimeType === "application/json" || mimeType === "text/xml" || mimeType === "text/plain") {
bodyType = mimeType;
body = { text };
} else {
bodyType = "other";
body = { text };
}
} else if (formDataParams.length) {
bodyType = mimeType ?? "multipart/form-data";
body = {
@@ -517,8 +526,8 @@ function importCommand(parseEntries: string[], workspaceId: string) {
// Method
let method = getPairValue(flagsByName, "", ["X", "request"]).toUpperCase();
if (method === "" && body) {
method = "text" in body || "form" in body ? "POST" : "GET";
if (method === "") {
method = hasDataBody || hasFormBody ? "POST" : "GET";
}
const request: ExportResources["httpRequests"][0] = {