te.js 2.1.3 → 2.1.5
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/docs/ammo.md +34 -14
- package/docs/configuration.md +61 -41
- package/package.json +1 -1
- package/server/ammo/dispatch-helper.js +50 -41
- package/server/handler.js +46 -10
- package/utils/response-config.js +38 -0
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,35 +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 |
|
|
61
71
|
|
|
62
72
|
### Developer warnings
|
|
63
73
|
|
|
64
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:
|
|
65
75
|
|
|
66
|
-
| Config Key
|
|
67
|
-
|
|
68
|
-
| `warn_missing_allowed_methods` | `WARN_MISSING_ALLOWED_METHODS` | boolean/string |
|
|
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. |
|
|
69
79
|
|
|
70
80
|
Example: in `tejas.config.json` use `"warn_missing_allowed_methods": false`, or in `.env` use `WARN_MISSING_ALLOWED_METHODS=false`.
|
|
71
81
|
|
|
72
82
|
### Request Body
|
|
73
83
|
|
|
74
|
-
| Config Key
|
|
75
|
-
|
|
76
|
-
| `body.max_size` | `BODY_MAX_SIZE` | number | `10485760` (10 MB) | Maximum request body size in bytes. Requests exceeding this receive a 413 error
|
|
77
|
-
| `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 |
|
|
78
88
|
|
|
79
89
|
### LLM configuration (feature as parent, LLM inside each feature)
|
|
80
90
|
|
|
@@ -84,31 +94,31 @@ Tejas uses a **feature-as-parent** pattern: each feature that needs an LLM has i
|
|
|
84
94
|
|
|
85
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.
|
|
86
96
|
|
|
87
|
-
| Config Key
|
|
88
|
-
|
|
89
|
-
| `docs.dirTargets`
|
|
90
|
-
| `docs.output`
|
|
91
|
-
| `docs.title`
|
|
92
|
-
| `docs.version`
|
|
93
|
-
| `docs.description`
|
|
94
|
-
| `docs.level`
|
|
95
|
-
| `docs.llm.baseURL`
|
|
96
|
-
| `docs.llm.apiKey`
|
|
97
|
-
| `docs.llm.model`
|
|
98
|
-
| `docs.overviewPath`
|
|
99
|
-
| `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` |
|
|
100
110
|
|
|
101
111
|
### Error handling (LLM-inferred errors)
|
|
102
112
|
|
|
103
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.
|
|
104
114
|
|
|
105
|
-
| Config Key
|
|
106
|
-
|
|
107
|
-
| `errors.llm.enabled`
|
|
108
|
-
| `errors.llm.baseURL`
|
|
109
|
-
| `errors.llm.apiKey`
|
|
110
|
-
| `errors.llm.model`
|
|
111
|
-
| `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. |
|
|
112
122
|
|
|
113
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.
|
|
114
124
|
|
|
@@ -127,6 +137,11 @@ Create a `tejas.config.json` in your project root:
|
|
|
127
137
|
"http_requests": true,
|
|
128
138
|
"exceptions": true
|
|
129
139
|
},
|
|
140
|
+
"response": {
|
|
141
|
+
"envelopeEnabled": true,
|
|
142
|
+
"successKey": "data",
|
|
143
|
+
"errorKey": "error"
|
|
144
|
+
},
|
|
130
145
|
"body": {
|
|
131
146
|
"max_size": 5242880,
|
|
132
147
|
"timeout": 15000
|
|
@@ -165,6 +180,11 @@ PORT=3000
|
|
|
165
180
|
LOG_HTTP_REQUESTS=true
|
|
166
181
|
LOG_EXCEPTIONS=true
|
|
167
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
|
+
|
|
168
188
|
# Body limits
|
|
169
189
|
BODY_MAX_SIZE=5242880
|
|
170
190
|
BODY_TIMEOUT=15000
|
|
@@ -204,12 +224,12 @@ const app = new Tejas({
|
|
|
204
224
|
port: 3000,
|
|
205
225
|
log: {
|
|
206
226
|
http_requests: true,
|
|
207
|
-
exceptions: true
|
|
227
|
+
exceptions: true,
|
|
208
228
|
},
|
|
209
229
|
body: {
|
|
210
230
|
max_size: 10 * 1024 * 1024,
|
|
211
|
-
timeout: 30000
|
|
212
|
-
}
|
|
231
|
+
timeout: 30000,
|
|
232
|
+
},
|
|
213
233
|
});
|
|
214
234
|
```
|
|
215
235
|
|
|
@@ -239,7 +259,7 @@ import { env } from 'tej-env';
|
|
|
239
259
|
target.register('/info', (ammo) => {
|
|
240
260
|
ammo.fire({
|
|
241
261
|
port: env('PORT'),
|
|
242
|
-
maxBodySize: env('BODY_MAX_SIZE')
|
|
262
|
+
maxBodySize: env('BODY_MAX_SIZE'),
|
|
243
263
|
});
|
|
244
264
|
});
|
|
245
265
|
```
|
|
@@ -274,7 +294,7 @@ Database connections are configured via `takeoff()` options, not through the con
|
|
|
274
294
|
```javascript
|
|
275
295
|
app.takeoff({
|
|
276
296
|
withRedis: { url: 'redis://localhost:6379' },
|
|
277
|
-
withMongo: { uri: 'mongodb://localhost:27017/myapp' }
|
|
297
|
+
withMongo: { uri: 'mongodb://localhost:27017/myapp' },
|
|
278
298
|
});
|
|
279
299
|
```
|
|
280
300
|
|
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
|
@@ -12,7 +12,13 @@ const logger = new TejLogger('Tejas');
|
|
|
12
12
|
const warnedPaths = new Set();
|
|
13
13
|
|
|
14
14
|
const DEFAULT_ALLOWED_METHODS = [
|
|
15
|
-
'GET',
|
|
15
|
+
'GET',
|
|
16
|
+
'POST',
|
|
17
|
+
'PUT',
|
|
18
|
+
'DELETE',
|
|
19
|
+
'PATCH',
|
|
20
|
+
'HEAD',
|
|
21
|
+
'OPTIONS',
|
|
16
22
|
];
|
|
17
23
|
|
|
18
24
|
/**
|
|
@@ -24,9 +30,13 @@ const getAllowedMethods = () => {
|
|
|
24
30
|
if (raw == null) return new Set(DEFAULT_ALLOWED_METHODS);
|
|
25
31
|
const arr = Array.isArray(raw)
|
|
26
32
|
? raw
|
|
27
|
-
:
|
|
33
|
+
: typeof raw === 'string'
|
|
34
|
+
? raw.split(',').map((s) => s.trim())
|
|
35
|
+
: [];
|
|
28
36
|
const normalized = arr.map((m) => String(m).toUpperCase()).filter(Boolean);
|
|
29
|
-
return normalized.length > 0
|
|
37
|
+
return normalized.length > 0
|
|
38
|
+
? new Set(normalized)
|
|
39
|
+
: new Set(DEFAULT_ALLOWED_METHODS);
|
|
30
40
|
};
|
|
31
41
|
|
|
32
42
|
/**
|
|
@@ -58,23 +68,31 @@ const executeChain = async (target, ammo) => {
|
|
|
58
68
|
|
|
59
69
|
try {
|
|
60
70
|
const result = await middleware(...args);
|
|
61
|
-
|
|
71
|
+
|
|
62
72
|
// Check again after middleware execution (passport might have redirected)
|
|
63
73
|
if (ammo.res.headersSent || ammo.res.writableEnded || ammo.res.finished) {
|
|
64
74
|
return;
|
|
65
75
|
}
|
|
66
|
-
|
|
76
|
+
|
|
67
77
|
// If middleware returned a promise that resolved, continue chain
|
|
68
78
|
if (result && typeof result.then === 'function') {
|
|
69
79
|
await result;
|
|
70
80
|
// Check one more time after promise resolution
|
|
71
|
-
if (
|
|
81
|
+
if (
|
|
82
|
+
ammo.res.headersSent ||
|
|
83
|
+
ammo.res.writableEnded ||
|
|
84
|
+
ammo.res.finished
|
|
85
|
+
) {
|
|
72
86
|
return;
|
|
73
87
|
}
|
|
74
88
|
}
|
|
75
89
|
} catch (err) {
|
|
76
90
|
// Only handle error if response hasn't been sent
|
|
77
|
-
if (
|
|
91
|
+
if (
|
|
92
|
+
!ammo.res.headersSent &&
|
|
93
|
+
!ammo.res.writableEnded &&
|
|
94
|
+
!ammo.res.finished
|
|
95
|
+
) {
|
|
78
96
|
await errorHandler(ammo, err);
|
|
79
97
|
}
|
|
80
98
|
}
|
|
@@ -126,9 +144,11 @@ const handler = async (req, res) => {
|
|
|
126
144
|
const ammo = new Ammo(req, res);
|
|
127
145
|
|
|
128
146
|
try {
|
|
129
|
-
|
|
130
|
-
|
|
147
|
+
// Enhance ammo for all requests (matched or not) so global middlewares
|
|
148
|
+
// always receive a fully-populated ammo (method flags, headers, payload, etc.).
|
|
149
|
+
await ammo.enhance();
|
|
131
150
|
|
|
151
|
+
if (match && match.target) {
|
|
132
152
|
const allowedMethods = match.target.getMethods();
|
|
133
153
|
if (allowedMethods != null && allowedMethods.length > 0) {
|
|
134
154
|
const method = ammo.method && String(ammo.method).toUpperCase();
|
|
@@ -156,7 +176,23 @@ const handler = async (req, res) => {
|
|
|
156
176
|
if (req.url === '/') {
|
|
157
177
|
ammo.defaultEntry();
|
|
158
178
|
} else {
|
|
159
|
-
|
|
179
|
+
// Run global middlewares (CORS preflight, auth, logging, etc.) even for
|
|
180
|
+
// unmatched routes. A pseudo-target with no route-specific middlewares
|
|
181
|
+
// is used so the 404 response is sent at the end of the global chain.
|
|
182
|
+
await executeChain(
|
|
183
|
+
{
|
|
184
|
+
getMiddlewares: () => [],
|
|
185
|
+
getHandler: () => async () => {
|
|
186
|
+
if (!ammo.res.headersSent) {
|
|
187
|
+
await errorHandler(
|
|
188
|
+
ammo,
|
|
189
|
+
new TejError(404, `URL not found: ${url}`),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
ammo,
|
|
195
|
+
);
|
|
160
196
|
}
|
|
161
197
|
}
|
|
162
198
|
} catch (err) {
|
|
@@ -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
|
+
}
|