@odatnurd/cf-requests 0.1.9 → 0.1.11

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 +68 -4
  2. package/lib/handlers.js +124 -40
  3. package/package.json +1 -1
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,15 @@ 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 **entire returned body, including `result`** you provide
237
+ matches the schema, and will also optionally mask it, if `verify()` was given
238
+ a mask function.
239
+
240
+ When using `verify()`, if the result body does not conform to the schema, a
241
+ `SchemaError` exception will be thrown. This is automatically handled by
242
+ `body()`, and will result in a `fail()` response instead of a `success()`.
243
+
231
244
  ---
232
245
 
233
246
  ```js
@@ -247,6 +260,10 @@ other meaningful result (which is the opposite of the `success` case).
247
260
  If `status` is not provided, it defaults to `400`, while if `result` is not
248
261
  provided, the `data` field will not be present in the result.
249
262
 
263
+ Note that unlike `success()`, `fail()` will not honor the addition of an output
264
+ validator via `verify()`, since it is usually expected that it will not provide
265
+ a meaningful data result.
266
+
250
267
  ---
251
268
 
252
269
  ```js
@@ -279,6 +296,36 @@ validation of the schema, the `fail()` method is invoked on it with a status of
279
296
 
280
297
  ---
281
298
 
299
+ ```js
300
+ export function verify({ validate, mask? }) {}
301
+ ```
302
+
303
+ This function registers the provided validation/masking pair for the current
304
+ route. This causes `success` to validate (and optionally mask) the resulting
305
+ message body before finalizing the request and sending the data out.
306
+
307
+ > ℹ️ The schema is run over the **entire returned body, not just the`result`**,
308
+ > so in practice the schema needs to match what the `success()` function uses
309
+ > as the final result.
310
+
311
+ The parameter should be an object that contains a `validate` and an (optional)
312
+ `mask` member:
313
+
314
+ - `validate` takes the item to validate, and returns `true` when the data given
315
+ is valid. Any other return value is considered to be an error.
316
+ - `mask` takes as an input the same item passed to `validate`, and returns a
317
+ masked version of the data that strips fields from the object that do not
318
+ appear in the schema.
319
+
320
+ If `mask` is not provided, then the data will be validated but not masked.
321
+
322
+ This method is intended to be used with the
323
+ [@axel669/joker](https://www.npmjs.com/package/@axel669/joker) library (and
324
+ in particular it's rollup plugin), though you are free to use any other
325
+ validation schema so long as the call signatures are as defined above.
326
+
327
+ ---
328
+
282
329
  ```js
283
330
  export function body(handler) {}
284
331
  ```
@@ -301,6 +348,12 @@ For debugging, if your worker has the `CF_REQUESTS_STACKTRACE` environment
301
348
  variable set to either `true` or `yes`, the `fail()` response will include in
302
349
  its data field the stack trace as an array of strings that represent the trace.
303
350
 
351
+ > ℹ️ If the body catches a `SchemaError`, the `CF_REQUESTS_STACKTRACE` variable
352
+ > will be ignored since in this case it is the data and not the code that was at
353
+ > fail. In these cases, the result inside of the returned body will be the
354
+ > validation error object instead.
355
+
356
+
304
357
  ```js
305
358
  export const $post = [
306
359
  validate('json', testSchema),
@@ -336,6 +389,17 @@ error with the same layout as any other exception class.
336
389
 
337
390
  ---
338
391
 
392
+ ```js
393
+ export class SchemaError extends HttpError { constructor(message, status=500, result=undefined) {} }
394
+ ```
395
+
396
+ This is a simple extension to HttpError and is thrown in cases where a schema
397
+ validation error has occurred; it is handled by `body()` the same as `HttpError`
398
+ is. If the exception has a `result`, it will be used in the call to `fail()` in
399
+ this case, so that the result of the validation will be returned.
400
+
401
+ ---
402
+
339
403
  ```js
340
404
  export function routeHandler(...args) {}
341
405
  ```
package/lib/handlers.js CHANGED
@@ -7,16 +7,86 @@ 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
+ // Construct the body that we will be returning back.
68
+ let body = { success: true, status, message, data: result };
69
+
70
+ // See if there is a validator attached to this; if so, we have more work to
71
+ // do; if not, we can skip.
72
+ const validator = ctx.get('__cf_requests_response_validator');
73
+ if (validator !== undefined) {
74
+ // Try to validate; if this does not return true, then the data is not valid
75
+ // and we should throw an error.
76
+ const valid = validator.validate(body);
77
+ if (valid !== true) {
78
+ throw new SchemaError('response data failed schema validation', 500, valid);
79
+ }
80
+
81
+ // If there is a mask function, then use it to set up the value of the
82
+ // result.
83
+ if (typeof validator.mask === 'function') {
84
+ body = validator.mask(body);
85
+ }
86
+ }
87
+
18
88
  ctx.status(status);
19
- return ctx.json({ success: true, status, message, data: result });
89
+ return ctx.json(body);
20
90
  }
21
91
 
22
92
 
@@ -56,31 +126,32 @@ export const validate = (dataType, { validate, mask }) => validator(dataType, as
56
126
  return typeof mask === 'function' ? mask(value) : value;
57
127
  }
58
128
 
59
- return fail(ctx, `${dataType} data is not valid`, 422, result);
129
+ // Fail with 422 to signal unprocessible entity.
130
+ return fail(ctx, `request ${dataType} data failed schema validation`, 422, result);
60
131
  });
61
132
 
62
133
 
63
134
  /******************************************************************************/
64
135
 
65
136
 
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.
137
+ /* Register a post-validator that will be used by any success() calls within the
138
+ * route handler to verify that the data being put into the returned result to
139
+ * the client conforms to the scheme presented.
69
140
  *
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.
141
+ * The validator provided is the same as that for validate(); an object that
142
+ * has a 'validate' key to validate the data and an optional 'mask' key to mask
143
+ * the result.
73
144
  *
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
- }
145
+ * Internally, this just stores the guard into the context for later usage and
146
+ * continues on its merry way. */
147
+ export const verify = (validator) => async (ctx, next) => {
148
+ // Due to my excessive amount of paranoia, this is namespaced with the package
149
+ // name to slightly reduce the possibility of a name conflict.
150
+ ctx.set('__cf_requests_response_validator', validator);
151
+ await next();
82
152
  }
83
153
 
154
+
84
155
  /******************************************************************************/
85
156
 
86
157
  /* Create a request handler that will execute the provided handler function and
@@ -92,29 +163,40 @@ export function body(handler) {
92
163
  return await handler(ctx);
93
164
  }
94
165
  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
- });
166
+ // By default, the result has no data attached.
167
+ let errorData = undefined;
168
+
169
+ // If the exception is a schema validation error, then the exception will
170
+ // carry what we want to use for the result, since that tells us how the
171
+ // validation failed.
172
+ if (err instanceof SchemaError) {
173
+ errorData = err.result;
174
+ } else {
175
+ // This is not a schema validation; check to see if we should be adding
176
+ // a stack trace as the data instead.
177
+ const generateTrace = ['true', 'yes'].includes(ctx.env.CF_REQUESTS_STACKTRACE);
178
+
179
+ // If we should generate the stack trace and the error that we got
180
+ // actually has a trace in it, then generate a trace array by converting
181
+ // the stack into an array of locations for better readability.
182
+ //
183
+ // This elides the start line, since we capture the message already
184
+ // below, and removes the `at` prefix since that is redundant.
185
+ if (generateTrace === true && typeof err.stack === 'string') {
186
+ errorData = err.stack.split('\n')
187
+ .slice(1)
188
+ .map(line => {
189
+ const trimmedLine = line.trim();
190
+ return trimmedLine.startsWith('at ') ? trimmedLine.substring(3) : trimmedLine;
191
+ });
192
+ }
111
193
  }
112
194
 
113
- if (err instanceof HttpError) {
114
- return fail(ctx, err.message, err.status, trace);
115
- }
195
+ // If the error was an HttpError or a SchemaError, then use its status as
196
+ // the status; otherwise default to 500.
197
+ const status = (err instanceof HttpError) ? err.status : 500;
116
198
 
117
- return fail(ctx, err.message, 500, trace);
199
+ return fail(ctx, err.message, status, errorData);
118
200
  }
119
201
  }
120
202
  }
@@ -122,6 +204,7 @@ export function body(handler) {
122
204
 
123
205
  /******************************************************************************/
124
206
 
207
+
125
208
  /* This is a utility wrapper function that simplifies the creation of a Hono
126
209
  * route handler; it accepts any number of arguments and returns back a prepared
127
210
  * array of items for use as a route handler.
@@ -143,4 +226,5 @@ export function body(handler) {
143
226
  });
144
227
  }
145
228
 
229
+
146
230
  /******************************************************************************/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@odatnurd/cf-requests",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Simple Cloudflare Hono request wrapper",
5
5
  "author": "OdatNurd (https://odatnurd.net)",
6
6
  "homepage": "https://github.com/OdatNurd/cf-requests",