@odatnurd/cf-requests 0.1.5 → 0.1.7

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 +7 -8
  2. package/aegis/index.js +113 -45
  3. package/package.json +27 -3
package/README.md CHANGED
@@ -16,6 +16,9 @@ and allowing file based in Cloudflare Workers, where that is not directly
16
16
  possible. This again is not strictly required, but examples below assume that
17
17
  this is the case.
18
18
 
19
+ > ℹ️ Technically, the validation wrapper will accept any validator (e.g. zod) as
20
+ > long as the object passed in conforms to the validation contract; see below.
21
+
19
22
 
20
23
  ## Installation
21
24
 
@@ -37,7 +40,7 @@ forward.
37
40
  ### Example
38
41
 
39
42
  Assuming a file named `test.joker.json` that contains the following Joker
40
- schema:
43
+ schema, and and you are using the Joker Rollup plugin:
41
44
 
42
45
  ```json
43
46
  {
@@ -58,6 +61,7 @@ fields declared by the schema are present.
58
61
  ```js
59
62
  import { validate, success, routeHandler } from '#lib/common';
60
63
 
64
+ // Use the Joker rollup plugin to obtain the object we require
61
65
  import * as testSchema from '#schemas/test';
62
66
 
63
67
 
@@ -89,15 +93,10 @@ To use these utilities, you must install the required peer dependencies into
89
93
  your own project's `devDependencies` if you have not already done so.
90
94
 
91
95
  ```sh
92
- pnpm add -D @odatnurd/d1-query @axel669/aegis miniflare fs-jetpack
96
+ pnpm add -D @axel669/aegis @axel669/joker @odatnurd/cf-aegis miniflare
93
97
  ```
94
98
 
