Comparing version 0.13.0-rc2 to 0.13.0
@@ -30,12 +30,2 @@ const assert = require('assert'); | ||
function normalizeHandler(func){ | ||
if(func.constructor.name === 'AsyncFunction') | ||
return func; | ||
return input => new Promise((resolve, reject) => { | ||
try{ func(input); resolve(); } | ||
catch(err){ reject(err); } | ||
}); | ||
} | ||
function matchRoute(method, path, params){ | ||
@@ -94,3 +84,3 @@ // this => API | ||
this.fallbackRoute = normalizeHandler(handler.bind(this.context)); | ||
this.fallbackRoute = handler.bind(this.context); | ||
}; | ||
@@ -112,3 +102,3 @@ | ||
const nmHandler = normalizeHandler(handler.bind(this.context)); | ||
const nmHandler = handler.bind(this.context); | ||
@@ -132,5 +122,4 @@ this.routes[route] = true; | ||
input = { | ||
...app.global, conf: app.conf, cookies: {}, headers: {}, | ||
query: {}, ...input, params, log: app.log, signedCookies: {}, method, | ||
path | ||
...app.global, conf: app.conf, cookies: {}, headers: {}, query: {}, | ||
...input, params, log: app.log, signedCookies: {}, method, path | ||
}; | ||
@@ -158,21 +147,16 @@ | ||
if(!handler){ | ||
res.status(404).end(); | ||
return res.ended; | ||
} | ||
try{ | ||
res.notFound(!handler); | ||
input.body = new RequestBody(input); | ||
if(app._autoParseBody && !input.websocket) | ||
try{ | ||
input.body = new RequestBody(input); | ||
if(app._autoParseBody && !input.websocket) | ||
input.body = await input.body.parse(); | ||
} | ||
catch(err){ | ||
res.status(400).end(); | ||
app.log.warn({ ...reqInfo, err }); | ||
return res.ended; | ||
} | ||
parseSignedCookies(app.conf.cookie, input); | ||
parseSignedCookies(app.conf.cookie, input); | ||
handler(input).catch(err => handleError(err, input)); | ||
await handler(input); | ||
} | ||
catch(err){ | ||
handleError(err, input); | ||
} | ||
@@ -179,0 +163,0 @@ return res.ended; |
@@ -5,20 +5,23 @@ | ||
function readStream(){ | ||
var complete, buffer = []; | ||
let complete, buffer = []; | ||
this.stream.on('data', chunk => buffer.push(chunk)); | ||
const fbto = setTimeout(() => { | ||
this.stream.emit('error', new Error('Client took too long before starting to send request body'), 408) | ||
}, 3000); | ||
this.stream.on('data', chunk => { | ||
clearTimeout(fbto); | ||
buffer.push(chunk); | ||
}); | ||
this.stream.on('close', () => { | ||
buffer = null; | ||
this.stream.removeAllListeners('aborted'); | ||
this.stream.removeAllListeners('data'); | ||
this.stream.removeAllListeners('end'); | ||
this.stream.removeAllListeners('error'); | ||
this.stream.removeAllListeners('close'); | ||
this.stream.removeAllListeners(); | ||
}); | ||
this.stream.on('aborted', () => | ||
this.stream.emit('error', new Error('Request aborted by the client'))); | ||
return new Promise((resolve, reject) => { | ||
this.stream.on('aborted', () => | ||
this.stream.emit('error', new Error('Request aborted by the client'))); | ||
this.stream.on('end', () => { | ||
@@ -29,4 +32,4 @@ !complete && resolve(Buffer.concat(buffer)); | ||
this.stream.on('error', err => { | ||
!complete && reject(err); | ||
this.stream.on('error', (err, code) => { | ||
!complete && reject(this.res.error(code ?? 400, err.message)); | ||
complete = true; | ||
@@ -38,7 +41,11 @@ }); | ||
async function read(parse){ | ||
let out = await readStream.call(this); | ||
if(typeof parse == 'function') | ||
out = parse(out); | ||
try{ | ||
if(typeof parse == 'function') | ||
out = parse(out); | ||
} | ||
catch(err){ | ||
throw this.res.error(400, 'Invalid format'); | ||
} | ||
@@ -45,0 +52,0 @@ return out; |
import { Server } from 'http'; | ||
import WebSocket from 'ws'; | ||
@@ -124,6 +125,8 @@ declare namespace Nodecaf { | ||
params: Record<string, string> | ||
/** The remote address of the client performing the request. Standard proxy headers are considered.*/ | ||
/** The remote address of the client performing the request. Standard proxy headers are considered. */ | ||
ip: string, | ||
/** Store `value` under the name `key` in the handler args for the lifetime of the request.*/ | ||
keep: (key: string, value: unknown) => void | ||
/** Store `value` under the name `key` in the handler args for the lifetime of the request. */ | ||
keep: (key: string, value: unknown) => void, | ||
/** Accept WebSocket connection on upgrade. Only available when `opts.websocket` is set. */ | ||
websocket: () => Promise<WebSocket.WebSocket> | ||
} & Record<string, unknown>; | ||
@@ -142,3 +145,14 @@ | ||
type Route = { | ||
/** Endpoint HTTP method */ | ||
method: string, | ||
/** Endpoint path starting with slash (e.g `/foo/:bar`) */ | ||
path: string, | ||
/** Function to be called when endpoint is triggered */ | ||
handler: RouteHandlerArgs | ||
}; | ||
type AppOpts = { | ||
/** An array with your api endpoints */ | ||
routes: Route[], | ||
/** A function to build your api endpoints */ | ||
@@ -156,7 +170,10 @@ api?: (this: Nodecaf, methods: Nodecaf.EndpointBuilders) => void, | ||
conf?: Nodecaf.ConfObject | string, | ||
/** whether request bodies should be parsed for known mime-types (json, text, urlencoded) */ | ||
/** Whether request bodies should be parsed for known mime-types (json, text, urlencoded). Defaults to `false`. */ | ||
autoParseBody?: boolean, | ||
/** A function that returns a custom HTTP server to be used by the app */ | ||
server?: (args: Nodecaf) => Server | ||
server?: (args: Nodecaf) => Server, | ||
/** Whether to handle websocket upgrade requests. Defaults to `false`. */ | ||
websocket?: boolean | ||
} | ||
} | ||
@@ -184,2 +201,15 @@ | ||
/** Define a POST endpoint to `path` that when triggered will run the `handler` function */ | ||
static post: (path: string, handler: Nodecaf.RouteHandler) => Nodecaf.Route | ||
/** Define a PUT endpoint to `path` that when triggered will run the `handler` function */ | ||
static put: (path: string, handler: Nodecaf.RouteHandler) => Nodecaf.Route | ||
/** Define a PATCH endpoint to `path` that when triggered will run the `handler` function */ | ||
static patch: (path: string, handler: Nodecaf.RouteHandler) => Nodecaf.Route | ||
/** Define a GET endpoint to `path` that when triggered will run the `handler` function */ | ||
static get: (path: string, handler: Nodecaf.RouteHandler) => Nodecaf.Route | ||
/** Define a DELETE endpoint to `path` that when triggered will run the `handler` function */ | ||
static del: (path: string, handler: Nodecaf.RouteHandler) => Nodecaf.Route | ||
/** Define a fallback `handler` function to be triggered when there are no matching routes */ | ||
static all: (handler: Nodecaf.RouteHandler) => Nodecaf.Route | ||
/** A user controlled object whose properties wil be spread in route handler args. */ | ||
@@ -186,0 +216,0 @@ global: Record<string, unknown> |
@@ -33,2 +33,3 @@ const | ||
this._apiSpec = opts.api; | ||
this._routes = opts.routes; | ||
this._startup = opts.startup; | ||
@@ -70,2 +71,3 @@ this._shutdown = opts.shutdown; | ||
this._api = new API(this, this._apiSpec); | ||
opts.routes?.forEach(r => this._api.addEndpoint(r.method.toLowerCase(), r.path, r.handler)); | ||
} | ||
@@ -126,7 +128,5 @@ | ||
if(this._wss) | ||
this._wss.close(); | ||
let actualHTTPClose | ||
if(this._server) | ||
var actualHTTPClose = new Promise(done => this._server.close(done)); | ||
actualHTTPClose = new Promise(done => this._server.close(done)); | ||
@@ -198,1 +198,5 @@ // Handle exceptions in user code to maintain proper app state | ||
} | ||
http.METHODS.forEach(m => module.exports[m.toLowerCase()] = function(path, handler, opts){ | ||
return { ...opts, method: m, path, handler }; | ||
}); |
@@ -44,2 +44,6 @@ const { sign } = require('cookie-signature'); | ||
end(body){ | ||
if(this.finished) | ||
this.log.warn({ err: new Error('Called `res.end()` after response was already finished') }); | ||
body && this.write(body); | ||
@@ -46,0 +50,0 @@ this.resStream.end(); |
@@ -27,7 +27,3 @@ const cookie = require('cookie'); | ||
// TODO MISSING THE RESPONSE OBJECT | ||
const res = new ServerResponse(req); | ||
const [ path, query ] = req.url.split('?'); | ||
@@ -47,5 +43,5 @@ | ||
wss.on('close', function() { | ||
app._server.on('close', function() { | ||
clearInterval(interval); | ||
this.clients.forEach(ws => ws.terminate()); | ||
wss.clients.forEach(ws => ws.terminate()); | ||
}); | ||
@@ -58,3 +54,2 @@ | ||
module.exports = { buildWebSocketServer }; |
{ | ||
"name": "nodecaf", | ||
"version": "0.13.0-rc2", | ||
"version": "0.13.0", | ||
"description": "Nodecaf is a light framework for developing RESTful Apps in a quick and convenient manner.", | ||
@@ -45,3 +45,4 @@ "main": "lib/main.js", | ||
"cors": "^2.8.5", | ||
"golog": "^0.5.0" | ||
"golog": "^0.5.0", | ||
"ws": "^8.6.0" | ||
}, | ||
@@ -48,0 +49,0 @@ "devDependencies": { |
# [Nodecaf](https://gitlab.com/GCSBOSS/nodecaf) | ||
> Docs for version v0.12.x. | ||
> Docs for version v0.13.x | ||
Nodecaf is a light framework for developing RESTful Apps in a quick and convenient manner. | ||
Using Nodecaf you'll get: | ||
- Familiar middleware style routes declaration | ||
- Useful [handler arguments](#handlers-args). | ||
- Built-in [settings file support](#settings-file) with layering. | ||
- [Logging functions](#logging). | ||
- Seamless support for [async functions as route handlers](#async-handlers). | ||
## Highlights | ||
- URL path pattern routing | ||
- Each route accepts a single function as an argument ([see why](#simple-routing)) | ||
- Useful [handler arguments](#handlers-args) | ||
- Optional automatic body parsing for popular formats | ||
- Support for [Settings files](#settings-file) or objects with straightforward layering | ||
- [Stdout logging](#logging) | ||
- Seamless support for [async functions as route handlers](#async-handlers) | ||
- [Uncaught exceptions](#error-handling) in routes always produce proper REST | ||
responses. | ||
- Built-in [assertions for readable RESTful error handling](#rest-assertions). | ||
- Function to [expose global objects](#expose-globals) to all routes (eg.: | ||
database connections). | ||
- Shortcut for [CORS Settings](#cors) on all routes. | ||
- Functions to [describe your API](#api-description) making your code the main | ||
source of truth. | ||
- Helpful [command line interface](https://gitlab.com/GCSBOSS/nodecaf-cli). | ||
responses | ||
- [Assertions for readable RESTful error handling](#rest-assertions) | ||
- Facility for [exposing global objects](#expose-globals) to all routes (eg.: | ||
database connections) | ||
- [CORS Settings](#cors) | ||
- Allow calling all endpoints programmatically with complete feature parity (awesome for unit testing) | ||
- Helper to [handle WebSocket](#handling-websocket) connections. | ||
- Helpful [command line interface](https://gitlab.com/GCSBOSS/nodecaf-cli) | ||
@@ -57,5 +60,5 @@ ## Get Started | ||
// Define routes and a list of middleware functions (async or regular no matter). | ||
get('/foo/:f/bar/:b', Foo.read, Bar.read); | ||
post('/foo/:f/bar', Foo.read, Bar.write); | ||
// Define routes with their handler functions (async or regular no matter). | ||
get('/foo/:f/bar/:b', FooBar.read); | ||
post('/foo/:f/bar', FooBar.write); | ||
// ... | ||
@@ -167,3 +170,3 @@ | ||
```js | ||
function({ method, path, res, query, params, body, conf, log, headers, call }){ | ||
function({ method, path, res, query, params, body, conf, log, headers, call, websocket }){ | ||
// Do your stuff. | ||
@@ -278,2 +281,3 @@ } | ||
| app | info | The application configuration has been reloaded | | ||
| event | warn | Called `res.end()` after response was already finished | | ||
@@ -296,6 +300,5 @@ Additionally, you can filter log entries by level and type with the following | ||
Nodecaf brings the useful feature of accepting async functions as route handlers | ||
with zero configuration. All rejections/error within your async handler will be | ||
gracefully handled by the same routine the deals with regular functions. You will | ||
be able to avoid callback hell without creating bogus adapters for your promises. | ||
Nodecaf accepts async functions as well as regular functions as route handlers. | ||
All rejections/error within your async handler will be gracefully handled. | ||
You will be able to avoid callback hell without creating bogus adapters for your promises. | ||
@@ -418,47 +421,19 @@ ```js | ||
### API Description | ||
### Handling Websocket | ||
Nodecaf allows you to descibe your api and it's functionality, effectively turning | ||
your code in the single source of truth. The described API can later be used to | ||
[generate](https://gitlab.com/GCSBOSS/nodecaf-cli#open-api-support) an | ||
[Open API](https://www.openapis.org/) compatible | ||
document. | ||
Use the `websocket` handler argument to expect a Websocket upgrade. | ||
In `lib/api.js` describe your API as whole through the `info` parameter: | ||
```js | ||
module.exports = function({ get, info }){ | ||
get('/my/ws/endpoint', async ({ websocket }) => { | ||
info({ | ||
description: 'My awesome API that foos the bars and bazes the bahs' | ||
}); | ||
// Wait till ws connection is open | ||
const ws = await websocket(); | ||
get('/my/thing/:id', function(){ | ||
// ... | ||
ws.on('message', m => { | ||
ws.send('Hello World!'); | ||
ws.close(); | ||
}); | ||
} | ||
}) | ||
``` | ||
The `info` funciton expects an object argument on the OpenAPI | ||
[Info Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#infoObject) | ||
format. If not defined the `title` and `version` keys will default to your server's. | ||
Describe your API endpoints by chaining a `desc` method to each route definition. | ||
```js | ||
module.exports = function({ get }){ | ||
get('/my/thing/:id', function(){ | ||
// ... | ||
}).desc('Retrieves a thing from the database\n' + | ||
`Searches the database for the thing with the given :id. Returns a | ||
NotFound error in case no thing is found.`); | ||
} | ||
``` | ||
The `desc` method takes a single string argument and uses it's first line (before `\n`) | ||
to set the | ||
[Operation object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operationObject)'s | ||
`summary` property and the rest of the text to set the `description` (CommonMark). | ||
### Other Settings | ||
@@ -465,0 +440,0 @@ |
Sorry, the diff of this file is not supported yet
847
50374
6
444