@odatnurd/cf-requests 0.1.9 → 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.
- package/README.md +63 -4
- package/lib/handlers.js +120 -39
- 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
|
|
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',
|
|
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
|
-
/*
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
/*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
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
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
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
|
-
*
|
|
75
|
-
*/
|
|
76
|
-
export
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
96
|
-
let
|
|
97
|
-
|
|
98
|
-
// If
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
trace
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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,
|
|
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