@odatnurd/cf-requests 0.1.6 → 0.1.8

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/README.md CHANGED
@@ -1,20 +1,28 @@
1
1
  # Simple CloudFlare Request Handlers
2
2
 
3
- `cf-requests` is a very simple set of wrapper functions that allow for working
4
- with requests in a
3
+ `cf-requests` is a simple set of wrapper functions that allow for working with
4
+ requests in a
5
5
  [Cloudflare Worker](https://developers.cloudflare.com/workers/) or in
6
6
  [Cloudflare Pages](https://developers.cloudflare.com/pages/) using
7
- [Hono](https://hono.dev/) as a route handler, with a focus on API routes,
8
- though this is not strictly required.
7
+ [Hono](https://hono.dev/) as a route handler.
9
8
 
10
- This is also intended to be used with
11
- [@axel669/joker](https://www.npmjs.com/package/@axel669/joker) as a schema
12
- validation library and
9
+ The focus is to allow for more easy creation of robust `API` routes, providing
10
+ a standard `JSON` response format on both success and failure.
11
+
12
+ The request validation functionality is intended to be used alongside the
13
+ [@axel669/joker](https://www.npmjs.com/package/@axel669/joker) schema
14
+ validation library. However this is not strictly required, and the validation
15
+ wrapper can be used with any validator with some simple wrapper code; see below
16
+ for more details.
17
+
18
+ The examples seen here utilize
13
19
  [@axel669/hono-file-routes](https://www.npmjs.com/package/@axel669/hono-file-routes),
14
- which collectively allow for fast and easy schema validation and data masking
15
- and allowing file based in Cloudflare Workers, where that is not directly
16
- possible. This again is not strictly required, but examples below assume that
17
- this is the case.
20
+ which allows for file based routing in a Cloudflare worker. This is also not
21
+ strictly required.
22
+
23
+ Finally, there are some routines here that can be used with the
24
+ [@axel669/aegis](https://www.npmjs.com/package/@axel669/aegis) test runner
25
+ library, which is also documented below.
18
26
 
19
27
 
20
28
  ## Installation
@@ -27,17 +35,27 @@ npm install @odatnurd/cf-requests
27
35
 
28
36
  ## Usage
29
37
 
30
- The library provides all of the pieces needed to validate incoming requests
31
- based on an appropriate schema and return a JSON result that has a consistent
32
- field layout. In addition, request handlers are wrapped such that any
33
- exceptions that propagate out of the handler function are handled as errors
34
- with an appropriate return, making the actual handler code more straight
35
- forward.
38
+ The library provides all of the pieces needed to:
39
+
40
+ - validate incoming requests based on a schema (applied to the `json` body, path
41
+ parameters, query parameters, etc).
42
+ - return a consistently structured `json` result in both `success` and `failure`
43
+ conditions
44
+ - wrap a request handler with exception handling to remove boilerplate and help
45
+ enforce consistency in results.
46
+ - compose all of the above into a single, cohesive handler for a route.
47
+
36
48
 
37
49
  ### Example
38
50
 
39
- Assuming a file named `test.joker.json` that contains the following Joker
40
- schema:
51
+ This example presumes that the
52
+ [@axel669/joker](https://www.npmjs.com/package/@axel669/joker) library is being
53
+ used to implement schema validation, and that routes are defined via the
54
+ [@axel669/hono-file-routes](https://www.npmjs.com/package/@axel669/hono-file-routes)
55
+ package.
56
+
57
+ The contents of the `test.joker.json` file looks like the following, defining
58
+ that the body of the request contain `key1` and `key2`, each of specific types:
41
59
 
42
60
  ```json
43
61
  {
@@ -49,26 +67,43 @@ schema:
49
67
  }
50
68
  ```
51
69
 
52
- The following is a minimal route handler that validates the JSON body against
53
- the schema and returning it back. The request will result in a `422` error if
54
- the data is not valid, and the JSON body is masked to ensure that only the
55
- fields declared by the schema are present.
70
+ Given this, the following is a minimal route handler that validates the JSON
71
+ body against the schema.
72
+
73
+ The request will result in a `422 Unprocessible Entity` error if the data is
74
+ not valid, and the JSON body is masked to ensure that only the fields declared
75
+ by the schema are present.
56
76
 
57
77
 
58
78
  ```js
59
- import { validate, success, routeHandler } from '#lib/common';
79
+ // Bring in the validation generator, the success response generator, and the
80
+ // route handler generator.
81
+ import { validate, success, routeHandler } from '@odatnurd/cf-requests';
60
82
 
83
+ // Using the Joker rollup plugin, this will result in a testSchema object with a
84
+ // `validate()` and `mask()` function within it, which verify that the result is
85
+ // correct and mask away any fields not defined by the schema, respectively.
61
86
  import * as testSchema from '#schemas/test';
62
87
 
63
-
88
+ // The hono-file-routes package defines routes in a file by exporting `$verb`
89
+ // as routes. Here we are using the routeHandler() generator, which constructs
90
+ // an appropriate route array based on its arguments.
91
+ //
92
+ // This value could also be used in a standard Hono app.
64
93
  export const $post = routeHandler(
94
+ // Generate a validator using the standard Hono mechanism; this will ensure
95
+ // that the JSON in the body fits the schema, and will mask extraneous fields.
65
96
  validate('json', testSchema),
66
97
 
98
+ // Async functions that take a single argument are route handlers; they will
99
+ // be automatically guarded with a try/catch block
67
100
  async (ctx) => {
101
+ // PUll out the validated JSON body.
68
102
  const body = ctx.req.valid('json');
69
103
 
70
- // Generic errors show up as a 500 server error; throwing HTTPError allows
71
- // for a specific error code to be returned instead
104
+ // Thrown exceptions inside the handler cause a fail() call to occur; the
105
+ // status is 500 for generic errors, but you can throw HTTPError instances
106
+ // to get a specific result as desired.
72
107
  if (body.key1 != 69) {
73
108
  throw new Error('key is not nice');
74
109
  }
@@ -82,31 +117,29 @@ export const $post = routeHandler(
82
117
  ## Testing Utilities (Optional)
83
118
 
84
119
  This package includes an optional set of helpers to facilitate testing your own
85
- projects with the [Aegis](https://www.npmjs.com/package/@axel669/aegis) test
86
- runner.
120
+ projects with the [@axel669/aegis](https://www.npmjs.com/package/@axel669/aegis)
121
+ test runner and the
122
+ [@odatnurd/cf-aegis](https://www.npmjs.com/package/@odatnurd/cf-aegis) helper
123
+ libraries.
87
124
 
88
125
  To use these utilities, you must install the required peer dependencies into
89
126
  your own project's `devDependencies` if you have not already done so.
90
127
 
91
128
  ```sh
92
- pnpm add -D @odatnurd/d1-query @axel669/aegis miniflare fs-jetpack
129
+ pnpm add -D @axel669/aegis @axel669/joker @odatnurd/cf-aegis miniflare
93
130
  ```
94
131
 
95
- > ℹ️ If you are actively using
96
- > [@odatnurd/d1-query](https://www.npmjs.com/package/@odatnurd/d1-query) in your
97
- > project, that library should be installed as a regular `dependency` and not a
98
- > `devDependency`
132
+ The `@odatnurd/cf-requests/aegis` module exports the following functions to
133
+ aid in setting up tests:
99
134
 
100
- The `@odatnurd/cf-requests` module exports the following functions:
101
135
 
102
-
103
- ### Helper Functions
136
+ ### Aegis Helper Functions
104
137
 
105
138
  ```javascript
106
- export function initializeResponseChecks() {}
139
+ export function initializeRequestChecks() {}
107
140
  ```
108
- Registers all [custom checks](#custom-checks) with Aegis. This should be called
109
- once at the top of your `aegis.config.js` file.
141
+ Registers all [custom checks](#custom-checks) with `Aegis`. This should be
142
+ called once at the top of your `aegis.config.js` file.
110
143
 
111
144
  ---
112
145
 
@@ -114,8 +147,8 @@ once at the top of your `aegis.config.js` file.
114
147
  export async function schemaTest(dataType, schema, data, validator = undefined) {}
115
148
  ```
116
149
  Takes a `dataType` and `schema` as would be provided to the `validate` function
117
- and runs the validation to see what the result is. The function will return
118
- either:
150
+ and runs the validation against `data` to see what the result is. The function
151
+ will return either:
119
152
 
120
153
  * `Valid Data`: An Object that represents the validated and masked data
121
154
  * `Invalid Data`: A `Response` object that carries the error payload
@@ -130,7 +163,7 @@ having to use it in the actual request first.
130
163
  > such as during migrations to this library.
131
164
 
132
165
 
133
- ### Configuration
166
+ ### Aegis Test Configuration
134
167
 
135
168
  You can import the helper functions into your `aegis.config.js` file to easily
136
169
  set up a test environment, optionally also populating one or more SQL files into
@@ -140,10 +173,10 @@ the database first in order to set up testing.
140
173
 
141
174
  ```js
142
175
  import { initializeCustomChecks, aegisSetup, aegisTeardown } from '@odatnurd/cf-aegis';
143
- import { initializeResponseChecks } from '@odatnurd/cf-requests/aegis';
176
+ import { initializeRequestChecks } from '@odatnurd/cf-requests/aegis';
144
177
 
145
178
  initializeCustomChecks();
146
- initializeResponseChecks()
179
+ initializeRequestChecks()
147
180
 
148
181
  export const config = {
149
182
  files: [
@@ -165,8 +198,8 @@ export const config = {
165
198
 
166
199
  ### Custom Checks
167
200
 
168
- The `initializeResponseChecks()` function registers several custom checks with Aegis
169
- to simplify testing database-related logic.
201
+ The `initializeRequestChecks()` function registers several custom checks with
202
+ Aegis to simplify testing database-related logic.
170
203
 
171
204
  * `.isResponse($)`: Checks if a value is a `Response` object.
172
205
  * `.isNotResponse($)`: Checks if a value is not a `Response` object.
@@ -174,14 +207,14 @@ to simplify testing database-related logic.
174
207
  specific `status` code.
175
208
 
176
209
 
177
- ## Methods
210
+ ## Library Methods
178
211
 
179
212
  ```js
180
- export function success(ctx, message, result, status) {}
213
+ export function success(ctx, message, result=[], status=200) {}
181
214
  ```
182
215
 
183
- Indicate a successful return in JSON with the given `HTTP` status code; the
184
- status code is used to construct the JSON as well as the response:
216
+ Generate a successful return in JSON with the given `HTTP` status code; the
217
+ status code is used to construct the JSON as well as the response object:
185
218
 
186
219
  ```js
187
220
  {
@@ -198,13 +231,14 @@ status code is used to construct the JSON as well as the response:
198
231
  ---
199
232
 
200
233
  ```js
201
- export function fail(ctx, message, status, result) {}
234
+ export function fail(ctx, message, status=400, result=undefined) {}
202
235
  ```
203
236
 
204
- Indicates a failure return in JSON with the given `HTTP` status code.
237
+ Generate a failure return in JSON with the given `HTTP` status code; the status
238
+ code is used to construct the JSON as well as the response object:
205
239
 
206
- This follows a similar form to `success`, though the `success` field is `false`
207
- instead of `true`.
240
+ This results in JSON in a similar form to `success`, though the `success` field
241
+ is `false` instead of `true`.
208
242
 
209
243
  Note that the order of the last two arguments is different because generally
210
244
  one wants to specify the status of an error but it usually does not return any
@@ -216,19 +250,28 @@ provided, the `data` field will not be present in the result.
216
250
  ---
217
251
 
218
252
  ```js
219
- export function validate(dataType, schemaObj) {}
253
+ export function validate(dataType, { validate, mask? }) {}
220
254
  ```
221
255
 
222
256
  This function uses the [Hono validator()](https://hono.dev/docs/guides/validation)
223
257
  function to create a validator that will validate the data of the provided type
224
- using the provided `Joker` schema.
258
+ using the provided validation object.
259
+
260
+ The second parameter should be an object that contains a `validate` and an
261
+ (optional) `mask` member:
262
+
263
+ - `validate` takes the item to validate, and returns `true` when the data given
264
+ is valid. Any other return value is considered to be an error.
265
+ - `mask` takes as an input the same item passed to `validate`, and returns a
266
+ masked version of the data that strips fields from the object that do not
267
+ appear in the schema.
268
+
269
+ If `mask` is not provided, then the data will be validated but not masked.
225
270
 
226
- `schemaObj` is an object that contains a `validate` and `mask` member that can
227
- validate data and store it into the `Hono` context, masking away all fields that
228
- are not present in the schema; this is intended to be used with
229
- [@axel669/joker](https://www.npmjs.com/package/@axel669/joker), though you are
230
- free to use any other validation schema so long as the call signatures match
231
- that of `joker`.
271
+ This method is intended to be used with the
272
+ [@axel669/joker](https://www.npmjs.com/package/@axel669/joker) library (and
273
+ in particular it's rollup plugin), though you are free to use any other
274
+ validation schema so long as the call signatures are as defined above.
232
275
 
233
276
  On success, the data is placed in the context. If the data does not pass the
234
277
  validation of the schema, the `fail()` method is invoked on it with a status of
@@ -244,12 +287,20 @@ This is a simple wrapper which returns a function that wraps the provided
244
287
  handler function in a `try-catch` block, so that any uncaught exceptions can
245
288
  gracefully return a `fail()` result.
246
289
 
247
- The wrapper returned by this function will itself return either the result of
248
- the provided handler, so long as no exceptions are raised.
290
+ The wrapper returned by this function will itself return the result of the
291
+ provided handler, so long as no exceptions are raised.
292
+
293
+ When an exception is caught, the `fail()` function is used to generate and
294
+ return a response; this will contain as a message the text of the exception
295
+ that was caught.
249
296
 
250
297
  Exceptions of type `HttpError` carry a specific `HTTP` status code, which will
251
298
  be used in the call to `fail()`; all other exceptions use a status of `500`.
252
299
 
300
+ For debugging, if your worker has the `CF_REQUESTS_STACKTRACE` environment
301
+ variable set to either `true` or `yes`, the `fail()` response will include in
302
+ its data field the stack trace as an array of strings that represent the trace.
303
+
253
304
  ```js
254
305
  export const $post = [
255
306
  validate('json', testSchema),
@@ -259,6 +310,9 @@ export const $post = [
259
310
 
260
311
  if (body.key1 != 69) {
261
312
  throw new Error('key is not nice');
313
+
314
+ // The above throw is the same as:
315
+ // return fail(ctx, 'key is not nice', 500)
262
316
  }
263
317
 
264
318
  return success(ctx, 'code test worked', body);
@@ -269,16 +323,16 @@ export const $post = [
269
323
  ---
270
324
 
271
325
  ```js
272
- export class HttpError extends Error(message: string, status) {}
326
+ export class HttpError extends Error { constructor(message, status=500) {} }
273
327
  ```
274
328
 
275
329
  This is a simple exception class that wraps a textual message and a status code.
276
330
 
277
- When `body()` catches an exception of this type, it directly returns an error
278
- JSON containing the provided message and uses the status code as the `HTTP`
279
- status of the return.
331
+ When `body()` catches an exception of this type, the `fail()` call it makes
332
+ will use the status provided here as the `HTTP` status of the return.
280
333
 
281
- If `status` is not provided, it defaults to `500`.
334
+ If `status` is not provided, it defaults to `500`, making this class generate an
335
+ error with the same layout as any other exception class.
282
336
 
283
337
  ---
284
338
 
@@ -297,14 +351,15 @@ cleaner looking, while still allowing for arbitrary middleware
297
351
  export const $post = routeHandler(
298
352
  validate('json', testSchema),
299
353
 
300
- // More than one argument, so function is directly returned; no body() call.
354
+ // More than one argument, so function is directly returned; no body() call
355
+ // wrapper here.
301
356
  async (ctx, next) => {
302
357
  console.log('Async middleware is running!');
303
358
  await next();
304
359
  },
305
360
 
306
361
  // Single argument async functions are wrapped in body(), so exceptions raised
307
- // are handled appropriately.
362
+ // are handled consistently.
308
363
  async (ctx) => {
309
364
  const body = ctx.req.valid('json');
310
365
 
package/aegis/index.js CHANGED
@@ -8,6 +8,45 @@ import { validate } from '../lib/handlers.js';
8
8
  /******************************************************************************/
9
9
 
10
10
 
11
+ /* A mapping of all of the common status errors that might be returned by the
12
+ * validator. */
13
+ const STATUS_TEXT = {
14
+ 400: 'Bad Request',
15
+ 401: 'Unauthorized',
16
+ 402: 'Payment Required',
17
+ 403: 'Forbidden',
18
+ 404: 'Not Found',
19
+ 405: 'Method Not Allowed',
20
+ 406: 'Not Acceptable',
21
+ 407: 'Proxy Authentication Required',
22
+ 408: 'Request Timeout',
23
+ 409: 'Conflict',
24
+ 410: 'Gone',
25
+ 411: 'Length Required',
26
+ 412: 'Precondition Failed',
27
+ 413: 'Payload Too Large',
28
+ 414: 'URI Too Long',
29
+ 415: 'Unsupported Media Type',
30
+ 416: 'Range Not Satisfiable',
31
+ 417: 'Expectation Failed',
32
+ 418: "I'm a teapot",
33
+ 421: 'Misdirected Request',
34
+ 422: 'Unprocessable Entity',
35
+ 423: 'Locked',
36
+ 424: 'Failed Dependency',
37
+ 425: 'Too Early',
38
+ 426: 'Upgrade Required',
39
+ 428: 'Precondition Required',
40
+ 429: 'Too Many Requests',
41
+ 431: 'Request Header Fields Too Large',
42
+ 451: 'Unavailable For Legal Reasons',
43
+ 500: 'Internal Server Error',
44
+ };
45
+
46
+
47
+ /******************************************************************************/
48
+
49
+
11
50
  /*
12
51
  * Initializes some custom Aegis checks that make testing of schema and data
13
52
  * requests easier.
@@ -46,126 +85,116 @@ export function initializeRequestChecks() {
46
85
  * within it. */
47
86
  export async function schemaTest(dataType, schema, data, validator) {
48
87
  // If a validator is provided, use it; otherwise use ours. This requires that
49
- // you provide a call-compatible validator. This is here only to support some
50
- // migrations of old code that is using a different validator than the one
51
- // this library currently uses.
88
+ // you provide a call-compatible validator. This is here primarily to support
89
+ // some migrations of old code that is using a different validator than the
90
+ // one this library currently uses.
52
91
  validator = validator ??= validate;
53
92
 
54
93
  // Use the Hono factory to create our middleware, just as a caller would.
55
- // Create a middleware using the Hono factory method for this, using the
56
- // schema object and data type provided.
57
94
  const middleware = validator(dataType, schema);
58
95
 
59
- // As a result of the middleware, we will either capture the validated (and
60
- // masked) input JSON data, or we will capture an error response. As a part of
61
- // this we also capture what the eventual status of the call would be if this
62
- // generates a response, so that we can put it into the response object.
96
+ // A successful test captures the validated and masked JSON output, while a
97
+ // failed test generates a failure JSON response and has a specific status
98
+ // as a result of the validator's call to fail().
63
99
  let validData = null;
64
100
  let errorResponse = null;
65
101
  let responseStatus = 200;
66
102
 
67
- // A fake next to pass to the middleware when we execute it, so that it does
68
- // not throw an error.
69
- const next = () => {};
70
-
71
- // For form data, we need to create a single, shared request object *before*
72
- // we build the context, so that both the header and the body can be derived
73
- // from the same source, ensuring the multipart boundary matches.
74
- let tempRequest = null;
103
+ // In order to handle formdata, cookie, and header validation we need a
104
+ // request object to put into the context. These portions are parsed out of
105
+ // the response by the validator and thus can't be backfilled. This also
106
+ // ensures that for formData we get a proper form encoded body.
107
+ const options = { method: 'POST' };
75
108
  if (dataType === 'form') {
76
- const formData = new FormData();
77
- for (const key in data) {
78
- formData.append(key, data[key]);
79
- }
80
- tempRequest = new Request('http://localhost', {
81
- method: 'POST',
82
- body: formData,
83
- });
109
+ // For form data, turn the passed in object into FormData and add it to
110
+ // the body.
111
+ options.body = new FormData();
112
+ Object.entries(data).forEach(([k, v]) => options.body.append(k, v));
113
+
114
+ } else if (dataType === 'cookie') {
115
+ // If we are testing cookies, we need a cookie header
116
+ options.headers = { 'Cookie': Object.entries(data).map(([k,v]) => `${k}=${v}`).join('; ') }
117
+
118
+ } else if (dataType === 'header') {
119
+ // If we are testing a header, we need actual headers.
120
+ options.headers = data;
84
121
  }
85
122
 
123
+ // Create the response now.
124
+ const rawRequest = new Request('http://localhost/', options)
86
125
 
87
- // In order to run the test we need to create a fake Hono context object to
88
- // pass to the middleware; this mimics the smallest possible footprint of
89
- // Hono context for our purposes.
126
+ // Construct a mock Hono context object to pass to the middleware. We have
127
+ // here a mix of functions that the validator will call to get data that Hono
128
+ // has already processed or should process, such as the JSON body or the
129
+ // mapped request URI paramters, as well as a raw Request object for things
130
+ // that Hono does not tend to parse, such as form data and headers.
90
131
  const ctx = {
91
132
  req: {
92
- // These methods are used by the validator to pull the parsed data out of
93
- // the request in order to validate it, except for when the data type is
94
- // header, in which case it invokes the header() function with no name.
133
+ // The raw request; used by form data, headers, and cookies.
134
+ raw: rawRequest,
135
+
136
+ // These methods in the context convey information that Hono parses as a
137
+ // part of its request handling; as such we can return the data back
138
+ // directly.
95
139
  param: () => data,
96
140
  json: async () => data,
97
- query: (key) => data[key],
98
- queries: (key) => {
99
- const result = {};
100
- for(const [k, v] of Object.entries(data)) {
101
- result[k] = Array.isArray(v) ? v : [v];
102
- }
103
- return key ? result[key] : result;
104
- },
105
- cookie: () => data,
106
- formData: async () => {
107
- if (dataType === 'form') {
108
- return tempRequest.formData();
109
- }
110
- // Fallback for other types, though not strictly needed by the validator
111
- const formData = new FormData();
112
- for (const key in data) {
113
- formData.append(key, data[key]);
114
- }
115
- return formData;
116
- },
117
- // For form data, the validator expects to be able to get the raw body
118
- // as an ArrayBuffer. We can simulate this by URL-encoding the data.
119
- arrayBuffer: async () => tempRequest ? tempRequest.arrayBuffer() : new ArrayBuffer(0),
120
- // The validator also uses a bodyCache property to store parsed bodies.
121
- bodyCache: {},
122
141
 
142
+ // Query paramters must always return the value of a key as an array
143
+ // since they can appear more than once; also, if you provide no key, you
144
+ // get them all. We're precomputing here for no good reason.
145
+ queries: (() => {
146
+ const result = Object.entries(data).reduce((acc, [key, value]) => {
147
+ acc[key] = Array.isArray(value) ? value : [value];
148
+ return acc;
149
+ }, {});
150
+
151
+ return key => key ? result[key] : result;
152
+ })(),
153
+
154
+ // For form data, the validator expects to be able to get at the raw body
155
+ // and a place to cache the parsed body data.
156
+ arrayBuffer: async () => rawRequest.arrayBuffer(),
157
+ bodyCache: {},
123
158
 
124
- // We need to populate an actual cookie header in headers for it the
125
- // validator to be able to pull cookie data because it wants to parse it
126
- // itself.
127
- headers: new Headers(dataType === 'cookie'
128
- ? { 'Cookie': Object.entries(data).map(([k,v]) => `${k}=${v}`).join('; ') }
129
- : {}),
130
-
131
- // The validator invokes this to get headers out of the request when the
132
- // data type is JSON.
133
- header: (name) => {
134
- // If there is no name, return the data back directly; this call pattern
135
- // happens when the data type is header.
159
+ // The context supports gathering either a single header by name, or all
160
+ // headers (by passing undefined as a name.
161
+ header: name => {
136
162
  if (name === undefined) {
137
163
  return data;
138
164
  }
139
165
 
140
166
  return name.toLowerCase() !== 'content-type' ? undefined : {
141
167
  json: 'application/json',
142
- form: tempRequest?.headers.get('Content-Type'),
168
+ form: rawRequest.headers.get('Content-Type'),
143
169
  }[dataType];
144
170
  },
145
171
 
146
- // When validation succeeds, it invokes this to store the data back into
147
- // the context.
172
+ // The validator invokes this to store the validated data back to the
173
+ // context; here we just capture it as the validated data for later
174
+ // return.
148
175
  addValidatedData: (target, data) => validData = data
149
176
  },
150
177
 
151
- // Used to capture a failure; the validator will invoke status to set the
152
- // required HTTP response and then invoke the json() method to populate the
153
- // error.
154
- status: (inStatus) => { responseStatus = inStatus; },
155
- json: (payload) => {
178
+ // If a failure occurs, the validator should call fail(), which invokes
179
+ // thee two endpoints to place an error status and JSON payload into the
180
+ // response. Here we just create an actual response object, since that is
181
+ // what the middleware would return.
182
+ status: status => responseStatus = status,
183
+ json: payload => {
156
184
  errorResponse = new Response(
157
185
  JSON.stringify(payload), {
158
186
  status: responseStatus,
159
- statusText: "Bad Request",
187
+ statusText: STATUS_TEXT[responseStatus] ?? 'Unknown Error',
160
188
  headers: { "Content-Type": "application/json" }
161
189
  }
162
190
  );
163
191
  },
164
192
  };
165
193
 
194
+ // Execute the middleware with an empty next().
166
195
  // Run the middleware; we either capture a result in the error payload or the
167
196
  // validation result.
168
- await middleware(ctx, next);
197
+ await middleware(ctx, () => {});
169
198
 
170
199
  // Return the error payload if validation failed, otherwise return the
171
200
  // validated data from the success path.
package/lib/handlers.js CHANGED
@@ -49,11 +49,11 @@ export const fail = (ctx, message, status, result) => {
49
49
  *
50
50
  * When using this filter, underlying requests can fetch the validated data
51
51
  * via the ctx.req.valid() function, e.g. ctx.req.valid('json'). */
52
- export const validate = (dataType, schemaObj) => validator(dataType, async (value, ctx) => {
52
+ export const validate = (dataType, { validate, mask }) => validator(dataType, async (value, ctx) => {
53
53
  // Joker returns true for valid data and an array of error objects on failure.
54
- const result = await schemaObj.validate(value);
54
+ const result = await validate(value);
55
55
  if (result === true) {
56
- return schemaObj.mask(value);
56
+ return typeof mask === 'function' ? mask(value) : value;
57
57
  }
58
58
 
59
59
  return fail(ctx, `${dataType} data is not valid`, 422, result);
@@ -92,11 +92,29 @@ export function body(handler) {
92
92
  return await handler(ctx);
93
93
  }
94
94
  catch (err) {
95
+ const generateTrace = ['true', 'yes'].includes(ctx.env.CF_REQUESTS_STACKTRACE);
96
+ let trace = undefined;
97
+
98
+ // If we should generate the stack trace and the error that we got
99
+ // actually has a trace in it, then generate a trace array by converting
100
+ // the stack into an array of locations for better readability.
101
+ //
102
+ // This elides the start line, since we capture the message already below,
103
+ // and removes the `at` prefix since that is redundant.
104
+ if (generateTrace === true && typeof err.stack === 'string') {
105
+ trace = err.stack.split('\n')
106
+ .slice(1)
107
+ .map(line => {
108
+ const trimmedLine = line.trim();
109
+ return trimmedLine.startsWith('at ') ? trimmedLine.substring(3) : trimmedLine;
110
+ });
111
+ }
112
+
95
113
  if (err instanceof HttpError) {
96
- return fail(ctx, err.message, err.status);
114
+ return fail(ctx, err.message, err.status, trace);
97
115
  }
98
116
 
99
- return fail(ctx, err.message, 500);
117
+ return fail(ctx, err.message, 500, trace);
100
118
  }
101
119
  }
102
120
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@odatnurd/cf-requests",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Simple Cloudflare Hono request wrapper",
5
5
  "author": "OdatNurd (https://odatnurd.net)",
6
6
  "homepage": "https://github.com/OdatNurd/cf-requests",
@@ -26,10 +26,34 @@
26
26
  "routing",
27
27
  "aegis"
28
28
  ],
29
+ "devDependencies": {
30
+ "@axel669/aegis": "^0.3.1",
31
+ "@axel669/joker": "^0.3.5",
32
+ "@odatnurd/cf-aegis": "^0.1.2",
33
+ "miniflare": "^4.20250813.0"
34
+ },
29
35
  "peerDependencies": {
30
- "hono": "^4.7.0"
36
+ "@axel669/aegis": "^0.3.1",
37
+ "@axel669/joker": "^0.3.5",
38
+ "@odatnurd/cf-aegis": "^0.1.2",
39
+ "hono": "^4.7.0",
40
+ "miniflare": "^4.20250813.0"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "@axel669/aegis": {
44
+ "optional": true
45
+ },
46
+ "@axel669/joker": {
47
+ "optional": true
48
+ },
49
+ "@odatnurd/cf-aegis": {
50
+ "optional": true
51
+ },
52
+ "miniflare": {
53
+ "optional": true
54
+ }
31
55
  },
32
56
  "scripts": {
33
- "test": "echo \"No tests specified\" && exit 0"
57
+ "test": "aegis test/aegis.config.js"
34
58
  }
35
59
  }