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 CHANGED
@@ -1,5 +1,7 @@
1
1
  # x-openapi-flow
2
2
 
3
+ ![x-openapi-flow logo](../docs/assets/x-openapi-flow-logo.svg)
4
+
3
5
  CLI and extension contract for documenting and validating resource lifecycle workflows in OpenAPI using `x-openapi-flow`.
4
6
 
5
7
  ## Overview
@@ -10,7 +12,7 @@ CLI and extension contract for documenting and validating resource lifecycle wor
10
12
  - Lifecycle graph consistency
11
13
  - Optional quality checks for transitions and references
12
14
 
13
- It also supports a sidecar workflow (`init` + `apply`) to preserve lifecycle metadata when OpenAPI files are regenerated.
15
+ It also supports a sidecar workflow (`init` + `apply`) so lifecycle metadata stays preserved when OpenAPI source files are regenerated.
14
16
 
15
17
  ## Installation
16
18
 
@@ -36,7 +38,7 @@ Use a GitHub PAT with `read:packages` (install) and `write:packages` (publish).
36
38
  ## Quick Start
37
39
 
38
40
  ```bash
39
- npx x-openapi-flow init openapi.yaml
41
+ npx x-openapi-flow init
40
42
  npx x-openapi-flow apply openapi.yaml
41
43
  ```
42
44
 
@@ -44,34 +46,331 @@ Optional checks:
44
46
 
45
47
  ```bash
46
48
  npx x-openapi-flow validate openapi.yaml --profile strict
49
+ npx x-openapi-flow lint openapi.yaml
47
50
  npx x-openapi-flow graph openapi.yaml
48
51
  ```
49
52
 
50
53
  ## CLI Commands
51
54
 
