mirror of https://gitee.com/bigwinds/arangodb
542 lines
15 KiB
JavaScript
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;
|
|
}
|