@odatnurd/cf-requests 0.1.0
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/LICENSE +21 -0
- package/README.md +241 -0
- package/lib/handlers.js +128 -0
- package/package.json +32 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Terence Martin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# Simple CloudFlare Request Handlers
|
|
2
|
+
|
|
3
|
+
`cf-requests` is a very simple set of wrapper functions that allow for working
|
|
4
|
+
with requests in a
|
|
5
|
+
[Cloudflare Worker](https://developers.cloudflare.com/workers/) or in
|
|
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.
|
|
9
|
+
|
|
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
|
|
13
|
+
[@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.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
Install `cf-requests` via `npm`, `pnpm`, and so on, in the usual way.
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npm install @odatnurd/cf-requests
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
The library provides all of the pieces needed to validate incoming requests
|
|
31
|
+
based on an appropriate schema and return a JSON result that has a consistent
|
|
32
|
+
field layout. In addition, request handlers are wrapped such that any
|
|
33
|
+
exceptions that propagate out of the handler function are handled as errors
|
|
34
|
+
with an appropriate return, making the actual handler code more straight
|
|
35
|
+
forward.
|
|
36
|
+
|
|
37
|
+
### Example
|
|
38
|
+
|
|
39
|
+
Assuming a file named `test.joker.json` that contains the following Joker
|
|
40
|
+
schema:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"itemName": "body",
|
|
45
|
+
"root": {
|
|
46
|
+
"key1": "number",
|
|
47
|
+
"key2": "string",
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The following is a minimal route handler that validates the JSON body against
|
|
53
|
+
the schema and returning it back. The request will result in a `422` error if
|
|
54
|
+
the data is not valid, and the JSON body is masked to ensure that only the
|
|
55
|
+
fields declared by the schema are present.
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
/******************************************************************************/
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
import { validate, success, routeHandler } from '#lib/common';
|
|
63
|
+
|
|
64
|
+
import * as testSchema from '#schemas/test';
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
export const $post = routeHandler(
|
|
68
|
+
validate('json', testSchema),
|
|
69
|
+
|
|
70
|
+
async (ctx) => {
|
|
71
|
+
const body = ctx.req.valid('json');
|
|
72
|
+
|
|
73
|
+
// Generic errors show up as a 500 server error; throwing HTTPError allows
|
|
74
|
+
// for a specific error code to be returned instead
|
|
75
|
+
if (body.key1 != 69) {
|
|
76
|
+
throw new Error('key is not nice');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return success(ctx, 'request body validated', body);
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Methods
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
export function success(ctx, message, result, status) {}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Indicate a successful return in JSON with the given `HTTP` status code:
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
{
|
|
95
|
+
"success": true,
|
|
96
|
+
message,
|
|
97
|
+
data: result
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`result` is optional and defaults to an empty array if not provided. Similarly
|
|
102
|
+
`status` is option and defaults to `200` if not provided.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
export function fail(ctx, message, status, result) {}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Indicates a failure return in JSON with the given `HTTP` status code.
|
|
111
|
+
|
|
112
|
+
This follows a similar form to `success`, though the `success` field is `false`
|
|
113
|
+
instead of `true`.
|
|
114
|
+
|
|
115
|
+
Note that the order of the last two arguments is different because generally
|
|
116
|
+
one wants to specify the status of an error but it usually does not return any
|
|
117
|
+
other meaningful result (which is the opposite of the `success` case).
|
|
118
|
+
|
|
119
|
+
If `status` is not provided, it defaults to `400`, while if `result` is not
|
|
120
|
+
provided, the `data` field will not be present in the result.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
export function validate(dataType, schemaObj) {}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
This function uses the [Hono validator()](https://hono.dev/docs/guides/validation)
|
|
129
|
+
function to create a validator that will validate the data of the provided type
|
|
130
|
+
using the provided `Joker` schema.
|
|
131
|
+
|
|
132
|
+
`schemaObj` is an object that contains a `validate` and `mask` member that can
|
|
133
|
+
validate data and store it into the `Hono` context, masking away all fields that
|
|
134
|
+
are not present in the schema; this is intended to be used with
|
|
135
|
+
[@axel669/joker](https://www.npmjs.com/package/@axel669/joker), though you are
|
|
136
|
+
free to use any other validation schema so long as the call signatures match
|
|
137
|
+
that of `joker`.
|
|
138
|
+
|
|
139
|
+
On success, the data is placed in the context. If the data does not pass the
|
|
140
|
+
validation of the schema, the `fail()` method is invoked on it with a status of
|
|
141
|
+
`422` to signify the issue directly.
|
|
142
|
+
|
|
143
|
+
Execute a fetch operation on the provided database, using the data in `sqlargs`
|
|
144
|
+
to create the statement(s) to be executed, and return the result(s) of the
|
|
145
|
+
query after logging statistics such as the rows read and written, which will be
|
|
146
|
+
annotated with the action string provided to give context to the operation.
|
|
147
|
+
|
|
148
|
+
The provided `sqlargs` is a variable length list of arguments that consists of
|
|
149
|
+
strings to be compiled to SQL, previously compiled statements, and/or arrays of
|
|
150
|
+
values to bind to statements.
|
|
151
|
+
|
|
152
|
+
For the purposes of binding, arrays will bind to the most recently seen
|
|
153
|
+
statement, allowing you to compile one statement and bind it multiple times if
|
|
154
|
+
desired.
|
|
155
|
+
|
|
156
|
+
When more than one statement is provided, all statements will be executed as a
|
|
157
|
+
batch operation, which implicitly runs as a transaction.
|
|
158
|
+
|
|
159
|
+
The return value is the direct result of executing the query or queries given
|
|
160
|
+
in `sqlargs`; this is either a (potentially empty) array of result rows, or an
|
|
161
|
+
array of such arrays (if a batch). As with `dbRawQuery`, the `meta` is stripped
|
|
162
|
+
from the results, providing you just the actual query result.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
export function body(handler) {}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
This is a simple wrapper which returns a function that wraps the provided
|
|
171
|
+
handler function in a `try-catch` block, so that any uncaught exceptions can
|
|
172
|
+
gracefully return a `fail()` result.
|
|
173
|
+
|
|
174
|
+
The wrapper returned by this function will itself return either the result of
|
|
175
|
+
the provided handler, so long as no exceptions are raised.
|
|
176
|
+
|
|
177
|
+
Exceptions of type `HttpError` carry a specific `HTTP` status code, which will
|
|
178
|
+
be used in the call to `fail()`; all other exceptions use a status of `500`.
|
|
179
|
+
|
|
180
|
+
```js
|
|
181
|
+
export const $post = [
|
|
182
|
+
validate('json', testSchema),
|
|
183
|
+
|
|
184
|
+
body(async (ctx) => {
|
|
185
|
+
const body = ctx.req.valid('json');
|
|
186
|
+
|
|
187
|
+
return success(ctx, 'code test worked', body);
|
|
188
|
+
}),
|
|
189
|
+
];
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
```js
|
|
195
|
+
export class HttpError extends Error(message: string, status) {}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
This is a simple exception class that wraps a textual message and a status code.
|
|
199
|
+
|
|
200
|
+
When `body()` catches an exception of this type, it directly returns an error
|
|
201
|
+
JSON containing the provided message and uses the status code as the `HTTP`
|
|
202
|
+
status of the return.
|
|
203
|
+
|
|
204
|
+
If `status` is not provided, it defaults to `500`.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
```js
|
|
209
|
+
export function routeHandler(...args) {}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
This small helper makes it slightly cleaner to set up a route handler by taking
|
|
213
|
+
any number of arguments and returning them back as an array.
|
|
214
|
+
|
|
215
|
+
As a part of this operation, any `async` function that takes exactly one argument
|
|
216
|
+
is implicitly wrapped in `body()`. This makes the resulting handler somewhat
|
|
217
|
+
cleaner looking, while still allowing for arbitrary middleware
|
|
218
|
+
|
|
219
|
+
```js
|
|
220
|
+
export const $post = routeHandler(
|
|
221
|
+
validate('json', testSchema),
|
|
222
|
+
|
|
223
|
+
// More than one argument, so function is directly returned; no body() call.
|
|
224
|
+
async (ctx, next) => {
|
|
225
|
+
console.log('Async middleware is running!');
|
|
226
|
+
await next();
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
// Single argument async functions are wrapped in body(), so exceptions raised
|
|
230
|
+
// are handled appropriately.
|
|
231
|
+
async (ctx) => {
|
|
232
|
+
const body = ctx.req.valid('json');
|
|
233
|
+
|
|
234
|
+
if (body.key1 != 69) {
|
|
235
|
+
throw new Error('key is not nice');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return success(ctx, 'code test worked', body);
|
|
239
|
+
},
|
|
240
|
+
);
|
|
241
|
+
```
|
package/lib/handlers.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/******************************************************************************/
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import { validator } from 'hono/validator';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/******************************************************************************/
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
/* Generate a standardized success response from an API call.
|
|
11
|
+
*
|
|
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) => {
|
|
15
|
+
status ??= 200;
|
|
16
|
+
result ??= [];
|
|
17
|
+
|
|
18
|
+
ctx.status(status);
|
|
19
|
+
return ctx.json({ success: true, message, data: result });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
/******************************************************************************/
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
/* Generate a standardized error response from an API call.
|
|
27
|
+
*
|
|
28
|
+
* This generates a JSON return value with the given HTTP status, with an
|
|
29
|
+
* error reason that is the reason specified. */
|
|
30
|
+
export const fail = (ctx, message, status, result) => {
|
|
31
|
+
status ??= 400;
|
|
32
|
+
|
|
33
|
+
ctx.status(status);
|
|
34
|
+
return ctx.json({ success: false, message, data: result });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
/******************************************************************************/
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
/* Create a validator that will validate the type of request data provided
|
|
42
|
+
* against a specifically defined Joker schema object. The data is both
|
|
43
|
+
* validated against the schema as well as filtered so that non-schema
|
|
44
|
+
* properties of the data are discarded.
|
|
45
|
+
*
|
|
46
|
+
* This provides a middleware filter for use in Hono; it is expected to either
|
|
47
|
+
* trigger a failure, or return the data that is the validated and cleaned
|
|
48
|
+
* object from the request.
|
|
49
|
+
*
|
|
50
|
+
* When using this filter, underlying requests can fetch the validated data
|
|
51
|
+
* via the ctx.req.valid() function, e.g. ctx.req.valid('json'). */
|
|
52
|
+
export const validate = (dataType, schemaObj) => validator(dataType, async (value, ctx) => {
|
|
53
|
+
// Joker returns true for valid data and an array of error objects on failure.
|
|
54
|
+
const result = await schemaObj.validate(value);
|
|
55
|
+
if (result === true) {
|
|
56
|
+
return schemaObj.mask(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return fail(ctx, `${dataType} data is not valid`, 422, result);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
/******************************************************************************/
|
|
64
|
+
|
|
65
|
+
|
|
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.
|
|
69
|
+
*
|
|
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.
|
|
73
|
+
*
|
|
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
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/******************************************************************************/
|
|
85
|
+
|
|
86
|
+
/* Create a request handler that will execute the provided handler function and
|
|
87
|
+
* catch any exceptions that it may raise, returning an appropriate error
|
|
88
|
+
* response back to the caller. */
|
|
89
|
+
export function body(handler) {
|
|
90
|
+
return async (ctx) => {
|
|
91
|
+
try {
|
|
92
|
+
return await handler(ctx);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
if (err instanceof HttpError) {
|
|
96
|
+
return fail(ctx, err.message, err.status);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return fail(ctx, err.message, 500);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
/******************************************************************************/
|
|
106
|
+
|
|
107
|
+
/* This is a utility wrapper function that simplifies the creation of a Hono
|
|
108
|
+
* route handler; it accepts any number of arguments and returns back a prepared
|
|
109
|
+
* array of items for use as a route handler.
|
|
110
|
+
*
|
|
111
|
+
* Any argument that is an async function with exactly one argument is wrapped
|
|
112
|
+
* in a call to body() directly, while all other values (including async
|
|
113
|
+
* functions that take more than one argument) are put into the array as-is.
|
|
114
|
+
*
|
|
115
|
+
* This allows for not only validations but also arbitrary middleware as well
|
|
116
|
+
* to be used. */
|
|
117
|
+
export function routeHandler(...args) {
|
|
118
|
+
return args.map(arg => {
|
|
119
|
+
// Any async functions that take exactly one argument are passed through the
|
|
120
|
+
// body wrapper to wrap them; everything else passes through as-is.
|
|
121
|
+
if (typeof arg === 'function' && arg.constructor.name === 'AsyncFunction' && arg.length === 1) {
|
|
122
|
+
return body(arg);
|
|
123
|
+
}
|
|
124
|
+
return arg;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/******************************************************************************/
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@odatnurd/cf-requests",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Simple Cloudflare Hono request wrapper",
|
|
5
|
+
"author": "OdatNurd (https://odatnurd.net)",
|
|
6
|
+
"homepage": "https://github.com/OdatNurd/cf-requests",
|
|
7
|
+
"bugs": "https://github.com/OdatNurd/cf-requests/issues",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/OdatNurd/cf-requests"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/handlers.js",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./lib/handlers.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"lib/handlers.js"
|
|
20
|
+
],
|
|
21
|
+
"keywords": [
|
|
22
|
+
"cloudflare",
|
|
23
|
+
"hono",
|
|
24
|
+
"routing"
|
|
25
|
+
],
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"hono": "^4.7.0"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "echo \"No tests specified\" && exit 0"
|
|
31
|
+
}
|
|
32
|
+
}
|