@odatnurd/cf-requests 0.1.8 → 0.1.10

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 +63 -4
  2. package/lib/handlers.js +120 -39
  3. package/package.json +12 -5
package/README.md CHANGED
@@ -78,12 +78,13 @@ by the schema are present.
78
78
  ```js
79
79
  // Bring in the validation generator, the success response generator, and the
80
80
  // route handler generator.
81
- import { validate, success, routeHandler } from '@odatnurd/cf-requests';
81
+ import { validate, verify, success, routeHandler } from '@odatnurd/cf-requests';
82
82
 
83
83
  // Using the Joker rollup plugin, this will result in a testSchema object with a
84
84
  // `validate()` and `mask()` function within it, which verify that the result is
85
85
  // correct and mask away any fields not defined by the schema, respectively.
86
- import * as testSchema from '#schemas/test';
86
+ import * as inputSchema from '#schemas/test_input';
87
+ import * as outputSchema from '#schemas/test_output';
87
88
 
88
89
  // The hono-file-routes package defines routes in a file by exporting `$verb`
89
90
  // as routes. Here we are using the routeHandler() generator, which constructs
@@ -93,7 +94,10 @@ import * as testSchema from '#schemas/test';
93
94
  export const $post = routeHandler(
94
95
  // Generate a validator using the standard Hono mechanism; this will ensure
95
96
  // that the JSON in the body fits the schema, and will mask extraneous fields.
96
- validate('json', testSchema),
97
+ validate('json', inputSchema),
98
+
99
+ // Verify that the resulting object, on success, follows the provided schema,
100
+ verify(outputSchema),
97
101
 
98
102
  // Async functions that take a single argument are route handlers; they will
99
103
  // be automatically guarded with a try/catch block
@@ -210,7 +214,7 @@ Aegis to simplify testing database-related logic.
210
214
  ## Library Methods
211
215
 
212
216
  ```js
213
- export function success(ctx, message, result=[], status=200) {}
217
+ export async function success(ctx, message, result=[], status=200) {}
214
218
  ```
215
219
 
216
220
  Generate a successful return in JSON with the given `HTTP` status code; the
@@ -228,6 +232,14 @@ status code is used to construct the JSON as well as the response object:
228
232
  `result` is optional and defaults to an empty array if not provided. Similarly
229
233
  `status` is option and defaults to `200` if not provided.
230
234
 
235
+ If the `verify()` function was used to set a schema, then this function will
236
+ validate that the `result` you provide matches the schema, and will also
237
+ optionally mask it, if `verify()` was given a mask function.
238
+
239
+ When using `verify()`, if the data in `result` does not conform to the schema,
240
+ a `SchemaError` exception will be thrown. This is automatically handled by
241
+ `body()`, and will result in a `fail()` response instead of a `success()`.
242
+
231
243
  ---
232
244
 
233
245
  ```js
@@ -247,6 +259,10 @@ other meaningful result (which is the opposite of the `success` case).
247
259
  If `status` is not provided, it defaults to `400`, while if `result` is not
248
260
  provided, the `data` field will not be present in the result.
249
261
 
262
+ Note that unlike `success()`, `fail()` will not honor the addition of an output
263
+ validator via `verify()`, since it is usually expected that it will not provide
264
+ a meaningful data result.
265
+
250
266
  ---
251
267
 
252
268
  ```js
@@ -279,6 +295,32 @@ validation of the schema, the `fail()` method is invoked on it with a status of
279
295
 
280
296
  ---
281
297
 
298
+ ```js
299
+ export function verify({ validate, mask? }) {}
300
+ ```
301
+
302
+ This function registers the provided validation/masking pair for the current
303
+ route. This causes `success` to validate (and optionally mask) the data payload
304
+ that you give it before finalizing the request and sending the data out.
305
+
306
+ The parameter should be an object that contains a `validate` and an (optional)
307
+ `mask` member:
308
+
309
+ - `validate` takes the item to validate, and returns `true` when the data given
310
+ is valid. Any other return value is considered to be an error.
311
+ - `mask` takes as an input the same item passed to `validate`, and returns a
312
+ masked version of the data that strips fields from the object that do not
313
+ appear in the schema.
314
+
315
+ If `mask` is not provided, then the data will be validated but not masked.
316
+
317
+ This method is intended to be used with the
318
+ [@axel669/joker](https://www.npmjs.com/package/@axel669/joker) library (and
319
+ in particular it's rollup plugin), though you are free to use any other
320
+ validation schema so long as the call signatures are as defined above.
321
+
322
+ ---
323
+
282
324
  ```js
283
325
  export function body(handler) {}
