te.js 2.1.6 → 2.2.1
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 +1 -12
- 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/docs/README.md +1 -2
- package/docs/api-reference.md +124 -186
- package/docs/configuration.md +0 -13
- package/docs/getting-started.md +19 -21
- package/docs/rate-limiting.md +59 -58
- package/lib/llm/client.js +7 -2
- package/lib/llm/index.js +14 -1
- package/lib/llm/parse.test.js +60 -0
- package/package.json +3 -1
- package/radar/index.js +382 -0
- package/rate-limit/base.js +12 -15
- package/rate-limit/index.js +19 -22
- package/rate-limit/index.test.js +93 -0
- package/rate-limit/storage/memory.js +13 -13
- package/rate-limit/storage/redis-install.js +70 -0
- package/rate-limit/storage/redis.js +94 -52
- 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 +138 -12
- 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/channels.test.js +148 -0
- package/server/errors/channels/index.js +1 -1
- package/server/errors/llm-cache.js +1 -1
- package/server/errors/llm-cache.test.js +160 -0
- package/server/errors/llm-error-service.js +1 -1
- package/server/errors/llm-rate-limiter.test.js +105 -0
- package/server/files/uploader.js +38 -26
- package/server/handler.js +1 -1
- package/server/targets/registry.js +3 -3
- package/server/targets/registry.test.js +108 -0
- package/te.js +233 -183
- 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 +74 -8
- package/utils/request-logger.js +49 -3
- package/utils/startup.js +80 -0
- package/database/index.js +0 -165
- package/database/mongodb.js +0 -146
- package/database/redis.js +0 -201
- package/docs/database.md +0 -390
package/README.md
CHANGED
|
@@ -41,7 +41,6 @@ api.register('/hello/:name', (ammo) => {
|
|
|
41
41
|
app.takeoff();
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
|
|
45
44
|
## Features
|
|
46
45
|
|
|
47
46
|
- **AI-Native (MCP)** — Ship with an MCP server so AI assistants can scaffold projects, generate routes, and write correct code with full framework knowledge
|
|
@@ -50,14 +49,12 @@ app.takeoff();
|
|
|
50
49
|
- **Zero-Config Error Handling** — No try-catch needed! Tejas catches all errors automatically. Opt in to have an LLM determine error code and message when you don't specify them (see [Error Handling](./docs/error-handling.md))
|
|
51
50
|
- **Built-in Rate Limiting** — Three algorithms (Token Bucket, Sliding Window, Fixed Window) with memory or Redis storage
|
|
52
51
|
- **Method Safety & CORS** — Opt-in method restriction per route (`register(path, { methods }, handler)` or `ammo.only('GET')`), global allowed-methods filter, and `app.withCORS()` for cross-origin requests
|
|
53
|
-
- **Database Ready** — First-class Redis and MongoDB support with auto-install of drivers
|
|
54
52
|
- **File Uploads** — Easy file handling with size limits and type validation
|
|
55
53
|
- **Auto-Documentation** — Generate OpenAPI specs from your code with LLM-powered analysis (`tejas generate:docs`)
|
|
56
54
|
- **Interactive API Docs** — Serve a Scalar API reference UI with `app.serveDocs()`
|
|
57
55
|
- **Auto-Discovery** — Automatic route registration from `.target.js` files
|
|
58
56
|
- **Request Logging** — Built-in HTTP request and exception logging
|
|
59
57
|
|
|
60
|
-
|
|
61
58
|
## AI-Assisted Setup (MCP)
|
|
62
59
|
|
|
63
60
|
> **Recommended** — The best way to get started with Tejas in the age of AI.
|
|
@@ -79,8 +76,7 @@ The [Tejas MCP server](https://www.npmjs.com/package/tejas-mcp) gives your IDE's
|
|
|
79
76
|
|
|
80
77
|
**Other MCP-compatible IDEs** — run `npx tejas-mcp` as the server command (stdio transport, no config needed).
|
|
81
78
|
|
|
82
|
-
Once connected, prompt your AI with things like
|
|
83
|
-
|
|
79
|
+
Once connected, prompt your AI with things like _"Scaffold a new te.js project called my-api"_ or _"Create a REST API with user CRUD routes"_ — the assistant will generate framework-correct code using real te.js patterns.
|
|
84
80
|
|
|
85
81
|
## Quick Start
|
|
86
82
|
|
|
@@ -132,7 +128,6 @@ node index.js
|
|
|
132
128
|
# Server running at http://localhost:3000
|
|
133
129
|
```
|
|
134
130
|
|
|
135
|
-
|
|
136
131
|
## Core Concepts
|
|
137
132
|
|
|
138
133
|
| Tejas Term | Purpose | Express Equivalent |
|
|
@@ -144,7 +139,6 @@ node index.js
|
|
|
144
139
|
| `midair()` | Register middleware | `use()` |
|
|
145
140
|
| `takeoff()` | Start server | `listen()` |
|
|
146
141
|
|
|
147
|
-
|
|
148
142
|
## CLI
|
|
149
143
|
|
|
150
144
|
```bash
|
|
@@ -153,7 +147,6 @@ tejas generate:docs [--ci] # Generate OpenAPI docs (interactive or CI mode)
|
|
|
153
147
|
tejas docs:on-push # Auto-generate docs when pushing to production branch
|
|
154
148
|
```
|
|
155
149
|
|
|
156
|
-
|
|
157
150
|
## API Documentation
|
|
158
151
|
|
|
159
152
|
Generate and serve interactive API docs:
|
|
@@ -168,7 +161,6 @@ app.takeoff();
|
|
|
168
161
|
// Visit http://localhost:1403/docs
|
|
169
162
|
```
|
|
170
163
|
|
|
171
|
-
|
|
172
164
|
## Documentation
|
|
173
165
|
|
|
174
166
|
For comprehensive documentation, see the [docs folder](./docs) or visit [tejas-documentation.vercel.app](https://tejas-documentation.vercel.app).
|
|
@@ -179,19 +171,16 @@ For comprehensive documentation, see the [docs folder](./docs) or visit [tejas-d
|
|
|
179
171
|
- [Ammo](./docs/ammo.md) — Request/response handling
|
|
180
172
|
- [Middleware](./docs/middleware.md) — Global, target, and route middleware
|
|
181
173
|
- [Error Handling](./docs/error-handling.md) — Zero-config error handling
|
|
182
|
-
- [Database](./docs/database.md) — Redis and MongoDB integration
|
|
183
174
|
- [Rate Limiting](./docs/rate-limiting.md) — API protection
|
|
184
175
|
- [File Uploads](./docs/file-uploads.md) — File handling
|
|
185
176
|
- [CLI Reference](./docs/cli.md) — Command-line interface
|
|
186
177
|
- [Auto-Documentation](./docs/auto-docs.md) — OpenAPI generation
|
|
187
178
|
- [API Reference](./docs/api-reference.md) — Complete API docs
|
|
188
179
|
|
|
189
|
-
|
|
190
180
|
## Contributing
|
|
191
181
|
|
|
192
182
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
193
183
|
|
|
194
|
-
|
|
195
184
|
## License
|
|
196
185
|
|
|
197
186
|
ISC © [Hirak Chhatbar](https://github.com/hirakchhatbar)
|
|
@@ -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;
|