Skip to content

Commit

Permalink
Heavy-ish refactor. API changes. Read on.
Browse files Browse the repository at this point in the history
- no more middleware nonsense, now getRoute does all
- new feature (not yet tested) for attaching a custom context to a
  method
- internal refactor
  • Loading branch information
Paolo Scanferla committed Apr 8, 2015
1 parent 962648d commit e899d3f
Show file tree
Hide file tree
Showing 19 changed files with 305 additions and 253 deletions.
14 changes: 7 additions & 7 deletions README.md
Expand Up @@ -24,18 +24,18 @@ MongoClient.connect(mongoUrl, function (err, db) {
}
var mw = new MW(db);

var optionalContext = {
prefix: "echo: "
};

mv.methods({
echo: function (string) {
return string;
return this.prefix + string;
}
});
}, optionalContext);

var path = "/method";
var app = express()
.use(path, bodyParser.json())
.use(path, MW.bodyValidationMiddleware)
.use(path, MW.contextMiddleware)
.use(path, mw.getUserMiddleware())
.post(path, mw.getRoute())
.listen(process.env.PORT || 4000);
});
Expand Down
5 changes: 4 additions & 1 deletion package.json
Expand Up @@ -13,14 +13,17 @@
},
"author": "Paolo Scanferla <paolo.scanferla@mondora.com>",
"license": "MIT",
"peerDependencies": {
"express": "^4.12.3"
},
"dependencies": {
"bluebird": "^2.9.15",
"body-parser": "^1.12.2",
"ramda": "^0.13.0",
"tcomb": "^1.0.0",
"tcomb-validation": "^1.0.1"
},
"devDependencies": {
"body-parser": "^1.12.2",
"coveralls": "^2.11.2",
"express": "^4.12.3",
"istanbul": "^0.3.11",
Expand Down
6 changes: 6 additions & 0 deletions src/lib/bpromise-type.js
@@ -0,0 +1,6 @@
var BPromise = require("bluebird");
var t = require("tcomb");

module.exports = t.irreducible("BPromise", function (p) {
return p instanceof BPromise;
});
18 changes: 18 additions & 0 deletions src/lib/get-user-from-token.js
@@ -0,0 +1,18 @@
var BPromise = require("bluebird");

var getLastValidDate = require("./get-last-valid-date.js");
var hashLoginToken = require("./hash-login-token.js");

module.exports = function getUserFromToken (mwInstance, loginToken) {
var users = mwInstance.db.collection("users");
return BPromise.promisify(users.findOne, users)({
"services.resume.loginTokens": {
$elemMatch: {
hashedToken: hashLoginToken(loginToken),
when: {
$gt: getLastValidDate()
}
}
}
});
};
129 changes: 62 additions & 67 deletions src/methods.js
@@ -1,12 +1,14 @@
var BPromise = require("bluebird");
var R = require("ramda");
var t = require("tcomb-validation");
var BPromise = require("bluebird");
var bodyParser = require("body-parser");
var express = require("express");
var R = require("ramda");
var t = require("tcomb");

var errorHandler = require("./lib/error-handler.js");
var getLastValidDate = require("./lib/get-last-valid-date.js");
var hashLoginToken = require("./lib/hash-login-token.js");
var MWError = require("./lib/mw-error.js");
var resultHandler = require("./lib/result-handler.js");
var BPromiseType = require("./lib/bpromise-type.js");
var errorHandler = require("./lib/error-handler.js");
var MWError = require("./lib/mw-error.js");
var resultHandler = require("./lib/result-handler.js");
var middleware = require("./middleware.js");

/*
* By convention, a method can either:
Expand All @@ -15,71 +17,64 @@ var resultHandler = require("./lib/result-handler.js");
* - return a thenable
*/

var methods = {

_getUserFromToken: function (loginToken) {
var users = this.db.collection("users");
return BPromise.promisify(users.findOne, users)({
"services.resume.loginTokens": {
$elemMatch: {
hashedToken: hashLoginToken(loginToken),
when: {
$gt: getLastValidDate()
}
}
}
});
},

_runMethod: function (context, name, args) {
var self = this;
return new BPromise(function (resolve, reject) {
try {
var fn = self._methods[name];
if (!fn) {
throw new MWError(404, "Method not found");
}
resolve(
fn.apply(context, args)
);
} catch (e) {
return reject(e);
var _runMethod = function (reqContext, name, args) {
return BPromise.resolve()
.bind(this)
.then(function () {
var method = this._methods[name];
if (!method) {
throw new MWError(404, "Method not found");
}
var context = R.merge(method.context, reqContext);
return method.fn.apply(context, args);
});
},
};

methods: t.func(t.dict(t.Str, t.Func), t.Nil).of(function (methodsMap) {
this._methods = R.merge(this._methods, methodsMap);
}),
var methods = function (methodsMap, context) {
var transform = function (fn) {
return {
fn: fn,
context: context || {}
};
};
this._methods = R.pipe(
R.mapObj(transform),
R.merge(this._methods)
)(methodsMap);
};

getRoute: function () {
var self = this;
return function (req, res) {
var getRoute = function () {
var self = this;
return express.Router()
.use(bodyParser.json())
.use(middleware.bodyValidation())
.use(middleware.context())
.use(middleware.user(self))
.post("*", function (req, res) {
self._runMethod(req.context, req.body.method, req.body.params)
.then(resultHandler(res))
.catch(errorHandler(res));
};
},

getUserMiddleware: function () {
var self = this;
return function (req, res, next) {
if (R.isNil(req.body.loginToken)) {
return next();
}
self._getUserFromToken(req.body.loginToken)
.then(function (user) {
if (R.isNil(user)) {
throw new MWError(401, "Invalid loginToken");
}
req.context.userId = user._id;
req.context.user = user;
next();
})
.catch(errorHandler(res));
};
}

});
};

module.exports = methods;
/*
* Dynamically type-check our API
*/
module.exports = {
_runMethod: t.func([t.Obj, t.Str, t.Arr], BPromiseType).of(_runMethod),
methods: function (methodsMap, context) {
/*
* Workaround tcomb issue #84 (which won't probably be fixed).
* tcomb curries functions wrapped with the `Func.of` method.
* Therefore, being the `context` argument optional, to avoid forcing
* our user to always specify it as `undefined` we do it for them
* (simply by calling the function with the two parameters, which
* guarantees us that, inside the tcomb wrapper function,
* `arguments.length` equals 2).
*/
return t.func([t.dict(t.Str, t.Func), t.maybe(t.Obj)], t.Nil)
.of(methods)
.call(this, methodsMap, context);
},
getRoute: t.func([], t.Func).of(getRoute)
};
51 changes: 51 additions & 0 deletions src/middleware.js
@@ -0,0 +1,51 @@
var BPromise = require("bluebird");
var R = require("ramda");
var t = require("tcomb-validation");

var BodyType = require("./lib/body-type.js");
var errorHandler = require("./lib/error-handler.js");
var getUserFromToken = require("./lib/get-user-from-token.js");
var MWError = require("./lib/mw-error.js");

/*
* To keep a consistent API between middleware (i.e., each property of the
* exported object is a getter for the actual middleware function) we "wrap"
* `bodyValidation` and `context` functions in `R.always`.
*/
module.exports = {

bodyValidation: R.always(function (req, res, next) {
var validation = t.validate(req.body, BodyType);
if (validation.isValid()) {
next();
} else {
errorHandler(res, new MWError(400, validation.firstError()));
}
}),

context: R.always(function (req, res, next) {
req.context = {
userId: null
};
next();
}),

user: function (mwInstance) {
return function (req, res, next) {
if (R.isNil(req.body.loginToken)) {
return next();
}
getUserFromToken(mwInstance, req.body.loginToken)
.then(function (user) {
if (R.isNil(user)) {
throw new MWError(401, "Invalid loginToken");
}
req.context.userId = user._id;
req.context.user = user;
next();
})
.catch(errorHandler(res));
};
}

};
8 changes: 2 additions & 6 deletions src/mw.js
@@ -1,18 +1,14 @@
var R = require("ramda");

var statics = require("./statics.js");
var methods = require("./methods.js");
var MWError = require("./lib/mw-error.js");

var MW = function (db) {
// db is a mongo client
this.db = db;
this._methods = {};
};

// Merge statics (can't use R.merge because MW is a function)
R.keys(statics).forEach(function (key) {
MW[key] = statics[key];
});
MW.Error = MWError;
// Merge methods
MW.prototype = R.merge(MW.prototype, methods);

Expand Down
29 changes: 0 additions & 29 deletions src/statics.js

This file was deleted.

7 changes: 1 addition & 6 deletions test/integration/bad-requests.js
Expand Up @@ -20,12 +20,7 @@ describe("Integration suite - Bad requests", function () {

it("the server should reply a 400 on malformed body", function (done) {
var mw = new MW(db);
var app = express()
.use(bodyParser.json())
.use(MW.bodyValidationMiddleware)
.use(MW.contextMiddleware)
.use(mw.getUserMiddleware())
.post(mw.getRoute());
var app = express().use("/", mw.getRoute());
request(app)
.post("/")
.send({unexpectedProp: "unexpectedValue"})
Expand Down

0 comments on commit e899d3f

Please sign in to comment.