@odatnurd/cf-requests 0.1.6 → 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 +107 -78
  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,126 +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
- // For form data, we need to create a single, shared request object *before*
72
- // we build the context, so that both the header and the body can be derived
73
- // from the same source, ensuring the multipart boundary matches.
74
- let tempRequest = null;
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' };
75
108
  if (dataType === 'form') {
76
- const formData = new FormData();
77
- for (const key in data) {
78
- formData.append(key, data[key]);
79
- }
80
- tempRequest = new Request('http://localhost', {
81
- method: 'POST',
82
- body: formData,
83
- });
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;
84
121
  }
85
122
 
123
+ // Create the response now.
124
+ const rawRequest = new Request('http://localhost/', options)
86
125
 
87
- // In order to run the test we need to create a fake Hono context object to
88
- // pass to the middleware; this mimics the smallest possible footprint of
89
- // Hono context for our purposes.
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.
90
131
  const ctx = {
91
132
  req: {
92
- // These methods are used by the validator to pull the parsed data out of
93
- // the request in order to validate it, except for when the data type is
94
- // 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.
95
139
  param: () => data,
96
140
  json: async () => data,
97
- query: (key) => data[key],
98
- queries: (key) => {
99
- const result = {};
100
- for(const [k, v] of Object.entries(data)) {
101
- result[k] = Array.isArray(v) ? v : [v];
102
- }
103
- return key ? result[key] : result;
104
- },
105
- cookie: () => data,
106
- formData: async () => {
107
- if (dataType === 'form') {
108
- return tempRequest.formData();
109
- }
110
- // Fallback for other types, though not strictly needed by the validator
111
- const formData = new FormData();
112
- for (const key in data) {
113
- formData.append(key, data[key]);
114
- }
115
- return formData;
116
- },
117
- // For form data, the validator expects to be able to get the raw body
118
- // as an ArrayBuffer. We can simulate this by URL-encoding the data.
119
- arrayBuffer: async () => tempRequest ? tempRequest.arrayBuffer() : new ArrayBuffer(0),
120
- // The validator also uses a bodyCache property to store parsed bodies.
121
- bodyCache: {},
122
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: {},
123
158
 
124
- // We need to populate an actual cookie header in headers for it the
125
- // validator to be able to pull cookie data because it wants to parse it
126
- // itself.
127
- headers: new Headers(dataType === 'cookie'
128
- ? { 'Cookie': Object.entries(data).map(([k,v]) => `${k}=${v}`).join('; ') }
129
- : {}),
130
-
131
- // The validator invokes this to get headers out of the request when the
132
- // data type is JSON.
133
- header: (name) => {
134
- // If there is no name, return the data back directly; this call pattern
135
- // happens when the data type is header.
159
+ // The context supports gathering either a single header by name, or all
160
+ // headers (by passing undefined as a name.
161
+ header: name => {
136
162
  if (name === undefined) {
137
163
  return data;
138
164
  }
139
165
 
140
166
  return name.toLowerCase() !== 'content-type' ? undefined : {
141
167
  json: 'application/json',
142
- form: tempRequest?.headers.get('Content-Type'),
168
+ form: rawRequest.headers.get('Content-Type'),
143
169
  }[dataType];
144
170
  },
145
171
 
146
- // When validation succeeds, it invokes this to store the data back into
147
- // 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.
148
175
  addValidatedData: (target, data) => validData = data
149
176
  },
150
177
 
151
- // Used to capture a failure; the validator will invoke status to set the
152
- // required HTTP response and then invoke the json() method to populate the
153
- // error.
154
- status: (inStatus) => { responseStatus = inStatus; },
155
- 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 => {
156
184
  errorResponse = new Response(
157
185
  JSON.stringify(payload), {
158
186
  status: responseStatus,
159
- statusText: "Bad Request",
187
+ statusText: STATUS_TEXT[responseStatus] ?? 'Unknown Error',
160
188
  headers: { "Content-Type": "application/json" }
161
189
  }
162
190
  );
163
191
  },
164
192
  };
165
193
 
194
+ // Execute the middleware with an empty next().
166
195
  // Run the middleware; we either capture a result in the error payload or the
167
196
  // validation result.
168
- await middleware(ctx, next);
197
+ await middleware(ctx, () => {});
169
198
 
170
199
  // Return the error payload if validation failed, otherwise return the
171
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.6",
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
  }