1
0
Fork 0
arangodb/js/server/modules/@arangodb/foxx/router/tree.js

542 lines
15 KiB
JavaScript

'use strict';
// //////////////////////////////////////////////////////////////////////////////
// / DISCLAIMER
// /
// / Copyright 2016 ArangoDB GmbH, Cologne, Germany
// /
// / Licensed under the Apache License, Version 2.0 (the "License")
// / you may not use this file except in compliance with the License.
// / You may obtain a copy of the License at
// /
// / http://www.apache.org/licenses/LICENSE-2.0
// /
// / Unless required by applicable law or agreed to in writing, software
// / distributed under the License is distributed on an "AS IS" BASIS,
// / WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// / See the License for the specific language governing permissions and
// / limitations under the License.
// /
// / Copyright holder is ArangoDB GmbH, Cologne, Germany
// /
// / @author Alan Plum
// //////////////////////////////////////////////////////////////////////////////
const _ = require('lodash');
const joinPath = require('path').posix.join;
const querystring = require('querystring');
const httperr = require('http-errors');
const union = require('@arangodb/util').union;
const SwaggerContext = require('@arangodb/foxx/router/swagger-context');
const SyntheticRequest = require('@arangodb/foxx/router/request');
const SyntheticResponse = require('@arangodb/foxx/router/response');
const tokenize = require('@arangodb/foxx/router/tokenize');
const validation = require('@arangodb/foxx/router/validation');
const $_ROUTES = Symbol.for('@@routes'); // routes and child routers
const $_MIDDLEWARE = Symbol.for('@@middleware'); // middleware
module.exports =
class Tree {
constructor (context, router) {
this.context = context;
this.router = router;
this.root = new Map();
for (const middleware of router._middleware) {
let node = this.root;
for (const token of middleware._pathTokens) {
if (!node.has(token)) {
node.set(token, new Map());
}
node = node.get(token);
}
if (!node.has($_MIDDLEWARE)) {
node.set($_MIDDLEWARE, []);
}
node.get($_MIDDLEWARE).push(middleware);
}
for (const route of router._routes) {
let node = this.root;
for (const token of route._pathTokens) {
if (!node.has(token)) {
node.set(token, new Map());
}
node = node.get(token);
}
if (!node.has($_ROUTES)) {
node.set($_ROUTES, []);
}
node.get($_ROUTES).push(route);
if (route.router) {
route.tree = new Tree(context, route.router);
}
}
}
* findRoutes (suffix) {
const result = [{router: this.router, path: [], suffix: suffix}];
yield* findRoutes(this.root, result, suffix, []);
}
* flatten () {
yield* flatten(this.root, [this.router]);
}
resolve (rawReq) {
const method = rawReq.requestType;
let error;
for (const route of this.findRoutes(rawReq.suffix)) {
const endpoint = route[route.length - 1].endpoint;
try {
applyPathParams(route);
} catch (e) {
if (!error) {
error = Object.assign(
new httperr.NotFound(),
{cause: e}
);
}
continue;
}
if (endpoint._methods.indexOf(method) === -1) {
error = Object.assign(
new httperr.MethodNotAllowed(),
{methods: endpoint._methods}
);
continue;
}
return route;
}
if (error) {
throw error;
}
}
dispatch (rawReq, rawRes) {
const route = this.resolve(rawReq);
if (!route) {
return false;
}
const req = new SyntheticRequest(rawReq, this.context);
const res = new SyntheticResponse(rawRes, this.context);
dispatch(route, req, res, this);
return true;
}
buildSwaggerPaths () {
const paths = {};
for (const route of this.flatten()) {
const parts = [];
const swagger = new SwaggerContext();
let i = 0;
for (const item of route) {
if (item.router) {
swagger._merge(item, true);
} else {
swagger._merge(item);
swagger._methods = item._methods || swagger._methods;
}
}
for (let token of swagger._pathTokens) {
if (token === tokenize.PARAM) {
token = `{${swagger._pathParamNames[i]}}`;
i++;
} else if (token === tokenize.WILDCARD) {
token = '*';
}
if (typeof token === 'string') {
parts.push(token);
}
}
const path = '/' + parts.join('/');
if (!paths[path]) {
paths[path] = {};
}
const pathItem = paths[path];
const operation = swagger._buildOperation();
for (let method of swagger._methods) {
method = method.toLowerCase();
if (!pathItem[method]) {
pathItem[method] = operation;
}
}
}
return paths;
}
reverse (route, routeName, params, suffix) {
if (typeof params === 'string') {
suffix = params;
params = undefined;
}
const reversedRoute = reverse(route, routeName);
if (!reversedRoute) {
throw new Error(`Route could not be resolved: "${routeName}"`);
}
params = Object.assign({}, params);
const parts = [];
for (const item of reversedRoute) {
const context = item.router || item.endpoint || item.middleware;
let i = 0;
for (let token of context._pathTokens) {
if (token === tokenize.PARAM) {
const name = context._pathParamNames[i];
if (params.hasOwnProperty(name)) {
if (Array.isArray(params[name])) {
if (!params[name].length) {
throw new Error(`Not enough values for parameter "${name}"`);
}
token = params[name][0];
params[name] = params[name].slice(1);
if (!params[name].length) {
delete params[name];
}
} else {
token = String(params[name]);
delete params[name];
}
} else {
throw new Error(`Missing value for parameter "${name}"`);
}
i++;
}
if (typeof token === 'string') {
parts.push(token);
}
}
}
const query = querystring.encode(params);
let path = '/' + parts.join('/');
if (suffix) {
path += '/' + suffix;
}
if (query) {
path += '?' + query;
}
return path;
}
};
function applyPathParams (route) {
for (const item of route) {
const context = item.middleware || item.endpoint || item.router;
const params = parsePathParams(
context._pathParamNames,
context._pathTokens,
item.path
);
try {
item.pathParams = validation.validateParams(
(
item.router && item.router.router
? union(context._pathParams, context.router._pathParams)
: context._pathParams
),
params,
'path parameter'
);
} catch (e) {
if (item.router || item.endpoint) {
throw e;
}
}
}
}
function dispatch (route, req, res, tree) {
let pathParams = {};
let queryParams = Object.assign({}, req.queryParams);
let headers = Object.assign({}, req.headers);
{
let basePath = [];
let responses = res._responses;
for (const item of route) {
const context = (
item.router
? (item.router.router || item.router)
: (item.middleware || item.endpoint)
);
item.path = basePath.concat(item.path);
if (item.router) {
basePath = item.path;
}
item._responses = union(responses, context._responses);
responses = item._responses;
}
}
let i = 0;
let requestBodyParsed = false;
function next (err) {
if (err) {
throw err;
}
const item = route[i];
i++;
if (!item) {
return;
}
const context = (
item.router
? (item.router.router || item.router)
: (item.middleware || item.endpoint)
);
if (context._bodyParam) {
try {
if (!requestBodyParsed) {
requestBodyParsed = true;
req.body = validation.parseRequestBody(
context._bodyParam,
req
);
}
req.body = validation.validateRequestBody(
context._bodyParam,
req
);
} catch (e) {
throw Object.assign(
new httperr.BadRequest(e.message),
{cause: e}
);
}
}
if (context._queryParams.size) {
try {
item.queryParams = validation.validateParams(
context._queryParams,
req.queryParams,
'query parameter'
);
} catch (e) {
throw Object.assign(
new httperr.BadRequest(e.message),
{cause: e}
);
}
}
if (context._headers.size) {
try {
item.headers = validation.validateParams(
context._headers,
req.headers,
'header'
);
} catch (e) {
throw Object.assign(
new httperr.BadRequest(e.message),
{cause: e}
);
}
}
let tempPathParams = req.pathParams;
let tempQueryParams = req.queryParams;
let tempHeaders = req.headers;
let tempSuffix = req.suffix;
let tempPath = req.path;
let tempReverse = req.reverse;
let tempResponses = res._responses;
req.path = '/' + item.path.join('/');
req.suffix = item.suffix.join('/');
if (req.suffix) {
req.path = joinPath(req.path, req.suffix);
}
res._responses = item._responses;
req.reverse = function (...args) {
return tree.reverse(route.slice(0, i), ...args);
};
if (item.endpoint || item.router) {
pathParams = Object.assign(pathParams, item.pathParams);
queryParams = Object.assign(queryParams, item.queryParams);
headers = Object.assign(headers, item.headers);
req.pathParams = pathParams;
req.queryParams = queryParams;
req.headers = headers;
} else {
req.pathParams = Object.assign({}, pathParams, item.pathParams);
req.queryParams = Object.assign({}, queryParams, item.queryParams);
req.headers = Object.assign({}, headers, item.headers);
}
if (!context._handler) {
next();
} else if (item.endpoint) {
context._handler(req, res);
} else {
context._handler(req, res, _.once(next));
}
res._responses = tempResponses;
req.reverse = tempReverse;
req.path = tempPath;
req.suffix = tempSuffix;
req.headers = tempHeaders;
req.queryParams = tempQueryParams;
req.pathParams = tempPathParams;
}
next();
if (res._raw.body && typeof res._raw.body !== 'string' && !(res._raw.body instanceof Buffer)) {
console.warn(`Coercing response body to string for ${req.method} ${req.originalUrl}`);
res._raw.body = String(res._raw.body);
}
if (!res.statusCode) {
res.statusCode = res._raw.body ? 200 : 204;
}
}
function * findRoutes (node, result, suffix, path) {
const wildcardNode = node.get(tokenize.WILDCARD);
if (wildcardNode && wildcardNode.has($_MIDDLEWARE)) {
const nodeMiddleware = wildcardNode.get($_MIDDLEWARE);
result = result.concat(nodeMiddleware.map(
(mw) => ({middleware: mw, path: path, suffix: suffix})
));
}
if (!suffix.length) {
const terminalNode = node.get(tokenize.TERMINAL);
const terminalRoutes = terminalNode && terminalNode.get($_ROUTES) || [];
for (const endpoint of terminalRoutes) {
yield result.concat(
{endpoint: endpoint, path: path, suffix: suffix}
);
}
} else {
const part = decodeURIComponent(suffix[0]);
const path2 = path.concat(part);
const suffix2 = suffix.slice(1);
for (const childNode of [node.get(part), node.get(tokenize.PARAM)]) {
if (childNode) {
yield* findRoutes(childNode, result, suffix2, path2);
}
}
}
const wildcardRoutes = wildcardNode && wildcardNode.get($_ROUTES) || [];
for (const endpoint of wildcardRoutes) {
if (endpoint.router) {
const childNode = endpoint.tree.root;
const result2 = result.concat(
{router: endpoint, path: path, suffix: suffix}
);
const path2 = [];
yield* findRoutes(childNode, result2, suffix, path2);
} else {
yield result.concat(
{endpoint: endpoint, path: path, suffix: suffix}
);
}
}
}
function * flatten (node, result) {
for (let entry of node.entries()) {
const token = entry[0];
const child = entry[1];
if (token === tokenize.WILDCARD || token === tokenize.TERMINAL) {
if (child.has($_MIDDLEWARE)) {
result = result.concat(...child.get($_MIDDLEWARE));
}
for (const endpoint of child.get($_ROUTES) || []) {
if (endpoint.router) {
yield* flatten(endpoint.tree.root, result.concat(endpoint, endpoint.router));
} else {
yield result.concat(endpoint);
}
}
} else {
yield* flatten(child, result);
}
}
}
function parsePathParams (names, route, path) {
const params = {};
let j = 0;
for (let i = 0; i < route.length; i++) {
if (route[i] === tokenize.PARAM) {
params[names[j]] = path[i];
j++;
}
}
return params;
}
function reverse (route, path) {
const routers = route.filter((item) => item.router);
const keys = path.split('.');
const visited = [];
while (routers.length) {
const item = routers.pop();
const router = item.router;
const result = search(router.router || router, keys.slice(), visited);
if (result) {
return route.slice(0, route.indexOf(item) + 1).concat(result);
}
visited.push(item);
}
return null;
}
function search (router, path, visited) {
const name = path[0];
const tail = path.slice(1);
if (router._namedRoutes.has(name)) {
const child = router._namedRoutes.get(name);
if (child.router) {
// traverse named child router
if (tail.length && visited.indexOf(child) === -1) {
visited.push(child);
const result = search(child.router, tail, visited);
if (result) {
result.unshift({router: child});
return result;
}
}
} else if (!tail.length) {
// found named route
return [{endpoint: child}];
}
}
// traverse anonymous child routers
for (const child of router._routes) {
if (child.router && visited.indexOf(child) === -1) {
visited.push(child);
const result = search(child.router, tail, visited);
if (result) {
result.unshift({router: child});
return result;
}
}
}
return null;
}