x-openapi-flow 1.3.6 → 1.4.0

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.
@@ -6,14 +6,179 @@ const { loadApi } = require("../../lib/validator");
6
6
  const { buildIntermediateModel } = require("../../lib/sdk-generator");
7
7
  const { toTitleCase, pathToPostmanUrl, buildLifecycleSequences } = require("../shared/helpers");
8
8
 
9
+ function getOperationMapById(api) {
10
+ const map = new Map();
11
+ const paths = (api && api.paths) || {};
12
+
13
+ for (const [pathKey, pathItem] of Object.entries(paths)) {
14
+ for (const [method, operation] of Object.entries(pathItem || {})) {
15
+ if (!operation || typeof operation !== "object") continue;
16
+ if (!operation.operationId) continue;
17
+ map.set(operation.operationId, {
18
+ ...operation,
19
+ __path: pathKey,
20
+ __method: String(method || "get").toLowerCase(),
21
+ });
22
+ }
23
+ }
24
+
25
+ return map;
26
+ }
27
+
28
+ function buildExampleFromSchema(schema) {
29
+ if (!schema || typeof schema !== "object") return {};
30
+
31
+ if (Object.prototype.hasOwnProperty.call(schema, "example")) {
32
+ return schema.example;
33
+ }
34
+
35
+ if (Array.isArray(schema.enum) && schema.enum.length > 0) {
36
+ return schema.enum[0];
37
+ }
38
+
39
+ if (schema.default !== undefined) {
40
+ return schema.default;
41
+ }
42
+
43
+ const type = schema.type;
44
+ if (type === "string") {
45
+ if (schema.format === "date-time") return "2026-01-01T00:00:00Z";
46
+ if (schema.format === "date") return "2026-01-01";
47
+ if (schema.format === "email") return "user@example.com";
48
+ return "string";
49
+ }
50
+ if (type === "number" || type === "integer") return 0;
51
+ if (type === "boolean") return false;
52
+ if (type === "array") {
53
+ const itemExample = buildExampleFromSchema(schema.items || {});
54
+ return itemExample === undefined ? [] : [itemExample];
55
+ }
56
+
57
+ const properties = schema.properties || {};
58
+ const required = Array.isArray(schema.required) ? schema.required : Object.keys(properties);
59
+ const payload = {};
60
+ for (const key of required) {
61
+ if (!properties[key]) continue;
62
+ payload[key] = buildExampleFromSchema(properties[key]);
63
+ }
64
+
65
+ if (Object.keys(payload).length === 0) {
66
+ for (const [key, propertySchema] of Object.entries(properties)) {
67
+ payload[key] = buildExampleFromSchema(propertySchema);
68
+ }
69
+ }
70
+
71
+ return payload;
72
+ }
73
+
74
+ function extractJsonRequestExample(rawOperation) {
75
+ if (!rawOperation || !rawOperation.requestBody || !rawOperation.requestBody.content) {
76
+ return null;
77
+ }
78
+
79
+ const jsonContent = rawOperation.requestBody.content["application/json"];
80
+ if (!jsonContent) return null;
81
+
82
+ if (jsonContent.example !== undefined) {
83
+ return jsonContent.example;
84
+ }
85
+
86
+ if (jsonContent.examples && typeof jsonContent.examples === "object") {
87
+ const firstExample = Object.values(jsonContent.examples)[0];
88
+ if (firstExample && typeof firstExample === "object" && firstExample.value !== undefined) {
89
+ return firstExample.value;
90
+ }
91
+ }
92
+
93
+ if (jsonContent.schema) {
94
+ return buildExampleFromSchema(jsonContent.schema);
95
+ }
96
+
97
+ return null;
98
+ }
99
+
100
+ function buildJourneyName(sequence, index) {
101
+ if (!Array.isArray(sequence) || sequence.length === 0) {
102
+ return `Journey ${index + 1}`;
103
+ }
104
+
105
+ if (sequence.length === 1) {
106
+ return `Journey ${index + 1}: ${sequence[0].operationId}`;
107
+ }
108
+
109
+ const first = sequence[0].operationId;
110
+ const last = sequence[sequence.length - 1].operationId;
111
+ return `Journey ${index + 1}: ${first} -> ${last}`;
112
+ }
113
+
114
+ function buildFlowDescription(operation) {
115
+ const lines = [];
116
+
117
+ if (operation.currentState) {
118
+ lines.push(`Current state: ${operation.currentState}`);
119
+ }
120
+
121
+ if (Array.isArray(operation.prerequisites) && operation.prerequisites.length > 0) {
122
+ lines.push(`Prerequisites: ${operation.prerequisites.join(", ")}`);
123
+ }
124
+
125
+ if (Array.isArray(operation.nextOperations) && operation.nextOperations.length > 0) {
126
+ const transitions = operation.nextOperations
127
+ .map((next) => {
128
+ const parts = [];
129
+ if (next.targetState) parts.push(`state ${next.targetState}`);
130
+ if (next.nextOperationId) parts.push(`op ${next.nextOperationId}`);
131
+ if (next.triggerType) parts.push(`trigger ${next.triggerType}`);
132
+ return parts.join(" | ");
133
+ })
134
+ .filter(Boolean);
135
+ if (transitions.length > 0) {
136
+ lines.push(`Next: ${transitions.join(" ; ")}`);
137
+ }
138
+ }
139
+
140
+ return lines.join("\n");
141
+ }
142
+
143
+ function createInsomniaRequest(requestId, parentId, operation, resource, rawOperation) {
144
+ const request = {
145
+ _id: requestId,
146
+ _type: "request",
147
+ parentId,
148
+ name: operation.operationId,
149
+ method: String(operation.httpMethod || "get").toUpperCase(),
150
+ url: `{{ base_url }}${pathToPostmanUrl(operation.path, resource.resourcePropertyName)}`,
151
+ headers: [],
152
+ body: {},
153
+ };
154
+
155
+ const description = buildFlowDescription(operation);
156
+ if (description) {
157
+ request.description = description;
158
+ }
159
+
160
+ if (["POST", "PUT", "PATCH"].includes(request.method)) {
161
+ request.headers.push({ name: "Content-Type", value: "application/json" });
162
+ const bodyExample = extractJsonRequestExample(rawOperation);
163
+ request.body = {
164
+ mimeType: "application/json",
165
+ text: JSON.stringify(bodyExample !== null ? bodyExample : {}, null, 2),
166
+ };
167
+ }
168
+
169
+ return request;
170
+ }
171
+
9
172
  function generateInsomniaWorkspace(options) {
10
173
  const apiPath = path.resolve(options.apiPath);
11
174
  const outputPath = path.resolve(options.outputPath || path.join(process.cwd(), "x-openapi-flow.insomnia.json"));
12
175
 
13
176
  const api = loadApi(apiPath);
14
177
  const model = buildIntermediateModel(api);
178
+ const operationMapById = getOperationMapById(api);
15
179
 
16
180
  const workspaceId = "wrk_x_openapi_flow";
181
+ const environmentId = "env_x_openapi_flow_base";
17
182
  const resources = [
18
183
  {
19
184
  _id: workspaceId,
@@ -22,6 +187,15 @@ function generateInsomniaWorkspace(options) {
22
187
  description: `Generated from ${apiPath}`,
23
188
  scope: "collection",
24
189
  },
190
+ {
191
+ _id: environmentId,
192
+ _type: "environment",
193
+ parentId: workspaceId,
194
+ name: "Base Environment",
195
+ data: {
196
+ base_url: "http://localhost:3000",
197
+ },
198
+ },
25
199
  ];
26
200
 
27
201
  for (const resource of model.resources) {
@@ -34,22 +208,44 @@ function generateInsomniaWorkspace(options) {
34
208
  });
35
209
 
36
210
  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: {},
211
+ if (sequences.length > 0) {
212
+ sequences.forEach((sequence, sequenceIndex) => {
213
+ const journeyId = `fld_${resource.resourcePropertyName}_journey_${sequenceIndex + 1}`;
214
+ resources.push({
215
+ _id: journeyId,
216
+ _type: "request_group",
217
+ parentId: groupId,
218
+ name: buildJourneyName(sequence, sequenceIndex),
219
+ });
220
+
221
+ sequence.forEach((operation, operationIndex) => {
222
+ const requestId = `req_${resource.resourcePropertyName}_${sequenceIndex + 1}_${operationIndex + 1}`;
223
+ resources.push(
224
+ createInsomniaRequest(
225
+ requestId,
226
+ journeyId,
227
+ operation,
228
+ resource,
229
+ operationMapById.get(operation.operationId)
230
+ )
231
+ );
232
+ });
51
233
  });
52
- });
234
+ } else {
235
+ const operations = resource.operations.filter((operation) => operation.hasFlow);
236
+ operations.forEach((operation, index) => {
237
+ const requestId = `req_${resource.resourcePropertyName}_${index + 1}`;
238
+ resources.push(
239
+ createInsomniaRequest(
240
+ requestId,
241
+ groupId,
242
+ operation,
243
+ resource,
244
+ operationMapById.get(operation.operationId)
245
+ )
246
+ );
247
+ });
248
+ }
53
249
  }
