https://github.com/momsfriendlydevco/cowboy
Wrapper around Cloudflare Wrangler to provide a more Express-like experience
https://github.com/momsfriendlydevco/cowboy
Last synced: 12 months ago
JSON representation
Wrapper around Cloudflare Wrangler to provide a more Express-like experience
- Host: GitHub
- URL: https://github.com/momsfriendlydevco/cowboy
- Owner: MomsFriendlyDevCo
- License: mit
- Created: 2023-09-27T22:18:05.000Z (almost 3 years ago)
- Default Branch: master
- Last Pushed: 2024-11-27T06:03:05.000Z (over 1 year ago)
- Last Synced: 2024-11-27T07:19:00.369Z (over 1 year ago)
- Language: JavaScript
- Size: 390 KB
- Stars: 0
- Watchers: 3
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
@MomsfriendlyDevCo/Cowboy
=========================
A friendler wrapper around the Cloudflare [Wrangler SDK](https://github.com/cloudflare/workers-sdk)
Features:
* Automatic CORS handling
* Basic router support
* Express-like `req` + `res` object for routes
* Built in middleware + request validation via [Joi](https://joi.dev)
* Built-in debug support for testkits + Wrangler
* Built-in JSON / Multipart (or FormData) / Plain text decoding and population of `req.body`
Examples
========
Simple request output
---------------------
Example `src/worker.js` file providing a GET server which generates random company profiles:
```javascript
import {faker} from '@faker-js/faker';
import cowboy from '@momsfriendlydevco/cowboy';
export default cowboy()
.use('cors') // Inject CORS functionality in every request
.get('/', ()=> ({
name: faker.company.name(),
motto: faker.company.catchPhrase(),
}))
```
ReST server example
-------------------
Example `src/worker.js` file providing a GET / POST ReST-like server:
```javascript
import cowboy from '@momsfriendlydevco/cowboy';
export default cowboy()
.use('cors')
.get('/widgets', ()=> // Fetch a list of widgets
widgetStore.fetchAll()
)
.post('/widgets', async (req, res, env) => { // Create a new widget
let newWidget = await widgetStore.create(req.body);
res.send({id: newWidget.id}); // Explicitly send response
})
.get('/widgets/:id', // Validate params + fetch an existing widget
['validateParams', joi => ({ // Use the 'validateParams' middleware with options
id: joi.number().required().above(10000).below(99999),
})],
req => widgetStore.fetch(req.params.id),
)
.delete('/widgets/:id', // Try to delete a widget
(req, res, env) => { // Apply custom middleware
let isAllowed = await widgetStore.userIsValid(req.headers.auth);
if (!isAllowed) return res.sendStatus(403); // Stop bad actors
},
req => widgetStore.delete(req.params.id)
)
};
Cron schedule handling
----------------------
Cron scheduling is a little basic at the moment but likely to improve in the future.
To set up a Cron handler simply install it by calling `.schedule(callback)`:
```javascript
import cowboy from '@momsfriendlydevco/cowboy';
export default cowboy()
.schedule(async (event, env, ctx) => {
// Handle cron code here
})
```
```
Debugging
---------
This module uses the [Debug NPM](https://github.com/visionmedia/debug#readme). To enable simply set the `DEBUG` environment variable to include `cowboy`.
Debugging workers in Testkits will automatically detect this token and enable debugging there. Use the `debug` export within Testkits to see output.
API
===
cowboy()
--------
```javascript
import cowboy from '@momsfriendlydevco/cowboy';
```
Instanciate a `Cowboy` class instance and provide a simple router skeleton.
Cowboy
------
```javascript
import {Cowboy} from '@momsfriendlydevco/cowboy';
```
The instance created by `cowboy()`.
Cowboy.delete(path) / .get() / .head() / .post() / .put() / .options()
----------------------------------------------------------------------
Queue up a route with a given path.
Each component is made up of a path + any number of middleware handlers.
```javascript
let router = new Cowboy()
.get('/my/path', middleware1, middleware2...)
```
Notes:
* All middleware items are called in sequence - and are async waited-on)
* If any middleware functions fail the entire chain aborts with an error
* All middleware functions are called as `(CowboyRequest, CowboyResponse, Env)`
* If any middleware functions call `res.end()` (or any of its automatic methods like `res.send()` / `res.sendStatus()`) the chain also aborts successfully
* If the last middleware function returns a non response object - i.e. the function didn't call `res.send()` its assumed to be a valid output and is automatically wrapped
Cowboy.use(middleware)
----------------------
Queue up a universal middleware handler which will be used on *all* endpoints.
Middleware is called as per `Cowboy.get()` and its equivelents.
Cowboy.resolve(CowboyRequest)
-----------------------------
Find the matching route that would be used if given a prototype request.
Cowboy.fetch(CloudflareRequest, CloudflareEnv)
----------------------------------------------
Execute the router when given various Cloudflare inputs.
This function will, in order:
1. Enable debugging if required
2. Create `(req:CowboyRequest, res:CowboyResponse)`
3. Execute all middleware setup via `Cowboy.use()`
4. Find a matching route - if no route is found, raise a 404 and quit
5. Execute the matching route middleware, in sequence
6. Return the final response - if it the function did not already explicitly do so
Cowboy.proxy(path, request, env)
--------------------------------
Forward from one route to another as if the second route was called first.
Cowboy.schedule(callback)
-------------------------
Install a scheduled Cron handler function.
CowboyRequest
-------------
```javascript
import CowboyRequest from '@momsfriendlydevco/cowboy/request';
```
A wrapped version of the incoming `CloudflareRequest` object.
This object is identical to the original [CloudflareRequest](https://developers.cloudflare.com/workers/runtime-apis/request/#properties) object with the following additions:
| Property | Type | Description |
|------------|----------|----------------------------------------------------------|
| `path` | `String` | Extracted `url.pathname` portion of the incoming request |
| `hostname` | `String` | Extracted `url.hostname` portion of the incoming request |
CowboyResponse
--------------
```javascript
import CowboyResponse from '@momsfriendlydevco/cowboy/request';
```
An Express-like response object.
Calling any method which ends the session will cause the middleware chain to terminate and the response to be served back.
This object contains various Express-like utility functions:
| Method | Description |
|-------------------------------------|----------------------------------------------------------|
| `set(options)` | Set response output headers (using an object) |
| `set(header, value)` | Alternate method to set headers individually |
| `send(data, end=true)` | Set the output response and optionally end the session |
| `end(data?, end=true)` | Set the output response and optionally end the session |
| `sendStatus(code, data?, end=true)` | Send a HTTP response code and optionally end the session |
| `status(code)` | Set the HTTP response code |
| `toCloudflareResponse()` | Return the equivelent CloudflareResponse object |
All functions (except `toCloudflareResponse()`) are chainable and return the original `CowboyResponse` instance.
CowboyTestkit
-------------
```javascript
import CowboyTestkit from '@momsfriendlydevco/cowboy/testkit';
```
A series of utilities to help write testkits with Wrangler + Cowboy.
CowboyTestkit.cowboyMocha()
---------------------------
Inject various Mocha before/after tooling.
```javascript
import axios from 'axios';
import {cowboyMocha} from '@momsfriendlydevco/cowboy/testkit';
import {expect} from 'chai';
describe('My Wrangler Endpoint', ()=> {
// Inject Cowboy/mocha testkit handling
cowboyMocha({
axios,
});
let checkCors = headers => {
expect(headers).to.be.an.instanceOf(axios.AxiosHeaders);
expect(headers).to.have.property('access-control-allow-origin', '*');
expect(headers).to.have.property('access-control-allow-methods', 'GET, POST, OPTIONS');
expect(headers).to.have.property('access-control-allow-headers', '*');
expect(headers).to.have.property('content-type', 'application/json;charset=UTF-8');
};
it('should expose CORS headers', ()=>
axios('/', {
method: 'OPTIONS',
}).then(({data, headers}) => {
expect(data).to.be.equal('ok');
checkCors(headers);
})
);
it('should do something useful', ()=>
axios('/', {
method: 'get',
}).then(({data, headers}) => {
checkCors(headers);
// ... Your functionality checks ... //
})
);
});
```
CowboyTestkit.start(options)
----------------------------
Boot a wranger instance in the background and prepare for testing.
Returns a promise.
| Option | Type | Default | Description |
|----------------|------------|---------------|-----------------------------------------------------------|
| `axios` | `Axios` | | Axios instance to mutate with the base URL, if specified |
| `logOutput` | `Function` | | Function to wrap STDOUT output. Called as `(line:String)` |
| `logOutputErr` | `Function` | | Function to wrap STDERR output. Called as `(line:String)` |
| `host` | `String` | `'127.0.0.1'` | Host to run Wrangler on |
| `port` | `String` | `8787` | Host to run Wrangler on |
| `logLevel` | `String` | `'log'` | Log level to instruct Wrangler to run as |
CowboyTestkit.stop()
--------------------
Terminate any running Wrangler background processes.
Middleware
==========
Cowboy ships with out-of-the-box middleware.
Middleware are simple functions which accept the paramters `(req:CowboyRequest, res:CowboyResponse)` and can modify the request, halt output with a call to `res` or perform other Async actions before continuing to the next middleware item.
To use middleware in your routes you can either declare it using `.use(middleware)` - which installs it globally or `.ROUTE(middleware...)` which installs it only for that route.
Middleware can be declared in the following ways:
```javascript
import cowboy from '@momsfriendlydevco/cowboy';
// Shorthand with defaults - just specify the name
cowboy()
.get('/path',
'cors',
(req, res, env) => /* ... */
)
// Name + options - specify an array with an optional options object
cowboy()
.get('/path',
['cors', {
option1: value1,
/* ... */
}],
(req, res, env) => /* ... */
)
// Middleware function - include the import
import cors from '@momsfriendlydevco/cowboy/middleware/cors';
cowboy()
.get('/path',
cors({
option1: value1,
/* ... */
}),
(req, res, env) => /* ... */
)
```
cors(options)
-------------
Inject simple CORS headers to allow websites to use the endpoint from the browser frontend.
devOnly()
---------
Allow access to the endpoint ONLY if Cloudflare is running in local development mode. Throws a 403 otherwise.
validate(key, validator)
------------------------
Validate the incoming `req.$KEY` object using [Joyful](https://github.com/MomsFriendlyDevCo/Joyful).
This function takes two arguments - the `req` subkey to examine and the validation function / object.
```javascript
import cowboy from '@momsfriendlydevco/cowboy';
// Shorthand with defaults - just specify the name
cowboy()
.get('/path',
['validate', 'body', joi => {
widget: joi.string().required().valid('froody', 'doodad'),
size: joi.number().optional(),
})],
(req, res, env) => /* ... */
)
```
validateBody(validator)
-----------------------
Shorthand validator which runs validation on the `req.body` parameter only.
```javascript
import cowboy from '@momsfriendlydevco/cowboy';
// Shorthand with defaults - just specify the name
cowboy()
.get('/path',
['validateBody', joi => ({
widget: joi.string().required().valid('froody', 'doodad'),
size: joi.number().optional(),
})],
(req, res, env) => /* ... */
)
```
validateHeaders(validator)
-----------------------
Shorthand validator which runs validation on the `req.headers` parameter only.
validateParams(validator)
-------------------------
Shorthand validator which runs validation on the `req.params` parameter only.
```javascript
import cowboy from '@momsfriendlydevco/cowboy';
// Shorthand with defaults - just specify the name
cowboy()
.get('/widgets/:id',
['validateParams', joi => {
id: joi.string().required(),
})],
(req, res, env) => /* ... */
)
```
validateQuery(validator)
------------------------
Shorthand validator which runs validation on the `req.query` parameter only.
```javascript
import cowboy from '@momsfriendlydevco/cowboy';
// Shorthand with defaults - just specify the name
cowboy()
.get('/widgets/search',
['validateQuery', joi => {
q: joi.string().requried(),
})],
(req, res, env) => /* ... */
)
```
parseJwt()
----------
Parse the incoming request as a JWT string and decode its contents into `req.body`.