te.js 1.3.1 → 2.0.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/.cursor/plans/ai_native_framework_features_5bb1a20a.plan.md +234 -0
- package/.cursor/plans/auto_error_fix_agent_e68979c5.plan.md +356 -0
- package/.cursor/plans/tejas_framework_test_suite_5e3c6fad.plan.md +168 -0
- package/.prettierignore +31 -0
- package/README.md +156 -14
- package/auto-docs/analysis/handler-analyzer.js +58 -0
- package/auto-docs/analysis/source-resolver.js +101 -0
- package/auto-docs/constants.js +37 -0
- package/auto-docs/index.js +146 -0
- package/auto-docs/llm/index.js +6 -0
- package/auto-docs/llm/parse.js +88 -0
- package/auto-docs/llm/prompts.js +222 -0
- package/auto-docs/llm/provider.js +187 -0
- package/auto-docs/openapi/endpoint-processor.js +277 -0
- package/auto-docs/openapi/generator.js +107 -0
- package/auto-docs/openapi/level3.js +131 -0
- package/auto-docs/openapi/spec-builders.js +244 -0
- package/auto-docs/ui/docs-ui.js +186 -0
- package/auto-docs/utils/logger.js +17 -0
- package/auto-docs/utils/strip-usage.js +10 -0
- package/cli/docs-command.js +315 -0
- package/cli/fly-command.js +71 -0
- package/cli/index.js +57 -0
- package/database/index.js +163 -5
- package/database/mongodb.js +146 -0
- package/database/redis.js +201 -0
- package/docs/README.md +36 -0
- package/docs/ammo.md +362 -0
- package/docs/api-reference.md +489 -0
- package/docs/auto-docs.md +215 -0
- package/docs/cli.md +152 -0
- package/docs/configuration.md +233 -0
- package/docs/database.md +391 -0
- package/docs/error-handling.md +417 -0
- package/docs/file-uploads.md +334 -0
- package/docs/getting-started.md +181 -0
- package/docs/middleware.md +356 -0
- package/docs/rate-limiting.md +394 -0
- package/docs/routing.md +302 -0
- package/example/API_OVERVIEW.md +77 -0
- package/example/README.md +155 -0
- package/example/index.js +27 -2
- package/example/openapi.json +390 -0
- package/example/package.json +5 -2
- package/example/services/cache.service.js +25 -0
- package/example/services/user.service.js +42 -0
- package/example/start-redis.js +2 -0
- package/example/targets/cache.target.js +35 -0
- package/example/targets/index.target.js +11 -2
- package/example/targets/users.target.js +60 -0
- package/example/tejas.config.json +13 -1
- package/package.json +20 -5
- package/rate-limit/algorithms/fixed-window.js +141 -0
- package/rate-limit/algorithms/sliding-window.js +147 -0
- package/rate-limit/algorithms/token-bucket.js +115 -0
- package/rate-limit/base.js +165 -0
- package/rate-limit/index.js +147 -0
- package/rate-limit/storage/base.js +104 -0
- package/rate-limit/storage/memory.js +102 -0
- package/rate-limit/storage/redis.js +88 -0
- package/server/ammo/body-parser.js +152 -25
- package/server/ammo/enhancer.js +6 -2
- package/server/ammo.js +356 -327
- package/server/endpoint.js +21 -0
- package/server/handler.js +113 -87
- package/server/target.js +50 -9
- package/server/targets/registry.js +160 -57
- package/te.js +363 -137
- package/tests/auto-docs/handler-analyzer.test.js +44 -0
- package/tests/auto-docs/openapi-generator.test.js +103 -0
- package/tests/auto-docs/parse.test.js +63 -0
- package/tests/auto-docs/source-resolver.test.js +58 -0
- package/tests/helpers/index.js +37 -0
- package/tests/helpers/mock-http.js +342 -0
- package/tests/helpers/test-utils.js +446 -0
- package/tests/setup.test.js +148 -0
- package/utils/configuration.js +13 -10
- package/vitest.config.js +54 -0
- package/database/mongo.js +0 -67
- package/example/targets/user/user.target.js +0 -17
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure OpenAPI 3.0 schema and operation builders for auto-documentation.
|
|
3
|
+
* No LLM, file I/O, or registry dependencies.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { ALL_METHODS } from '../analysis/handler-analyzer.js';
|
|
7
|
+
import { METHOD_KEYS } from '../constants.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns true if the object is method-keyed metadata (keys are HTTP methods, values are { summary?, description?, request?, response? }).
|
|
11
|
+
* @param {object} obj - Parsed LLM response
|
|
12
|
+
* @returns {boolean}
|
|
13
|
+
*/
|
|
14
|
+
export function isMethodKeyed(obj) {
|
|
15
|
+
if (!obj || typeof obj !== 'object') return false;
|
|
16
|
+
for (const key of Object.keys(obj)) {
|
|
17
|
+
if (METHOD_KEYS.has(key.toLowerCase())) {
|
|
18
|
+
const val = obj[key];
|
|
19
|
+
if (val && typeof val === 'object' && (val.summary != null || val.response != null)) return true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Convert te.js path pattern to OpenAPI path (e.g. /users/:id -> /users/{id}).
|
|
27
|
+
* @param {string} path - Route path possibly containing :param segments
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
export function toOpenAPIPath(path) {
|
|
31
|
+
if (!path || typeof path !== 'string') return '/';
|
|
32
|
+
return path.replace(/:([^/]+)/g, '{$1}');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Extract path parameter definitions from a te.js path for OpenAPI.
|
|
37
|
+
* @param {string} path - Route path (e.g. /users/:id)
|
|
38
|
+
* @returns {Array<{ name: string, in: string, required: boolean, schema: object }>}
|
|
39
|
+
*/
|
|
40
|
+
export function getPathParameters(path) {
|
|
41
|
+
if (!path || typeof path !== 'string') return [];
|
|
42
|
+
const params = [];
|
|
43
|
+
const segmentRegex = /:([^/]+)/g;
|
|
44
|
+
let match;
|
|
45
|
+
while ((match = segmentRegex.exec(path)) !== null) {
|
|
46
|
+
params.push({
|
|
47
|
+
name: match[1],
|
|
48
|
+
in: 'path',
|
|
49
|
+
required: true,
|
|
50
|
+
description: `Path parameter: ${match[1]}`,
|
|
51
|
+
schema: { type: 'string' },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return params;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build OpenAPI query parameter list from request.query metadata.
|
|
59
|
+
* @param {object} queryMeta - e.g. { limit: { type: 'integer', required: false }, q: { type: 'string', required: true } }
|
|
60
|
+
* @returns {Array<{ name: string, in: string, required: boolean, description?: string, schema: object }>}
|
|
61
|
+
*/
|
|
62
|
+
export function getQueryParameters(queryMeta) {
|
|
63
|
+
if (!queryMeta || typeof queryMeta !== 'object') return [];
|
|
64
|
+
const params = [];
|
|
65
|
+
for (const [name, spec] of Object.entries(queryMeta)) {
|
|
66
|
+
if (!spec || typeof spec !== 'object' || !spec.type) continue;
|
|
67
|
+
params.push({
|
|
68
|
+
name,
|
|
69
|
+
in: 'query',
|
|
70
|
+
required: spec.required === true || spec.required === 'true',
|
|
71
|
+
...(spec.description && { description: spec.description }),
|
|
72
|
+
schema: { type: spec.type, ...(spec.format && { format: spec.format }) },
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return params;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Convert simple metadata schema (field -> { type, description? }) to OpenAPI schema.
|
|
80
|
+
* @param {object} meta - e.g. { name: { type: 'string' }, email: { type: 'string' } }
|
|
81
|
+
* @returns {object} OpenAPI schema with properties
|
|
82
|
+
*/
|
|
83
|
+
export function buildSchemaFromMetadata(meta) {
|
|
84
|
+
if (!meta || typeof meta !== 'object') return {};
|
|
85
|
+
const properties = {};
|
|
86
|
+
const required = [];
|
|
87
|
+
for (const [key, value] of Object.entries(meta)) {
|
|
88
|
+
if (value && typeof value === 'object' && value.type) {
|
|
89
|
+
properties[key] = {
|
|
90
|
+
type: value.type,
|
|
91
|
+
...(value.description && { description: value.description }),
|
|
92
|
+
...(value.format && { format: value.format }),
|
|
93
|
+
};
|
|
94
|
+
if (value.required === true || value.required === 'true') required.push(key);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (Object.keys(properties).length === 0) return {};
|
|
98
|
+
return {
|
|
99
|
+
type: 'object',
|
|
100
|
+
properties,
|
|
101
|
+
...(required.length > 0 && { required }),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Build request body schema for an operation from metadata or empty.
|
|
107
|
+
* @param {object} requestMeta - metadata.request (body schema)
|
|
108
|
+
* @returns {object|undefined} OpenAPI requestBody or undefined
|
|
109
|
+
*/
|
|
110
|
+
export function buildRequestBody(requestMeta) {
|
|
111
|
+
if (!requestMeta?.body || typeof requestMeta.body !== 'object') return undefined;
|
|
112
|
+
const schema = buildSchemaFromMetadata(requestMeta.body);
|
|
113
|
+
if (!schema || Object.keys(schema).length === 0) return undefined;
|
|
114
|
+
return {
|
|
115
|
+
required: true,
|
|
116
|
+
content: {
|
|
117
|
+
'application/json': {
|
|
118
|
+
schema,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Build response object for an operation from metadata.
|
|
126
|
+
* @param {object} responseMeta - metadata.response (e.g. { 200: { description, schema? }, 201: { ... } })
|
|
127
|
+
* @returns {object} OpenAPI responses
|
|
128
|
+
*/
|
|
129
|
+
export function buildResponses(responseMeta) {
|
|
130
|
+
const responses = {};
|
|
131
|
+
if (!responseMeta || typeof responseMeta !== 'object') {
|
|
132
|
+
responses['200'] = { description: 'Success' };
|
|
133
|
+
return responses;
|
|
134
|
+
}
|
|
135
|
+
for (const [code, spec] of Object.entries(responseMeta)) {
|
|
136
|
+
if (!spec || typeof spec !== 'object') continue;
|
|
137
|
+
responses[String(code)] = {
|
|
138
|
+
description: spec.description || `Response ${code}`,
|
|
139
|
+
...(spec.schema && {
|
|
140
|
+
content: {
|
|
141
|
+
'application/json': {
|
|
142
|
+
schema: typeof spec.schema === 'object' && spec.schema.type
|
|
143
|
+
? spec.schema
|
|
144
|
+
: { type: 'object' },
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
}),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (Object.keys(responses).length === 0) {
|
|
151
|
+
responses['200'] = { description: 'Success' };
|
|
152
|
+
}
|
|
153
|
+
return responses;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Returns true when the endpoint accepts all standard HTTP methods (method-agnostic).
|
|
158
|
+
* @param {string[]} methods
|
|
159
|
+
* @returns {boolean}
|
|
160
|
+
*/
|
|
161
|
+
export function isMethodAgnostic(methods) {
|
|
162
|
+
if (!Array.isArray(methods) || methods.length !== ALL_METHODS.length) return false;
|
|
163
|
+
const set = new Set(methods.map((m) => m.toUpperCase()));
|
|
164
|
+
return ALL_METHODS.every((m) => set.has(m));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Build a single OpenAPI operation for one HTTP method.
|
|
169
|
+
* @param {string} method - HTTP method (GET, POST, etc.)
|
|
170
|
+
* @param {object} meta - Merged metadata (explicit + LLM-enhanced): summary, description?, request?, response?
|
|
171
|
+
* @param {Array} pathParams - Path parameters for this path
|
|
172
|
+
* @param {object} [options] - { methodAgnostic?: boolean } - when true, description notes all accepted methods
|
|
173
|
+
* @returns {object} OpenAPI operation object
|
|
174
|
+
*/
|
|
175
|
+
export function buildOperation(method, meta, pathParams, options = {}) {
|
|
176
|
+
const { methodAgnostic = false } = options;
|
|
177
|
+
let description = meta.description || '';
|
|
178
|
+
if (methodAgnostic) {
|
|
179
|
+
const methodList = ALL_METHODS.join(', ');
|
|
180
|
+
description = description
|
|
181
|
+
? `${description}\n\nAccepts any HTTP method: ${methodList}.`
|
|
182
|
+
: `Accepts any HTTP method: ${methodList}.`;
|
|
183
|
+
}
|
|
184
|
+
const queryParams = getQueryParameters(meta.request?.query);
|
|
185
|
+
const allParams = [...pathParams, ...queryParams];
|
|
186
|
+
const op = {
|
|
187
|
+
summary: meta.summary || '',
|
|
188
|
+
...(description && { description }),
|
|
189
|
+
parameters: allParams.length > 0 ? allParams : undefined,
|
|
190
|
+
};
|
|
191
|
+
const methodUpper = method.toUpperCase();
|
|
192
|
+
const body = buildRequestBody(meta.request);
|
|
193
|
+
if (body && (methodAgnostic || (methodUpper !== 'GET' && methodUpper !== 'HEAD'))) {
|
|
194
|
+
op.requestBody = body;
|
|
195
|
+
}
|
|
196
|
+
op.responses = buildResponses(meta.response);
|
|
197
|
+
return op;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Merge explicit endpoint metadata with LLM-enhanced result.
|
|
202
|
+
* @param {object} explicit - From endpoint.getMetadata()
|
|
203
|
+
* @param {object} enhanced - From llm.enhanceEndpointDocs() or per-method enhancer
|
|
204
|
+
* @param {object} [options] - { preferEnhanced?: boolean } - when true (level 2), LLM response wins over explicit
|
|
205
|
+
* @returns {object}
|
|
206
|
+
*/
|
|
207
|
+
export function mergeMetadata(explicit, enhanced, options = {}) {
|
|
208
|
+
const preferEnhanced = options.preferEnhanced === true;
|
|
209
|
+
const summary = preferEnhanced
|
|
210
|
+
? (enhanced?.summary ?? explicit?.summary ?? '')
|
|
211
|
+
: (explicit?.summary ?? enhanced?.summary ?? '');
|
|
212
|
+
const description = preferEnhanced
|
|
213
|
+
? (enhanced?.description ?? explicit?.description ?? '')
|
|
214
|
+
: (explicit?.description ?? enhanced?.description ?? '');
|
|
215
|
+
const request = preferEnhanced
|
|
216
|
+
? (enhanced?.request ?? explicit?.request)
|
|
217
|
+
: (explicit?.request ?? enhanced?.request);
|
|
218
|
+
const response = preferEnhanced
|
|
219
|
+
? (enhanced?.response ?? explicit?.response)
|
|
220
|
+
: (explicit?.response ?? enhanced?.response);
|
|
221
|
+
return {
|
|
222
|
+
summary: summary || 'Endpoint',
|
|
223
|
+
description: description || undefined,
|
|
224
|
+
...(request && { request }),
|
|
225
|
+
...(response && { response }),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Merge method-keyed metadata for a method-agnostic endpoint into a single meta (preferred order).
|
|
231
|
+
* @param {Map<string,object>} metaByMethod
|
|
232
|
+
* @param {string[]} methods
|
|
233
|
+
* @param {object} fallbackMeta
|
|
234
|
+
* @returns {object}
|
|
235
|
+
*/
|
|
236
|
+
export function mergeMethodAgnosticMeta(metaByMethod, methods, fallbackMeta) {
|
|
237
|
+
const preferredOrder = ['post', 'put', 'patch', 'get', 'delete', 'head', 'options'];
|
|
238
|
+
for (const k of preferredOrder) {
|
|
239
|
+
const m = metaByMethod.get(k);
|
|
240
|
+
if (m && (m.summary || m.description)) return m;
|
|
241
|
+
}
|
|
242
|
+
const first = metaByMethod.values().next().value;
|
|
243
|
+
return first || fallbackMeta;
|
|
244
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serves interactive API docs at /docs with try-out functionality.
|
|
3
|
+
* Uses Scalar API Reference (modern UI, try-it-out, themes). MIT.
|
|
4
|
+
* Registers internal routes: GET /docs (HTML page) and GET /docs/openapi.json (spec).
|
|
5
|
+
*
|
|
6
|
+
* @see https://scalar.com/products/api-references/integrations/html-js
|
|
7
|
+
* @see https://scalar.com/products/api-references/configuration
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import Endpoint from '../../server/endpoint.js';
|
|
11
|
+
import targetRegistry from '../../server/targets/registry.js';
|
|
12
|
+
|
|
13
|
+
/** Scalar API Reference browser standalone (IIFE, sets window.Scalar). Pinned for stability. */
|
|
14
|
+
const SCALAR_VERSION = '1.46.0';
|
|
15
|
+
const SCALAR_STANDALONE = `https://cdn.jsdelivr.net/npm/@scalar/api-reference@${SCALAR_VERSION}/dist/browser/standalone.js`;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Default Scalar API Reference config. Use layout: 'classic' to show the test request
|
|
19
|
+
* inline on the same page instead of opening a dialog (modern layout).
|
|
20
|
+
*
|
|
21
|
+
* @see https://scalar.com/products/api-references/configuration
|
|
22
|
+
*/
|
|
23
|
+
const DEFAULT_SCALAR_CONFIG = {
|
|
24
|
+
layout: 'modern',
|
|
25
|
+
theme: 'default',
|
|
26
|
+
showSidebar: true,
|
|
27
|
+
hideDownloadButton: false,
|
|
28
|
+
hideModels: false,
|
|
29
|
+
hideSearch: false,
|
|
30
|
+
hideDarkModeToggle: false,
|
|
31
|
+
hideTestRequestButton: false,
|
|
32
|
+
showDeveloperTools: 'localhost',
|
|
33
|
+
documentDownloadType: 'both',
|
|
34
|
+
defaultOpenAllTags: false,
|
|
35
|
+
defaultOpenFirstTag: true,
|
|
36
|
+
expandAllModelSections: false,
|
|
37
|
+
expandAllResponses: false,
|
|
38
|
+
withDefaultFonts: true,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build HTML shell for Scalar docs: script tag + inline config and mount logic.
|
|
43
|
+
* @param {string} scriptUrl - URL to Scalar standalone JS
|
|
44
|
+
* @param {string} configJson - JSON string (already escaped for embedding in JS)
|
|
45
|
+
* @returns {string} Full HTML document
|
|
46
|
+
*/
|
|
47
|
+
function buildDocsHtmlShell(scriptUrl, configJson) {
|
|
48
|
+
return `<!DOCTYPE html>
|
|
49
|
+
<html lang="en">
|
|
50
|
+
<head>
|
|
51
|
+
<meta charset="UTF-8" />
|
|
52
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
53
|
+
<title>API Reference</title>
|
|
54
|
+
<style>
|
|
55
|
+
body { margin: 0; min-height: 100vh; font-family: system-ui, sans-serif; }
|
|
56
|
+
#scalar-app { min-height: 100vh; width: 100%; }
|
|
57
|
+
</style>
|
|
58
|
+
</head>
|
|
59
|
+
<body>
|
|
60
|
+
<div id="scalar-app"></div>
|
|
61
|
+
<script src="${scriptUrl}" id="scalar-script"><\/script>
|
|
62
|
+
<script>
|
|
63
|
+
(function() {
|
|
64
|
+
var config = JSON.parse('${configJson}');
|
|
65
|
+
function mount() {
|
|
66
|
+
if (typeof Scalar !== 'undefined' && typeof Scalar.createApiReference === 'function') {
|
|
67
|
+
Scalar.createApiReference('#scalar-app', config);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
var el = document.getElementById('scalar-script');
|
|
73
|
+
if (el) {
|
|
74
|
+
el.addEventListener('load', function() { mount(); });
|
|
75
|
+
if (el.readyState === 'complete') setTimeout(function() { mount(); }, 0);
|
|
76
|
+
}
|
|
77
|
+
if (!mount()) setTimeout(function() { mount(); }, 100);
|
|
78
|
+
})();
|
|
79
|
+
</script>
|
|
80
|
+
</body>
|
|
81
|
+
</html>`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Builds the HTML page that embeds Scalar and points to the spec URL.
|
|
86
|
+
* @param {string} specUrl - URL to the OpenAPI spec (e.g. '/docs/openapi.json' or full URL).
|
|
87
|
+
* @param {object} [scalarConfig] - Optional Scalar API Reference config (merged with defaults).
|
|
88
|
+
* @returns {string} Full HTML document.
|
|
89
|
+
*/
|
|
90
|
+
function buildDocsPage(specUrl, scalarConfig = {}) {
|
|
91
|
+
const url = specUrl || '/docs/openapi.json';
|
|
92
|
+
const config = { ...DEFAULT_SCALAR_CONFIG, ...scalarConfig, url };
|
|
93
|
+
const configJson = JSON.stringify(config)
|
|
94
|
+
.replace(/</g, '\\u003c')
|
|
95
|
+
.replace(/>/g, '\\u003e');
|
|
96
|
+
return buildDocsHtmlShell(SCALAR_STANDALONE, configJson);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Create endpoint that serves the docs HTML page at GET docsPath.
|
|
101
|
+
* @param {string} docsPath - e.g. '/docs'
|
|
102
|
+
* @param {string} htmlContent - Full HTML document
|
|
103
|
+
* @returns {Endpoint}
|
|
104
|
+
*/
|
|
105
|
+
function createDocsHtmlEndpoint(docsPath, htmlContent) {
|
|
106
|
+
const endpoint = new Endpoint();
|
|
107
|
+
endpoint.setPath('', docsPath);
|
|
108
|
+
endpoint.setMiddlewares([]);
|
|
109
|
+
endpoint.setHandler((ammo) => {
|
|
110
|
+
if (!ammo.GET) return ammo.notAllowed();
|
|
111
|
+
ammo.fire(200, htmlContent, 'text/html');
|
|
112
|
+
});
|
|
113
|
+
return endpoint;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Create endpoint that serves the OpenAPI spec JSON at GET specPath.
|
|
118
|
+
* @param {string} specPath - e.g. '/docs/openapi.json'
|
|
119
|
+
* @param {() => object | Promise<object>} getSpec - Function that returns the current spec
|
|
120
|
+
* @returns {Endpoint}
|
|
121
|
+
*/
|
|
122
|
+
function createSpecJsonEndpoint(specPath, getSpec) {
|
|
123
|
+
const endpoint = new Endpoint();
|
|
124
|
+
endpoint.setPath('', specPath);
|
|
125
|
+
endpoint.setMiddlewares([]);
|
|
126
|
+
endpoint.setHandler(async (ammo) => {
|
|
127
|
+
if (!ammo.GET) return ammo.notAllowed();
|
|
128
|
+
try {
|
|
129
|
+
const spec = await Promise.resolve(getSpec());
|
|
130
|
+
ammo.fire(200, spec);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
ammo.fire(500, {
|
|
133
|
+
error: 'Failed to generate OpenAPI spec',
|
|
134
|
+
message: err?.message,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
return endpoint;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Registers the docs and openapi.json routes (or returns endpoints when mutateRegistry is false).
|
|
143
|
+
* Call this after the OpenAPI spec is available (e.g. after generateOpenAPISpec).
|
|
144
|
+
*
|
|
145
|
+
* @param {object} [options]
|
|
146
|
+
* @param {() => object | Promise<object>} options.getSpec - Function that returns the current OpenAPI spec (sync or async). Used for GET {docsPath}/openapi.json.
|
|
147
|
+
* @param {string} [options.docsPath='/docs'] - Base path for docs (HTML page and spec URL). Routes: GET {docsPath}, GET {docsPath}/openapi.json.
|
|
148
|
+
* @param {string} [options.specUrl] - Override for the spec URL shown in the docs page (default: '{docsPath}/openapi.json'). Use when serving behind a proxy with a different base path.
|
|
149
|
+
* @param {object} [options.scalarConfig] - Optional Scalar API Reference config (e.g. { layout: 'classic' } for try-it on the same page).
|
|
150
|
+
* @param {boolean} [options.mutateRegistry=true] - If true, push endpoints to registry. If false, return [docsEndpoint, specEndpoint] without mutating.
|
|
151
|
+
* @param {object} [registry] - Target registry to register routes on when mutateRegistry is true. Defaults to the module's targetRegistry.
|
|
152
|
+
* @returns {undefined | [Endpoint, Endpoint]} When mutateRegistry is false, returns the two endpoints for the caller to register.
|
|
153
|
+
*/
|
|
154
|
+
export function registerDocRoutes(options = {}, registry = targetRegistry) {
|
|
155
|
+
const {
|
|
156
|
+
getSpec,
|
|
157
|
+
docsPath = '/docs',
|
|
158
|
+
specUrl: specUrlOption,
|
|
159
|
+
scalarConfig,
|
|
160
|
+
mutateRegistry = true,
|
|
161
|
+
} = options;
|
|
162
|
+
|
|
163
|
+
if (typeof getSpec !== 'function') {
|
|
164
|
+
throw new Error(
|
|
165
|
+
'registerDocRoutes requires options.getSpec (function returning the OpenAPI spec)',
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const basePath = docsPath.replace(/\/$/, '');
|
|
170
|
+
const specUrl = specUrlOption ?? `${basePath}/openapi.json`;
|
|
171
|
+
const specPath = `${basePath}/openapi.json`;
|
|
172
|
+
const htmlContent = buildDocsPage(specUrl, scalarConfig);
|
|
173
|
+
|
|
174
|
+
const docsEndpoint = createDocsHtmlEndpoint(docsPath, htmlContent);
|
|
175
|
+
const specEndpoint = createSpecJsonEndpoint(specPath, getSpec);
|
|
176
|
+
|
|
177
|
+
if (mutateRegistry) {
|
|
178
|
+
registry.targets.push(docsEndpoint);
|
|
179
|
+
registry.targets.push(specEndpoint);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
return [docsEndpoint, specEndpoint];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export { buildDocsPage };
|
|
186
|
+
export default registerDocRoutes;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verbose logging helper for auto-docs.
|
|
3
|
+
* Returns a no-op when verbose is false or logger has no info method.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a logger that only logs when verbose is true and logger.info exists.
|
|
8
|
+
* @param {object|null} logger - Logger with .info(msg) method
|
|
9
|
+
* @param {boolean} verbose - Whether to log
|
|
10
|
+
* @returns {(msg: string) => void} Function that logs or does nothing
|
|
11
|
+
*/
|
|
12
|
+
export function createVerboseLogger(logger, verbose) {
|
|
13
|
+
if (!verbose || !logger || typeof logger.info !== 'function') {
|
|
14
|
+
return () => {};
|
|
15
|
+
}
|
|
16
|
+
return (msg) => logger.info(msg);
|
|
17
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip LLM-only fields from response so they are not merged into metadata.
|
|
3
|
+
* @param {object|null} obj - Raw LLM response (may contain _usage, _fallback)
|
|
4
|
+
* @returns {object|null} Same object without _usage and _fallback
|
|
5
|
+
*/
|
|
6
|
+
export function stripLlmUsage(obj) {
|
|
7
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
8
|
+
const { _usage, _fallback, ...rest } = obj;
|
|
9
|
+
return rest;
|
|
10
|
+
}
|