Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Feat]: Add Import from cURL in Query Library #1803

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 24, 2025
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
1 change: 1 addition & 0 deletions client/packages/lowcoder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"types": "src/index.sdk.ts",
"dependencies": {
"@ant-design/icons": "^5.3.0",
"@bany/curl-to-json": "^1.2.8",
"@codemirror/autocomplete": "^6.11.1",
"@codemirror/commands": "^6.3.2",
"@codemirror/lang-css": "^6.2.1",
Expand Down
97 changes: 97 additions & 0 deletions client/packages/lowcoder/src/components/CurlImport.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React, { useState } from "react";
import { Modal, Input, Button, message } from "antd";
import { trans } from "i18n";
import parseCurl from "@bany/curl-to-json";
const { TextArea } = Input;
interface CurlImportModalProps {
open: boolean;
onCancel: () => void;
onSuccess: (parsedData: any) => void;
}

export function CurlImportModal(props: CurlImportModalProps) {
const { open, onCancel, onSuccess } = props;
const [curlCommand, setCurlCommand] = useState("");
const [loading, setLoading] = useState(false);

const handleImport = async () => {
if (!curlCommand.trim()) {
message.error("Please enter a cURL command");
return;
}

setLoading(true);
try {
// Parse the cURL command using the correct import
const parsedData = parseCurl(curlCommand);



// Log the result for now as requested
// console.log("Parsed cURL data:", parsedData);

// Call success callback with parsed data
onSuccess(parsedData);

// Reset form and close modal
setCurlCommand("");
onCancel();

message.success("cURL command imported successfully!");
} catch (error: any) {
console.error("Error parsing cURL command:", error);
message.error(`Failed to parse cURL command: ${error.message}`);
} finally {
setLoading(false);
}
};

const handleCancel = () => {
setCurlCommand("");
onCancel();
};

return (
<Modal
title="Import from cURL"
open={open}
onCancel={handleCancel}
footer={[
<Button key="cancel" onClick={handleCancel}>
Cancel
</Button>,
<Button key="import" type="primary" loading={loading} onClick={handleImport}>
Import
</Button>,
]}
width={600}
>
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 8, fontWeight: 500 }}>
Paste cURL Command Here
</div>
<div style={{ marginBottom: 12, color: "#666", fontSize: "12px" }}>
<div style={{ marginBottom: 4 }}>
<strong>Examples:</strong>
</div>
<div style={{ marginBottom: 2 }}>
GET: <code>curl -X GET https://jsonplaceholder.typicode.com/posts/1</code>
</div>
<div style={{ marginBottom: 2 }}>
POST: <code>curl -X POST https://jsonplaceholder.typicode.com/posts -H "Content-Type: application/json" -d '&#123;"title":"foo","body":"bar","userId":1&#125;'</code>
</div>
<div>
Users: <code>curl -X GET https://jsonplaceholder.typicode.com/users</code>
</div>
</div>
<TextArea
value={curlCommand}
onChange={(e) => setCurlCommand(e.target.value)}
placeholder="curl -X GET https://jsonplaceholder.typicode.com/posts/1"
rows={8}
style={{ fontFamily: "monospace" }}
/>
</div>
</Modal>
);
}
19 changes: 19 additions & 0 deletions client/packages/lowcoder/src/components/ResCreatePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { getUser } from "../redux/selectors/usersSelectors";
import DataSourceIcon from "./DataSourceIcon";
import { genRandomKey } from "comps/utils/idGenerator";
import { isPublicApplication } from "@lowcoder-ee/redux/selectors/applicationSelector";
import { CurlImportModal } from "./CurlImport";