95
- > ℹ️ If you are actively using
96
- > [@odatnurd/d1-query](https://www.npmjs.com/package/@odatnurd/d1-query) in your
97
- > project, that library should be installed as a regular `dependency` and not a
98
- > `devDependency`
99
-
100
- The `@odatnurd/cf-requests` module exports the following functions:
99
+ The `@odatnurd/cf-requests/aegis` module exports the following functions:
101
100
 
102
101
 
103
102
  ### Helper Functions
package/aegis/index.js CHANGED
@@ -8,6 +8,45 @@ import { validate } from '../lib/handlers.js';
8
8
  /******************************************************************************/
9
9
 
10
10
 
11
+ /* A mapping of all of the common status errors that might be returned by the
12
+ * validator. */
13
+ const STATUS_TEXT = {
14
+ 400: 'Bad Request',
15
+ 401: 'Unauthorized',
16
+ 402: 'Payment Required',
17
+ 403: 'Forbidden',
18
+ 404: 'Not Found',
19
+ 405: 'Method Not Allowed',
20
+ 406: 'Not Acceptable',
21
+ 407: 'Proxy Authentication Required',
22
+ 408: 'Request Timeout',
23
+ 409: 'Conflict',
24
+ 410: 'Gone',
25
+ 411: 'Length Required',
26
+ 412: 'Precondition Failed',
27
+ 413: 'Payload Too Large',
28
+ 414: 'URI Too Long',
29
+ 415: 'Unsupported Media Type',
30
+ 416: 'Range Not Satisfiable',
31
+ 417: 'Expectation Failed',
32
+ 418: "I'm a teapot",
33
+ 421: 'Misdirected Request',
34
+ 422: 'Unprocessable Entity',
35
+ 423: 'Locked',
36
+ 424: 'Failed Dependency',
37
+ 425: 'Too Early',
38
+ 426: 'Upgrade Required',
39
+ 428: 'Precondition Required',
40
+ 429: 'Too Many Requests',
41
+ 431: 'Request Header Fields Too Large',
42
+ 451: 'Unavailable For Legal Reasons',
43
+ 500: 'Internal Server Error',
44
+ };
45
+
46
+
47
+ /******************************************************************************/
48
+
49
+
11
50
  /*
12
51
  * Initializes some custom Aegis checks that make testing of schema and data
13
52
  * requests easier.
@@ -46,87 +85,116 @@ export function initializeRequestChecks() {
46
85
  * within it. */
47
86
  export async function schemaTest(dataType, schema, data, validator) {
48
87
  // If a validator is provided, use it; otherwise use ours. This requires that
49
- // you provide a call-compatible validator. This is here only to support some
50
- // migrations of old code that is using a different validator than the one
51
- // this library currently uses.
88
+ // you provide a call-compatible validator. This is here primarily to support
89
+ // some migrations of old code that is using a different validator than the
90
+ // one this library currently uses.
52
91
  validator = validator ??= validate;
53
92
 
54
93
  // Use the Hono factory to create our middleware, just as a caller would.
55
- // Create a middleware using the Hono factory method for this, using the
56
- // schema object and data type provided.
57
94
  const middleware = validator(dataType, schema);
58
95
 
59
- // As a result of the middleware, we will either capture the validated (and
60
- // masked) input JSON data, or we will capture an error response. As a part of
61
- // this we also capture what the eventual status of the call would be if this
62
- // generates a response, so that we can put it into the response object.
96
+ // A successful test captures the validated and masked JSON output, while a
97
+ // failed test generates a failure JSON response and has a specific status
98
+ // as a result of the validator's call to fail().
63
99
  let validData = null;
64
100
  let errorResponse = null;
65
101
  let responseStatus = 200;
66
102
 
67
- // A fake next to pass to the middleware when we execute it, so that it does
68
- // not throw an error.
69
- const next = () => {};
70
-
71
- // In order to run the test we need to create a fake Hono context object to
72
- // pass to the middleware; this mimics the smallest possible footprint of
73
- // Hono context for our purposes.
103
+ // In order to handle formdata, cookie, and header validation we need a
104
+ // request object to put into the context. These portions are parsed out of
105
+ // the response by the validator and thus can't be backfilled. This also
106
+ // ensures that for formData we get a proper form encoded body.
107
+ const options = { method: 'POST' };
108
+ if (dataType === 'form') {
109
+ // For form data, turn the passed in object into FormData and add it to
110
+ // the body.
111
+ options.body = new FormData();
112
+ Object.entries(data).forEach(([k, v]) => options.body.append(k, v));
113
+
114
+ } else if (dataType === 'cookie') {
115
+ // If we are testing cookies, we need a cookie header
116
+ options.headers = { 'Cookie': Object.entries(data).map(([k,v]) => `${k}=${v}`).join('; ') }
117
+
118
+ } else if (dataType === 'header') {
119
+ // If we are testing a header, we need actual headers.
120
+ options.headers = data;
121
+ }
122
+
123
+ // Create the response now.
124
+ const rawRequest = new Request('http://localhost/', options)
125
+
126
+ // Construct a mock Hono context object to pass to the middleware. We have
127
+ // here a mix of functions that the validator will call to get data that Hono
128
+ // has already processed or should process, such as the JSON body or the
129
+ // mapped request URI paramters, as well as a raw Request object for things
130
+ // that Hono does not tend to parse, such as form data and headers.
74
131
  const ctx = {
75
132
  req: {
76
- // These methods are used by the validator to pull the parsed data out of
77
- // the request in order to validate it, except for when the data type is
78
- // header, in which case it invokes the header() function with no name.
133
+ // The raw request; used by form data, headers, and cookies.
134
+ raw: rawRequest,
135
+
136
+ // These methods in the context convey information that Hono parses as a
137
+ // part of its request handling; as such we can return the data back
138
+ // directly.
79
139
  param: () => data,
80
140
  json: async () => data,
81
- query: () => data,
82
- cookie: () => data,
83
- formData: async () => data,
84
-
85
- // We need to populate an actual cookie header in headers for it the
86
- // validator to be able to pull cookie data because it wants to parse it
87
- // itself.
88
- headers: new Headers(dataType === 'cookie'
89
- ? { 'Cookie': Object.entries(data).map(([k,v]) => `${k}=${v}`).join('; ') }
90
- : {}),
91
-
92
- // The validator invokes this to get headers out of the request when the
93
- // data type is JSON.
94
- header: (name) => {
95
- // If there is no name, return the data back directly; this call pattern
96
- // happens when the data type is header.
141
+
142
+ // Query paramters must always return the value of a key as an array
143
+ // since they can appear more than once; also, if you provide no key, you
144
+ // get them all. We're precomputing here for no good reason.
145
+ queries: (() => {
146
+ const result = Object.entries(data).reduce((acc, [key, value]) => {
147
+ acc[key] = Array.isArray(value) ? value : [value];
148
+ return acc;
149
+ }, {});
150
+
151
+ return key => key ? result[key] : result;
152
+ })(),
153
+
154
+ // For form data, the validator expects to be able to get at the raw body
155
+ // and a place to cache the parsed body data.
156
+ arrayBuffer: async () => rawRequest.arrayBuffer(),
157
+ bodyCache: {},
158
+
159
+ // The context supports gathering either a single header by name, or all
160
+ // headers (by passing undefined as a name.
161
+ header: name => {
97
162
  if (name === undefined) {
98
163
  return data;
99
164
  }
100
165
 
101
166
  return name.toLowerCase() !== 'content-type' ? undefined : {
102
167
  json: 'application/json',
103
- form: 'multipart/form-data',
168
+ form: rawRequest.headers.get('Content-Type'),
104
169
  }[dataType];
105
170
  },
106
171
 
107
- // When validation succeeds, it invokes this to store the data back into
108
- // the context.
172
+ // The validator invokes this to store the validated data back to the
173
+ // context; here we just capture it as the validated data for later
174
+ // return.
109
175
  addValidatedData: (target, data) => validData = data
110
176
  },
111
177
 
112
- // Used to capture a failure; the validator will invoke status to set the
113
- // required HTTP response and then invoke the json() method to populate the
114
- // error.
115
- status: (inStatus) => { responseStatus = inStatus; },
116
- json: (payload) => {
178
+ // If a failure occurs, the validator should call fail(), which invokes
179
+ // thee two endpoints to place an error status and JSON payload into the
180
+ // response. Here we just create an actual response object, since that is
181
+ // what the middleware would return.
182
+ status: status => responseStatus = status,
183
+ json: payload => {
117
184
  errorResponse = new Response(
118
185
  JSON.stringify(payload), {
119
186
  status: responseStatus,
120
- statusText: "Bad Request",
187
+ statusText: STATUS_TEXT[responseStatus] ?? 'Unknown Error',
121
188
  headers: { "Content-Type": "application/json" }
122
189
  }
123
190
  );
124
191
  },
125
192
  };
126
193
 
194
+ // Execute the middleware with an empty next().
127
195
  // Run the middleware; we either capture a result in the error payload or the
128
196
  // validation result.
129
- await middleware(ctx, next);
197
+ await middleware(ctx, () => {});
130
198
 
131
199
  // Return the error payload if validation failed, otherwise return the
132
200
  // validated data from the success path.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@odatnurd/cf-requests",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Simple Cloudflare Hono request wrapper",
5
5
  "author": "OdatNurd (https://odatnurd.net)",
6
6
  "homepage": "https://github.com/OdatNurd/cf-requests",
@@ -26,10 +26,34 @@
26
26
  "routing",
27
27
  "aegis"
28
28
  ],
29
+ "devDependencies": {
30
+ "@axel669/aegis": "^0.3.1",
31
+ "@axel669/joker": "^0.3.5",
32
+ "@odatnurd/cf-aegis": "^0.1.2",
33
+ "miniflare": "^4.20250813.0"
34
+ },
29
35
  "peerDependencies": {
30
- "hono": "^4.7.0"
36
+ "@axel669/aegis": "^0.3.1",
37
+ "@axel669/joker": "^0.3.5",
38
+ "@odatnurd/cf-aegis": "^0.1.2",
39
+ "hono": "^4.7.0",
40
+ "miniflare": "^4.20250813.0"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "@axel669/aegis": {
44
+ "optional": true
45
+ },
46
+ "@axel669/joker": {
47
+ "optional": true
48
+ },
49
+ "@odatnurd/cf-aegis": {
50
+ "optional": true
51
+ },
52
+ "miniflare": {
53
+ "optional": true
54
+ }
31
55
  },
32
56
  "scripts": {
33
- "test": "echo \"No tests specified\" && exit 0"
57
+ "test": "aegis test/aegis.config.js"
34
58
  }
35
59
  }