te.js 2.1.5 → 2.2.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/auto-docs/analysis/handler-analyzer.test.js +106 -0
- package/auto-docs/analysis/source-resolver.test.js +58 -0
- package/auto-docs/constants.js +13 -2
- package/auto-docs/openapi/generator.js +7 -5
- package/auto-docs/openapi/generator.test.js +132 -0
- package/auto-docs/openapi/spec-builders.js +39 -19
- package/cli/docs-command.js +44 -36
- package/cors/index.test.js +82 -0
- package/database/index.js +3 -1
- package/database/mongodb.js +17 -11
- package/database/redis.js +53 -44
- package/docs/configuration.md +24 -10
- package/docs/error-handling.md +134 -50
- package/lib/llm/client.js +40 -10
- package/lib/llm/index.js +14 -1
- package/lib/llm/parse.test.js +60 -0
- package/package.json +3 -1
- package/radar/index.js +281 -0
- package/rate-limit/index.js +8 -11
- package/rate-limit/index.test.js +64 -0
- package/server/ammo/body-parser.js +156 -152
- package/server/ammo/body-parser.test.js +79 -0
- package/server/ammo/enhancer.js +8 -4
- package/server/ammo.js +216 -17
- package/server/context/request-context.js +51 -0
- package/server/context/request-context.test.js +53 -0
- package/server/endpoint.js +15 -0
- package/server/error.js +56 -3
- package/server/error.test.js +45 -0
- package/server/errors/channels/base.js +31 -0
- package/server/errors/channels/channels.test.js +148 -0
- package/server/errors/channels/console.js +64 -0
- package/server/errors/channels/index.js +111 -0
- package/server/errors/channels/log.js +27 -0
- package/server/errors/llm-cache.js +102 -0
- package/server/errors/llm-cache.test.js +160 -0
- package/server/errors/llm-error-service.js +77 -16
- package/server/errors/llm-rate-limiter.js +72 -0
- package/server/errors/llm-rate-limiter.test.js +105 -0
- package/server/files/uploader.js +38 -26
- package/server/handler.js +5 -3
- package/server/targets/registry.js +9 -9
- package/server/targets/registry.test.js +108 -0
- package/te.js +214 -57
- package/utils/auto-register.js +1 -1
- package/utils/configuration.js +23 -9
- package/utils/configuration.test.js +58 -0
- package/utils/errors-llm-config.js +142 -9
- package/utils/request-logger.js +49 -3
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for auto-docs handler-analyzer (detectMethods, analyzeHandler).
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
detectMethods,
|
|
7
|
+
analyzeHandler,
|
|
8
|
+
ALL_METHODS,
|
|
9
|
+
} from './handler-analyzer.js';
|
|
10
|
+
|
|
11
|
+
describe('handler-analyzer', () => {
|
|
12
|
+
describe('detectMethods', () => {
|
|
13
|
+
it('returns all methods when handler is not a function', () => {
|
|
14
|
+
expect(detectMethods(null)).toEqual(ALL_METHODS);
|
|
15
|
+
expect(detectMethods(undefined)).toEqual(ALL_METHODS);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('detects GET when handler uses ammo.GET', () => {
|
|
19
|
+
const handler = (ammo) => {
|
|
20
|
+
if (ammo.GET) ammo.fire(200, {});
|
|
21
|
+
};
|
|
22
|
+
expect(detectMethods(handler)).toContain('GET');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('detects POST and GET when handler checks both', () => {
|
|
26
|
+
const handler = (ammo) => {
|
|
27
|
+
if (ammo.GET) ammo.fire(200, {});
|
|
28
|
+
if (ammo.POST) ammo.fire(201, {});
|
|
29
|
+
};
|
|
30
|
+
const detected = detectMethods(handler);
|
|
31
|
+
expect(detected).toContain('GET');
|
|
32
|
+
expect(detected).toContain('POST');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('returns all methods when no method checks found (method-agnostic)', () => {
|
|
36
|
+
const handler = () => {};
|
|
37
|
+
expect(detectMethods(handler)).toEqual(ALL_METHODS);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('detects GET and HEAD when handler uses ammo.only("GET")', () => {
|
|
41
|
+
const handler = (ammo) => {
|
|
42
|
+
ammo.only('GET');
|
|
43
|
+
ammo.fire({ status: 'ok' });
|
|
44
|
+
};
|
|
45
|
+
const detected = detectMethods(handler);
|
|
46
|
+
expect(detected).toContain('GET');
|
|
47
|
+
expect(detected).toContain('HEAD');
|
|
48
|
+
expect(detected).toHaveLength(2);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('detects POST and PUT when handler uses ammo.only("POST", "PUT")', () => {
|
|
52
|
+
const handler = (ammo) => {
|
|
53
|
+
ammo.only('POST', 'PUT');
|
|
54
|
+
ammo.fire(200, {});
|
|
55
|
+
};
|
|
56
|
+
const detected = detectMethods(handler);
|
|
57
|
+
expect(detected).toContain('POST');
|
|
58
|
+
expect(detected).toContain('PUT');
|
|
59
|
+
expect(detected).toHaveLength(2);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('detects from ammo.only with double-quoted methods', () => {
|
|
63
|
+
const handler = (ammo) => {
|
|
64
|
+
ammo.only('GET');
|
|
65
|
+
ammo.fire(200);
|
|
66
|
+
};
|
|
67
|
+
const detected = detectMethods(handler);
|
|
68
|
+
expect(detected).toContain('GET');
|
|
69
|
+
expect(detected).toContain('HEAD');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('detects from .only with no space after comma', () => {
|
|
73
|
+
const handler = (ammo) => {
|
|
74
|
+
ammo.only('GET', 'POST');
|
|
75
|
+
ammo.fire(200);
|
|
76
|
+
};
|
|
77
|
+
const detected = detectMethods(handler);
|
|
78
|
+
expect(detected).toContain('GET');
|
|
79
|
+
expect(detected).toContain('HEAD');
|
|
80
|
+
expect(detected).toContain('POST');
|
|
81
|
+
expect(detected).toHaveLength(3);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('prefers ammo.only over property access when both present', () => {
|
|
85
|
+
const handler = (ammo) => {
|
|
86
|
+
ammo.only('POST');
|
|
87
|
+
if (ammo.GET) ammo.fire(200);
|
|
88
|
+
ammo.fire(201, {});
|
|
89
|
+
};
|
|
90
|
+
const detected = detectMethods(handler);
|
|
91
|
+
expect(detected).toEqual(['POST']);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('analyzeHandler', () => {
|
|
96
|
+
it('returns object with methods array', () => {
|
|
97
|
+
const handler = (ammo) => {
|
|
98
|
+
if (ammo.GET) ammo.fire(200);
|
|
99
|
+
};
|
|
100
|
+
const result = analyzeHandler(handler);
|
|
101
|
+
expect(result).toHaveProperty('methods');
|
|
102
|
+
expect(Array.isArray(result.methods)).toBe(true);
|
|
103
|
+
expect(result.methods).toContain('GET');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for auto-docs source-resolver (extractRelativeImports, resolveTargetFilePath, formatDependencyContext).
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {
|
|
7
|
+
extractRelativeImports,
|
|
8
|
+
resolveTargetFilePath,
|
|
9
|
+
formatDependencyContext,
|
|
10
|
+
} from './source-resolver.js';
|
|
11
|
+
|
|
12
|
+
describe('source-resolver', () => {
|
|
13
|
+
describe('extractRelativeImports', () => {
|
|
14
|
+
it('extracts relative import paths', () => {
|
|
15
|
+
const source = `import x from './foo.js';\nimport { y } from '../bar.js';`;
|
|
16
|
+
const imports = extractRelativeImports(source);
|
|
17
|
+
expect(imports).toContain('./foo.js');
|
|
18
|
+
expect(imports).toContain('../bar.js');
|
|
19
|
+
});
|
|
20
|
+
it('ignores absolute or node imports', () => {
|
|
21
|
+
const source = `import path from 'path';\nimport x from './local.js';`;
|
|
22
|
+
const imports = extractRelativeImports(source);
|
|
23
|
+
expect(imports).toEqual(['./local.js']);
|
|
24
|
+
});
|
|
25
|
+
it('returns unique paths', () => {
|
|
26
|
+
const source = `import a from './foo.js';\nimport b from './foo.js';`;
|
|
27
|
+
expect(extractRelativeImports(source)).toHaveLength(1);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('resolveTargetFilePath', () => {
|
|
32
|
+
it('joins dirTargets with groupId and .target.js', () => {
|
|
33
|
+
const resolved = resolveTargetFilePath('users', 'targets');
|
|
34
|
+
expect(resolved).toMatch(/targets[\\/]users\.target\.js$/);
|
|
35
|
+
});
|
|
36
|
+
it('converts groupId slashes to path sep', () => {
|
|
37
|
+
const resolved = resolveTargetFilePath('subdir/users', 'targets');
|
|
38
|
+
expect(resolved).toMatch(/subdir[\\/]users\.target\.js$/);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('formatDependencyContext', () => {
|
|
43
|
+
it('returns empty string for empty sources', () => {
|
|
44
|
+
expect(formatDependencyContext(new Map(), null)).toBe('');
|
|
45
|
+
});
|
|
46
|
+
it('formats map of path -> source with labels', () => {
|
|
47
|
+
const sources = new Map([
|
|
48
|
+
['/cwd/targets/users.target.js', 'const x = 1;'],
|
|
49
|
+
['/cwd/services/user.js', 'const y = 2;'],
|
|
50
|
+
]);
|
|
51
|
+
const targetPath = '/cwd/targets/users.target.js';
|
|
52
|
+
const out = formatDependencyContext(sources, targetPath, 1000);
|
|
53
|
+
expect(out).toContain('Target');
|
|
54
|
+
expect(out).toContain('const x = 1;');
|
|
55
|
+
expect(out).toContain('const y = 2;');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
package/auto-docs/constants.js
CHANGED
|
@@ -7,13 +7,24 @@
|
|
|
7
7
|
export const OPENAPI_VERSION = '3.0.3';
|
|
8
8
|
|
|
9
9
|
/** Lowercase HTTP method names (for method-keyed LLM response detection and OpenAPI operation keys). */
|
|
10
|
-
export const METHOD_KEYS = new Set([
|
|
10
|
+
export const METHOD_KEYS = new Set([
|
|
11
|
+
'get',
|
|
12
|
+
'put',
|
|
13
|
+
'post',
|
|
14
|
+
'delete',
|
|
15
|
+
'patch',
|
|
16
|
+
'head',
|
|
17
|
+
'options',
|
|
18
|
+
]);
|
|
11
19
|
|
|
12
20
|
/** When an endpoint accepts all HTTP methods, document it once under this key. */
|
|
13
21
|
export const METHOD_AGNOSTIC_OPERATION_KEY = 'get';
|
|
14
22
|
|
|
15
23
|
/** Max handler source length by level (chars; tokens roughly scale). Level 1 = moderate, 2 = high. */
|
|
16
|
-
export const HANDLER_SOURCE_MAX_LENGTH_BY_LEVEL = {
|
|
24
|
+
export const HANDLER_SOURCE_MAX_LENGTH_BY_LEVEL = Object.freeze({
|
|
25
|
+
1: 2800,
|
|
26
|
+
2: 6000,
|
|
27
|
+
});
|
|
17
28
|
|
|
18
29
|
/** Default max chars for dependency context in formatDependencyContext and level-2 prompts. */
|
|
19
30
|
export const DEPENDENCY_CONTEXT_MAX_CHARS = 6000;
|
|
@@ -39,7 +39,7 @@ async function generateOpenAPISpec(registry, options = {}) {
|
|
|
39
39
|
logger = null,
|
|
40
40
|
} = options;
|
|
41
41
|
const targets = registry?.targets ?? [];
|
|
42
|
-
const paths =
|
|
42
|
+
const paths = Object.create(null);
|
|
43
43
|
const groupEndpoints = new Map();
|
|
44
44
|
const dependencyContextByGroup = new Map();
|
|
45
45
|
|
|
@@ -71,10 +71,12 @@ async function generateOpenAPISpec(registry, options = {}) {
|
|
|
71
71
|
);
|
|
72
72
|
applyTagDisplayNames(paths, tagDescriptions);
|
|
73
73
|
|
|
74
|
-
const tags = Array.from(tagDescriptions.entries()).map(
|
|
75
|
-
name,
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
const tags = Array.from(tagDescriptions.entries()).map(
|
|
75
|
+
([, { name, description }]) => ({
|
|
76
|
+
name,
|
|
77
|
+
...(description && { description }),
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
78
80
|
|
|
79
81
|
const spec = {
|
|
80
82
|
openapi: OPENAPI_VERSION,
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for auto-docs openapi/generator pure functions.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
toOpenAPIPath,
|
|
7
|
+
getPathParameters,
|
|
8
|
+
getQueryParameters,
|
|
9
|
+
buildSchemaFromMetadata,
|
|
10
|
+
buildResponses,
|
|
11
|
+
buildOperation,
|
|
12
|
+
mergeMetadata,
|
|
13
|
+
} from './generator.js';
|
|
14
|
+
|
|
15
|
+
describe('openapi-generator', () => {
|
|
16
|
+
describe('toOpenAPIPath', () => {
|
|
17
|
+
it('converts :param to {param}', () => {
|
|
18
|
+
expect(toOpenAPIPath('/users/:id')).toBe('/users/{id}');
|
|
19
|
+
});
|
|
20
|
+
it('converts multiple params', () => {
|
|
21
|
+
expect(toOpenAPIPath('/users/:userId/posts/:postId')).toBe(
|
|
22
|
+
'/users/{userId}/posts/{postId}',
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
it('returns / for empty or non-string', () => {
|
|
26
|
+
expect(toOpenAPIPath('')).toBe('/');
|
|
27
|
+
expect(toOpenAPIPath(null)).toBe('/');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('getPathParameters', () => {
|
|
32
|
+
it('extracts path params from te.js path', () => {
|
|
33
|
+
const params = getPathParameters('/users/:id');
|
|
34
|
+
expect(params).toHaveLength(1);
|
|
35
|
+
expect(params[0]).toMatchObject({
|
|
36
|
+
name: 'id',
|
|
37
|
+
in: 'path',
|
|
38
|
+
required: true,
|
|
39
|
+
schema: { type: 'string' },
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
it('returns empty array for path without params', () => {
|
|
43
|
+
expect(getPathParameters('/users')).toEqual([]);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('getQueryParameters', () => {
|
|
48
|
+
it('builds query params from metadata', () => {
|
|
49
|
+
const queryMeta = {
|
|
50
|
+
limit: { type: 'integer', required: false },
|
|
51
|
+
q: { type: 'string', required: true },
|
|
52
|
+
};
|
|
53
|
+
const params = getQueryParameters(queryMeta);
|
|
54
|
+
expect(params).toHaveLength(2);
|
|
55
|
+
expect(params.find((p) => p.name === 'limit')).toMatchObject({
|
|
56
|
+
in: 'query',
|
|
57
|
+
required: false,
|
|
58
|
+
});
|
|
59
|
+
expect(params.find((p) => p.name === 'q')).toMatchObject({
|
|
60
|
+
in: 'query',
|
|
61
|
+
required: true,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
it('returns empty array for invalid meta', () => {
|
|
65
|
+
expect(getQueryParameters(null)).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('buildSchemaFromMetadata', () => {
|
|
70
|
+
it('builds OpenAPI schema from field meta', () => {
|
|
71
|
+
const meta = {
|
|
72
|
+
name: { type: 'string' },
|
|
73
|
+
email: { type: 'string', format: 'email', required: true },
|
|
74
|
+
};
|
|
75
|
+
const schema = buildSchemaFromMetadata(meta);
|
|
76
|
+
expect(schema.type).toBe('object');
|
|
77
|
+
expect(schema.properties.name.type).toBe('string');
|
|
78
|
+
expect(schema.properties.email.format).toBe('email');
|
|
79
|
+
expect(schema.required).toContain('email');
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('buildResponses', () => {
|
|
84
|
+
it('returns 200 Success when responseMeta empty', () => {
|
|
85
|
+
const r = buildResponses(null);
|
|
86
|
+
expect(r['200']).toEqual({ description: 'Success' });
|
|
87
|
+
});
|
|
88
|
+
it('builds responses from metadata', () => {
|
|
89
|
+
const meta = {
|
|
90
|
+
200: { description: 'OK' },
|
|
91
|
+
201: { description: 'Created' },
|
|
92
|
+
};
|
|
93
|
+
const r = buildResponses(meta);
|
|
94
|
+
expect(r['200'].description).toBe('OK');
|
|
95
|
+
expect(r['201'].description).toBe('Created');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('buildOperation', () => {
|
|
100
|
+
it('builds operation with summary and responses', () => {
|
|
101
|
+
const meta = {
|
|
102
|
+
summary: 'Get user',
|
|
103
|
+
response: { 200: { description: 'OK' } },
|
|
104
|
+
};
|
|
105
|
+
const pathParams = [];
|
|
106
|
+
const op = buildOperation('get', meta, pathParams);
|
|
107
|
+
expect(op.summary).toBe('Get user');
|
|
108
|
+
expect(op.responses['200'].description).toBe('OK');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('mergeMetadata', () => {
|
|
113
|
+
it('prefers explicit when preferEnhanced false', () => {
|
|
114
|
+
const explicit = { summary: 'A', description: 'B' };
|
|
115
|
+
const enhanced = { summary: 'C', description: 'D' };
|
|
116
|
+
const merged = mergeMetadata(explicit, enhanced, {
|
|
117
|
+
preferEnhanced: false,
|
|
118
|
+
});
|
|
119
|
+
expect(merged.summary).toBe('A');
|
|
120
|
+
expect(merged.description).toBe('B');
|
|
121
|
+
});
|
|
122
|
+
it('prefers enhanced when preferEnhanced true', () => {
|
|
123
|
+
const explicit = { summary: 'A' };
|
|
124
|
+
const enhanced = { summary: 'C', description: 'D' };
|
|
125
|
+
const merged = mergeMetadata(explicit, enhanced, {
|
|
126
|
+
preferEnhanced: true,
|
|
127
|
+
});
|
|
128
|
+
expect(merged.summary).toBe('C');
|
|
129
|
+
expect(merged.description).toBe('D');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -16,7 +16,12 @@ export function isMethodKeyed(obj) {
|
|
|
16
16
|
for (const key of Object.keys(obj)) {
|
|
17
17
|
if (METHOD_KEYS.has(key.toLowerCase())) {
|
|
18
18
|
const val = obj[key];
|
|
19
|
-
if (
|
|
19
|
+
if (
|
|
20
|
+
val &&
|
|
21
|
+
typeof val === 'object' &&
|
|
22
|
+
(val.summary != null || val.response != null)
|
|
23
|
+
)
|
|
24
|
+
return true;
|
|
20
25
|
}
|
|
21
26
|
}
|
|
22
27
|
return false;
|
|
@@ -82,7 +87,7 @@ export function getQueryParameters(queryMeta) {
|
|
|
82
87
|
*/
|
|
83
88
|
export function buildSchemaFromMetadata(meta) {
|
|
84
89
|
if (!meta || typeof meta !== 'object') return {};
|
|
85
|
-
const properties =
|
|
90
|
+
const properties = Object.create(null);
|
|
86
91
|
const required = [];
|
|
87
92
|
for (const [key, value] of Object.entries(meta)) {
|
|
88
93
|
if (value && typeof value === 'object' && value.type) {
|
|
@@ -91,7 +96,8 @@ export function buildSchemaFromMetadata(meta) {
|
|
|
91
96
|
...(value.description && { description: value.description }),
|
|
92
97
|
...(value.format && { format: value.format }),
|
|
93
98
|
};
|
|
94
|
-
if (value.required === true || value.required === 'true')
|
|
99
|
+
if (value.required === true || value.required === 'true')
|
|
100
|
+
required.push(key);
|
|
95
101
|
}
|
|
96
102
|
}
|
|
97
103
|
if (Object.keys(properties).length === 0) return {};
|
|
@@ -108,7 +114,8 @@ export function buildSchemaFromMetadata(meta) {
|
|
|
108
114
|
* @returns {object|undefined} OpenAPI requestBody or undefined
|
|
109
115
|
*/
|
|
110
116
|
export function buildRequestBody(requestMeta) {
|
|
111
|
-
if (!requestMeta?.body || typeof requestMeta.body !== 'object')
|
|
117
|
+
if (!requestMeta?.body || typeof requestMeta.body !== 'object')
|
|
118
|
+
return undefined;
|
|
112
119
|
const schema = buildSchemaFromMetadata(requestMeta.body);
|
|
113
120
|
if (!schema || Object.keys(schema).length === 0) return undefined;
|
|
114
121
|
return {
|
|
@@ -127,7 +134,7 @@ export function buildRequestBody(requestMeta) {
|
|
|
127
134
|
* @returns {object} OpenAPI responses
|
|
128
135
|
*/
|
|
129
136
|
export function buildResponses(responseMeta) {
|
|
130
|
-
const responses =
|
|
137
|
+
const responses = Object.create(null);
|
|
131
138
|
if (!responseMeta || typeof responseMeta !== 'object') {
|
|
132
139
|
responses['200'] = { description: 'Success' };
|
|
133
140
|
return responses;
|
|
@@ -139,9 +146,10 @@ export function buildResponses(responseMeta) {
|
|
|
139
146
|
...(spec.schema && {
|
|
140
147
|
content: {
|
|
141
148
|
'application/json': {
|
|
142
|
-
schema:
|
|
143
|
-
|
|
144
|
-
|
|
149
|
+
schema:
|
|
150
|
+
typeof spec.schema === 'object' && spec.schema.type
|
|
151
|
+
? spec.schema
|
|
152
|
+
: { type: 'object' },
|
|
145
153
|
},
|
|
146
154
|
},
|
|
147
155
|
}),
|
|
@@ -159,7 +167,8 @@ export function buildResponses(responseMeta) {
|
|
|
159
167
|
* @returns {boolean}
|
|
160
168
|
*/
|
|
161
169
|
export function isMethodAgnostic(methods) {
|
|
162
|
-
if (!Array.isArray(methods) || methods.length !== ALL_METHODS.length)
|
|
170
|
+
if (!Array.isArray(methods) || methods.length !== ALL_METHODS.length)
|
|
171
|
+
return false;
|
|
163
172
|
const set = new Set(methods.map((m) => m.toUpperCase()));
|
|
164
173
|
return ALL_METHODS.every((m) => set.has(m));
|
|
165
174
|
}
|
|
@@ -190,7 +199,10 @@ export function buildOperation(method, meta, pathParams, options = {}) {
|
|
|
190
199
|
};
|
|
191
200
|
const methodUpper = method.toUpperCase();
|
|
192
201
|
const body = buildRequestBody(meta.request);
|
|
193
|
-
if (
|
|
202
|
+
if (
|
|
203
|
+
body &&
|
|
204
|
+
(methodAgnostic || (methodUpper !== 'GET' && methodUpper !== 'HEAD'))
|
|
205
|
+
) {
|
|
194
206
|
op.requestBody = body;
|
|
195
207
|
}
|
|
196
208
|
op.responses = buildResponses(meta.response);
|
|
@@ -207,17 +219,17 @@ export function buildOperation(method, meta, pathParams, options = {}) {
|
|
|
207
219
|
export function mergeMetadata(explicit, enhanced, options = {}) {
|
|
208
220
|
const preferEnhanced = options.preferEnhanced === true;
|
|
209
221
|
const summary = preferEnhanced
|
|
210
|
-
?
|
|
211
|
-
:
|
|
222
|
+
? enhanced?.summary ?? explicit?.summary ?? ''
|
|
223
|
+
: explicit?.summary ?? enhanced?.summary ?? '';
|
|
212
224
|
const description = preferEnhanced
|
|
213
|
-
?
|
|
214
|
-
:
|
|
225
|
+
? enhanced?.description ?? explicit?.description ?? ''
|
|
226
|
+
: explicit?.description ?? enhanced?.description ?? '';
|
|
215
227
|
const request = preferEnhanced
|
|
216
|
-
?
|
|
217
|
-
:
|
|
228
|
+
? enhanced?.request ?? explicit?.request
|
|
229
|
+
: explicit?.request ?? enhanced?.request;
|
|
218
230
|
const response = preferEnhanced
|
|
219
|
-
?
|
|
220
|
-
:
|
|
231
|
+
? enhanced?.response ?? explicit?.response
|
|
232
|
+
: explicit?.response ?? enhanced?.response;
|
|
221
233
|
return {
|
|
222
234
|
summary: summary || 'Endpoint',
|
|
223
235
|
description: description || undefined,
|
|
@@ -234,7 +246,15 @@ export function mergeMetadata(explicit, enhanced, options = {}) {
|
|
|
234
246
|
* @returns {object}
|
|
235
247
|
*/
|
|
236
248
|
export function mergeMethodAgnosticMeta(metaByMethod, methods, fallbackMeta) {
|
|
237
|
-
const preferredOrder = [
|
|
249
|
+
const preferredOrder = [
|
|
250
|
+
'post',
|
|
251
|
+
'put',
|
|
252
|
+
'patch',
|
|
253
|
+
'get',
|
|
254
|
+
'delete',
|
|
255
|
+
'head',
|
|
256
|
+
'options',
|
|
257
|
+
];
|
|
238
258
|
for (const k of preferredOrder) {
|
|
239
259
|
const m = metaByMethod.get(k);
|
|
240
260
|
if (m && (m.summary || m.description)) return m;
|
package/cli/docs-command.js
CHANGED
|
@@ -32,25 +32,25 @@ function mask(value) {
|
|
|
32
32
|
|
|
33
33
|
function ask(question, fallback = '') {
|
|
34
34
|
const hint = fallback ? c.dim(` (${fallback})`) : '';
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
});
|
|
35
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
36
|
+
rl.question(`${c.cyan('?')} ${question}${hint}${c.dim(': ')}`, (answer) => {
|
|
37
|
+
resolve(answer.trim() || fallback);
|
|
39
38
|
});
|
|
39
|
+
return promise;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function askYesNo(question, fallback = false) {
|
|
43
43
|
const hint = fallback ? 'Y/n' : 'y/N';
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
44
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
45
|
+
rl.question(
|
|
46
|
+
`${c.cyan('?')} ${question} ${c.dim(`(${hint})`)}${c.dim(': ')}`,
|
|
47
|
+
(answer) => {
|
|
48
|
+
const val = answer.trim().toLowerCase();
|
|
49
|
+
if (!val) return resolve(fallback);
|
|
50
|
+
resolve(val === 'y' || val === 'yes');
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
return promise;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
async function loadTargetFiles(dirTargets = 'targets') {
|
|
@@ -88,13 +88,15 @@ async function loadTargetFiles(dirTargets = 'targets') {
|
|
|
88
88
|
function getDocsOptionsFromConfig(config = {}) {
|
|
89
89
|
const docs = config.docs || config.generateDocs || {};
|
|
90
90
|
const e = process.env;
|
|
91
|
-
const baseURL =
|
|
91
|
+
const baseURL =
|
|
92
|
+
docs.llm?.baseURL ?? e.LLM_BASE_URL ?? 'https://api.openai.com/v1';
|
|
92
93
|
const apiKey = docs.llm?.apiKey ?? e.LLM_API_KEY ?? e.OPENAI_API_KEY;
|
|
93
94
|
const model = docs.llm?.model ?? e.LLM_MODEL ?? 'gpt-4o-mini';
|
|
94
95
|
if (!apiKey && !e.OPENAI_API_KEY) {
|
|
95
96
|
return null;
|
|
96
97
|
}
|
|
97
|
-
const dirTargets =
|
|
98
|
+
const dirTargets =
|
|
99
|
+
docs.dirTargets ?? docs.dir?.targets ?? config.dir?.targets ?? 'targets';
|
|
98
100
|
const output = docs.output ?? './openapi.json';
|
|
99
101
|
const title = docs.title ?? 'API';
|
|
100
102
|
const version = docs.version ?? '1.0.0';
|
|
@@ -116,7 +118,7 @@ function getDocsOptionsFromConfig(config = {}) {
|
|
|
116
118
|
* @returns {Promise<void>}
|
|
117
119
|
*/
|
|
118
120
|
export async function runDocsCommandCI() {
|
|
119
|
-
const config = loadConfigFile();
|
|
121
|
+
const config = await loadConfigFile();
|
|
120
122
|
const options = getDocsOptionsFromConfig(config);
|
|
121
123
|
if (!options) {
|
|
122
124
|
console.error(
|
|
@@ -128,7 +130,9 @@ export async function runDocsCommandCI() {
|
|
|
128
130
|
process.stdout.write(`${c.yellow('⏳')} Loading targets...`);
|
|
129
131
|
const fileCount = await loadTargetFiles(dirTargets);
|
|
130
132
|
const endpointCount = targetRegistry.targets?.length ?? 0;
|
|
131
|
-
process.stdout.write(
|
|
133
|
+
process.stdout.write(
|
|
134
|
+
`\r${c.green('✓')} Loaded ${c.bold(fileCount)} target file(s) — ${c.bold(endpointCount)} endpoint(s)\n`,
|
|
135
|
+
);
|
|
132
136
|
if (endpointCount === 0) {
|
|
133
137
|
console.log(c.yellow(' No endpoints found. Skipping doc generation.\n'));
|
|
134
138
|
return;
|
|
@@ -156,19 +160,19 @@ export async function runDocsCommandCI() {
|
|
|
156
160
|
* @returns {Promise<string[]>} e.g. ['refs/heads/main', 'refs/heads/feature/x']
|
|
157
161
|
*/
|
|
158
162
|
function readPrePushRefs() {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
});
|
|
163
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
164
|
+
const chunks = [];
|
|
165
|
+
stdin.on('data', (chunk) => chunks.push(chunk));
|
|
166
|
+
stdin.on('end', () => {
|
|
167
|
+
const lines = Buffer.concat(chunks).toString('utf8').trim().split('\n');
|
|
168
|
+
const refs = [];
|
|
169
|
+
for (let i = 0; i < lines.length; i++) {
|
|
170
|
+
const parts = lines[i].split(/\s+/);
|
|
171
|
+
if (parts.length >= 4) refs.push(parts[2]);
|
|
172
|
+
}
|
|
173
|
+
resolve(refs);
|
|
171
174
|
});
|
|
175
|
+
return promise;
|
|
172
176
|
}
|
|
173
177
|
|
|
174
178
|
/**
|
|
@@ -178,7 +182,7 @@ function readPrePushRefs() {
|
|
|
178
182
|
* Configure via tejas.config.json docs.productionBranch or env DOCS_PRODUCTION_BRANCH (default: main).
|
|
179
183
|
*/
|
|
180
184
|
export async function runDocsOnPush() {
|
|
181
|
-
const config = loadConfigFile();
|
|
185
|
+
const config = await loadConfigFile();
|
|
182
186
|
const docs = config.docs || config.generateDocs || {};
|
|
183
187
|
const productionBranch =
|
|
184
188
|
docs.productionBranch ?? process.env.DOCS_PRODUCTION_BRANCH ?? 'main';
|
|
@@ -186,7 +190,11 @@ export async function runDocsOnPush() {
|
|
|
186
190
|
const productionRef = `refs/heads/${productionBranch}`;
|
|
187
191
|
const isPushingToProduction = remoteRefs.some((ref) => ref === productionRef);
|
|
188
192
|
if (!isPushingToProduction) return;
|
|
189
|
-
console.log(
|
|
193
|
+
console.log(
|
|
194
|
+
c.dim(
|
|
195
|
+
` Docs: pushing to ${productionBranch} — generating documentation...\n`,
|
|
196
|
+
),
|
|
197
|
+
);
|
|
190
198
|
await runDocsCommandCI();
|
|
191
199
|
}
|
|
192
200
|
|
|
@@ -207,13 +215,13 @@ function serveDocsPreview(spec, port = 3333) {
|
|
|
207
215
|
res.writeHead(404);
|
|
208
216
|
res.end('Not found');
|
|
209
217
|
});
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
218
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
219
|
+
server.listen(port, () => resolve(server));
|
|
220
|
+
return promise;
|
|
213
221
|
}
|
|
214
222
|
|
|
215
223
|
export async function runDocsCommand() {
|
|
216
|
-
const config = loadConfigFile();
|
|
224
|
+
const config = await loadConfigFile();
|
|
217
225
|
const e = process.env;
|
|
218
226
|
|
|
219
227
|
console.log();
|