mirror of https://gitee.com/bigwinds/arangodb
623 lines
19 KiB
JavaScript
623 lines
19 KiB
JavaScript
/*jslint indent: 2, nomen: true, maxlen: 120, sloppy: true, vars: true */
|
|
/*global module, require, exports */
|
|
|
|
// Foxx is an easy way to create APIs and simple web applications
|
|
// from within **ArangoDB**.
|
|
// It is inspired by Sinatra, the classy Ruby web framework. If FoxxApplication is Sinatra,
|
|
// [ArangoDB Actions](http://www.arangodb.org/manuals/current/UserManualActions.html)
|
|
// are the corresponding `Rack`. They provide all the HTTP goodness.
|
|
//
|
|
// So let's get started, shall we?
|
|
|
|
// FoxxApplication uses Underscore internally. This library is wonderful.
|
|
var FoxxApplication,
|
|
baseMiddleware,
|
|
formatMiddleware,
|
|
_ = require("underscore"),
|
|
db = require("org/arangodb").db,
|
|
fs = require("fs"),
|
|
console = require("console"),
|
|
internal = {};
|
|
|
|
// ArangoDB uses a certain structure we refer to as `UrlObject`.
|
|
// With the following function (which is only internal, and not
|
|
// exported) you can create an UrlObject with a given URL,
|
|
// a constraint and a method. For example:
|
|
//
|
|
// internal.createUrlObject('/lecker/gans', null, 'get')
|
|
internal.createUrlObject = function (url, constraint, method) {
|
|
'use strict';
|
|
var urlObject = {};
|
|
|
|
if (!_.isString(url)) {
|
|
throw "URL has to be a String";
|
|
}
|
|
|
|
urlObject.match = url;
|
|
urlObject.methods = [method];
|
|
|
|
if (!_.isUndefined(constraint)) {
|
|
urlObject.constraint = constraint;
|
|
}
|
|
|
|
return urlObject;
|
|
};
|
|
|
|
// ## Creating a new Application
|
|
// And that's FoxxApplication. It's a constructor, so call it like this:
|
|
//
|
|
// app = new FoxxApplication({
|
|
// urlPrefix: "/wiese",
|
|
// templateCollection: "my_templates"
|
|
// })
|
|
//
|
|
// It takes two optional arguments as displayed above:
|
|
//
|
|
// * **The URL Prefix:** All routes you define within will be prefixed with it
|
|
// * **The Template Collection:** More information in the template section
|
|
FoxxApplication = function (options) {
|
|
'use strict';
|
|
var urlPrefix, templateCollection, myMiddleware;
|
|
|
|
options = options || {};
|
|
|
|
urlPrefix = options.urlPrefix;
|
|
templateCollection = options.templateCollection;
|
|
|
|
this.routingInfo = {
|
|
routes: []
|
|
};
|
|
|
|
this.requires = {};
|
|
this.models = {};
|
|
this.helperCollection = {};
|
|
|
|
if (_.isString(urlPrefix)) {
|
|
this.routingInfo.urlPrefix = urlPrefix;
|
|
}
|
|
|
|
if (_.isString(templateCollection)) {
|
|
this.routingInfo.templateCollection = db._collection(templateCollection) ||
|
|
db._create(templateCollection);
|
|
myMiddleware = baseMiddleware(templateCollection, this.helperCollection);
|
|
} else {
|
|
myMiddleware = baseMiddleware();
|
|
}
|
|
|
|
this.routingInfo.middleware = [
|
|
{
|
|
url: {match: "/*"},
|
|
action: {callback: myMiddleware}
|
|
}
|
|
];
|
|
};
|
|
|
|
// ## The functions on a created FoxxApplication
|
|
//
|
|
// When you have created your FoxxApplication you can now define routes
|
|
// on it. You provide each with a function that will handle
|
|
// the request. It gets two arguments (four, to be honest. But the
|
|
// other two are not relevant for now):
|
|
//
|
|
// * The request object
|
|
// * The response object
|
|
//
|
|
// You can find more about those in their individual sections.
|
|
_.extend(FoxxApplication.prototype, {
|
|
// Sometimes it is a good idea to actually start the application
|
|
// you wrote. If this precious moment has arrived, you should
|
|
// use this function.
|
|
// You have to provide the start function with the `applicationContext`
|
|
// variable.
|
|
start: function (context, testMode) {
|
|
'use strict';
|
|
var models = this.models,
|
|
requires = this.requires;
|
|
|
|
_.each(this.routingInfo.routes, function (route) {
|
|
route.action.context = context;
|
|
route.action.requires = requires;
|
|
route.action.models = models;
|
|
});
|
|
|
|
if (!testMode) {
|
|
db._collection("_routing").save(this.routingInfo);
|
|
}
|
|
},
|
|
|
|
// The `handleRequest` method is the raw way to create a new
|
|
// route. You probably wont call it directly, but it is used
|
|
// in the other request methods:
|
|
//
|
|
// app.handleRequest("get", "/gaense", function (req, res) {
|
|
// //handle the request
|
|
// });
|
|
//
|
|
// When defining a route you can also define a so called 'parameterized'
|
|
// route like `/gaense/:stable`. In this case you can later get the value
|
|
// the user provided for `stable` via the `params` function (see the Request
|
|
// object).
|
|
handleRequest: function (method, route, argument1, argument2) {
|
|
'use strict';
|
|
var newRoute = {}, options, callback;
|
|
|
|
if (_.isUndefined(argument2)) {
|
|
callback = argument1;
|
|
options = {};
|
|
} else {
|
|
options = argument1;
|
|
callback = argument2;
|
|
}
|
|
|
|
newRoute.url = internal.createUrlObject(route, options.constraint, method);
|
|
newRoute.action = {
|
|
callback: String(callback)
|
|
};
|
|
|
|
this.routingInfo.routes.push(newRoute);
|
|
},
|
|
|
|
// ### Handle a `head` request
|
|
// This handles requests from the HTTP verb `head`.
|
|
// As with all other requests you can give an option as the third
|
|
// argument, or leave it blank. You have to give a function as
|
|
// the last argument however. It will get a request and response
|
|
// object as its arguments
|
|
head: function (route, argument1, argument2) {
|
|
'use strict';
|
|
this.handleRequest("head", route, argument1, argument2);
|
|
},
|
|
|
|
// ### Manage a `get` request
|
|
// This handles requests from the HTTP verb `get`.
|
|
// See above for the arguments you can give. An example:
|
|
//
|
|
// app.get('/gaense/stall', function (req, res) {
|
|
// // Take this request and deal with it!
|
|
// });
|
|
get: function (route, argument1, argument2) {
|
|
'use strict';
|
|
this.handleRequest("get", route, argument1, argument2);
|
|
},
|
|
|
|
// ### Tackle a `post` request
|
|
// This handles requests from the HTTP verb `post`.
|
|
// See above for the arguments you can give. An example:
|
|
//
|
|
// app.post('/gaense/stall', function (req, res) {
|
|
// // Take this request and deal with it!
|
|
// });
|
|
post: function (route, argument1, argument2) {
|
|
'use strict';
|
|
this.handleRequest("post", route, argument1, argument2);
|
|
},
|
|
|
|
// ### Sort out a `put` request
|
|
// This handles requests from the HTTP verb `put`.
|
|
// See above for the arguments you can give. An example:
|
|
//
|
|
// app.put('/gaense/stall', function (req, res) {
|
|
// // Take this request and deal with it!
|
|
// });
|
|
put: function (route, argument1, argument2) {
|
|
'use strict';
|
|
this.handleRequest("put", route, argument1, argument2);
|
|
},
|
|
|
|
// ### Take charge of a `patch` request
|
|
// This handles requests from the HTTP verb `patch`.
|
|
// See above for the arguments you can give. An example:
|
|
//
|
|
// app.patch('/gaense/stall', function (req, res) {
|
|
// // Take this request and deal with it!
|
|
// });
|
|
patch: function (route, argument1, argument2) {
|
|
'use strict';
|
|
this.handleRequest("patch", route, argument1, argument2);
|
|
},
|
|
|
|
// ### Respond to a `delete` request
|
|
// This handles requests from the HTTP verb `delete`.
|
|
// See above for the arguments you can give.
|
|
// **A word of warning:** Do not forget that `delete` is
|
|
// a reserved word in JavaScript so call it as follows:
|
|
//
|
|
// app['delete']('/gaense/stall', function (req, res) {
|
|
// // Take this request and deal with it!
|
|
// });
|
|
'delete': function (route, argument1, argument2) {
|
|
'use strict';
|
|
this.handleRequest("delete", route, argument1, argument2);
|
|
},
|
|
|
|
// ## Before and After Hooks
|
|
// You can use the following two functions to do something
|
|
// before or respectively after the normal routing process
|
|
// is happening. You could use that for logging or to manipulate
|
|
// the request or response (translate it to a certain format for
|
|
// example).
|
|
|
|
// ### Before
|
|
// The before function takes a path on which it should watch
|
|
// and a function that it should execute before the routing
|
|
// takes place. If you do omit the path, the function will
|
|
// be executed before each request, no matter the path.
|
|
// Your function gets a Request and a Response object.
|
|
// An example:
|
|
//
|
|
// app.before('/high/way', function(req, res) {
|
|
// //Do some crazy request logging
|
|
// });
|
|
before: function (path, func) {
|
|
'use strict';
|
|
if (_.isUndefined(func)) {
|
|
func = path;
|
|
path = "/*";
|
|
}
|
|
|
|
this.routingInfo.middleware.push({
|
|
url: {match: path},
|
|
action: {callback: String(function (req, res, opts, next) {
|
|
func(req, res);
|
|
next();
|
|
})}
|
|
});
|
|
},
|
|
|
|
// ### After
|
|
// This works pretty similar to the before function.
|
|
// But it acts after the execution of the handlers
|
|
// (Big surprise, I suppose).
|
|
//
|
|
// An example:
|
|
//
|
|
// app.after('/high/way', function(req, res) {
|
|
// //Do some crazy response logging
|
|
// });
|
|
after: function (path, func) {
|
|
'use strict';
|
|
if (_.isUndefined(func)) {
|
|
func = path;
|
|
path = "/*";
|
|
}
|
|
|
|
this.routingInfo.middleware.push({
|
|
url: {match: path},
|
|
action: {callback: String(function (req, res, opts, next) {
|
|
next();
|
|
func(req, res);
|
|
})}
|
|
});
|
|
},
|
|
|
|
// ## The ViewHelper concept
|
|
// If you want to use a function inside your templates, the ViewHelpers
|
|
// will come to rescue you. Define them on your app like this:
|
|
//
|
|
// app.helper("link_to", function (identifier) {
|
|
// return urlRepository[identifier];
|
|
// });
|
|
//
|
|
// Then you can just call this function in your template's JavaScript
|
|
// blocks.
|
|
helper: function (name, func) {
|
|
'use strict';
|
|
this.helperCollection[name] = func;
|
|
},
|
|
|
|
// ## Shortform for using the formatMiddleware
|
|
//
|
|
// More information about the formatMiddleware in the corresponding section.
|
|
// This is a shortcut to add the middleware to your application:
|
|
//
|
|
// app.accepts(["json"], "json");
|
|
accepts: function (allowedFormats, defaultFormat) {
|
|
'use strict';
|
|
|
|
this.routingInfo.middleware.push({
|
|
url: { match: "/*" },
|
|
action: { callback: formatMiddleware(allowedFormats, defaultFormat) }
|
|
});
|
|
}
|
|
});
|
|
|
|
|
|
// ## The Base Middleware
|
|
// The `baseMiddleware` manipulates the request and response
|
|
// objects to give you a nicer API.
|
|
baseMiddleware = function (templateCollection, helperCollection) {
|
|
'use strict';
|
|
var middleware = function (request, response, options, next) {
|
|
var responseFunctions,
|
|
requestFunctions,
|
|
_ = require("underscore");
|
|
|
|
// ### The Request Object
|
|
// Every request object has the following attributes from the underlying Actions,
|
|
// amongst others:
|
|
//
|
|
// * path: The complete path as supplied by the user
|
|
//
|
|
// FoxxApplication adds the following methods to this request object:
|
|
requestFunctions = {
|
|
// ### The superfluous `body` function
|
|
// Get the body of the request
|
|
body: function () {
|
|
return this.requestBody;
|
|
},
|
|
|
|
// ### The jinxed `params` function
|
|
// Get the parameters of the request. This process is two-fold:
|
|
//
|
|
// 1. If you have defined an URL like `/test/:id` and the user
|
|
// requested `/test/1`, the call `params("id")` will return `1`.
|
|
// 2. If you have defined an URL like `/test` and the user gives a
|
|
// query component, the query parameters will also be returned.
|
|
// So for example if the user requested `/test?a=2`, the call
|
|
// `params("a")` will return `2`.
|
|
params: function (key) {
|
|
var ps = {};
|
|
_.extend(ps, this.urlParameters);
|
|
_.extend(ps, this.parameters);
|
|
return ps[key];
|
|
}
|
|
};
|
|
|
|
// ### The Response Object
|
|
// Every response object has the following attributes from the underlying Actions:
|
|
//
|
|
// * contentType
|
|
// * responseCode
|
|
// * body
|
|
// * headers (an object)
|
|
//
|
|
// FoxxApplication adds the following methods to this response object.
|
|
responseFunctions = {
|
|
// ### The straightforward `status` function
|
|
// Set the status code of your response, for example:
|
|
//
|
|
// response.status(404);
|
|
status: function (code) {
|
|
this.responseCode = code;
|
|
},
|
|
|
|
// ### The radical `set` function
|
|
// Set a header attribute, for example:
|
|
//
|
|
// response.set("Content-Length", 123);
|
|
// response.set("Content-Type", "text/plain");
|
|
//
|
|
// or alternatively:
|
|
//
|
|
// response.set({
|
|
// "Content-Length": "123",
|
|
// "Content-Type": "text/plain"
|
|
// });
|
|
set: function (key, value) {
|
|
var attributes = {};
|
|
if (_.isUndefined(value)) {
|
|
attributes = key;
|
|
} else {
|
|
attributes[key] = value;
|
|
}
|
|
|
|
_.each(attributes, function (value, key) {
|
|
key = key.toLowerCase();
|
|
this.headers = this.headers || {};
|
|
this.headers[key] = value;
|
|
|
|
if (key === "content-type") {
|
|
this.contentType = value;
|
|
}
|
|
}, this);
|
|
},
|
|
|
|
// ### The magical `json` function
|
|
// Set the content type to JSON and the body to the
|
|
// JSON encoded object you provided.
|
|
//
|
|
// response.json({'born': 'December 12, 1915'});
|
|
json: function (obj) {
|
|
this.contentType = "application/json";
|
|
this.body = JSON.stringify(obj);
|
|
},
|
|
|
|
// ### The mysterious `render` function
|
|
// If you initialize your FoxxApplication with a `templateCollection`,
|
|
// you're in luck now.
|
|
// It expects documents in the following form in this collection:
|
|
//
|
|
// {
|
|
// path: "high/way",
|
|
// content: "hallo <%= username %>",
|
|
// contentType: "text/plain",
|
|
// templateLanguage: "underscore"
|
|
// }
|
|
//
|
|
// The `content` is the string that will be rendered by the template
|
|
// processor. The `contentType` is the type of content that results
|
|
// from this call. And with the `templateLanguage` you can choose
|
|
// your template processor. There is only one choice now: `underscore`.
|
|
//
|
|
// If you call render, FoxxApplication will look
|
|
// into the this collection and search by the path attribute.
|
|
// It will then render the template with the given data:
|
|
//
|
|
// response.render("high/way", {username: 'FoxxApplication'})
|
|
//
|
|
// Which would set the body of the response to `hello FoxxApplication` with the
|
|
// template defined above. It will also set the `contentType` to
|
|
// `text/plain` in this case.
|
|
//
|
|
// In addition to the attributes you provided, you also have access to
|
|
// all your view helpers. How to define them? Read above in the
|
|
// ViewHelper section.
|
|
render: function (templatePath, data) {
|
|
var template;
|
|
|
|
if (_.isUndefined(templateCollection)) {
|
|
throw "No template collection has been provided when creating a new FoxxApplication";
|
|
}
|
|
|
|
template = templateCollection.firstExample({path: templatePath });
|
|
|
|
if (_.isNull(template)) {
|
|
throw "Template '" + templatePath + "' does not exist";
|
|
}
|
|
|
|
if (template.templateLanguage !== "underscore") {
|
|
throw "Unknown template language '" + template.templateLanguage + "'";
|
|
}
|
|
|
|
this.body = _.template(template.content, _.extend(data, helperCollection));
|
|
this.contentType = template.contentType;
|
|
}
|
|
};
|
|
|
|
// Now enhance the request and response as described above and call next at the
|
|
// end of this middleware (otherwise your application would never
|
|
// be executed. Would be a shame, really).
|
|
request = _.extend(request, requestFunctions);
|
|
response = _.extend(response, responseFunctions);
|
|
next();
|
|
};
|
|
|
|
return String(middleware);
|
|
};
|
|
|
|
// ## The Format Middleware
|
|
// Unlike the `baseMiddleware` this Middleware is only loaded if you
|
|
// want it. This Middleware gives you Rails-like format handling via
|
|
// the `extension` of the URL or the accept header.
|
|
// Say you request an URL like `/people.json`:
|
|
// The `formatMiddleware` will set the format of the request to JSON
|
|
// and then delete the `.json` from the request. You can therefore write
|
|
// handlers that do not take an `extension` into consideration and instead
|
|
// handle the format via a simple String.
|
|
// To determine the format of the request it checks the URL and then
|
|
// the `accept` header. If one of them gives a format or both give
|
|
// the same, the format is set. If the formats are not the same,
|
|
// an error is raised.
|
|
//
|
|
// Use it by calling:
|
|
//
|
|
// formatMiddleware = require('foxx').formatMiddleware;
|
|
// app.before("/*", formatMiddleware.new(['json']));
|
|
//
|
|
// or the shortcut:
|
|
//
|
|
// app.accepts(['json']);
|
|
//
|
|
// In both forms you can give a default format as a second parameter,
|
|
// if no format could be determined. If you give no `defaultFormat` this
|
|
// case will be handled as an error.
|
|
formatMiddleware = function (allowedFormats, defaultFormat) {
|
|
'use strict';
|
|
var middleware, urlFormatToMime, mimeToUrlFormat, determinePathAndFormat;
|
|
|
|
urlFormatToMime = {
|
|
json: "application/json",
|
|
html: "text/html",
|
|
txt: "text/plain"
|
|
};
|
|
|
|
mimeToUrlFormat = {
|
|
"application/json": "json",
|
|
"text/html": "html",
|
|
"text/plain": "txt"
|
|
};
|
|
|
|
determinePathAndFormat = function (path, headers) {
|
|
var parsed = {
|
|
contentType: headers.accept
|
|
};
|
|
path = path.split('.');
|
|
|
|
if (path.length === 1) {
|
|
parsed.path = path.join();
|
|
} else {
|
|
parsed.format = path.pop();
|
|
parsed.path = path.join('.');
|
|
}
|
|
|
|
if (parsed.contentType === undefined && parsed.format === undefined) {
|
|
parsed.format = defaultFormat;
|
|
parsed.contentType = urlFormatToMime[defaultFormat];
|
|
} else if (parsed.contentType === undefined) {
|
|
parsed.contentType = urlFormatToMime[parsed.format];
|
|
} else if (parsed.format === undefined) {
|
|
parsed.format = mimeToUrlFormat[parsed.contentType];
|
|
}
|
|
|
|
if (parsed.format !== mimeToUrlFormat[parsed.contentType]) {
|
|
throw "Contradiction between Accept Header and URL.";
|
|
}
|
|
|
|
if (allowedFormats.indexOf(parsed.format) < 0) {
|
|
throw "Format '" + parsed.format + "' is not allowed.";
|
|
}
|
|
|
|
return parsed;
|
|
};
|
|
|
|
middleware = function (request, response, options, next) {
|
|
var parsed;
|
|
|
|
try {
|
|
parsed = determinePathAndFormat(request.path, request.headers);
|
|
request.path = parsed.path;
|
|
request.format = parsed.format;
|
|
response.contentType = parsed.contentType;
|
|
next();
|
|
} catch (e) {
|
|
response.responseCode = 406;
|
|
response.body = e;
|
|
}
|
|
};
|
|
|
|
return middleware;
|
|
};
|
|
|
|
// We finish off with exporting FoxxApplication and the middlewares.
|
|
// Everything else will remain our secret.
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief loads a manifest file
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
exports.loadManifest = function (path) {
|
|
'use strict';
|
|
var name,
|
|
content,
|
|
manifest,
|
|
key,
|
|
app;
|
|
|
|
name = fs.join(path, "manifest.json");
|
|
content = fs.read(name);
|
|
manifest = JSON.parse(content);
|
|
|
|
for (key in manifest.apps) {
|
|
if (manifest.apps.hasOwnProperty(key)) {
|
|
app = manifest.apps[key];
|
|
|
|
console.info("loading app '%s' from '%s'", key, app);
|
|
|
|
module.loadAppScript(path, manifest, app);
|
|
}
|
|
}
|
|
};
|
|
|
|
exports.FoxxApplication = FoxxApplication;
|
|
exports.baseMiddleware = baseMiddleware;
|
|
//TODO: Make a String exports.formatMiddleware = formatMiddleware;
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// --SECTION-- END-OF-FILE
|
|
// -----------------------------------------------------------------------------
|
|
|
|
// Local Variables:
|
|
// mode: outline-minor
|
|
// outline-regexp: "/// @brief\\|/// @addtogroup\\|/// @page\\|// --SECTION--\\|/// @\\}\\|/\\*jslint"
|
|
// End:
|