/* global AQL_EXECUTE */ 'use strict'; // ////////////////////////////////////////////////////////////////////////////// // DISCLAIMER // // Copyright 2010-2013 triAGENS GmbH, Cologne, Germany // 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 Michael Hackstein // @author Heiko Kernbach // @author Alan Plum // ////////////////////////////////////////////////////////////////////////////// const joi = require('joi'); const dd = require('dedent'); const internal = require('internal'); const db = require('@arangodb').db; const errors = require('@arangodb').errors; const notifications = require('@arangodb/configuration').notifications; const examples = require('@arangodb/graph-examples/example-graph'); const createRouter = require('@arangodb/foxx/router'); const users = require('@arangodb/users'); const cluster = require('@arangodb/cluster'); const isEnterprise = require('internal').isEnterprise(); const ERROR_USER_NOT_FOUND = errors.ERROR_USER_NOT_FOUND.code; const API_DOCS = require(module.context.fileName('api-docs.json')); API_DOCS.basePath = `/_db/${encodeURIComponent(db._name())}`; const router = createRouter(); module.exports = router; router.get('/config.js', function (req, res) { const scriptName = req.get('x-script-name'); const basePath = req.trustProxy && scriptName || ''; const isEnterprise = internal.isEnterprise(); res.send( `var frontendConfig = ${JSON.stringify({ basePath: basePath, db: req.database, isEnterprise: isEnterprise, authenticationEnabled: internal.authenticationEnabled(), isCluster: cluster.isCluster() })}` ); }) .response(['text/javascript']); router.get('/whoAmI', function (req, res) { res.json({user: req.arangoUser || null}); }) .summary('Return the current user') .description(dd` Returns the current user or "null" if the user is not authenticated. Returns "false" if authentication is disabled. `); const authRouter = createRouter(); router.use(authRouter); authRouter.use((req, res, next) => { if (internal.authenticationEnabled()) { if (!req.arangoUser) { res.throw('unauthorized'); } } next(); }); router.get('/api/*', module.context.apiDocumentation({ swaggerJson (req, res) { res.json(API_DOCS); } })) .summary('System API documentation') .description(dd` Mounts the system API documentation. `); authRouter.get('shouldCheckVersion', function (req, res) { const versions = notifications.versions(); res.json(Boolean(versions && versions.enableVersionNotification)); }) .summary('Is version check allowed') .description(dd` Check if version check is allowed. `); authRouter.post('disableVersionCheck', function (req, res) { notifications.setVersions({enableVersionNotification: false}); res.json('ok'); }) .summary('Disable version check') .description(dd` Disable the version check in web interface `); authRouter.post('/query/explain', function (req, res) { const bindVars = req.body.bindVars; const query = req.body.query; const id = req.body.id; const batchSize = req.body.batchSize; let msg = null; try { if (bindVars) { msg = require('@arangodb/aql/explainer').explain({ query: query, bindVars: bindVars, batchSize: batchSize, id: id }, {colors: false}, false, bindVars); } else { msg = require('@arangodb/aql/explainer').explain(query, {colors: false}, false); } } catch (e) { res.throw('bad request', e.message, {cause: e}); } res.json({msg}); }) .body(joi.object({ query: joi.string().required(), bindVars: joi.object().optional(), batchSize: joi.number().optional(), id: joi.string().optional() }).required(), 'Query and bindVars to explain.') .summary('Explains a query') .description(dd` Explains a query in a more user-friendly way than the query_api/explain `); authRouter.post('/query/upload/:user', function (req, res) { let user = req.pathParams.user; try { user = users.document(user); } catch (e) { if (!e.isArangoError || e.errorNum !== ERROR_USER_NOT_FOUND) { throw e; } res.throw('not found'); } if (!user.extra.queries) { user.extra.queries = []; } const existingQueries = user.extra.queries .map(query => query.name); for (const query of req.body) { if (existingQueries.indexOf(query.name) === -1) { existingQueries.push(query.name); user.extra.queries.push(query); } } users.update(user.user, undefined, undefined, user.extra); res.json(user.extra.queries); }) .pathParam('user', joi.string().required(), 'Username. Ignored if authentication is enabled.') .body(joi.array().items(joi.object({ name: joi.string().required(), parameter: joi.any().optional(), value: joi.any().optional() }).required()).required(), 'User query array to import.') .error('not found', 'User does not exist.') .summary('Upload user queries') .description(dd` This function uploads all given user queries. `); authRouter.get('/query/download/:user', function (req, res) { let user = req.pathParams.user; try { user = users.document(user); } catch (e) { if (!e.isArangoError || e.errorNum !== ERROR_USER_NOT_FOUND) { throw e; } res.throw('not found'); } res.attachment(`queries-${db._name()}-${user.user}.json`); res.json(user.extra.queries || []); }) .pathParam('user', joi.string().required(), 'Username. Ignored if authentication is enabled.') .error('not found', 'User does not exist.') .summary('Download stored queries') .description(dd` Download and export all queries from the given username. `); authRouter.get('/query/result/download/:query', function (req, res) { let query; try { query = internal.base64Decode(req.pathParams.query); query = JSON.parse(query); } catch (e) { res.throw('bad request', e.message, {cause: e}); } const result = db._query(query.query, query.bindVars).toArray(); res.attachment(`results-${db._name()}.json`); res.json(result); }) .pathParam('query', joi.string().required(), 'Base64 encoded query.') .error('bad request', 'The query is invalid or malformed.') .summary('Download the result of a query') .description(dd` This function downloads the result of a user query. `); authRouter.post('/graph-examples/create/:name', function (req, res) { const name = req.pathParams.name; if (['knows_graph', 'social', 'routeplanner'].indexOf(name) === -1) { res.throw('not found'); } const g = examples.loadGraph(name); res.json({error: !g}); }) .pathParam('name', joi.string().required(), 'Name of the example graph.') .summary('Create example graphs') .description(dd` Create one of the given example graphs. `); authRouter.post('/job', function (req, res) { db._frontend.save(Object.assign(req.body, {model: 'job'})); res.json(true); }) .body(joi.object({ id: joi.any().required(), collection: joi.any().required(), type: joi.any().required(), desc: joi.any().required() }).required()) .summary('Store job id of a running job') .description(dd` Create a new job id entry in a specific system database with a given id. `); authRouter.delete('/job', function (req, res) { db._frontend.removeByExample({model: 'job'}, false); res.json(true); }) .summary('Delete all jobs') .description(dd` Delete all jobs in a specific system database with a given id. `); authRouter.delete('/job/:id', function (req, res) { db._frontend.removeByExample({id: req.pathParams.id}, false); res.json(true); }) .summary('Delete a job id') .description(dd` Delete an existing job id entry in a specific system database with a given id. `); authRouter.get('/job', function (req, res) { const result = db._frontend.all().toArray(); res.json(result); }) .summary('Return all job ids.') .description(dd` This function returns the job ids of all currently running jobs. `); authRouter.get('/graph/:name', function (req, res) { var _ = require('lodash'); var name = req.pathParams.name; var gm; if (isEnterprise) { gm = require('@arangodb/smart-graph'); } else { gm = require('@arangodb/general-graph'); } var colors = { default: [ '#68BDF6', '#6DCE9E', '#FF756E', '#DE9BF9', '#FB95AF', '#FFD86E', '#A5ABB6' ], jans: ['rgba(166, 109, 161, 1)', 'rgba(64, 74, 83, 1)', 'rgba(90, 147, 189, 1)', 'rgba(153,63,0,1)', 'rgba(76,0,92,1)', 'rgba(25,25,25,1)', 'rgba(0,92,49,1)', 'rgba(43,206,72,1)', 'rgba(255,204,153,1)', 'rgba(128,128,128,1)', 'rgba(148,255,181,1)', 'rgba(143,124,0,1)', 'rgba(157,204,0,1)', 'rgba(194,0,136,1)', 'rgba(0,51,128,1)', 'rgba(255,164,5,1)', 'rgba(255,168,187,1)', 'rgba(66,102,0,1)', 'rgba(255,0,16,1)', 'rgba(94,241,242,1)', 'rgba(0,153,143,1)', 'rgba(224,255,102,1)', 'rgba(116,10,255,1)', 'rgba(153,0,0,1)', 'rgba(255,255,128,1)', 'rgba(255,255,0,1)', 'rgba(255,80,5,1)'], highContrast: [ '#EACD3F', '#6F308A', '#DA6927', '#98CDE5', '#B81F34', '#C0BC82', '#7F7E80', '#61A547', '#60A446', '#D285B0', '#4477B3', '#DD8465', '#473896', '#E0A02F', '#8F2689', '#E7E655', '#7C1514', '#93AD3C', '#6D3312', '#D02C26', '#2A3415' ] }; var graph = gm._graph(name); var verticesCollections = graph._vertexCollections(); if (!verticesCollections || verticesCollections.length === 0) { res.throw('bad request', 'no vertex collections found for graph'); } var vertexName; try { vertexName = verticesCollections[Math.floor(Math.random() * verticesCollections.length)].name(); } catch (err) { res.throw('bad request', 'vertex collection of graph not found'); } var vertexCollections = []; _.each(graph._vertexCollections(), function (vertex) { vertexCollections.push({ name: vertex.name(), id: vertex._id }); }); var startVertex; var config; try { config = req.queryParams; } catch (e) { res.throw('bad request', e.message, {cause: e}); } if (config.nodeStart) { try { startVertex = db._document(config.nodeStart); } catch (e) { res.throw('bad request', e.message, {cause: e}); } if (!startVertex) { startVertex = db[vertexName].any(); } } else { startVertex = db[vertexName].any(); } var limit = 0; if (config.limit !== undefined) { if (config.limit.length > 0 && config.limit !== '0') { limit = config.limit; } } var toReturn; if (startVertex === null) { toReturn = { empty: true, msg: 'Your graph is empty', settings: { vertexCollections: vertexCollections } }; if (isEnterprise) { if (graph.__isSmart) { toReturn.settings.isSmart = graph.__isSmart; toReturn.settings.smartGraphAttribute = graph.__smartGraphAttribute; } } } else { var aqlQuery; if (config.query) { aqlQuery = config.query; } else { aqlQuery = 'FOR v, e, p IN 1..' + (config.depth || '2') + ' ANY "' + startVertex._id + '" GRAPH "' + name + '"'; if (limit !== 0) { aqlQuery += ' LIMIT ' + limit; } aqlQuery += ' RETURN p'; } var getAttributeByKey = function (o, s) { s = s.replace(/\[(\w+)\]/g, '.$1'); s = s.replace(/^\./, ''); var a = s.split('.'); for (var i = 0, n = a.length; i < n; ++i) { var k = a[i]; if (k in o) { o = o[k]; } else { return; } } return o; }; var cursor; // get all nodes and edges, even if they are not connected // atm there is no server side function, so we need to get all docs // and edges of all related collections until the given limit is reached. if (config.mode === 'all') { var insertedEdges = 0; var insertedNodes = 0; var tmpEdges, tmpNodes; cursor = { json: [{ vertices: [], edges: [] }] }; // get all nodes _.each(graph._vertexCollections(), function (node) { if (insertedNodes < limit || limit === 0) { tmpNodes = node.all().limit(limit).toArray(); _.each(tmpNodes, function (n) { cursor.json[0].vertices.push(n); }); insertedNodes += tmpNodes.length; } }); // get all edges _.each(graph._edgeCollections(), function (edge) { if (insertedEdges < limit || limit === 0) { tmpEdges = edge.all().limit(limit).toArray(); _.each(tmpEdges, function (e) { cursor.json[0].edges.push(e); }); insertedEdges += tmpEdges.length; } }); } else { // get all nodes and edges which are connected to the given start node cursor = AQL_EXECUTE(aqlQuery); } var nodesObj = {}; var nodesArr = []; var nodeNames = {}; var edgesObj = {}; var edgesArr = []; var nodeEdgesCount = {}; var handledEdges = {}; var tmpObjEdges = {}; var tmpObjNodes = {}; _.each(cursor.json, function (obj) { var edgeLabel = ''; var edgeObj; _.each(obj.edges, function (edge) { if (edge._to && edge._from) { if (config.edgeLabel && config.edgeLabel.length > 0) { // configure edge labels if (config.edgeLabel.indexOf('.') > -1) { edgeLabel = getAttributeByKey(edge, config.edgeLabel); if (nodeLabel === undefined || nodeLabel === '') { edgeLabel = edgeLabel._id; } } else { edgeLabel = edge[config.edgeLabel]; } if (typeof edgeLabel !== 'string') { edgeLabel = JSON.stringify(edgeLabel); } if (config.edgeLabelByCollection === 'true') { edgeLabel += ' - ' + edge._id.split('/')[0]; } } else { if (config.edgeLabelByCollection === 'true') { edgeLabel = edge._id.split('/')[0]; } } if (config.nodeSizeByEdges === 'true') { if (handledEdges[edge._id] === undefined) { handledEdges[edge._id] = true; if (nodeEdgesCount[edge._from] === undefined) { nodeEdgesCount[edge._from] = 1; } else { nodeEdgesCount[edge._from] += 1; } if (nodeEdgesCount[edge._to] === undefined) { nodeEdgesCount[edge._to] = 1; } else { nodeEdgesCount[edge._to] += 1; } } } edgeObj = { id: edge._id, source: edge._from, label: edgeLabel, color: config.edgeColor || '#cccccc', target: edge._to }; if (config.edgeEditable === 'true') { edgeObj.size = 1; } else { edgeObj.size = 1; } if (config.edgeColorByCollection === 'true') { var coll = edge._id.split('/')[0]; if (tmpObjEdges.hasOwnProperty(coll)) { edgeObj.color = tmpObjEdges[coll]; } else { tmpObjEdges[coll] = colors.jans[Object.keys(tmpObjEdges).length]; edgeObj.color = tmpObjEdges[coll]; } } else if (config.edgeColorAttribute !== '') { var attr = edge[config.edgeColorAttribute]; if (attr) { if (tmpObjEdges.hasOwnProperty(attr)) { edgeObj.color = tmpObjEdges[attr]; } else { tmpObjEdges[attr] = colors.jans[Object.keys(tmpObjEdges).length]; edgeObj.color = tmpObjEdges[attr]; } } } } edgeObj.sortColor = edgeObj.color; edgesObj[edge._id] = edgeObj; }); var nodeLabel; var nodeSize; var nodeObj; _.each(obj.vertices, function (node) { if (node !== null) { nodeNames[node._id] = true; if (config.nodeLabel) { if (config.nodeLabel.indexOf('.') > -1) { nodeLabel = getAttributeByKey(node, config.nodeLabel); if (nodeLabel === undefined || nodeLabel === '') { nodeLabel = node._id; } } else { nodeLabel = node[config.nodeLabel]; } } else { nodeLabel = node._key; } if (config.nodeLabelByCollection === 'true') { nodeLabel += ' - ' + node._id.split('/')[0]; } if (typeof nodeLabel === 'number') { nodeLabel = JSON.stringify(nodeLabel); } if (config.nodeSize && config.nodeSizeByEdges === 'false') { nodeSize = node[config.nodeSize]; } nodeObj = { id: node._id, label: nodeLabel, size: nodeSize || 3, color: config.nodeColor || '#2ecc71', sortColor: undefined, x: Math.random(), y: Math.random() }; if (config.nodeColorByCollection === 'true') { var coll = node._id.split('/')[0]; if (tmpObjNodes.hasOwnProperty(coll)) { nodeObj.color = tmpObjNodes[coll]; } else { tmpObjNodes[coll] = colors.jans[Object.keys(tmpObjNodes).length]; nodeObj.color = tmpObjNodes[coll]; } } else if (config.nodeColorAttribute !== '') { var attr = node[config.nodeColorAttribute]; if (attr) { if (tmpObjNodes.hasOwnProperty(attr)) { nodeObj.color = tmpObjNodes[attr]; } else { tmpObjNodes[attr] = colors.jans[Object.keys(tmpObjNodes).length]; nodeObj.color = tmpObjNodes[attr]; } } } nodeObj.sortColor = nodeObj.color; nodesObj[node._id] = nodeObj; } }); }); _.each(nodesObj, function (node) { if (config.nodeSizeByEdges === 'true') { // + 10 visual adjustment sigma node.size = nodeEdgesCount[node.id] + 10; // if a node without edges is found, use def. size 10 if (Number.isNaN(node.size)) { node.size = 10; } } nodesArr.push(node); }); var nodeNamesArr = []; _.each(nodeNames, function (found, key) { nodeNamesArr.push(key); }); // array format for sigma.js _.each(edgesObj, function (edge) { if (nodeNamesArr.indexOf(edge.source) > -1 && nodeNamesArr.indexOf(edge.target) > -1) { edgesArr.push(edge); } }); toReturn = { nodes: nodesArr, edges: edgesArr, settings: { vertexCollections: vertexCollections, startVertex: startVertex } }; if (isEnterprise) { if (graph.__isSmart) { toReturn.settings.isSmart = graph.__isSmart; toReturn.settings.smartGraphAttribute = graph.__smartGraphAttribute; } } } res.json(toReturn); }) .summary('Return vertices and edges of a graph.') .description(dd` This function returns vertices and edges for a specific graph. `);