52
55
  ```bash
53
- x-openapi-flow validate <openapi-file> [--format pretty|json] [--profile core|relaxed|strict] [--strict-quality] [--config path]
54
- x-openapi-flow init [openapi-file] [--flows path]
55
- x-openapi-flow apply [openapi-file] [--flows path] [--out path]
56
- x-openapi-flow graph <openapi-file> [--format mermaid|json]
57
- x-openapi-flow doctor [--config path]
56
+ npx x-openapi-flow validate <openapi-file> [--format pretty|json] [--profile core|relaxed|strict] [--strict-quality] [--config path]
57
+ npx x-openapi-flow init [--flows path] [--force] [--dry-run]
58
+ npx x-openapi-flow apply [openapi-file] [--flows path] [--out path]
59
+ npx x-openapi-flow diff [openapi-file] [--flows path] [--format pretty|json]
60
+ npx x-openapi-flow lint [openapi-file] [--format pretty|json] [--config path]
61
+ npx x-openapi-flow analyze [openapi-file] [--format pretty|json] [--out path] [--merge] [--flows path]
62
+ npx x-openapi-flow generate-sdk [openapi-file] --lang typescript [--output path]
63
+ npx x-openapi-flow export-doc-flows [openapi-file] [--output path] [--format markdown|json]
64
+ npx x-openapi-flow generate-postman [openapi-file] [--output path] [--with-scripts]
65
+ npx x-openapi-flow generate-insomnia [openapi-file] [--output path]
66
+ npx x-openapi-flow generate-redoc [openapi-file] [--output path]
67
+ npx x-openapi-flow graph <openapi-file> [--format mermaid|json]
68
+ npx x-openapi-flow doctor [--config path]
69
+ ```
70
+
71
+ ## Output Adapters
72
+
73
+ `x-openapi-flow` now supports modular output adapters that reuse the same internal flow graph:
74
+
75
+ - OpenAPI + `x-openapi-flow` -> parser -> graph builder -> adapters
76
+ - Adapters: docs (`export-doc-flows`), SDK (`generate-sdk`), Postman (`generate-postman`), Insomnia (`generate-insomnia`)
77
+ and Redoc package (`generate-redoc`)
78
+
79
+ ### Redoc/Docs Adapter (`export-doc-flows`)
80
+
81
+ ```bash
82
+ npx x-openapi-flow export-doc-flows openapi.yaml --output ./docs/api-flows.md
83
+ npx x-openapi-flow export-doc-flows openapi.yaml --format json --output ./docs/api-flows.json
84
+ ```
85
+
86
+ Generates a lifecycle page (or JSON model) with:
87
+
88
+ - Flow/Lifecycle panel per resource
89
+ - Mermaid diagram per resource
90
+ - Current state, prerequisites (`prerequisite_operation_ids`), next operations (`next_operation_id`)
91
+
92
+ ### Redoc Package Adapter (`generate-redoc`)
93
+
94
+ ```bash
95
+ npx x-openapi-flow generate-redoc openapi.yaml --output ./redoc-flow
96
+ ```
97
+
98
+ Generates a ready-to-open Redoc bundle with:
99
+
100
+ - `index.html` (Redoc + Flow/Lifecycle panel)
101
+ - `x-openapi-flow-redoc-plugin.js` (DOM enhancer)
102
+ - `flow-model.json` (flow graph model)
103
+ - copied OpenAPI spec (`openapi.yaml`/`openapi.json`)
104
+
105
+ ### Postman Adapter (`generate-postman`)
106
+
107
+ ```bash
108
+ npx x-openapi-flow generate-postman openapi.yaml --output ./x-openapi-flow.postman_collection.json --with-scripts
109
+ ```
110
+
111
+ Generates lifecycle-oriented folders/journeys and optional scripts for:
112
+
113
+ - prerequisite enforcement before request execution
114
+ - propagated operation tracking and ID persistence in collection variables
115
+
116
+ ### Insomnia Adapter (`generate-insomnia`)
117
+
118
+ ```bash
119
+ npx x-openapi-flow generate-insomnia openapi.yaml --output ./x-openapi-flow.insomnia.json
120
+ ```
121
+
122
+ Generates an Insomnia export organized by resource flow groups and ordered requests.
123
+
124
+ ## SDK Generator (`generate-sdk`)
125
+
126
+ Generate a flow-aware SDK from OpenAPI + `x-openapi-flow` metadata.
127
+
128
+ ```bash
129
+ npx x-openapi-flow generate-sdk openapi.yaml --lang typescript --output ./sdk
58
130
  ```
59
131
 
