Comparing version 0.2.0 to 0.3.0
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const dispatcher_1 = require("./dispatcher"); | ||
const http = require("http"); | ||
const util_1 = require("util"); | ||
const handlers_1 = require("./handlers"); | ||
const context_1 = require("./context"); | ||
const createDebugger = require("debug"); | ||
const aldo_http_1 = require("aldo-http"); | ||
const debug = createDebugger('aldo:application'); | ||
/** | ||
* A global facade to manage routes, error handlers, dispatching, etc... | ||
* | ||
*/ | ||
class Application { | ||
constructor() { | ||
this._routes = []; | ||
this._preHandlers = []; | ||
this._postHandlers = []; | ||
this._dispatcher = new dispatcher_1.default(_respond); | ||
this._context = Object.create(null); | ||
this._handlers = []; | ||
this._errorHandlers = []; | ||
this._context = new context_1.default(); | ||
} | ||
/** | ||
* Add before route handler | ||
* Use request handlers | ||
* | ||
* @param fns | ||
*/ | ||
pre(...fns) { | ||
use(...fns) { | ||
for (let fn of fns) { | ||
this._preHandlers.push(_ensureFunction(fn)); | ||
debug(`use pre handler: ${fn.name || '<anonymous>'}`); | ||
this._handlers.push(_ensureFunction(fn)); | ||
debug(`use handler: ${fn.name || '<anonymous>'}`); | ||
} | ||
@@ -31,21 +31,9 @@ return this; | ||
/** | ||
* Add after route handler | ||
* Use error handlers | ||
* | ||
* @param fns | ||
*/ | ||
post(...fns) { | ||
for (let fn of fns) { | ||
this._postHandlers.push(_ensureFunction(fn)); | ||
debug(`use post handler: ${fn.name || '<anonymous>'}`); | ||
} | ||
return this; | ||
} | ||
/** | ||
* Add an error handler | ||
* | ||
* @param fns | ||
*/ | ||
catch(...fns) { | ||
for (let fn of fns) { | ||
this._dispatcher.onError(_ensureFunction(fn)); | ||
this._errorHandlers.push(_ensureFunction(fn)); | ||
debug(`use error handler: ${fn.name || '<anonymous>'}`); | ||
@@ -56,52 +44,22 @@ } | ||
/** | ||
* Set the final request handler | ||
* | ||
* @param fn final request handler | ||
* Return a request callback | ||
*/ | ||
finally(fn) { | ||
this._dispatcher.onFinished(_ensureFunction(fn)); | ||
debug(`use final handler: ${fn.name || '<anonymous>'}`); | ||
return this; | ||
} | ||
/** | ||
* Register router's routes | ||
* | ||
* @param routers | ||
*/ | ||
use(...routers) { | ||
for (let router of routers) { | ||
this._routes.push(...router.routes()); | ||
debug('use routes'); | ||
} | ||
return this; | ||
} | ||
/** | ||
* Return a request handler callback | ||
*/ | ||
callback() { | ||
this._registerRoutes(); | ||
return (request, response) => { | ||
debug(`dispatching: ${request.method} ${request.url}`); | ||
this._dispatcher.dispatch(this.makeContext(request, response)); | ||
var handleError = handlers_1.compose(this._errorHandlers.length > 0 ? this._errorHandlers : [_report]); | ||
var dispatch = handlers_1.compose(this._handlers); | ||
return (req, res) => { | ||
var ctx = this._context.from(req, res); | ||
debug(`dispatching: ${req.method} ${req.url}`); | ||
dispatch(ctx).catch((err) => { | ||
// ensure `err` is an instance of `Error` | ||
if (!(err instanceof Error)) { | ||
err = new TypeError(util_1.format('non-error thrown: %j', err)); | ||
} | ||
// set the error | ||
ctx.error = err; | ||
return handleError(ctx); | ||
}); | ||
}; | ||
} | ||
async start(arg, options = {}) { | ||
var server = this._server = aldo_http_1.createServer(options, this.callback()); | ||
if (typeof arg === 'number') | ||
arg = { port: arg }; | ||
// listen | ||
await server.start(arg); | ||
debug(`app started with %o`, arg); | ||
return server; | ||
} | ||
/** | ||
* Stop listening for requests | ||
*/ | ||
async stop() { | ||
var server = this._server; | ||
await server.stop(); | ||
debug(`app stopped`); | ||
return server; | ||
} | ||
/** | ||
* Extend the app context by adding per instance property | ||
@@ -113,17 +71,4 @@ * | ||
bind(prop, fn) { | ||
_ensureFunction(fn); | ||
var field = `_${prop}`; | ||
Reflect.defineProperty(this._context, prop, { | ||
configurable: true, | ||
enumerable: true, | ||
get() { | ||
if (this[field] === undefined) { | ||
// private property | ||
Reflect.defineProperty(this, field, { | ||
value: fn(this) | ||
}); | ||
} | ||
return this[field]; | ||
} | ||
}); | ||
this._context.bind(prop, _ensureFunction(fn)); | ||
debug(`set a private context attribute: ${prop}`); | ||
return this; | ||
@@ -138,6 +83,4 @@ } | ||
set(prop, value) { | ||
if (typeof value === 'function') { | ||
return this.bind(prop, value); | ||
} | ||
this._context[prop] = value; | ||
this._context.set(prop, value); | ||
debug(`set a shared context attribute: ${prop}`); | ||
return this; | ||
@@ -151,3 +94,3 @@ } | ||
get(prop) { | ||
return this._context[prop]; | ||
return this._context.get(prop); | ||
} | ||
@@ -160,34 +103,12 @@ /** | ||
has(prop) { | ||
return prop in this._context; | ||
return this._context.has(prop); | ||
} | ||
/** | ||
* Create a new copy of the context object | ||
* Shorthand for: | ||
* | ||
* @param request | ||
* @param response | ||
* http.createServer(app.callback()).listen(...args) | ||
*/ | ||
makeContext(request, response) { | ||
var ctx = Object.create(this._context); | ||
ctx.response = response; | ||
ctx.request = request; | ||
ctx.params = {}; | ||
ctx.app = this; | ||
return ctx; | ||
listen() { | ||
return http.createServer(this.callback()).listen(...arguments); | ||
} | ||
/** | ||
* Construct the routes tree | ||
* | ||
* @private | ||
*/ | ||
_registerRoutes() { | ||
for (let route of this._routes) { | ||
for (let [method, fns] of route.handlers()) { | ||
fns = [...this._preHandlers, ...fns, ...this._postHandlers]; | ||
this._dispatcher.register(method, route.path, fns); | ||
} | ||
// TODO register named routes | ||
} | ||
// reset the handlers | ||
this._routes = []; | ||
} | ||
} | ||
@@ -204,6 +125,6 @@ exports.default = Application; | ||
return arg; | ||
throw new TypeError(`Function expected but ${typeof arg} given.`); | ||
throw new TypeError(`Function expected but got ${typeof arg}.`); | ||
} | ||
/** | ||
* Send the response | ||
* Send the error response | ||
* | ||
@@ -213,4 +134,6 @@ * @param ctx | ||
*/ | ||
function _respond(ctx) { | ||
ctx.response.send(); | ||
function _report({ error, res }) { | ||
res.statusCode = error.status || 500; | ||
res.end(error.message); | ||
console.error(error); | ||
} |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var route_1 = require("./route"); | ||
exports.Route = route_1.default; | ||
var router_1 = require("./router"); | ||
exports.Router = router_1.default; | ||
var application_1 = require("./application"); | ||
exports.Application = application_1.default; |
{ | ||
"name": "aldo", | ||
"version": "0.2.0", | ||
"description": "Fast web framework for Node.js", | ||
"version": "0.3.0", | ||
"description": "Fast micro framework for Node.js 8+", | ||
"main": "lib/index.js", | ||
"scripts": { | ||
"build": "tsc", | ||
"prepublishOnly": "tsc && npm test", | ||
"prepublishOnly": "npm run build", | ||
"test": "mocha -R dot -r ts-node/register \"test/**/*.ts\"" | ||
@@ -20,6 +20,6 @@ }, | ||
"keywords": [ | ||
"fast", | ||
"http", | ||
"web", | ||
"framework", | ||
"http", | ||
"fast" | ||
"framework" | ||
], | ||
@@ -33,3 +33,5 @@ "author": "absolux", | ||
"devDependencies": { | ||
"@types/debug": "0.0.30", | ||
"@types/mocha": "^2.2.48", | ||
"@types/node": "^8.9.5", | ||
"mocha": "^5.0.1", | ||
@@ -40,8 +42,4 @@ "ts-node": "^4.1.0", | ||
"dependencies": { | ||
"@types/debug": "0.0.30", | ||
"@types/node": "^8.9.2", | ||
"aldo-http": "^0.2.0", | ||
"debug": "^3.1.0", | ||
"find-my-way": "^1.10.1" | ||
"debug": "^3.1.0" | ||
} | ||
} |
180
README.md
> This project is under heavy development, and the API may change frequently. | ||
> This project is under heavy development, and the API is unstable and may change frequently. | ||
`Aldo` is yet another framework to build Node.js web applications. | ||
It uses the best parts of `Koa` and `Express` to provide a fast engine for your web projects. | ||
`Aldo` is a micro framework that helps you quickly write simple yet powerful web applications and APIs for Node.js 8+. | ||
It is super fast and has very little code. | ||
In fact, you can read and understand its source code in only an afternoon! | ||
## Installation | ||
```bash | ||
npm add aldo | ||
``` | ||
At its core, `Aldo` is a dispatcher that invokes a serial callback stack to handle the incoming HTTP request. That’s it. | ||
## Testing | ||
```bash | ||
npm test | ||
``` | ||
## Hello world! | ||
```js | ||
// import the needed parts | ||
const { Application, Router } = require('aldo') | ||
const { Application } = require('aldo') | ||
const router = new Router() | ||
const app = new Application() | ||
// we define a `hello world` route | ||
router.get('/', () => 'Hello world!') | ||
// add the first handler | ||
app.use(({ res }) => { | ||
res.setHeader('Content-Type', 'text/plain') | ||
res.end('Hello world!') | ||
}) | ||
// we add the router to be used by our application | ||
app.use(router) | ||
// we serve the application, | ||
// which creates and uses an internal HTTP server | ||
app.start(3000) | ||
// create a HTTP server to serve the application | ||
app.listen(3000) | ||
``` | ||
## Request Flow | ||
The request handling logic is similar to the `try..catch..finally` JavaScript block. | ||
In other words, the application will try to call the route handlers one by one to the final handler. | ||
If an error occurs, it will be handled by the error handlers before reaching the final handler which will terminate and send the response to the client. | ||
## Handlers (aka middlewares) | ||
Unlike the other web frameworks, `Aldo` uses handlers that take only **one** argument "the [*context*](#context)", which is a literal object holding everything you need to handle the incoming HTTP request. | ||
You can use `.pre`, `.post`, `.use`, `.catch` and `.finally` application's methods to control the flow of request handling. | ||
When the current handler has finished its job, the [context](#context) object is passed automatically to the next handler, and so on, till the last one. | ||
```js | ||
const app = new Application() | ||
To break this chain, just return `false` or `Promise<false>`. | ||
// 1. The `try` block | ||
```ts | ||
// Handler function signature | ||
declare type Handler = (ctx: Context) => any; | ||
``` | ||
// attach one or more global handlers before the route | ||
// useful to configure global services like session, cache ...etc | ||
app.pre(...handlers) | ||
You may be wondering, where is the `next` argument ? | ||
`Aldo` doesn't provide it, only the [context](#context) is given. | ||
The `next` function is invoked internally after the current handler has finished. | ||
// use one or many routers with `.use` | ||
app.use(...routers) | ||
// ... etc | ||
```js | ||
// the `next` function is called immediately after a sync handler | ||
app.use(({ req: { url, method } }) => { | ||
console.log(`incoming request: ${method} ${url}`) | ||
}) | ||
// attach global handlers to be executed after the route handlers | ||
// like saving a cached version, persisting session data, setting more headers ...etc | ||
app.post(...handlers) | ||
// the `next` function is awaiting async functions | ||
app.use(async (ctx) => { | ||
await fetch('some data from the DB') | ||
}) | ||
// the `next` function is also awaiting sync functions returning promises | ||
app.use((ctx) => { | ||
return fetch('some data from the DB') | ||
}) | ||
``` | ||
// 2. The `catch` block | ||
### Request handlers | ||
Request handlers are invoked for every incoming HTTP request. | ||
You may add handlers with the application instance’s `.use(...fns)` method. | ||
// attaching error handlers is done as below | ||
app.catch(...handlers) | ||
```js | ||
// will attach one or more global handlers | ||
app.use(...handlers) | ||
``` | ||
### Error handlers | ||
You may attach error handlers as many as needed with `.catch(...fns)`. | ||
// 3. The `finally` block | ||
// at last, only one final handler is used | ||
// the default one is simply sending the response | ||
app.finally(finalHandler) | ||
```js | ||
app.catch(...handlers) | ||
``` | ||
> Since each method controls a processing step, the order doesn't matter any more. | ||
> We can define routes before or after the final handler, it won't create any issue, since the routes are only compiled during the application launch. | ||
When an error is thrown, during the request handling, will be attached to the [context](#context) object and passed to each handler one by one, unless you return a `false` which will stop the error handling sequence. | ||
Unlike the other web frameworks, each `handler` takes only **one** argument: the **`context`**. This object holds everything you need to handle the incoming HTTP request. | ||
Handlers may return *void* or a promise for asynchronous operations. | ||
## Context | ||
The context object is not a proxy to the request and response properties, it's a simple plain object with only 2 mandatory properties: `req`, `res`, which are the `IncomingMessage` and the `ServerResponse` HTTP native objects. | ||
So, when the current handler finished its job, the *context* object is passed automatically to the next handler, and so on, till the final handler which terminates and ends the response. | ||
To break the chain, throw an error, to get the first error handler called instead of the next middleware. | ||
The error handlers receive the same object, but with an additonal `error` property. | ||
```ts | ||
// Handler function signature | ||
declare type Handler = (ctx: Context) => any | ||
declare type Literal = { [x: string]: any; }; | ||
declare interface Context extends Literal { | ||
req: IncomingMessage; | ||
res: ServerResponse; | ||
error?: any; | ||
} | ||
``` | ||
## Context | ||
The context object is not a proxy to the request and response properties, it's a simple plain object with only 4 mandatory properties `app`, `request`, `response` and route `params`. | ||
Even the error handlers have the same signature, but with an additonal `error` property. | ||
### `.set(key, value)` | ||
To extend the request context, and add shared properties, like a DB connection, or a global logger, you may use `.set()` | ||
```ts | ||
declare type Literal = { [x: string]: any } | ||
```js | ||
const mongoose = require('mongoose') | ||
declare interface Context extends Literal { | ||
response: Response; // Response object provided by the package `aldo-http` | ||
request: Request; // Request object provided by the package `aldo-http` | ||
app: Application; // Application instance | ||
params: Literal; // Route parameters | ||
error?: any; // The error | ||
} | ||
await mongoose.connect('mongodb://localhost/test') | ||
app.set('db', mongoose) | ||
``` | ||
To extend the request context, and add more properties, you can use `app.set(key, value)` or `app.bind(key, factory)` | ||
### `.bind(key, fn)` | ||
To set a per request private properties, you may use `.bind()`. This method takes a field name, and a function to be used as a `lazy` getter of the field value. | ||
```js | ||
// set a global value to be available for all requests | ||
app.set('mongo', dbConnection) | ||
// set a per request property using a function to lazily get the value | ||
// This time, each context instance has a distinct `session` property | ||
app.bind('session', () => new Session(options)) | ||
``` | ||
`app.has(key)` and `app.get(key)` are also available to check the existence of a certain field, or to get a previously defined property. | ||
This method is very useful, since it allows you to lazily (only when needed) attach a per request property into the context without adding a dedicated handler. | ||
## Router | ||
Each `router` instance control an erea in the application, it acts like a route namespace. | ||
You can use as many routers as you need. For example, a router to manage authentication, another for the API, a private router for admin routing, and so on. | ||
### `.has(key)` | ||
You may use it to check the existence of a certain field | ||
> The order of defining routes is not important any more. Thanks to [find-my-way](https://npmjs.com/find-my-way) witch is a [radix tree](https://en.wikipedia.org/wiki/Radix_tree). | ||
### `.get(key)` | ||
You may use it to get a previously defined field value. | ||
```js | ||
const { Router } = require('aldo') | ||
const { Users } = require('./controllers') | ||
## Contributing | ||
You have a lot of ways to help the project grow. | ||
// Let's create an admin area router | ||
// `/admin` is a URL prefix for all routes | ||
const router = new Router('/admin') | ||
- Report an [issue](https://github.com/aldojs/aldo/issues) | ||
- Propose a [feature](https://github.com/aldojs/aldo/issues) | ||
- Send a [pull request](https://github.com/aldojs/aldo/pulls) | ||
- Star the project on [GitHub](https://github.com/aldojs/aldo) | ||
- Tell about project around your community | ||
// define a single handler for admin home page for the GET method | ||
router.get('/', Users.home) | ||
All contributions are appreciated, even the smallest. Thank you! | ||
// we can define multiple handlers per HTTP method for the same route | ||
router | ||
.route('/Users') | ||
.get(Users.show) | ||
.delete(Users.delete) | ||
.post(Users.validate, Users.add) | ||
.put(Users.validate, Users.modify) | ||
``` | ||
## License | ||
[MIT License](https://opensource.org/licenses/MIT) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Deprecated
MaintenanceThe maintainer of the package marked it as deprecated. This could indicate that a single version should not be used, or that the package is no longer maintained and any new vulnerabilities will not be fixed.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
1
0
13940
6
8
324
130
2
- Removed@types/debug@0.0.30
- Removed@types/node@^8.9.2
- Removedaldo-http@^0.2.0
- Removedfind-my-way@^1.10.1
- Removed@types/debug@0.0.30(transitive)
- Removed@types/node@8.10.66(transitive)
- Removedaldo-http@0.2.2(transitive)
- Removedcookie@0.3.1(transitive)
- Removedfast-decode-uri-component@1.0.1(transitive)
- Removedfind-my-way@1.18.1(transitive)
- Removedmedia-typer@0.3.0(transitive)
- Removedmime-db@1.52.0(transitive)
- Removedmime-types@2.1.35(transitive)
- Removedparseurl@1.3.3(transitive)
- Removedret@0.1.15(transitive)
- Removedsafe-regex@1.1.0(transitive)
- Removedsemver-store@0.3.0(transitive)
- Removedstatuses@1.5.0(transitive)
- Removedtype-is@1.6.18(transitive)