@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 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
+ ```
@@ -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
+ }