1
0
Fork 0
arangodb/js/apps/system/_admin/aardvark/APP/aardvark.js

679 lines
19 KiB
JavaScript

/* 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.
`);