x-openapi-flow 1.2.3 → 1.3.1
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 +319 -20
- package/adapters/collections/insomnia-adapter.js +73 -0
- package/adapters/collections/postman-adapter.js +145 -0
- package/adapters/docs/doc-adapter.js +119 -0
- package/adapters/flow-output-adapters.js +15 -0
- package/adapters/shared/helpers.js +87 -0
- package/adapters/ui/redoc/x-openapi-flow-redoc-plugin.js +127 -0
- package/adapters/ui/redoc-adapter.js +75 -0
- package/adapters/ui/swagger-ui/x-openapi-flow-plugin.js +856 -0
- package/bin/x-openapi-flow.js +1502 -64
- package/lib/sdk-generator.js +673 -0
- package/lib/validator.js +36 -3
- package/package.json +9 -3
- package/schema/flow-schema.json +2 -2
- package/templates/go/README.md +3 -0
- package/templates/kotlin/README.md +3 -0
- package/templates/python/README.md +3 -0
- package/templates/typescript/flow-helpers.hbs +26 -0
- package/templates/typescript/http-client.hbs +37 -0
- package/templates/typescript/index.hbs +16 -0
- package/templates/typescript/resource.hbs +24 -0
- package/examples/swagger-ui/index.html +0 -33
- package/lib/swagger-ui/x-openapi-flow-plugin.js +0 -455
package/lib/validator.js
CHANGED
|
@@ -169,6 +169,8 @@ function getOperationsById(api) {
|
|
|
169
169
|
|
|
170
170
|
operationsById.set(operation.operationId, {
|
|
171
171
|
operation,
|
|
172
|
+
path_item: pathItem,
|
|
173
|
+
path_key: pathKey,
|
|
172
174
|
endpoint: `${method.toUpperCase()} ${pathKey}`,
|
|
173
175
|
});
|
|
174
176
|
}
|
|
@@ -224,15 +226,15 @@ function parseFieldReference(refValue) {
|
|
|
224
226
|
return null;
|
|
225
227
|
}
|
|
226
228
|
|
|
227
|
-
const match = refValue.match(/^([^:]+):(request\.body|response\.(\d{3}|default)\.body)\.(.+)$/);
|
|
229
|
+
const match = refValue.match(/^([^:]+):(request\.(body|path)|response\.(\d{3}|default)\.body)\.(.+)$/);
|
|
228
230
|
if (!match) {
|
|
229
231
|
return null;
|
|
230
232
|
}
|
|
231
233
|
|
|
232
234
|
const operationId = match[1];
|
|
233
235
|
const scope = match[2];
|
|
234
|
-
const responseCode = match[
|
|
235
|
-
const fieldPath = match[
|
|
236
|
+
const responseCode = match[4];
|
|
237
|
+
const fieldPath = match[5];
|
|
236
238
|
|
|
237
239
|
return {
|
|
238
240
|
operation_id: operationId,
|
|
@@ -332,6 +334,34 @@ function resolveFieldReferenceSchema(api, operationsById, parsedRef) {
|
|
|
332
334
|
return { schema: requestSchema };
|
|
333
335
|
}
|
|
334
336
|
|
|
337
|
+
if (parsedRef.scope === "request.path") {
|
|
338
|
+
const pathLevelParams = Array.isArray(operationInfo.path_item && operationInfo.path_item.parameters)
|
|
339
|
+
? operationInfo.path_item.parameters
|
|
340
|
+
: [];
|
|
341
|
+
const operationLevelParams = Array.isArray(operation.parameters)
|
|
342
|
+
? operation.parameters
|
|
343
|
+
: [];
|
|
344
|
+
|
|
345
|
+
const allParams = [...pathLevelParams, ...operationLevelParams].filter(
|
|
346
|
+
(param) => param && typeof param === "object" && param.in === "path" && param.name
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
if (!allParams.length) {
|
|
350
|
+
return { error: "path_parameters_not_found" };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const pathSchema = {
|
|
354
|
+
type: "object",
|
|
355
|
+
properties: {},
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
for (const param of allParams) {
|
|
359
|
+
pathSchema.properties[param.name] = param.schema || { type: "string" };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return { schema: pathSchema };
|
|
363
|
+
}
|
|
364
|
+
|
|
335
365
|
const responseCode = parsedRef.response_code;
|
|
336
366
|
const responseSchema = operation.responses
|
|
337
367
|
&& operation.responses[responseCode]
|
|
@@ -985,5 +1015,8 @@ module.exports = {
|
|
|
985
1015
|
validateFlows,
|
|
986
1016
|
detectOrphanStates,
|
|
987
1017
|
buildStateGraph,
|
|
1018
|
+
detectDuplicateTransitions,
|
|
1019
|
+
detectInvalidOperationReferences,
|
|
1020
|
+
detectTerminalCoverage,
|
|
988
1021
|
run,
|
|
989
1022
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "x-openapi-flow",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "OpenAPI extension for resource workflow and lifecycle management",
|
|
5
5
|
"main": "lib/validator.js",
|
|
6
6
|
"repository": {
|
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
"bin",
|
|
16
16
|
"lib",
|
|
17
|
+
"adapters",
|
|
18
|
+
"templates",
|
|
19
|
+
"schema",
|
|
17
20
|
"schema",
|
|
18
21
|
"examples",
|
|
19
22
|
"README.md",
|
|
@@ -29,8 +32,10 @@
|
|
|
29
32
|
"x-openapi-flow": "bin/x-openapi-flow.js"
|
|
30
33
|
},
|
|
31
34
|
"scripts": {
|
|
32
|
-
"test": "npm run test:cli && npm run test:smoke",
|
|
33
|
-
"test:cli": "node --test tests/cli.test.js",
|
|
35
|
+
"test": "npm run test:cli && npm run test:ui && npm run test:integration && npm run test:smoke",
|
|
36
|
+
"test:cli": "node --test tests/cli/cli.test.js",
|
|
37
|
+
"test:ui": "node --test tests/plugins/plugin-ui.test.js",
|
|
38
|
+
"test:integration": "node --test tests/integration/*.test.js",
|
|
34
39
|
"test:smoke": "node bin/x-openapi-flow.js validate examples/payment-api.yaml --profile strict"
|
|
35
40
|
},
|
|
36
41
|
"keywords": [
|
|
@@ -44,6 +49,7 @@
|
|
|
44
49
|
"license": "MIT",
|
|
45
50
|
"dependencies": {
|
|
46
51
|
"ajv": "^8.17.1",
|
|
52
|
+
"handlebars": "^4.7.8",
|
|
47
53
|
"js-yaml": "^4.1.0"
|
|
48
54
|
}
|
|
49
55
|
}
|
package/schema/flow-schema.json
CHANGED
|
@@ -72,14 +72,14 @@
|
|
|
72
72
|
},
|
|
73
73
|
"prerequisite_field_refs": {
|
|
74
74
|
"type": "array",
|
|
75
|
-
"description": "Field references required before this transition. Format: operationId:request.body.field or operationId:response.<status>.body.field",
|
|
75
|
+
"description": "Field references required before this transition. Format: operationId:request.body.field, operationId:request.path.paramName, or operationId:response.<status>.body.field",
|
|
76
76
|
"items": {
|
|
77
77
|
"type": "string"
|
|
78
78
|
}
|
|
79
79
|
},
|
|
80
80
|
"propagated_field_refs": {
|
|
81
81
|
"type": "array",
|
|
82
|
-
"description": "Field references produced/propagated for downstream flows. Format: operationId:request.body.field or operationId:response.<status>.body.field",
|
|
82
|
+
"description": "Field references produced/propagated for downstream flows. Format: operationId:request.body.field, operationId:request.path.paramName, or operationId:response.<status>.body.field",
|
|
83
83
|
"items": {
|
|
84
84
|
"type": "string"
|
|
85
85
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface FlowExecutable {
|
|
2
|
+
[methodName: string]: (...args: unknown[]) => Promise<unknown>;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export async function runFlow(
|
|
6
|
+
executable: FlowExecutable,
|
|
7
|
+
flow: string,
|
|
8
|
+
...args: unknown[]
|
|
9
|
+
): Promise<unknown> {
|
|
10
|
+
const steps = flow
|
|
11
|
+
.split("->")
|
|
12
|
+
.map((step) => step.trim())
|
|
13
|
+
.filter(Boolean);
|
|
14
|
+
|
|
15
|
+
let current: unknown = executable;
|
|
16
|
+
for (const step of steps) {
|
|
17
|
+
const target = current as FlowExecutable;
|
|
18
|
+
if (!target || typeof target[step] !== "function") {
|
|
19
|
+
throw new Error(`Flow step '${step}' is not available in the current state.`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
current = await target[step](...args);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return current;
|
|
26
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface RequestOptions {
|
|
2
|
+
body?: unknown;
|
|
3
|
+
headers?: Record<string, string>;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface HttpClient {
|
|
7
|
+
request(method: string, path: string, options?: RequestOptions): Promise<unknown>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class FetchHttpClient implements HttpClient {
|
|
11
|
+
constructor(
|
|
12
|
+
private readonly baseUrl: string,
|
|
13
|
+
private readonly defaultHeaders: Record<string, string> = {},
|
|
14
|
+
) {}
|
|
15
|
+
|
|
16
|
+
async request(method: string, path: string, options: RequestOptions = {}): Promise<unknown> {
|
|
17
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
18
|
+
method,
|
|
19
|
+
headers: {
|
|
20
|
+
"content-type": "application/json",
|
|
21
|
+
...this.defaultHeaders,
|
|
22
|
+
...(options.headers || {}),
|
|
23
|
+
},
|
|
24
|
+
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (!response.ok) {
|
|
28
|
+
throw new Error(`HTTP ${response.status} when calling ${method} ${path}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (response.status === 204) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return response.json();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { HttpClient } from "./http-client";
|
|
2
|
+
{{#each resources}}
|
|
3
|
+
import { {{resourceClassName}}Resource } from "./resources/{{resourceClassName}}";
|
|
4
|
+
{{/each}}
|
|
5
|
+
|
|
6
|
+
export class FlowApiClient {
|
|
7
|
+
{{#each resources}}
|
|
8
|
+
public readonly {{resourcePropertyName}}: {{resourceClassName}}Resource;
|
|
9
|
+
{{/each}}
|
|
10
|
+
|
|
11
|
+
constructor(httpClient: HttpClient) {
|
|
12
|
+
{{#each resources}}
|
|
13
|
+
this.{{resourcePropertyName}} = new {{resourceClassName}}Resource(httpClient);
|
|
14
|
+
{{/each}}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { HttpClient, RequestOptions } from "../http-client";
|
|
2
|
+
|
|
3
|
+
const ensurePrerequisites = (
|
|
4
|
+
completedOperations: Set<string>,
|
|
5
|
+
requiredOperationIds: string[],
|
|
6
|
+
methodName: string,
|
|
7
|
+
): void => {
|
|
8
|
+
const missing = requiredOperationIds.filter((operationId) => !completedOperations.has(operationId));
|
|
9
|
+
if (missing.length > 0) {
|
|
10
|
+
throw new Error(
|
|
11
|
+
`Cannot call ${methodName} before prerequisites are satisfied: ${missing.join(", ")}`,
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
{{{sharedTypesCode}}}
|
|
17
|
+
|
|
18
|
+
{{{stateClassesCode}}}
|
|
19
|
+
|
|
20
|
+
export class {{resourceClassName}}Resource {
|
|
21
|
+
constructor(private readonly httpClient: HttpClient) {}
|
|
22
|
+
|
|
23
|
+
{{{serviceMethodsCode}}}
|
|
24
|
+
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>x-openapi-flow + Swagger UI</title>
|
|
7
|
-
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" />
|
|
8
|
-
<style>
|
|
9
|
-
body { margin: 0; }
|
|
10
|
-
#swagger-ui { max-width: 1200px; margin: 0 auto; }
|
|
11
|
-
</style>
|
|
12
|
-
</head>
|
|
13
|
-
<body>
|
|
14
|
-
<div id="swagger-ui"></div>
|
|
15
|
-
|
|
16
|
-
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
|
17
|
-
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-standalone-preset.js"></script>
|
|
18
|
-
<script>
|
|
19
|
-
window.XOpenApiFlowGraphImageUrl = "https://raw.githubusercontent.com/tiago-marques/x-openapi-flow/main/docs/assets/graph-order-guided.svg";
|
|
20
|
-
</script>
|
|
21
|
-
<script src="../../lib/swagger-ui/x-openapi-flow-plugin.js"></script>
|
|
22
|
-
<script>
|
|
23
|
-
window.ui = SwaggerUIBundle({
|
|
24
|
-
url: "../payment-api.yaml",
|
|
25
|
-
dom_id: "#swagger-ui",
|
|
26
|
-
deepLinking: true,
|
|
27
|
-
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
|
|
28
|
-
plugins: [window.XOpenApiFlowPlugin],
|
|
29
|
-
showExtensions: true,
|
|
30
|
-
});
|
|
31
|
-
</script>
|
|
32
|
-
</body>
|
|
33
|
-
</html>
|