te.js 2.1.2 → 2.1.4
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.js +46 -4
- package/docs/ammo.md +34 -14
- package/docs/configuration.md +71 -38
- package/docs/routing.md +2 -0
- package/package.json +1 -1
- package/server/ammo/dispatch-helper.js +50 -41
- package/server/handler.js +9 -0
- package/utils/response-config.js +38 -0
|
@@ -15,9 +15,49 @@ const ALL_METHODS = [
|
|
|
15
15
|
];
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* Extracts allowed methods from .only('GET'), .only("POST", "PUT"), etc. in source.
|
|
19
|
+
* Returns a non-empty array only when at least one valid quoted method is found;
|
|
20
|
+
* otherwise [] so caller can fall back to other detection.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} src - Handler source (e.g. handler.toString())
|
|
23
|
+
* @returns {string[]} Normalized method names (uppercase, HEAD added when GET present), or []
|
|
24
|
+
*/
|
|
25
|
+
function detectOnlyMethods(src) {
|
|
26
|
+
const startMarker = '.only(';
|
|
27
|
+
const start = src.indexOf(startMarker);
|
|
28
|
+
if (start === -1) return [];
|
|
29
|
+
|
|
30
|
+
let depth = 1;
|
|
31
|
+
let pos = start + startMarker.length;
|
|
32
|
+
while (pos < src.length && depth > 0) {
|
|
33
|
+
const ch = src[pos];
|
|
34
|
+
if (ch === '(') depth += 1;
|
|
35
|
+
else if (ch === ')') depth -= 1;
|
|
36
|
+
pos += 1;
|
|
37
|
+
}
|
|
38
|
+
const argsStr = src.slice(start + startMarker.length, pos - 1);
|
|
39
|
+
|
|
40
|
+
// Match quoted method names: 'GET', "POST", etc. Only accept known methods.
|
|
41
|
+
const quotedMethodRe = /['"](GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)['"]/gi;
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
let match;
|
|
44
|
+
while ((match = quotedMethodRe.exec(argsStr)) !== null) {
|
|
45
|
+
seen.add(match[1].toUpperCase());
|
|
46
|
+
}
|
|
47
|
+
if (seen.size === 0) return [];
|
|
48
|
+
|
|
49
|
+
const list = [...seen];
|
|
50
|
+
if (list.includes('GET') && !list.includes('HEAD')) {
|
|
51
|
+
list.push('HEAD');
|
|
52
|
+
}
|
|
53
|
+
return list;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Detects which HTTP methods the handler checks (e.g. ammo.GET, ammo.POST)
|
|
58
|
+
* or restricts via ammo.only('GET'), ammo.only('GET','POST').
|
|
59
|
+
* Prefers .only(...) when present with valid string args; otherwise matches
|
|
60
|
+
* property access like `.GET`, `ammo.GET`.
|
|
21
61
|
*
|
|
22
62
|
* When no method checks are found, the endpoint is treated as method-agnostic
|
|
23
63
|
* and accepts ALL methods (te.js default behavior).
|
|
@@ -29,8 +69,10 @@ function detectMethods(handler) {
|
|
|
29
69
|
if (typeof handler !== 'function') return [...ALL_METHODS];
|
|
30
70
|
|
|
31
71
|
const src = handler.toString();
|
|
32
|
-
const
|
|
72
|
+
const onlyMethods = detectOnlyMethods(src);
|
|
73
|
+
if (onlyMethods.length > 0) return onlyMethods;
|
|
33
74
|
|
|
75
|
+
const detected = [];
|
|
34
76
|
for (const m of ALL_METHODS) {
|
|
35
77
|
// Match property access patterns like .GET, ammo.GET, avoiding
|
|
36
78
|
// false positives in strings or unrelated identifiers
|
package/docs/ammo.md
CHANGED
|
@@ -123,16 +123,36 @@ ammo.fire('Hello, World!');
|
|
|
123
123
|
ammo.fire(200, '<h1>Hello</h1>', 'text/html');
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
+
**Response structure (default, recommended)**
|
|
127
|
+
|
|
128
|
+
Tejas wraps responses in a consistent envelope so clients always get the same shape. This is **enabled by default** and applies to every `fire()` call (and to `ammo.throw()`, `ammo.unauthorized()`, and other error methods, since they use `fire()` internally):
|
|
129
|
+
|
|
130
|
+
```javascript
|
|
131
|
+
ammo.fire(200, { id: 1, name: 'Alice' });
|
|
132
|
+
// Wire: { "data": { "id": 1, "name": "Alice" } }
|
|
133
|
+
|
|
134
|
+
ammo.fire(400, 'Email is required');
|
|
135
|
+
// Wire: { "error": "Email is required" }
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
- **2xx** → body is wrapped as `{ data: <payload> }`
|
|
139
|
+
- **4xx/5xx** → body is wrapped as `{ error: <message> }`
|
|
140
|
+
- **204** and **3xx** (e.g. redirects) → no wrapping
|
|
141
|
+
|
|
142
|
+
You can disable wrapping or customize the keys (`data` / `error`) via the [response config](./configuration.md#response-structure) in `tejas.config.json` or environment variables.
|
|
143
|
+
|
|
126
144
|
**All signatures:**
|
|
127
145
|
|
|
128
|
-
| Call | Status | Body | Content-Type |
|
|
129
|
-
|
|
146
|
+
| Call | Status | Wire Body | Content-Type |
|
|
147
|
+
|------|--------|-----------|-------------|
|
|
130
148
|
| `fire()` | 204 | *(empty)* | — |
|
|
131
|
-
| `fire("text")` | 200 | `text` | `
|
|
132
|
-
| `fire({ json })` | 200 |
|
|
133
|
-
| `fire(201)` | 201 | status message | `
|
|
134
|
-
| `fire(201, data)` | 201 | `data` |
|
|
135
|
-
| `fire(200, html, "text/html")` | 200 | `html` | `text/html` |
|
|
149
|
+
| `fire("text")` | 200 | `{ "data": "text" }` | `application/json` |
|
|
150
|
+
| `fire({ json })` | 200 | `{ "data": { ... } }` | `application/json` |
|
|
151
|
+
| `fire(201)` | 201 | `{ "data": "<status message>" }` | `application/json` |
|
|
152
|
+
| `fire(201, data)` | 201 | `{ "data": <data> }` | `application/json` |
|
|
153
|
+
| `fire(200, html, "text/html")` | 200 | `html` *(no envelope)* | `text/html` |
|
|
154
|
+
|
|
155
|
+
When response structure is disabled, the Body column matches what you pass (no envelope). The table above reflects the default behaviour.
|
|
136
156
|
|
|
137
157
|
After `fire()` is called, the sent data is available as `ammo.dispatchedData`.
|
|
138
158
|
|
|
@@ -243,8 +263,8 @@ import { Target, TejError } from 'te.js';
|
|
|
243
263
|
|
|
244
264
|
const users = new Target('/users');
|
|
245
265
|
|
|
246
|
-
// GET /users - List all
|
|
247
|
-
// POST /users - Create new
|
|
266
|
+
// GET /users - List all → { "data": { users, page, limit } }
|
|
267
|
+
// POST /users - Create new → { "data": user }
|
|
248
268
|
users.register('/', async (ammo) => {
|
|
249
269
|
if (ammo.GET) {
|
|
250
270
|
const { page = 1, limit = 10 } = ammo.payload;
|
|
@@ -256,7 +276,7 @@ users.register('/', async (ammo) => {
|
|
|
256
276
|
const { name, email } = ammo.payload;
|
|
257
277
|
|
|
258
278
|
if (!name || !email) {
|
|
259
|
-
throw new TejError(400, 'Name and email are required');
|
|
279
|
+
throw new TejError(400, 'Name and email are required'); // → { "error": "..." }
|
|
260
280
|
}
|
|
261
281
|
|
|
262
282
|
const user = await createUser({ name, email });
|
|
@@ -266,15 +286,15 @@ users.register('/', async (ammo) => {
|
|
|
266
286
|
ammo.notAllowed();
|
|
267
287
|
});
|
|
268
288
|
|
|
269
|
-
// GET /users/:id - Get one
|
|
270
|
-
// PUT /users/:id - Update
|
|
271
|
-
// DELETE /users/:id - Delete
|
|
289
|
+
// GET /users/:id - Get one → { "data": user }
|
|
290
|
+
// PUT /users/:id - Update → { "data": user }
|
|
291
|
+
// DELETE /users/:id - Delete → 204 (no body)
|
|
272
292
|
users.register('/:id', async (ammo) => {
|
|
273
293
|
const { id } = ammo.payload;
|
|
274
294
|
|
|
275
295
|
if (ammo.GET) {
|
|
276
296
|
const user = await getUser(id);
|
|
277
|
-
if (!user) throw new TejError(404, 'User not found');
|
|
297
|
+
if (!user) throw new TejError(404, 'User not found'); // → { "error": "User not found" }
|
|
278
298
|
return ammo.fire(user);
|
|
279
299
|
}
|
|
280
300
|
|
package/docs/configuration.md
CHANGED
|
@@ -36,7 +36,7 @@ import Tejas from 'te.js';
|
|
|
36
36
|
|
|
37
37
|
const app = new Tejas({
|
|
38
38
|
port: 3000,
|
|
39
|
-
log: { http_requests: true }
|
|
39
|
+
log: { http_requests: true },
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
app.takeoff();
|
|
@@ -46,25 +46,45 @@ app.takeoff();
|
|
|
46
46
|
|
|
47
47
|
### Core
|
|
48
48
|
|
|
49
|
-
| Config Key
|
|
50
|
-
|
|
51
|
-
| `entry`
|
|
52
|
-
| `port`
|
|
53
|
-
| `dir.targets` | `DIR_TARGETS` | string | `"targets"`
|
|
49
|
+
| Config Key | Env Variable | Type | Default | Description |
|
|
50
|
+
| ------------- | ------------- | ------ | ----------------- | --------------------------------------------------------------------------------------------------------- |
|
|
51
|
+
| `entry` | `ENTRY` | string | _(auto-resolved)_ | Entry file for `tejas fly`. Falls back to `package.json` `main`, then `index.js` / `app.js` / `server.js` |
|
|
52
|
+
| `port` | `PORT` | number | `1403` | Server port |
|
|
53
|
+
| `dir.targets` | `DIR_TARGETS` | string | `"targets"` | Directory containing `.target.js` files for auto-discovery |
|
|
54
54
|
|
|
55
55
|
### Logging
|
|
56
56
|
|
|
57
|
-
| Config Key
|
|
58
|
-
|
|
57
|
+
| Config Key | Env Variable | Type | Default | Description |
|
|
58
|
+
| ------------------- | ------------------- | ------- | ------- | ------------------------------------------------------- |
|
|
59
59
|
| `log.http_requests` | `LOG_HTTP_REQUESTS` | boolean | `false` | Log incoming HTTP requests (method, path, status, time) |
|
|
60
|
-
| `log.exceptions`
|
|
60
|
+
| `log.exceptions` | `LOG_EXCEPTIONS` | boolean | `false` | Log unhandled exceptions and errors |
|
|
61
|
+
|
|
62
|
+
### Response Structure {#response-structure}
|
|
63
|
+
|
|
64
|
+
By default, Tejas wraps all success responses in `{ data: ... }` and all error responses in `{ error: ... }`. This gives clients a consistent envelope. See [Ammo — fire()](./ammo.md#fire----send-response) for examples. Disable or customize via the options below.
|
|
65
|
+
|
|
66
|
+
| Config Key | Env Variable | Type | Default | Description |
|
|
67
|
+
| -------------------------- | --------------------------- | ------- | --------- | ---------------------------------------------------------------------------------------- |
|
|
68
|
+
| `response.envelopeEnabled` | `RESPONSE_ENVELOPE_ENABLED` | boolean | `true` | Enable response envelope: wrap success in `{ data: ... }` and errors in `{ error: ... }` |
|
|
69
|
+
| `response.successKey` | `RESPONSE_SUCCESSKEY` | string | `"data"` | Key used to wrap 2xx response bodies |
|
|
70
|
+
| `response.errorKey` | `RESPONSE_ERRORKEY` | string | `"error"` | Key used to wrap 4xx/5xx response bodies |
|
|
71
|
+
|
|
72
|
+
### Developer warnings
|
|
73
|
+
|
|
74
|
+
When an endpoint is called and it has no allowed methods defined (see [Routing — Endpoint Metadata](./routing.md#endpoint-metadata)), the framework logs a warning once per path so you can restrict methods for security (405 and `Allow` header). To disable this warning:
|
|
75
|
+
|
|
76
|
+
| Config Key | Env Variable | Type | Default | Description |
|
|
77
|
+
| ------------------------------ | ------------------------------ | -------------- | -------- | ------------------------------------------------------------------------------------ |
|
|
78
|
+
| `warn_missing_allowed_methods` | `WARN_MISSING_ALLOWED_METHODS` | boolean/string | _(warn)_ | Set to `false` to disable the runtime warning for endpoints without allowed methods. |
|
|
79
|
+
|
|
80
|
+
Example: in `tejas.config.json` use `"warn_missing_allowed_methods": false`, or in `.env` use `WARN_MISSING_ALLOWED_METHODS=false`.
|
|
61
81
|
|
|
62
82
|
### Request Body
|
|
63
83
|
|
|
64
|
-
| Config Key
|
|
65
|
-
|
|
66
|
-
| `body.max_size` | `BODY_MAX_SIZE` | number | `10485760` (10 MB) | Maximum request body size in bytes. Requests exceeding this receive a 413 error
|
|
67
|
-
| `body.timeout`
|
|
84
|
+
| Config Key | Env Variable | Type | Default | Description |
|
|
85
|
+
| --------------- | --------------- | ------ | ------------------ | --------------------------------------------------------------------------------- |
|
|
86
|
+
| `body.max_size` | `BODY_MAX_SIZE` | number | `10485760` (10 MB) | Maximum request body size in bytes. Requests exceeding this receive a 413 error |
|
|
87
|
+
| `body.timeout` | `BODY_TIMEOUT` | number | `30000` (30 s) | Body parsing timeout in milliseconds. Requests exceeding this receive a 408 error |
|
|
68
88
|
|
|
69
89
|
### LLM configuration (feature as parent, LLM inside each feature)
|
|
70
90
|
|
|
@@ -74,31 +94,31 @@ Tejas uses a **feature-as-parent** pattern: each feature that needs an LLM has i
|
|
|
74
94
|
|
|
75
95
|
These options configure the `tejas generate:docs` CLI command and the auto-documentation system. The **`docs.llm`** block is the LLM configuration for this feature. See [Auto-Documentation](./auto-docs.md) for full details.
|
|
76
96
|
|
|
77
|
-
| Config Key
|
|
78
|
-
|
|
79
|
-
| `docs.dirTargets`
|
|
80
|
-
| `docs.output`
|
|
81
|
-
| `docs.title`
|
|
82
|
-
| `docs.version`
|
|
83
|
-
| `docs.description`
|
|
84
|
-
| `docs.level`
|
|
85
|
-
| `docs.llm.baseURL`
|
|
86
|
-
| `docs.llm.apiKey`
|
|
87
|
-
| `docs.llm.model`
|
|
88
|
-
| `docs.overviewPath`
|
|
89
|
-
| `docs.productionBranch` | `DOCS_PRODUCTION_BRANCH`
|
|
97
|
+
| Config Key | Env Variable | Type | Default | Description |
|
|
98
|
+
| ----------------------- | ------------------------------------- | ------ | ----------------------------- | ------------------------------------------------------------------- |
|
|
99
|
+
| `docs.dirTargets` | `DOCS_DIR_TARGETS` | string | `"targets"` | Target directory for doc generation (can differ from `dir.targets`) |
|
|
100
|
+
| `docs.output` | — | string | `"./openapi.json"` | Output path for the generated OpenAPI spec |
|
|
101
|
+
| `docs.title` | — | string | `"API"` | API title in the OpenAPI `info` block |
|
|
102
|
+
| `docs.version` | — | string | `"1.0.0"` | API version in the OpenAPI `info` block |
|
|
103
|
+
| `docs.description` | — | string | `""` | API description |
|
|
104
|
+
| `docs.level` | — | number | `1` | LLM enhancement level (1–3). Higher = better docs, more tokens |
|
|
105
|
+
| `docs.llm.baseURL` | `DOCS_LLM_BASE_URL` or `LLM_BASE_URL` | string | `"https://api.openai.com/v1"` | LLM provider endpoint for auto-docs |
|
|
106
|
+
| `docs.llm.apiKey` | `DOCS_LLM_API_KEY` or `LLM_API_KEY` | string | — | LLM provider API key for auto-docs |
|
|
107
|
+
| `docs.llm.model` | `DOCS_LLM_MODEL` or `LLM_MODEL` | string | `"gpt-4o-mini"` | LLM model for auto-docs |
|
|
108
|
+
| `docs.overviewPath` | — | string | `"./API_OVERVIEW.md"` | Path for the generated overview page (level 3 only) |
|
|
109
|
+
| `docs.productionBranch` | `DOCS_PRODUCTION_BRANCH` | string | `"main"` | Git branch that triggers `docs:on-push` |
|
|
90
110
|
|
|
91
111
|
### Error handling (LLM-inferred errors)
|
|
92
112
|
|
|
93
113
|
When [LLM-inferred error codes and messages](./error-handling.md#llm-inferred-errors) are enabled, the **`errors.llm`** block configures the LLM used for inferring status code and message when you call `ammo.throw()` without explicit code or message. Unset values fall back to `LLM_BASE_URL`, `LLM_API_KEY`, `LLM_MODEL`. You can also enable (and optionally set connection options) by calling **`app.withLLMErrors(config?)`** before `takeoff()` — e.g. `app.withLLMErrors()` to use env/config for baseURL, apiKey, model, or `app.withLLMErrors({ baseURL, apiKey, model, messageType })` to override in code.
|
|
94
114
|
|
|
95
|
-
| Config Key
|
|
96
|
-
|
|
97
|
-
| `errors.llm.enabled`
|
|
98
|
-
| `errors.llm.baseURL`
|
|
99
|
-
| `errors.llm.apiKey`
|
|
100
|
-
| `errors.llm.model`
|
|
101
|
-
| `errors.llm.messageType` | `ERRORS_LLM_MESSAGE_TYPE` or `LLM_MESSAGE_TYPE`
|
|
115
|
+
| Config Key | Env Variable | Type | Default | Description |
|
|
116
|
+
| ------------------------ | ------------------------------------------------ | ---------------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
117
|
+
| `errors.llm.enabled` | `ERRORS_LLM_ENABLED` or `LLM_*` (for connection) | boolean | `false` | Enable LLM-inferred error code and message for `ammo.throw()` |
|
|
118
|
+
| `errors.llm.baseURL` | `ERRORS_LLM_BASE_URL` or `LLM_BASE_URL` | string | `"https://api.openai.com/v1"` | LLM provider endpoint for error inference |
|
|
119
|
+
| `errors.llm.apiKey` | `ERRORS_LLM_API_KEY` or `LLM_API_KEY` | string | — | LLM provider API key for error inference |
|
|
120
|
+
| `errors.llm.model` | `ERRORS_LLM_MODEL` or `LLM_MODEL` | string | `"gpt-4o-mini"` | LLM model for error inference |
|
|
121
|
+
| `errors.llm.messageType` | `ERRORS_LLM_MESSAGE_TYPE` or `LLM_MESSAGE_TYPE` | `"endUser"` \| `"developer"` | `"endUser"` | Default tone for LLM-generated message: `endUser` (safe for clients) or `developer` (technical detail). Overridable per `ammo.throw()` call. |
|
|
102
122
|
|
|
103
123
|
When enabled, the same behaviour applies whether you call `ammo.throw()` or the framework calls it when it catches an error — one mechanism, no separate config.
|
|
104
124
|
|
|
@@ -117,6 +137,11 @@ Create a `tejas.config.json` in your project root:
|
|
|
117
137
|
"http_requests": true,
|
|
118
138
|
"exceptions": true
|
|
119
139
|
},
|
|
140
|
+
"response": {
|
|
141
|
+
"envelopeEnabled": true,
|
|
142
|
+
"successKey": "data",
|
|
143
|
+
"errorKey": "error"
|
|
144
|
+
},
|
|
120
145
|
"body": {
|
|
121
146
|
"max_size": 5242880,
|
|
122
147
|
"timeout": 15000
|
|
@@ -155,6 +180,11 @@ PORT=3000
|
|
|
155
180
|
LOG_HTTP_REQUESTS=true
|
|
156
181
|
LOG_EXCEPTIONS=true
|
|
157
182
|
|
|
183
|
+
# Response envelope (default: enabled; 2xx → { data }, 4xx/5xx → { error })
|
|
184
|
+
# RESPONSE_ENVELOPE_ENABLED=true
|
|
185
|
+
# RESPONSE_SUCCESSKEY=data
|
|
186
|
+
# RESPONSE_ERRORKEY=error
|
|
187
|
+
|
|
158
188
|
# Body limits
|
|
159
189
|
BODY_MAX_SIZE=5242880
|
|
160
190
|
BODY_TIMEOUT=15000
|
|
@@ -178,6 +208,9 @@ LLM_MODEL=gpt-4o-mini
|
|
|
178
208
|
# ERRORS_LLM_API_KEY=...
|
|
179
209
|
# ERRORS_LLM_MODEL=...
|
|
180
210
|
# ERRORS_LLM_MESSAGE_TYPE=endUser # or "developer" for technical messages
|
|
211
|
+
|
|
212
|
+
# Optional: disable runtime warning for endpoints without allowed methods
|
|
213
|
+
# WARN_MISSING_ALLOWED_METHODS=false
|
|
181
214
|
```
|
|
182
215
|
|
|
183
216
|
## Constructor Options
|
|
@@ -191,12 +224,12 @@ const app = new Tejas({
|
|
|
191
224
|
port: 3000,
|
|
192
225
|
log: {
|
|
193
226
|
http_requests: true,
|
|
194
|
-
exceptions: true
|
|
227
|
+
exceptions: true,
|
|
195
228
|
},
|
|
196
229
|
body: {
|
|
197
230
|
max_size: 10 * 1024 * 1024,
|
|
198
|
-
timeout: 30000
|
|
199
|
-
}
|
|
231
|
+
timeout: 30000,
|
|
232
|
+
},
|
|
200
233
|
});
|
|
201
234
|
```
|
|
202
235
|
|
|
@@ -226,7 +259,7 @@ import { env } from 'tej-env';
|
|
|
226
259
|
target.register('/info', (ammo) => {
|
|
227
260
|
ammo.fire({
|
|
228
261
|
port: env('PORT'),
|
|
229
|
-
maxBodySize: env('BODY_MAX_SIZE')
|
|
262
|
+
maxBodySize: env('BODY_MAX_SIZE'),
|
|
230
263
|
});
|
|
231
264
|
});
|
|
232
265
|
```
|
|
@@ -261,7 +294,7 @@ Database connections are configured via `takeoff()` options, not through the con
|
|
|
261
294
|
```javascript
|
|
262
295
|
app.takeoff({
|
|
263
296
|
withRedis: { url: 'redis://localhost:6379' },
|
|
264
|
-
withMongo: { uri: 'mongodb://localhost:27017/myapp' }
|
|
297
|
+
withMongo: { uri: 'mongodb://localhost:27017/myapp' },
|
|
265
298
|
});
|
|
266
299
|
```
|
|
267
300
|
|
package/docs/routing.md
CHANGED
|
@@ -214,6 +214,8 @@ target.register('/users', {
|
|
|
214
214
|
|
|
215
215
|
When metadata is omitted, the auto-docs LLM infers everything from the handler source code.
|
|
216
216
|
|
|
217
|
+
If an endpoint has no `methods` in its metadata (and does not use `ammo.only()` to restrict methods), the framework logs a warning the first time that path is called. You can disable this warning via config: set `WARN_MISSING_ALLOWED_METHODS=false` (env) or `warn_missing_allowed_methods: false` in config. See [Configuration — Developer warnings](./configuration.md#developer-warnings).
|
|
218
|
+
|
|
217
219
|
## Method-Agnostic Handlers
|
|
218
220
|
|
|
219
221
|
If a handler does not check any method flags (`ammo.GET`, `ammo.POST`, etc.), it is treated as accepting **all HTTP methods**. This is useful for simple endpoints:
|
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import status from 'statuses';
|
|
2
|
+
import { getResponseConfig } from '../../utils/response-config.js';
|
|
2
3
|
|
|
3
4
|
const formattedData = (data) => {
|
|
4
5
|
if (data === null || data === undefined) return '';
|
|
@@ -17,64 +18,72 @@ const formattedData = (data) => {
|
|
|
17
18
|
return String(data);
|
|
18
19
|
};
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Apply response structure envelope when enabled.
|
|
23
|
+
* 2xx → { [successKey]: data }; 4xx/5xx → { [errorKey]: data }; 204 and when disabled → pass through.
|
|
24
|
+
* @param {number} statusCode
|
|
25
|
+
* @param {unknown} data
|
|
26
|
+
* @returns {unknown}
|
|
27
|
+
*/
|
|
28
|
+
const applyResponseStructure = (statusCode, data) => {
|
|
29
|
+
const { enabled, successKey, errorKey } = getResponseConfig();
|
|
30
|
+
if (!enabled) return data;
|
|
31
|
+
if (statusCode === 204) return data;
|
|
32
|
+
if (statusCode >= 200 && statusCode < 300) {
|
|
33
|
+
return { [successKey]: data };
|
|
34
|
+
}
|
|
35
|
+
if (statusCode >= 400) {
|
|
36
|
+
return { [errorKey]: data };
|
|
37
|
+
}
|
|
38
|
+
return data;
|
|
39
|
+
};
|
|
40
|
+
|
|
20
41
|
const statusAndData = (args) => {
|
|
42
|
+
let statusCode;
|
|
43
|
+
let rawData;
|
|
44
|
+
let customContentType = null;
|
|
45
|
+
|
|
21
46
|
// Handle no arguments
|
|
22
47
|
if (!args || args.length === 0) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
contentType: 'text/plain',
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Handle single argument
|
|
31
|
-
if (args.length === 1) {
|
|
48
|
+
statusCode = 204;
|
|
49
|
+
rawData = status(204);
|
|
50
|
+
} else if (args.length === 1) {
|
|
32
51
|
const arg = args[0];
|
|
33
52
|
|
|
34
53
|
// If it's a number, treat as status code
|
|
35
54
|
if (typeof arg === 'number') {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
55
|
+
statusCode = arg;
|
|
56
|
+
rawData = status(arg) || String(arg);
|
|
57
|
+
} else {
|
|
58
|
+
// Otherwise treat as data
|
|
59
|
+
statusCode = 200;
|
|
60
|
+
rawData = arg;
|
|
41
61
|
}
|
|
42
|
-
|
|
43
|
-
// Otherwise treat as data
|
|
44
|
-
return {
|
|
45
|
-
statusCode: 200,
|
|
46
|
-
data: formattedData(arg),
|
|
47
|
-
contentType: contentType(arg),
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Handle multiple arguments
|
|
52
|
-
let statusCode = 200;
|
|
53
|
-
let data = args[0];
|
|
54
|
-
|
|
55
|
-
// If first argument is a number, treat as status code
|
|
56
|
-
if (typeof args[0] === 'number') {
|
|
57
|
-
statusCode = args[0];
|
|
58
|
-
data = args[1];
|
|
59
62
|
} else {
|
|
60
|
-
//
|
|
61
|
-
|
|
63
|
+
// Handle multiple arguments
|
|
64
|
+
statusCode = 200;
|
|
65
|
+
rawData = args[0];
|
|
66
|
+
|
|
67
|
+
if (typeof args[0] === 'number') {
|
|
68
|
+
statusCode = args[0];
|
|
69
|
+
rawData = args[1];
|
|
70
|
+
} else if (typeof args[1] === 'number') {
|
|
62
71
|
statusCode = args[1];
|
|
63
72
|
}
|
|
64
|
-
}
|
|
65
73
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
74
|
+
if (rawData === undefined) {
|
|
75
|
+
rawData = status[statusCode] || String(statusCode);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
customContentType = args.length > 2 ? args[2] : null;
|
|
69
79
|
}
|
|
70
80
|
|
|
71
|
-
|
|
72
|
-
const customContentType = args.length > 2 ? args[2] : null;
|
|
81
|
+
const wrapped = applyResponseStructure(statusCode, rawData);
|
|
73
82
|
|
|
74
83
|
return {
|
|
75
84
|
statusCode,
|
|
76
|
-
data: formattedData(
|
|
77
|
-
contentType: customContentType || contentType(
|
|
85
|
+
data: formattedData(wrapped),
|
|
86
|
+
contentType: customContentType || contentType(wrapped),
|
|
78
87
|
};
|
|
79
88
|
};
|
|
80
89
|
|
package/server/handler.js
CHANGED
|
@@ -7,6 +7,9 @@ import TejError from './error.js';
|
|
|
7
7
|
import targetRegistry from './targets/registry.js';
|
|
8
8
|
|
|
9
9
|
const errorLogger = new TejLogger('Tejas.Exception');
|
|
10
|
+
const logger = new TejLogger('Tejas');
|
|
11
|
+
/** Paths we have already warned about (missing allowed methods). */
|
|
12
|
+
const warnedPaths = new Set();
|
|
10
13
|
|
|
11
14
|
const DEFAULT_ALLOWED_METHODS = [
|
|
12
15
|
'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS',
|
|
@@ -134,6 +137,12 @@ const handler = async (req, res) => {
|
|
|
134
137
|
await errorHandler(ammo, new TejError(405, 'Method Not Allowed'));
|
|
135
138
|
return;
|
|
136
139
|
}
|
|
140
|
+
} else if (env('WARN_MISSING_ALLOWED_METHODS') !== 'false') {
|
|
141
|
+
const path = match.target.getPath();
|
|
142
|
+
if (!warnedPaths.has(path)) {
|
|
143
|
+
warnedPaths.add(path);
|
|
144
|
+
logger.warn(`Endpoint missing allowed methods: ${path}`);
|
|
145
|
+
}
|
|
137
146
|
}
|
|
138
147
|
|
|
139
148
|
// Add route parameters to ammo.payload
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve response structure configuration.
|
|
3
|
+
* Uses RESPONSE_* env vars (populated from tejas.config.json response section).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { env } from 'tej-env';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolve response config from env.
|
|
10
|
+
* @returns {{ enabled: boolean, successKey: string, errorKey: string }}
|
|
11
|
+
*/
|
|
12
|
+
export function getResponseConfig() {
|
|
13
|
+
// Response envelope: from .env use RESPONSE_ENVELOPE_ENABLED; from config file → RESPONSE_ENVELOPEENABLED; legacy names supported
|
|
14
|
+
const enabledRaw =
|
|
15
|
+
env('RESPONSE_ENVELOPE_ENABLED') ??
|
|
16
|
+
env('RESPONSE_ENVELOPEENABLED') ??
|
|
17
|
+
env('RESPONSE_FORMAT_ENABLED') ??
|
|
18
|
+
env('RESPONSE_FORMATENABLED') ??
|
|
19
|
+
env('RESPONSE_ENABLED') ??
|
|
20
|
+
'';
|
|
21
|
+
const enabled =
|
|
22
|
+
enabledRaw === true ||
|
|
23
|
+
enabledRaw === 'true' ||
|
|
24
|
+
enabledRaw === '1' ||
|
|
25
|
+
enabledRaw === 1;
|
|
26
|
+
|
|
27
|
+
const successKey =
|
|
28
|
+
env('RESPONSE_SUCCESSKEY') ?? env('RESPONSE_SUCCESS_KEY') ?? 'data';
|
|
29
|
+
const errorKey =
|
|
30
|
+
env('RESPONSE_ERRORKEY') ?? env('RESPONSE_ERROR_KEY') ?? 'error';
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
enabled:
|
|
34
|
+
enabledRaw === undefined || enabledRaw === '' ? true : Boolean(enabled),
|
|
35
|
+
successKey: String(successKey ?? 'data').trim() || 'data',
|
|
36
|
+
errorKey: String(errorKey ?? 'error').trim() || 'error',
|
|
37
|
+
};
|
|
38
|
+
}
|