te.js 1.3.0 → 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.
Files changed (80) hide show
  1. package/.cursor/plans/ai_native_framework_features_5bb1a20a.plan.md +234 -0
  2. package/.cursor/plans/auto_error_fix_agent_e68979c5.plan.md +356 -0
  3. package/.cursor/plans/tejas_framework_test_suite_5e3c6fad.plan.md +168 -0
  4. package/.prettierignore +31 -0
  5. package/README.md +156 -14
  6. package/auto-docs/analysis/handler-analyzer.js +58 -0
  7. package/auto-docs/analysis/source-resolver.js +101 -0
  8. package/auto-docs/constants.js +37 -0
  9. package/auto-docs/index.js +146 -0
  10. package/auto-docs/llm/index.js +6 -0
  11. package/auto-docs/llm/parse.js +88 -0
  12. package/auto-docs/llm/prompts.js +222 -0
  13. package/auto-docs/llm/provider.js +187 -0
  14. package/auto-docs/openapi/endpoint-processor.js +277 -0
  15. package/auto-docs/openapi/generator.js +107 -0
  16. package/auto-docs/openapi/level3.js +131 -0
  17. package/auto-docs/openapi/spec-builders.js +244 -0
  18. package/auto-docs/ui/docs-ui.js +186 -0
  19. package/auto-docs/utils/logger.js +17 -0
  20. package/auto-docs/utils/strip-usage.js +10 -0
  21. package/cli/docs-command.js +315 -0
  22. package/cli/fly-command.js +71 -0
  23. package/cli/index.js +57 -0
  24. package/database/index.js +163 -5
  25. package/database/mongodb.js +146 -0
  26. package/database/redis.js +201 -0
  27. package/docs/README.md +36 -0
  28. package/docs/ammo.md +362 -0
  29. package/docs/api-reference.md +489 -0
  30. package/docs/auto-docs.md +215 -0
  31. package/docs/cli.md +152 -0
  32. package/docs/configuration.md +233 -0
  33. package/docs/database.md +391 -0
  34. package/docs/error-handling.md +417 -0
  35. package/docs/file-uploads.md +334 -0
  36. package/docs/getting-started.md +181 -0
  37. package/docs/middleware.md +356 -0
  38. package/docs/rate-limiting.md +394 -0
  39. package/docs/routing.md +302 -0
  40. package/example/API_OVERVIEW.md +77 -0
  41. package/example/README.md +155 -0
  42. package/example/index.js +27 -2
  43. package/example/openapi.json +390 -0
  44. package/example/package.json +5 -2
  45. package/example/services/cache.service.js +25 -0
  46. package/example/services/user.service.js +42 -0
  47. package/example/start-redis.js +2 -0
  48. package/example/targets/cache.target.js +35 -0
  49. package/example/targets/index.target.js +11 -2
  50. package/example/targets/users.target.js +60 -0
  51. package/example/tejas.config.json +13 -1
  52. package/package.json +20 -5
  53. package/rate-limit/algorithms/fixed-window.js +141 -0
  54. package/rate-limit/algorithms/sliding-window.js +147 -0
  55. package/rate-limit/algorithms/token-bucket.js +115 -0
  56. package/rate-limit/base.js +165 -0
  57. package/rate-limit/index.js +147 -0
  58. package/rate-limit/storage/base.js +104 -0
  59. package/rate-limit/storage/memory.js +102 -0
  60. package/rate-limit/storage/redis.js +88 -0
  61. package/server/ammo/body-parser.js +152 -25
  62. package/server/ammo/enhancer.js +6 -2
  63. package/server/ammo.js +356 -327
  64. package/server/endpoint.js +21 -0
  65. package/server/handler.js +113 -87
  66. package/server/target.js +50 -9
  67. package/server/targets/registry.js +111 -6
  68. package/te.js +363 -137
  69. package/tests/auto-docs/handler-analyzer.test.js +44 -0
  70. package/tests/auto-docs/openapi-generator.test.js +103 -0
  71. package/tests/auto-docs/parse.test.js +63 -0
  72. package/tests/auto-docs/source-resolver.test.js +58 -0
  73. package/tests/helpers/index.js +37 -0
  74. package/tests/helpers/mock-http.js +342 -0
  75. package/tests/helpers/test-utils.js +446 -0
  76. package/tests/setup.test.js +148 -0
  77. package/utils/configuration.js +13 -10
  78. package/vitest.config.js +54 -0
  79. package/database/mongo.js +0 -67
  80. package/example/targets/user/user.target.js +0 -17