54
250
 
55
251
  const exportPayload = {
@@ -6,7 +6,167 @@ const { loadApi } = require("../../lib/validator");
6
6
  const { buildIntermediateModel } = require("../../lib/sdk-generator");
7
7
  const { toTitleCase, pathToPostmanUrl, buildLifecycleSequences } = require("../shared/helpers");
8
8
 
9
- function buildPostmanItem(operation, resource) {
9
+ function getOperationMapById(api) {
10
+ const map = new Map();
11
+ const paths = (api && api.paths) || {};
12
+
13
+ for (const [pathKey, pathItem] of Object.entries(paths)) {
14
+ for (const [method, operation] of Object.entries(pathItem || {})) {
15
+ if (!operation || typeof operation !== "object") continue;
16
+ if (!operation.operationId) continue;
17
+ map.set(operation.operationId, {
18
+ ...operation,
19
+ __path: pathKey,
20
+ __method: String(method || "get").toLowerCase(),
21
+ });
22
+ }
23
+ }
24
+
25
+ return map;
26
+ }
27
+
28
+ function buildExampleFromSchema(schema) {
29
+ if (!schema || typeof schema !== "object") return {};
30
+
31
+ if (Object.prototype.hasOwnProperty.call(schema, "example")) {
32
+ return schema.example;
33
+ }
34
+
35
+ if (Array.isArray(schema.enum) && schema.enum.length > 0) {
36
+ return schema.enum[0];
37
+ }
38
+
39
+ if (schema.default !== undefined) {
40
+ return schema.default;
41
+ }
42
+
43
+ const type = schema.type;
44
+ if (type === "string") {
45
+ if (schema.format === "date-time") return "2026-01-01T00:00:00Z";
46
+ if (schema.format === "date") return "2026-01-01";
47
+ if (schema.format === "email") return "user@example.com";
48
+ return "string";
49
+ }
50
+ if (type === "number" || type === "integer") return 0;
51
+ if (type === "boolean") return false;
52
+ if (type === "array") {
53
+ const itemExample = buildExampleFromSchema(schema.items || {});
54
+ return itemExample === undefined ? [] : [itemExample];
55
+ }
56
+
57
+ const properties = schema.properties || {};
58
+ const required = Array.isArray(schema.required) ? schema.required : Object.keys(properties);
59
+ const payload = {};
60
+ for (const key of required) {
61
+ if (!properties[key]) continue;
62
+ payload[key] = buildExampleFromSchema(properties[key]);
63
+ }
64
+
65
+ if (Object.keys(payload).length === 0) {
66
+ for (const [key, propertySchema] of Object.entries(properties)) {
67
+ payload[key] = buildExampleFromSchema(propertySchema);
68
+ }
69
+ }
70
+
71
+ return payload;
72
+ }
73
+
74
+ function extractJsonRequestExample(rawOperation) {
75
+ if (!rawOperation || !rawOperation.requestBody || !rawOperation.requestBody.content) {
76
+ return null;
77
+ }
78
+
79
+ const jsonContent = rawOperation.requestBody.content["application/json"];
80
+ if (!jsonContent) return null;
81
+
82
+ if (jsonContent.example !== undefined) {
83
+ return jsonContent.example;
84
+ }
85
+
86
+ if (jsonContent.examples && typeof jsonContent.examples === "object") {
87
+ const firstExample = Object.values(jsonContent.examples)[0];
88
+ if (firstExample && typeof firstExample === "object" && firstExample.value !== undefined) {
89
+ return firstExample.value;
90
+ }
91
+ }
92
+
93
+ if (jsonContent.schema) {
94
+ return buildExampleFromSchema(jsonContent.schema);
95
+ }
96
+
97
+ return null;
98
+ }
99
+
100
+ function extractResponseIdKeys(rawOperation) {
101
+ if (!rawOperation || !rawOperation.responses || typeof rawOperation.responses !== "object") {
102
+ return ["id"];
103
+ }
104
+
105
+ const keys = new Set(["id"]);
106
+ const successResponse = Object.entries(rawOperation.responses).find(([statusCode]) => /^2\d\d$/.test(String(statusCode)));
107
+ const response = successResponse ? successResponse[1] : null;
108
+ const schema = response
109
+ && response.content
110
+ && response.content["application/json"]
111
+ && response.content["application/json"].schema;
112
+
113
+ const properties = schema && schema.properties ? Object.keys(schema.properties) : [];
114
+ properties.forEach((key) => {
115
+ if (key === "id" || /_id$/i.test(key)) {
116
+ keys.add(key);
117
+ }
118
+ });
119
+
120
+ return [...keys];
121
+ }
122
+
123
+ function buildPrerequisiteRuleSets(resource) {
124
+ const incomingByTarget = new Map();
125
+
126
+ for (const sourceOperation of resource.operations || []) {
127
+ for (const nextOperation of sourceOperation.nextOperations || []) {
128
+ const target = nextOperation && nextOperation.nextOperationId;
129
+ if (!target) continue;
130
+
131
+ if (!incomingByTarget.has(target)) {
132
+ incomingByTarget.set(target, []);
133
+ }
134
+
135
+ const prereqSet = Array.from(new Set(Array.isArray(nextOperation.prerequisites) ? nextOperation.prerequisites : []));
136
+ incomingByTarget.get(target).push(prereqSet);
137
+ }
138
+ }
139
+
140
+ const dedupByTarget = new Map();
141
+ for (const [target, sets] of incomingByTarget.entries()) {
142
+ const unique = new Map();
143
+ for (const set of sets) {
144
+ const key = [...set].sort().join("|");
145
+ if (!unique.has(key)) {
146
+ unique.set(key, set);
147
+ }
148
+ }
149
+ dedupByTarget.set(target, [...unique.values()]);
150
+ }
151
+
152
+ return dedupByTarget;
153
+ }
154
+
155
+ function buildJourneyName(sequence, index) {
156
+ if (!Array.isArray(sequence) || sequence.length === 0) {
157
+ return `Journey ${index + 1}`;
158
+ }
159
+
160
+ if (sequence.length === 1) {
161
+ return `Journey ${index + 1}: ${sequence[0].operationId}`;
162
+ }
163
+
164
+ const first = sequence[0].operationId;
165
+ const last = sequence[sequence.length - 1].operationId;
166
+ return `Journey ${index + 1}: ${first} -> ${last}`;
167
+ }
168
+
169
+ function buildPostmanItem(operation, resource, rawOperation) {
10
170
  const rawPath = pathToPostmanUrl(operation.path, resource.resourcePropertyName);
11
171
  const urlRaw = `{{baseUrl}}${rawPath}`;
12
172
 
@@ -31,9 +191,10 @@ function buildPostmanItem(operation, resource) {
31
191
  };
32
192
 
33
193
  if (["POST", "PUT", "PATCH"].includes(item.request.method)) {
194
+ const bodyExample = extractJsonRequestExample(rawOperation);
34
195
  item.request.body = {
35
196
  mode: "raw",
36
- raw: "{}",
197
+ raw: JSON.stringify(bodyExample !== null ? bodyExample : {}, null, 2),
37
198
  options: { raw: { language: "json" } },
38
199
  };
39
200
  }
@@ -41,10 +202,11 @@ function buildPostmanItem(operation, resource) {
41
202
  return item;
42
203
  }
43
204
 
44
- function addPostmanScripts(item, operation, resource) {
45
- const prereqs = JSON.stringify(operation.prerequisites || []);
205
+ function addPostmanScripts(item, operation, resource, ruleSetsByOperation, responseIdKeysByOperation) {
206
+ const ruleSets = JSON.stringify((ruleSetsByOperation.get(operation.operationId) || []));
46
207
  const operationId = operation.operationId;
47
208
  const idCandidateKey = `${resource.resourcePropertyName}Id`;
209
+ const idCandidateFields = JSON.stringify(responseIdKeysByOperation.get(operationId) || ["id"]);
48
210
 
49
211
  item.event = [
50
212
  {
@@ -52,11 +214,14 @@ function addPostmanScripts(item, operation, resource) {
52
214
  script: {
53
215
  type: "text/javascript",
54
216
  exec: [
55
- `const required = ${prereqs};`,
217
+ `const ruleSets = ${ruleSets};`,
56
218
  "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(', '));`,
219
+ "if (ruleSets.length > 0) {",
220
+ " const isSatisfied = ruleSets.some((required) => required.every((operationId) => executed.includes(operationId)));",
221
+ " if (!isSatisfied) {",
222
+ " const expected = ruleSets.map((set) => set.join(' + ')).join(' OR ');",
223
+ ` throw new Error('Missing prerequisites for ${operationId}. Expected one of: ' + expected);`,
224
+ " }",
60
225
  "}",
61
226
  ],
62
227
  },
@@ -66,8 +231,11 @@ function addPostmanScripts(item, operation, resource) {
66
231
  script: {
67
232
  type: "text/javascript",
68
233
  exec: [
69
- "const payload = pm.response.json ? pm.response.json() : {};",
70
- `if (payload && payload.id) pm.collectionVariables.set('${idCandidateKey}', payload.id);`,
234
+ "let payload = {};",
235
+ "try { payload = pm.response.json(); } catch (_err) { payload = {}; }",
236
+ `const idFields = ${idCandidateFields};`,
237
+ "const discovered = idFields.find((field) => payload && payload[field] !== undefined && payload[field] !== null);",
238
+ `if (discovered) pm.collectionVariables.set('${idCandidateKey}', String(payload[discovered]));`,
71
239
  "const executed = JSON.parse(pm.collectionVariables.get('flowExecutedOps') || '[]');",
72
240
  `if (!executed.includes('${operationId}')) executed.push('${operationId}');`,
73
241
  "pm.collectionVariables.set('flowExecutedOps', JSON.stringify(executed));",
@@ -84,6 +252,7 @@ function generatePostmanCollection(options) {
84
252
 
85
253
  const api = loadApi(apiPath);
86
254
  const model = buildIntermediateModel(api);
255
+ const operationMapById = getOperationMapById(api);
87
256
 
88
257
  const collection = {
89
258
  info: {
@@ -100,6 +269,13 @@ function generatePostmanCollection(options) {
100
269
 
101
270
  for (const resource of model.resources) {
102
271
  const sequences = buildLifecycleSequences(resource);
272
+ const ruleSetsByOperation = buildPrerequisiteRuleSets(resource);
273
+ const responseIdKeysByOperation = new Map(
274
+ resource.operations.map((operation) => [
275
+ operation.operationId,
276
+ extractResponseIdKeys(operationMapById.get(operation.operationId)),
277
+ ])
278
+ );
103
279
  const folder = {
104
280
  name: `${toTitleCase(resource.resourcePlural || resource.resource)} Lifecycle`,
105
281
  item: [],
@@ -109,18 +285,22 @@ function generatePostmanCollection(options) {
109
285
  const fallbackItems = resource.operations
110
286
  .filter((operation) => operation.hasFlow)
111
287
  .map((operation) => {
112
- const item = buildPostmanItem(operation, resource);
113
- if (withScripts) addPostmanScripts(item, operation, resource);
288
+ const item = buildPostmanItem(operation, resource, operationMapById.get(operation.operationId));
289
+ if (withScripts) {
290
+ addPostmanScripts(item, operation, resource, ruleSetsByOperation, responseIdKeysByOperation);
291
+ }
114
292
  return item;
115
293
  });
116
294
  folder.item.push(...fallbackItems);
117
295
  } else {
118
296
  sequences.forEach((sequence, index) => {
119
297
  const journey = {
120
- name: `Journey ${index + 1}`,
298
+ name: buildJourneyName(sequence, index),
121
299
  item: sequence.map((operation) => {
122
- const item = buildPostmanItem(operation, resource);
123
- if (withScripts) addPostmanScripts(item, operation, resource);
300
+ const item = buildPostmanItem(operation, resource, operationMapById.get(operation.operationId));
301
+ if (withScripts) {
302
+ addPostmanScripts(item, operation, resource, ruleSetsByOperation, responseIdKeysByOperation);
303
+ }
124
304
  return item;
125
305
  }),
126
306
  };