132
+ MVP output (TypeScript):
133
+
134
+ - `src/resources/<Resource>.ts`: resource client + state classes (`PaymentAuthorized`, `PaymentCaptured`, etc.)
135
+ - `src/index.ts`: root `FlowApiClient`
136
+ - `src/http-client.ts`: pluggable HTTP client interface and fetch implementation
137
+ - `src/flow-helpers.ts`: `runFlow("authorize -> capture")`
138
+ - `flow-model.json`: intermediate model `{ resource, operations, prerequisites, nextOperations, states }`
139
+
140
+ SDK layers (resource-centric):
141
+
142
+ - Collection/service layer: `api.payments.create()`, `api.payments.retrieve(id)`, `api.payments.list()`
143
+ - Resource instance/state layer: objects expose valid lifecycle transitions (`payment.capture()`, etc.)
144
+ - Optional lifecycle helper methods at service level (`api.payments.capture(id, params, { autoPrerequisites: true })`)
145
+
146
+ Pipeline used by the generator:
147
+
148
+ - OpenAPI -> parser -> flow graph -> state machine -> templates -> SDK
149
+ - Reuses lifecycle graph logic from the validator to stay consistent with `validate`, `graph`, and `diff`
150
+ - Transition ordering uses `next_operation_id`, `prerequisite_operation_ids`, and state transitions from `x-openapi-flow`
151
+
152
+ ## Flow Analyzer (`analyze`)
153
+
154
+ Use `analyze` to bootstrap a sidecar from OpenAPI paths/operation names.
155
+
156
+ ```bash
157
+ npx x-openapi-flow analyze openapi.yaml --out openapi.x.yaml
158
+ npx x-openapi-flow analyze openapi.yaml --format json
159
+ npx x-openapi-flow analyze openapi.yaml --merge --flows openapi.x.yaml
160
+ ```
161
+
162
+ Notes:
163
+
164
+ - The output is heuristic and intended as a starting point.
165
+ - Inferred states/transitions should be reviewed and adjusted by API/domain owners.
166
+ - Default output format is `pretty`; without `--out`, the suggested sidecar is printed to stdout.
167
+ - `--merge` merges inferred data into an existing sidecar (default path or `--flows`) while preserving existing operation fields.
168
+ - In `json`, inferred transition confidence is available in `analysis.transitionConfidence`.
169
+
170
+ `diff` now reports field-level changes for operations that already exist in the sidecar.
171
+ In `pretty` format, this appears under `Changed details` with changed paths per operation (for example, `current_state` or `transitions[0].target_state`).
172
+ In `json` format, this appears in `diff.changedOperationDetails`:
173
+
174
+ ```json
175
+ {
176
+ "diff": {
177
+ "changedOperationDetails": [
178
+ {
179
+ "operationId": "listItems",
180
+ "changedPaths": ["current_state"]
181
+ }
182
+ ]
183
+ }
184
+ }
185
+ ```
186
+
187
+ ### CI usage (`diff` as a gate)
188
+
189
+ Fail the pipeline when sidecar drift is detected:
190
+
191
+ ```bash
192
+ npx x-openapi-flow diff openapi.yaml --format json | node -e '
193
+ const fs = require("fs");
194
+ const payload = JSON.parse(fs.readFileSync(0, "utf8"));
195
+ const diff = payload.diff || {};
196
+ const changes = (diff.added || 0) + (diff.removed || 0) + (diff.changed || 0);
197
+ if (changes > 0) {
198
+ console.error("x-openapi-flow diff detected changes. Run init/apply and commit sidecar updates.");
199
+ process.exit(1);
200
+ }
201
+ '
202
+ ```
203
+
204
+ This keeps `.x` sidecar data aligned with the OpenAPI source in pull requests.
205
+
206
+ ## Semantic lint (`lint`)
207
+
208
+ Use `lint` to run semantic checks focused on flow modeling quality.
209
+
210
+ ```bash
211
+ npx x-openapi-flow lint openapi.yaml
212
+ npx x-openapi-flow lint openapi.yaml --format json
213
+ ```
214
+
215
+ MVP semantic rules:
216
+
217
+ - `next_operation_id_exists`
218
+ - `prerequisite_operation_ids_exist`
219
+ - `duplicate_transitions`
220
+ - `terminal_path` (states without path to terminal)
221
+
222
+ Disable individual rules with config (`x-openapi-flow.config.json`):
223
+
224
+ ```json
225
+ {
226
+ "lint": {
227
+ "rules": {
228
+ "next_operation_id_exists": true,
229
+ "prerequisite_operation_ids_exist": true,
230
+ "duplicate_transitions": false,
231
+ "terminal_path": true
232
+ }
233
+ }
234
+ }
235
+ ```
236
+
237
+ ## Graph JSON contract (`graph --format json`)
238
+
239
+ `graph --format json` returns a stable contract for CI/pipeline integrations:
240
+
241
+ - `format_version`: output contract version (currently `"1.0"`).
242
+ - `flowCount`: number of operations with `x-openapi-flow`.
243
+ - `nodes`: sorted state names (deterministic order).
244
+ - `edges`: sorted transitions by `from`, `to`, `next_operation_id`, and prerequisites.
245
+ - `mermaid`: deterministic Mermaid rendering of the same graph.
246
+
247
+ Example:
248
+
249
+ ```json
250
+ {
251
+ "format_version": "1.0",
252
+ "flowCount": 3,
253
+ "nodes": ["CONFIRMED", "CREATED", "SHIPPED"],
254
+ "edges": [
255
+ {
256
+ "from": "CONFIRMED",
257
+ "to": "SHIPPED",
258
+ "next_operation_id": "shipOrder",
259
+ "prerequisite_operation_ids": []
260
+ }
261
+ ],
262
+ "mermaid": "stateDiagram-v2\n state CONFIRMED\n state CREATED\n state SHIPPED\n CONFIRMED --> SHIPPED: next:shipOrder"
263
+ }
264
+ ```
265
+
266
+ ## HTTP Methods Support
267
+
268
+ `init`, `apply`, and `graph` support all OpenAPI 3 HTTP operation methods:
269
+
270
+ - `get`
271
+ - `put`
272
+ - `post`
273
+ - `delete`
274
+ - `options`
275
+ - `head`
276
+ - `patch`
277
+ - `trace`
278
+
60
279
  ## Sidecar Workflow
