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,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-endpoint processing for OpenAPI spec: extract info, resolve deps, LLM enhance, build path ops and tag descriptions.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { analyzeHandler } from '../analysis/handler-analyzer.js';
|
|
6
|
+
import {
|
|
7
|
+
resolveDependencySources,
|
|
8
|
+
formatDependencyContext,
|
|
9
|
+
resolveTargetFilePath,
|
|
10
|
+
} from '../analysis/source-resolver.js';
|
|
11
|
+
import {
|
|
12
|
+
HANDLER_SOURCE_MAX_LENGTH_BY_LEVEL,
|
|
13
|
+
DEPENDENCY_CONTEXT_MAX_CHARS,
|
|
14
|
+
METHOD_AGNOSTIC_OPERATION_KEY,
|
|
15
|
+
} from '../constants.js';
|
|
16
|
+
import { stripLlmUsage } from '../utils/strip-usage.js';
|
|
17
|
+
import {
|
|
18
|
+
isMethodKeyed,
|
|
19
|
+
toOpenAPIPath,
|
|
20
|
+
getPathParameters,
|
|
21
|
+
isMethodAgnostic,
|
|
22
|
+
mergeMetadata,
|
|
23
|
+
mergeMethodAgnosticMeta,
|
|
24
|
+
buildOperation,
|
|
25
|
+
} from './spec-builders.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Extract path, handler, metadata, methods, groupId, tag from a registry target.
|
|
29
|
+
* @param {object} target - Endpoint-like with getPath(), getHandler(), getMetadata(), getGroup?()
|
|
30
|
+
* @returns {{ path: string, handler: function|null, explicitMeta: object, methods: string[], groupId: string|null, tag: string }}
|
|
31
|
+
*/
|
|
32
|
+
export function extractTargetInfo(target) {
|
|
33
|
+
const path = target.getPath();
|
|
34
|
+
const handler = target.getHandler();
|
|
35
|
+
const explicitMeta = target.getMetadata() || {};
|
|
36
|
+
const analyzed = analyzeHandler(handler);
|
|
37
|
+
const methods = Array.isArray(explicitMeta.methods) && explicitMeta.methods.length > 0
|
|
38
|
+
? explicitMeta.methods
|
|
39
|
+
: analyzed.methods;
|
|
40
|
+
const groupId = target.getGroup?.() ?? null;
|
|
41
|
+
const tag = groupId != null && groupId !== '' ? groupId : 'default';
|
|
42
|
+
return { path, handler, explicitMeta, methods, groupId, tag };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Slice handler source to max length for the given level.
|
|
47
|
+
* @param {function|null} handler
|
|
48
|
+
* @param {number} effectiveLevel
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
export function resolveHandlerSource(handler, effectiveLevel) {
|
|
52
|
+
const raw = typeof handler === 'function' ? handler.toString() : '';
|
|
53
|
+
const max = HANDLER_SOURCE_MAX_LENGTH_BY_LEVEL[effectiveLevel] ?? 2800;
|
|
54
|
+
return raw.slice(0, max);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve and cache dependency context for a group. Mutates cache.
|
|
59
|
+
* @param {string|null} groupId
|
|
60
|
+
* @param {string} tag
|
|
61
|
+
* @param {number} effectiveLevel
|
|
62
|
+
* @param {string} dirTargets
|
|
63
|
+
* @param {Map<string,string>} cache - dependencyContextByGroup
|
|
64
|
+
* @returns {Promise<string>}
|
|
65
|
+
*/
|
|
66
|
+
export async function resolveDependencyContext(groupId, tag, effectiveLevel, dirTargets, cache) {
|
|
67
|
+
if (effectiveLevel !== 2 || !groupId) return '';
|
|
68
|
+
if (cache.has(tag)) return cache.get(tag) || '';
|
|
69
|
+
try {
|
|
70
|
+
const sources = await resolveDependencySources(groupId, dirTargets);
|
|
71
|
+
const targetPath = resolveTargetFilePath(groupId, dirTargets);
|
|
72
|
+
const context = formatDependencyContext(sources, targetPath, DEPENDENCY_CONTEXT_MAX_CHARS);
|
|
73
|
+
cache.set(tag, context);
|
|
74
|
+
return context;
|
|
75
|
+
} catch {
|
|
76
|
+
cache.set(tag, '');
|
|
77
|
+
return '';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Call LLM to enhance endpoint metadata. Returns { meta, metaByMethod }.
|
|
83
|
+
* @param {object} endpointInfo - { path, methods, metadata, handlerSource, dependencySources? }
|
|
84
|
+
* @param {object} llm
|
|
85
|
+
* @param {object} explicitMeta
|
|
86
|
+
* @param {boolean} preferEnhanced
|
|
87
|
+
* @param {string[]} methods
|
|
88
|
+
* @param {string} path
|
|
89
|
+
* @param {function} log
|
|
90
|
+
* @returns {Promise<{ meta: object, metaByMethod: Map<string,object>|null }>}
|
|
91
|
+
*/
|
|
92
|
+
export async function enhanceWithLlm(endpointInfo, llm, explicitMeta, preferEnhanced, methods, path, log) {
|
|
93
|
+
let meta = {
|
|
94
|
+
summary: explicitMeta.summary || path || 'Endpoint',
|
|
95
|
+
description: explicitMeta.description,
|
|
96
|
+
request: explicitMeta.request,
|
|
97
|
+
response: explicitMeta.response,
|
|
98
|
+
};
|
|
99
|
+
let metaByMethod = null;
|
|
100
|
+
try {
|
|
101
|
+
if (typeof llm.enhanceEndpointDocsPerMethod === 'function') {
|
|
102
|
+
const rawPerMethod = await llm.enhanceEndpointDocsPerMethod(endpointInfo);
|
|
103
|
+
const cleaned = stripLlmUsage(rawPerMethod);
|
|
104
|
+
if (cleaned && isMethodKeyed(cleaned)) {
|
|
105
|
+
metaByMethod = new Map();
|
|
106
|
+
for (const m of methods) {
|
|
107
|
+
const k = m.toLowerCase();
|
|
108
|
+
metaByMethod.set(k, mergeMetadata(explicitMeta, cleaned[k] || {}, { preferEnhanced }));
|
|
109
|
+
}
|
|
110
|
+
meta = mergeMetadata(explicitMeta, {}, { preferEnhanced });
|
|
111
|
+
} else {
|
|
112
|
+
meta = mergeMetadata(explicitMeta, cleaned || {}, { preferEnhanced });
|
|
113
|
+
}
|
|
114
|
+
const tokenStr = rawPerMethod?._usage?.total_tokens != null ? ` — ${rawPerMethod._usage.total_tokens} tokens` : '';
|
|
115
|
+
log(` ${path} [${methods.join(', ').toUpperCase()}]${tokenStr}`);
|
|
116
|
+
} else {
|
|
117
|
+
const enhanced = await llm.enhanceEndpointDocs(endpointInfo);
|
|
118
|
+
meta = mergeMetadata(explicitMeta, stripLlmUsage(enhanced) || enhanced, { preferEnhanced });
|
|
119
|
+
const tokenStr = enhanced?._usage?.total_tokens != null ? ` — ${enhanced._usage.total_tokens} tokens` : '';
|
|
120
|
+
log(` ${path} [${methods.join(', ').toUpperCase()}]${tokenStr}`);
|
|
121
|
+
}
|
|
122
|
+
} catch (err) {
|
|
123
|
+
log(` ${path} [${(methods || []).join(', ').toUpperCase()}] — LLM failed`);
|
|
124
|
+
}
|
|
125
|
+
return { meta, metaByMethod };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Process one registry target: analyze, optionally enhance with LLM, build meta and path params.
|
|
130
|
+
* @param {object} target - Endpoint-like with getPath(), getHandler(), getMetadata(), getGroup?()
|
|
131
|
+
* @param {object} options - { llm?, effectiveLevel, dirTargets, dependencyContextByGroup, useLlm, preferEnhanced, log }
|
|
132
|
+
* @returns {Promise<{ openAPIPath: string, tag: string, methodAgnostic: boolean, meta: object, metaByMethod: Map|null, methods: string[], pathParams: array, groupEntry: object }>}
|
|
133
|
+
*/
|
|
134
|
+
export async function processEndpoint(target, options) {
|
|
135
|
+
const { llm, effectiveLevel, dirTargets, dependencyContextByGroup, useLlm, preferEnhanced, log } = options;
|
|
136
|
+
|
|
137
|
+
const { path, handler, explicitMeta, methods, groupId, tag } = extractTargetInfo(target);
|
|
138
|
+
let meta = {
|
|
139
|
+
summary: explicitMeta.summary || path || 'Endpoint',
|
|
140
|
+
description: explicitMeta.description,
|
|
141
|
+
request: explicitMeta.request,
|
|
142
|
+
response: explicitMeta.response,
|
|
143
|
+
};
|
|
144
|
+
const handlerSource = resolveHandlerSource(handler, effectiveLevel);
|
|
145
|
+
let metaByMethod = null;
|
|
146
|
+
|
|
147
|
+
if (useLlm) {
|
|
148
|
+
const dependencySources = await resolveDependencyContext(groupId, tag, effectiveLevel, dirTargets, dependencyContextByGroup);
|
|
149
|
+
const endpointInfo = {
|
|
150
|
+
path,
|
|
151
|
+
methods,
|
|
152
|
+
metadata: explicitMeta,
|
|
153
|
+
handlerSource,
|
|
154
|
+
...(dependencySources && { dependencySources }),
|
|
155
|
+
};
|
|
156
|
+
const enhanced = await enhanceWithLlm(endpointInfo, llm, explicitMeta, preferEnhanced, methods, path, log);
|
|
157
|
+
meta = enhanced.meta;
|
|
158
|
+
metaByMethod = enhanced.metaByMethod;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const openAPIPath = toOpenAPIPath(path);
|
|
162
|
+
const pathParams = getPathParameters(path);
|
|
163
|
+
const handlerIsMethodAgnostic = isMethodAgnostic(methods);
|
|
164
|
+
if (handlerIsMethodAgnostic && metaByMethod != null) {
|
|
165
|
+
meta = mergeMethodAgnosticMeta(metaByMethod, methods, meta);
|
|
166
|
+
metaByMethod = null;
|
|
167
|
+
}
|
|
168
|
+
const methodAgnostic = metaByMethod == null && handlerIsMethodAgnostic;
|
|
169
|
+
|
|
170
|
+
const groupEntry = {
|
|
171
|
+
path,
|
|
172
|
+
methods,
|
|
173
|
+
summary: meta.summary,
|
|
174
|
+
description: meta.description,
|
|
175
|
+
handlerSource,
|
|
176
|
+
...(dependencyContextByGroup.has(tag) && { dependencySources: dependencyContextByGroup.get(tag) }),
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
openAPIPath,
|
|
181
|
+
tag,
|
|
182
|
+
methodAgnostic,
|
|
183
|
+
meta,
|
|
184
|
+
metaByMethod,
|
|
185
|
+
methods,
|
|
186
|
+
pathParams,
|
|
187
|
+
groupEntry,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Add one endpoint's operations to the paths object (mutates paths).
|
|
193
|
+
* @param {object} paths - OpenAPI paths object
|
|
194
|
+
* @param {object} result - From processEndpoint
|
|
195
|
+
*/
|
|
196
|
+
export function addEndpointToPaths(paths, result) {
|
|
197
|
+
const { openAPIPath, tag, methodAgnostic, meta, metaByMethod, methods, pathParams } = result;
|
|
198
|
+
if (!paths[openAPIPath]) paths[openAPIPath] = {};
|
|
199
|
+
if (methodAgnostic) {
|
|
200
|
+
const op = buildOperation(METHOD_AGNOSTIC_OPERATION_KEY, meta, pathParams, { methodAgnostic: true });
|
|
201
|
+
op.tags = [tag];
|
|
202
|
+
paths[openAPIPath][METHOD_AGNOSTIC_OPERATION_KEY] = op;
|
|
203
|
+
} else {
|
|
204
|
+
for (const method of methods) {
|
|
205
|
+
const key = method.toLowerCase();
|
|
206
|
+
const methodMeta = metaByMethod?.get(key) ?? meta;
|
|
207
|
+
const op = buildOperation(method, methodMeta, pathParams);
|
|
208
|
+
op.tags = [tag];
|
|
209
|
+
paths[openAPIPath][key] = op;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Build tag name and description for each group (LLM or fallback).
|
|
216
|
+
* @param {Map<string,object[]>} groupEndpoints
|
|
217
|
+
* @param {Map<string,string>} dependencyContextByGroup
|
|
218
|
+
* @param {object|null} llm
|
|
219
|
+
* @param {{ effectiveLevel: number, log: function }} options
|
|
220
|
+
* @returns {Promise<Map<string,{ name: string, description: string }>>}
|
|
221
|
+
*/
|
|
222
|
+
export async function buildTagDescriptions(groupEndpoints, dependencyContextByGroup, llm, options) {
|
|
223
|
+
const { effectiveLevel, log } = options;
|
|
224
|
+
const tagDescriptions = new Map();
|
|
225
|
+
if (llm && typeof llm.summarizeTargetGroup === 'function') {
|
|
226
|
+
for (const [groupId, endpoints] of groupEndpoints) {
|
|
227
|
+
try {
|
|
228
|
+
const dependencySources = effectiveLevel === 2 ? dependencyContextByGroup.get(groupId) || '' : '';
|
|
229
|
+
const infos = endpoints.map((e) => ({
|
|
230
|
+
path: e.path,
|
|
231
|
+
methods: e.methods,
|
|
232
|
+
summary: e.summary,
|
|
233
|
+
description: e.description,
|
|
234
|
+
handlerSource: e.handlerSource,
|
|
235
|
+
...(e.dependencySources && { dependencySources: e.dependencySources }),
|
|
236
|
+
}));
|
|
237
|
+
const result = await llm.summarizeTargetGroup(groupId, infos, dependencySources);
|
|
238
|
+
const { name, description, _usage: summaryUsage } = result;
|
|
239
|
+
tagDescriptions.set(groupId, { name: name || groupId, description });
|
|
240
|
+
const tokenStr = summaryUsage?.total_tokens != null && summaryUsage.total_tokens > 0
|
|
241
|
+
? ` — ${summaryUsage.total_tokens} tokens`
|
|
242
|
+
: '';
|
|
243
|
+
log(` [group ${groupId}] summary${tokenStr}`);
|
|
244
|
+
} catch (err) {
|
|
245
|
+
tagDescriptions.set(groupId, {
|
|
246
|
+
name: groupId.split('/').pop() || groupId,
|
|
247
|
+
description: '',
|
|
248
|
+
});
|
|
249
|
+
log(` [group ${groupId}] summary — failed`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
for (const groupId of groupEndpoints.keys()) {
|
|
254
|
+
tagDescriptions.set(groupId, {
|
|
255
|
+
name: groupId.split('/').pop() || groupId,
|
|
256
|
+
description: '',
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return tagDescriptions;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Replace operation tags (groupId) with display names from tagDescriptions. Mutates paths.
|
|
265
|
+
* @param {object} paths - OpenAPI paths object
|
|
266
|
+
* @param {Map<string,{ name: string, description?: string }>} tagDescriptions
|
|
267
|
+
*/
|
|
268
|
+
export function applyTagDisplayNames(paths, tagDescriptions) {
|
|
269
|
+
for (const pathItem of Object.values(paths)) {
|
|
270
|
+
for (const op of Object.values(pathItem)) {
|
|
271
|
+
if (op?.tags?.[0]) {
|
|
272
|
+
const groupId = op.tags[0];
|
|
273
|
+
op.tags[0] = tagDescriptions.get(groupId)?.name ?? groupId;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI 3.0 spec generator for te.js auto-documentation.
|
|
3
|
+
* Orchestrates spec-builders and endpoint-processor; builds final spec object.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { OPENAPI_VERSION } from '../constants.js';
|
|
7
|
+
import { createVerboseLogger } from '../utils/logger.js';
|
|
8
|
+
import {
|
|
9
|
+
processEndpoint,
|
|
10
|
+
addEndpointToPaths,
|
|
11
|
+
buildTagDescriptions,
|
|
12
|
+
applyTagDisplayNames,
|
|
13
|
+
} from './endpoint-processor.js';
|
|
14
|
+
import {
|
|
15
|
+
toOpenAPIPath,
|
|
16
|
+
getPathParameters,
|
|
17
|
+
getQueryParameters,
|
|
18
|
+
buildSchemaFromMetadata,
|
|
19
|
+
buildRequestBody,
|
|
20
|
+
buildResponses,
|
|
21
|
+
buildOperation,
|
|
22
|
+
mergeMetadata,
|
|
23
|
+
} from './spec-builders.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Build OpenAPI 3.0 spec from registry and options.
|
|
27
|
+
* @param {object} registry - Target registry with .targets
|
|
28
|
+
* @param {object} [options] - { llm?, info?, servers?, level?, dirTargets?, verbose?, logger? }
|
|
29
|
+
* @returns {Promise<object>} OpenAPI 3.0 spec
|
|
30
|
+
*/
|
|
31
|
+
async function generateOpenAPISpec(registry, options = {}) {
|
|
32
|
+
const {
|
|
33
|
+
llm,
|
|
34
|
+
info = {},
|
|
35
|
+
servers,
|
|
36
|
+
level = 1,
|
|
37
|
+
dirTargets = process.env.DIR_TARGETS || 'targets',
|
|
38
|
+
verbose = false,
|
|
39
|
+
logger = null,
|
|
40
|
+
} = options;
|
|
41
|
+
const targets = registry?.targets ?? [];
|
|
42
|
+
const paths = {};
|
|
43
|
+
const groupEndpoints = new Map();
|
|
44
|
+
const dependencyContextByGroup = new Map();
|
|
45
|
+
|
|
46
|
+
const useLlm = !!llm && typeof llm.enhanceEndpointDocs === 'function';
|
|
47
|
+
const effectiveLevel = level === 3 ? 2 : Math.max(1, Math.min(2, level));
|
|
48
|
+
const log = createVerboseLogger(logger, verbose);
|
|
49
|
+
const preferEnhanced = effectiveLevel === 2;
|
|
50
|
+
|
|
51
|
+
for (const target of targets) {
|
|
52
|
+
const result = await processEndpoint(target, {
|
|
53
|
+
llm,
|
|
54
|
+
effectiveLevel,
|
|
55
|
+
dirTargets,
|
|
56
|
+
dependencyContextByGroup,
|
|
57
|
+
useLlm,
|
|
58
|
+
preferEnhanced,
|
|
59
|
+
log,
|
|
60
|
+
});
|
|
61
|
+
if (!groupEndpoints.has(result.tag)) groupEndpoints.set(result.tag, []);
|
|
62
|
+
groupEndpoints.get(result.tag).push(result.groupEntry);
|
|
63
|
+
addEndpointToPaths(paths, result);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const tagDescriptions = await buildTagDescriptions(
|
|
67
|
+
groupEndpoints,
|
|
68
|
+
dependencyContextByGroup,
|
|
69
|
+
useLlm ? llm : null,
|
|
70
|
+
{ effectiveLevel, log },
|
|
71
|
+
);
|
|
72
|
+
applyTagDisplayNames(paths, tagDescriptions);
|
|
73
|
+
|
|
74
|
+
const tags = Array.from(tagDescriptions.entries()).map(([, { name, description }]) => ({
|
|
75
|
+
name,
|
|
76
|
+
...(description && { description }),
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
const spec = {
|
|
80
|
+
openapi: OPENAPI_VERSION,
|
|
81
|
+
info: {
|
|
82
|
+
title: info.title ?? 'API',
|
|
83
|
+
version: info.version ?? '1.0.0',
|
|
84
|
+
...(info.description && { description: info.description }),
|
|
85
|
+
},
|
|
86
|
+
tags: tags.length > 0 ? tags : undefined,
|
|
87
|
+
paths,
|
|
88
|
+
};
|
|
89
|
+
if (Array.isArray(servers) && servers.length > 0) {
|
|
90
|
+
spec.servers = servers;
|
|
91
|
+
}
|
|
92
|
+
return spec;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export {
|
|
96
|
+
OPENAPI_VERSION,
|
|
97
|
+
toOpenAPIPath,
|
|
98
|
+
getPathParameters,
|
|
99
|
+
getQueryParameters,
|
|
100
|
+
buildSchemaFromMetadata,
|
|
101
|
+
buildRequestBody,
|
|
102
|
+
buildResponses,
|
|
103
|
+
buildOperation,
|
|
104
|
+
mergeMetadata,
|
|
105
|
+
generateOpenAPISpec,
|
|
106
|
+
};
|
|
107
|
+
export default generateOpenAPISpec;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Level 3 pipeline: reorder OpenAPI tag groups by importance and generate overview page.
|
|
3
|
+
* Single entry point is runLevel3(spec, options, llm). Runs after the spec is generated (level 2 quality).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { writeFile } from 'node:fs/promises';
|
|
8
|
+
import { createVerboseLogger } from '../utils/logger.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolve overview file path from option or derive from outputPath.
|
|
12
|
+
* @param {string} [overviewPathOption] - Explicit path for API_OVERVIEW.md
|
|
13
|
+
* @param {string} [outputPath] - OpenAPI spec output path (dir used for default overview)
|
|
14
|
+
* @returns {string} Overview path or ''
|
|
15
|
+
*/
|
|
16
|
+
export function resolveOverviewPath(overviewPathOption, outputPath) {
|
|
17
|
+
if (overviewPathOption && typeof overviewPathOption === 'string') {
|
|
18
|
+
return overviewPathOption;
|
|
19
|
+
}
|
|
20
|
+
if (outputPath && typeof outputPath === 'string') {
|
|
21
|
+
return path.join(path.dirname(outputPath), 'API_OVERVIEW.md');
|
|
22
|
+
}
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generate overview markdown from the spec (no I/O). Pure except LLM call.
|
|
28
|
+
* @param {object} spec - OpenAPI 3 spec (after reorder)
|
|
29
|
+
* @param {object} options - { info?: { title?, version?, description? } }
|
|
30
|
+
* @param {object} llm - LLM provider with generateOverviewPage(spec, options)
|
|
31
|
+
* @returns {Promise<{ markdown: string }>}
|
|
32
|
+
*/
|
|
33
|
+
export async function generateOverview(spec, options, llm) {
|
|
34
|
+
if (typeof llm?.generateOverviewPage !== 'function') {
|
|
35
|
+
return { markdown: '' };
|
|
36
|
+
}
|
|
37
|
+
const info = options?.info ?? {};
|
|
38
|
+
const { markdown } = await llm.generateOverviewPage(spec, {
|
|
39
|
+
title: info.title,
|
|
40
|
+
version: info.version,
|
|
41
|
+
description: info.description,
|
|
42
|
+
});
|
|
43
|
+
return { markdown: markdown || '' };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Write level-3 artifacts: overview file and optionally the spec file.
|
|
48
|
+
* @param {object} spec - OpenAPI 3 spec (may have info.description set)
|
|
49
|
+
* @param {string} overviewPath - Path for API_OVERVIEW.md
|
|
50
|
+
* @param {string} [outputPath] - Path for openapi.json
|
|
51
|
+
* @param {string} [markdown] - Overview markdown content
|
|
52
|
+
* @returns {Promise<void>}
|
|
53
|
+
*/
|
|
54
|
+
export async function writeLevel3Artifacts(spec, overviewPath, outputPath, markdown) {
|
|
55
|
+
if (overviewPath && markdown) {
|
|
56
|
+
await writeFile(overviewPath, markdown, 'utf8');
|
|
57
|
+
}
|
|
58
|
+
if (outputPath && typeof outputPath === 'string') {
|
|
59
|
+
await writeFile(outputPath, JSON.stringify(spec, null, 2), 'utf8');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Run the full level-3 pipeline: reorder tags, generate overview, write artifacts.
|
|
65
|
+
* @param {object} spec - OpenAPI 3 spec (mutated: tags reordered, info.description set)
|
|
66
|
+
* @param {object} options - { outputPath?, overviewPath?, info?, verbose?, logger? }
|
|
67
|
+
* @param {object} llm - LLM provider with reorderTagsByImportance and generateOverviewPage
|
|
68
|
+
* @returns {Promise<void>}
|
|
69
|
+
*/
|
|
70
|
+
export async function runLevel3(spec, options, llm) {
|
|
71
|
+
const { outputPath, overviewPath: overviewPathOption, info = {}, verbose = false, logger = null } = options;
|
|
72
|
+
const log = createVerboseLogger(logger, verbose);
|
|
73
|
+
|
|
74
|
+
if (!spec?.tags?.length || !llm) return;
|
|
75
|
+
|
|
76
|
+
log('Level 3: reordering tag groups by importance...');
|
|
77
|
+
await reorderSpecTags(spec, llm);
|
|
78
|
+
|
|
79
|
+
const overviewPath = resolveOverviewPath(overviewPathOption, outputPath);
|
|
80
|
+
if (!overviewPath) {
|
|
81
|
+
await writeLevel3Artifacts(spec, '', outputPath);
|
|
82
|
+
if (outputPath) log('Spec updated with reordered tags.');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
log('Level 3: generating overview...');
|
|
87
|
+
const { markdown } = await generateOverview(spec, { info }, llm);
|
|
88
|
+
if (markdown) {
|
|
89
|
+
spec.info = spec.info || {};
|
|
90
|
+
spec.info.description = markdown;
|
|
91
|
+
log('Overview embedded in spec (info.description) for Scalar.');
|
|
92
|
+
}
|
|
93
|
+
log('Overview file written to ' + overviewPath + '.');
|
|
94
|
+
|
|
95
|
+
await writeLevel3Artifacts(spec, overviewPath, outputPath, markdown);
|
|
96
|
+
if (outputPath) log('Spec written with reordered tags and overview.');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Reorder spec.tags by importance using the LLM. Mutates and returns the same spec object.
|
|
101
|
+
* @param {object} spec - OpenAPI 3 spec with tags array
|
|
102
|
+
* @param {object} llm - LLM provider with reorderTagsByImportance(spec)
|
|
103
|
+
* @returns {Promise<object>} The same spec with tags reordered
|
|
104
|
+
*/
|
|
105
|
+
export async function reorderSpecTags(spec, llm) {
|
|
106
|
+
if (!spec?.tags?.length || typeof llm?.reorderTagsByImportance !== 'function') {
|
|
107
|
+
return spec;
|
|
108
|
+
}
|
|
109
|
+
const result = await llm.reorderTagsByImportance(spec);
|
|
110
|
+
const ordered = result._orderedTags;
|
|
111
|
+
if (Array.isArray(ordered) && ordered.length > 0) {
|
|
112
|
+
spec.tags = ordered;
|
|
113
|
+
}
|
|
114
|
+
return spec;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Generate overview markdown and write it to overviewPath. (Convenience: generateOverview + write.)
|
|
119
|
+
* @param {object} spec - OpenAPI 3 spec (after reorder)
|
|
120
|
+
* @param {object} options - { overviewPath: string, info?: { title?, version?, description? } }
|
|
121
|
+
* @param {object} llm - LLM provider with generateOverviewPage(spec, options)
|
|
122
|
+
* @returns {Promise<{ markdown: string }>}
|
|
123
|
+
*/
|
|
124
|
+
export async function generateAndWriteOverview(spec, options, llm) {
|
|
125
|
+
const overviewPath = options?.overviewPath;
|
|
126
|
+
const { markdown } = await generateOverview(spec, options, llm);
|
|
127
|
+
if (overviewPath && markdown) {
|
|
128
|
+
await writeFile(overviewPath, markdown, 'utf8');
|
|
129
|
+
}
|
|
130
|
+
return { markdown };
|
|
131
|
+
}
|