package/te.js CHANGED
@@ -1,137 +1,363 @@
1
- import { createServer } from 'node:http';
2
-
3
- import { env, setEnv } from 'tej-env';
4
- import TejLogger from 'tej-logger';
5
- import database from './database/index.js';
6
-
7
- import TargetRegistry from './server/targets/registry.js';
8
-
9
- import { loadConfigFile, standardizeObj } from './utils/configuration.js';
10
-
11
- import targetHandler from './server/handler.js';
12
- import { findTargetFiles } from './utils/auto-register.js';
13
- import { pathToFileURL } from 'node:url';
14
- import { log } from 'node:console';
15
-
16
- const logger = new TejLogger('Tejas');
17
- const targetRegistry = new TargetRegistry();
18
-
19
- class Tejas {
20
- /*
21
- * Constructor for Tejas
22
- * @param {Object} args - Arguments for Tejas
23
- * @param {Number} args.port - Port to run Tejas on
24
- * @param {Boolean} args.log.http_requests - Whether to log incoming HTTP requests
25
- * @param {Boolean} args.log.exceptions - Whether to log exceptions
26
- * @param {String} args.db.type - Database type. It can be 'mongodb', 'mysql', 'postgres', 'sqlite'
27
- * @param {String} args.db.uri - Connection URI string for the database
28
- */
29
- constructor(args) {
30
- if (Tejas.instance) return Tejas.instance;
31
- Tejas.instance = this;
32
-
33
- this.generateConfiguration(args);
34
- this.registerTargetsDir();
35
- }
36
-
37
- /*
38
- * Connect to a database
39
- * @param {Object}
40
- * @param {String} args.db - Database type. It can be 'mongodb', 'mysql', 'postgres', 'sqlite'
41
- * @param {String} args.uri - Connection URI string for the database
42
- * @param {Object} args.options - Options for the database connection
43
- */
44
- connectDatabase(args) {
45
- const db = env('DB_TYPE');
46
- const uri = env('DB_URI');
47
-
48
- if (!db) return;
49
- if (!uri) {
50
- logger.error(
51
- `Tejas could not connect to ${db} as it couldn't find a connection URI. See our documentation for more information.`,
52
- false,
53
- );
54
- return;
55
- }
56
-
57
- const connect = database[db];
58
- if (!connect) {
59
- logger.error(
60
- `Tejas could not connect to ${db} as it is not supported. See our documentation for more information.`,
61
- false,
62
- );
63
- return;
64
- }
65
-
66
- connect(uri, {}, (error) => {
67
- if (error) {
68
- logger.error(
69
- `Tejas could not connect to ${db}. Error: ${error}`,
70
- false,
71
- );
72
- return;
73
- }
74
-
75
- logger.info(`Tejas connected to ${db} successfully.`);
76
- });
77
- }
78
-
79
- generateConfiguration(options) {
80
- const configVars = standardizeObj(loadConfigFile());
81
- const envVars = standardizeObj(process.env);
82
- const userVars = standardizeObj(options);
83
-
84
- const config = { ...configVars, ...envVars, ...userVars };
85
- for (const key in config) {
86
- if (config.hasOwnProperty(key)) {
87
- setEnv(key, config[key]);
88
- }
89
- }
90
-
91
- // Load defaults
92
- if (!env('PORT')) setEnv('PORT', 1403);
93
- }
94
-
95
- midair() {
96
- if (!arguments) return;
97
- targetRegistry.addGlobalMiddleware(...arguments);
98
- }
99
-
100
- registerTargetsDir() {
101
- findTargetFiles()
102
- .then((targetFiles) => {
103
- if (targetFiles) {
104
- for (const file of targetFiles) {
105
- import(pathToFileURL(`${file.parentPath}/${file.name}`));
106
- }
107
- }
108
- })
109
- .catch((err) => {
110
- logger.error(
111
- `Tejas could not register target files. Error: ${err}`,
112
- false,
113
- );
114
- });
115
- }
116
-
117
- takeoff() {
118
- this.engine = createServer(targetHandler);
119
- this.engine.listen(env('PORT'), () => {
120
- logger.info(`Took off from port ${env('PORT')}`);
121
- this.connectDatabase();
122
- });
123
- }
124
- }
125
-
126
- const listAllEndpoints = (grouped = false) => {
127
- return targetRegistry.getAllEndpoints(grouped);
128
- };
129
-
130
- export { default as Target } from './server/target.js';
131
- export { default as TejFileUploader } from './server/files/uploader.js';
132
- export { default as TejError } from './server/error.js';
133
- export { listAllEndpoints };
134
- export default Tejas;
135
-
136
- // TODO Ability to register a target (route) from tejas instance
137
- // TODO tejas as CLI tool
1
+ import { createServer } from 'node:http';
2
+ import { env, setEnv } from 'tej-env';
3
+ import TejLogger from 'tej-logger';
4
+ import rateLimiter from './rate-limit/index.js';
5
+
6
+ import targetRegistry from './server/targets/registry.js';
7
+ import dbManager from './database/index.js';
8
+
9
+ import { loadConfigFile, standardizeObj } from './utils/configuration.js';
10
+
11
+ import targetHandler from './server/handler.js';
12
+ import path from 'node:path';
13
+ import { pathToFileURL } from 'node:url';
14
+ import { readFile } from 'node:fs/promises';
15
+ import { findTargetFiles } from './utils/auto-register.js';
16
+ import { registerDocRoutes } from './auto-docs/ui/docs-ui.js';
17
+
18
+ const logger = new TejLogger('Tejas');
19
+
20
+ /**
21
+ * Main Tejas Framework Class
22
+ *
23
+ * @class
24
+ * @description
25
+ * Tejas is a Node.js framework for building powerful backend services.
26
+ * It provides features like routing, middleware support, database connections,
27
+ * and automatic target (route) registration.
28
+ */
29
+ class Tejas {
30
+ /**
31
+ * Creates a new Tejas instance with the specified configuration
32
+ *
33
+ * @param {Object} [args] - Configuration options for Tejas
34
+ * @param {number} [args.port] - Port number to run the server on (defaults to 1403)
35
+ * @param {Object} [args.log] - Logging configuration
36
+ * @param {boolean} [args.log.http_requests] - Whether to log incoming HTTP requests
37
+ * @param {boolean} [args.log.exceptions] - Whether to log exceptions
38
+ * @param {Object} [args.db] - Database configuration options
39
+ * @param {Object} [args.withRedis] - Redis connection configuration
40
+ * @param {boolean} [args.withRedis.isCluster=false] - Whether to use Redis Cluster
41
+ * @param {Object} [args.withRedis.socket] - Redis socket connection options
42
+ * @param {string} [args.withRedis.socket.host] - Redis server hostname
43
+ * @param {number} [args.withRedis.socket.port] - Redis server port
44
+ * @param {boolean} [args.withRedis.socket.tls] - Whether to use TLS for connection
45
+ * @param {string} [args.withRedis.url] - Redis connection URL (alternative to socket config)
46
+ */
47
+ constructor(args) {
48
+ if (Tejas.instance) return Tejas.instance;
49
+ Tejas.instance = this;
50
+
51
+ this.options = args || {};
52
+
53
+ this.generateConfiguration();
54
+ this.registerTargetsDir();
55
+ }
56
+
57
+ /**
58
+ * Generates and loads configuration from multiple sources
59
+ *
60
+ * @private
61
+ * @description
62
+ * Loads and merges configuration from:
63
+ * 1. tejas.config.json file (lowest priority)
64
+ * 2. Environment variables
65
+ * 3. Constructor options (highest priority)
66
+ *
67
+ * All configuration keys are standardized to uppercase and flattened.
68
+ * Sets default values for required configuration if not provided.
69
+ */
70
+ generateConfiguration() {
71
+ const configVars = standardizeObj(loadConfigFile());
72
+ const envVars = standardizeObj(process.env);
73
+ const userVars = standardizeObj(this.options);
74
+
75
+ const config = { ...configVars, ...envVars, ...userVars };
76
+
77
+ for (const key in config) {
78
+ if (config.hasOwnProperty(key)) {
79
+ setEnv(key, config[key]);
80
+ }
81
+ }
82
+
83
+ // Set default values for required configuration if not provided
84
+ if (!env('PORT')) setEnv('PORT', 1403);
85
+ if (!env('BODY_MAX_SIZE')) setEnv('BODY_MAX_SIZE', 10 * 1024 * 1024); // 10MB default
86
+ if (!env('BODY_TIMEOUT')) setEnv('BODY_TIMEOUT', 30000); // 30 seconds default
87
+ }
88
+
89
+ /**
90
+ * Registers global middleware functions
91
+ *
92
+ * @param {...Function} arguments - Middleware functions to register globally
93
+ * @description
94
+ * Middleware functions are executed in order for all incoming requests.
95
+ * Each middleware should have the signature (ammo, next) or (req, res, next).
96
+ *
97
+ * @example
98
+ * app.midair(
99
+ * (ammo, next) => {
100
+ * console.log('Request received');
101
+ * next();
102
+ * },
103
+ * authenticationMiddleware
104
+ * );
105
+ */
106
+ midair() {
107
+ if (arguments.length === 0) return;
108
+ targetRegistry.addGlobalMiddleware(...arguments);
109
+ }
110
+
111
+ /**
112
+ * Automatically registers target files from the configured directory
113
+ *
114
+ * @private
115
+ * @description
116
+ * Searches for and registers all files ending in 'target.js' from the
117
+ * directory specified by DIR_TARGETS environment variable.
118
+ * Target files define routes and their handlers.
119
+ *
120
+ * @throws {Error} If target files cannot be registered
121
+ */
122
+ registerTargetsDir() {
123
+ const baseDir = path.join(process.cwd(), process.env.DIR_TARGETS || '');
124
+ findTargetFiles()
125
+ .then((targetFiles) => {
126
+ if (!targetFiles?.length) return;
127
+ (async () => {
128
+ for (const file of targetFiles) {
129
+ const parentPath = file.path || '';
130
+ const fullPath = path.isAbsolute(parentPath)
131
+ ? path.join(parentPath, file.name)
132
+ : path.join(baseDir, parentPath, file.name);
133
+ const relativePath = path.relative(baseDir, fullPath);
134
+ const groupId = relativePath
135
+ .replace(/\.target\.js$/i, '')
136
+ .replace(/\\/g, '/')
137
+ || 'index';
138
+ targetRegistry.setCurrentSourceGroup(groupId);
139
+ try {
140
+ await import(pathToFileURL(fullPath).href);
141
+ } finally {
142
+ targetRegistry.setCurrentSourceGroup(null);
143
+ }
144
+ }
145
+ })().catch((err) => {
146
+ logger.error(
147
+ `Tejas could not register target files. Error: ${err}`,
148
+ false,
149
+ );
150
+ });
151
+ })
152
+ .catch((err) => {
153
+ logger.error(
154
+ `Tejas could not register target files. Error: ${err}`,
155
+ false,
156
+ );
157
+ });
158
+ }
159
+
160
+ /**
161
+ * Starts the Tejas server
162
+ *
163
+ * @param {Object} [options] - Server configuration options
164
+ * @param {Object} [options.withRedis] - Redis connection options
165
+ * @param {Object} [options.withMongo] - MongoDB connection options (https://www.mongodb.com/docs/drivers/node/current/fundamentals/connection/)
166
+ * @description
167
+ * Creates and starts an HTTP server on the configured port.
168
+ * Optionally initializes Redis and/or MongoDB connections if configuration is provided.
169
+ * For Redis, accepts cluster flag and all connection options supported by node-redis package.
170
+ * For MongoDB, accepts all connection options supported by the official MongoDB Node.js driver.
171
+ *
172
+ * @example
173
+ * const app = new Tejas();
174
+ *
175
+ * // Start server with Redis and MongoDB
176
+ * app.takeoff({
177
+ * withRedis: {
178
+ * url: 'redis://alice:foobared@awesome.redis.server:6380',
179
+ * isCluster: false
180
+ * },
181
+ * withMongo: { url: 'mongodb://localhost:27017/mydatabase' }
182
+ * });
183
+ *
184
+ * // Start server with only Redis using defaults
185
+ * app.takeoff({
186
+ * withRedis: { url: 'redis://localhost:6379' }
187
+ * });
188
+ *
189
+ * // Start server without databases
190
+ * app.takeoff(); // Server starts on default port 1403
191
+ */
192
+ takeoff({ withRedis, withMongo } = {}) {
193
+ this.engine = createServer(targetHandler);
194
+ this.engine.listen(env('PORT'), async () => {
195
+ logger.info(`Took off from port ${env('PORT')}`);
196
+
197
+ if (withRedis) await this.withRedis(withRedis);
198
+ if (withMongo) await this.withMongo(withMongo);
199
+ });
200
+
201
+ this.engine.on('error', (err) => {
202
+ logger.error(`Server error: ${err}`);
203
+ });
204
+ }
205
+
206
+ /**
207
+ * Initializes a Redis connection
208
+ *
209
+ * @param {Object} [config] - Redis connection configuration
210
+ * @param {boolean} [config.isCluster=false] - Whether to use Redis Cluster
211
+ * @param {Object} [config.socket] - Redis socket connection options
212
+ * @param {string} [config.socket.host] - Redis server hostname
213
+ * @param {number} [config.socket.port] - Redis server port
214
+ * @param {boolean} [config.socket.tls] - Whether to use TLS for connection
215
+ * @param {string} [config.url] - Redis connection URL (alternative to socket config)
216
+ * @returns {Promise<Tejas>} Returns a Promise that resolves to this instance for chaining
217
+ *
218
+ * @example
219
+ * // Initialize Redis with URL
220
+ * await app.withRedis({
221
+ * url: 'redis://localhost:6379'
222
+ * }).withRateLimit({
223
+ * maxRequests: 100,
224
+ * store: 'redis'
225
+ * });
226
+ *
227
+ * @example
228
+ * // Initialize Redis with socket options
229
+ * await app.withRedis({
230
+ * socket: {
231
+ * host: 'localhost',
232
+ * port: 6379
233
+ * }
234
+ * });
235
+ */
236
+ async withRedis(config) {
237
+ if (config) {
238
+ await dbManager.initializeConnection('redis', config);
239
+ } else {
240
+ logger.warn(
241
+ 'No Redis configuration provided. Skipping Redis connection.',
242
+ );
243
+ }
244
+
245
+ return this;
246
+ }
247
+
248
+ /**
249
+ * Initializes a MongoDB connection
250
+ *
251
+ * @param {Object} [config] - MongoDB connection configuration
252
+ * @param {string} [config.uri] - MongoDB connection URI
253
+ * @param {Object} [config.options] - Additional MongoDB connection options
254
+ * @returns {Tejas} Returns a Promise that resolves to this instance for chaining
255
+ *
256
+ * @example
257
+ * // Initialize MongoDB with URI
258
+ * await app.withMongo({
259
+ * uri: 'mongodb://localhost:27017/myapp'
260
+ * });
261
+ *
262
+ * @example
263
+ * // Initialize MongoDB with options
264
+ * await app.withMongo({
265
+ * uri: 'mongodb://localhost:27017/myapp',
266
+ * options: {
267
+ * useNewUrlParser: true,
268
+ * useUnifiedTopology: true
269
+ * }
270
+ * });
271
+ *
272
+ * @example
273
+ * // Chain database connections
274
+ * await app
275
+ * .withMongo({
276
+ * uri: 'mongodb://localhost:27017/myapp'
277
+ * })
278
+ * .withRedis({
279
+ * url: 'redis://localhost:6379'
280
+ * })
281
+ * .withRateLimit({
282
+ * maxRequests: 100,
283
+ * store: 'redis'
284
+ * });
285
+ */
286
+ withMongo(config) {
287
+ if (config) {
288
+ dbManager.initializeConnection('mongodb', config);
289
+ } else {
290
+ logger.warn(
291
+ 'No MongoDB configuration provided. Skipping MongoDB connection.',
292
+ );
293
+ }
294
+ return this;
295
+ }
296
+
297
+ /**
298
+ * Adds global rate limiting to all endpoints
299
+ *
300
+ * @param {Object} config - Rate limiting configuration
301
+ * @param {number} [config.maxRequests=60] - Maximum number of requests allowed in the time window
302
+ * @param {number} [config.timeWindowSeconds=60] - Time window in seconds
303
+ * @param {string} [config.algorithm='sliding-window'] - Rate-limiting algorithm ('token-bucket', 'sliding-window', or 'fixed-window')
304
+ * @param {Object} [config.algorithmOptions] - Algorithm-specific options
305
+ * @param {Object} [config.redis] - Redis configuration for distributed rate limiting
306
+ * @param {Function} [config.keyGenerator] - Function to generate unique identifiers (defaults to IP-based)
307
+ * @param {Object} [config.headerFormat] - Rate limit header format configuration
308
+ * @returns {Tejas} The Tejas instance for chaining
309
+ *
310
+ */
311
+ withRateLimit(config) {
312
+ if (!config) {
313
+ logger.warn(
314
+ 'No rate limit configuration provided. Skipping rate limit setup.',
315
+ );
316
+ return this;
317
+ }
318
+
319
+ this.midair(rateLimiter(config));
320
+ return this;
321
+ }
322
+
323
+ /**
324
+ * Serves the API documentation at GET /docs and GET /docs/openapi.json from a pre-generated spec file.
325
+ * Generate the spec with `tejas generate:docs`, then call this to serve it on your app.
326
+ * Uses Scalar API Reference; default layout is 'classic' so the test request appears on the same page (not in a dialog).
327
+ *
328
+ * @param {Object} [config] - Configuration
329
+ * @param {string} [config.specPath='./openapi.json'] - Path to the OpenAPI spec JSON file (relative to process.cwd())
330
+ * @param {object} [config.scalarConfig] - Optional Scalar API Reference config (e.g. { layout: 'modern' } for dialog try-it)
331
+ * @returns {Tejas} The Tejas instance for chaining
332
+ *
333
+ * @example
334
+ * app.serveDocs({ specPath: './openapi.json' });
335
+ * app.serveDocs({ specPath: './openapi.json', scalarConfig: { layout: 'modern' } });
336
+ * app.takeoff();
337
+ */
338
+ serveDocs(config = {}) {
339
+ const specPath = path.resolve(process.cwd(), config.specPath || './openapi.json');
340
+ const { scalarConfig } = config;
341
+ const getSpec = async () => {
342
+ const content = await readFile(specPath, 'utf8');
343
+ return JSON.parse(content);
344
+ };
345
+ registerDocRoutes({ getSpec, specUrl: '/docs/openapi.json', scalarConfig }, targetRegistry);
346
+ return this;
347
+ }
348
+
349
+ }
350
+
351
+ const listAllEndpoints = (grouped = false) => {
352
+ return targetRegistry.getAllEndpoints(grouped);
353
+ };
354
+
355
+ export { default as Target } from './server/target.js';
356
+ export { default as TejFileUploader } from './server/files/uploader.js';
357
+ export { default as TejError } from './server/error.js';
358
+ export { listAllEndpoints };
359
+
360
+ export default Tejas;
361
+
362
+ // TODO Ability to register a target (route) from tejas instance
363
+ // TODO tejas as CLI tool
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Unit tests for auto-docs handler-analyzer (detectMethods, analyzeHandler).
3
+ */
4
+ import { describe, it, expect } from 'vitest';
5
+ import { detectMethods, analyzeHandler, ALL_METHODS } from '../../auto-docs/analysis/handler-analyzer.js';
6
+
7
+ describe('handler-analyzer', () => {
8
+ describe('detectMethods', () => {
9
+ it('returns all methods when handler is not a function', () => {
10
+ expect(detectMethods(null)).toEqual(ALL_METHODS);
11
+ expect(detectMethods(undefined)).toEqual(ALL_METHODS);
12
+ });
13
+
14
+ it('detects GET when handler uses ammo.GET', () => {
15
+ const handler = (ammo) => { if (ammo.GET) ammo.fire(200, {}); };
16
+ expect(detectMethods(handler)).toContain('GET');
17
+ });
18
+
19
+ it('detects POST and GET when handler checks both', () => {
20
+ const handler = (ammo) => {
21
+ if (ammo.GET) ammo.fire(200, {});
22
+ if (ammo.POST) ammo.fire(201, {});
23
+ };
24
+ const detected = detectMethods(handler);
25
+ expect(detected).toContain('GET');
26
+ expect(detected).toContain('POST');
27
+ });
28
+
29
+ it('returns all methods when no method checks found (method-agnostic)', () => {
30
+ const handler = () => {};
31
+ expect(detectMethods(handler)).toEqual(ALL_METHODS);
32
+ });
33
+ });
34
+
35
+ describe('analyzeHandler', () => {
36
+ it('returns object with methods array', () => {
37
+ const handler = (ammo) => { if (ammo.GET) ammo.fire(200); };
38
+ const result = analyzeHandler(handler);
39
+ expect(result).toHaveProperty('methods');
40
+ expect(Array.isArray(result.methods)).toBe(true);
41
+ expect(result.methods).toContain('GET');
42
+ });
43
+ });
44
+ });
@@ -0,0 +1,103 @@
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 '../../auto-docs/openapi/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('/users/{userId}/posts/{postId}');
22
+ });
23
+ it('returns / for empty or non-string', () => {
24
+ expect(toOpenAPIPath('')).toBe('/');
25
+ expect(toOpenAPIPath(null)).toBe('/');
26
+ });
27
+ });
28
+
29
+ describe('getPathParameters', () => {
30
+ it('extracts path params from te.js path', () => {
31
+ const params = getPathParameters('/users/:id');
32
+ expect(params).toHaveLength(1);
33
+ expect(params[0]).toMatchObject({ name: 'id', in: 'path', required: true, schema: { type: 'string' } });
34
+ });
35
+ it('returns empty array for path without params', () => {
36
+ expect(getPathParameters('/users')).toEqual([]);
37
+ });
38
+ });
39
+
40
+ describe('getQueryParameters', () => {
41
+ it('builds query params from metadata', () => {
42
+ const queryMeta = { limit: { type: 'integer', required: false }, q: { type: 'string', required: true } };
43
+ const params = getQueryParameters(queryMeta);
44
+ expect(params).toHaveLength(2);
45
+ expect(params.find((p) => p.name === 'limit')).toMatchObject({ in: 'query', required: false });
46
+ expect(params.find((p) => p.name === 'q')).toMatchObject({ in: 'query', required: true });
47
+ });
48
+ it('returns empty array for invalid meta', () => {
49
+ expect(getQueryParameters(null)).toEqual([]);
50
+ });
51
+ });
52
+
53
+ describe('buildSchemaFromMetadata', () => {
54
+ it('builds OpenAPI schema from field meta', () => {
55
+ const meta = { name: { type: 'string' }, email: { type: 'string', format: 'email', required: true } };
56
+ const schema = buildSchemaFromMetadata(meta);
57
+ expect(schema.type).toBe('object');
58
+ expect(schema.properties.name.type).toBe('string');
59
+ expect(schema.properties.email.format).toBe('email');
60
+ expect(schema.required).toContain('email');
61
+ });
62
+ });
63
+
64
+ describe('buildResponses', () => {
65
+ it('returns 200 Success when responseMeta empty', () => {
66
+ const r = buildResponses(null);
67
+ expect(r['200']).toEqual({ description: 'Success' });
68
+ });
69
+ it('builds responses from metadata', () => {
70
+ const meta = { 200: { description: 'OK' }, 201: { description: 'Created' } };
71
+ const r = buildResponses(meta);
72
+ expect(r['200'].description).toBe('OK');
73
+ expect(r['201'].description).toBe('Created');
74
+ });
75
+ });
76
+
77
+ describe('buildOperation', () => {
78
+ it('builds operation with summary and responses', () => {
79
+ const meta = { summary: 'Get user', response: { 200: { description: 'OK' } } };
80
+ const pathParams = [];
81
+ const op = buildOperation('get', meta, pathParams);
82
+ expect(op.summary).toBe('Get user');
83
+ expect(op.responses['200'].description).toBe('OK');
84
+ });
85
+ });
86
+
87
+ describe('mergeMetadata', () => {
88
+ it('prefers explicit when preferEnhanced false', () => {
89
+ const explicit = { summary: 'A', description: 'B' };
90
+ const enhanced = { summary: 'C', description: 'D' };
91
+ const merged = mergeMetadata(explicit, enhanced, { preferEnhanced: false });
92
+ expect(merged.summary).toBe('A');
93
+ expect(merged.description).toBe('B');
94
+ });
95
+ it('prefers enhanced when preferEnhanced true', () => {
96
+ const explicit = { summary: 'A' };
97
+ const enhanced = { summary: 'C', description: 'D' };
98
+ const merged = mergeMetadata(explicit, enhanced, { preferEnhanced: true });
99
+ expect(merged.summary).toBe('C');
100
+ expect(merged.description).toBe('D');
101
+ });
102
+ });
103
+ });