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/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[3];
235
- const fieldPath = match[4];
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.2.3",
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
  }
@@ -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,3 @@
1
+ # Go Templates
2
+
3
+ Reserved for Go SDK generation templates.
@@ -0,0 +1,3 @@
1
+ # Kotlin Templates
2
+
3
+ Reserved for Kotlin SDK generation templates.
@@ -0,0 +1,3 @@
1
+ # Python Templates
2
+
3
+ Reserved for Python SDK generation templates.
@@ -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>