@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 +127 -72
- package/aegis/index.js +107 -78
- package/lib/handlers.js +23 -5
- package/package.json +27 -3
package/README.md
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
# Simple CloudFlare Request Handlers
|
|
2
2
|
|
|
3
|
-
`cf-requests` is a
|
|
4
|
-
|
|
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
|
|
8
|
-
though this is not strictly required.
|
|
7
|
+
[Hono](https://hono.dev/) as a route handler.
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
53
|
-
the schema
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
71
|
-
// for
|
|
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 [
|
|
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 @
|
|
129
|
+
pnpm add -D @axel669/aegis @axel669/joker @odatnurd/cf-aegis miniflare
|
|
93
130
|
```
|
|
94
131
|
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
139
|
+
export function initializeRequestChecks() {}
|
|
107
140
|
```
|
|
108
|
-
Registers all [custom checks](#custom-checks) with Aegis
|
|
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
|
|
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 {
|
|
176
|
+
import { initializeRequestChecks } from '@odatnurd/cf-requests/aegis';
|
|
144
177
|
|
|
145
178
|
initializeCustomChecks();
|
|
146
|
-
|
|
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 `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
|
248
|
-
|
|
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
|
|
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,
|
|
278
|
-
|
|
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
|
|
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
|
|
50
|
-
// migrations of old code that is using a different validator than the
|
|
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
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
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
|
-
//
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
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
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
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:
|
|
168
|
+
form: rawRequest.headers.get('Content-Type'),
|
|
143
169
|
}[dataType];
|
|
144
170
|
},
|
|
145
171
|
|
|
146
|
-
//
|
|
147
|
-
// the
|
|
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
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
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:
|
|
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,
|
|
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,
|
|
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
|
|
54
|
+
const result = await validate(value);
|
|
55
55
|
if (result === true) {
|
|
56
|
-
return
|
|
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.
|
|
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
|
-
"
|
|
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": "
|
|
57
|
+
"test": "aegis test/aegis.config.js"
|
|
34
58
|
}
|
|
35
59
|
}
|