61
280
 
62
- `init` always works on an existing OpenAPI file in your repository.
63
- `init` creates/synchronizes `{context}-openapi-flow.(json|yaml)` as a persistent sidecar for your `x-openapi-flow` data.
64
- Use `apply` to inject sidecar flows back into regenerated OpenAPI files.
65
- If no OpenAPI/Swagger file exists yet, generate one first with your framework's official OpenAPI/Swagger tooling.
281
+ Behavior summary:
282
+
283
+ - `init` works on an existing OpenAPI source file in your repository.
284
+ - `init` creates/synchronizes `{context}.x.(json|yaml)` as a persistent sidecar for `x-openapi-flow` data.
285
+ - If `{context}.flow.(json|yaml)` does not exist, `init` generates it automatically (same merge result as `apply`).
286
+ - If `{context}.flow.(json|yaml)` already exists, `init` asks in interactive mode whether to recreate it.
287
+ - On confirmation (or with `--force`), `init` creates a sidecar backup as `{context}.x.(json|yaml).backup-N` before regenerating `{context}.flow.(json|yaml)`.
288
+ - In non-interactive mode, `init` fails when flow output already exists unless `--force` is provided.
289
+ - With `--dry-run`, `init` prints a summary of sidecar/flow behavior without writing files.
290
+ - Use `apply` to inject sidecar flows back into regenerated OpenAPI source files.
291
+ - If no OpenAPI/Swagger source file exists yet, generate one first with your framework's official tooling.
66
292
 
67
293
  ### Recommended Sequence
68
294
 
69
295
  ```bash
70
- x-openapi-flow init openapi.yaml
71
- # edit {context}-openapi-flow.(json|yaml)
72
- x-openapi-flow apply openapi.yaml
296
+ npx x-openapi-flow init
297
+ npx x-openapi-flow init --dry-run
298
+ # edit {context}.x.(json|yaml)
299
+ npx x-openapi-flow diff openapi.yaml
300
+ npx x-openapi-flow apply openapi.yaml
301
+ ```
302
+
303
+ ## Sidecar File Contract (all supported fields)
304
+
305
+ Sidecar top-level shape:
306
+
307
+ ```yaml
308
+ version: "1.0"
309
+ operations:
310
+ - operationId: createOrder
311
+ x-openapi-flow:
312
+ version: "1.0"
313
+ id: create-order
314
+ current_state: CREATED
315
+ description: Creates an order and starts its lifecycle
316
+ idempotency:
317
+ header: Idempotency-Key
318
+ required: true
319
+ transitions:
320
+ - target_state: PAID
321
+ trigger_type: synchronous
322
+ condition: Payment approved
323
+ next_operation_id: payOrder
324
+ prerequisite_operation_ids:
325
+ - createOrder
326
+ prerequisite_field_refs:
327
+ - createOrder:request.body.customer_id
328
+ propagated_field_refs:
329
+ - createOrder:response.201.body.order_id
73
330
  ```
74
331
 
