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
@@ -7,6 +7,9 @@ class Endpoint {
7
7
  this.path = '';
8
8
  this.middlewares = [];
9
9
  this.handler = null;
10
+ this.metadata = null;
11
+ /** Source group (e.g. target file id) for grouping in docs. Set by loader before register(). */
12
+ this.group = null;
10
13
  }
11
14
 
12
15
  setPath(base, path) {
@@ -37,6 +40,16 @@ class Endpoint {
37
40
  return this;
38
41
  }
39
42
 
43
+ setMetadata(metadata) {
44
+ this.metadata = metadata ?? null;
45
+ return this;
46
+ }
47
+
48
+ setGroup(group) {
49
+ this.group = group ?? null;
50
+ return this;
51
+ }
52
+
40
53
  getPath() {
41
54
  return this.path;
42
55
  }
@@ -48,6 +61,14 @@ class Endpoint {
48
61
  getHandler() {
49
62
  return this.handler;
50
63
  }
64
+
65
+ getMetadata() {
66
+ return this.metadata;
67
+ }
68
+
69
+ getGroup() {
70
+ return this.group;
71
+ }
51
72
  }
52
73
 
53
74
  export default Endpoint;
package/server/handler.js CHANGED
@@ -1,87 +1,113 @@
1
- import { env } from 'tej-env';
2
- import TejLogger from 'tej-logger';
3
- import logHttpRequest from '../utils/request-logger.js';
4
-
5
- import Ammo from './ammo.js';
6
- import TejError from './error.js';
7
- import TargetRegistry from './targets/registry.js';
8
-
9
- const targetRegistry = new TargetRegistry();
10
- const errorLogger = new TejLogger('Tejas.Exception');
11
-
12
- /**
13
- * Executes the middleware and handler chain for a given target.
14
- *
15
- * @param {Object} target - The target endpoint object.
16
- * @param {Ammo} ammo - The Ammo instance containing request and response objects.
17
- * @returns {Promise<void>} A promise that resolves when the chain execution is complete.
18
- */
19
- const executeChain = async (target, ammo) => {
20
- let i = 0;
21
-
22
- const chain = targetRegistry.globalMiddlewares.concat(
23
- target.getMiddlewares(),
24
- );
25
- chain.push(target.getHandler());
26
-
27
- const next = async () => {
28
- const middleware = chain[i];
29
- i++;
30
-
31
- const args =
32
- middleware.length === 3 ? [ammo.req, ammo.res, next] : [ammo, next];
33
-
34
- try {
35
- await middleware(...args);
36
- } catch (err) {
37
- errorHandler(ammo, err);
38
- }
39
- };
40
-
41
- await next();
42
- };
43
-
44
- /**
45
- * Handles errors by logging them and sending an appropriate response.
46
- *
47
- * @param {Ammo} ammo - The Ammo instance containing request and response objects.
48
- * @param {Error} err - The error object to handle.
49
- */
50
- const errorHandler = (ammo, err) => {
51
- if (env('LOG_EXCEPTIONS')) errorLogger.error(err);
52
-
53
- if (err instanceof TejError) return ammo.throw(err);
54
- return ammo.throw(err);
55
- };
56
-
57
- /**
58
- * Main request handler function.
59
- *
60
- * @param {http.IncomingMessage} req - The HTTP request object.
61
- * @param {http.ServerResponse} res - The HTTP response object.
62
- * @returns {Promise<void>} A promise that resolves when the request handling is complete.
63
- */
64
- const handler = async (req, res) => {
65
- const url = req.url.split('?')[0];
66
- const target = targetRegistry.aim(url);
67
- const ammo = new Ammo(req, res);
68
-
69
- try {
70
- if (target) {
71
- await ammo.enhance();
72
-
73
- if (env('LOG_HTTP_REQUESTS')) logHttpRequest(ammo);
74
- await executeChain(target, ammo);
75
- } else {
76
- if (req.url === '/') {
77
- ammo.defaultEntry();
78
- } else {
79
- errorHandler(ammo, new TejError(404, `URL not found: ${url}`));
80
- }
81
- }
82
- } catch (err) {
83
- errorHandler(ammo, err);
84
- }
85
- };
86
-
87
- export default handler;
1
+ import { env } from 'tej-env';
2
+ import TejLogger from 'tej-logger';
3
+ import logHttpRequest from '../utils/request-logger.js';
4
+
5
+ import Ammo from './ammo.js';
6
+ import TejError from './error.js';
7
+ import targetRegistry from './targets/registry.js';
8
+
9
+ const errorLogger = new TejLogger('Tejas.Exception');
10
+
11
+ /**
12
+ * Executes the middleware and handler chain for a given target.
13
+ *
14
+ * @param {Object} target - The target endpoint object.
15
+ * @param {Ammo} ammo - The Ammo instance containing request and response objects.
16
+ * @returns {Promise<void>} A promise that resolves when the chain execution is complete.
17
+ */
18
+ const executeChain = async (target, ammo) => {
19
+ let i = 0;
20
+
21
+ const chain = targetRegistry.globalMiddlewares.concat(
22
+ target.getMiddlewares(),
23
+ );
24
+ chain.push(target.getHandler());
25
+
26
+ const next = async () => {
27
+ // Check if response has already been sent (e.g., by passport.authenticate redirect)
28
+ if (ammo.res.headersSent || ammo.res.writableEnded || ammo.res.finished) {
29
+ return;
30
+ }
31
+
32
+ const middleware = chain[i];
33
+ i++;
34
+
35
+ const args =
36
+ middleware.length === 3 ? [ammo.req, ammo.res, next] : [ammo, next];
37
+
38
+ try {
39
+ const result = await middleware(...args);
40
+
41
+ // Check again after middleware execution (passport might have redirected)
42
+ if (ammo.res.headersSent || ammo.res.writableEnded || ammo.res.finished) {
43
+ return;
44
+ }
45
+
46
+ // If middleware returned a promise that resolved, continue chain
47
+ if (result && typeof result.then === 'function') {
48
+ await result;
49
+ // Check one more time after promise resolution
50
+ if (ammo.res.headersSent || ammo.res.writableEnded || ammo.res.finished) {
51
+ return;
52
+ }
53
+ }
54
+ } catch (err) {
55
+ // Only handle error if response hasn't been sent
56
+ if (!ammo.res.headersSent && !ammo.res.writableEnded && !ammo.res.finished) {
57
+ errorHandler(ammo, err);
58
+ }
59
+ }
60
+ };
61
+
62
+ await next();
63
+ };
64
+
65
+ /**
66
+ * Handles errors by logging them and sending an appropriate response.
67
+ *
68
+ * @param {Ammo} ammo - The Ammo instance containing request and response objects.
69
+ * @param {Error} err - The error object to handle.
70
+ */
71
+ const errorHandler = (ammo, err) => {
72
+ if (env('LOG_EXCEPTIONS')) errorLogger.error(err);
73
+
74
+ if (err instanceof TejError) return ammo.throw(err);
75
+ return ammo.throw(err);
76
+ };
77
+
78
+ /**
79
+ * Main request handler function.
80
+ *
81
+ * @param {http.IncomingMessage} req - The HTTP request object.
82
+ * @param {http.ServerResponse} res - The HTTP response object.
83
+ * @returns {Promise<void>} A promise that resolves when the request handling is complete.
84
+ */
85
+ const handler = async (req, res) => {
86
+ const url = req.url.split('?')[0];
87
+ const match = targetRegistry.aim(url);
88
+ const ammo = new Ammo(req, res);
89
+
90
+ try {
91
+ if (match && match.target) {
92
+ await ammo.enhance();
93
+
94
+ // Add route parameters to ammo.payload
95
+ if (match.params && Object.keys(match.params).length > 0) {
96
+ Object.assign(ammo.payload, match.params);
97
+ }
98
+
99
+ if (env('LOG_HTTP_REQUESTS')) logHttpRequest(ammo);
100
+ await executeChain(match.target, ammo);
101
+ } else {
102
+ if (req.url === '/') {
103
+ ammo.defaultEntry();
104
+ } else {
105
+ errorHandler(ammo, new TejError(404, `URL not found: ${url}`));
106
+ }
107
+ }
108
+ } catch (err) {
109
+ errorHandler(ammo, err);
110
+ }
111
+ };
112
+
113
+ export default handler;
package/server/target.js CHANGED
@@ -3,8 +3,7 @@ import TejLogger from 'tej-logger';
3
3
  import isMiddlewareValid from './targets/middleware-validator.js';
4
4
  import Endpoint from './endpoint.js';
5
5
 
6
- import TargetRegistry from './targets/registry.js';
7
- const targetRegistry = new TargetRegistry();
6
+ import targetRegistry from './targets/registry.js';
8
7
 
9
8
  const logger = new TejLogger('Target');
10
9
 
@@ -110,19 +109,61 @@ class Target {
110
109
  * );
111
110
  */
112
111
  register() {
113
- let args = arguments;
114
- if (!args) return;
112
+ const args = Array.from(arguments);
113
+ if (args.length < 2) {
114
+ logger.error('register(path, [...middlewares], handler) requires at least path and handler. Skipping.');
115
+ return;
116
+ }
115
117
 
116
118
  const path = args[0];
117
119
  const shoot = args[args.length - 1];
118
- const middlewares = Array.from(args).slice(1, args.length - 1);
120
+
121
+ if (typeof path !== 'string') {
122
+ logger.error(`register() path must be a string, got ${typeof path}. Skipping.`);
123
+ return;
124
+ }
125
+ if (typeof shoot !== 'function') {
126
+ logger.error(`register() last argument (handler) must be a function, got ${typeof shoot}. Skipping.`);
127
+ return;
128
+ }
129
+
130
+ const second = args[1];
131
+ const isPlainObject = (v) =>
132
+ typeof v === 'object' &&
133
+ v !== null &&
134
+ !Array.isArray(v) &&
135
+ (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null);
136
+ const isMetadataObject = isPlainObject(second);
137
+
138
+ let middlewares;
139
+ let metadata = null;
140
+ if (isMetadataObject && args.length >= 3) {
141
+ metadata = second;
142
+ middlewares = args.slice(2, -1);
143
+ } else {
144
+ middlewares = args.slice(1, -1);
145
+ }
119
146
 
120
147
  try {
121
148
  const endpoint = new Endpoint();
122
- endpoint
123
- .setPath(this.base, path)
124
- .setMiddlewares(middlewares)
125
- .setHandler(shoot);
149
+ endpoint.setPath(this.base, path);
150
+ if (!endpoint.getPath()) {
151
+ logger.error(`Invalid path for endpoint "${path}". Skipping.`);
152
+ return;
153
+ }
154
+ endpoint.setMiddlewares(middlewares);
155
+ endpoint.setHandler(shoot);
156
+ if (!endpoint.getHandler()) {
157
+ logger.error(`Invalid handler for endpoint "${path}". Skipping.`);
158
+ return;
159
+ }
160
+ if (metadata !== null) {
161
+ endpoint.setMetadata(metadata);
162
+ }
163
+ const group = targetRegistry.getCurrentSourceGroup();
164
+ if (group != null) {
165
+ endpoint.setGroup(group);
166
+ }
126
167
 
127
168
  targetRegistry.targets.push(endpoint);
128
169
  } catch (error) {
@@ -1,4 +1,5 @@
1
1
  import isMiddlewareValid from './middleware-validator.js';
2
+ import { standardizePath } from './path-validator.js';
2
3
 
3
4
  class TargetRegistry {
4
5
  constructor() {
@@ -11,6 +12,16 @@ class TargetRegistry {
11
12
  // TODO - Add a default target
12
13
  this.targets = [];
13
14
  this.globalMiddlewares = [];
15
+ /** Current source group (target file id) set by loader before importing a target file. */
16
+ this._currentSourceGroup = null;
17
+ }
18
+
19
+ setCurrentSourceGroup(group) {
20
+ this._currentSourceGroup = group ?? null;
21
+ }
22
+
23
+ getCurrentSourceGroup() {
24
+ return this._currentSourceGroup;
14
25
  }
15
26
 
16
27
  addGlobalMiddleware() {
@@ -32,13 +43,107 @@ class TargetRegistry {
32
43
  }
33
44
  }
34
45
 
46
+ /**
47
+ * Matches an endpoint URL to a registered target, supporting parameterized routes.
48
+ *
49
+ * @param {string} endpoint - The endpoint URL to match
50
+ * @returns {Object|null} An object with `target` and `params`, or null if no match
51
+ */
35
52
  aim(endpoint) {
36
- return this.targets.find((target) => {
37
- return target.getPath() === endpoint;
53
+ const standardizedEndpoint = standardizePath(endpoint);
54
+
55
+ // First, try exact match (most specific)
56
+ const exactMatch = this.targets.find((target) => {
57
+ return target.getPath() === standardizedEndpoint;
38
58
  });
59
+
60
+ if (exactMatch) {
61
+ return { target: exactMatch, params: {} };
62
+ }
63
+
64
+ // Then, try parameterized route matching
65
+ for (const target of this.targets) {
66
+ const targetPath = target.getPath();
67
+ const params = this.matchParameterizedRoute(
68
+ targetPath,
69
+ standardizedEndpoint,
70
+ );
71
+
72
+ if (params !== null) {
73
+ return { target, params };
74
+ }
75
+ }
76
+
77
+ return null;
39
78
  }
40
79
 
41
- getAllEndpoints(grouped) {
80
+ /**
81
+ * Matches a parameterized route pattern against an actual URL.
82
+ *
83
+ * @param {string} pattern - The route pattern (e.g., '/api/categories/:id')
84
+ * @param {string} url - The actual URL to match (e.g., '/api/categories/123')
85
+ * @returns {Object|null} An object with extracted parameters, or null if no match
86
+ */
87
+ matchParameterizedRoute(pattern, url) {
88
+ // Handle root path case
89
+ if (pattern === '/' && url === '/') {
90
+ return {};
91
+ }
92
+
93
+ // Split both pattern and URL into segments
94
+ const patternSegments = pattern.split('/').filter((s) => s.length > 0);
95
+ const urlSegments = url.split('/').filter((s) => s.length > 0);
96
+
97
+ // Must have same number of segments
98
+ if (patternSegments.length !== urlSegments.length) {
99
+ return null;
100
+ }
101
+
102
+ // If both are empty (root paths), they match
103
+ if (patternSegments.length === 0 && urlSegments.length === 0) {
104
+ return {};
105
+ }
106
+
107
+ const params = {};
108
+
109
+ // Match each segment
110
+ for (let i = 0; i < patternSegments.length; i++) {
111
+ const patternSegment = patternSegments[i];
112
+ const urlSegment = urlSegments[i];
113
+
114
+ // If it's a parameter (starts with :)
115
+ if (patternSegment.startsWith(':')) {
116
+ const paramName = patternSegment.slice(1); // Remove the ':'
117
+ params[paramName] = urlSegment;
118
+ } else if (patternSegment !== urlSegment) {
119
+ // If it's not a parameter and doesn't match, no match
120
+ return null;
121
+ }
122
+ }
123
+
124
+ return params;
125
+ }
126
+
127
+ /**
128
+ * Get all registered endpoints.
129
+ *
130
+ * @param {boolean|{ detailed?: boolean, grouped?: boolean }} [options] - If boolean, treated as grouped (backward compat).
131
+ * If object: detailed=true returns full metadata per endpoint; grouped=true returns paths grouped by first segment.
132
+ * @returns {string[]|Object|Array<{ path: string, metadata: object|null, handler: function }>}
133
+ */
134
+ getAllEndpoints(options = {}) {
135
+ const grouped =
136
+ typeof options === 'boolean' ? options : (options && options.grouped);
137
+ const detailed =
138
+ typeof options === 'object' && options && options.detailed === true;
139
+
140
+ if (detailed) {
141
+ return this.targets.map((t) => ({
142
+ path: t.getPath(),
143
+ metadata: t.getMetadata(),
144
+ handler: t.getHandler(),
145
+ }));
146
+ }
42
147
  if (grouped) {
43
148
  return this.targets.reduce((acc, target) => {
44
149
  const group = target.getPath().split('/')[1];
@@ -46,10 +151,10 @@ class TargetRegistry {
46
151
  acc[group].push(target.getPath());
47
152
  return acc;
48
153
  }, {});
49
- } else {
50
- return this.targets.map((target) => target.getPath());
51
154
  }
155
+ return this.targets.map((target) => target.getPath());
52
156
  }
53
157
  }
54
158
 
55
- export default TargetRegistry;
159
+ const targetRegistry = new TargetRegistry();
160
+ export default targetRegistry;