Skip to content

Commit d3bfcaa

Browse files
Write correctly formatted ndjson bodies in the curl exporter
1 parent 96626b9 commit d3bfcaa

File tree

5 files changed

+77
-5
lines changed

5 files changed

+77
-5
lines changed

src/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ async function main() {
1616
"output complete code that includes the creation of the client",
1717
false,
1818
)
19+
.option("--windows", "output a Windows command (curl format only)", false)
1920
.option(
2021
"--elasticsearch-url",
2122
"Elasticsearch endpoint URL. Only needed when --complete is given.",
@@ -34,6 +35,7 @@ async function main() {
3435

3536
const code = (await convertRequests(data, opts.format, {
3637
complete: opts.complete,
38+
windows: opts.windows,
3739
elasticsearchUrl: opts.elasticsearchUrl,
3840
debug: opts.debug,
3941
printResponse: opts.printResponse,

src/exporters/curl.ts

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,50 @@ export class CurlExporter implements FormatExporter {
1212
options: ConvertOptions,
1313
): Promise<string> {
1414
const escapedSingleQuote = options.windows ? "''" : "'\"'\"'";
15+
const escapedDoubleQuote = options.windows ? '""' : '\\"';
1516
const envPrefix = options.windows ? "$Env:" : "$";
17+
const newLineInCmd = options.windows ? "`n" : "\\n";
1618
const auth = ` -H "Authorization: ApiKey ${envPrefix}ELASTIC_API_KEY"`;
1719
const otherUrls = (options.otherUrls as Record<string, string>) ?? {};
1820
let output = "";
1921
for (const request of requests) {
2022
let headers = auth;
2123
let body = "";
2224
if (request.body) {
23-
headers += ' -H "Content-Type: application/json"';
24-
body =
25-
" -d '" +
26-
JSON.stringify(request.body).replaceAll("'", escapedSingleQuote) +
27-
"'";
25+
if (
26+
Array.isArray(request.body) &&
27+
request.mediaTypes?.includes("application/x-ndjson")
28+
) {
29+
// this is a bulk request with ndjson payload
30+
headers += ' -H "Content-Type: application/x-ndjson"';
31+
if (!options.windows) {
32+
body =
33+
" -d $'" +
34+
request.body
35+
.map((line) =>
36+
JSON.stringify(line).replaceAll("'", escapedSingleQuote),
37+
)
38+
.join(newLineInCmd) +
39+
newLineInCmd +
40+
"'";
41+
} else {
42+
body =
43+
' -d "' +
44+
request.body
45+
.map((line) =>
46+
JSON.stringify(line).replaceAll('"', escapedDoubleQuote),
47+
)
48+
.join(newLineInCmd) +
49+
newLineInCmd +
50+
'"';
51+
}
52+
} else {
53+
headers += ' -H "Content-Type: application/json"';
54+
body =
55+
" -d '" +
56+
JSON.stringify(request.body).replaceAll("'", escapedSingleQuote) +
57+
"'";
58+
}
2859
}
2960
const method =
3061
request.method != "HEAD" ? `-X ${request.method}` : "--head";

src/parse.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ export type ParsedRequest = {
2626
api?: string;
2727
/** The availability definition from the Elasticsearch specification. */
2828
availability?: Availabilities;
29+
/** The media types that are accepted for the request body. */
30+
mediaTypes?: string[];
2931
/** The request definition from the Elasticsearch specification that applies to this request. */
3032
request?: Request;
3133
/** The dynamic parameters that are part of the request's URL. */
@@ -49,6 +51,7 @@ type ESRoute = {
4951
name: string;
5052
availability: Availabilities;
5153
request: Request;
54+
mediaTypes?: string[];
5255
};
5356

5457
let router = Router.make<ESRoute>({
@@ -296,6 +299,7 @@ export async function loadSchema(filename_or_object: string | object) {
296299
// find the request in the spec
297300
try {
298301
let req: Request | undefined;
302+
let mt: string[] | undefined;
299303
for (const type of spec.types) {
300304
if (
301305
type.name.namespace == endpoint.request?.namespace &&
@@ -308,13 +312,15 @@ export async function loadSchema(filename_or_object: string | object) {
308312
);
309313
}
310314
req = type as Request;
315+
mt = endpoint.requestMediaType;
311316
break;
312317
}
313318
}
314319
const r = {
315320
name: endpoint.name,
316321
availability: endpoint.availability,
317322
request: req as Request,
323+
mediaTypes: mt,
318324
};
319325
router.on(methods, formattedPath as Router.PathInput, r);
320326
} catch (err) {
@@ -365,6 +371,7 @@ export async function parseRequest(
365371
req.api = route.handler.name;
366372
req.availability = route.handler.availability;
367373
req.request = route.handler.request;
374+
req.mediaTypes = route.handler.mediaTypes;
368375
if (Object.keys(route.params).length > 0) {
369376
req.params = route.params;
370377
}

tests/convert.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ POST /my-index/_search?from=40&size=20
2323
}
2424
}`;
2525

26+
const devConsoleBulkScript = `POST _bulk
27+
{ "index" : { "_index" : "test", "_id" : "1" } }
28+
{ "field1" : "value1" }
29+
{ "delete" : { "_index" : "test", "_id" : "2" } }
30+
{ "create" : { "_index" : "test", "_id" : "3" } }
31+
{ "field1" : "value3" }
32+
{ "update" : {"_id" : "1", "_index" : "test"} }
33+
{ "doc" : {"field2" : "value2"} }`;
34+
2635
const kibanaScript = `GET /
2736
2837
GET kbn:/api/saved_objects/_find?type=dashboard`;
@@ -139,6 +148,27 @@ describe("convert", () => {
139148
);
140149
});
141150

151+
it("converts a bulk request to curl", async () => {
152+
expect(
153+
await convertRequests(devConsoleBulkScript, "curl", {
154+
elasticsearchUrl: "http://localhost:9876",
155+
}),
156+
).toEqual(
157+
`curl -X POST -H "Authorization: ApiKey $ELASTIC_API_KEY" -H "Content-Type: application/x-ndjson" -d $'{"index":{"_index":"test","_id":"1"}}\\n{"field1":"value1"}\\n{"delete":{"_index":"test","_id":"2"}}\\n{"create":{"_index":"test","_id":"3"}}\\n{"field1":"value3"}\\n{"update":{"_id":"1","_index":"test"}}\\n{"doc":{"field2":"value2"}}\\n' "http://localhost:9876/_bulk"\n`,
158+
);
159+
});
160+
161+
it("converts a bulk request to curl on Windows", async () => {
162+
expect(
163+
await convertRequests(devConsoleBulkScript, "curl", {
164+
elasticsearchUrl: "http://localhost:9876",
165+
windows: true,
166+
}),
167+
).toEqual(
168+
`curl -X POST -H "Authorization: ApiKey $Env:ELASTIC_API_KEY" -H "Content-Type: application/x-ndjson" -d "{""index"":{""_index"":""test"",""_id"":""1""}}\`n{""field1"":""value1""}\`n{""delete"":{""_index"":""test"",""_id"":""2""}}\`n{""create"":{""_index"":""test"",""_id"":""3""}}\`n{""field1"":""value3""}\`n{""update"":{""_id"":""1"",""_index"":""test""}}\`n{""doc"":{""field2"":""value2""}}\`n" "http://localhost:9876/_bulk"\n`,
169+
);
170+
});
171+
142172
it("converts Kibana to curl", async () => {
143173
expect(
144174
await convertRequests(kibanaScript, "curl", {

tests/parse.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ POST\n_ml/anomaly_detectors/it_ops_new_logs/model_snapshots/1491852978/_update\n
169169
rawPath: "/_bulk",
170170
query: { foo: "bar" },
171171
body: [{ name: "John Doe" }, { name: "John Doe" }, { name: "John Doe" }],
172+
mediaTypes: ["application/x-ndjson"],
172173
});
173174
expect(reqs[6]).toMatchObject({
174175
source:
@@ -186,6 +187,7 @@ POST\n_ml/anomaly_detectors/it_ops_new_logs/model_snapshots/1491852978/_update\n
186187
{ name: "John\nDoe" },
187188
{ name: "John\nDoe" },
188189
],
190+
mediaTypes: ["application/x-ndjson"],
189191
});
190192
expect(reqs[7]).toMatchObject({
191193
source: "GET /{customer}/_doc/1",

0 commit comments

Comments
 (0)