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.
@@ -15,9 +15,49 @@ const ALL_METHODS = [
15
15
  ];
16
16
 
17
17
  /**
18
- * Detects which HTTP methods the handler checks (e.g. ammo.GET, ammo.POST).
19
- * Matches property access patterns like `.GET`, `ammo.GET`, avoiding false positives
20
- * in strings or unrelated identifiers.
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 detected = [];
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` | `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` |
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
 
@@ -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 | 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 |
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 | Env Variable | Type | Default | Description |
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` | `LOG_EXCEPTIONS` | boolean | `false` | Log unhandled exceptions and errors |
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 | Env Variable | Type | Default | Description |
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` | `BODY_TIMEOUT` | number | `30000` (30 s) | Body parsing timeout in milliseconds. Requests exceeding this receive a 408 error |
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 | Env Variable | Type | Default | Description |
78
- |------------|-------------|------|---------|-------------|
79
- | `docs.dirTargets` | `DOCS_DIR_TARGETS` | string | `"targets"` | Target directory for doc generation (can differ from `dir.targets`) |
80
- | `docs.output` | — | string | `"./openapi.json"` | Output path for the generated OpenAPI spec |
81
- | `docs.title` | — | string | `"API"` | API title in the OpenAPI `info` block |
82
- | `docs.version` | — | string | `"1.0.0"` | API version in the OpenAPI `info` block |
83
- | `docs.description` | — | string | `""` | API description |
84
- | `docs.level` | — | number | `1` | LLM enhancement level (1–3). Higher = better docs, more tokens |
85
- | `docs.llm.baseURL` | `DOCS_LLM_BASE_URL` or `LLM_BASE_URL` | string | `"https://api.openai.com/v1"` | LLM provider endpoint for auto-docs |
86
- | `docs.llm.apiKey` | `DOCS_LLM_API_KEY` or `LLM_API_KEY` | string | — | LLM provider API key for auto-docs |
87
- | `docs.llm.model` | `DOCS_LLM_MODEL` or `LLM_MODEL` | string | `"gpt-4o-mini"` | LLM model for auto-docs |
88
- | `docs.overviewPath` | — | string | `"./API_OVERVIEW.md"` | Path for the generated overview page (level 3 only) |
89
- | `docs.productionBranch` | `DOCS_PRODUCTION_BRANCH` | string | `"main"` | Git branch that triggers `docs:on-push` |
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 | Env Variable | Type | Default | Description |
96
- |------------|-------------|------|---------|-------------|
97
- | `errors.llm.enabled` | `ERRORS_LLM_ENABLED` or `LLM_*` (for connection) | boolean | `false` | Enable LLM-inferred error code and message for `ammo.throw()` |
98
- | `errors.llm.baseURL` | `ERRORS_LLM_BASE_URL` or `LLM_BASE_URL` | string | `"https://api.openai.com/v1"` | LLM provider endpoint for error inference |
99
- | `errors.llm.apiKey` | `ERRORS_LLM_API_KEY` or `LLM_API_KEY` | string | — | LLM provider API key for error inference |
100
- | `errors.llm.model` | `ERRORS_LLM_MODEL` or `LLM_MODEL` | string | `"gpt-4o-mini"` | LLM model for error inference |
101
- | `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. |
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,6 +1,6 @@
1
1
  {
2
2
  "name": "te.js",
3
- "version": "2.1.2",
3
+ "version": "2.1.4",
4
4
  "description": "AI Native Node.js Framework",
5
5
  "type": "module",
6
6
  "main": "te.js",
@@ -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
- return {
24
- statusCode: 204,
25
- data: status(204),
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
- return {
37
- statusCode: arg,
38
- data: status(arg) || String(arg),
39
- contentType: 'text/plain',
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
- // If first argument is not a number, check if second is
61
- if (typeof args[1] === 'number') {
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
- // If data is undefined, use status message
67
- if (data === undefined) {
68
- data = status[statusCode] || String(statusCode);
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
- // If third argument is provided, it's the content type
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(data),
77
- contentType: customContentType || contentType(data),
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
+ }