x-openapi-flow 1.1.0 → 1.1.2
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 +23 -3
- package/bin/x-openapi-flow.js +380 -49
- package/examples/non-terminating-api.yaml +4 -4
- package/examples/order-api.yaml +6 -6
- package/examples/payment-api.yaml +3 -3
- package/examples/quality-warning-api.yaml +4 -4
- package/examples/swagger-ui/index.html +30 -0
- package/examples/swagger-ui/x-openapi-flow-plugin.js +46 -0
- package/examples/ticket-api.yaml +6 -6
- package/lib/validator.js +13 -13
- package/package.json +2 -2
- package/schema/flow-schema.json +4 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# x-openapi-flow
|
|
2
2
|
|
|
3
|
-
CLI and specification for validating the `x-flow` extension field in OpenAPI documents.
|
|
3
|
+
CLI and specification for validating the `x-openapi-flow` extension field in OpenAPI documents.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -20,11 +20,25 @@ x-openapi-flow doctor
|
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
22
|
x-openapi-flow validate <openapi-file> [--format pretty|json] [--profile core|relaxed|strict] [--strict-quality] [--config path]
|
|
23
|
-
x-openapi-flow init [
|
|
23
|
+
x-openapi-flow init [openapi-file] [--flows path]
|
|
24
|
+
x-openapi-flow apply [openapi-file] [--flows path] [--out path]
|
|
24
25
|
x-openapi-flow graph <openapi-file> [--format mermaid|json]
|
|
25
26
|
x-openapi-flow doctor [--config path]
|
|
26
27
|
```
|
|
27
28
|
|
|
29
|
+
`init` always works on an existing OpenAPI file in your repository.
|
|
30
|
+
`init` creates/synchronizes `x-openapi-flow.flows.yaml` as a persistent sidecar for your `x-openapi-flow` data.
|
|
31
|
+
Use `apply` to inject sidecar flows back into regenerated OpenAPI files.
|
|
32
|
+
If no OpenAPI/Swagger file exists yet, generate one first with your framework's official OpenAPI/Swagger tooling.
|
|
33
|
+
|
|
34
|
+
## Recommended Workflow
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
x-openapi-flow init openapi.yaml
|
|
38
|
+
# edit x-openapi-flow.flows.yaml
|
|
39
|
+
x-openapi-flow apply openapi.yaml
|
|
40
|
+
```
|
|
41
|
+
|
|
28
42
|
## Optional Configuration
|
|
29
43
|
|
|
30
44
|
Create `x-openapi-flow.config.json` in your project directory:
|
|
@@ -40,7 +54,13 @@ Create `x-openapi-flow.config.json` in your project directory:
|
|
|
40
54
|
## File Compatibility
|
|
41
55
|
|
|
42
56
|
- OpenAPI input in `.yaml`, `.yml`, and `.json`
|
|
43
|
-
- Validation processes OAS content with the `x-flow` extension
|
|
57
|
+
- Validation processes OAS content with the `x-openapi-flow` extension
|
|
58
|
+
|
|
59
|
+
## Swagger UI
|
|
60
|
+
|
|
61
|
+
- There is no Swagger UI-based automated test in this repo today (tests are CLI-only).
|
|
62
|
+
- For UI interpretation of `x-openapi-flow`, use `showExtensions: true` plus the example plugin at `examples/swagger-ui/x-openapi-flow-plugin.js`.
|
|
63
|
+
- A ready HTML example is available at `examples/swagger-ui/index.html`.
|
|
44
64
|
|
|
45
65
|
## Repository and Full Documentation
|
|
46
66
|
|
package/bin/x-openapi-flow.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
const fs = require("fs");
|
|
5
5
|
const path = require("path");
|
|
6
|
+
const yaml = require("js-yaml");
|
|
6
7
|
const {
|
|
7
8
|
run,
|
|
8
9
|
loadApi,
|
|
@@ -11,6 +12,7 @@ const {
|
|
|
11
12
|
} = require("../lib/validator");
|
|
12
13
|
|
|
13
14
|
const DEFAULT_CONFIG_NAME = "x-openapi-flow.config.json";
|
|
15
|
+
const DEFAULT_FLOWS_FILE = "x-openapi-flow.flows.yaml";
|
|
14
16
|
|
|
15
17
|
function resolveConfigPath(configPathArg) {
|
|
16
18
|
return configPathArg
|
|
@@ -42,7 +44,8 @@ function printHelp() {
|
|
|
42
44
|
|
|
43
45
|
Usage:
|
|
44
46
|
x-openapi-flow validate <openapi-file> [--format pretty|json] [--profile core|relaxed|strict] [--strict-quality] [--config path]
|
|
45
|
-
x-openapi-flow init [
|
|
47
|
+
x-openapi-flow init [openapi-file] [--flows path]
|
|
48
|
+
x-openapi-flow apply [openapi-file] [--flows path] [--out path]
|
|
46
49
|
x-openapi-flow graph <openapi-file> [--format mermaid|json]
|
|
47
50
|
x-openapi-flow doctor [--config path]
|
|
48
51
|
x-openapi-flow --help
|
|
@@ -51,7 +54,10 @@ Examples:
|
|
|
51
54
|
x-openapi-flow validate examples/order-api.yaml
|
|
52
55
|
x-openapi-flow validate examples/order-api.yaml --profile relaxed
|
|
53
56
|
x-openapi-flow validate examples/order-api.yaml --strict-quality
|
|
54
|
-
x-openapi-flow init
|
|
57
|
+
x-openapi-flow init openapi.yaml --flows x-openapi-flow.flows.yaml
|
|
58
|
+
x-openapi-flow init
|
|
59
|
+
x-openapi-flow apply openapi.yaml
|
|
60
|
+
x-openapi-flow apply openapi.yaml --out openapi.flow.yaml
|
|
55
61
|
x-openapi-flow graph examples/order-api.yaml
|
|
56
62
|
x-openapi-flow doctor
|
|
57
63
|
`);
|
|
@@ -163,21 +169,21 @@ function parseValidateArgs(args) {
|
|
|
163
169
|
}
|
|
164
170
|
|
|
165
171
|
function parseInitArgs(args) {
|
|
166
|
-
const unknown = findUnknownOptions(args, ["--
|
|
172
|
+
const unknown = findUnknownOptions(args, ["--flows"], []);
|
|
167
173
|
if (unknown) {
|
|
168
174
|
return { error: `Unknown option: ${unknown}` };
|
|
169
175
|
}
|
|
170
176
|
|
|
171
|
-
const
|
|
172
|
-
if (
|
|
173
|
-
return { error:
|
|
177
|
+
const flowsOpt = getOptionValue(args, "--flows");
|
|
178
|
+
if (flowsOpt.error) {
|
|
179
|
+
return { error: flowsOpt.error };
|
|
174
180
|
}
|
|
175
181
|
|
|
176
182
|
const positional = args.filter((token, index) => {
|
|
177
|
-
if (token === "--
|
|
183
|
+
if (token === "--flows") {
|
|
178
184
|
return false;
|
|
179
185
|
}
|
|
180
|
-
if (index > 0 && args[index - 1] === "--
|
|
186
|
+
if (index > 0 && args[index - 1] === "--flows") {
|
|
181
187
|
return false;
|
|
182
188
|
}
|
|
183
189
|
return !token.startsWith("--");
|
|
@@ -188,8 +194,45 @@ function parseInitArgs(args) {
|
|
|
188
194
|
}
|
|
189
195
|
|
|
190
196
|
return {
|
|
191
|
-
|
|
192
|
-
|
|
197
|
+
openApiFile: positional[0] ? path.resolve(positional[0]) : undefined,
|
|
198
|
+
flowsPath: flowsOpt.found ? path.resolve(flowsOpt.value) : undefined,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function parseApplyArgs(args) {
|
|
203
|
+
const unknown = findUnknownOptions(args, ["--flows", "--out"], []);
|
|
204
|
+
if (unknown) {
|
|
205
|
+
return { error: `Unknown option: ${unknown}` };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const flowsOpt = getOptionValue(args, "--flows");
|
|
209
|
+
if (flowsOpt.error) {
|
|
210
|
+
return { error: flowsOpt.error };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const outOpt = getOptionValue(args, "--out");
|
|
214
|
+
if (outOpt.error) {
|
|
215
|
+
return { error: outOpt.error };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const positional = args.filter((token, index) => {
|
|
219
|
+
if (token === "--flows" || token === "--out") {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
if (index > 0 && (args[index - 1] === "--flows" || args[index - 1] === "--out")) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
return !token.startsWith("--");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (positional.length > 1) {
|
|
229
|
+
return { error: `Unexpected argument: ${positional[1]}` };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
openApiFile: positional[0] ? path.resolve(positional[0]) : undefined,
|
|
234
|
+
flowsPath: flowsOpt.found ? path.resolve(flowsOpt.value) : undefined,
|
|
235
|
+
outPath: outOpt.found ? path.resolve(outOpt.value) : undefined,
|
|
193
236
|
};
|
|
194
237
|
}
|
|
195
238
|
|
|
@@ -274,6 +317,11 @@ function parseArgs(argv) {
|
|
|
274
317
|
return parsed.error ? parsed : { command, ...parsed };
|
|
275
318
|
}
|
|
276
319
|
|
|
320
|
+
if (command === "apply") {
|
|
321
|
+
const parsed = parseApplyArgs(commandArgs);
|
|
322
|
+
return parsed.error ? parsed : { command, ...parsed };
|
|
323
|
+
}
|
|
324
|
+
|
|
277
325
|
if (command === "doctor") {
|
|
278
326
|
const parsed = parseDoctorArgs(commandArgs);
|
|
279
327
|
return parsed.error ? parsed : { command, ...parsed };
|
|
@@ -282,51 +330,330 @@ function parseArgs(argv) {
|
|
|
282
330
|
return { error: `Unknown command: ${command}` };
|
|
283
331
|
}
|
|
284
332
|
|
|
285
|
-
function
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
333
|
+
function findOpenApiFile(startDirectory) {
|
|
334
|
+
const preferredNames = [
|
|
335
|
+
"openapi.yaml",
|
|
336
|
+
"openapi.yml",
|
|
337
|
+
"openapi.json",
|
|
338
|
+
"swagger.yaml",
|
|
339
|
+
"swagger.yml",
|
|
340
|
+
"swagger.json",
|
|
341
|
+
];
|
|
342
|
+
|
|
343
|
+
for (const fileName of preferredNames) {
|
|
344
|
+
const candidate = path.join(startDirectory, fileName);
|
|
345
|
+
if (fs.existsSync(candidate)) {
|
|
346
|
+
return candidate;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const ignoredDirs = new Set(["node_modules", ".git", "dist", "build"]);
|
|
351
|
+
|
|
352
|
+
function walk(directory) {
|
|
353
|
+
let entries = [];
|
|
354
|
+
try {
|
|
355
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
356
|
+
} catch (_err) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
for (const entry of entries) {
|
|
361
|
+
const fullPath = path.join(directory, entry.name);
|
|
362
|
+
|
|
363
|
+
if (entry.isDirectory()) {
|
|
364
|
+
if (!ignoredDirs.has(entry.name)) {
|
|
365
|
+
const nested = walk(fullPath);
|
|
366
|
+
if (nested) {
|
|
367
|
+
return nested;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (preferredNames.includes(entry.name)) {
|
|
374
|
+
return fullPath;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return walk(startDirectory);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function resolveFlowsPath(openApiFile, customFlowsPath) {
|
|
385
|
+
if (customFlowsPath) {
|
|
386
|
+
return customFlowsPath;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (openApiFile) {
|
|
390
|
+
return path.join(path.dirname(openApiFile), DEFAULT_FLOWS_FILE);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return path.resolve(process.cwd(), DEFAULT_FLOWS_FILE);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function getOpenApiFormat(filePath) {
|
|
397
|
+
return filePath.endsWith(".json") ? "json" : "yaml";
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function saveOpenApi(filePath, api) {
|
|
401
|
+
const format = getOpenApiFormat(filePath);
|
|
402
|
+
const content = format === "json"
|
|
403
|
+
? JSON.stringify(api, null, 2) + "\n"
|
|
404
|
+
: yaml.dump(api, { noRefs: true, lineWidth: -1 });
|
|
405
|
+
|
|
406
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
407
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function extractOperationEntries(api) {
|
|
411
|
+
const entries = [];
|
|
412
|
+
const paths = (api && api.paths) || {};
|
|
413
|
+
const httpMethods = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
|
|
414
|
+
|
|
415
|
+
for (const [pathKey, pathItem] of Object.entries(paths)) {
|
|
416
|
+
for (const method of httpMethods) {
|
|
417
|
+
const operation = pathItem[method];
|
|
418
|
+
if (!operation) {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const operationId = operation.operationId;
|
|
423
|
+
const key = operationId ? `operationId:${operationId}` : `${method.toUpperCase()} ${pathKey}`;
|
|
424
|
+
|
|
425
|
+
entries.push({
|
|
426
|
+
key,
|
|
427
|
+
operationId,
|
|
428
|
+
method,
|
|
429
|
+
path: pathKey,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return entries;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function readFlowsFile(flowsPath) {
|
|
438
|
+
if (!fs.existsSync(flowsPath)) {
|
|
439
|
+
return {
|
|
440
|
+
version: "1.0",
|
|
441
|
+
operations: [],
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const content = fs.readFileSync(flowsPath, "utf8");
|
|
446
|
+
const parsed = yaml.load(content);
|
|
447
|
+
|
|
448
|
+
if (!parsed || typeof parsed !== "object") {
|
|
449
|
+
return { version: "1.0", operations: [] };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
version: parsed.version || "1.0",
|
|
454
|
+
operations: Array.isArray(parsed.operations) ? parsed.operations : [],
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function writeFlowsFile(flowsPath, flowsDoc) {
|
|
459
|
+
fs.mkdirSync(path.dirname(flowsPath), { recursive: true });
|
|
460
|
+
const content = yaml.dump(flowsDoc, { noRefs: true, lineWidth: -1 });
|
|
461
|
+
fs.writeFileSync(flowsPath, content, "utf8");
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function buildOperationLookup(api) {
|
|
465
|
+
const lookupByKey = new Map();
|
|
466
|
+
const lookupByOperationId = new Map();
|
|
467
|
+
const entries = extractOperationEntries(api);
|
|
468
|
+
|
|
469
|
+
for (const entry of entries) {
|
|
470
|
+
lookupByKey.set(entry.key, entry);
|
|
471
|
+
if (entry.operationId) {
|
|
472
|
+
lookupByOperationId.set(entry.operationId, entry);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return { entries, lookupByKey, lookupByOperationId };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function mergeFlowsWithOpenApi(api, flowsDoc) {
|
|
480
|
+
const { entries, lookupByKey, lookupByOperationId } = buildOperationLookup(api);
|
|
481
|
+
|
|
482
|
+
const existingByKey = new Map();
|
|
483
|
+
for (const entry of flowsDoc.operations) {
|
|
484
|
+
const entryKey = entry.key || (entry.operationId ? `operationId:${entry.operationId}` : null);
|
|
485
|
+
if (entryKey) {
|
|
486
|
+
existingByKey.set(entryKey, entry);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const mergedOperations = [];
|
|
491
|
+
|
|
492
|
+
for (const op of entries) {
|
|
493
|
+
const existing = existingByKey.get(op.key);
|
|
494
|
+
if (existing) {
|
|
495
|
+
mergedOperations.push({
|
|
496
|
+
...existing,
|
|
497
|
+
key: op.key,
|
|
498
|
+
operationId: op.operationId,
|
|
499
|
+
method: op.method,
|
|
500
|
+
path: op.path,
|
|
501
|
+
missing_in_openapi: false,
|
|
502
|
+
});
|
|
503
|
+
} else {
|
|
504
|
+
mergedOperations.push({
|
|
505
|
+
key: op.key,
|
|
506
|
+
operationId: op.operationId,
|
|
507
|
+
method: op.method,
|
|
508
|
+
path: op.path,
|
|
509
|
+
"x-openapi-flow": null,
|
|
510
|
+
missing_in_openapi: false,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
for (const existing of flowsDoc.operations) {
|
|
516
|
+
const existingKey = existing.key || (existing.operationId ? `operationId:${existing.operationId}` : null);
|
|
517
|
+
const found = existingKey && lookupByKey.has(existingKey);
|
|
518
|
+
|
|
519
|
+
if (!found) {
|
|
520
|
+
mergedOperations.push({
|
|
521
|
+
...existing,
|
|
522
|
+
missing_in_openapi: true,
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return {
|
|
528
|
+
version: "1.0",
|
|
529
|
+
operations: mergedOperations,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function applyFlowsToOpenApi(api, flowsDoc) {
|
|
534
|
+
const paths = (api && api.paths) || {};
|
|
535
|
+
let appliedCount = 0;
|
|
536
|
+
const { lookupByKey, lookupByOperationId } = buildOperationLookup(api);
|
|
537
|
+
|
|
538
|
+
for (const flowEntry of flowsDoc.operations || []) {
|
|
539
|
+
if (!flowEntry || flowEntry.missing_in_openapi === true) {
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const flowValue = flowEntry["x-openapi-flow"];
|
|
544
|
+
if (!flowValue || typeof flowValue !== "object") {
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
let target = null;
|
|
549
|
+
if (flowEntry.operationId && lookupByOperationId.has(flowEntry.operationId)) {
|
|
550
|
+
target = lookupByOperationId.get(flowEntry.operationId);
|
|
551
|
+
} else if (flowEntry.key && lookupByKey.has(flowEntry.key)) {
|
|
552
|
+
target = lookupByKey.get(flowEntry.key);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (!target) {
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const pathItem = paths[target.path];
|
|
560
|
+
if (pathItem && pathItem[target.method]) {
|
|
561
|
+
pathItem[target.method]["x-openapi-flow"] = flowValue;
|
|
562
|
+
appliedCount += 1;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return appliedCount;
|
|
319
567
|
}
|
|
320
568
|
|
|
321
569
|
function runInit(parsed) {
|
|
322
|
-
|
|
323
|
-
|
|
570
|
+
const targetOpenApiFile = parsed.openApiFile || findOpenApiFile(process.cwd());
|
|
571
|
+
|
|
572
|
+
if (!targetOpenApiFile) {
|
|
573
|
+
console.error("ERROR: Could not find an existing OpenAPI file in this repository.");
|
|
574
|
+
console.error("Expected one of: openapi.yaml|yml|json, swagger.yaml|yml|json");
|
|
575
|
+
console.error("Create your OpenAPI/Swagger spec first (using your framework's official generator), then run init again.");
|
|
576
|
+
return 1;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const flowsPath = resolveFlowsPath(targetOpenApiFile, parsed.flowsPath);
|
|
580
|
+
|
|
581
|
+
let api;
|
|
582
|
+
try {
|
|
583
|
+
api = loadApi(targetOpenApiFile);
|
|
584
|
+
} catch (err) {
|
|
585
|
+
console.error(`ERROR: Could not parse OpenAPI file — ${err.message}`);
|
|
586
|
+
return 1;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
let flowsDoc;
|
|
590
|
+
try {
|
|
591
|
+
flowsDoc = readFlowsFile(flowsPath);
|
|
592
|
+
} catch (err) {
|
|
593
|
+
console.error(`ERROR: Could not parse flows file — ${err.message}`);
|
|
594
|
+
return 1;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const mergedFlows = mergeFlowsWithOpenApi(api, flowsDoc);
|
|
598
|
+
writeFlowsFile(flowsPath, mergedFlows);
|
|
599
|
+
const appliedCount = applyFlowsToOpenApi(api, mergedFlows);
|
|
600
|
+
saveOpenApi(targetOpenApiFile, api);
|
|
601
|
+
|
|
602
|
+
const trackedCount = mergedFlows.operations.filter((entry) => !entry.missing_in_openapi).length;
|
|
603
|
+
const orphanCount = mergedFlows.operations.filter((entry) => entry.missing_in_openapi).length;
|
|
604
|
+
|
|
605
|
+
console.log(`Using existing OpenAPI file: ${targetOpenApiFile}`);
|
|
606
|
+
console.log(`Flows sidecar synced: ${flowsPath}`);
|
|
607
|
+
console.log(`Tracked operations: ${trackedCount}`);
|
|
608
|
+
if (orphanCount > 0) {
|
|
609
|
+
console.log(`Orphan flow entries kept in sidecar: ${orphanCount}`);
|
|
610
|
+
}
|
|
611
|
+
console.log(`Applied x-openapi-flow entries to OpenAPI: ${appliedCount}`);
|
|
612
|
+
|
|
613
|
+
console.log(`Validate now: x-openapi-flow validate ${targetOpenApiFile}`);
|
|
614
|
+
return 0;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function runApply(parsed) {
|
|
618
|
+
const targetOpenApiFile = parsed.openApiFile || findOpenApiFile(process.cwd());
|
|
619
|
+
|
|
620
|
+
if (!targetOpenApiFile) {
|
|
621
|
+
console.error("ERROR: Could not find an existing OpenAPI file in this repository.");
|
|
622
|
+
console.error("Expected one of: openapi.yaml|yml|json, swagger.yaml|yml|json");
|
|
623
|
+
return 1;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const flowsPath = resolveFlowsPath(targetOpenApiFile, parsed.flowsPath);
|
|
627
|
+
if (!fs.existsSync(flowsPath)) {
|
|
628
|
+
console.error(`ERROR: Flows sidecar not found: ${flowsPath}`);
|
|
629
|
+
console.error("Run `x-openapi-flow init` first to create and sync the sidecar.");
|
|
630
|
+
return 1;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
let api;
|
|
634
|
+
let flowsDoc;
|
|
635
|
+
try {
|
|
636
|
+
api = loadApi(targetOpenApiFile);
|
|
637
|
+
} catch (err) {
|
|
638
|
+
console.error(`ERROR: Could not parse OpenAPI file — ${err.message}`);
|
|
324
639
|
return 1;
|
|
325
640
|
}
|
|
326
641
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
642
|
+
try {
|
|
643
|
+
flowsDoc = readFlowsFile(flowsPath);
|
|
644
|
+
} catch (err) {
|
|
645
|
+
console.error(`ERROR: Could not parse flows file — ${err.message}`);
|
|
646
|
+
return 1;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const appliedCount = applyFlowsToOpenApi(api, flowsDoc);
|
|
650
|
+
const outputPath = parsed.outPath || targetOpenApiFile;
|
|
651
|
+
saveOpenApi(outputPath, api);
|
|
652
|
+
|
|
653
|
+
console.log(`OpenAPI source: ${targetOpenApiFile}`);
|
|
654
|
+
console.log(`Flows sidecar: ${flowsPath}`);
|
|
655
|
+
console.log(`Applied x-openapi-flow entries: ${appliedCount}`);
|
|
656
|
+
console.log(`Output written to: ${outputPath}`);
|
|
330
657
|
return 0;
|
|
331
658
|
}
|
|
332
659
|
|
|
@@ -440,6 +767,10 @@ function main() {
|
|
|
440
767
|
process.exit(runGraph(parsed));
|
|
441
768
|
}
|
|
442
769
|
|
|
770
|
+
if (parsed.command === "apply") {
|
|
771
|
+
process.exit(runApply(parsed));
|
|
772
|
+
}
|
|
773
|
+
|
|
443
774
|
const config = loadConfig(parsed.configPath);
|
|
444
775
|
if (config.error) {
|
|
445
776
|
console.error(`ERROR: ${config.error}`);
|
|
@@ -10,7 +10,7 @@ paths:
|
|
|
10
10
|
post:
|
|
11
11
|
summary: Start flow
|
|
12
12
|
operationId: startFlow
|
|
13
|
-
x-flow:
|
|
13
|
+
x-openapi-flow:
|
|
14
14
|
version: "1.0"
|
|
15
15
|
id: start-flow
|
|
16
16
|
current_state: START
|
|
@@ -29,7 +29,7 @@ paths:
|
|
|
29
29
|
post:
|
|
30
30
|
summary: Loop A
|
|
31
31
|
operationId: loopA
|
|
32
|
-
x-flow:
|
|
32
|
+
x-openapi-flow:
|
|
33
33
|
version: "1.0"
|
|
34
34
|
id: loop-a-flow
|
|
35
35
|
current_state: LOOP_A
|
|
@@ -45,7 +45,7 @@ paths:
|
|
|
45
45
|
post:
|
|
46
46
|
summary: Loop B
|
|
47
47
|
operationId: loopB
|
|
48
|
-
x-flow:
|
|
48
|
+
x-openapi-flow:
|
|
49
49
|
version: "1.0"
|
|
50
50
|
id: loop-b-flow
|
|
51
51
|
current_state: LOOP_B
|
|
@@ -61,7 +61,7 @@ paths:
|
|
|
61
61
|
post:
|
|
62
62
|
summary: Done
|
|
63
63
|
operationId: doneFlow
|
|
64
|
-
x-flow:
|
|
64
|
+
x-openapi-flow:
|
|
65
65
|
version: "1.0"
|
|
66
66
|
id: done-flow
|
|
67
67
|
current_state: DONE
|
package/examples/order-api.yaml
CHANGED
|
@@ -3,14 +3,14 @@ info:
|
|
|
3
3
|
title: Order API
|
|
4
4
|
version: "1.0.0"
|
|
5
5
|
description: >
|
|
6
|
-
Example API demonstrating x-flow for order lifecycle orchestration.
|
|
6
|
+
Example API demonstrating x-openapi-flow for order lifecycle orchestration.
|
|
7
7
|
|
|
8
8
|
paths:
|
|
9
9
|
/orders:
|
|
10
10
|
post:
|
|
11
11
|
summary: Create a new order
|
|
12
12
|
operationId: createOrder
|
|
13
|
-
x-flow:
|
|
13
|
+
x-openapi-flow:
|
|
14
14
|
version: "1.0"
|
|
15
15
|
id: create-order-flow
|
|
16
16
|
current_state: CREATED
|
|
@@ -30,7 +30,7 @@ paths:
|
|
|
30
30
|
post:
|
|
31
31
|
summary: Confirm an order
|
|
32
32
|
operationId: confirmOrder
|
|
33
|
-
x-flow:
|
|
33
|
+
x-openapi-flow:
|
|
34
34
|
version: "1.0"
|
|
35
35
|
id: confirm-order-flow
|
|
36
36
|
current_state: CONFIRMED
|
|
@@ -53,7 +53,7 @@ paths:
|
|
|
53
53
|
post:
|
|
54
54
|
summary: Mark order as shipped
|
|
55
55
|
operationId: shipOrder
|
|
56
|
-
x-flow:
|
|
56
|
+
x-openapi-flow:
|
|
57
57
|
version: "1.0"
|
|
58
58
|
id: ship-order-flow
|
|
59
59
|
current_state: SHIPPED
|
|
@@ -76,7 +76,7 @@ paths:
|
|
|
76
76
|
post:
|
|
77
77
|
summary: Mark order as delivered
|
|
78
78
|
operationId: deliverOrder
|
|
79
|
-
x-flow:
|
|
79
|
+
x-openapi-flow:
|
|
80
80
|
version: "1.0"
|
|
81
81
|
id: deliver-order-flow
|
|
82
82
|
current_state: DELIVERED
|
|
@@ -95,7 +95,7 @@ paths:
|
|
|
95
95
|
post:
|
|
96
96
|
summary: Cancel an order
|
|
97
97
|
operationId: cancelOrder
|
|
98
|
-
x-flow:
|
|
98
|
+
x-openapi-flow:
|
|
99
99
|
version: "1.0"
|
|
100
100
|
id: cancel-order-flow
|
|
101
101
|
current_state: CANCELLED
|
|
@@ -3,14 +3,14 @@ info:
|
|
|
3
3
|
title: Payment API
|
|
4
4
|
version: "1.0.0"
|
|
5
5
|
description: >
|
|
6
|
-
Simple payment API demonstrating the x-flow extension for OpenAPI Spec.
|
|
6
|
+
Simple payment API demonstrating the x-openapi-flow extension for OpenAPI Spec.
|
|
7
7
|
|
|
8
8
|
paths:
|
|
9
9
|
/payments:
|
|
10
10
|
post:
|
|
11
11
|
summary: Create and authorize a payment
|
|
12
12
|
operationId: createPayment
|
|
13
|
-
x-flow:
|
|
13
|
+
x-openapi-flow:
|
|
14
14
|
version: "1.0"
|
|
15
15
|
id: create-payment-flow
|
|
16
16
|
current_state: AUTHORIZED
|
|
@@ -44,7 +44,7 @@ paths:
|
|
|
44
44
|
post:
|
|
45
45
|
summary: Capture an authorized payment
|
|
46
46
|
operationId: capturePayment
|
|
47
|
-
x-flow:
|
|
47
|
+
x-openapi-flow:
|
|
48
48
|
version: "1.0"
|
|
49
49
|
id: capture-payment-flow
|
|
50
50
|
current_state: CAPTURED
|
|
@@ -10,7 +10,7 @@ paths:
|
|
|
10
10
|
post:
|
|
11
11
|
summary: Start flow A
|
|
12
12
|
operationId: startA
|
|
13
|
-
x-flow:
|
|
13
|
+
x-openapi-flow:
|
|
14
14
|
version: "1.0"
|
|
15
15
|
id: start-a-flow
|
|
16
16
|
current_state: A_START
|
|
@@ -29,7 +29,7 @@ paths:
|
|
|
29
29
|
post:
|
|
30
30
|
summary: Complete flow A
|
|
31
31
|
operationId: doneA
|
|
32
|
-
x-flow:
|
|
32
|
+
x-openapi-flow:
|
|
33
33
|
version: "1.0"
|
|
34
34
|
id: done-a-flow
|
|
35
35
|
current_state: A_DONE
|
|
@@ -41,7 +41,7 @@ paths:
|
|
|
41
41
|
post:
|
|
42
42
|
summary: Start flow B
|
|
43
43
|
operationId: startB
|
|
44
|
-
x-flow:
|
|
44
|
+
x-openapi-flow:
|
|
45
45
|
version: "1.0"
|
|
46
46
|
id: start-b-flow
|
|
47
47
|
current_state: B_START
|
|
@@ -57,7 +57,7 @@ paths:
|
|
|
57
57
|
post:
|
|
58
58
|
summary: Complete flow B
|
|
59
59
|
operationId: doneB
|
|
60
|
-
x-flow:
|
|
60
|
+
x-openapi-flow:
|
|
61
61
|
version: "1.0"
|
|
62
62
|
id: done-b-flow
|
|
63
63
|
current_state: B_DONE
|
|
@@ -0,0 +1,30 @@
|
|
|
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 src="./x-openapi-flow-plugin.js"></script>
|
|
19
|
+
<script>
|
|
20
|
+
window.ui = SwaggerUIBundle({
|
|
21
|
+
url: "../payment-api.yaml",
|
|
22
|
+
dom_id: "#swagger-ui",
|
|
23
|
+
deepLinking: true,
|
|
24
|
+
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
|
|
25
|
+
plugins: [window.XOpenApiFlowPlugin],
|
|
26
|
+
showExtensions: true,
|
|
27
|
+
});
|
|
28
|
+
</script>
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
window.XOpenApiFlowPlugin = function () {
|
|
2
|
+
return {
|
|
3
|
+
wrapComponents: {
|
|
4
|
+
OperationSummary: (Original, system) => (props) => {
|
|
5
|
+
const operation = props.operation;
|
|
6
|
+
const flow = operation && operation.get && operation.get("x-openapi-flow");
|
|
7
|
+
|
|
8
|
+
if (!flow) {
|
|
9
|
+
return React.createElement(Original, props);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const flowObject = flow && flow.toJS ? flow.toJS() : flow;
|
|
13
|
+
const currentState = flowObject.current_state || "-";
|
|
14
|
+
const version = flowObject.version || "-";
|
|
15
|
+
|
|
16
|
+
return React.createElement(
|
|
17
|
+
"div",
|
|
18
|
+
null,
|
|
19
|
+
React.createElement(Original, props),
|
|
20
|
+
React.createElement(
|
|
21
|
+
"div",
|
|
22
|
+
{
|
|
23
|
+
style: {
|
|
24
|
+
marginTop: "8px",
|
|
25
|
+
padding: "8px 10px",
|
|
26
|
+
border: "1px solid #d9d9d9",
|
|
27
|
+
borderRadius: "6px",
|
|
28
|
+
background: "#fafafa",
|
|
29
|
+
fontSize: "12px",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
React.createElement("strong", null, "x-openapi-flow"),
|
|
33
|
+
React.createElement(
|
|
34
|
+
"div",
|
|
35
|
+
{ style: { marginTop: "4px" } },
|
|
36
|
+
"version: ",
|
|
37
|
+
version,
|
|
38
|
+
" | current_state: ",
|
|
39
|
+
currentState
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
};
|
package/examples/ticket-api.yaml
CHANGED
|
@@ -3,14 +3,14 @@ info:
|
|
|
3
3
|
title: Support Ticket API
|
|
4
4
|
version: "1.0.0"
|
|
5
5
|
description: >
|
|
6
|
-
Example API demonstrating x-flow for support ticket lifecycle.
|
|
6
|
+
Example API demonstrating x-openapi-flow for support ticket lifecycle.
|
|
7
7
|
|
|
8
8
|
paths:
|
|
9
9
|
/tickets:
|
|
10
10
|
post:
|
|
11
11
|
summary: Open a support ticket
|
|
12
12
|
operationId: openTicket
|
|
13
|
-
x-flow:
|
|
13
|
+
x-openapi-flow:
|
|
14
14
|
version: "1.0"
|
|
15
15
|
id: open-ticket-flow
|
|
16
16
|
current_state: OPEN
|
|
@@ -27,7 +27,7 @@ paths:
|
|
|
27
27
|
post:
|
|
28
28
|
summary: Start ticket handling
|
|
29
29
|
operationId: startTicket
|
|
30
|
-
x-flow:
|
|
30
|
+
x-openapi-flow:
|
|
31
31
|
version: "1.0"
|
|
32
32
|
id: start-ticket-flow
|
|
33
33
|
current_state: IN_PROGRESS
|
|
@@ -53,7 +53,7 @@ paths:
|
|
|
53
53
|
post:
|
|
54
54
|
summary: Wait for customer response
|
|
55
55
|
operationId: waitCustomer
|
|
56
|
-
x-flow:
|
|
56
|
+
x-openapi-flow:
|
|
57
57
|
version: "1.0"
|
|
58
58
|
id: wait-customer-flow
|
|
59
59
|
current_state: WAITING_CUSTOMER
|
|
@@ -76,7 +76,7 @@ paths:
|
|
|
76
76
|
post:
|
|
77
77
|
summary: Resolve ticket
|
|
78
78
|
operationId: resolveTicket
|
|
79
|
-
x-flow:
|
|
79
|
+
x-openapi-flow:
|
|
80
80
|
version: "1.0"
|
|
81
81
|
id: resolve-ticket-flow
|
|
82
82
|
current_state: RESOLVED
|
|
@@ -99,7 +99,7 @@ paths:
|
|
|
99
99
|
post:
|
|
100
100
|
summary: Close ticket
|
|
101
101
|
operationId: closeTicket
|
|
102
|
-
x-flow:
|
|
102
|
+
x-openapi-flow:
|
|
103
103
|
version: "1.0"
|
|
104
104
|
id: close-ticket-flow
|
|
105
105
|
current_state: CLOSED
|
package/lib/validator.js
CHANGED
|
@@ -38,7 +38,7 @@ function loadApi(filePath) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
* Extract every x-flow object found in the `paths` section of an OAS document.
|
|
41
|
+
* Extract every x-openapi-flow object found in the `paths` section of an OAS document.
|
|
42
42
|
* @param {object} api - Parsed OAS document.
|
|
43
43
|
* @returns {{ endpoint: string, flow: object }[]}
|
|
44
44
|
*/
|
|
@@ -59,10 +59,10 @@ function extractFlows(api) {
|
|
|
59
59
|
];
|
|
60
60
|
for (const method of HTTP_METHODS) {
|
|
61
61
|
const operation = pathItem[method];
|
|
62
|
-
if (operation && operation["x-flow"]) {
|
|
62
|
+
if (operation && operation["x-openapi-flow"]) {
|
|
63
63
|
entries.push({
|
|
64
64
|
endpoint: `${method.toUpperCase()} ${pathKey}`,
|
|
65
|
-
flow: operation["x-flow"],
|
|
65
|
+
flow: operation["x-openapi-flow"],
|
|
66
66
|
});
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -76,7 +76,7 @@ function extractFlows(api) {
|
|
|
76
76
|
// ---------------------------------------------------------------------------
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
|
-
* Validate all x-flow objects against the JSON Schema.
|
|
79
|
+
* Validate all x-openapi-flow objects against the JSON Schema.
|
|
80
80
|
* @param {{ endpoint: string, flow: object }[]} flows
|
|
81
81
|
* @returns {{ endpoint: string, errors: object[] }[]} Array of validation failures.
|
|
82
82
|
*/
|
|
@@ -105,23 +105,23 @@ function suggestFixes(errors = []) {
|
|
|
105
105
|
if (err.keyword === "required" && err.params && err.params.missingProperty) {
|
|
106
106
|
const missing = err.params.missingProperty;
|
|
107
107
|
if (missing === "version") {
|
|
108
|
-
suggestions.add("Add `version: \"1.0\"` to the x-flow object.");
|
|
108
|
+
suggestions.add("Add `version: \"1.0\"` to the x-openapi-flow object.");
|
|
109
109
|
} else if (missing === "id") {
|
|
110
|
-
suggestions.add("Add a unique `id` to the x-flow object.");
|
|
110
|
+
suggestions.add("Add a unique `id` to the x-openapi-flow object.");
|
|
111
111
|
} else if (missing === "current_state") {
|
|
112
112
|
suggestions.add("Add `current_state` to describe the operation state.");
|
|
113
113
|
} else {
|
|
114
|
-
suggestions.add(`Add required property \`${missing}\` in x-flow.`);
|
|
114
|
+
suggestions.add(`Add required property \`${missing}\` in x-openapi-flow.`);
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
if (err.keyword === "enum" && err.instancePath.endsWith("/version")) {
|
|
119
|
-
suggestions.add("Use supported x-flow version: `\"1.0\"`.");
|
|
119
|
+
suggestions.add("Use supported x-openapi-flow version: `\"1.0\"`.");
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
if (err.keyword === "additionalProperties" && err.params && err.params.additionalProperty) {
|
|
123
123
|
suggestions.add(
|
|
124
|
-
`Remove unsupported property \`${err.params.additionalProperty}\` from x-flow payload.`
|
|
124
|
+
`Remove unsupported property \`${err.params.additionalProperty}\` from x-openapi-flow payload.`
|
|
125
125
|
);
|
|
126
126
|
}
|
|
127
127
|
}
|
|
@@ -474,7 +474,7 @@ function run(apiPath, options = {}) {
|
|
|
474
474
|
return result;
|
|
475
475
|
}
|
|
476
476
|
|
|
477
|
-
// 2. Extract x-flow objects
|
|
477
|
+
// 2. Extract x-openapi-flow objects
|
|
478
478
|
const flows = extractFlows(api);
|
|
479
479
|
|
|
480
480
|
if (flows.length === 0) {
|
|
@@ -484,14 +484,14 @@ function run(apiPath, options = {}) {
|
|
|
484
484
|
if (output === "json") {
|
|
485
485
|
console.log(JSON.stringify(result, null, 2));
|
|
486
486
|
} else {
|
|
487
|
-
console.warn("WARNING: No x-flow extensions found in the API paths.");
|
|
487
|
+
console.warn("WARNING: No x-openapi-flow extensions found in the API paths.");
|
|
488
488
|
}
|
|
489
489
|
|
|
490
490
|
return result;
|
|
491
491
|
}
|
|
492
492
|
|
|
493
493
|
if (output === "pretty") {
|
|
494
|
-
console.log(`Found ${flows.length} x-flow definition(s).\n`);
|
|
494
|
+
console.log(`Found ${flows.length} x-openapi-flow definition(s).\n`);
|
|
495
495
|
}
|
|
496
496
|
|
|
497
497
|
let hasErrors = false;
|
|
@@ -504,7 +504,7 @@ function run(apiPath, options = {}) {
|
|
|
504
504
|
|
|
505
505
|
if (output === "pretty") {
|
|
506
506
|
if (schemaFailures.length === 0) {
|
|
507
|
-
console.log("✔ Schema validation passed for all x-flow definitions.");
|
|
507
|
+
console.log("✔ Schema validation passed for all x-openapi-flow definitions.");
|
|
508
508
|
} else {
|
|
509
509
|
console.error("✘ Schema validation FAILED:");
|
|
510
510
|
for (const { endpoint, errors } of schemaFailures) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "x-openapi-flow",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "OpenAPI extension for resource workflow and lifecycle management",
|
|
5
5
|
"main": "lib/validator.js",
|
|
6
6
|
"repository": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"openapi-flow",
|
|
40
40
|
"workflow",
|
|
41
41
|
"lifecycle",
|
|
42
|
-
"x-flow"
|
|
42
|
+
"x-openapi-flow"
|
|
43
43
|
],
|
|
44
44
|
"license": "MIT",
|
|
45
45
|
"dependencies": {
|
package/schema/flow-schema.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"$id": "https://x-flow.dev/schema/flow-schema.json",
|
|
4
|
-
"title": "x-flow",
|
|
5
|
-
"description": "JSON Schema for the x-flow OpenAPI extension, defining resource lifecycle states and transitions.",
|
|
3
|
+
"$id": "https://x-openapi-flow.dev/schema/flow-schema.json",
|
|
4
|
+
"title": "x-openapi-flow",
|
|
5
|
+
"description": "JSON Schema for the x-openapi-flow OpenAPI extension, defining resource lifecycle states and transitions.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": ["version", "id", "current_state"],
|
|
8
8
|
"properties": {
|
|
9
9
|
"version": {
|
|
10
10
|
"type": "string",
|
|
11
11
|
"enum": ["1.0"],
|
|
12
|
-
"description": "Version of the x-flow schema contract used by this definition."
|
|
12
|
+
"description": "Version of the x-openapi-flow schema contract used by this definition."
|
|
13
13
|
},
|
|
14
14
|
"id": {
|
|
15
15
|
"type": "string",
|