@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 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
- The only argument is the Joyful validator which is run against `req`.
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
- body: {
312
- widget: joi.string().required().valid('froody', 'doodad'),
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
- if (env.COWBOY_DEBUG) {
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({req, res, middleware: this.earlyMiddleware});
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.method} ${req.path}"`);
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({req, res, middleware: route.middleware});
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 = [...middleware] // Shallow copy middleware stack to execute
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[1]) // Lookup from middleware with options
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
- response = await middleware(req, res);
161
- if (response?.hasSent) { // Stop middleware chain as some intermediate has signalled the chain should end
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;
@@ -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(req, validator, {throw: false});
5
+ let joyfulResult = joyful({
6
+ [subkey]: validator,
7
+ });
6
8
 
7
9
  if (joyfulResult !== true) { // Failed body validation?
8
10
  return res
@@ -1,7 +1,7 @@
1
1
  import CowboyMiddlewareValidate from '#middleware/validate';
2
2
 
3
3
  export default function CowboyMiddlewareValidateBody(validator) {
4
- return CowboyMiddlewareValidate({
4
+ return CowboyMiddlewareValidate('body', {
5
5
  body: validator,
6
6
  })
7
7
  }
@@ -1,7 +1,7 @@
1
1
  import CowboyMiddlewareValidate from '#middleware/validate';
2
2
 
3
3
  export default function CowboyMiddlewareValidateParams(validator) {
4
- return CowboyMiddlewareValidate({
4
+ return CowboyMiddlewareValidate('params', {
5
5
  params: validator,
6
6
  })
7
7
  }
@@ -1,7 +1,7 @@
1
1
  import CowboyMiddlewareValidate from '#middleware/validate';
2
2
 
3
3
  export default function CowboyMiddlewareValidateQuery(validator) {
4
- return CowboyMiddlewareValidate({
4
+ return CowboyMiddlewareValidate('query', {
5
5
  query: validator,
6
6
  })
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momsfriendlydevco/cowboy",
3
- "version": "1.0.3",
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.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.50.0"
43
+ "eslint": "^8.51.0"
44
44
  },
45
45
  "eslintConfig": {
46
46
  "extends": "@momsfriendlydevco",