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/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# x-openapi-flow
|
|
2
2
|
|
|
3
|
+

|
|
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`)
|
|
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
|
|
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 [
|
|
55
|
-
x-openapi-flow apply [openapi-file] [--flows path] [--out path]
|
|
56
|
-
x-openapi-flow
|
|
57
|
-
x-openapi-flow
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
-
|
|
109
|
-
- For UI interpretation of `x-openapi-flow`, use `showExtensions: true`
|
|
110
|
-
- A ready HTML example is available at
|
|
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
|
-

|
|
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
|
-

|
|
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 };
|