te.js 1.3.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/plans/ai_native_framework_features_5bb1a20a.plan.md +234 -0
- package/.cursor/plans/auto_error_fix_agent_e68979c5.plan.md +356 -0
- package/.cursor/plans/tejas_framework_test_suite_5e3c6fad.plan.md +168 -0
- package/.prettierignore +31 -0
- package/README.md +156 -14
- package/auto-docs/analysis/handler-analyzer.js +58 -0
- package/auto-docs/analysis/source-resolver.js +101 -0
- package/auto-docs/constants.js +37 -0
- package/auto-docs/index.js +146 -0
- package/auto-docs/llm/index.js +6 -0
- package/auto-docs/llm/parse.js +88 -0
- package/auto-docs/llm/prompts.js +222 -0
- package/auto-docs/llm/provider.js +187 -0
- package/auto-docs/openapi/endpoint-processor.js +277 -0
- package/auto-docs/openapi/generator.js +107 -0
- package/auto-docs/openapi/level3.js +131 -0
- package/auto-docs/openapi/spec-builders.js +244 -0
- package/auto-docs/ui/docs-ui.js +186 -0
- package/auto-docs/utils/logger.js +17 -0
- package/auto-docs/utils/strip-usage.js +10 -0
- package/cli/docs-command.js +315 -0
- package/cli/fly-command.js +71 -0
- package/cli/index.js +57 -0
- package/database/index.js +163 -5
- package/database/mongodb.js +146 -0
- package/database/redis.js +201 -0
- package/docs/README.md +36 -0
- package/docs/ammo.md +362 -0
- package/docs/api-reference.md +489 -0
- package/docs/auto-docs.md +215 -0
- package/docs/cli.md +152 -0
- package/docs/configuration.md +233 -0
- package/docs/database.md +391 -0
- package/docs/error-handling.md +417 -0
- package/docs/file-uploads.md +334 -0
- package/docs/getting-started.md +181 -0
- package/docs/middleware.md +356 -0
- package/docs/rate-limiting.md +394 -0
- package/docs/routing.md +302 -0
- package/example/API_OVERVIEW.md +77 -0
- package/example/README.md +155 -0
- package/example/index.js +27 -2
- package/example/openapi.json +390 -0
- package/example/package.json +5 -2
- package/example/services/cache.service.js +25 -0
- package/example/services/user.service.js +42 -0
- package/example/start-redis.js +2 -0
- package/example/targets/cache.target.js +35 -0
- package/example/targets/index.target.js +11 -2
- package/example/targets/users.target.js +60 -0
- package/example/tejas.config.json +13 -1
- package/package.json +20 -5
- package/rate-limit/algorithms/fixed-window.js +141 -0
- package/rate-limit/algorithms/sliding-window.js +147 -0
- package/rate-limit/algorithms/token-bucket.js +115 -0
- package/rate-limit/base.js +165 -0
- package/rate-limit/index.js +147 -0
- package/rate-limit/storage/base.js +104 -0
- package/rate-limit/storage/memory.js +102 -0
- package/rate-limit/storage/redis.js +88 -0
- package/server/ammo/body-parser.js +152 -25
- package/server/ammo/enhancer.js +6 -2
- package/server/ammo.js +356 -327
- package/server/endpoint.js +21 -0
- package/server/handler.js +113 -87
- package/server/target.js +50 -9
- package/server/targets/registry.js +160 -57
- package/te.js +363 -137
- package/tests/auto-docs/handler-analyzer.test.js +44 -0
- package/tests/auto-docs/openapi-generator.test.js +103 -0
- package/tests/auto-docs/parse.test.js +63 -0
- package/tests/auto-docs/source-resolver.test.js +58 -0
- package/tests/helpers/index.js +37 -0
- package/tests/helpers/mock-http.js +342 -0
- package/tests/helpers/test-utils.js +446 -0
- package/tests/setup.test.js +148 -0
- package/utils/configuration.js +13 -10
- package/vitest.config.js +54 -0
- package/database/mongo.js +0 -67
- package/example/targets/user/user.target.js +0 -17
package/docs/ammo.md
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
# Ammo - Request & Response
|
|
2
|
+
|
|
3
|
+
The `Ammo` class is Tejas's unified request/response object. It wraps Node.js's `req` and `res` objects, providing a clean API for handling HTTP interactions.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Every route handler receives an `ammo` object:
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
target.register('/example', (ammo) => {
|
|
11
|
+
// ammo contains everything you need
|
|
12
|
+
console.log(ammo.method); // 'GET'
|
|
13
|
+
console.log(ammo.payload); // { query: 'params', body: 'data' }
|
|
14
|
+
ammo.fire({ success: true });
|
|
15
|
+
});
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Properties
|
|
19
|
+
|
|
20
|
+
### HTTP Method Flags
|
|
21
|
+
|
|
22
|
+
Boolean flags for quick method checking:
|
|
23
|
+
|
|
24
|
+
```javascript
|
|
25
|
+
ammo.GET // true if GET request
|
|
26
|
+
ammo.POST // true if POST request
|
|
27
|
+
ammo.PUT // true if PUT request
|
|
28
|
+
ammo.DELETE // true if DELETE request
|
|
29
|
+
ammo.PATCH // true if PATCH request
|
|
30
|
+
ammo.HEAD // true if HEAD request
|
|
31
|
+
ammo.OPTIONS // true if OPTIONS request
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Usage:**
|
|
35
|
+
|
|
36
|
+
```javascript
|
|
37
|
+
target.register('/resource', (ammo) => {
|
|
38
|
+
if (ammo.GET) {
|
|
39
|
+
return ammo.fire({ items: [] });
|
|
40
|
+
}
|
|
41
|
+
if (ammo.POST) {
|
|
42
|
+
return ammo.fire(201, { created: true });
|
|
43
|
+
}
|
|
44
|
+
ammo.notAllowed();
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Request Data
|
|
49
|
+
|
|
50
|
+
| Property | Type | Description |
|
|
51
|
+
|----------|------|-------------|
|
|
52
|
+
| `ammo.method` | string | HTTP method (`'GET'`, `'POST'`, etc.) |
|
|
53
|
+
| `ammo.payload` | object | Combined body, query params, and route params |
|
|
54
|
+
| `ammo.headers` | object | Request headers (lowercase keys) |
|
|
55
|
+
| `ammo.ip` | string | Client IP address |
|
|
56
|
+
|
|
57
|
+
### URL Data
|
|
58
|
+
|
|
59
|
+
| Property | Type | Description |
|
|
60
|
+
|----------|------|-------------|
|
|
61
|
+
| `ammo.path` | string | Full URL path with query string |
|
|
62
|
+
| `ammo.endpoint` | string | Path without query string |
|
|
63
|
+
| `ammo.protocol` | string | `'http'` or `'https'` |
|
|
64
|
+
| `ammo.hostname` | string | Request hostname |
|
|
65
|
+
| `ammo.fullURL` | string | Complete URL |
|
|
66
|
+
|
|
67
|
+
### Raw Objects
|
|
68
|
+
|
|
69
|
+
| Property | Type | Description |
|
|
70
|
+
|----------|------|-------------|
|
|
71
|
+
| `ammo.req` | IncomingMessage | Node.js request object |
|
|
72
|
+
| `ammo.res` | ServerResponse | Node.js response object |
|
|
73
|
+
|
|
74
|
+
### Response Data
|
|
75
|
+
|
|
76
|
+
| Property | Type | Description |
|
|
77
|
+
|----------|------|-------------|
|
|
78
|
+
| `ammo.dispatchedData` | any | The data sent via the most recent `fire()` call. `undefined` until `fire()` is called |
|
|
79
|
+
|
|
80
|
+
## The Payload Object
|
|
81
|
+
|
|
82
|
+
`ammo.payload` is a merged object containing data from three sources, applied in this order (later sources override earlier ones for the same key):
|
|
83
|
+
|
|
84
|
+
1. **Query parameters** — From the URL query string
|
|
85
|
+
2. **Request body** — Parsed JSON, URL-encoded form data, or multipart form data
|
|
86
|
+
3. **Route parameters** — From parameterized routes (`:id`) — highest priority
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
// Request: POST /users/123?notify=true
|
|
90
|
+
// Body: { "name": "John", "email": "john@example.com" }
|
|
91
|
+
|
|
92
|
+
target.register('/users/:id', (ammo) => {
|
|
93
|
+
console.log(ammo.payload);
|
|
94
|
+
// {
|
|
95
|
+
// id: '123', // Route param
|
|
96
|
+
// notify: 'true', // Query param
|
|
97
|
+
// name: 'John', // Body
|
|
98
|
+
// email: 'john@example.com' // Body
|
|
99
|
+
// }
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Methods
|
|
104
|
+
|
|
105
|
+
### fire() — Send Response
|
|
106
|
+
|
|
107
|
+
The primary method for sending responses:
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
// Send JSON with 200 status (default)
|
|
111
|
+
ammo.fire({ message: 'Success' });
|
|
112
|
+
|
|
113
|
+
// Send with specific status code
|
|
114
|
+
ammo.fire(201, { id: 1, created: true });
|
|
115
|
+
|
|
116
|
+
// Send just a status code
|
|
117
|
+
ammo.fire(204);
|
|
118
|
+
|
|
119
|
+
// Send plain text
|
|
120
|
+
ammo.fire('Hello, World!');
|
|
121
|
+
|
|
122
|
+
// Send HTML with custom content type
|
|
123
|
+
ammo.fire(200, '<h1>Hello</h1>', 'text/html');
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**All signatures:**
|
|
127
|
+
|
|
128
|
+
| Call | Status | Body | Content-Type |
|
|
129
|
+
|------|--------|------|-------------|
|
|
130
|
+
| `fire()` | 204 | *(empty)* | — |
|
|
131
|
+
| `fire("text")` | 200 | `text` | `text/plain` |
|
|
132
|
+
| `fire({ json })` | 200 | JSON string | `application/json` |
|
|
133
|
+
| `fire(201)` | 201 | status message | `text/plain` |
|
|
134
|
+
| `fire(201, data)` | 201 | `data` | auto-detected |
|
|
135
|
+
| `fire(200, html, "text/html")` | 200 | `html` | `text/html` |
|
|
136
|
+
|
|
137
|
+
After `fire()` is called, the sent data is available as `ammo.dispatchedData`.
|
|
138
|
+
|
|
139
|
+
### throw() — Send Error Response
|
|
140
|
+
|
|
141
|
+
For intentional error responses:
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
// Send 500 Internal Server Error
|
|
145
|
+
ammo.throw();
|
|
146
|
+
|
|
147
|
+
// Send specific error code
|
|
148
|
+
ammo.throw(404);
|
|
149
|
+
ammo.throw(404, 'User not found');
|
|
150
|
+
|
|
151
|
+
// Throw from Error object
|
|
152
|
+
ammo.throw(new Error('Something went wrong'));
|
|
153
|
+
|
|
154
|
+
// Throw TejError
|
|
155
|
+
import { TejError } from 'te.js';
|
|
156
|
+
throw new TejError(400, 'Invalid input');
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**All `throw()` signatures:**
|
|
160
|
+
|
|
161
|
+
| Call | Status | Message |
|
|
162
|
+
|------|--------|---------|
|
|
163
|
+
| `throw()` | 500 | `"Internal Server Error"` |
|
|
164
|
+
| `throw(404)` | 404 | Default message for that status code |
|
|
165
|
+
| `throw(404, "msg")` | 404 | `"msg"` |
|
|
166
|
+
| `throw(new TejError(code, msg))` | `code` | `msg` |
|
|
167
|
+
| `throw(new Error("msg"))` | 500 | `"msg"` (or parses numeric messages as status codes) |
|
|
168
|
+
|
|
169
|
+
> **Note:** You don't need try-catch blocks in your handlers! Tejas automatically catches all errors and converts them to appropriate HTTP responses. Use `throw()` or `TejError` only for intentional, expected error conditions. See [Error Handling](./error-handling.md) for details.
|
|
170
|
+
|
|
171
|
+
### redirect() — HTTP Redirect
|
|
172
|
+
|
|
173
|
+
```javascript
|
|
174
|
+
// Temporary redirect (302)
|
|
175
|
+
ammo.redirect('/new-location');
|
|
176
|
+
|
|
177
|
+
// Permanent redirect (301)
|
|
178
|
+
ammo.redirect('/new-location', 301);
|
|
179
|
+
|
|
180
|
+
// External redirect
|
|
181
|
+
ammo.redirect('https://example.com');
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Convenience Error Methods
|
|
185
|
+
|
|
186
|
+
```javascript
|
|
187
|
+
ammo.notFound(); // Throws 404 Not Found
|
|
188
|
+
ammo.notAllowed(); // Throws 405 Method Not Allowed
|
|
189
|
+
ammo.unauthorized(); // Throws 401 Unauthorized
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Working with Headers
|
|
193
|
+
|
|
194
|
+
### Reading Headers
|
|
195
|
+
|
|
196
|
+
```javascript
|
|
197
|
+
target.register('/example', (ammo) => {
|
|
198
|
+
const authHeader = ammo.headers['authorization'];
|
|
199
|
+
const contentType = ammo.headers['content-type'];
|
|
200
|
+
const userAgent = ammo.headers['user-agent'];
|
|
201
|
+
|
|
202
|
+
ammo.fire({ userAgent });
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Setting Response Headers
|
|
207
|
+
|
|
208
|
+
Use the underlying `res` object:
|
|
209
|
+
|
|
210
|
+
```javascript
|
|
211
|
+
target.register('/example', (ammo) => {
|
|
212
|
+
ammo.res.setHeader('X-Custom-Header', 'value');
|
|
213
|
+
ammo.res.setHeader('Cache-Control', 'max-age=3600');
|
|
214
|
+
|
|
215
|
+
ammo.fire({ data: 'with headers' });
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Content Types
|
|
220
|
+
|
|
221
|
+
`fire()` automatically sets `Content-Type` based on the data:
|
|
222
|
+
|
|
223
|
+
| Data Type | Content-Type |
|
|
224
|
+
|-----------|--------------|
|
|
225
|
+
| Object/Array | `application/json` |
|
|
226
|
+
| String | `text/plain` |
|
|
227
|
+
| Buffer | `application/octet-stream` |
|
|
228
|
+
|
|
229
|
+
Override with the third parameter:
|
|
230
|
+
|
|
231
|
+
```javascript
|
|
232
|
+
ammo.fire(200, htmlString, 'text/html');
|
|
233
|
+
ammo.fire(200, xmlString, 'application/xml');
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Examples
|
|
237
|
+
|
|
238
|
+
### REST API Resource
|
|
239
|
+
|
|
240
|
+
```javascript
|
|
241
|
+
import { Target, TejError } from 'te.js';
|
|
242
|
+
|
|
243
|
+
const users = new Target('/users');
|
|
244
|
+
|
|
245
|
+
// GET /users - List all
|
|
246
|
+
// POST /users - Create new
|
|
247
|
+
users.register('/', async (ammo) => {
|
|
248
|
+
if (ammo.GET) {
|
|
249
|
+
const { page = 1, limit = 10 } = ammo.payload;
|
|
250
|
+
const users = await getUsers(page, limit);
|
|
251
|
+
return ammo.fire({ users, page, limit });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (ammo.POST) {
|
|
255
|
+
const { name, email } = ammo.payload;
|
|
256
|
+
|
|
257
|
+
if (!name || !email) {
|
|
258
|
+
throw new TejError(400, 'Name and email are required');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const user = await createUser({ name, email });
|
|
262
|
+
return ammo.fire(201, user);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
ammo.notAllowed();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// GET /users/:id - Get one
|
|
269
|
+
// PUT /users/:id - Update
|
|
270
|
+
// DELETE /users/:id - Delete
|
|
271
|
+
users.register('/:id', async (ammo) => {
|
|
272
|
+
const { id } = ammo.payload;
|
|
273
|
+
|
|
274
|
+
if (ammo.GET) {
|
|
275
|
+
const user = await getUser(id);
|
|
276
|
+
if (!user) throw new TejError(404, 'User not found');
|
|
277
|
+
return ammo.fire(user);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (ammo.PUT) {
|
|
281
|
+
const { name, email } = ammo.payload;
|
|
282
|
+
const user = await updateUser(id, { name, email });
|
|
283
|
+
return ammo.fire(user);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (ammo.DELETE) {
|
|
287
|
+
await deleteUser(id);
|
|
288
|
+
return ammo.fire(204);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
ammo.notAllowed();
|
|
292
|
+
});
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### File Download
|
|
296
|
+
|
|
297
|
+
```javascript
|
|
298
|
+
import fs from 'fs';
|
|
299
|
+
import path from 'path';
|
|
300
|
+
|
|
301
|
+
target.register('/download/:filename', (ammo) => {
|
|
302
|
+
const { filename } = ammo.payload;
|
|
303
|
+
const filepath = path.join('uploads', filename);
|
|
304
|
+
|
|
305
|
+
if (!fs.existsSync(filepath)) {
|
|
306
|
+
throw new TejError(404, 'File not found');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const file = fs.readFileSync(filepath);
|
|
310
|
+
|
|
311
|
+
ammo.res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
|
312
|
+
ammo.fire(200, file, 'application/octet-stream');
|
|
313
|
+
});
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Streaming Response
|
|
317
|
+
|
|
318
|
+
For large responses, use the raw `res` object:
|
|
319
|
+
|
|
320
|
+
```javascript
|
|
321
|
+
target.register('/stream', (ammo) => {
|
|
322
|
+
ammo.res.setHeader('Content-Type', 'text/event-stream');
|
|
323
|
+
ammo.res.setHeader('Cache-Control', 'no-cache');
|
|
324
|
+
ammo.res.setHeader('Connection', 'keep-alive');
|
|
325
|
+
|
|
326
|
+
let count = 0;
|
|
327
|
+
const interval = setInterval(() => {
|
|
328
|
+
ammo.res.write(`data: ${JSON.stringify({ count: ++count })}\n\n`);
|
|
329
|
+
|
|
330
|
+
if (count >= 10) {
|
|
331
|
+
clearInterval(interval);
|
|
332
|
+
ammo.res.end();
|
|
333
|
+
}
|
|
334
|
+
}, 1000);
|
|
335
|
+
});
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Adding Custom Properties
|
|
339
|
+
|
|
340
|
+
Extend `ammo` in middleware:
|
|
341
|
+
|
|
342
|
+
```javascript
|
|
343
|
+
// In middleware
|
|
344
|
+
const authMiddleware = async (ammo, next) => {
|
|
345
|
+
const token = ammo.headers.authorization;
|
|
346
|
+
const user = await verifyToken(token);
|
|
347
|
+
|
|
348
|
+
ammo.user = user; // Add user
|
|
349
|
+
ammo.isAdmin = user.role === 'admin';
|
|
350
|
+
|
|
351
|
+
next();
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// In handler
|
|
355
|
+
target.register('/profile', authMiddleware, (ammo) => {
|
|
356
|
+
ammo.fire({
|
|
357
|
+
user: ammo.user,
|
|
358
|
+
isAdmin: ammo.isAdmin
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
```
|
|
362
|
+
|