284
326
  ```
@@ -301,6 +343,12 @@ For debugging, if your worker has the `CF_REQUESTS_STACKTRACE` environment
301
343
  variable set to either `true` or `yes`, the `fail()` response will include in
302
344
  its data field the stack trace as an array of strings that represent the trace.
303
345
 
346
+ > ℹ️ If the body catches a `SchemaError`, the `CF_REQUESTS_STACKTRACE` variable
347
+ > will be ignored since in this case it is the data and not the code that was at
348
+ > fail. In these cases, the result inside of the returned body will be the
349
+ > validation error object instead.
350
+
351
+
304
352
  ```js
305
353
  export const $post = [
306
354
  validate('json', testSchema),
@@ -336,6 +384,17 @@ error with the same layout as any other exception class.
336
384
 
337
385
  ---
338
386
 
387
+ ```js
388
+ export class SchemaError extends HttpError { constructor(message, status=500, result=undefined) {} }
389
+ ```
390
+
391
+ This is a simple extension to HttpError and is thrown in cases where a schema
392
+ validation error has occurred; it is handled by `body()` the same as `HttpError`
393
+ is. If the exception has a `result`, it will be used in the call to `fail()` in
394
+ this case, so that the result of the validation will be returned.
395
+
396
+ ---
397
+
339
398
  ```js
340
399
  export function routeHandler(...args) {}
341
400
  ```
package/lib/handlers.js CHANGED
@@ -7,14 +7,81 @@ import { validator } from 'hono/validator';
7
7
  /******************************************************************************/
8
8
 
9
9
 
10
- /* Generate a standardized success response from an API call.
10
+ /* A custom error base class that allows for route handlers to generate errors
11
+ * with a specific message and status code without having to have more explicit
12
+ * exception handling logic.
13
+ *
14
+ * When instances of this class are thrown by the code that is wrapped in a
15
+ * call to body(), the error response from that handler will follow a standard
16
+ * form and have a distinct HTTP error code.
17
+ *
18
+ * For simplicity, if the status code is not provided, 500 is assumed.
19
+ */
20
+ export class HttpError extends Error {
21
+ constructor(message, status=500) {
22
+ super(message);
23
+ this.status = status;
24
+ this.name = 'HttpError';
25
+ }
26
+ }
27
+
28
+
29
+ /******************************************************************************/
30
+
31
+
32
+ /* This custom error class works as HttpError does, but it is specificaly thrown
33
+ * to indicate that there was a schema validation error, either on input or on
34
+ * output.
35
+ *
36
+ * The result here is optional and if given is used as the result in the
37
+ * handler's fail() call; for schema errors this generally returns the object
38
+ * that the schema validator returns when it signals the error. */
39
+ export class SchemaError extends HttpError {
40
+ constructor(message, status=500, result=undefined) {
41
+ super(message, status);
42
+ this.name = 'SchemaError';
43
+ this.result = result;
44
+ }
45
+ }
46
+
47
+
48
+ /******************************************************************************/
49
+
50
+
51
+ /* Generate a standardized success response from an API call. If the provided
52
+ * context has a response guard attached to it, the result that is provided will
53
+ * be validated against it (and also masked, if a mask was provided) prior to
54
+ * being attached to the output and returned.
55
+ *
56
+ * If this results in an error, an exception is thrown to indicate this; the
57
+ * standard machinery will catch this and handle it as appropriate.
11
58
  *
12
- * This generates a JSON return value with the given HTTP status, with a
13
- * data section that contains the provided result, whatever it may be. */
14
- export const success = (ctx, message, result, status) => {
59
+ * On success (no validation, or validation passes), this generates a JSON
60
+ * return value with the given HTTP status, with a data section that contains
61
+ * the provided result, whatever it may be (and which could possibly have been
62
+ * masked). */
63
+ export const success = async (ctx, message, result, status) => {
15
64
  status ??= 200;
16
65
  result ??= [];
17
66
 
67
+ // See if there is a validator attached to this; if so, we have more work to
68
+ // do; if not, we can skip.
69
+ const validator = ctx.get('__cf_requests_response_validator');
70
+ if (validator !== undefined) {
71
+ // Try to validate; if this does not return true, then the data is not valid
72
+ // and we should throw an error.
73
+ const valid = validator.validate(result);
74
+ if (valid !== true) {
75
+ throw new SchemaError('response data failed schema validation', 500, valid);
76
+ }
77
+
78
+ // If there is a mask function, then use it to set up the value of the
79
+ // result.
80
+ if (typeof validator.mask === 'function') {
81
+ result = validator.mask(result);
82
+ }
83
+ }
84
+
18
85
  ctx.status(status);
19
86
  return ctx.json({ success: true, status, message, data: result });
20
87
  }
@@ -56,31 +123,32 @@ export const validate = (dataType, { validate, mask }) => validator(dataType, as
56
123
  return typeof mask === 'function' ? mask(value) : value;
57
124
  }
58
125
 
59
- return fail(ctx, `${dataType} data is not valid`, 422, result);
126
+ // Fail with 422 to signal unprocessible entity.
127
+ return fail(ctx, `request ${dataType} data failed schema validation`, 422, result);
60
128
  });