const Wrapper = styled.div<{ $placement: PageType }>`
width: 100%;
Expand Down Expand Up @@ -230,6 +231,7 @@ export function ResCreatePanel(props: ResCreateModalProps) {
const { onSelect, onClose, recentlyUsed, datasource, placement = "editor" } = props;
const [isScrolling, setScrolling] = useState(false);
const [visible, setVisible] = useState(false);
const [curlModalVisible, setCurlModalVisible] = useState(false);

const isPublicApp = useSelector(isPublicApplication);
const user = useSelector(getUser);
Expand All @@ -244,6 +246,14 @@ export function ResCreatePanel(props: ResCreateModalProps) {
setScrolling(top > 0);
}, 100);

const handleCurlImportSuccess = (parsedData: any) => {
onSelect(BottomResTypeEnum.Query, {
compType: "restApi",
dataSourceId: QUICK_REST_API_ID,
curlData: parsedData
});
};

return (
<Wrapper $placement={placement}>
<Title $shadow={isScrolling} $placement={placement}>
Expand Down Expand Up @@ -331,6 +341,10 @@ export function ResCreatePanel(props: ResCreateModalProps) {
<ResButton size={buttonSize} identifier={"streamApi"} onSelect={onSelect} />
<ResButton size={buttonSize} identifier={"alasql"} onSelect={onSelect} />
<ResButton size={buttonSize} identifier={"graphql"} onSelect={onSelect} />
<DataSourceButton size={buttonSize} onClick={() => setCurlModalVisible(true)}>
<DataSourceIcon size="large" dataSourceType="restApi" />
Import from cURL
</DataSourceButton>
</DataSourceListWrapper>
</div>

Expand Down Expand Up @@ -374,6 +388,11 @@ export function ResCreatePanel(props: ResCreateModalProps) {
onCancel={() => setVisible(false)}
onCreated={() => setVisible(false)}
/>
<CurlImportModal
open={curlModalVisible}
onCancel={() => setCurlModalVisible(false)}
onSuccess={handleCurlImportSuccess}
/>
</Wrapper>
);
}
43 changes: 34 additions & 9 deletions client/packages/lowcoder/src/comps/queries/queryComp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import {
import { QueryContext } from "../../util/context/QueryContext";
import { useFixedDelay } from "../../util/hooks";
import { JSONObject, JSONValue } from "../../util/jsonTypes";
import { processCurlData } from "../../util/curlUtils";
import { BoolPureControl } from "../controls/boolControl";
import { millisecondsControl } from "../controls/millisecondControl";
import { paramsMillisecondsControl } from "../controls/paramsControl";
Expand Down Expand Up @@ -743,18 +744,42 @@ class QueryListComp extends QueryListTmpComp implements BottomResListComp {
const name = this.genNewName(editorState);
const compType = extraInfo?.compType || "js";
const dataSourceId = extraInfo?.dataSourceId;
const curlData = extraInfo?.curlData;
console.log("CURL DATA", curlData);

// Build the basic payload
let payload: any = {
id: id,
name: name,
datasourceId: dataSourceId,
compType,
triggerType: manualTriggerResource.includes(compType) ? "manual" : "automatic",
isNewCreate: true,
order: Date.now(),
};

// If this is a REST API created from cURL, pre-populate the HTTP query fields
if (compType === "restApi" && curlData) {
const curlConfig = processCurlData(curlData);
if (curlConfig) {
payload = {
...payload,
comp: {
httpMethod: curlConfig.method,
path: curlConfig.url,
headers: curlConfig.headers,
params: curlConfig.params,
bodyType: curlConfig.bodyType,
body: curlConfig.body,
bodyFormData: curlConfig.bodyFormData,
},
};
}
}

this.dispatch(
wrapActionExtraInfo(
this.pushAction({
id: id,
name: name,
datasourceId: dataSourceId,
compType,
triggerType: manualTriggerResource.includes(compType) ? "manual" : "automatic",
isNewCreate: true,
order: Date.now(),
}),
this.pushAction(payload),
{
compInfos: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"
import { Helmet } from "react-helmet";
import {fetchQLPaginationByOrg} from "@lowcoder-ee/util/pagination/axios";
import { isEmpty } from "lodash";
import { processCurlData } from "../../util/curlUtils";

const Wrapper = styled.div`
display: flex;
Expand Down Expand Up @@ -199,17 +200,39 @@ export const QueryLibraryEditor = () => {
const newName = nameGenerator.genItemName(trans("queryLibrary.unnamed"));

const handleAdd = (type: BottomResTypeEnum, extraInfo?: any) => {
// Build basic query DSL
let queryDSL: any = {
triggerType: "manual",
datasourceId: extraInfo?.dataSourceId,
compType: extraInfo?.compType,
};

// If it is a REST API created from cURL, pre-populate the HTTP query fields
if (extraInfo?.compType === "restApi" && extraInfo?.curlData) {
const curlConfig = processCurlData(extraInfo.curlData);
if (curlConfig) {
queryDSL = {
...queryDSL,
comp: {
httpMethod: curlConfig.method,
path: curlConfig.url,
headers: curlConfig.headers,
params: curlConfig.params,
bodyType: curlConfig.bodyType,
body: curlConfig.body,
bodyFormData: curlConfig.bodyFormData,
},
};
}
}

dispatch(
createQueryLibrary(
{
name: newName,
organizationId: orgId,
libraryQueryDSL: {
query: {
triggerType: "manual",
datasourceId: extraInfo?.dataSourceId,
compType: extraInfo?.compType,
},
query: queryDSL,
},
},
(resp) => {
Expand All @@ -218,7 +241,6 @@ export const QueryLibraryEditor = () => {
setModify(!modify);
}, 200);
setCurrentPage(Math.ceil(elements.total / pageSize));

},
() => {}
)
Expand Down
114 changes: 114 additions & 0 deletions client/packages/lowcoder/src/util/curlUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* Utility to convert parsed cURL data from @bany/curl-to-json library
* to the format expected by REST API query components
*/

// Body type mapping to match the dropdown values in httpQuery.tsx
const CONTENT_TYPE_TO_BODY_TYPE: Record<string, string> = {
"application/json": "application/json",
"text/plain": "text/plain",
"text/html": "text/plain",
"text/xml": "text/plain",
"application/xml": "text/plain",
"application/x-www-form-urlencoded": "application/x-www-form-urlencoded",
"multipart/form-data": "multipart/form-data",
};

/**
* Parse URL-encoded form data - handles both string and object input
*/
function parseUrlEncodedData(data: string | object): Array<{ key: string; value: string; type: string }> {
if (!data) {
return [{ key: "", value: "", type: "text" }];
}

try {
let result: Array<{ key: string; value: string; type: string }> = [];

if (typeof data === 'object') {
// @bany/curl-to-json already parsed it into an object
Object.entries(data).forEach(([key, value]) => {
result.push({
key: key,
value: decodeURIComponent(String(value).replace(/\+/g, ' ')), // Handle URL encoding
type: "text"
});
});
} else if (typeof data === 'string') {
// Raw URL-encoded string - use URLSearchParams
const params = new URLSearchParams(data);
params.forEach((value, key) => {
result.push({
key: key,
value: value,
type: "text"
});
});
}

return result.length > 0 ? result : [{ key: "", value: "", type: "text" }];
} catch (error) {
console.warn('Failed to parse URL-encoded data:', error);
return [{ key: "", value: "", type: "text" }];
}
}

export function processCurlData(curlData: any) {
if (!curlData) return null;


// Convert headers object to key-value array format expected by UI
const headers = curlData.header
? Object.entries(curlData.header).map(([key, value]) => ({ key, value }))
: [{ key: "", value: "" }];

// Convert query params object to key-value array format expected by UI
const params = curlData.params
? Object.entries(curlData.params).map(([key, value]) => ({ key, value }))
: [{ key: "", value: "" }];

// Get request body - @bany/curl-to-json may use 'body' or 'data'
const bodyContent = curlData.body !== undefined ? curlData.body : curlData.data;

// Determine body type based on Content-Type header or content structure
let bodyType = "none";
let bodyFormData = [{ key: "", value: "", type: "text" }];
let processedBody = "";

if (bodyContent !== undefined && bodyContent !== "") {
const contentTypeHeader = curlData.header?.["Content-Type"] || curlData.header?.["content-type"];

if (contentTypeHeader) {
// Extract base content type (remove charset, boundary, etc.)
const baseContentType = contentTypeHeader.split(';')[0].trim().toLowerCase();
bodyType = CONTENT_TYPE_TO_BODY_TYPE[baseContentType] || "text/plain";
} else {
// Fallback: infer from content structure
if (typeof bodyContent === "object") {
bodyType = "application/json";
} else {
bodyType = "text/plain";
}
}

// Handle different body types
if (bodyType === "application/x-www-form-urlencoded") {
bodyFormData = parseUrlEncodedData(bodyContent);
processedBody = ""; // Form data goes in bodyFormData, not body
} else if (typeof bodyContent === "object") {
processedBody = JSON.stringify(bodyContent, null, 2);
} else {
processedBody = bodyContent;
}
}

return {
method: curlData.method || "GET",
url: curlData.url || "",
headers,
params,
bodyType,
body: processedBody,
bodyFormData,
};
}
Loading
Loading