332
+ Top-level (sidecar document):
333
+
334
+ - `version` (optional, string): sidecar contract version. Default is `"1.0"`.
335
+ - `operations` (optional, array): entries keyed by operation.
336
+
337
+ Per operation entry:
338
+
339
+ - `operationId` (recommended, string): target operation identifier in OpenAPI.
340
+ - `x-openapi-flow` (object): lifecycle metadata for that operation.
341
+ - `key` (optional, legacy): backward-compatibility fallback key used by apply.
342
+
343
+ `x-openapi-flow` fields:
344
+
345
+ - Required:
346
+ - `version` (string): currently `"1.0"`.
347
+ - `id` (string): unique flow id.
348
+ - `current_state` (string): state represented by this operation.
349
+ - Optional:
350
+ - `description` (string): human-readable purpose.
351
+ - `idempotency` (object):
352
+ - `header` (required, string)
353
+ - `required` (optional, boolean)
354
+ - `transitions` (array of transition objects)
355
+
356
+ Transition object fields:
357
+
358
+ - Required:
359
+ - `target_state` (string)
360
+ - `trigger_type` (enum): `synchronous`, `webhook`, `polling`
361
+ - Optional:
362
+ - `condition` (string)
363
+ - `next_operation_id` (string)
364
+ - `prerequisite_operation_ids` (array of strings)
365
+ - `prerequisite_field_refs` (array of strings)
366
+ - `propagated_field_refs` (array of strings)
367
+
368
+ Field refs format:
369
+
370
+ - `operationId:request.body.field`
371
+ - `operationId:request.path.paramName`
372
+ - `operationId:response.<status>.body.field`
373
+
75
374
  ## Configuration
76
375
 
77
376
  Create `x-openapi-flow.config.json` in your project directory:
@@ -105,19 +404,19 @@ Field reference format:
105
404
 
106
405
  ### Swagger UI
107
406
 
108
- - There is no Swagger UI-based automated test in this repo today (tests are CLI-only).
109
- - For UI interpretation of `x-openapi-flow`, use `showExtensions: true` plus the plugin at `lib/swagger-ui/x-openapi-flow-plugin.js`.
110
- - A ready HTML example is available at `examples/swagger-ui/index.html`.
407
+ - UI plugin behavior is covered by tests in `tests/plugins/plugin-ui.test.js`.
408
+ - For UI interpretation of `x-openapi-flow`, use `showExtensions: true` with the plugin at `adapters/ui/swagger-ui/x-openapi-flow-plugin.js`.
409
+ - A ready HTML example is available at `../example-project/examples/swagger-ui/index.html`.
111
410
  - The plugin renders a global **Flow Overview** (Mermaid image) near the top of the docs, plus operation-level flow cards.
112
411
 
113
- ![Swagger UI integration result](../docs/assets/swagger-ui-integration-result-v2.svg)
412
+ ![Swagger UI integration result](../docs/assets/x-openapi-flow-extension.png)
114
413
 
115
414
  ### Graph Output Example
116
415
 
117
416
  `x-openapi-flow graph` includes transition guidance labels in Mermaid output when present (`next_operation_id`, `prerequisite_operation_ids`).
118
- The `graph` command accepts both full OpenAPI files and sidecar files (`{context}-openapi-flow.(json|yaml)`).
417
+ The `graph` command accepts both full OpenAPI source files and sidecar files (`{context}.x.(json|yaml)` and legacy `{context}-openapi-flow.(json|yaml)`).
119
418
 
120
- ![Guided graph example](../docs/assets/graph-order-guided.svg)
419
+ ![Guided graph example](../docs/assets/x-openapi-flow-overview.png)
121
420
 
122
421
  ## Repository and Documentation