61
129
 
62
130
 
63
131
  /******************************************************************************/
64
132
 
65
133
 
66
- /* A custom error base class that allows for route handlers to generate errors
67
- * with a specific message and status code without having to have more explicit
68
- * exception handling logic.
134
+ /* Register a post-validator that will be used by any success() calls within the
135
+ * route handler to verify that the data being put into the returned result to
136
+ * the client conforms to the scheme presented.
69
137
  *
70
- * When instances of this class are thrown by the code that is wrapped in a
71
- * call to body(), the error response from that handler will follow a standard
72
- * form and have a distinct HTTP error code.
138
+ * The validator provided is the same as that for validate(); an object that
139
+ * has a 'validate' key to validate the data and an optional 'mask' key to mask
140
+ * the result.
73
141
  *
74
- * For simplicity, if the status code is not provided, 500 is assumed.
75
- */
76
- export class HttpError extends Error {
77
- constructor(message, status=500) {
78
- super(message);
79
- this.status = status;
80
- this.name = 'HttpError';
81
- }
142
+ * Internally, this just stores the guard into the context for later usage and
143
+ * continues on its merry way. */
144
+ export const verify = (validator) => async (ctx, next) => {
145
+ // Due to my excessive amount of paranoia, this is namespaced with the package
146
+ // name to slightly reduce the possibility of a name conflict.
147
+ ctx.set('__cf_requests_response_validator', validator);
148
+ await next();
82
149
  }
83
150
 
151
+
84
152
  /******************************************************************************/
85
153
 
86
154
  /* Create a request handler that will execute the provided handler function and
@@ -92,29 +160,40 @@ export function body(handler) {
92
160
  return await handler(ctx);
93
161
  }
94
162
  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
- });
163
+ // By default, the result has no data attached.
164
+ let errorData = undefined;
165
+
166
+ // If the exception is a schema validation error, then the exception will
167
+ // carry what we want to use for the result, since that tells us how the
168
+ // validation failed.
169
+ if (err instanceof SchemaError) {
170
+ errorData = err.result;
171
+ } else {
172
+ // This is not a schema validation; check to see if we should be adding
173
+ // a stack trace as the data instead.
174
+ const generateTrace = ['true', 'yes'].includes(ctx.env.CF_REQUESTS_STACKTRACE);
175
+
176
+ // If we should generate the stack trace and the error that we got
177
+ // actually has a trace in it, then generate a trace array by converting
178
+ // the stack into an array of locations for better readability.
179
+ //
180
+ // This elides the start line, since we capture the message already
181
+ // below, and removes the `at` prefix since that is redundant.
182
+ if (generateTrace === true && typeof err.stack === 'string') {
183
+ errorData = err.stack.split('\n')
184
+ .slice(1)
185
+ .map(line => {
186
+ const trimmedLine = line.trim();
187
+ return trimmedLine.startsWith('at ') ? trimmedLine.substring(3) : trimmedLine;
188
+ });
189
+ }
111
190
  }
112
191
 
113
- if (err instanceof HttpError) {
114
- return fail(ctx, err.message, err.status, trace);
115
- }
192
+ // If the error was an HttpError or a SchemaError, then use its status as
193
+ // the status; otherwise default to 500.
194
+ const status = (err instanceof HttpError) ? err.status : 500;
116
195
 
117
- return fail(ctx, err.message, 500, trace);
196
+ return fail(ctx, err.message, status, errorData);
118
197
  }
119
198
  }
120
199
  }
@@ -122,6 +201,7 @@ export function body(handler) {
122
201
 
123
202
  /******************************************************************************/
124
203
 
204
+
125
205
  /* This is a utility wrapper function that simplifies the creation of a Hono
126
206
  * route handler; it accepts any number of arguments and returns back a prepared
127
207
  * array of items for use as a route handler.
@@ -143,4 +223,5 @@ export function body(handler) {
143
223
  });
144
224
  }
145
225
 
226
+
146
227
  /******************************************************************************/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@odatnurd/cf-requests",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
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
  },