@prauga/flexdoc-backend 2.9.9 → 3.1.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.
- package/README.md +36 -179
- package/dist/auth.js.map +1 -1
- package/dist/contract-validation.d.ts +43 -0
- package/dist/contract-validation.js +120 -0
- package/dist/contract-validation.js.map +1 -0
- package/dist/contract-validation.test.d.ts +1 -0
- package/dist/contract-validation.test.js +112 -0
- package/dist/contract-validation.test.js.map +1 -0
- package/dist/flexdoc.module.js.map +1 -1
- package/dist/flexdoc.service.js.map +1 -1
- package/dist/framework-adapters.d.ts +2 -0
- package/dist/framework-adapters.js +21 -0
- package/dist/framework-adapters.js.map +1 -1
- package/dist/framework-adapters.test.js +32 -0
- package/dist/framework-adapters.test.js.map +1 -1
- package/dist/hono-adapter.d.ts +4 -0
- package/dist/hono-adapter.js +22 -0
- package/dist/hono-adapter.js.map +1 -1
- package/dist/hono-adapter.test.js +30 -6
- package/dist/hono-adapter.test.js.map +1 -1
- package/dist/host-execution-route.d.ts +9 -6
- package/dist/host-execution-route.js.map +1 -1
- package/dist/host-execution.d.ts +38 -42
- package/dist/host-execution.js +1 -1
- package/dist/host-execution.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/interfaces.d.ts +105 -105
- package/dist/page-cache.js.map +1 -1
- package/dist/renderer/flexdoc.standalone.css +1 -1
- package/dist/renderer/flexdoc.standalone.js +82 -72
- package/dist/renderer-assets.js.map +1 -1
- package/dist/runtime-intelligence-setup.test.d.ts +1 -0
- package/dist/runtime-intelligence-setup.test.js +51 -0
- package/dist/runtime-intelligence-setup.test.js.map +1 -0
- package/dist/runtime-intelligence.d.ts +66 -0
- package/dist/runtime-intelligence.js +270 -0
- package/dist/runtime-intelligence.js.map +1 -0
- package/dist/runtime-intelligence.test.d.ts +1 -0
- package/dist/runtime-intelligence.test.js +143 -0
- package/dist/runtime-intelligence.test.js.map +1 -0
- package/dist/setup.js +26 -0
- package/dist/setup.js.map +1 -1
- package/dist/template.d.ts +1 -0
- package/dist/template.js +3 -1
- package/dist/template.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,27 +1,9 @@
|
|
|
1
1
|
# FlexDoc Backend
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@prauga/flexdoc-backend)
|
|
4
|
-
[](https://www.gnu.org/licenses/agpl-3.0)
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
## Screenshots
|
|
9
|
-
|
|
10
|
-
### Light Mode
|
|
11
|
-
|
|
12
|
-

|
|
13
|
-
|
|
14
|
-
### Dark Mode
|
|
15
|
-
|
|
16
|
-

|
|
17
|
-
|
|
18
|
-
## Features
|
|
19
|
-
|
|
20
|
-
- **Modern UI**: Clean, responsive interface with dark mode support
|
|
21
|
-
- **Customizable**: Easily customize colors, typography, and layout
|
|
22
|
-
- **Interactive**: Test API endpoints directly from the documentation
|
|
23
|
-
- **Framework Agnostic**: Works with any JavaScript framework
|
|
24
|
-
- **OpenAPI Compatible**: Supports OpenAPI 3.0 specifications
|
|
6
|
+
Thin self-hosted integrations that mount the FlexDoc renderer, optional API-host execution routes, and runtime intelligence endpoints on Express, Fastify, NestJS, or Hono.
|
|
25
7
|
|
|
26
8
|
## Installation
|
|
27
9
|
|
|
@@ -31,197 +13,72 @@ npm install @prauga/flexdoc-backend
|
|
|
31
13
|
|
|
32
14
|
## Usage
|
|
33
15
|
|
|
34
|
-
###
|
|
16
|
+
### Express
|
|
35
17
|
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
// Create a new FlexDoc instance with your OpenAPI spec
|
|
41
|
-
const flexdoc = new FlexDoc({
|
|
42
|
-
spec: yourOpenAPISpec as OpenAPIObject,
|
|
43
|
-
title: 'My API Documentation',
|
|
44
|
-
description: 'Documentation for my awesome API',
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
// Generate HTML documentation
|
|
48
|
-
const html = flexdoc.generateHTML();
|
|
49
|
-
|
|
50
|
-
// Serve the documentation
|
|
51
|
-
app.get('/api/docs', (req, res) => {
|
|
52
|
-
res.send(html);
|
|
53
|
-
});
|
|
54
|
-
```
|
|
18
|
+
```javascript
|
|
19
|
+
const express = require('express');
|
|
20
|
+
const { setupExpressFlexDoc } = require('@prauga/flexdoc-backend');
|
|
21
|
+
const spec = require('./openapi.json');
|
|
55
22
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
FlexDoc is highly customizable through the `FlexDocOptions` interface:
|
|
59
|
-
|
|
60
|
-
```typescript
|
|
61
|
-
import { FlexDoc, FlexDocOptions } from '@prauga/flexdoc-backend';
|
|
62
|
-
|
|
63
|
-
const options: FlexDocOptions = {
|
|
64
|
-
// Required
|
|
65
|
-
spec: yourOpenAPISpec,
|
|
66
|
-
|
|
67
|
-
// Basic metadata
|
|
68
|
-
title: 'My API Documentation',
|
|
69
|
-
description: 'Documentation for my awesome API',
|
|
70
|
-
|
|
71
|
-
// Theme configuration
|
|
72
|
-
themeConfig: {
|
|
73
|
-
colors: {
|
|
74
|
-
primary: {
|
|
75
|
-
main: '#3b82f6',
|
|
76
|
-
light: '#eff6ff',
|
|
77
|
-
dark: '#2563eb',
|
|
78
|
-
},
|
|
79
|
-
// Additional color options...
|
|
80
|
-
},
|
|
81
|
-
typography: {
|
|
82
|
-
fontFamily: 'Inter, system-ui, sans-serif',
|
|
83
|
-
fontSize: '16px',
|
|
84
|
-
// Additional typography options...
|
|
85
|
-
},
|
|
86
|
-
},
|
|
23
|
+
const app = express();
|
|
87
24
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
url: '/terms',
|
|
95
|
-
icon: 'file-text', // Optional Lucide icon name
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
text: 'Privacy',
|
|
99
|
-
url: '/privacy',
|
|
100
|
-
},
|
|
101
|
-
],
|
|
25
|
+
setupExpressFlexDoc(app, '/docs', {
|
|
26
|
+
spec,
|
|
27
|
+
options: {
|
|
28
|
+
title: 'My API Documentation',
|
|
29
|
+
tryIt: { enabled: true },
|
|
30
|
+
runtimeIntelligence: true,
|
|
102
31
|
},
|
|
32
|
+
});
|
|
103
33
|
|
|
104
|
-
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
const flexdoc = new FlexDoc(options);
|
|
34
|
+
app.listen(3000);
|
|
108
35
|
```
|
|
109
36
|
|
|
110
|
-
###
|
|
111
|
-
|
|
112
|
-
#### NestJS
|
|
37
|
+
### NestJS
|
|
113
38
|
|
|
114
39
|
```typescript
|
|
115
40
|
import { NestFactory } from '@nestjs/core';
|
|
116
|
-
import {
|
|
117
|
-
import {
|
|
41
|
+
import { DocumentBuilder } from '@nestjs/swagger';
|
|
42
|
+
import { setupNestFlexDoc } from '@prauga/flexdoc-backend';
|
|
118
43
|
import { AppModule } from './app.module';
|
|
119
44
|
|
|
120
45
|
async function bootstrap() {
|
|
121
46
|
const app = await NestFactory.create(AppModule);
|
|
122
47
|
|
|
123
|
-
|
|
124
|
-
const config = new DocumentBuilder()
|
|
48
|
+
const openApiConfig = new DocumentBuilder()
|
|
125
49
|
.setTitle('My API')
|
|
126
|
-
.
|
|
127
|
-
.setVersion('1.0')
|
|
50
|
+
.setVersion('1.0.0')
|
|
128
51
|
.build();
|
|
129
|
-
const document = SwaggerModule.createDocument(app, config);
|
|
130
52
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
footer: {
|
|
137
|
-
copyright: '© 2025 My Company',
|
|
53
|
+
setupNestFlexDoc(app, '/docs', openApiConfig, {
|
|
54
|
+
options: {
|
|
55
|
+
title: 'My API Documentation',
|
|
56
|
+
tryIt: { enabled: true, hostExecution: true },
|
|
57
|
+
runtimeIntelligence: true,
|
|
138
58
|
},
|
|
139
59
|
});
|
|
140
60
|
|
|
141
|
-
// Serve FlexDoc at /api/docs
|
|
142
|
-
app.use('/api/docs', (req, res) => {
|
|
143
|
-
res.send(flexdoc.generateHTML());
|
|
144
|
-
});
|
|
145
|
-
|
|
146
61
|
await app.listen(3000);
|
|
147
62
|
}
|
|
148
63
|
bootstrap();
|
|
149
64
|
```
|
|
150
65
|
|
|
151
|
-
|
|
66
|
+
### Fastify and Hono
|
|
152
67
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
import swaggerJsdoc from 'swagger-jsdoc';
|
|
68
|
+
- `setupFastifyFlexDoc(app, path, options)` — static OpenAPI document
|
|
69
|
+
- `setupFastifySwaggerFlexDoc(app, path, options)` — document from `@fastify/swagger`
|
|
70
|
+
- `setupHonoFlexDoc(app, path, options)` — Hono without adding Hono as a dependency
|
|
157
71
|
|
|
158
|
-
|
|
72
|
+
Use `setupFlexDoc(app, path, options)` directly when you already have an Express-compatible `app.use` surface.
|
|
159
73
|
|
|
160
|
-
|
|
161
|
-
const options = {
|
|
162
|
-
definition: {
|
|
163
|
-
openapi: '3.0.0',
|
|
164
|
-
info: {
|
|
165
|
-
title: 'My API',
|
|
166
|
-
version: '1.0.0',
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
|
-
apis: ['./src/routes/*.js'],
|
|
170
|
-
};
|
|
171
|
-
const openapiSpec = swaggerJsdoc(options);
|
|
172
|
-
|
|
173
|
-
// Create FlexDoc instance
|
|
174
|
-
const flexdoc = new FlexDoc({
|
|
175
|
-
spec: openapiSpec,
|
|
176
|
-
title: 'My API Documentation',
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// Serve FlexDoc
|
|
180
|
-
app.get('/api/docs', (req, res) => {
|
|
181
|
-
res.send(flexdoc.generateHTML());
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
app.listen(3000);
|
|
185
|
-
```
|
|
74
|
+
## Configuration
|
|
186
75
|
|
|
187
|
-
|
|
76
|
+
`FlexDocModuleOptions` requires a mount `path` and either an inline `spec` or a remote `specUrl`. Renderer behavior is configured through `options`, which mirrors the client `FlexDocRendererOptions` contract (theme, Try It, code samples, footer, optional docs-route auth, host execution, and runtime intelligence).
|
|
188
77
|
|
|
189
|
-
|
|
78
|
+
## NestJS module
|
|
190
79
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
#### Constructor
|
|
194
|
-
|
|
195
|
-
```typescript
|
|
196
|
-
constructor(options: FlexDocOptions)
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
#### Methods
|
|
200
|
-
|
|
201
|
-
- `generateHTML()`: Generates the HTML documentation
|
|
202
|
-
- `getOpenAPISpec()`: Returns the processed OpenAPI specification
|
|
203
|
-
|
|
204
|
-
### `FlexDocOptions` Interface
|
|
205
|
-
|
|
206
|
-
Configuration options for FlexDoc:
|
|
207
|
-
|
|
208
|
-
| Property | Type | Description |
|
|
209
|
-
| -------------- | ------------------------------- | -------------------------------- |
|
|
210
|
-
| `spec` | `OpenAPIObject` | The OpenAPI specification object |
|
|
211
|
-
| `title` | `string` | Documentation title |
|
|
212
|
-
| `description` | `string` | Documentation description |
|
|
213
|
-
| `themeConfig` | `ThemeConfig` | Theme configuration |
|
|
214
|
-
| `footer` | `FooterConfig` | Footer configuration |
|
|
215
|
-
| `favicon` | `string` | URL to favicon |
|
|
216
|
-
| `customCss` | `string` | Custom CSS to inject |
|
|
217
|
-
| `customJs` | `string` | Custom JavaScript to inject |
|
|
218
|
-
| `defaultTheme` | `'light' \| 'dark' \| 'system'` | Default theme mode |
|
|
219
|
-
|
|
220
|
-
## Contributing
|
|
221
|
-
|
|
222
|
-
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
80
|
+
`FlexDocModule.forRoot` / `forRootAsync` registers the same routes through Nest's HTTP adapter. `FlexDocService.generateHTML` is available for programmatic HTML generation.
|
|
223
81
|
|
|
224
82
|
## License
|
|
225
83
|
|
|
226
|
-
|
|
227
|
-
|
|
84
|
+
AGPL-3.0-or-later
|
package/dist/auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type FlexDocContractValidationSeverity = 'error' | 'warning' | 'info';
|
|
2
|
+
export type FlexDocContractValidationCode = 'runtime.operation-undocumented' | 'runtime.operation-unobserved' | 'runtime.method-mismatch' | 'runtime.duplicate-operation';
|
|
3
|
+
export interface FlexDocContractRoute {
|
|
4
|
+
method: string;
|
|
5
|
+
path: string;
|
|
6
|
+
}
|
|
7
|
+
export interface FlexDocContractDuplicateRuntimeRoute extends FlexDocContractRoute {
|
|
8
|
+
count: number;
|
|
9
|
+
}
|
|
10
|
+
export interface FlexDocContractValidationLocation {
|
|
11
|
+
kind: 'operation';
|
|
12
|
+
path: string;
|
|
13
|
+
method?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface FlexDocContractValidationFinding {
|
|
16
|
+
id: string;
|
|
17
|
+
code: FlexDocContractValidationCode;
|
|
18
|
+
severity: FlexDocContractValidationSeverity;
|
|
19
|
+
location: FlexDocContractValidationLocation;
|
|
20
|
+
message: string;
|
|
21
|
+
expected?: string | string[];
|
|
22
|
+
observed?: string | string[];
|
|
23
|
+
}
|
|
24
|
+
export interface FlexDocContractValidationSummary {
|
|
25
|
+
total: number;
|
|
26
|
+
errors: number;
|
|
27
|
+
warnings: number;
|
|
28
|
+
info: number;
|
|
29
|
+
}
|
|
30
|
+
export type FlexDocContractValidationStatus = 'pass' | 'warn' | 'fail' | 'partial';
|
|
31
|
+
export interface FlexDocContractValidationResult {
|
|
32
|
+
status: FlexDocContractValidationStatus;
|
|
33
|
+
complete: boolean;
|
|
34
|
+
findings: FlexDocContractValidationFinding[];
|
|
35
|
+
summary: FlexDocContractValidationSummary;
|
|
36
|
+
}
|
|
37
|
+
export interface ValidateRuntimeContractOptions {
|
|
38
|
+
documentedRoutes: FlexDocContractRoute[];
|
|
39
|
+
runtimeRoutes: FlexDocContractRoute[];
|
|
40
|
+
duplicateRuntimeRoutes?: FlexDocContractDuplicateRuntimeRoute[];
|
|
41
|
+
discoveryComplete: boolean;
|
|
42
|
+
}
|
|
43
|
+
export declare function validateRuntimeContract(options: ValidateRuntimeContractOptions): FlexDocContractValidationResult;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateRuntimeContract = validateRuntimeContract;
|
|
4
|
+
function routeShape(path) {
|
|
5
|
+
return path.replace(/\{[^/{}]+\}/g, '{}');
|
|
6
|
+
}
|
|
7
|
+
function exactShapeKey(route) {
|
|
8
|
+
return `${route.method.toUpperCase()} ${routeShape(route.path)}`;
|
|
9
|
+
}
|
|
10
|
+
function findingId(code, path, method) {
|
|
11
|
+
return `${code}:${method ? `${method}:` : ''}${routeShape(path)}`;
|
|
12
|
+
}
|
|
13
|
+
function groupedMethods(routes) {
|
|
14
|
+
const grouped = new Map();
|
|
15
|
+
for (const route of routes) {
|
|
16
|
+
const shape = routeShape(route.path);
|
|
17
|
+
const methods = grouped.get(shape) || new Set();
|
|
18
|
+
methods.add(route.method.toUpperCase());
|
|
19
|
+
grouped.set(shape, methods);
|
|
20
|
+
}
|
|
21
|
+
return new Map([...grouped.entries()].map(([shape, methods]) => [shape, [...methods].sort()]));
|
|
22
|
+
}
|
|
23
|
+
function representativePath(routes, shape) {
|
|
24
|
+
return routes.find((route) => routeShape(route.path) === shape)?.path || shape;
|
|
25
|
+
}
|
|
26
|
+
function validateRuntimeContract(options) {
|
|
27
|
+
const documentedKeys = new Set(options.documentedRoutes.map(exactShapeKey));
|
|
28
|
+
const runtimeKeys = new Set(options.runtimeRoutes.map(exactShapeKey));
|
|
29
|
+
const findings = [];
|
|
30
|
+
for (const duplicate of options.duplicateRuntimeRoutes || []) {
|
|
31
|
+
const method = duplicate.method.toUpperCase();
|
|
32
|
+
findings.push({
|
|
33
|
+
id: findingId('runtime.duplicate-operation', duplicate.path, method),
|
|
34
|
+
code: 'runtime.duplicate-operation',
|
|
35
|
+
severity: 'warning',
|
|
36
|
+
location: { kind: 'operation', method, path: duplicate.path },
|
|
37
|
+
message: `Runtime registers ${method} ${duplicate.path} ${duplicate.count} times. Multiple host registrations can shadow or chain handlers behind one documented operation.`,
|
|
38
|
+
expected: 'One runtime registration for this HTTP operation',
|
|
39
|
+
observed: `${duplicate.count} runtime registrations`,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const documentedMethods = groupedMethods(options.documentedRoutes);
|
|
43
|
+
const runtimeMethods = groupedMethods(options.runtimeRoutes);
|
|
44
|
+
const methodMismatchShapes = new Set();
|
|
45
|
+
for (const [shape, expectedMethods] of documentedMethods) {
|
|
46
|
+
const observedMethods = runtimeMethods.get(shape);
|
|
47
|
+
if (!observedMethods)
|
|
48
|
+
continue;
|
|
49
|
+
const hasExact = expectedMethods.some((method) => observedMethods.includes(method));
|
|
50
|
+
if (hasExact)
|
|
51
|
+
continue;
|
|
52
|
+
methodMismatchShapes.add(shape);
|
|
53
|
+
const path = representativePath(options.documentedRoutes, shape);
|
|
54
|
+
findings.push({
|
|
55
|
+
id: findingId('runtime.method-mismatch', path),
|
|
56
|
+
code: 'runtime.method-mismatch',
|
|
57
|
+
severity: options.discoveryComplete ? 'error' : 'warning',
|
|
58
|
+
location: { kind: 'operation', method: expectedMethods[0], path },
|
|
59
|
+
message: `Runtime route ${path} is registered for different HTTP methods than OpenAPI documents.`,
|
|
60
|
+
expected: expectedMethods,
|
|
61
|
+
observed: observedMethods,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
for (const route of options.runtimeRoutes) {
|
|
65
|
+
const shape = routeShape(route.path);
|
|
66
|
+
if (methodMismatchShapes.has(shape))
|
|
67
|
+
continue;
|
|
68
|
+
if (documentedKeys.has(exactShapeKey(route)))
|
|
69
|
+
continue;
|
|
70
|
+
findings.push({
|
|
71
|
+
id: findingId('runtime.operation-undocumented', route.path, route.method),
|
|
72
|
+
code: 'runtime.operation-undocumented',
|
|
73
|
+
severity: 'warning',
|
|
74
|
+
location: { kind: 'operation', method: route.method.toUpperCase(), path: route.path },
|
|
75
|
+
message: `Runtime implements ${route.method.toUpperCase()} ${route.path}, but OpenAPI does not document that operation.`,
|
|
76
|
+
expected: 'Operation is represented in OpenAPI',
|
|
77
|
+
observed: 'Operation exists only in the running backend',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
for (const route of options.documentedRoutes) {
|
|
81
|
+
const shape = routeShape(route.path);
|
|
82
|
+
if (methodMismatchShapes.has(shape))
|
|
83
|
+
continue;
|
|
84
|
+
if (runtimeKeys.has(exactShapeKey(route)))
|
|
85
|
+
continue;
|
|
86
|
+
findings.push({
|
|
87
|
+
id: findingId('runtime.operation-unobserved', route.path, route.method),
|
|
88
|
+
code: 'runtime.operation-unobserved',
|
|
89
|
+
severity: options.discoveryComplete ? 'error' : 'info',
|
|
90
|
+
location: { kind: 'operation', method: route.method.toUpperCase(), path: route.path },
|
|
91
|
+
message: options.discoveryComplete
|
|
92
|
+
? `OpenAPI documents ${route.method.toUpperCase()} ${route.path}, but the running backend does not expose that operation.`
|
|
93
|
+
: `OpenAPI documents ${route.method.toUpperCase()} ${route.path}, but it was not observed during partial runtime discovery.`,
|
|
94
|
+
expected: 'Operation is exposed by the running backend',
|
|
95
|
+
observed: options.discoveryComplete ? 'No matching runtime operation exists' : 'No matching operation was observed during partial discovery',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const ordered = findings.sort((left, right) => {
|
|
99
|
+
const rank = { error: 0, warning: 1, info: 2 };
|
|
100
|
+
return rank[left.severity] - rank[right.severity]
|
|
101
|
+
|| left.location.path.localeCompare(right.location.path)
|
|
102
|
+
|| (left.location.method || '').localeCompare(right.location.method || '')
|
|
103
|
+
|| left.code.localeCompare(right.code);
|
|
104
|
+
});
|
|
105
|
+
const summary = {
|
|
106
|
+
total: ordered.length,
|
|
107
|
+
errors: ordered.filter((finding) => finding.severity === 'error').length,
|
|
108
|
+
warnings: ordered.filter((finding) => finding.severity === 'warning').length,
|
|
109
|
+
info: ordered.filter((finding) => finding.severity === 'info').length,
|
|
110
|
+
};
|
|
111
|
+
const status = summary.errors > 0
|
|
112
|
+
? 'fail'
|
|
113
|
+
: summary.warnings > 0
|
|
114
|
+
? 'warn'
|
|
115
|
+
: !options.discoveryComplete
|
|
116
|
+
? 'partial'
|
|
117
|
+
: 'pass';
|
|
118
|
+
return { status, complete: options.discoveryComplete, findings: ordered, summary };
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=contract-validation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract-validation.js","sourceRoot":"","sources":["../src/contract-validation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const contract_validation_1 = require("./contract-validation");
|
|
4
|
+
describe('runtime contract validation', () => {
|
|
5
|
+
it('passes when runtime and OpenAPI expose the same operations', () => {
|
|
6
|
+
expect((0, contract_validation_1.validateRuntimeContract)({
|
|
7
|
+
documentedRoutes: [
|
|
8
|
+
{ method: 'GET', path: '/pets' },
|
|
9
|
+
{ method: 'GET', path: '/pets/{petId}' },
|
|
10
|
+
],
|
|
11
|
+
runtimeRoutes: [
|
|
12
|
+
{ method: 'GET', path: '/pets' },
|
|
13
|
+
{ method: 'GET', path: '/pets/{id}' },
|
|
14
|
+
],
|
|
15
|
+
discoveryComplete: true,
|
|
16
|
+
})).toEqual({
|
|
17
|
+
status: 'pass',
|
|
18
|
+
complete: true,
|
|
19
|
+
findings: [],
|
|
20
|
+
summary: { total: 0, errors: 0, warnings: 0, info: 0 },
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
it('reports one navigable method mismatch instead of duplicate presence findings', () => {
|
|
24
|
+
const result = (0, contract_validation_1.validateRuntimeContract)({
|
|
25
|
+
documentedRoutes: [{ method: 'POST', path: '/pets/{petId}' }],
|
|
26
|
+
runtimeRoutes: [{ method: 'GET', path: '/pets/{id}' }],
|
|
27
|
+
discoveryComplete: true,
|
|
28
|
+
});
|
|
29
|
+
expect(result.status).toBe('fail');
|
|
30
|
+
expect(result.summary).toEqual({ total: 1, errors: 1, warnings: 0, info: 0 });
|
|
31
|
+
expect(result.findings).toEqual([expect.objectContaining({
|
|
32
|
+
code: 'runtime.method-mismatch',
|
|
33
|
+
severity: 'error',
|
|
34
|
+
location: { kind: 'operation', method: 'POST', path: '/pets/{petId}' },
|
|
35
|
+
expected: ['POST'],
|
|
36
|
+
observed: ['GET'],
|
|
37
|
+
})]);
|
|
38
|
+
});
|
|
39
|
+
it('uses a deterministic expected method when a path documents multiple methods', () => {
|
|
40
|
+
const result = (0, contract_validation_1.validateRuntimeContract)({
|
|
41
|
+
documentedRoutes: [
|
|
42
|
+
{ method: 'POST', path: '/pets/{petId}' },
|
|
43
|
+
{ method: 'GET', path: '/pets/{petId}' },
|
|
44
|
+
],
|
|
45
|
+
runtimeRoutes: [{ method: 'DELETE', path: '/pets/{id}' }],
|
|
46
|
+
discoveryComplete: true,
|
|
47
|
+
});
|
|
48
|
+
expect(result.findings[0]).toEqual(expect.objectContaining({
|
|
49
|
+
code: 'runtime.method-mismatch',
|
|
50
|
+
location: { kind: 'operation', method: 'GET', path: '/pets/{petId}' },
|
|
51
|
+
expected: ['GET', 'POST'],
|
|
52
|
+
observed: ['DELETE'],
|
|
53
|
+
}));
|
|
54
|
+
});
|
|
55
|
+
it('reports undocumented runtime operations as warnings', () => {
|
|
56
|
+
const result = (0, contract_validation_1.validateRuntimeContract)({
|
|
57
|
+
documentedRoutes: [{ method: 'GET', path: '/pets' }],
|
|
58
|
+
runtimeRoutes: [
|
|
59
|
+
{ method: 'GET', path: '/pets' },
|
|
60
|
+
{ method: 'POST', path: '/internal/reindex' },
|
|
61
|
+
],
|
|
62
|
+
discoveryComplete: true,
|
|
63
|
+
});
|
|
64
|
+
expect(result.status).toBe('warn');
|
|
65
|
+
expect(result.findings).toEqual([expect.objectContaining({
|
|
66
|
+
code: 'runtime.operation-undocumented',
|
|
67
|
+
severity: 'warning',
|
|
68
|
+
location: { kind: 'operation', method: 'POST', path: '/internal/reindex' },
|
|
69
|
+
})]);
|
|
70
|
+
});
|
|
71
|
+
it('reports duplicate host registrations that standalone OpenAPI tooling cannot observe', () => {
|
|
72
|
+
const result = (0, contract_validation_1.validateRuntimeContract)({
|
|
73
|
+
documentedRoutes: [{ method: 'GET', path: '/pets/{petId}' }],
|
|
74
|
+
runtimeRoutes: [{ method: 'GET', path: '/pets/{id}' }],
|
|
75
|
+
duplicateRuntimeRoutes: [{ method: 'GET', path: '/pets/{id}', count: 2 }],
|
|
76
|
+
discoveryComplete: true,
|
|
77
|
+
});
|
|
78
|
+
expect(result.status).toBe('warn');
|
|
79
|
+
expect(result.summary).toEqual({ total: 1, errors: 0, warnings: 1, info: 0 });
|
|
80
|
+
expect(result.findings[0]).toEqual(expect.objectContaining({
|
|
81
|
+
code: 'runtime.duplicate-operation',
|
|
82
|
+
severity: 'warning',
|
|
83
|
+
location: { kind: 'operation', method: 'GET', path: '/pets/{id}' },
|
|
84
|
+
expected: 'One runtime registration for this HTTP operation',
|
|
85
|
+
observed: '2 runtime registrations',
|
|
86
|
+
}));
|
|
87
|
+
});
|
|
88
|
+
it('treats documented absence as an error only when discovery is complete', () => {
|
|
89
|
+
const complete = (0, contract_validation_1.validateRuntimeContract)({
|
|
90
|
+
documentedRoutes: [{ method: 'GET', path: '/pets/{petId}' }],
|
|
91
|
+
runtimeRoutes: [],
|
|
92
|
+
discoveryComplete: true,
|
|
93
|
+
});
|
|
94
|
+
expect(complete.status).toBe('fail');
|
|
95
|
+
expect(complete.findings[0]).toEqual(expect.objectContaining({
|
|
96
|
+
code: 'runtime.operation-unobserved',
|
|
97
|
+
severity: 'error',
|
|
98
|
+
}));
|
|
99
|
+
const partial = (0, contract_validation_1.validateRuntimeContract)({
|
|
100
|
+
documentedRoutes: [{ method: 'GET', path: '/pets/{petId}' }],
|
|
101
|
+
runtimeRoutes: [],
|
|
102
|
+
discoveryComplete: false,
|
|
103
|
+
});
|
|
104
|
+
expect(partial.status).toBe('partial');
|
|
105
|
+
expect(partial.complete).toBe(false);
|
|
106
|
+
expect(partial.findings[0]).toEqual(expect.objectContaining({
|
|
107
|
+
code: 'runtime.operation-unobserved',
|
|
108
|
+
severity: 'info',
|
|
109
|
+
}));
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
//# sourceMappingURL=contract-validation.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract-validation.test.js","sourceRoot":"","sources":["../src/contract-validation.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flexdoc.module.js","sourceRoot":"","sources":["../src/flexdoc.module.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,2CAMwB;AACxB,uCAA+C;AAC/C,mCAAuC;AACvC,uDAAmD;
|
|
1
|
+
{"version":3,"file":"flexdoc.module.js","sourceRoot":"","sources":["../src/flexdoc.module.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,2CAMwB;AACxB,uCAA+C;AAC/C,mCAAuC;AACvC,uDAAmD;AAK5C,IAAM,aAAa,qBAAnB,MAAM,aAAa;IAMxB,YAC8C,OAA6B,EACxD,eAAiC;QADN,YAAO,GAAP,OAAO,CAAsB;QACxD,oBAAe,GAAf,eAAe,CAAkB;IACjD,CAAC;IAGJ,YAAY;QACV,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC;YAC1B,OAAO,CAAC,IAAI,CACV,uEAAuE,CACxE,CAAC;YACF,OAAO;QACT,CAAC;QAGD,MAAM,WAAW,GAAG,IAAI,CAAC,eAAe,CAAC,WAAW,CAAC;QACrD,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,OAAO,CAAC,IAAI,CACV,oEAAoE,CACrE,CAAC;YACF,OAAO;QACT,CAAC;QAGD,MAAM,GAAG,GAAG,WAAW,CAAC,WAAW,EAAE,CAAC;QAGtC,IAAA,oBAAY,EAAC,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE;YACnC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI;YACvB,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,OAAO;YAC7B,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,OAAO;SAC9B,CAAC,CAAC;IACL,CAAC;IAOD,MAAM,CAAC,OAAO,CAAC,OAA6B;QAC1C,OAAO;YACL,MAAM,EAAE,eAAa;YACrB,SAAS,EAAE;gBACT,gCAAc;gBACd;oBACE,OAAO,EAAE,iBAAiB;oBAC1B,QAAQ,EAAE,OAAO;iBAClB;aACF;YACD,OAAO,EAAE,CAAC,gCAAc,CAAC;SAC1B,CAAC;IACJ,CAAC;IAOD,MAAM,CAAC,YAAY,CAAC,OAOnB;QACC,OAAO;YACL,MAAM,EAAE,eAAa;YACrB,SAAS,EAAE;gBACT,gCAAc;gBACd;oBACE,OAAO,EAAE,iBAAiB;oBAC1B,UAAU,EAAE,OAAO,CAAC,UAAU;oBAC9B,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,EAAE;iBAC7B;aACF;YACD,OAAO,EAAE,CAAC,gCAAc,CAAC;SAC1B,CAAC;IACJ,CAAC;CACF,CAAA;AArFY,sCAAa;wBAAb,aAAa;IADzB,IAAA,eAAM,EAAC,EAAE,CAAC;IAQN,WAAA,IAAA,eAAM,EAAC,iBAAiB,CAAC,CAAA;6CACS,sBAAe;GARzC,aAAa,CAqFzB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flexdoc.service.js","sourceRoot":"","sources":["../src/flexdoc.service.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAA4C;AAE5C,yCAAiD;
|
|
1
|
+
{"version":3,"file":"flexdoc.service.js","sourceRoot":"","sources":["../src/flexdoc.service.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAA4C;AAE5C,yCAAiD;AAI1C,IAAM,cAAc,GAApB,MAAM,cAAc;IAOzB,YAAY,CAAC,IAAY,EAAE,UAA0B,EAAE;QACrD,OAAO,IAAA,8BAAmB,EAAC,IAA+B,EAAE,OAAO,CAAC,CAAC;IACvE,CAAC;IAQD,mBAAmB,CAAC,OAAe,EAAE,UAA0B,EAAE;QAC/D,OAAO,IAAA,8BAAmB,EAAC,IAAI,EAAE,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;IAC5D,CAAC;CACF,CAAA;AApBY,wCAAc;yBAAd,cAAc;IAD1B,IAAA,mBAAU,GAAE;GACA,cAAc,CAoB1B"}
|
|
@@ -28,6 +28,8 @@ export interface FastifyLikeApplication {
|
|
|
28
28
|
hasContentTypeParser?: (contentType: string) => boolean;
|
|
29
29
|
ready?: () => Promise<unknown>;
|
|
30
30
|
swagger?: () => Record<string, unknown>;
|
|
31
|
+
printRoutes?: (options?: Record<string, unknown>) => string;
|
|
32
|
+
version?: string;
|
|
31
33
|
}
|
|
32
34
|
export interface NestLikeApplication {
|
|
33
35
|
getHttpAdapter(): {
|
|
@@ -45,6 +45,7 @@ const template_1 = require("./template");
|
|
|
45
45
|
const host_execution_1 = require("./host-execution");
|
|
46
46
|
const host_execution_route_1 = require("./host-execution-route");
|
|
47
47
|
const page_cache_1 = require("./page-cache");
|
|
48
|
+
const runtime_intelligence_1 = require("./runtime-intelligence");
|
|
48
49
|
function setupExpressFlexDoc(app, path, options) {
|
|
49
50
|
(0, setup_1.setupFlexDoc)(app, path, options);
|
|
50
51
|
}
|
|
@@ -108,6 +109,8 @@ function setupFastifyFlexDocInternal(app, path, options, specProvider) {
|
|
|
108
109
|
const auth = options.options?.auth;
|
|
109
110
|
const hostExecutionState = (0, host_execution_1.createHostExecutionState)(options.options?.tryIt?.hostExecution);
|
|
110
111
|
const hostRouteAvailable = hostExecutionState.enabled && typeof app.post === 'function';
|
|
112
|
+
const runtimeEnabled = (0, runtime_intelligence_1.runtimeIntelligenceEnabled)(options.options?.runtimeIntelligence);
|
|
113
|
+
const runtimeEndpoint = `${rendererBasePath}/runtime`;
|
|
111
114
|
let remoteSpecPromise = null;
|
|
112
115
|
let generatedSpecPromise = null;
|
|
113
116
|
const routeOptions = auth ? {
|
|
@@ -145,6 +148,7 @@ function setupFastifyFlexDocInternal(app, path, options, specProvider) {
|
|
|
145
148
|
rendererBasePath,
|
|
146
149
|
rendererVersion: assets.version,
|
|
147
150
|
hostExecutionPublic: hostRouteAvailable ? (0, host_execution_1.publicHostExecutionOptions)(hostExecutionState, rendererBasePath) : undefined,
|
|
151
|
+
runtimeIntelligencePublic: runtimeEnabled ? { available: true, endpoint: runtimeEndpoint, framework: 'fastify' } : undefined,
|
|
148
152
|
});
|
|
149
153
|
});
|
|
150
154
|
const sendHostResult = (reply, result) => {
|
|
@@ -153,6 +157,23 @@ function setupFastifyFlexDocInternal(app, path, options, specProvider) {
|
|
|
153
157
|
target = target.header(name, value);
|
|
154
158
|
return target.send(result.body);
|
|
155
159
|
};
|
|
160
|
+
if (runtimeEnabled) {
|
|
161
|
+
app.get(runtimeEndpoint, routeOptions, async (request, reply) => {
|
|
162
|
+
const serverOrigin = (0, host_execution_route_1.hostExecutionRequestOrigin)({
|
|
163
|
+
headers: request.headers,
|
|
164
|
+
protocol: request.protocol || (request.raw?.socket?.encrypted ? 'https' : 'http'),
|
|
165
|
+
});
|
|
166
|
+
const snapshot = (0, runtime_intelligence_1.buildRuntimeIntelligenceSnapshot)({
|
|
167
|
+
spec: await resolvedSpec(),
|
|
168
|
+
discovery: await (0, runtime_intelligence_1.discoverFastifyRoutes)(app, normalizedPath),
|
|
169
|
+
serverOrigin,
|
|
170
|
+
});
|
|
171
|
+
return reply
|
|
172
|
+
.type('application/json; charset=utf-8')
|
|
173
|
+
.header('Cache-Control', 'no-store')
|
|
174
|
+
.send(JSON.stringify(snapshot));
|
|
175
|
+
});
|
|
176
|
+
}
|
|
156
177
|
if (hostRouteAvailable) {
|
|
157
178
|
if (app.addContentTypeParser && !app.hasContentTypeParser?.('multipart/form-data')) {
|
|
158
179
|
app.addContentTypeParser('multipart/form-data', { parseAs: 'buffer' }, (_request, body, done) => done(null, body));
|