arazzo-runner 0.0.8 → 0.0.11
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 +109 -6
- package/cli.js +5 -1
- package/package.json +2 -1
- package/src/Arazzo.js +228 -6
- package/src/DocFactory.js +18 -1
- package/src/Expression.js +81 -4
- package/src/Logger.js +3 -2
- package/src/OpenAPI.js +40 -15
- package/src/Runner.js +3 -2
- package/src/runtime.js +127 -0
package/README.md
CHANGED
|
@@ -34,6 +34,9 @@ arazzo-runner -a arazzo.json -i input.json
|
|
|
34
34
|
# With URL
|
|
35
35
|
arazzo-runner -a https://example.com/arazzo.json -i ./input.json
|
|
36
36
|
|
|
37
|
+
# With Verbose Logging
|
|
38
|
+
arazzo-runner -a https://example.com/arazzo.json -i ./input.json --verbose
|
|
39
|
+
|
|
37
40
|
# Show help
|
|
38
41
|
arazzo-runner --help
|
|
39
42
|
|
|
@@ -65,7 +68,7 @@ Obviously, if you have a lot of secret variables that need adding as inputs, the
|
|
|
65
68
|
|
|
66
69
|
## OpenAPI Servers
|
|
67
70
|
|
|
68
|
-
OpenAPI Documents allow you to specify servers at the root, [path](https://spec.openapis.org/oas/latest.html#path-item-object) and [operation](https://spec.openapis.org/oas/latest.html#operation-object) level. They allow you to specify multiple servers, however the OpenAPI specification is opinionated that all servers specified in a Document should return the same thing.
|
|
71
|
+
OpenAPI Documents allow you to specify servers at the root, [path](https://spec.openapis.org/oas/latest.html#path-item-object) and [operation](https://spec.openapis.org/oas/latest.html#operation-object) level. They allow you to specify multiple servers, however the OpenAPI specification is opinionated that all servers specified in a Document should return the same thing and this Arazzo Runner will follow this opinion and only attempt one of the specified servers.
|
|
69
72
|
|
|
70
73
|
This Arazzo Runner will pick the first server it comes across in the array of servers and run the operation against that.
|
|
71
74
|
|
|
@@ -77,10 +80,114 @@ It will attempt to map to the [Server Variables](https://spec.openapis.org/oas/l
|
|
|
77
80
|
|
|
78
81
|
## OpenAPI Parameters
|
|
79
82
|
|
|
80
|
-
OpenAPI Documents allow you to specify [`header`, `path` and `query` parameters](https://spec.openapis.org/oas/latest.html#parameter-object) in myriad of styles. This Arazzo Runner will respect your styling and send the format to the server as specified by your OpenAPI
|
|
83
|
+
OpenAPI Documents allow you to specify [`header`, `path` and `query` parameters](https://spec.openapis.org/oas/latest.html#parameter-object) in myriad of styles. This Arazzo Runner will respect your styling (unless you specify stylings for `Accept`, `Authorization` or `Content-Type` headers, then it will ignore the stylings, as per the OpenAPI specification) and send the format to the server as specified by your OpenAPI Document.
|
|
81
84
|
|
|
82
85
|
It currently does not follow the `allowEmptyValue`, `allowReserved` or the `content` keywords currently.
|
|
83
86
|
|
|
87
|
+
## OpenAPI Security
|
|
88
|
+
|
|
89
|
+
OpenAPI Document security is supported. There are a couple of ways that you will have to document your Arazzo Workflow for certain documentation types.
|
|
90
|
+
|
|
91
|
+
### Basic
|
|
92
|
+
|
|
93
|
+
For HTTP Basic authentication, you should document your Arazzo like:
|
|
94
|
+
|
|
95
|
+
**arazzo.json**
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
"steps": [
|
|
99
|
+
{
|
|
100
|
+
"stepId": "deleteUser",
|
|
101
|
+
"operationId": "deleteUser",
|
|
102
|
+
"parameters": [
|
|
103
|
+
{
|
|
104
|
+
"name": "Authorization",
|
|
105
|
+
"in": "header",
|
|
106
|
+
"value": "{$inputs.username}:{$inputs.password}"
|
|
107
|
+
},
|
|
108
|
+
{ "name": "username", "in": "path", "value": "$inputs.username" }
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The Runner will correctly encode and prepend `Basic` to the Authorization Header.
|
|
115
|
+
|
|
116
|
+
### Bearer
|
|
117
|
+
|
|
118
|
+
For HTTP Bearer authentication, you should document your Arazzo like:
|
|
119
|
+
|
|
120
|
+
**arazzo.json**
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"stepId": "LoginExistingUser",
|
|
125
|
+
"operationId": "loginUser",
|
|
126
|
+
"requestBody": {
|
|
127
|
+
"contentType": "application/json",
|
|
128
|
+
"payload": {
|
|
129
|
+
"username": "$inputs.username",
|
|
130
|
+
"password": "$inputs.password"
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"outputs": { "AccessToken": "$response.body#/AccessToken" }
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"stepId": "deleteUser",
|
|
137
|
+
"operationId": "deleteUser",
|
|
138
|
+
"parameters": [
|
|
139
|
+
{
|
|
140
|
+
"name": "Authorization",
|
|
141
|
+
"in": "header",
|
|
142
|
+
"value": "$steps.LoginExistingUser.outputs.AccessToken"
|
|
143
|
+
},
|
|
144
|
+
{ "name": "username", "in": "path", "value": "$inputs.username" }
|
|
145
|
+
]
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The Runner will prepend `Bearer` for you.
|
|
150
|
+
|
|
151
|
+
### mutualTLS
|
|
152
|
+
|
|
153
|
+
> mutualTLS is quite a complex authorization topic. I have written a naive way of dealing with it that I am unsure will actually work in production. If you are using mutualTLS and this Arazzo Runner and find that you run into bugs/issues, please do feel free to opena. report. The more I know and understand mutualTLS the better I can support it.
|
|
154
|
+
|
|
155
|
+
You will need to provide inputs for your ClientKey and ClientCert as their path locations:
|
|
156
|
+
|
|
157
|
+
**input.json**
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"deleteCurrentUser-mutualTLS": {
|
|
162
|
+
"username": "jack",
|
|
163
|
+
"key": "./client-key.pem",
|
|
164
|
+
"cert": "./client-cert.pem"
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`key` and `cert` are reserved names when used in an OpenAPI Document with `mutualTLS` as the authentication method. The Runner will error out if they are not found.
|
|
170
|
+
|
|
171
|
+
### UNSUPPORTED oauth/openId
|
|
172
|
+
|
|
173
|
+
**CURRENTLY UNSUPPORTED**
|
|
174
|
+
|
|
175
|
+
You will need to provide inputs for your clientId and clientSecret:
|
|
176
|
+
|
|
177
|
+
**input.json**
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"deleteCurrentUser-mutualTLS": {
|
|
182
|
+
"username": "jack",
|
|
183
|
+
"clientId": "abc123",
|
|
184
|
+
"clientSecret": "123abc"
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`clientId` and `clientSecret` are reserved name and will be used when oauth or openId authentication is set.
|
|
190
|
+
|
|
84
191
|
## Logging And Reporting
|
|
85
192
|
|
|
86
193
|
### Logging
|
|
@@ -111,10 +218,6 @@ Work on Reporting still needs completeing.
|
|
|
111
218
|
|
|
112
219
|
## Still unsupported
|
|
113
220
|
|
|
114
|
-
### Security
|
|
115
|
-
|
|
116
|
-
OpenAPI security is still not fully supported
|
|
117
|
-
|
|
118
221
|
### PathOperation
|
|
119
222
|
|
|
120
223
|
Accessing an OpenAPI operation by Operation Path `'{$sourceDescriptions.petstoreDescription.url}#/paths/~1pet~1findByStatus/get'` does not work currently
|
package/cli.js
CHANGED
|
@@ -17,6 +17,9 @@ const options = {
|
|
|
17
17
|
type: "string",
|
|
18
18
|
short: "i",
|
|
19
19
|
},
|
|
20
|
+
verbose: {
|
|
21
|
+
type: "boolean",
|
|
22
|
+
},
|
|
20
23
|
help: {
|
|
21
24
|
type: "boolean",
|
|
22
25
|
short: "h",
|
|
@@ -40,6 +43,7 @@ OPTIONS:
|
|
|
40
43
|
-i, --input <file> Path to input JSON file (required)
|
|
41
44
|
-h, --help Show this help message
|
|
42
45
|
-v, --version Show version number
|
|
46
|
+
-vv, --verbose Verbose Logging
|
|
43
47
|
|
|
44
48
|
EXAMPLES:
|
|
45
49
|
# Run with local files
|
|
@@ -139,7 +143,7 @@ async function main() {
|
|
|
139
143
|
console.log("");
|
|
140
144
|
|
|
141
145
|
try {
|
|
142
|
-
const runner = new Runner();
|
|
146
|
+
const runner = new Runner({ verbose: values?.verbose || false });
|
|
143
147
|
await runner.runArazzo(arazzoPath, inputPath);
|
|
144
148
|
|
|
145
149
|
console.log("");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arazzo-runner",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"description": "A runner to run through Arazzo Document workflows",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"jsonpath": "^1.1.1",
|
|
44
44
|
"openapi-params": "^0.0.5",
|
|
45
45
|
"openapi-server-url-templating": "^1.3.0",
|
|
46
|
+
"openid-client": "^6.8.1",
|
|
46
47
|
"stream-chain": "^3.4.0",
|
|
47
48
|
"stream-json": "^1.9.1"
|
|
48
49
|
}
|
package/src/Arazzo.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const URLParams = require("openapi-params");
|
|
4
|
+
const client = require("openid-client");
|
|
4
5
|
|
|
6
|
+
const fs = require("node:fs");
|
|
7
|
+
const https = require("node:https");
|
|
5
8
|
const path = require("node:path");
|
|
6
9
|
|
|
7
10
|
const Document = require("./Document");
|
|
8
|
-
// const docFactory = require("./DocFactory");
|
|
9
11
|
const Expression = require("./Expression");
|
|
10
12
|
const Rules = require("./Rules");
|
|
11
13
|
|
|
@@ -252,6 +254,17 @@ class Arazzo extends Document {
|
|
|
252
254
|
this.step,
|
|
253
255
|
);
|
|
254
256
|
|
|
257
|
+
// await this.sourceDescriptionFile.getSecurity();
|
|
258
|
+
|
|
259
|
+
if (Object.keys(this.sourceDescriptionFile.securitySchemes).length === 1) {
|
|
260
|
+
for (const [key, value] of Object.entries(
|
|
261
|
+
this.sourceDescriptionFile.securitySchemes,
|
|
262
|
+
)) {
|
|
263
|
+
if (value.type.toLowerCase() === "mutualtls")
|
|
264
|
+
this.operation.mutualTLS = true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
255
268
|
this.mapInputs();
|
|
256
269
|
|
|
257
270
|
await this.runOperation();
|
|
@@ -328,7 +341,24 @@ class Arazzo extends Document {
|
|
|
328
341
|
|
|
329
342
|
this.logger.notice(`Making a ${options.method.toUpperCase()} to ${url}`);
|
|
330
343
|
|
|
331
|
-
|
|
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(`url: ${url}`);
|
|
352
|
+
this.logger.verbose(`method: ${options.method}`);
|
|
353
|
+
this.logger.verbose("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
|
+
}
|
|
332
362
|
|
|
333
363
|
if (response.headers.has("retry-after")) {
|
|
334
364
|
const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
|
|
@@ -345,6 +375,130 @@ class Arazzo extends Document {
|
|
|
345
375
|
await this.dealWithResponse(response);
|
|
346
376
|
}
|
|
347
377
|
|
|
378
|
+
async mutualTLS() {
|
|
379
|
+
let clientKeyPath;
|
|
380
|
+
try {
|
|
381
|
+
clientKeyPath = path.resolve(this.inputs.key);
|
|
382
|
+
this.logger.verbose(`clientKey Path: ${clientKeyPath}`);
|
|
383
|
+
} catch (err) {
|
|
384
|
+
this.logger.error(`could not resolve clientKey`);
|
|
385
|
+
throw err;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
let clientCertPath;
|
|
389
|
+
try {
|
|
390
|
+
clientCertPath = path.resolve(this.inputs.cert);
|
|
391
|
+
this.logger.verbose(`clientCert Path: ${clientCertPath}`);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
this.logger.error(`could not resolve clientCert`);
|
|
394
|
+
throw err;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
let url = this.operation.url;
|
|
398
|
+
|
|
399
|
+
if (this.operation.queryParams.size) {
|
|
400
|
+
url += `?${this.operation.queryParams}`;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const opUrl = new URL(url);
|
|
404
|
+
|
|
405
|
+
this.logger.verbose(`url: ${opUrl.pathname + opUrl.search}`);
|
|
406
|
+
this.logger.verbose(`method: ${this.operation.method}`);
|
|
407
|
+
this.logger.verbose("headers:");
|
|
408
|
+
const headersObj = {};
|
|
409
|
+
for (const [key, value] of this.operation.headers.entries()) {
|
|
410
|
+
this.logger.verbose(`${key}: ${value}`);
|
|
411
|
+
Object.assign(headersObj, { [key]: value });
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
this.logger.verbose(`body: ${this.operation.data}`);
|
|
415
|
+
|
|
416
|
+
return new Promise((resolve, reject) => {
|
|
417
|
+
const options = {
|
|
418
|
+
key: fs.readFileSync(clientKeyPath),
|
|
419
|
+
cert: fs.readFileSync(clientCertPath),
|
|
420
|
+
method: this.operation.method,
|
|
421
|
+
headers: headersObj,
|
|
422
|
+
rejectUnauthorized: true,
|
|
423
|
+
hostname: opUrl.hostname,
|
|
424
|
+
path: opUrl.pathname + opUrl.search,
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
if (this.operation.data) {
|
|
428
|
+
options.headers = options.headers || {};
|
|
429
|
+
if (!options.headers["content-length"]) {
|
|
430
|
+
const bodyBuffer = Buffer.from(
|
|
431
|
+
typeof this.operation.data === "string"
|
|
432
|
+
? this.operation.data
|
|
433
|
+
: JSON.stringify(this.operation.data),
|
|
434
|
+
);
|
|
435
|
+
options.headers["content-length"] = bodyBuffer.length;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const headers = new Headers();
|
|
440
|
+
const chunks = [];
|
|
441
|
+
|
|
442
|
+
const req = https.request(options, (res) => {
|
|
443
|
+
for (const [name, value] of Object.entries(res.headers)) {
|
|
444
|
+
headers.append(name, value);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Collect data chunks as buffers for proper encoding
|
|
448
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
449
|
+
|
|
450
|
+
res.on("end", () => {
|
|
451
|
+
// Concatenate buffers and decode based on content-type
|
|
452
|
+
const buffer = Buffer.concat(chunks);
|
|
453
|
+
let body;
|
|
454
|
+
|
|
455
|
+
const contentType = res.headers["content-type"] || "";
|
|
456
|
+
|
|
457
|
+
if (contentType.includes("application/json")) {
|
|
458
|
+
try {
|
|
459
|
+
body = JSON.parse(buffer.toString("utf8"));
|
|
460
|
+
} catch (err) {
|
|
461
|
+
body = buffer.toString("utf8");
|
|
462
|
+
}
|
|
463
|
+
} else {
|
|
464
|
+
body = buffer.toString("utf8");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const response = new Response(buffer, {
|
|
468
|
+
status: res.status,
|
|
469
|
+
statusText: res.statusMessage,
|
|
470
|
+
headers,
|
|
471
|
+
});
|
|
472
|
+
// resolve({
|
|
473
|
+
// headers: headers,
|
|
474
|
+
// body: body,
|
|
475
|
+
|
|
476
|
+
// status: res.statusCode,
|
|
477
|
+
// statusText: res.statusMessage,
|
|
478
|
+
// ok: res.statusCode >= 200 && res.statusCode < 300,
|
|
479
|
+
// });
|
|
480
|
+
resolve(response);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
req.on("error", (err) => {
|
|
485
|
+
this.logger.error(`mTLS Error: ${err.message}`);
|
|
486
|
+
reject(new Error(`mTLS request failed: ${err.message}`));
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// Write request body if present
|
|
490
|
+
if (this.operation.data) {
|
|
491
|
+
const body =
|
|
492
|
+
typeof this.operation.data === "string"
|
|
493
|
+
? this.operation.data
|
|
494
|
+
: JSON.stringify(this.operation.data);
|
|
495
|
+
req.write(body);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
req.end();
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
348
502
|
/**
|
|
349
503
|
* @private
|
|
350
504
|
* @param {*} response
|
|
@@ -691,7 +845,6 @@ class Arazzo extends Document {
|
|
|
691
845
|
*/
|
|
692
846
|
async dealWithStepOutputs(response) {
|
|
693
847
|
const json = await response?.json().catch((err) => {
|
|
694
|
-
console.error(err);
|
|
695
848
|
this.logger.error(`Error trying to resolve ${this.step.stepId} outputs`);
|
|
696
849
|
throw new Error(err);
|
|
697
850
|
});
|
|
@@ -736,16 +889,83 @@ class Arazzo extends Document {
|
|
|
736
889
|
.filter((obj) => obj.name === param.name && obj.in === param.in)
|
|
737
890
|
.at(0);
|
|
738
891
|
|
|
739
|
-
|
|
892
|
+
let value = this.expression.resolveExpression(param.value);
|
|
740
893
|
|
|
741
894
|
switch (param.in) {
|
|
742
895
|
case "header":
|
|
743
|
-
|
|
744
|
-
|
|
896
|
+
let headerStyle = "simple";
|
|
897
|
+
let headerExplode = false;
|
|
898
|
+
|
|
899
|
+
if (
|
|
900
|
+
operationDetailParam?.style &&
|
|
901
|
+
["accept", "authorization", "content-type"].includes(
|
|
902
|
+
operationDetailParam.name.toLowerCase() === false,
|
|
903
|
+
)
|
|
904
|
+
) {
|
|
905
|
+
headerStyle = operationDetailParam.style;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (
|
|
909
|
+
operationDetailParam?.explode &&
|
|
910
|
+
["accept", "authorization", "content-type"].includes(
|
|
911
|
+
operationDetailParam.name.toLowerCase() === false,
|
|
912
|
+
)
|
|
913
|
+
) {
|
|
914
|
+
headerExplode = operationDetailParam.explode;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (this.operation.security) {
|
|
918
|
+
// console.log(this.operation.security);
|
|
919
|
+
for (const key in this.sourceDescriptionFile.securitySchemes) {
|
|
920
|
+
if (
|
|
921
|
+
this.sourceDescriptionFile.securitySchemes[key].type ===
|
|
922
|
+
"http" &&
|
|
923
|
+
param.name === key
|
|
924
|
+
) {
|
|
925
|
+
if (
|
|
926
|
+
this.sourceDescriptionFile.securitySchemes[
|
|
927
|
+
key
|
|
928
|
+
].scheme.toLowerCase() === "bearer"
|
|
929
|
+
) {
|
|
930
|
+
value = `Bearer ${value}`;
|
|
931
|
+
} else {
|
|
932
|
+
const basicPass = Buffer.from(
|
|
933
|
+
this.expression.resolveExpression(value),
|
|
934
|
+
).toString("base64");
|
|
935
|
+
value = `Basic ${basicPass}`;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// const authSchemaName = Object.keys(
|
|
941
|
+
// this.operation.security.at(0),
|
|
942
|
+
// ).at(0);
|
|
943
|
+
|
|
944
|
+
// const securityScheme =
|
|
945
|
+
// this.sourceDescriptionFile.securitySchemes[authSchemaName];
|
|
946
|
+
// console.log(securityScheme);
|
|
947
|
+
|
|
948
|
+
// if (
|
|
949
|
+
// securityScheme.type === "http" &&
|
|
950
|
+
// securityScheme?.scheme?.toLowerCase() === "bearer"
|
|
951
|
+
// ) {
|
|
952
|
+
// value = `Bearer ${value}`;
|
|
953
|
+
// } else if (
|
|
954
|
+
// securityScheme.type === "http" &&
|
|
955
|
+
// securityScheme?.scheme?.toLowerCase() === "basic"
|
|
956
|
+
// ) {
|
|
957
|
+
// const basicPass = Buffer.from(
|
|
958
|
+
// this.expression.resolveExpression(value),
|
|
959
|
+
// ).toString("base64");
|
|
960
|
+
// value = `Basic ${basicPass}`;
|
|
961
|
+
// }
|
|
962
|
+
}
|
|
963
|
+
|
|
745
964
|
headers.append(param.name, value, {
|
|
746
965
|
style: headerStyle,
|
|
747
966
|
explode: headerExplode,
|
|
748
967
|
});
|
|
968
|
+
|
|
749
969
|
for (const [header, value] of headers) {
|
|
750
970
|
if (header === param.name) {
|
|
751
971
|
headersObj.append(param.name, value);
|
|
@@ -795,6 +1015,8 @@ class Arazzo extends Document {
|
|
|
795
1015
|
this.operation.queryParams = queryParams;
|
|
796
1016
|
}
|
|
797
1017
|
|
|
1018
|
+
buildHeaderParams() {}
|
|
1019
|
+
|
|
798
1020
|
/**
|
|
799
1021
|
* @private
|
|
800
1022
|
* @param {*} params
|
package/src/DocFactory.js
CHANGED
|
@@ -6,6 +6,21 @@ const OpenAPI = require("./OpenAPI");
|
|
|
6
6
|
class DocumentFactory {
|
|
7
7
|
constructor() {}
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Tests whether a string is a URL or not
|
|
11
|
+
* @private
|
|
12
|
+
* @param {string} str
|
|
13
|
+
* @returns
|
|
14
|
+
*/
|
|
15
|
+
isUrl(str) {
|
|
16
|
+
try {
|
|
17
|
+
new URL(str);
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
9
24
|
/**
|
|
10
25
|
* Document Factory to create Arazzo or OpenAPI documents
|
|
11
26
|
* @function buildDocument
|
|
@@ -23,7 +38,9 @@ class DocumentFactory {
|
|
|
23
38
|
document = new Arazzo(path, name, options, this);
|
|
24
39
|
}
|
|
25
40
|
|
|
26
|
-
|
|
41
|
+
if (this.isUrl(path)) {
|
|
42
|
+
await document.loadDocument();
|
|
43
|
+
} else document.setMainArazzo();
|
|
27
44
|
|
|
28
45
|
return document;
|
|
29
46
|
}
|
package/src/Expression.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const {
|
|
4
|
+
extractAll,
|
|
5
|
+
extractAndResolve,
|
|
6
|
+
hasRuntimeExpressions,
|
|
7
|
+
} = require("./runtime");
|
|
8
|
+
|
|
3
9
|
const {
|
|
4
10
|
parse,
|
|
5
11
|
test,
|
|
@@ -193,6 +199,49 @@ class Expression {
|
|
|
193
199
|
}
|
|
194
200
|
}
|
|
195
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Resolves a runtime expression to its value in the context
|
|
204
|
+
* @public
|
|
205
|
+
* @param {string|Object|Array} expression - The runtime expression to resolve
|
|
206
|
+
* @returns {*} The resolved value
|
|
207
|
+
* @throws {Error} If expression is invalid or context path doesn't exist
|
|
208
|
+
*/
|
|
209
|
+
// resolveExpression(expression) {
|
|
210
|
+
// // Handle arrays recursively
|
|
211
|
+
// if (Array.isArray(expression)) {
|
|
212
|
+
// return expression.map((item) => this.resolveExpression(item));
|
|
213
|
+
// }
|
|
214
|
+
|
|
215
|
+
// // Handle objects recursively
|
|
216
|
+
// if (typeof expression === "object" && expression !== null) {
|
|
217
|
+
// const resolved = {};
|
|
218
|
+
// for (const [key, value] of Object.entries(expression)) {
|
|
219
|
+
// resolved[key] = this.resolveExpression(value);
|
|
220
|
+
// }
|
|
221
|
+
// return resolved;
|
|
222
|
+
// }
|
|
223
|
+
|
|
224
|
+
// // Handle strings (runtime expressions)
|
|
225
|
+
// if (typeof expression !== "string") {
|
|
226
|
+
// return expression;
|
|
227
|
+
// }
|
|
228
|
+
|
|
229
|
+
// this.expression = expression;
|
|
230
|
+
|
|
231
|
+
// if (this.isARunTimeExpression()) {
|
|
232
|
+
// return this.mapToContext();
|
|
233
|
+
// }
|
|
234
|
+
|
|
235
|
+
// const extractedExpression = extract(expression);
|
|
236
|
+
|
|
237
|
+
// if (extractedExpression !== expression && test(extractedExpression)) {
|
|
238
|
+
// this.expression = extractedExpression;
|
|
239
|
+
// return this.mapToContext();
|
|
240
|
+
// }
|
|
241
|
+
|
|
242
|
+
// return expression;
|
|
243
|
+
// }
|
|
244
|
+
|
|
196
245
|
/**
|
|
197
246
|
* Resolves a runtime expression to its value in the context
|
|
198
247
|
* @public
|
|
@@ -222,14 +271,42 @@ class Expression {
|
|
|
222
271
|
|
|
223
272
|
this.expression = expression;
|
|
224
273
|
|
|
274
|
+
// Check if it's a runtime expression
|
|
225
275
|
if (this.isARunTimeExpression()) {
|
|
226
276
|
return this.mapToContext();
|
|
227
277
|
}
|
|
228
278
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
279
|
+
// NEW: Handle multiple templated expressions
|
|
280
|
+
const runtimeExprRegex = /{(\$[^}]+)}/g;
|
|
281
|
+
const matches = [...expression.matchAll(runtimeExprRegex)];
|
|
282
|
+
|
|
283
|
+
if (matches.length > 0) {
|
|
284
|
+
// If entire string is single expression, return resolved value
|
|
285
|
+
if (matches.length === 1 && matches[0][0] === expression) {
|
|
286
|
+
const extractedExpression = matches[0][1];
|
|
287
|
+
if (test(extractedExpression)) {
|
|
288
|
+
this.expression = extractedExpression;
|
|
289
|
+
return this.mapToContext();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Multiple expressions - substitute each one
|
|
294
|
+
let result = expression;
|
|
295
|
+
for (const match of matches) {
|
|
296
|
+
const fullMatch = match[0];
|
|
297
|
+
const expr = match[1];
|
|
298
|
+
|
|
299
|
+
if (test(expr)) {
|
|
300
|
+
this.expression = expr;
|
|
301
|
+
const resolvedValue = this.mapToContext();
|
|
302
|
+
const stringValue =
|
|
303
|
+
typeof resolvedValue === "object"
|
|
304
|
+
? JSON.stringify(resolvedValue)
|
|
305
|
+
: String(resolvedValue);
|
|
306
|
+
result = result.replace(fullMatch, stringValue);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return result;
|
|
233
310
|
}
|
|
234
311
|
|
|
235
312
|
return expression;
|
package/src/Logger.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
class Logger {
|
|
4
|
-
constructor() {
|
|
4
|
+
constructor(verboseLogging) {
|
|
5
|
+
this.verboseLogging = verboseLogging || false;
|
|
5
6
|
this.logOutput = {
|
|
6
7
|
debug: (message) => console.debug(message),
|
|
7
8
|
error: (message) => console.error(`❌ ${message}`),
|
|
@@ -50,7 +51,7 @@ class Logger {
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
verbose(str) {
|
|
53
|
-
this.log(str, this.logTypes.VERBOSE);
|
|
54
|
+
if (this.verboseLogging) this.log(str, this.logTypes.VERBOSE);
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
warning(str) {
|
package/src/OpenAPI.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const { evaluate } = require("@swaggerexpert/json-pointer");
|
|
3
4
|
const { substitute, parse } = require("openapi-server-url-templating");
|
|
4
5
|
|
|
5
6
|
const Document = require("./Document");
|
|
@@ -43,7 +44,7 @@ class OpenAPI extends Document {
|
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
this.
|
|
47
|
+
await this.buildOperationDetails();
|
|
47
48
|
|
|
48
49
|
return this.operation;
|
|
49
50
|
}
|
|
@@ -56,24 +57,26 @@ class OpenAPI extends Document {
|
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
async
|
|
60
|
+
async getSecuritySchemes() {
|
|
61
|
+
const componentPipeline = await this.JSONPicker(
|
|
62
|
+
"components",
|
|
63
|
+
this.filePath,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
for await (const { value } of componentPipeline) {
|
|
67
|
+
if (value.securitySchemes) {
|
|
68
|
+
console.log(value.securitySchemes);
|
|
69
|
+
this.securitySchemes = value.securitySchemes;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async getGlobalSecurity() {
|
|
60
75
|
const pipeline = await this.JSONPicker("security", this.filePath);
|
|
61
76
|
|
|
62
77
|
for await (const { value } of pipeline) {
|
|
63
78
|
if (value) this.security = value;
|
|
64
79
|
}
|
|
65
|
-
|
|
66
|
-
if (this.security) {
|
|
67
|
-
const componentPipeline = await this.JSONPicker(
|
|
68
|
-
"components",
|
|
69
|
-
this.filePath,
|
|
70
|
-
);
|
|
71
|
-
for await (const { value } of componentPipeline) {
|
|
72
|
-
if (value.securitySchemes) {
|
|
73
|
-
console.log(value.securitySchemes);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
parseServer() {
|
|
@@ -103,7 +106,27 @@ class OpenAPI extends Document {
|
|
|
103
106
|
return server;
|
|
104
107
|
}
|
|
105
108
|
|
|
106
|
-
|
|
109
|
+
async buildOperationSecurity() {
|
|
110
|
+
if (!this.securitySchemes) {
|
|
111
|
+
await this.getSecuritySchemes();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!this.security) {
|
|
115
|
+
await this.getGlobalSecurity();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (this.security) {
|
|
119
|
+
this.globalSecurity = true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (this.operationDetails?.security) {
|
|
123
|
+
this.operation.security = [this.operationDetails.security.at(0)];
|
|
124
|
+
} else {
|
|
125
|
+
if (this.security) this.operation.security = [this.security.at(0)];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async buildOperationDetails() {
|
|
107
130
|
this.operation = {};
|
|
108
131
|
|
|
109
132
|
const server = this.parseServer();
|
|
@@ -112,6 +135,8 @@ class OpenAPI extends Document {
|
|
|
112
135
|
url: `${server.url}${this.path}`,
|
|
113
136
|
method: this.method,
|
|
114
137
|
});
|
|
138
|
+
|
|
139
|
+
await this.buildOperationSecurity();
|
|
115
140
|
}
|
|
116
141
|
}
|
|
117
142
|
|
package/src/Runner.js
CHANGED
|
@@ -6,8 +6,9 @@ const Input = require("./Input");
|
|
|
6
6
|
const Logger = require("./Logger");
|
|
7
7
|
|
|
8
8
|
class Runner {
|
|
9
|
-
constructor(logger) {
|
|
10
|
-
|
|
9
|
+
constructor(options, logger) {
|
|
10
|
+
const verboseLogging = options?.verbose || false;
|
|
11
|
+
this.logger = logger || new Logger(verboseLogging);
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
/**
|
package/src/runtime.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract and resolve multiple runtime expressions from a template string
|
|
3
|
+
*
|
|
4
|
+
* @param {string} template - Template string with runtime expressions like "{$inputs.user}"
|
|
5
|
+
* @param {Function} resolver - Function to resolve each runtime expression
|
|
6
|
+
* @returns {string} The template with all expressions resolved
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const result = extractAndResolve(
|
|
10
|
+
* "{$inputs.user} has a pet called {$inputs.petName}",
|
|
11
|
+
* (expr) => resolveExpression(expr)
|
|
12
|
+
* );
|
|
13
|
+
* // "Jack has a pet called Jill"
|
|
14
|
+
*/
|
|
15
|
+
function extractAndResolve(template, resolver) {
|
|
16
|
+
if (typeof template !== "string") {
|
|
17
|
+
return template;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Match all runtime expressions wrapped in {}
|
|
21
|
+
const runtimeExpressionRegex = /{(\$[^}]+)}/g;
|
|
22
|
+
|
|
23
|
+
let result = template;
|
|
24
|
+
const matches = [...template.matchAll(runtimeExpressionRegex)];
|
|
25
|
+
|
|
26
|
+
if (matches.length === 0) {
|
|
27
|
+
return template;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// If the entire string is a single expression like "{$url}", return the resolved value
|
|
31
|
+
if (matches.length === 1 && matches[0][0] === template) {
|
|
32
|
+
return resolver(matches[0][1]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Multiple expressions or mixed template - replace each one
|
|
36
|
+
for (const match of matches) {
|
|
37
|
+
const fullMatch = match[0]; // e.g., "{$inputs.user}"
|
|
38
|
+
const expression = match[1]; // e.g., "$inputs.user"
|
|
39
|
+
|
|
40
|
+
const resolvedValue = resolver(expression);
|
|
41
|
+
|
|
42
|
+
// Convert resolved value to string for template substitution
|
|
43
|
+
const stringValue =
|
|
44
|
+
typeof resolvedValue === "object"
|
|
45
|
+
? JSON.stringify(resolvedValue)
|
|
46
|
+
: String(resolvedValue);
|
|
47
|
+
|
|
48
|
+
result = result.replace(fullMatch, stringValue);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if a string contains any runtime expressions
|
|
56
|
+
*
|
|
57
|
+
* @param {string} str - String to check
|
|
58
|
+
* @returns {boolean} True if the string contains runtime expressions
|
|
59
|
+
*/
|
|
60
|
+
function hasRuntimeExpressions(str) {
|
|
61
|
+
if (typeof str !== "string") {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return /{(\$[^}]+)}/g.test(str);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Extract all runtime expressions from a template string
|
|
69
|
+
*
|
|
70
|
+
* @param {string} template - Template string
|
|
71
|
+
* @returns {Array<string>} Array of runtime expressions (without the {} wrapper)
|
|
72
|
+
*/
|
|
73
|
+
function extractAll(template) {
|
|
74
|
+
if (typeof template !== "string") {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const runtimeExpressionRegex = /{(\$[^}]+)}/g;
|
|
79
|
+
const matches = [...template.matchAll(runtimeExpressionRegex)];
|
|
80
|
+
|
|
81
|
+
return matches.map((match) => match[1]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Example usage
|
|
85
|
+
// const Expression = require("./Expression");
|
|
86
|
+
|
|
87
|
+
// const expression = new Expression();
|
|
88
|
+
// expression.addToContext("inputs", {
|
|
89
|
+
// user: { name: "Jack" },
|
|
90
|
+
// petName: "Jill",
|
|
91
|
+
// pet_id: 1,
|
|
92
|
+
// coupon_code: "abc-def",
|
|
93
|
+
// quantity: 1,
|
|
94
|
+
// });
|
|
95
|
+
|
|
96
|
+
// // Example 1: Multiple expressions in a sentence
|
|
97
|
+
// const result1 = extractAndResolve(
|
|
98
|
+
// "{$inputs.user} has a pet called {$inputs.petName}",
|
|
99
|
+
// (expr) => expression.resolveExpression(expr),
|
|
100
|
+
// );
|
|
101
|
+
// console.log(result1);
|
|
102
|
+
// // Output: {"name":"Jack"} has a pet called Jill
|
|
103
|
+
|
|
104
|
+
// // Example 2: Single expression (returns the resolved value directly)
|
|
105
|
+
// const result2 = extractAndResolve("{$inputs.user}", (expr) =>
|
|
106
|
+
// expression.resolveExpression(expr),
|
|
107
|
+
// );
|
|
108
|
+
// console.log(result2);
|
|
109
|
+
// // Output: { name: 'Jack' }
|
|
110
|
+
|
|
111
|
+
// // Example 3: JSON template with multiple expressions
|
|
112
|
+
// const result3 = extractAndResolve(
|
|
113
|
+
// '{\n "petOrder": {\n "petId": "{$inputs.pet_id}",\n "couponCode": "{$inputs.coupon_code}",\n "quantity": "{$inputs.quantity}"\n }\n}',
|
|
114
|
+
// (expr) => expression.resolveExpression(expr),
|
|
115
|
+
// );
|
|
116
|
+
// console.log(result3);
|
|
117
|
+
|
|
118
|
+
// // Helper functions
|
|
119
|
+
// console.log("\nHelper examples:");
|
|
120
|
+
// console.log("Has expressions:", hasRuntimeExpressions("{$inputs.user} test"));
|
|
121
|
+
// console.log("Extract all:", extractAll("{$inputs.user} and {$inputs.petName}"));
|
|
122
|
+
|
|
123
|
+
module.exports = {
|
|
124
|
+
extractAndResolve,
|
|
125
|
+
hasRuntimeExpressions,
|
|
126
|
+
extractAll,
|
|
127
|
+
};
|