123
422
 
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { loadApi } = require("../../lib/validator");
6
+ const { buildIntermediateModel } = require("../../lib/sdk-generator");
7
+ const { toTitleCase, pathToPostmanUrl, buildLifecycleSequences } = require("../shared/helpers");
8
+
9
+ function generateInsomniaWorkspace(options) {
10
+ const apiPath = path.resolve(options.apiPath);
11
+ const outputPath = path.resolve(options.outputPath || path.join(process.cwd(), "x-openapi-flow.insomnia.json"));
12
+
13
+ const api = loadApi(apiPath);
14
+ const model = buildIntermediateModel(api);
15
+
16
+ const workspaceId = "wrk_x_openapi_flow";
17
+ const resources = [
18
+ {
19
+ _id: workspaceId,
20
+ _type: "workspace",
21
+ name: "x-openapi-flow Workspace",
22
+ description: `Generated from ${apiPath}`,
23
+ scope: "collection",
24
+ },
25
+ ];
26
+
27
+ for (const resource of model.resources) {
28
+ const groupId = `fld_${resource.resourcePropertyName}`;
29
+ resources.push({
30
+ _id: groupId,
31
+ _type: "request_group",
32
+ parentId: workspaceId,
33
+ name: `${toTitleCase(resource.resourcePlural || resource.resource)} Flow`,
34
+ });
35
+
36
+ const sequences = buildLifecycleSequences(resource);
37
+ const operations = sequences.length > 0
38
+ ? Array.from(new Map(sequences.flat().map((op) => [op.operationId, op])).values())
39
+ : resource.operations.filter((operation) => operation.hasFlow);
40
+
41
+ operations.forEach((operation, index) => {
42
+ const requestId = `req_${resource.resourcePropertyName}_${index + 1}`;
43
+ resources.push({
44
+ _id: requestId,
45
+ _type: "request",
46
+ parentId: groupId,
47
+ name: operation.operationId,
48
+ method: String(operation.httpMethod || "get").toUpperCase(),
49
+ url: `{{ base_url }}${pathToPostmanUrl(operation.path, resource.resourcePropertyName)}`,
50
+ body: {},
51
+ });
52
+ });
53
+ }
54
+
55
+ const exportPayload = {
56
+ _type: "export",
57
+ __export_format: 4,
58
+ __export_date: new Date().toISOString(),
59
+ __export_source: "x-openapi-flow",
60
+ resources,
61
+ };
62
+
63
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
64
+ fs.writeFileSync(outputPath, `${JSON.stringify(exportPayload, null, 2)}\n`, "utf8");
65
+
66
+ return {
67
+ outputPath,
68
+ resources: model.resources.length,
69
+ flowCount: model.flowCount,
70
+ };
71
+ }
72
+
73
+ module.exports = { generateInsomniaWorkspace };
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { loadApi } = require("../../lib/validator");
6
+ const { buildIntermediateModel } = require("../../lib/sdk-generator");
7
+ const { toTitleCase, pathToPostmanUrl, buildLifecycleSequences } = require("../shared/helpers");
8
+
9
+ function buildPostmanItem(operation, resource) {
10
+ const rawPath = pathToPostmanUrl(operation.path, resource.resourcePropertyName);
11
+ const urlRaw = `{{baseUrl}}${rawPath}`;
12
+
13
+ const item = {
14
+ name: operation.operationId,
15
+ request: {
16
+ method: String(operation.httpMethod || "get").toUpperCase(),
17
+ header: [
18
+ {
19
+ key: "Content-Type",
20
+ value: "application/json",
21
+ type: "text",
22
+ },
23
+ ],
24
+ url: {
25
+ raw: urlRaw,
26
+ host: ["{{baseUrl}}"],
27
+ path: rawPath.split("/").filter(Boolean),
28
+ },
29
+ },
30
+ response: [],
31
+ };
32
+
33
+ if (["POST", "PUT", "PATCH"].includes(item.request.method)) {
34
+ item.request.body = {
35
+ mode: "raw",
36
+ raw: "{}",
37
+ options: { raw: { language: "json" } },
38
+ };
39
+ }
40
+
41
+ return item;
42
+ }
43
+
44
+ function addPostmanScripts(item, operation, resource) {
45
+ const prereqs = JSON.stringify(operation.prerequisites || []);
46
+ const operationId = operation.operationId;
47
+ const idCandidateKey = `${resource.resourcePropertyName}Id`;
48
+
49
+ item.event = [
50
+ {
51
+ listen: "prerequest",
52
+ script: {
53
+ type: "text/javascript",
54
+ exec: [
55
+ `const required = ${prereqs};`,
56
+ "const executed = JSON.parse(pm.collectionVariables.get('flowExecutedOps') || '[]');",
57
+ "const missing = required.filter((operationId) => !executed.includes(operationId));",
58
+ "if (missing.length > 0) {",
59
+ ` throw new Error('Missing prerequisites for ${operationId}: ' + missing.join(', '));`,
60
+ "}",
61
+ ],
62
+ },
63
+ },
64
+ {
65
+ listen: "test",
66
+ script: {
67
+ type: "text/javascript",
68
+ exec: [
69
+ "const payload = pm.response.json ? pm.response.json() : {};",
70
+ `if (payload && payload.id) pm.collectionVariables.set('${idCandidateKey}', payload.id);`,
71
+ "const executed = JSON.parse(pm.collectionVariables.get('flowExecutedOps') || '[]');",
72
+ `if (!executed.includes('${operationId}')) executed.push('${operationId}');`,
73
+ "pm.collectionVariables.set('flowExecutedOps', JSON.stringify(executed));",
74
+ ],
75
+ },
76
+ },
77
+ ];
78
+ }
79
+
80
+ function generatePostmanCollection(options) {
81
+ const apiPath = path.resolve(options.apiPath);
82
+ const outputPath = path.resolve(options.outputPath || path.join(process.cwd(), "x-openapi-flow.postman_collection.json"));
83
+ const withScripts = options.withScripts !== false;
84
+
85
+ const api = loadApi(apiPath);
86
+ const model = buildIntermediateModel(api);
87
+
88
+ const collection = {
89
+ info: {
90
+ name: "x-openapi-flow Lifecycle Collection",
91
+ schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
92
+ description: `Generated from ${apiPath}`,
93
+ },
94
+ item: [],
95
+ variable: [
96
+ { key: "baseUrl", value: "http://localhost:3000" },
97
+ { key: "flowExecutedOps", value: "[]" },
98
+ ],
99
+ };
100
+
101
+ for (const resource of model.resources) {
102
+ const sequences = buildLifecycleSequences(resource);
103
+ const folder = {
104
+ name: `${toTitleCase(resource.resourcePlural || resource.resource)} Lifecycle`,
105
+ item: [],
106
+ };
107
+
108
+ if (sequences.length === 0) {
109
+ const fallbackItems = resource.operations
110
+ .filter((operation) => operation.hasFlow)
111
+ .map((operation) => {
112
+ const item = buildPostmanItem(operation, resource);
113
+ if (withScripts) addPostmanScripts(item, operation, resource);
114
+ return item;
115
+ });
116
+ folder.item.push(...fallbackItems);
117
+ } else {
118
+ sequences.forEach((sequence, index) => {
119
+ const journey = {
120
+ name: `Journey ${index + 1}`,
121
+ item: sequence.map((operation) => {
122
+ const item = buildPostmanItem(operation, resource);
123
+ if (withScripts) addPostmanScripts(item, operation, resource);
124
+ return item;
125
+ }),
126
+ };
127
+ folder.item.push(journey);
128
+ });
129
+ }
130
+
131
+ collection.item.push(folder);
132
+ }
133
+
134
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
135
+ fs.writeFileSync(outputPath, `${JSON.stringify(collection, null, 2)}\n`, "utf8");
136
+
137
+ return {
138
+ outputPath,
139
+ resources: model.resources.length,
140
+ flowCount: model.flowCount,
141
+ withScripts,
142
+ };
143
+ }
144
+
145
+ module.exports = { generatePostmanCollection };