@odatnurd/cf-requests 0.1.7 → 0.1.9
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 +125 -69
- package/lib/handlers.js +23 -5
- package/package.json +12 -5
package/README.md
CHANGED
|
@@ -1,23 +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
|
-
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.
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
|
|
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.
|
|
21
26
|
|
|
22
27
|
|
|
23
28
|
## Installation
|
|
@@ -30,17 +35,27 @@ npm install @odatnurd/cf-requests
|
|
|
30
35
|
|
|
31
36
|
## Usage
|
|
32
37
|
|
|
33
|
-
The library provides all of the pieces needed to
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
+
|
|
39
48
|
|
|
40
49
|
### Example
|
|
41
50
|
|
|
42
|
-
|
|
43
|
-
|
|
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:
|
|
44
59
|
|
|
45
60
|
```json
|
|
46
61
|
{
|
|
@@ -52,27 +67,43 @@ schema, and and you are using the Joker Rollup plugin:
|
|
|
52
67
|
}
|
|
53
68
|
```
|
|
54
69
|
|
|
55
|
-
|
|
56
|
-
the schema
|
|
57
|
-
|
|
58
|
-
|
|
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.
|
|
59
76
|
|
|
60
77
|
|
|
61
78
|
```js
|
|
62
|
-
|
|
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';
|
|
63
82
|
|
|
64
|
-
//
|
|
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.
|
|
65
86
|
import * as testSchema from '#schemas/test';
|
|
66
87
|
|
|
67
|
-
|
|
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.
|
|
68
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.
|
|
69
96
|
validate('json', testSchema),
|
|
70
97
|
|
|
98
|
+
// Async functions that take a single argument are route handlers; they will
|
|
99
|
+
// be automatically guarded with a try/catch block
|
|
71
100
|
async (ctx) => {
|
|
101
|
+
// PUll out the validated JSON body.
|
|
72
102
|
const body = ctx.req.valid('json');
|
|
73
103
|
|
|
74
|
-
//
|
|
75
|
-
// 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.
|
|
76
107
|
if (body.key1 != 69) {
|
|
77
108
|
throw new Error('key is not nice');
|
|
78
109
|
}
|
|
@@ -86,8 +117,10 @@ export const $post = routeHandler(
|
|
|
86
117
|
## Testing Utilities (Optional)
|
|
87
118
|
|
|
88
119
|
This package includes an optional set of helpers to facilitate testing your own
|
|
89
|
-
projects with the [
|
|
90
|
-
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.
|
|
91
124
|
|
|
92
125
|
To use these utilities, you must install the required peer dependencies into
|
|
93
126
|
your own project's `devDependencies` if you have not already done so.
|
|
@@ -96,16 +129,17 @@ your own project's `devDependencies` if you have not already done so.
|
|
|
96
129
|
pnpm add -D @axel669/aegis @axel669/joker @odatnurd/cf-aegis miniflare
|
|
97
130
|
```
|
|
98
131
|
|
|
99
|
-
The `@odatnurd/cf-requests/aegis` module exports the following functions
|
|
132
|
+
The `@odatnurd/cf-requests/aegis` module exports the following functions to
|
|
133
|
+
aid in setting up tests:
|
|
100
134
|
|
|
101
135
|
|
|
102
|
-
### Helper Functions
|
|
136
|
+
### Aegis Helper Functions
|
|
103
137
|
|
|
104
138
|
```javascript
|
|
105
|
-
export function
|
|
139
|
+
export function initializeRequestChecks() {}
|
|
106
140
|
```
|
|
107
|
-
Registers all [custom checks](#custom-checks) with Aegis
|
|
108
|
-
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.
|
|
109
143
|
|
|
110
144
|
---
|
|
111
145
|
|
|
@@ -113,8 +147,8 @@ once at the top of your `aegis.config.js` file.
|
|
|
113
147
|
export async function schemaTest(dataType, schema, data, validator = undefined) {}
|
|
114
148
|
```
|
|
115
149
|
Takes a `dataType` and `schema` as would be provided to the `validate` function
|
|
116
|
-
and runs the validation to see what the result is. The function
|
|
117
|
-
either:
|
|
150
|
+
and runs the validation against `data` to see what the result is. The function
|
|
151
|
+
will return either:
|
|
118
152
|
|
|
119
153
|
* `Valid Data`: An Object that represents the validated and masked data
|
|
120
154
|
* `Invalid Data`: A `Response` object that carries the error payload
|
|
@@ -129,7 +163,7 @@ having to use it in the actual request first.
|
|
|
129
163
|
> such as during migrations to this library.
|
|
130
164
|
|
|
131
165
|
|
|
132
|
-
### Configuration
|
|
166
|
+
### Aegis Test Configuration
|
|
133
167
|
|
|
134
168
|
You can import the helper functions into your `aegis.config.js` file to easily
|
|
135
169
|
set up a test environment, optionally also populating one or more SQL files into
|
|
@@ -139,10 +173,10 @@ the database first in order to set up testing.
|
|
|
139
173
|
|
|
140
174
|
```js
|
|
141
175
|
import { initializeCustomChecks, aegisSetup, aegisTeardown } from '@odatnurd/cf-aegis';
|
|
142
|
-
import {
|
|
176
|
+
import { initializeRequestChecks } from '@odatnurd/cf-requests/aegis';
|
|
143
177
|
|
|
144
178
|
initializeCustomChecks();
|
|
145
|
-
|
|
179
|
+
initializeRequestChecks()
|
|
146
180
|
|
|
147
181
|
export const config = {
|
|
148
182
|
files: [
|
|
@@ -164,8 +198,8 @@ export const config = {
|
|
|
164
198
|
|
|
165
199
|
### Custom Checks
|
|
166
200
|
|
|
167
|
-
The `
|
|
168
|
-
to simplify testing database-related logic.
|
|
201
|
+
The `initializeRequestChecks()` function registers several custom checks with
|
|
202
|
+
Aegis to simplify testing database-related logic.
|
|
169
203
|
|
|
170
204
|
* `.isResponse($)`: Checks if a value is a `Response` object.
|
|
171
205
|
* `.isNotResponse($)`: Checks if a value is not a `Response` object.
|
|
@@ -173,14 +207,14 @@ to simplify testing database-related logic.
|
|
|
173
207
|
specific `status` code.
|
|
174
208
|
|
|
175
209
|
|
|
176
|
-
## Methods
|
|
210
|
+
## Library Methods
|
|
177
211
|
|
|
178
212
|
```js
|
|
179
|
-
export function success(ctx, message, result, status) {}
|
|
213
|
+
export function success(ctx, message, result=[], status=200) {}
|
|
180
214
|
```
|
|
181
215
|
|
|
182
|
-
|
|
183
|
-
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:
|
|
184
218
|
|
|
185
219
|
```js
|
|
186
220
|
{
|
|
@@ -197,13 +231,14 @@ status code is used to construct the JSON as well as the response:
|
|
|
197
231
|
---
|
|
198
232
|
|
|
199
233
|
```js
|
|
200
|
-
export function fail(ctx, message, status, result) {}
|
|
234
|
+
export function fail(ctx, message, status=400, result=undefined) {}
|
|
201
235
|
```
|
|
202
236
|
|
|
203
|
-
|
|
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:
|
|
204
239
|
|
|
205
|
-
This
|
|
206
|
-
instead of `true`.
|
|
240
|
+
This results in JSON in a similar form to `success`, though the `success` field
|
|
241
|
+
is `false` instead of `true`.
|
|
207
242
|
|
|
208
243
|
Note that the order of the last two arguments is different because generally
|
|
209
244
|
one wants to specify the status of an error but it usually does not return any
|
|
@@ -215,19 +250,28 @@ provided, the `data` field will not be present in the result.
|
|
|
215
250
|
---
|
|
216
251
|
|
|
217
252
|
```js
|
|
218
|
-
export function validate(dataType,
|
|
253
|
+
export function validate(dataType, { validate, mask? }) {}
|
|
219
254
|
```
|
|
220
255
|
|
|
221
256
|
This function uses the [Hono validator()](https://hono.dev/docs/guides/validation)
|
|
222
257
|
function to create a validator that will validate the data of the provided type
|
|
223
|
-
using the provided
|
|
258
|
+
using the provided validation object.
|
|
224
259
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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.
|
|
270
|
+
|
|
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.
|
|
231
275
|
|
|
232
276
|
On success, the data is placed in the context. If the data does not pass the
|
|
233
277
|
validation of the schema, the `fail()` method is invoked on it with a status of
|
|
@@ -243,12 +287,20 @@ This is a simple wrapper which returns a function that wraps the provided
|
|
|
243
287
|
handler function in a `try-catch` block, so that any uncaught exceptions can
|
|
244
288
|
gracefully return a `fail()` result.
|
|
245
289
|
|
|
246
|
-
The wrapper returned by this function will itself return
|
|
247
|
-
|
|
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.
|
|
248
296
|
|
|
249
297
|
Exceptions of type `HttpError` carry a specific `HTTP` status code, which will
|
|
250
298
|
be used in the call to `fail()`; all other exceptions use a status of `500`.
|
|
251
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
|
+
|
|
252
304
|
```js
|
|
253
305
|
export const $post = [
|
|
254
306
|
validate('json', testSchema),
|
|
@@ -258,6 +310,9 @@ export const $post = [
|
|
|
258
310
|
|
|
259
311
|
if (body.key1 != 69) {
|
|
260
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)
|
|
261
316
|
}
|
|
262
317
|
|
|
263
318
|
return success(ctx, 'code test worked', body);
|
|
@@ -268,16 +323,16 @@ export const $post = [
|
|
|
268
323
|
---
|
|
269
324
|
|
|
270
325
|
```js
|
|
271
|
-
export class HttpError extends Error(message
|
|
326
|
+
export class HttpError extends Error { constructor(message, status=500) {} }
|
|
272
327
|
```
|
|
273
328
|
|
|
274
329
|
This is a simple exception class that wraps a textual message and a status code.
|
|
275
330
|
|
|
276
|
-
When `body()` catches an exception of this type,
|
|
277
|
-
|
|
278
|
-
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.
|
|
279
333
|
|
|
280
|
-
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.
|
|
281
336
|
|
|
282
337
|
---
|
|
283
338
|
|
|
@@ -296,14 +351,15 @@ cleaner looking, while still allowing for arbitrary middleware
|
|
|
296
351
|
export const $post = routeHandler(
|
|
297
352
|
validate('json', testSchema),
|
|
298
353
|
|
|
299
|
-
// 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.
|
|
300
356
|
async (ctx, next) => {
|
|
301
357
|
console.log('Async middleware is running!');
|
|
302
358
|
await next();
|
|
303
359
|
},
|
|
304
360
|
|
|
305
361
|
// Single argument async functions are wrapped in body(), so exceptions raised
|
|
306
|
-
// are handled
|
|
362
|
+
// are handled consistently.
|
|
307
363
|
async (ctx) => {
|
|
308
364
|
const body = ctx.req.valid('json');
|
|
309
365
|
|
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.9",
|
|
4
4
|
"description": "Simple Cloudflare Hono request wrapper",
|
|
5
5
|
"author": "OdatNurd (https://odatnurd.net)",
|
|
6
6
|
"homepage": "https://github.com/OdatNurd/cf-requests",
|
|
@@ -29,15 +29,19 @@
|
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@axel669/aegis": "^0.3.1",
|
|
31
31
|
"@axel669/joker": "^0.3.5",
|
|
32
|
-
"@odatnurd/cf-aegis": "^0.1.
|
|
33
|
-
"
|
|
32
|
+
"@odatnurd/cf-aegis": "^0.1.4",
|
|
33
|
+
"json5": "^2.2.3",
|
|
34
|
+
"miniflare": "^4.20250923.0",
|
|
35
|
+
"smol-toml": "^1.4.2"
|
|
34
36
|
},
|
|
35
37
|
"peerDependencies": {
|
|
36
38
|
"@axel669/aegis": "^0.3.1",
|
|
37
39
|
"@axel669/joker": "^0.3.5",
|
|
38
|
-
"@odatnurd/cf-aegis": "^0.1.
|
|
40
|
+
"@odatnurd/cf-aegis": "^0.1.4",
|
|
39
41
|
"hono": "^4.7.0",
|
|
40
|
-
"
|
|
42
|
+
"json5": "^2.2.3",
|
|
43
|
+
"miniflare": "^4.20250923.0",
|
|
44
|
+
"smol-toml": "^1.4.2"
|
|
41
45
|
},
|
|
42
46
|
"peerDependenciesMeta": {
|
|
43
47
|
"@axel669/aegis": {
|
|
@@ -50,6 +54,9 @@
|
|
|
50
54
|
"optional": true
|
|
51
55
|
},
|
|
52
56
|
"miniflare": {
|
|
57
|
+
"smol-toml": true
|
|
58
|
+
},
|
|
59
|
+
"json5": {
|
|
53
60
|
"optional": true
|
|
54
61
|
}
|
|
55
62
|
},
|