@alterior/core
Advanced tools
Comparing version 0.0.1 to 0.0.3
@@ -0,1 +1,5 @@ | ||
/** | ||
* Abstracts the fetching of the application's arguments. This is most useful for testing, | ||
* but could also be used if trying to host Alterior in a strange environment. | ||
*/ | ||
export class ApplicationArgs { | ||
@@ -2,0 +6,0 @@ public get() { |
import 'reflect-metadata'; | ||
const getParameterNames = require('@avejidah/get-parameter-names'); | ||
import { AppOptions, ApplicationOptions, ApplicationInstance } from './application'; | ||
import { controllerClasses } from './controller'; | ||
import { Middleware, prepareMiddleware } from './middleware'; | ||
import { RouteReflector } from './route'; | ||
import { RouteReflector, RouteEvent } from './route'; | ||
import { ExpressRef } from './express'; | ||
@@ -150,12 +152,71 @@ import { HttpException } from './errors'; | ||
let routeParams = route.path.match(/:([A-Za-z][A-Za-z0-9]*)/); | ||
// Do analysis of the controller method ahead of time so we can | ||
// minimize the amount of overhead of actual web requests | ||
let returnType = Reflect.getMetadata("design:returntype", controllerInstance.constructor.prototype, route.method); | ||
let paramTypes = Reflect.getMetadata("design:paramtypes", controllerInstance.constructor.prototype, route.method); | ||
let paramNames = getParameterNames(controllerInstance[route.method]); | ||
let paramFactories = []; | ||
if (paramTypes) { | ||
for (let i = 0, max = paramNames.length; i < max; ++i) { | ||
let paramName = paramNames[i]; | ||
let paramType = paramTypes[i]; | ||
let simpleTypes = [String, Number]; | ||
if (paramType === RouteEvent) { | ||
paramFactories.push(ev => ev); | ||
} else if (paramName === "body") { | ||
paramFactories.push((ev : RouteEvent) => ev.request['body']); | ||
} else if (paramName === "req" || paramName === "request") { | ||
paramFactories.push((ev : RouteEvent) => ev.request); | ||
} else if (paramName === "res" || paramName === "response") { | ||
paramFactories.push((ev : RouteEvent) => ev.response); | ||
} else if (routeParams.find(x => x == paramName) && simpleTypes.indexOf(paramType) >= 0) { | ||
// This is a route parameter binding. | ||
paramFactories.push((ev : RouteEvent) => ev.request.params[paramName]); | ||
} else { | ||
throw new Error(`Unable to provider parameter ${paramName} of type ${paramType.name}`); | ||
} | ||
} | ||
} else { | ||
paramFactories = [ | ||
(ev : RouteEvent) => ev.request, | ||
(ev : RouteEvent) => ev.response | ||
]; | ||
} | ||
// Append the actual controller method | ||
args.push((req, res) => { | ||
if (!silent) | ||
console.log(`[${new Date().toLocaleString()}] ${route.path} => ${controller.name}.${route.method}()`); | ||
let result = controllerInstance[route.method](req, res); | ||
// Execute our function by resolving the parameter factories into a set of parameters to provide to the | ||
// function. | ||
if (!result) | ||
let ev = new RouteEvent(req, res); | ||
let result; | ||
try { | ||
result = controllerInstance[route.method].apply(controllerInstance, paramFactories.map(x => x(ev))); | ||
} catch (e) { | ||
if (e.constructor === HttpException) { | ||
let httpException = <HttpException>e; | ||
res.status(httpException.statusCode).send(httpException.body); | ||
} else { | ||
res.status(500).send(JSON.stringify({ | ||
message: 'Failed to resolve this resource.', | ||
error: e | ||
})); | ||
} | ||
} | ||
// Return value handling | ||
if (result === undefined) | ||
return; | ||
@@ -181,2 +242,6 @@ | ||
}); | ||
} else if (result.constructor === String) { | ||
res.status(200).send(result); | ||
} else { | ||
res.status(200).header('Content-Type', 'application/json').send(JSON.stringify(result)); | ||
} | ||
@@ -183,0 +248,0 @@ |
import { controllerClasses, Controller as _Controller } from './controller'; | ||
import { Get, Post, Put, Patch, Delete, Options } from './route'; | ||
import { Get, Post, Put, Patch, Delete, Options, RouteEvent } from './route'; | ||
import { suite, test as it } from 'mocha-typescript'; | ||
@@ -7,2 +7,4 @@ import * as assert from 'assert'; | ||
import * as http from 'http'; | ||
import * as bodyParser from 'body-parser'; | ||
import { HttpException } from './errors'; | ||
@@ -131,2 +133,36 @@ | ||
@it 'should allow a method to return an explicit body value' (done) { | ||
@_Controller() | ||
class TestController { | ||
@Get('/foo') | ||
getX(req : express.Request, res : express.Response) { | ||
return {foo:"we promised"}; | ||
} | ||
} | ||
@AppOptions({ port: 10001, silent: true, | ||
autoRegisterControllers: false, | ||
controllers: [TestController], | ||
middleware: [ | ||
(req, res, next) => { res.header('Content-Type', 'application/json'); next(); } | ||
] | ||
}) | ||
class FakeApp { | ||
} | ||
bootstrap(FakeApp).then(app => { | ||
supertest(app.express) | ||
.get('/foo') | ||
.expect(200, <any>{ foo: "we promised" }) | ||
.end((err, res) => { | ||
app.stop(); | ||
if (err) | ||
return done(err); | ||
done(); | ||
}); | ||
}); | ||
} | ||
@it 'should 500 when a method returns a promise that rejects' (done) { | ||
@@ -200,2 +236,129 @@ | ||
@it 'should be reading parameter type metadata to discover how to provide parameters' (done) { | ||
@_Controller() | ||
class TestController { | ||
@Get('/foo') | ||
getX(res : express.Response, req : express.Request) { // note they are swapped | ||
assert(res.send); | ||
assert(req.path); | ||
return Promise.resolve({ok: true}); | ||
} | ||
} | ||
@AppOptions({ port: 10001, silent: true, | ||
autoRegisterControllers: false, | ||
controllers: [TestController], | ||
middleware: [ | ||
(req, res, next) => { res.header('Content-Type', 'application/json'); next(); } | ||
] | ||
}) | ||
class FakeApp { | ||
} | ||
bootstrap(FakeApp).then(app => { | ||
supertest(app.express) | ||
.get('/foo') | ||
.expect(200, <any>{ | ||
ok: true | ||
}) | ||
.end((err, res) => { | ||
app.stop(); | ||
if (err) | ||
return done(err); | ||
done(); | ||
}); | ||
}); | ||
} | ||
@it 'should be able to inject body when the body parsing middleware is used' (done) { | ||
interface MyRequestType { | ||
zoom : number; | ||
} | ||
@_Controller() | ||
class TestController { | ||
@Post('/foo') | ||
getX(body : MyRequestType) { | ||
assert(body.zoom === 123); | ||
return Promise.resolve({ok: true}); | ||
} | ||
} | ||
@AppOptions({ port: 10001, silent: true, | ||
autoRegisterControllers: false, | ||
controllers: [TestController], | ||
middleware: [ | ||
bodyParser.json(), | ||
(req, res, next) => { res.header('Content-Type', 'application/json'); next(); } | ||
] | ||
}) | ||
class FakeApp { | ||
} | ||
bootstrap(FakeApp).then(app => { | ||
supertest(app.express) | ||
.post('/foo') | ||
.send({ | ||
zoom: 123 | ||
}) | ||
.expect(200, <any>{ | ||
ok: true | ||
}) | ||
.end((err, res) => { | ||
app.stop(); | ||
if (err) | ||
return done(err); | ||
done(); | ||
}); | ||
}); | ||
} | ||
@it 'should be able to inject RouteEvent instead of request/response' (done) { | ||
interface MyRequestType { | ||
zoom : number; | ||
} | ||
@_Controller() | ||
class TestController { | ||
@Post('/foo') | ||
getX(ev : RouteEvent) { | ||
assert(ev.request.path); | ||
assert(ev.response.send); | ||
return Promise.resolve({ok: true}); | ||
} | ||
} | ||
@AppOptions({ port: 10001, silent: true, | ||
autoRegisterControllers: false, | ||
controllers: [TestController], | ||
middleware: [ | ||
bodyParser.json(), | ||
(req, res, next) => { res.header('Content-Type', 'application/json'); next(); } | ||
] | ||
}) | ||
class FakeApp { | ||
} | ||
bootstrap(FakeApp).then(app => { | ||
supertest(app.express) | ||
.post('/foo') | ||
.send({ | ||
zoom: 123 | ||
}) | ||
.expect(200, <any>{ | ||
ok: true | ||
}) | ||
.end((err, res) => { | ||
app.stop(); | ||
if (err) | ||
return done(err); | ||
done(); | ||
}); | ||
}); | ||
} | ||
@it 'should support POST' (done) { | ||
@@ -202,0 +365,0 @@ |
@@ -0,1 +1,2 @@ | ||
import * as express from 'express'; | ||
@@ -17,2 +18,12 @@ export class RouteReflector { | ||
export class RouteEvent { | ||
constructor(request : express.Request, response : express.Response) { | ||
this.request = request; | ||
this.response = response; | ||
} | ||
request : express.Request; | ||
response : express.Response; | ||
} | ||
export interface RouteOptions { | ||
@@ -19,0 +30,0 @@ middleware?: Function[]; |
{ | ||
"name": "@alterior/core", | ||
"version": "0.0.1", | ||
"version": "0.0.3", | ||
"private": false, | ||
@@ -9,4 +9,4 @@ "description": "An Express-based Typescript MVC framework using decorators and Angular 2 dependency injection.", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/rezonant/alterior-core.git" | ||
"type": "git", | ||
"url": "https://github.com/alterior-mvc/alterior.git" | ||
}, | ||
@@ -23,2 +23,3 @@ "scripts": { | ||
"@angular/core": "^2.0.0", | ||
"@avejidah/get-parameter-names": "^0.3.2", | ||
"body-parser": "^1.15.2", | ||
@@ -25,0 +26,0 @@ "connect-mongo": "^1.3.2", |
# Alterior | ||
[![Build status on Travis CI](https://travis-ci.org/rezonant/alterior-core.svg?branch=master)](https://travis-ci.org/rezonant/alterior-core) | ||
[![Build status on Travis CI](https://travis-ci.org/alterior-mvc/alterior.svg?branch=master)](https://travis-ci.org/alterior-mvc/alterior) | ||
[![Join the chat at https://gitter.im/alterior-mvc/Lobby](https://badges.gitter.im/alterior-core/Lobby.svg)](https://gitter.im/alterior-mvc/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) | ||
An Express-based Typescript MVC framework using decorators and Angular 2 dependency injection. | ||
An Express-based Typescript MVC framework using decorators and Angular 2 dependency injection. | ||
@@ -36,3 +36,3 @@ ## Getting started | ||
```typescript | ||
import { Controller, Get } from '@alterior/core'; | ||
import { Controller, Get, RouteEvent } from '@alterior/core'; | ||
import * as express from 'express'; | ||
@@ -43,3 +43,3 @@ | ||
@Get('/foo') | ||
public foo(req : express.Request, res : express.Response) | ||
public foo(ev : RouteEvent) | ||
{ | ||
@@ -50,3 +50,6 @@ res.status(200).send("/foo works!"); | ||
/** | ||
* You can also return promises. | ||
* You can also return promises, or | ||
* request the Express request/response explicitly (note that this is | ||
* based on the parameter name, see below for more information about | ||
* route method parameters. | ||
*/ | ||
@@ -59,10 +62,15 @@ @Get('/bar') | ||
/** | ||
* Those promises can reject | ||
*/ | ||
@Get('/error') | ||
public errorExample(req : express.Request, res : express.Response) | ||
{ | ||
// Promises can reject | ||
return Promise.reject(new HttpException(301, {message: "No, over there"})); | ||
} | ||
@Get('/error') | ||
public errorExample(req : express.Request, res : express.Response) | ||
{ | ||
// Values are OK too | ||
return { nifty: 123}; | ||
} | ||
} | ||
@@ -74,2 +82,3 @@ ``` | ||
When using automatic discovery, the simplest way to ensure a controller gets loaded is with: | ||
```typescript | ||
@@ -94,2 +103,48 @@ import "foo"; | ||
## Route Parameters | ||
Alterior inspects the parameters of controller methods to determine what values to provide. Firstly, parameters of type `RouteEvent` will be provided with an instance of that class which | ||
contains the Express request and response objects. | ||
```typescript | ||
@Get('/do') | ||
doThings(ev : RouteEvent) { | ||
ev.response.status(404).send("Not found."); | ||
} | ||
``` | ||
Alternatively, parameters which are named `request` or `req` will also be fulfilled with the Express request. Likewise, `response` or `res` | ||
can be used to get the response object. Note that using `RouteEvent` is preferred and recommended since it is a type-based rule. | ||
```typescript | ||
@Get('/do') | ||
doThings(req : express.Request, res : express.Response) { | ||
res.status(404).send("Not found."); | ||
} | ||
``` | ||
Also, parameters named `body` will be filled with the value of `request.body`, which is useful since you can set the type of the parameter to whatever you | ||
need to, such as an interface describing the expected fields that clients can send. When combined with value returns, you can achieve a very natural style: | ||
```typescript | ||
interface MyRequestType { | ||
action : string; | ||
foo? : number; | ||
} | ||
@Get('/do') | ||
doThings(body : MyRequestType) { | ||
return {status: "success"}; | ||
} | ||
``` | ||
## HTTP Exceptions | ||
The `HttpException` class is included to signal Alterior to send certain HTTP status codes and responses back to the client when exceptional circumstances occur. | ||
```typescript | ||
// Perhaps we couldn't contact a microservice needed to fulfill the request. | ||
throw new HttpException(502, "Service is not available"); | ||
``` | ||
## Dependency Injection | ||
@@ -206,2 +261,6 @@ | ||
This is Angular 2's dependency injector, so you can define your own services just as you would in Angular. | ||
You can add providers at the bootstrap, app-class or controller levels. | ||
You can add providers at the bootstrap, or app-class levels. | ||
## That's great but how do you pronounce this? | ||
Alterior is pronounced like "ulterior" but with an A. We know it's not a proper word :-) |
Sorry, the diff of this file is not supported yet
49088
1186
259
11
+ Added@avejidah/get-parameter-names@0.3.2(transitive)