diff --git a/README.md b/README.md index 1d9b1bd..72bd2b0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ Neap Pty Ltd logo -# WebFunc - Lightweight Serverless Web Framework +# WebFunc - Universal Serverless Web Framework [![NPM][1]][2] [![Tests][3]][4] [1]: https://img.shields.io/npm/v/webfunc.svg?style=flat @@ -8,6 +8,20 @@ [3]: https://travis-ci.org/nicolasdao/webfunc.svg?branch=master [4]: https://travis-ci.org/nicolasdao/webfunc +__*Wefunc*__ is a _universal_ web framework that uses the awesome [Zeit Now CLI](https://zeit.co/now) to manage deployments. Universal means write your code once with _webfunc_ and deploy it on any serverless platform: +- [Zeit Now](https://zeit.co/now) +- [Google Cloud Functions](https://cloud.google.com/functions/) +- [AWS Lambda](https://aws.amazon.com/lambda/) (COMING SOON...) +- [Azure Functions](https://azure.microsoft.com/en-us/services/functions/) (COMING SOON...) + +You can also run it locally without any other dependencies. Run `node index.js` and that's it. + +Out-of-the-box features include: +- [_Routing_](#how-to-use-it) +- [_CORS_](#cors) +- [_Environment Variables_](#adding-multiple-deployment-environments) +- 3rd party middleware. + # Table of Contents > * [Install](#install) @@ -16,17 +30,76 @@ > * [Authentication](#authentication) > * [Make It Work With Express](#make-it-work-with-express) -Add CORS support and routing to Google Cloud Functions & Firebase Functions projects (AWS Lambdas coming soon). -- [Routing](#how-to-use-it) -- [CORS Support](#cors) -- [Environment Variables](#adding-multiple-deployment-environments) +# Intro + + + +_index.js:_ + +```js +const { listen, serve } = require('webfunc') + +const server = serve('/users/:username', (req, res, params) => res.status(200).send(`Hello env ${params.username}`)) +eval(listen('server', 4000)) +``` + +Add a __*start*__ script in your _package.json_ +```js + "scripts": { + "start": "node index.js" + } +``` + +_Deploy locally without any other dependencies_ +``` +npm start +``` + +_Deploy to Zeit Now_ +``` +now +``` + +_Deploy to Google Cloud Function_ +``` +now gcp +``` +> For this to work you need to configure a __*now.json*__ file as well as update the _start_ script. More detail + + # Install ``` npm install webfunc --save ``` -# How To Use It +# How To Use It - Show Me The Code +## Creating A REST API +### Single Endpoint Locally +_index.js:_ + +```js +const { listen, serve } = require('webfunc') + +const server = serve('/users/:username', (req, res, params) => res.status(200).send(`Hello ${params.username}`)) +eval(listen('server', 4000)) +``` + +To run this code locally, simply run in your terminal: +``` +node index.js +``` + +> To speed up your development, use [_hot reloading_](#easy-hot-reloading) as explained in the [Tips & Tricks](#tips-tricks) section below. + +### Multiple Endpoints Locally +```js +const { listen, serve, app } = require('webfunc') + +const server = serve('/users/:username', (req, res, params) => res.status(200).send(`Hello env ${params.username}`)) +eval(listen('server', 4000)) +``` + In its simplest form, a Google Cloud Functions project looks like this: ```js @@ -282,6 +355,25 @@ If you do need to allow access to anybody, then do not allow requests to send co ``` If you do need to pass authentication token, you will have to pass it using a special header(e.g. Authorization), or pass it in the query string if you want to avoid preflight queries (preflight queries happens in cross-origin requests when special headers are being used). However, passing credentials in the query string are considered a bad practice. +# Tips & Tricks +## Dev Lifecycle +### Easy Hot Reloading +While developing on your localhost, we recommend using hot reloading to help you automatically restart your node process after each change. [_node-dev_](https://github.com/fgnass/node-dev) is a lightweight development tools that watches the minimum amount of files in your project and automatically restart the node process each time a file has changed. +``` +npm install node-dev --save-dev +``` +Change your __*start*__ script in your _package.json_ from `"start": "node index.js"` to: +```js + "scripts": { + "start": "node-dev index.js" + } +``` +Then simply start your server as follow: +``` +npm start +``` + + # Contributing ``` npm test diff --git a/package.json b/package.json index 6bf2d8f..360e881 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "express": "^4.16.2", "http-errors": "^1.6.1", "path-to-regexp": "^2.1.0", + "raw-body": "^2.3.2", "shortid": "^2.2.8" }, "devDependencies": { diff --git a/src/webfunc.js b/src/webfunc.js index 9651a06..f4901b8 100644 --- a/src/webfunc.js +++ b/src/webfunc.js @@ -8,6 +8,7 @@ const path = require('path') const fs = require('fs') const shortid = require('shortid') +const getRawBody = require('raw-body') const { getRouteDetails, matchRoute } = require('./routing') const { app, HttpHandler } = require('./handler') require('colors') @@ -287,7 +288,7 @@ const serveHttp = (arg1, arg2, arg3) => { return handleHttpRequest(req, res, _appconfig) .then(() => !res.headersSent - ? setResponseHeaders(res, _appconfig).then(res => httpNextRequest(req, res, Object.assign(parameters, getRequestParameters(req)))) + ? setResponseHeaders(res, _appconfig).then(res => getRequestParameters(req).then(paramts => httpNextRequest(req, res, Object.assign(parameters, paramts)))) : res) .then(() => ({ req, res, ctx })) } @@ -364,24 +365,28 @@ const listen = (functionName, port) => { } const getRequestParameters = req => { - let bodyParameters = {} - if (req.body) { - const bodyType = typeof(req.body) - if (bodyType == 'object') - bodyParameters = req.body - else if (bodyType == 'string') { - try { - bodyParameters = JSON.parse(req.body) - } - catch(err) { - bodyParameters = {} - console.log(err) + const getBody = req.body ? Promise.resolve(req.body) : getRawBody(req) + return getBody.then(body => { + let bodyParameters = {} + if (body) { + const bodyType = typeof(req.body) + if (bodyType == 'object') + bodyParameters = req.body + else if (bodyType == 'string') { + try { + bodyParameters = JSON.parse(req.body) + } + catch(err) { + bodyParameters = {} + console.log(err) + } } } - } - const parameters = Object.assign((bodyParameters || {}), req.query || {}) - return parameters + const parameters = Object.assign((bodyParameters || {}), req.query || {}) + + return parameters + }) } const getLongestRoute = (routes=[]) => routes.sort((a,b) => b.match.length - a.match.length)[0] @@ -448,9 +453,8 @@ const serveHttpEndpoints = (endpoints, appconfig) => { if (typeof(next) != 'function') return res.status(500).send(`Wrong argument exception. Endpoint '${httpEndpoint}' for method ${httpMethod} defines a 'next' argument that is not a function similar to '(req, res, params) => ...'.`) - const parameters = getRequestParameters(req) - - return next(req, res, Object.assign(parameters, endpoint.winningRoute.parameters)) + const paramts = Object.assign({}, endpoint.winningRoute.parameters) + return getRequestParameters(req).then(parameters => next(req, res, Object.assign(parameters, paramts))) }) : res) .then(() => ({ req, res, ctx })) diff --git a/test/webfunc.js b/test/webfunc.js index 3d9441a..a0f1975 100644 --- a/test/webfunc.js +++ b/test/webfunc.js @@ -1413,6 +1413,40 @@ describe('webfunc', () => return Promise.all([result_01, result_02]) }))) - +/*eslint-disable */ +describe('webfunc', () => + describe('#serveHttp: 22', () => + it('Should capture the body of a POST request and interpret it as a JSON in the params argument.', () => { + /*eslint-enable */ + const req = httpMocks.createRequest({ + method: 'POST', + headers: { + origin: 'http://localhost:8080', + referer: 'http://localhost:8080' + }, + body: { + username: 'nic', + password: '1234' + } + }) + const res = httpMocks.createResponse() + const appconfig = { + headers: { + 'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS, POST', + 'Access-Control-Allow-Headers': 'Authorization, Content-Type, Origin', + 'Access-Control-Allow-Origin': 'http://boris.com, http://localhost:8080', + 'Access-Control-Max-Age': '1296000' + } + } + const fn = serveHttp((req, res, params) => { + res.status(200).send(`The secret password of ${params.username} is ${params.password}`) + return res + }, appconfig) + return fn(req, res).then(() => { + assert.isOk(req) + assert.equal(res.statusCode, 200) + assert.equal(res._getData(), 'The secret password of nic is 1234') + }) + })))