arazzo-runner 0.0.15 → 0.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -4
- package/package.json +1 -1
- package/src/Arazzo.js +8 -454
- package/src/Operation.js +685 -0
package/README.md
CHANGED
|
@@ -206,6 +206,21 @@ You will need to provide inputs for your clientId and clientSecret:
|
|
|
206
206
|
|
|
207
207
|
`clientId` and `clientSecret` are reserved name and will be used when oauth or openId authentication is set.
|
|
208
208
|
|
|
209
|
+
## Request Bodies
|
|
210
|
+
|
|
211
|
+
This can handle the encoding and sending of:
|
|
212
|
+
|
|
213
|
+
- application/json
|
|
214
|
+
- application/xml
|
|
215
|
+
- text/xml
|
|
216
|
+
- text/html
|
|
217
|
+
- text/plain
|
|
218
|
+
- application/x-www-form-urlencoded
|
|
219
|
+
- multipart/form-data
|
|
220
|
+
- application/octet-stream
|
|
221
|
+
|
|
222
|
+
For anything outside of this range, it will attempt to encode as JSON if you specify an object, otherwise it will encode as plain text.
|
|
223
|
+
|
|
209
224
|
## Logging And Reporting
|
|
210
225
|
|
|
211
226
|
### Logging
|
|
@@ -243,7 +258,3 @@ Accessing an OpenAPI operation by Operation Path `'{$sourceDescriptions.petstore
|
|
|
243
258
|
### Non application/json Responses
|
|
244
259
|
|
|
245
260
|
Responses that do not conform to application/json do not work
|
|
246
|
-
|
|
247
|
-
### Non application/json Requests
|
|
248
|
-
|
|
249
|
-
Requests that do not conform to application/json do not work
|
package/package.json
CHANGED
package/src/Arazzo.js
CHANGED
|
@@ -9,6 +9,7 @@ const path = require("node:path");
|
|
|
9
9
|
|
|
10
10
|
const Document = require("./Document");
|
|
11
11
|
const Expression = require("./Expression");
|
|
12
|
+
const Operation = require('./Operation');
|
|
12
13
|
const Rules = require("./Rules");
|
|
13
14
|
|
|
14
15
|
class Arazzo extends Document {
|
|
@@ -254,255 +255,15 @@ class Arazzo extends Document {
|
|
|
254
255
|
this.step,
|
|
255
256
|
);
|
|
256
257
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
this.sourceDescriptionFile.securitySchemes,
|
|
262
|
-
)) {
|
|
263
|
-
if (value.type.toLowerCase() === "mutualtls")
|
|
264
|
-
this.operation.mutualTLS = true;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
this.mapInputs();
|
|
269
|
-
|
|
270
|
-
await this.runOperation();
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
*
|
|
275
|
-
* @private
|
|
276
|
-
* @param {*} retry
|
|
277
|
-
* @param {*} retryAfter
|
|
278
|
-
*/
|
|
279
|
-
async runOperation(retry = 0, retryAfter = 0) {
|
|
280
|
-
const sleep = function (ms) {
|
|
281
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
const parseRetryAfter = function (retryAfter) {
|
|
285
|
-
if (!retryAfter || typeof retryAfter !== "string") {
|
|
286
|
-
return null;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const trimmed = retryAfter.trim();
|
|
290
|
-
|
|
291
|
-
// Try parsing as a number (seconds format)
|
|
292
|
-
const asNumber = parseInt(trimmed, 10);
|
|
293
|
-
if (!isNaN(asNumber) && asNumber >= 0 && String(asNumber) === trimmed) {
|
|
294
|
-
return asNumber;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Try parsing as HTTP date format
|
|
298
|
-
try {
|
|
299
|
-
const date = new Date(trimmed);
|
|
300
|
-
|
|
301
|
-
// Check if date is valid
|
|
302
|
-
if (isNaN(date.getTime())) {
|
|
303
|
-
return null;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Calculate seconds from now until the date
|
|
307
|
-
const now = new Date();
|
|
308
|
-
const secondsUntil = Math.ceil((date.getTime() - now.getTime()) / 1000);
|
|
309
|
-
|
|
310
|
-
// Return the delay, but don't return negative values
|
|
311
|
-
return secondsUntil >= 0 ? secondsUntil : 0;
|
|
312
|
-
} catch (err) {
|
|
313
|
-
return null;
|
|
314
|
-
}
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
let url = this.operation.url;
|
|
318
|
-
|
|
319
|
-
if (this.operation.queryParams.size) {
|
|
320
|
-
url += `?${this.operation.queryParams}`;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const options = {
|
|
324
|
-
method: this.operation.method,
|
|
325
|
-
headers: this.operation.headers,
|
|
258
|
+
const miniStep = {
|
|
259
|
+
stepId: this.step.stepId,
|
|
260
|
+
parameters: this.step.parameters || [],
|
|
261
|
+
requestBody: this.step.requestBody || undefined,
|
|
326
262
|
};
|
|
327
263
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (this.retryAfter) {
|
|
333
|
-
this.logger.notice(
|
|
334
|
-
`retryAfter was set: waiting ${this.retryAfter * 1000} seconds`,
|
|
335
|
-
);
|
|
336
|
-
await sleep(this.retryAfter * 1000);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
this.expression.addToContext("url", url);
|
|
340
|
-
this.expression.addToContext("method", options.method);
|
|
341
|
-
|
|
342
|
-
this.logger.notice(`Making a ${options.method.toUpperCase()} to ${url}`);
|
|
343
|
-
|
|
344
|
-
let response;
|
|
345
|
-
if (this.operation?.mutualTLS) {
|
|
346
|
-
this.logger.verbose("Using mutualTLS call");
|
|
347
|
-
response = await this.mutualTLS();
|
|
348
|
-
} else {
|
|
349
|
-
this.logger.verbose("Using fetch call");
|
|
350
|
-
|
|
351
|
-
this.logger.verbose(`request url: ${url}`);
|
|
352
|
-
this.logger.verbose(`request method: ${options.method}`);
|
|
353
|
-
this.logger.verbose("request headers:");
|
|
354
|
-
for (const [key, value] of options.headers.entries()) {
|
|
355
|
-
this.logger.verbose(`${key}: ${value}`);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
this.logger.verbose(`body: ${options.body}`);
|
|
359
|
-
|
|
360
|
-
response = await fetch(url, options);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
this.logger.verbose(`received StatusCode: ${response.status}`);
|
|
364
|
-
this.logger.verbose(`received headers:`);
|
|
365
|
-
for (const [key, value] of response.headers.entries()) {
|
|
366
|
-
this.logger.verbose(`${key}: ${value}`);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (response.headers.has("retry-after")) {
|
|
370
|
-
const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
|
|
371
|
-
if (retryAfter !== null) {
|
|
372
|
-
this.retryAfter = retryAfter;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
this.addParamsToContext(response.headers, "header", "response");
|
|
377
|
-
this.expression.addToContext("statusCode", response.status);
|
|
378
|
-
|
|
379
|
-
this.logger.notice(`${url} responded with a: ${response.status}`);
|
|
380
|
-
|
|
381
|
-
await this.dealWithResponse(response);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
async mutualTLS() {
|
|
385
|
-
let clientKeyPath;
|
|
386
|
-
try {
|
|
387
|
-
clientKeyPath = path.resolve(this.inputs.key);
|
|
388
|
-
this.logger.verbose(`clientKey Path: ${clientKeyPath}`);
|
|
389
|
-
} catch (err) {
|
|
390
|
-
this.logger.error(`could not resolve clientKey`);
|
|
391
|
-
throw err;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
let clientCertPath;
|
|
395
|
-
try {
|
|
396
|
-
clientCertPath = path.resolve(this.inputs.cert);
|
|
397
|
-
this.logger.verbose(`clientCert Path: ${clientCertPath}`);
|
|
398
|
-
} catch (err) {
|
|
399
|
-
this.logger.error(`could not resolve clientCert`);
|
|
400
|
-
throw err;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
let url = this.operation.url;
|
|
404
|
-
|
|
405
|
-
if (this.operation.queryParams.size) {
|
|
406
|
-
url += `?${this.operation.queryParams}`;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
const opUrl = new URL(url);
|
|
410
|
-
|
|
411
|
-
this.logger.verbose(`url: ${opUrl.pathname + opUrl.search}`);
|
|
412
|
-
this.logger.verbose(`method: ${this.operation.method}`);
|
|
413
|
-
this.logger.verbose("headers:");
|
|
414
|
-
const headersObj = {};
|
|
415
|
-
for (const [key, value] of this.operation.headers.entries()) {
|
|
416
|
-
this.logger.verbose(`${key}: ${value}`);
|
|
417
|
-
Object.assign(headersObj, { [key]: value });
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
this.logger.verbose(`body: ${this.operation.data}`);
|
|
421
|
-
|
|
422
|
-
return new Promise((resolve, reject) => {
|
|
423
|
-
const options = {
|
|
424
|
-
key: fs.readFileSync(clientKeyPath),
|
|
425
|
-
cert: fs.readFileSync(clientCertPath),
|
|
426
|
-
method: this.operation.method,
|
|
427
|
-
headers: headersObj,
|
|
428
|
-
rejectUnauthorized: true,
|
|
429
|
-
hostname: opUrl.hostname,
|
|
430
|
-
path: opUrl.pathname + opUrl.search,
|
|
431
|
-
};
|
|
432
|
-
|
|
433
|
-
if (this.operation.data) {
|
|
434
|
-
options.headers = options.headers || {};
|
|
435
|
-
if (!options.headers["content-length"]) {
|
|
436
|
-
const bodyBuffer = Buffer.from(
|
|
437
|
-
typeof this.operation.data === "string"
|
|
438
|
-
? this.operation.data
|
|
439
|
-
: JSON.stringify(this.operation.data),
|
|
440
|
-
);
|
|
441
|
-
options.headers["content-length"] = bodyBuffer.length;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
const headers = new Headers();
|
|
446
|
-
const chunks = [];
|
|
447
|
-
|
|
448
|
-
const req = https.request(options, (res) => {
|
|
449
|
-
for (const [name, value] of Object.entries(res.headers)) {
|
|
450
|
-
headers.append(name, value);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// Collect data chunks as buffers for proper encoding
|
|
454
|
-
res.on("data", (chunk) => chunks.push(chunk));
|
|
455
|
-
|
|
456
|
-
res.on("end", () => {
|
|
457
|
-
// Concatenate buffers and decode based on content-type
|
|
458
|
-
const buffer = Buffer.concat(chunks);
|
|
459
|
-
let body;
|
|
460
|
-
|
|
461
|
-
const contentType = res.headers["content-type"] || "";
|
|
462
|
-
|
|
463
|
-
if (contentType.includes("application/json")) {
|
|
464
|
-
try {
|
|
465
|
-
body = JSON.parse(buffer.toString("utf8"));
|
|
466
|
-
} catch (err) {
|
|
467
|
-
body = buffer.toString("utf8");
|
|
468
|
-
}
|
|
469
|
-
} else {
|
|
470
|
-
body = buffer.toString("utf8");
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
const response = new Response(buffer, {
|
|
474
|
-
status: res.status,
|
|
475
|
-
statusText: res.statusMessage,
|
|
476
|
-
headers,
|
|
477
|
-
});
|
|
478
|
-
// resolve({
|
|
479
|
-
// headers: headers,
|
|
480
|
-
// body: body,
|
|
481
|
-
|
|
482
|
-
// status: res.statusCode,
|
|
483
|
-
// statusText: res.statusMessage,
|
|
484
|
-
// ok: res.statusCode >= 200 && res.statusCode < 300,
|
|
485
|
-
// });
|
|
486
|
-
resolve(response);
|
|
487
|
-
});
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
req.on("error", (err) => {
|
|
491
|
-
this.logger.error(`mTLS Error: ${err.message}`);
|
|
492
|
-
reject(new Error(`mTLS request failed: ${err.message}`));
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
// Write request body if present
|
|
496
|
-
if (this.operation.data) {
|
|
497
|
-
const body =
|
|
498
|
-
typeof this.operation.data === "string"
|
|
499
|
-
? this.operation.data
|
|
500
|
-
: JSON.stringify(this.operation.data);
|
|
501
|
-
req.write(body);
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
req.end();
|
|
505
|
-
});
|
|
264
|
+
const apiOperation = new Operation(this.operation, this.sourceDescriptionFile, this.expression, this.inputs, this.logger, miniStep);
|
|
265
|
+
const response = await apiOperation.runOperation()
|
|
266
|
+
await this.dealWithResponse(response)
|
|
506
267
|
}
|
|
507
268
|
|
|
508
269
|
/**
|
|
@@ -510,24 +271,6 @@ class Arazzo extends Document {
|
|
|
510
271
|
* @param {*} response
|
|
511
272
|
*/
|
|
512
273
|
async dealWithResponse(response) {
|
|
513
|
-
if (
|
|
514
|
-
response.headers.has("Content-Type") &&
|
|
515
|
-
response.headers.get("Content-Type") === "application/json"
|
|
516
|
-
) {
|
|
517
|
-
const json = await response?.json().catch((err) => {
|
|
518
|
-
this.logger.error(
|
|
519
|
-
`Error trying to resolve ${this.step.stepId} outputs`,
|
|
520
|
-
);
|
|
521
|
-
throw new Error(err);
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
this.expression.addToContext("response.body", json);
|
|
525
|
-
} else {
|
|
526
|
-
const body = await response.body;
|
|
527
|
-
|
|
528
|
-
this.expression.addToContext("response.body", body);
|
|
529
|
-
}
|
|
530
|
-
|
|
531
274
|
this.doNotProcessStep = false;
|
|
532
275
|
this.alreadyProcessingOnFailure = false;
|
|
533
276
|
|
|
@@ -878,195 +621,6 @@ class Arazzo extends Document {
|
|
|
878
621
|
});
|
|
879
622
|
}
|
|
880
623
|
|
|
881
|
-
/**
|
|
882
|
-
* @private
|
|
883
|
-
*/
|
|
884
|
-
mapInputs() {
|
|
885
|
-
this.mapParameters();
|
|
886
|
-
this.mapRequestBody();
|
|
887
|
-
|
|
888
|
-
this.addParamsToContext(this.operation.headers, "headers", "request");
|
|
889
|
-
this.addParamsToContext(this.operation.queryParams, "query", "request");
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
/**
|
|
893
|
-
* @private
|
|
894
|
-
*/
|
|
895
|
-
mapParameters() {
|
|
896
|
-
const headersObj = new Headers();
|
|
897
|
-
const headers = new URLParams();
|
|
898
|
-
const queryParams = new URLParams();
|
|
899
|
-
const pathParams = new URLParams();
|
|
900
|
-
|
|
901
|
-
for (const param of this.step?.parameters || []) {
|
|
902
|
-
const operationDetailParam =
|
|
903
|
-
this.sourceDescriptionFile.operationDetails?.parameters
|
|
904
|
-
.filter((obj) => obj.name === param.name && obj.in === param.in)
|
|
905
|
-
.at(0);
|
|
906
|
-
|
|
907
|
-
let value = this.expression.resolveExpression(param.value);
|
|
908
|
-
|
|
909
|
-
switch (param.in) {
|
|
910
|
-
case "header":
|
|
911
|
-
let headerStyle = "simple";
|
|
912
|
-
let headerExplode = false;
|
|
913
|
-
|
|
914
|
-
if (
|
|
915
|
-
operationDetailParam?.style &&
|
|
916
|
-
["accept", "authorization", "content-type"].includes(
|
|
917
|
-
operationDetailParam.name.toLowerCase() === false,
|
|
918
|
-
)
|
|
919
|
-
) {
|
|
920
|
-
headerStyle = operationDetailParam.style;
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
if (
|
|
924
|
-
operationDetailParam?.explode &&
|
|
925
|
-
["accept", "authorization", "content-type"].includes(
|
|
926
|
-
operationDetailParam.name.toLowerCase() === false,
|
|
927
|
-
)
|
|
928
|
-
) {
|
|
929
|
-
headerExplode = operationDetailParam.explode;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
if (this.operation.security) {
|
|
933
|
-
// console.log(this.operation.security);
|
|
934
|
-
for (const key in this.sourceDescriptionFile.securitySchemes) {
|
|
935
|
-
if (
|
|
936
|
-
this.sourceDescriptionFile.securitySchemes[key].type ===
|
|
937
|
-
"http" &&
|
|
938
|
-
param.name === key
|
|
939
|
-
) {
|
|
940
|
-
if (
|
|
941
|
-
this.sourceDescriptionFile.securitySchemes[
|
|
942
|
-
key
|
|
943
|
-
].scheme.toLowerCase() === "bearer"
|
|
944
|
-
) {
|
|
945
|
-
value = `Bearer ${value}`;
|
|
946
|
-
} else {
|
|
947
|
-
const basicPass = Buffer.from(
|
|
948
|
-
this.expression.resolveExpression(value),
|
|
949
|
-
).toString("base64");
|
|
950
|
-
value = `Basic ${basicPass}`;
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
// const authSchemaName = Object.keys(
|
|
956
|
-
// this.operation.security.at(0),
|
|
957
|
-
// ).at(0);
|
|
958
|
-
|
|
959
|
-
// const securityScheme =
|
|
960
|
-
// this.sourceDescriptionFile.securitySchemes[authSchemaName];
|
|
961
|
-
// console.log(securityScheme);
|
|
962
|
-
|
|
963
|
-
// if (
|
|
964
|
-
// securityScheme.type === "http" &&
|
|
965
|
-
// securityScheme?.scheme?.toLowerCase() === "bearer"
|
|
966
|
-
// ) {
|
|
967
|
-
// value = `Bearer ${value}`;
|
|
968
|
-
// } else if (
|
|
969
|
-
// securityScheme.type === "http" &&
|
|
970
|
-
// securityScheme?.scheme?.toLowerCase() === "basic"
|
|
971
|
-
// ) {
|
|
972
|
-
// const basicPass = Buffer.from(
|
|
973
|
-
// this.expression.resolveExpression(value),
|
|
974
|
-
// ).toString("base64");
|
|
975
|
-
// value = `Basic ${basicPass}`;
|
|
976
|
-
// }
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
headers.append(param.name, value, {
|
|
980
|
-
style: headerStyle,
|
|
981
|
-
explode: headerExplode,
|
|
982
|
-
});
|
|
983
|
-
|
|
984
|
-
for (const [header, value] of headers) {
|
|
985
|
-
if (header === param.name) {
|
|
986
|
-
headersObj.append(param.name, value);
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
break;
|
|
991
|
-
|
|
992
|
-
case "path":
|
|
993
|
-
const pathStyle = operationDetailParam?.style || "simple";
|
|
994
|
-
const pathExplode = operationDetailParam?.explode || false;
|
|
995
|
-
|
|
996
|
-
pathParams.append(param.name, value, {
|
|
997
|
-
style: pathStyle,
|
|
998
|
-
explode: pathExplode,
|
|
999
|
-
});
|
|
1000
|
-
|
|
1001
|
-
for (const [name, value] of pathParams.entries()) {
|
|
1002
|
-
this.operation.url = this.operation.url.replace(`{${name}}`, value);
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
break;
|
|
1006
|
-
|
|
1007
|
-
case "query":
|
|
1008
|
-
// queryParams.append(param.name, value);
|
|
1009
|
-
const style = operationDetailParam?.style || "form";
|
|
1010
|
-
let explode = false;
|
|
1011
|
-
if (Object.hasOwn(operationDetailParam, "explode")) {
|
|
1012
|
-
explode = operationDetailParam.explode;
|
|
1013
|
-
} else {
|
|
1014
|
-
if (style === "form") {
|
|
1015
|
-
explode = true;
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
// const explode = operationDetailParam?.explode || false;
|
|
1019
|
-
queryParams.append(param.name, value, {
|
|
1020
|
-
style: style,
|
|
1021
|
-
explode: explode,
|
|
1022
|
-
});
|
|
1023
|
-
break;
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
this.addParamsToContext(pathParams, "path", "request");
|
|
1028
|
-
|
|
1029
|
-
this.operation.headers = headersObj;
|
|
1030
|
-
this.operation.queryParams = queryParams;
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
buildHeaderParams() {}
|
|
1034
|
-
|
|
1035
|
-
/**
|
|
1036
|
-
* @private
|
|
1037
|
-
* @param {*} params
|
|
1038
|
-
* @param {*} paramType
|
|
1039
|
-
* @param {*} contextType
|
|
1040
|
-
*/
|
|
1041
|
-
addParamsToContext(params, paramType, contextType) {
|
|
1042
|
-
const parameters = {};
|
|
1043
|
-
for (const [key, value] of params.entries()) {
|
|
1044
|
-
Object.assign(parameters, { [key]: value });
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
this.expression.addToContext(contextType, { [paramType]: parameters });
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
/**
|
|
1051
|
-
* @private
|
|
1052
|
-
*/
|
|
1053
|
-
mapRequestBody() {
|
|
1054
|
-
if (this.step?.requestBody) {
|
|
1055
|
-
const payload = this.expression.resolveExpression(
|
|
1056
|
-
this.step.requestBody.payload,
|
|
1057
|
-
);
|
|
1058
|
-
|
|
1059
|
-
if (this.step.requestBody.contentType) {
|
|
1060
|
-
this.operation.headers.append(
|
|
1061
|
-
"accept",
|
|
1062
|
-
this.step.requestBody.contentType,
|
|
1063
|
-
);
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
this.operation.data = payload;
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
624
|
/**
|
|
1071
625
|
* @private
|
|
1072
626
|
*/
|
package/src/Operation.js
ADDED
|
@@ -0,0 +1,685 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const URLParams = require("openapi-params");
|
|
4
|
+
const client = require("openid-client");
|
|
5
|
+
|
|
6
|
+
const fs = require("node:fs");
|
|
7
|
+
const https = require("node:https");
|
|
8
|
+
const path = require("node:path");
|
|
9
|
+
|
|
10
|
+
class Operation {
|
|
11
|
+
constructor(operation, sourceDescriptionFile, expression, inputs, logger, step) {
|
|
12
|
+
this.sourceDescriptionFile = sourceDescriptionFile;
|
|
13
|
+
this.expression = expression;
|
|
14
|
+
this.inputs = inputs;
|
|
15
|
+
this.logger = logger;
|
|
16
|
+
this.step = step;
|
|
17
|
+
this.operation = operation;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async runOperation() {
|
|
21
|
+
this.buildOperation()
|
|
22
|
+
|
|
23
|
+
return await this.makeRequest()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async makeRequest() {
|
|
27
|
+
const sleep = function (ms) {
|
|
28
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const parseRetryAfter = function (retryAfter) {
|
|
32
|
+
if (!retryAfter || typeof retryAfter !== "string") {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const trimmed = retryAfter.trim();
|
|
37
|
+
|
|
38
|
+
// Try parsing as a number (seconds format)
|
|
39
|
+
const asNumber = parseInt(trimmed, 10);
|
|
40
|
+
if (!isNaN(asNumber) && asNumber >= 0 && String(asNumber) === trimmed) {
|
|
41
|
+
return asNumber;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Try parsing as HTTP date format
|
|
45
|
+
try {
|
|
46
|
+
const date = new Date(trimmed);
|
|
47
|
+
|
|
48
|
+
// Check if date is valid
|
|
49
|
+
if (isNaN(date.getTime())) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Calculate seconds from now until the date
|
|
54
|
+
const now = new Date();
|
|
55
|
+
const secondsUntil = Math.ceil((date.getTime() - now.getTime()) / 1000);
|
|
56
|
+
|
|
57
|
+
// Return the delay, but don't return negative values
|
|
58
|
+
return secondsUntil >= 0 ? secondsUntil : 0;
|
|
59
|
+
} catch (err) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
let url = this.operation.url;
|
|
65
|
+
|
|
66
|
+
if (this.operation.queryParams.size) {
|
|
67
|
+
url += `?${this.operation.queryParams}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const options = {
|
|
71
|
+
method: this.operation.method,
|
|
72
|
+
headers: this.operation.headers,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (this.operation.data) {
|
|
76
|
+
options.body = this.operation.data;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (this.retryAfter) {
|
|
80
|
+
this.logger.notice(
|
|
81
|
+
`retryAfter was set: waiting ${this.retryAfter * 1000} seconds`,
|
|
82
|
+
);
|
|
83
|
+
await sleep(this.retryAfter * 1000);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.expression.addToContext("url", url);
|
|
87
|
+
this.expression.addToContext("method", options.method);
|
|
88
|
+
|
|
89
|
+
this.logger.notice(`Making a ${options.method.toUpperCase()} to ${url}`);
|
|
90
|
+
|
|
91
|
+
let response;
|
|
92
|
+
if (this.operation?.mutualTLS) {
|
|
93
|
+
this.logger.verbose("Using mutualTLS call");
|
|
94
|
+
response = await this.mutualTLS();
|
|
95
|
+
} else {
|
|
96
|
+
this.logger.verbose("Using fetch call");
|
|
97
|
+
|
|
98
|
+
this.logger.verbose(`request url: ${url}`);
|
|
99
|
+
this.logger.verbose(`request method: ${options.method}`);
|
|
100
|
+
this.logger.verbose("request headers:");
|
|
101
|
+
for (const [key, value] of options.headers.entries()) {
|
|
102
|
+
this.logger.verbose(`${key}: ${value}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.logger.verbose(`body: ${options.body}`);
|
|
106
|
+
|
|
107
|
+
response = await fetch(url, options);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this.logger.verbose(`received StatusCode: ${response.status}`);
|
|
111
|
+
this.logger.verbose(`received headers:`);
|
|
112
|
+
for (const [key, value] of response.headers.entries()) {
|
|
113
|
+
this.logger.verbose(`${key}: ${value}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (response.headers.has("retry-after")) {
|
|
117
|
+
const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
|
|
118
|
+
if (retryAfter !== null) {
|
|
119
|
+
this.retryAfter = retryAfter;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.addParamsToContext(response.headers, "header", "response");
|
|
124
|
+
this.expression.addToContext("statusCode", response.status);
|
|
125
|
+
|
|
126
|
+
this.logger.notice(`${url} responded with a: ${response.status}`);
|
|
127
|
+
|
|
128
|
+
await this.dealWithResponse(response);
|
|
129
|
+
|
|
130
|
+
return response;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async mutualTLS() {
|
|
134
|
+
let clientKeyPath;
|
|
135
|
+
try {
|
|
136
|
+
clientKeyPath = path.resolve(this.inputs.key);
|
|
137
|
+
this.logger.verbose(`clientKey Path: ${clientKeyPath}`);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
this.logger.error(`could not resolve clientKey`);
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let clientCertPath;
|
|
144
|
+
try {
|
|
145
|
+
clientCertPath = path.resolve(this.inputs.cert);
|
|
146
|
+
this.logger.verbose(`clientCert Path: ${clientCertPath}`);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
this.logger.error(`could not resolve clientCert`);
|
|
149
|
+
throw err;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let url = this.operation.url;
|
|
153
|
+
|
|
154
|
+
if (this.operation.queryParams.size) {
|
|
155
|
+
url += `?${this.operation.queryParams}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const opUrl = new URL(url);
|
|
159
|
+
|
|
160
|
+
this.logger.verbose(`url: ${opUrl.pathname + opUrl.search}`);
|
|
161
|
+
this.logger.verbose(`method: ${this.operation.method}`);
|
|
162
|
+
this.logger.verbose("headers:");
|
|
163
|
+
const headersObj = {};
|
|
164
|
+
for (const [key, value] of this.operation.headers.entries()) {
|
|
165
|
+
this.logger.verbose(`${key}: ${value}`);
|
|
166
|
+
Object.assign(headersObj, { [key]: value });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.logger.verbose(`body: ${this.operation.data}`);
|
|
170
|
+
|
|
171
|
+
return new Promise((resolve, reject) => {
|
|
172
|
+
const options = {
|
|
173
|
+
key: fs.readFileSync(clientKeyPath),
|
|
174
|
+
cert: fs.readFileSync(clientCertPath),
|
|
175
|
+
method: this.operation.method,
|
|
176
|
+
headers: headersObj,
|
|
177
|
+
rejectUnauthorized: true,
|
|
178
|
+
hostname: opUrl.hostname,
|
|
179
|
+
path: opUrl.pathname + opUrl.search,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
if (this.operation.data) {
|
|
183
|
+
options.headers = options.headers || {};
|
|
184
|
+
if (!options.headers["content-length"]) {
|
|
185
|
+
const bodyBuffer = Buffer.from(
|
|
186
|
+
typeof this.operation.data === "string"
|
|
187
|
+
? this.operation.data
|
|
188
|
+
: JSON.stringify(this.operation.data),
|
|
189
|
+
);
|
|
190
|
+
options.headers["content-length"] = bodyBuffer.length;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const headers = new Headers();
|
|
195
|
+
const chunks = [];
|
|
196
|
+
|
|
197
|
+
const req = https.request(options, (res) => {
|
|
198
|
+
for (const [name, value] of Object.entries(res.headers)) {
|
|
199
|
+
headers.append(name, value);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Collect data chunks as buffers for proper encoding
|
|
203
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
204
|
+
|
|
205
|
+
res.on("end", () => {
|
|
206
|
+
// Concatenate buffers and decode based on content-type
|
|
207
|
+
const buffer = Buffer.concat(chunks);
|
|
208
|
+
let body;
|
|
209
|
+
|
|
210
|
+
const contentType = res.headers["content-type"] || "";
|
|
211
|
+
|
|
212
|
+
if (contentType.includes("application/json")) {
|
|
213
|
+
try {
|
|
214
|
+
body = JSON.parse(buffer.toString("utf8"));
|
|
215
|
+
} catch (err) {
|
|
216
|
+
body = buffer.toString("utf8");
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
body = buffer.toString("utf8");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const response = new Response(buffer, {
|
|
223
|
+
status: res.status,
|
|
224
|
+
statusText: res.statusMessage,
|
|
225
|
+
headers,
|
|
226
|
+
});
|
|
227
|
+
// resolve({
|
|
228
|
+
// headers: headers,
|
|
229
|
+
// body: body,
|
|
230
|
+
|
|
231
|
+
// status: res.statusCode,
|
|
232
|
+
// statusText: res.statusMessage,
|
|
233
|
+
// ok: res.statusCode >= 200 && res.statusCode < 300,
|
|
234
|
+
// });
|
|
235
|
+
resolve(response);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
req.on("error", (err) => {
|
|
240
|
+
this.logger.error(`mTLS Error: ${err.message}`);
|
|
241
|
+
reject(new Error(`mTLS request failed: ${err.message}`));
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Write request body if present
|
|
245
|
+
if (this.operation.data) {
|
|
246
|
+
const body =
|
|
247
|
+
typeof this.operation.data === "string"
|
|
248
|
+
? this.operation.data
|
|
249
|
+
: JSON.stringify(this.operation.data);
|
|
250
|
+
req.write(body);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
req.end();
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async dealWithResponse(response) {
|
|
258
|
+
if (
|
|
259
|
+
response.headers.has("Content-Type") &&
|
|
260
|
+
response.headers.get("Content-Type") === "application/json"
|
|
261
|
+
) {
|
|
262
|
+
const json = await response?.json().catch((err) => {
|
|
263
|
+
this.logger.error(
|
|
264
|
+
`Error trying to resolve ${this.step.stepId} outputs`,
|
|
265
|
+
);
|
|
266
|
+
throw new Error(err);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
this.expression.addToContext("response.body", json);
|
|
270
|
+
} else {
|
|
271
|
+
const body = await response.body;
|
|
272
|
+
|
|
273
|
+
this.expression.addToContext("response.body", body);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
buildOperation() {
|
|
278
|
+
if (Object.keys(this.sourceDescriptionFile.securitySchemes).length === 1) {
|
|
279
|
+
for (const [key, value] of Object.entries(
|
|
280
|
+
this.sourceDescriptionFile.securitySchemes,
|
|
281
|
+
)) {
|
|
282
|
+
if (value.type.toLowerCase() === "mutualtls")
|
|
283
|
+
this.operation.mutualTLS = true;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
this.mapInputs()
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
mapInputs() {
|
|
291
|
+
this.mapParameters();
|
|
292
|
+
this.encodeRequestBody();
|
|
293
|
+
|
|
294
|
+
this.addParamsToContext(this.operation.headers, "headers", "request");
|
|
295
|
+
this.addParamsToContext(this.operation.queryParams, "query", "request");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
mapParameters() {
|
|
299
|
+
const headersObj = new Headers();
|
|
300
|
+
const headers = new URLParams();
|
|
301
|
+
const queryParams = new URLParams();
|
|
302
|
+
const pathParams = new URLParams();
|
|
303
|
+
|
|
304
|
+
for (const param of this.step?.parameters || []) {
|
|
305
|
+
const operationDetailParam =
|
|
306
|
+
this.sourceDescriptionFile.operationDetails?.parameters
|
|
307
|
+
.filter((obj) => obj.name === param.name && obj.in === param.in)
|
|
308
|
+
.at(0);
|
|
309
|
+
|
|
310
|
+
let value = this.expression.resolveExpression(param.value);
|
|
311
|
+
|
|
312
|
+
switch (param.in) {
|
|
313
|
+
case "header":
|
|
314
|
+
let headerStyle = "simple";
|
|
315
|
+
let headerExplode = false;
|
|
316
|
+
|
|
317
|
+
if (
|
|
318
|
+
operationDetailParam?.style &&
|
|
319
|
+
["accept", "authorization", "content-type"].includes(
|
|
320
|
+
operationDetailParam.name.toLowerCase() === false,
|
|
321
|
+
)
|
|
322
|
+
) {
|
|
323
|
+
headerStyle = operationDetailParam.style;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (
|
|
327
|
+
operationDetailParam?.explode &&
|
|
328
|
+
["accept", "authorization", "content-type"].includes(
|
|
329
|
+
operationDetailParam.name.toLowerCase() === false,
|
|
330
|
+
)
|
|
331
|
+
) {
|
|
332
|
+
headerExplode = operationDetailParam.explode;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (this.operation.security) {
|
|
336
|
+
// console.log(this.operation.security);
|
|
337
|
+
for (const key in this.sourceDescriptionFile.securitySchemes) {
|
|
338
|
+
if (
|
|
339
|
+
this.sourceDescriptionFile.securitySchemes[key].type ===
|
|
340
|
+
"http" &&
|
|
341
|
+
param.name === key
|
|
342
|
+
) {
|
|
343
|
+
if (
|
|
344
|
+
this.sourceDescriptionFile.securitySchemes[
|
|
345
|
+
key
|
|
346
|
+
].scheme.toLowerCase() === "bearer"
|
|
347
|
+
) {
|
|
348
|
+
value = `Bearer ${value}`;
|
|
349
|
+
} else {
|
|
350
|
+
const basicPass = Buffer.from(
|
|
351
|
+
this.expression.resolveExpression(value),
|
|
352
|
+
).toString("base64");
|
|
353
|
+
value = `Basic ${basicPass}`;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// const authSchemaName = Object.keys(
|
|
359
|
+
// this.operation.security.at(0),
|
|
360
|
+
// ).at(0);
|
|
361
|
+
|
|
362
|
+
// const securityScheme =
|
|
363
|
+
// this.sourceDescriptionFile.securitySchemes[authSchemaName];
|
|
364
|
+
// console.log(securityScheme);
|
|
365
|
+
|
|
366
|
+
// if (
|
|
367
|
+
// securityScheme.type === "http" &&
|
|
368
|
+
// securityScheme?.scheme?.toLowerCase() === "bearer"
|
|
369
|
+
// ) {
|
|
370
|
+
// value = `Bearer ${value}`;
|
|
371
|
+
// } else if (
|
|
372
|
+
// securityScheme.type === "http" &&
|
|
373
|
+
// securityScheme?.scheme?.toLowerCase() === "basic"
|
|
374
|
+
// ) {
|
|
375
|
+
// const basicPass = Buffer.from(
|
|
376
|
+
// this.expression.resolveExpression(value),
|
|
377
|
+
// ).toString("base64");
|
|
378
|
+
// value = `Basic ${basicPass}`;
|
|
379
|
+
// }
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
headers.append(param.name, value, {
|
|
383
|
+
style: headerStyle,
|
|
384
|
+
explode: headerExplode,
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
for (const [header, value] of headers) {
|
|
388
|
+
if (header === param.name) {
|
|
389
|
+
headersObj.append(param.name, value);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
break;
|
|
394
|
+
|
|
395
|
+
case "path":
|
|
396
|
+
const pathStyle = operationDetailParam?.style || "simple";
|
|
397
|
+
const pathExplode = operationDetailParam?.explode || false;
|
|
398
|
+
|
|
399
|
+
pathParams.append(param.name, value, {
|
|
400
|
+
style: pathStyle,
|
|
401
|
+
explode: pathExplode,
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
for (const [name, value] of pathParams.entries()) {
|
|
406
|
+
this.operation.url = this.operation.url.replace(`{${name}}`, value);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
break;
|
|
411
|
+
|
|
412
|
+
case "query":
|
|
413
|
+
// queryParams.append(param.name, value);
|
|
414
|
+
const style = operationDetailParam?.style || "form";
|
|
415
|
+
let explode = false;
|
|
416
|
+
if (Object.hasOwn(operationDetailParam, "explode")) {
|
|
417
|
+
explode = operationDetailParam.explode;
|
|
418
|
+
} else {
|
|
419
|
+
if (style === "form") {
|
|
420
|
+
explode = true;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// const explode = operationDetailParam?.explode || false;
|
|
424
|
+
queryParams.append(param.name, value, {
|
|
425
|
+
style: style,
|
|
426
|
+
explode: explode,
|
|
427
|
+
});
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
this.addParamsToContext(pathParams, "path", "request");
|
|
433
|
+
|
|
434
|
+
this.operation.headers = headersObj;
|
|
435
|
+
this.operation.queryParams = queryParams;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
addParamsToContext(params, paramType, contextType) {
|
|
439
|
+
const parameters = {};
|
|
440
|
+
for (const [key, value] of params.entries()) {
|
|
441
|
+
Object.assign(parameters, { [key]: value });
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
this.expression.addToContext(contextType, { [paramType]: parameters });
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Encode request body based on Content-Type
|
|
449
|
+
* Returns { body } with properly encoded body and headers
|
|
450
|
+
*
|
|
451
|
+
* @param {*} data - The data to encode
|
|
452
|
+
* @param {string} contentType - The Content-Type header value
|
|
453
|
+
* @param {Headers} headers - Existing headers object (will be modified)
|
|
454
|
+
* @returns {{body: *, headers: Headers}} Encoded body and updated headers
|
|
455
|
+
*/
|
|
456
|
+
encodeRequestBody() {
|
|
457
|
+
if (this.step?.requestBody) {
|
|
458
|
+
const payload = this.expression.resolveExpression(
|
|
459
|
+
this.step.requestBody.payload,
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
if (payload === null || payload === undefined) {
|
|
463
|
+
return { body: null };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Normalize content type (remove charset, parameters)
|
|
467
|
+
// const normalizedType = contentType.split(';')[0].trim().toLowerCase();
|
|
468
|
+
let normalizedType
|
|
469
|
+
if (this.step.requestBody.contentType) {
|
|
470
|
+
normalizedType = this.step.requestBody.contentType.split(';')[0].trim().toLowerCase();
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
switch (normalizedType) {
|
|
474
|
+
case 'application/json':
|
|
475
|
+
this.encodeJSON(payload);
|
|
476
|
+
break;
|
|
477
|
+
|
|
478
|
+
case 'application/x-www-form-urlencoded':
|
|
479
|
+
this.encodeFormURLEncoded(payload);
|
|
480
|
+
break;
|
|
481
|
+
|
|
482
|
+
case 'multipart/form-data':
|
|
483
|
+
this.encodeMultipartFormData(payload);
|
|
484
|
+
break;
|
|
485
|
+
|
|
486
|
+
case 'text/plain':
|
|
487
|
+
this.encodeTextPlain(payload);
|
|
488
|
+
break;
|
|
489
|
+
|
|
490
|
+
case 'text/xml':
|
|
491
|
+
case 'application/xml':
|
|
492
|
+
this.encodeXML(payload, normalizedType);
|
|
493
|
+
break;
|
|
494
|
+
|
|
495
|
+
case 'text/html':
|
|
496
|
+
this.encodeHTML(payload);
|
|
497
|
+
break;
|
|
498
|
+
|
|
499
|
+
case 'application/octet-stream':
|
|
500
|
+
this.encodeBinary(payload);
|
|
501
|
+
break;
|
|
502
|
+
|
|
503
|
+
default:
|
|
504
|
+
// For unknown types, try JSON if it's an object, otherwise text
|
|
505
|
+
if (typeof payload === 'object' && !(payload instanceof Blob) && !(payload instanceof ArrayBuffer)) {
|
|
506
|
+
this.encodeJSON(payload);
|
|
507
|
+
}
|
|
508
|
+
this.encodeTextPlain(payload);
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Encode as JSON
|
|
516
|
+
*/
|
|
517
|
+
encodeJSON(data) {
|
|
518
|
+
this.operation.data = typeof data === 'string' ? data : JSON.stringify(data);
|
|
519
|
+
this.operation.headers.set('Content-Type', 'application/json');
|
|
520
|
+
this.operation.headers.set('Content-Length', String(Buffer.byteLength(this.operation.data, 'utf8')));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Encode as application/x-www-form-urlencoded
|
|
525
|
+
*/
|
|
526
|
+
encodeFormURLEncoded(data) {
|
|
527
|
+
let body;
|
|
528
|
+
|
|
529
|
+
if (typeof data === 'string') {
|
|
530
|
+
body = data;
|
|
531
|
+
} else if (data instanceof URLSearchParams) {
|
|
532
|
+
body = data.toString();
|
|
533
|
+
} else if (typeof data === 'object') {
|
|
534
|
+
const params = new URLSearchParams();
|
|
535
|
+
for (const [key, value] of Object.entries(data)) {
|
|
536
|
+
if (Array.isArray(value)) {
|
|
537
|
+
value.forEach(v => params.append(key, String(v)));
|
|
538
|
+
} else {
|
|
539
|
+
params.append(key, String(value));
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
body = params.toString();
|
|
543
|
+
} else {
|
|
544
|
+
body = String(data);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
this.operation.headers.set('Content-Type', 'application/x-www-form-urlencoded');
|
|
548
|
+
this.operation.headers.set('Content-Length', String(Buffer.byteLength(body, 'utf8')));
|
|
549
|
+
this.operation.data = body;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Encode as multipart/form-data
|
|
554
|
+
*/
|
|
555
|
+
encodeMultipartFormData(data) {
|
|
556
|
+
const formData = new FormData();
|
|
557
|
+
|
|
558
|
+
if (data instanceof FormData) {
|
|
559
|
+
this.operation.data = data; // Let fetch handle the boundary
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (typeof data === 'object') {
|
|
564
|
+
for (const [key, value] of Object.entries(data)) {
|
|
565
|
+
if (value instanceof Blob || value instanceof File) {
|
|
566
|
+
formData.append(key, value);
|
|
567
|
+
} else if (Array.isArray(value)) {
|
|
568
|
+
value.forEach(v => formData.append(key, v));
|
|
569
|
+
} else {
|
|
570
|
+
formData.append(key, String(value));
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Do NOT set Content-Type - let fetch set it with the boundary
|
|
576
|
+
// headers.set('Content-Type', 'multipart/form-data'); // WRONG!
|
|
577
|
+
this.operation.headers.delete('Content-Type'); // Let fetch handle it
|
|
578
|
+
|
|
579
|
+
this.operation.data = formData;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Encode as plain text
|
|
584
|
+
*/
|
|
585
|
+
encodeTextPlain(data) {
|
|
586
|
+
const body = typeof data === 'string' ? data : String(data);
|
|
587
|
+
this.operation.headers.set('Content-Type', 'text/plain; charset=utf-8');
|
|
588
|
+
this.operation.headers.set('Content-Length', String(Buffer.byteLength(body, 'utf8')));
|
|
589
|
+
this.operation.data = body;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Encode as XML
|
|
594
|
+
*/
|
|
595
|
+
encodeXML(data, contentType) {
|
|
596
|
+
let body;
|
|
597
|
+
|
|
598
|
+
if (typeof data === 'string') {
|
|
599
|
+
body = data;
|
|
600
|
+
} else if (typeof data === 'object') {
|
|
601
|
+
// Simple object to XML conversion
|
|
602
|
+
body = this.objectToXML(data);
|
|
603
|
+
} else {
|
|
604
|
+
body = String(data);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
this.operation.headers.set('Content-Type', `${contentType}; charset=utf-8`);
|
|
608
|
+
this.operation.headers.set('Content-Length', String(Buffer.byteLength(body, 'utf8')));
|
|
609
|
+
this.operation.data = body;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Encode as HTML
|
|
614
|
+
*/
|
|
615
|
+
encodeHTML(data) {
|
|
616
|
+
const body = typeof data === 'string' ? data : String(data);
|
|
617
|
+
this.operation.headers.set('Content-Type', 'text/html; charset=utf-8');
|
|
618
|
+
this.operation.headers.set('Content-Length', String(Buffer.byteLength(body, 'utf8')));
|
|
619
|
+
this.operation.data = body;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Encode as binary (octet-stream)
|
|
624
|
+
*/
|
|
625
|
+
encodeBinary(data) {
|
|
626
|
+
let body;
|
|
627
|
+
|
|
628
|
+
if (data instanceof Blob || data instanceof ArrayBuffer || Buffer.isBuffer(data)) {
|
|
629
|
+
body = data;
|
|
630
|
+
} else if (typeof data === 'string') {
|
|
631
|
+
body = Buffer.from(data);
|
|
632
|
+
} else {
|
|
633
|
+
body = Buffer.from(JSON.stringify(data));
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
this.operation.headers.set('Content-Type', 'application/octet-stream');
|
|
637
|
+
|
|
638
|
+
if (Buffer.isBuffer(body)) {
|
|
639
|
+
this.operation.headers.set('Content-Length', String(body.length));
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
this.operation.data = body;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Simple object to XML converter
|
|
647
|
+
*/
|
|
648
|
+
objectToXML(obj, rootName = 'root') {
|
|
649
|
+
let xml = '<?xml version="1.0" encoding="UTF-8"?>';
|
|
650
|
+
|
|
651
|
+
function buildXML(data, tagName) {
|
|
652
|
+
if (data === null || data === undefined) {
|
|
653
|
+
return `<${tagName}/>`;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (typeof data === 'object' && !Array.isArray(data)) {
|
|
657
|
+
let result = `<${tagName}>`;
|
|
658
|
+
for (const [key, value] of Object.entries(data)) {
|
|
659
|
+
result += buildXML(value, key);
|
|
660
|
+
}
|
|
661
|
+
result += `</${tagName}>`;
|
|
662
|
+
return result;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (Array.isArray(data)) {
|
|
666
|
+
return data.map(item => buildXML(item, tagName)).join('');
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Escape XML special characters
|
|
670
|
+
const escaped = String(data)
|
|
671
|
+
.replace(/&/g, '&')
|
|
672
|
+
.replace(/</g, '<')
|
|
673
|
+
.replace(/>/g, '>')
|
|
674
|
+
.replace(/"/g, '"')
|
|
675
|
+
.replace(/'/g, ''');
|
|
676
|
+
|
|
677
|
+
return `<${tagName}>${escaped}</${tagName}>`;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
xml += buildXML(obj, rootName);
|
|
681
|
+
return xml;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
module.exports = Operation
|