@momsfriendlydevco/cowboy 1.0.3 → 1.0.5
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 +14 -16
- package/lib/cowboy.js +67 -13
- package/lib/request.js +9 -0
- package/lib/response.js +1 -1
- package/lib/testkit.js +3 -0
- package/middleware/validate.js +4 -2
- package/middleware/validateBody.js +1 -1
- package/middleware/validateParams.js +1 -1
- package/middleware/validateQuery.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -296,10 +296,10 @@ cors
|
|
|
296
296
|
Inject simple CORS headers to allow websites to use the endpoint from the browser frontend.
|
|
297
297
|
|
|
298
298
|
|
|
299
|
-
validate
|
|
300
|
-
|
|
301
|
-
Validate the incoming `req` object using [Joyful](https://github.com/MomsFriendlyDevCo/Joyful).
|
|
302
|
-
|
|
299
|
+
validate(key, validator)
|
|
300
|
+
------------------------
|
|
301
|
+
Validate the incoming `req.$KEY` object using [Joyful](https://github.com/MomsFriendlyDevCo/Joyful).
|
|
302
|
+
This function takes two arguments - the `req` subkey to examine and the validation function / object.
|
|
303
303
|
|
|
304
304
|
```javascript
|
|
305
305
|
import cowboy from '@momsfriendlydevco/cowboy';
|
|
@@ -307,18 +307,16 @@ import cowboy from '@momsfriendlydevco/cowboy';
|
|
|
307
307
|
// Shorthand with defaults - just specify the name
|
|
308
308
|
cowboy()
|
|
309
309
|
.get('/path',
|
|
310
|
-
['validate', joi => {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
size: joi.number().optional(),
|
|
314
|
-
},
|
|
310
|
+
['validate', 'body', joi => {
|
|
311
|
+
widget: joi.string().required().valid('froody', 'doodad'),
|
|
312
|
+
size: joi.number().optional(),
|
|
315
313
|
})],
|
|
316
314
|
(req, res) => /* ... */
|
|
317
315
|
)
|
|
318
316
|
```
|
|
319
317
|
|
|
320
|
-
validateBody
|
|
321
|
-
|
|
318
|
+
validateBody(validator)
|
|
319
|
+
-----------------------
|
|
322
320
|
Shorthand validator which runs validation on the `req.body` parameter only.
|
|
323
321
|
|
|
324
322
|
|
|
@@ -328,7 +326,7 @@ import cowboy from '@momsfriendlydevco/cowboy';
|
|
|
328
326
|
// Shorthand with defaults - just specify the name
|
|
329
327
|
cowboy()
|
|
330
328
|
.get('/path',
|
|
331
|
-
['validateBody', joi => {
|
|
329
|
+
['validateBody', joi => ({
|
|
332
330
|
widget: joi.string().required().valid('froody', 'doodad'),
|
|
333
331
|
size: joi.number().optional(),
|
|
334
332
|
})],
|
|
@@ -337,8 +335,8 @@ cowboy()
|
|
|
337
335
|
```
|
|
338
336
|
|
|
339
337
|
|
|
340
|
-
validateParams
|
|
341
|
-
|
|
338
|
+
validateParams(validator)
|
|
339
|
+
-------------------------
|
|
342
340
|
Shorthand validator which runs validation on the `req.params` parameter only.
|
|
343
341
|
|
|
344
342
|
|
|
@@ -356,8 +354,8 @@ cowboy()
|
|
|
356
354
|
```
|
|
357
355
|
|
|
358
356
|
|
|
359
|
-
validateQuery
|
|
360
|
-
|
|
357
|
+
validateQuery(validator)
|
|
358
|
+
------------------------
|
|
361
359
|
Shorthand validator which runs validation on the `req.query` parameter only.
|
|
362
360
|
|
|
363
361
|
|
package/lib/cowboy.js
CHANGED
|
@@ -27,6 +27,7 @@ export class Cowboy {
|
|
|
27
27
|
* @property {Function} pathTidy Additional tidyup for server request paths, useful if the API does not live at the server root. Defaults to removing a "/api/:worker/" prefix
|
|
28
28
|
*/
|
|
29
29
|
settings = {
|
|
30
|
+
patchAxios: true,
|
|
30
31
|
pathTidy(path) {
|
|
31
32
|
return path.replace(/^\/api\/\w+/, '/');
|
|
32
33
|
},
|
|
@@ -47,6 +48,13 @@ export class Cowboy {
|
|
|
47
48
|
routes = [];
|
|
48
49
|
|
|
49
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Has completed one init() cycle
|
|
53
|
+
* @type {Boolean}
|
|
54
|
+
*/
|
|
55
|
+
doneInit = false;
|
|
56
|
+
|
|
57
|
+
|
|
50
58
|
/**
|
|
51
59
|
* Queue up a middleware path
|
|
52
60
|
* All given middleware is called in sequence, if middleware
|
|
@@ -70,6 +78,7 @@ export class Cowboy {
|
|
|
70
78
|
/**
|
|
71
79
|
* Prepend middleware which will be used for all routes
|
|
72
80
|
* @param {CowboyMiddleware} middleware Middleware to use
|
|
81
|
+
* @returns {Cowboy} This chainable Cowboy router instance
|
|
73
82
|
*/
|
|
74
83
|
use(...middleware) {
|
|
75
84
|
this.earlyMiddleware.push(middleware);
|
|
@@ -101,26 +110,32 @@ export class Cowboy {
|
|
|
101
110
|
* @returns {Promise<CowboyResponse>} A promise which will eventually resolve when all middleware completes
|
|
102
111
|
*/
|
|
103
112
|
async fetch(cfReq, env) {
|
|
104
|
-
|
|
113
|
+
this.init();
|
|
114
|
+
|
|
115
|
+
if (env.COWBOY_DEBUG)
|
|
105
116
|
debug.enabled = true;
|
|
106
|
-
debug('Cowboy Worker debugging is enabled');
|
|
107
|
-
}
|
|
108
117
|
|
|
109
118
|
// Create basic [req]uest / [res]ponse objects
|
|
110
119
|
let req = new CowboyRequest(cfReq, {
|
|
111
120
|
router: this,
|
|
112
121
|
pathTidy: this.settings.pathTidy,
|
|
113
122
|
});
|
|
123
|
+
if (cfReq.body) req.body = await cfReq.json();
|
|
124
|
+
|
|
114
125
|
let res = new CowboyResponse();
|
|
126
|
+
debug('Incoming request:', req.toString());
|
|
115
127
|
|
|
116
128
|
// Exec all earlyMiddleware - every time
|
|
117
|
-
await this.execMiddleware({
|
|
129
|
+
await this.execMiddleware({
|
|
130
|
+
req, res,
|
|
131
|
+
middleware: this.earlyMiddleware,
|
|
132
|
+
});
|
|
118
133
|
|
|
119
134
|
// Find matching route
|
|
120
135
|
let route = this.resolve(req);
|
|
121
136
|
if (!route) {
|
|
122
137
|
if (debug.enabled) {
|
|
123
|
-
debug(`No matching route for "${req.
|
|
138
|
+
debug(`No matching route for "${req.toString()}"`);
|
|
124
139
|
this.routes.forEach((r, i) =>
|
|
125
140
|
debug(
|
|
126
141
|
`Route #${i}`,
|
|
@@ -133,7 +148,10 @@ export class Cowboy {
|
|
|
133
148
|
}
|
|
134
149
|
|
|
135
150
|
// Exec route middleware
|
|
136
|
-
let response = await this.execMiddleware({
|
|
151
|
+
let response = await this.execMiddleware({
|
|
152
|
+
req, res,
|
|
153
|
+
middleware: route.middleware,
|
|
154
|
+
});
|
|
137
155
|
|
|
138
156
|
if (!response) throw new Error('Middleware chain ended without returning a response!');
|
|
139
157
|
if (!response.toCloudflareResponse) throw new Error('Eventual middleware chain output should have a .toCloudflareResponse() method');
|
|
@@ -142,12 +160,12 @@ export class Cowboy {
|
|
|
142
160
|
|
|
143
161
|
|
|
144
162
|
async execMiddleware({middleware, req, res}) {
|
|
145
|
-
let middlewareStack =
|
|
163
|
+
let middlewareStack = middleware
|
|
146
164
|
.map(m => {
|
|
147
165
|
let mFunc =
|
|
148
166
|
typeof m == 'function' ? m // Already a function
|
|
149
|
-
: typeof m == 'string' ? CowboyMiddleware[m]() // Lookup from middleware with defaults
|
|
150
|
-
: Array.isArray(m) ? CowboyMiddleware[m[0]](m
|
|
167
|
+
: typeof m == 'string' ? CowboyMiddleware[m].apply(this) // Lookup from middleware with defaults
|
|
168
|
+
: Array.isArray(m) ? CowboyMiddleware[m[0]].apply(this, m.slice(1)) // Lookup from middleware with options
|
|
151
169
|
: (()=> { throw new Error(`Unknown middleware type "${typeof m}"`) })()
|
|
152
170
|
|
|
153
171
|
if (!mFunc) throw new Error('Cowboy Middleware must be a function, string or Record(name, options)');
|
|
@@ -157,12 +175,29 @@ export class Cowboy {
|
|
|
157
175
|
let response; // Response to eventually send
|
|
158
176
|
while (middlewareStack.length > 0) {
|
|
159
177
|
let middleware = middlewareStack.shift();
|
|
160
|
-
|
|
161
|
-
|
|
178
|
+
try {
|
|
179
|
+
response = await middleware(req, res);
|
|
180
|
+
if (response?.hasSent) { // Stop middleware chain as some intermediate has signalled the chain should end
|
|
181
|
+
response = res;
|
|
182
|
+
break;
|
|
183
|
+
} else if (response && !(response instanceof CowboyResponse) && middlewareStack.length == 0) { // Last item in middleware chain returned something but it doesn't look like a regular response - wrap it
|
|
184
|
+
response = res.end(response);
|
|
185
|
+
}
|
|
186
|
+
} catch (e) {
|
|
187
|
+
let errorText = typeof e == 'string' ? e : e.toString();
|
|
188
|
+
|
|
189
|
+
debug('Error thrown', errorText);
|
|
190
|
+
|
|
191
|
+
// Form: '404: Not found'
|
|
192
|
+
if (/^(\d{3}):/.test(errorText)) {
|
|
193
|
+
let errorBits = /^(?<status>\d{3}):?(?<text>.*)$/.exec(errorText).groups;
|
|
194
|
+
res.status(errorBits.status).send(errorBits.text);
|
|
195
|
+
} else { // Generic error - assume 400
|
|
196
|
+
res.status(400).send(e.toString());
|
|
197
|
+
}
|
|
198
|
+
|
|
162
199
|
response = res;
|
|
163
200
|
break;
|
|
164
|
-
} else if (response && !(response instanceof CowboyResponse) && middlewareStack.length == 0) { // Last item in middleware chain returned something but it doesn't look like a regular response - wrap it
|
|
165
|
-
response = res.end(response);
|
|
166
201
|
}
|
|
167
202
|
}
|
|
168
203
|
return response;
|
|
@@ -176,6 +211,23 @@ export class Cowboy {
|
|
|
176
211
|
post(path, ...middleware) { return this.route('POST', path, ...middleware) }
|
|
177
212
|
put(path, ...middleware) { return this.route('PUT', path, ...middleware) }
|
|
178
213
|
options(path, ...middleware) { return this.route('OPTIONS', path, ...middleware) }
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Generial Init() sequence
|
|
218
|
+
* This will be run automatically on setup or the first fetch()
|
|
219
|
+
* @returns {Cowboy} This chainable Cowboy router instance
|
|
220
|
+
*/
|
|
221
|
+
init() {
|
|
222
|
+
debug('INIT!');
|
|
223
|
+
if (this.doneInit) return this; // Already completed init
|
|
224
|
+
|
|
225
|
+
if (this.settings.patchAxios) {
|
|
226
|
+
// TODO: Patch Axios somehow
|
|
227
|
+
// axios.defaults.adapter = axiosFetchAdapter;
|
|
228
|
+
}
|
|
229
|
+
return this;
|
|
230
|
+
}
|
|
179
231
|
}
|
|
180
232
|
|
|
181
233
|
|
|
@@ -191,5 +243,7 @@ export default function cowboy(options) {
|
|
|
191
243
|
fetch: cowboyInstance.fetch.bind(cowboyInstance),
|
|
192
244
|
});
|
|
193
245
|
|
|
246
|
+
cowboyInstance.init();
|
|
247
|
+
|
|
194
248
|
return cowboyInstance;
|
|
195
249
|
}
|
package/lib/request.js
CHANGED
|
@@ -36,4 +36,13 @@ export default class CowboyRequest {
|
|
|
36
36
|
this.path = this.pathTidy(url.pathname);
|
|
37
37
|
this.hostname = url.hostname;
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Utility function to simplify an incoming request
|
|
43
|
+
* @returns {String} Human readable string
|
|
44
|
+
*/
|
|
45
|
+
toString() {
|
|
46
|
+
return `${this.method} ${this.path}`;
|
|
47
|
+
}
|
|
39
48
|
}
|
package/lib/response.js
CHANGED
|
@@ -99,7 +99,7 @@ export default class CowboyResponse {
|
|
|
99
99
|
status: this.code,
|
|
100
100
|
headers: this.headers,
|
|
101
101
|
};
|
|
102
|
-
console.log('Build response', JSON.stringify(cfOptions));
|
|
102
|
+
console.log('Build response', JSON.stringify({...cfOptions, body: this.body}, null, '\t'));
|
|
103
103
|
return new this.CloudflareResponse(this.body, cfOptions);
|
|
104
104
|
}
|
|
105
105
|
}
|
package/lib/testkit.js
CHANGED
|
@@ -17,6 +17,7 @@ export let worker;
|
|
|
17
17
|
*
|
|
18
18
|
* @param {Object} [options] Additional options to mutate behaviour
|
|
19
19
|
* @param {Axios} [options.axios] Axios instance to mutate with the base URL, if specified
|
|
20
|
+
* @param {Boolean} [options.server=true] Initialize a local server - disable this if you're running your own
|
|
20
21
|
* @param {Function} [options.logOutput] Function to wrap STDOUT output. Called as `(line:String)`
|
|
21
22
|
* @param {Function} [options.logOutputErr] Function to wrap STDERR output. Called as `(line:String)`
|
|
22
23
|
* @param {String} [options.host='127.0.0.1'] Host to run Wrangler on
|
|
@@ -28,6 +29,7 @@ export let worker;
|
|
|
28
29
|
export function start(options) {
|
|
29
30
|
let settings = {
|
|
30
31
|
axios: null,
|
|
32
|
+
server: true,
|
|
31
33
|
logOutput: output => console.log('WRANGLER>', output),
|
|
32
34
|
logOutputErr: output => console.log('WRANGLER!', output),
|
|
33
35
|
host: '127.0.0.1',
|
|
@@ -51,6 +53,7 @@ export function start(options) {
|
|
|
51
53
|
// }}}
|
|
52
54
|
// Launch worker {{{
|
|
53
55
|
.then(()=> {
|
|
56
|
+
if (!settings.server) return;
|
|
54
57
|
debug('Running Wrangler against script', wranglerConfig.main);
|
|
55
58
|
|
|
56
59
|
let isRunning = false;
|
package/middleware/validate.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import joyful from '@momsfriendlydevco/joyful';
|
|
2
2
|
|
|
3
|
-
export default function CowboyMiddlewareValidate(validator) {
|
|
3
|
+
export default function CowboyMiddlewareValidate(subkey, validator) {
|
|
4
4
|
return (req, res) => {
|
|
5
|
-
let joyfulResult = joyful(
|
|
5
|
+
let joyfulResult = joyful({
|
|
6
|
+
[subkey]: validator,
|
|
7
|
+
});
|
|
6
8
|
|
|
7
9
|
if (joyfulResult !== true) { // Failed body validation?
|
|
8
10
|
return res
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@momsfriendlydevco/cowboy",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Wrapper around Cloudflare Wrangler to provide a more Express-like experience",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"lint": "eslint ."
|
|
@@ -35,12 +35,12 @@
|
|
|
35
35
|
"node": ">=20.x"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@momsfriendlydevco/joyful": "^1.0.
|
|
38
|
+
"@momsfriendlydevco/joyful": "^1.0.1",
|
|
39
39
|
"toml": "^3.0.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@momsfriendlydevco/eslint-config": "^1.0.7",
|
|
43
|
-
"eslint": "^8.
|
|
43
|
+
"eslint": "^8.51.0"
|
|
44
44
|
},
|
|
45
45
|
"eslintConfig": {
|
|
46
46
|
"extends": "@momsfriendlydevco",
|