1.0.17 • Published 2 months ago

@momsfriendlydevco/cowboy v1.0.17

Weekly downloads
-
License
MIT
Repository
github
Last release
2 months ago

@MomsfriendlyDevCo/Cowboy

A friendler wrapper around the Cloudflare Wrangler SDK

Features:

  • Automatic CORS handling
  • Basic router support
  • Express-like req + res object for routes
  • Built in middleware + request validation via Joi
  • 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:

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:

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)
	)
};

Debugging

This module uses the Debug NPM. 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()

import cowboy from '@momsfriendlydevco/cowboy';

Instanciate a Cowboy class instance and provide a simple router skeleton.

Cowboy

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.

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

CowboyRequest

import CowboyRequest from '@momsfriendlydevco/cowboy/request';

A wrapped version of the incoming CloudflareRequest object.

This object is identical to the original CloudflareRequest object with the following additions:

PropertyTypeDescription
pathStringExtracted url.pathname portion of the incoming request
hostnameStringExtracted url.hostname portion of the incoming request

CowboyResponse

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:

MethodDescription
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

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.

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.

OptionTypeDefaultDescription
axiosAxiosAxios instance to mutate with the base URL, if specified
logOutputFunctionFunction to wrap STDOUT output. Called as (line:String)
logOutputErrFunctionFunction to wrap STDERR output. Called as (line:String)
hostString'127.0.0.1'Host to run Wrangler on
portString8787Host to run Wrangler on
logLevelString'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:

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.

validate(key, validator)

Validate the incoming req.$KEY object using Joyful. This function takes two arguments - the req subkey to examine and the validation function / object.

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.

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) => /* ... */
	)

validateParams(validator)

Shorthand validator which runs validation on the req.params parameter only.

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.

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) => /* ... */
	)
1.0.17

2 months ago

1.0.16

2 months ago

1.0.15

5 months ago

1.0.14

5 months ago

1.0.13

5 months ago

1.0.11

5 months ago

1.0.10

5 months ago

1.0.9

5 months ago

1.0.8

5 months ago

1.0.7

5 months ago

1.0.6

6 months ago

1.0.5

7 months ago

1.0.4

7 months ago

1.0.3

7 months ago

1.0.2

7 months ago

1.0.1

7 months ago

1.0.0

7 months ago