@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.
Files changed (3) hide show
  1. package/README.md +125 -69
  2. package/lib/handlers.js +23 -5
  3. 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 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.
18
22
 
19
- > ℹ️ Technically, the validation wrapper will accept any validator (e.g. zod) as
20
- > long as the object passed in conforms to the validation contract; see below.
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 validate incoming requests
34
- based on an appropriate schema and return a JSON result that has a consistent
35
- field layout. In addition, request handlers are wrapped such that any
36
- exceptions that propagate out of the handler function are handled as errors
37
- with an appropriate return, making the actual handler code more straight
38
- 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
+
39
48
 
40
49
  ### Example
41
50
 
42
- Assuming a file named `test.joker.json` that contains the following Joker
43
- schema, and and you are using the Joker Rollup plugin:
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
- The following is a minimal route handler that validates the JSON body against
56
- the schema and returning it back. The request will result in a `422` error if
57
- the data is not valid, and the JSON body is masked to ensure that only the
58
- 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.
59
76
 
60
77
 
61
78
  ```js
62
- 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';
63
82
 
64
- // Use the Joker rollup plugin to obtain the object we require
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
- // Generic errors show up as a 500 server error; throwing HTTPError allows
75
- // 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.
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 [Aegis](https://www.npmjs.com/package/@axel669/aegis) test
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 initializeResponseChecks() {}
139
+ export function initializeRequestChecks() {}
106
140
  ```
107
- Registers all [custom checks](#custom-checks) with Aegis. This should be called
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 will return
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 { initializeResponseChecks } from '@odatnurd/cf-requests/aegis';
176
+ import { initializeRequestChecks } from '@odatnurd/cf-requests/aegis';
143
177
 
144
178
  initializeCustomChecks();
145
- initializeResponseChecks()
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 `initializeResponseChecks()` function registers several custom checks with Aegis
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
- Indicate a successful return in JSON with the given `HTTP` status code; the
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
- 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:
204
239
 
205
- This follows a similar form to `success`, though the `success` field is `false`
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, schemaObj) {}
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 `Joker` schema.
258
+ using the provided validation object.
224
259
 
225
- `schemaObj` is an object that contains a `validate` and `mask` member that can
226
- validate data and store it into the `Hono` context, masking away all fields that
227
- are not present in the schema; this is intended to be used with
228
- [@axel669/joker](https://www.npmjs.com/package/@axel669/joker), though you are
229
- free to use any other validation schema so long as the call signatures match
230
- that of `joker`.
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 either the result of
247
- 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.
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: string, status) {}
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, it directly returns an error
277
- JSON containing the provided message and uses the status code as the `HTTP`
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 appropriately.
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, 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.7",
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.2",
33
- "miniflare": "^4.20250813.0"
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.2",
40
+ "@odatnurd/cf-aegis": "^0.1.4",
39
41
  "hono": "^4.7.0",
40
- "miniflare": "^4.20250813.0"
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
  },