1
0
Fork 0
arangodb/js/common/modules/org/arangodb/general-graph.js

856 lines
26 KiB
JavaScript

/*jslint indent: 2, nomen: true, maxlen: 100, sloppy: true, vars: true, white: true, plusplus: true */
/*global require, exports, Graph, arguments */
////////////////////////////////////////////////////////////////////////////////
/// @brief Graph functionality
///
/// @file
///
/// DISCLAIMER
///
/// Copyright 2010-2014 triagens 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 triAGENS GmbH, Cologne, Germany
///
/// @author Florian Bartels, Michael Hackstein, Guido Schwab
/// @author Copyright 2011-2014, triAGENS GmbH, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////
var arangodb = require("org/arangodb"),
ArangoCollection = arangodb.ArangoCollection,
ArangoError = arangodb.ArangoError,
db = arangodb.db,
errors = arangodb.errors,
_ = require("underscore");
// -----------------------------------------------------------------------------
// --SECTION-- module "org/arangodb/general-graph"
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// --SECTION-- private functions
// -----------------------------------------------------------------------------
////////////////////////////////////////////////////////////////////////////////
/// @brief transform a string into an array.
////////////////////////////////////////////////////////////////////////////////
var stringToArray = function (x) {
if (typeof x === "string") {
return [x];
}
return _.clone(x);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief checks if a parameter is not defined, an empty string or an empty
// array
////////////////////////////////////////////////////////////////////////////////
var isValidCollectionsParameter = function (x) {
if (!x) {
return false;
}
if (Array.isArray(x) && x.length === 0) {
return false;
}
if (typeof x !== "string" && !Array.isArray(x)) {
return false;
}
return true;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief find or create a collection by name
////////////////////////////////////////////////////////////////////////////////
var findOrCreateCollectionByName = function (name, type, noCreate) {
var col = db._collection(name),res = false;
if (col === null && !noCreate) {
if (type === ArangoCollection.TYPE_DOCUMENT) {
col = db._create(name);
} else {
col = db._createEdgeCollection(name);
}
res = true;
} else if (!(col instanceof ArangoCollection) || col.type() !== type) {
throw "<" + name + "> must be a " +
(type === ArangoCollection.TYPE_DOCUMENT ? "document" : "edge")
+ " collection";
}
return res;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief find or create a collection by name
////////////////////////////////////////////////////////////////////////////////
var findOrCreateCollectionsByEdgeDefinitions = function (edgeDefinitions, noCreate) {
var vertexCollections = {},
edgeCollections = {};
edgeDefinitions.forEach(function (e) {
e.from.concat(e.to).forEach(function (v) {
findOrCreateCollectionByName(v, ArangoCollection.TYPE_DOCUMENT, noCreate);
vertexCollections[v] = db[v];
});
findOrCreateCollectionByName(e.collection, ArangoCollection.TYPE_EDGE, noCreate);
edgeCollections[e.collection] = db[e.collection];
});
return [
vertexCollections,
edgeCollections
];
};
////////////////////////////////////////////////////////////////////////////////
/// @brief internal function to get graphs collection
////////////////////////////////////////////////////////////////////////////////
var getGraphCollection = function() {
var gCol = db._graphs;
if (gCol === null || gCol === undefined) {
throw "_graphs collection does not exist.";
}
return gCol;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief internal function to wrap arango collections for overwrite
////////////////////////////////////////////////////////////////////////////////
var wrapCollection = function(col) {
var wrapper = {};
_.each(_.functions(col), function(func) {
wrapper[func] = function() {
return col[func].apply(col, arguments);
};
});
return wrapper;
};
var transformExample = function(example) {
if (example === undefined) {
return {};
}
if (typeof example === "string") {
return {_id: example};
}
if (typeof example === "object") {
return example;
}
throw "Invalid example type. Has to be String, Array or Object";
};
// -----------------------------------------------------------------------------
// --SECTION-- module "org/arangodb/general-graph"
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// --SECTION-- Fluent AQL Interface
// -----------------------------------------------------------------------------
var AQLStatement = function(query, type) {
this.query = query;
if (type) {
this.type = type;
}
};
AQLStatement.prototype.printQuery = function() {
return this.query;
};
AQLStatement.prototype.isEdgeQuery = function() {
return this.type === "edge";
};
AQLStatement.prototype.isVertexQuery = function() {
return this.type === "vertex";
};
// -----------------------------------------------------------------------------
// --SECTION-- AQL Generator
// -----------------------------------------------------------------------------
var AQLGenerator = function(graph) {
this.stack = [];
this.bindVars = {
"graphName": graph.__name
};
this.graph = graph;
this.cursor = null;
this.lastVar = "";
};
AQLGenerator.prototype._clearCursor = function() {
if (this.cursor) {
this.cursor.dispose();
this.cursor = null;
}
};
AQLGenerator.prototype._createCursor = function() {
if (!this.cursor) {
this.cursor = this.execute();
}
};
AQLGenerator.prototype._edges = function(startVertex, direction) {
this._clearCursor();
var edgeName = "edges_" + this.stack.length;
var query = "FOR " + edgeName
+ " IN GRAPH_EDGES(@graphName,@startVertex_"
+ this.stack.length + ',"'
+ direction + '")';
this.bindVars["startVertex_" + this.stack.length] = startVertex;
var stmt = new AQLStatement(query, "edge");
this.stack.push(stmt);
this.lastVar = edgeName;
return this;
};
AQLGenerator.prototype.edges = function(example) {
return this._edges(example, "any");
};
AQLGenerator.prototype.outEdges = function(example) {
return this._edges(example, "outbound");
};
AQLGenerator.prototype.inEdges = function(example) {
return this._edges(example, "inbound");
};
AQLGenerator.prototype._vertices = function(example, direction) {
this._clearCursor();
var ex = transformExample(example);
var vertexName = "vertices_" + this.stack.length;
var query = "FOR " + vertexName
+ " IN GRAPH_VERTICIES(@graphName,@startEdge_"
+ this.stack.length + ','
+ '"' + direction + '")';
this.bindVars["startEdge_" + this.stack.length] = ex;
var stmt = new AQLStatement(query, "vertex");
this.stack.push(stmt);
this.lastVar = vertexName;
return this;
};
AQLGenerator.prototype.verticies = function(example) {
return this._verticies(example, "both");
};
AQLGenerator.prototype.getLastVar = function() {
if (this.lastVar === "") {
return false;
}
return this.lastVar;
};
AQLGenerator.prototype.restrict = function(restrictions) {
this._clearCursor();
var rest = stringToArray(restrictions);
var unknown = [];
var g = this.graph;
_.each(rest, function(r) {
if (!g.__edgeCollections[r]) {
unknown.push(r);
}
});
if (unknown.length > 0) {
var err = new ArangoError();
err.errorNum = arangodb.errors.ERROR_BAD_PARAMETER.code;
err.errorMessage = "edge collections: " + unknown.join(" and ") + " are not known to the graph";
throw err;
}
var lastQuery = this.stack.pop();
if (!lastQuery.isEdgeQuery()) {
this.stack.push(lastQuery);
throw "Restrict can only be applied directly after edge selectors";
}
lastQuery.query = lastQuery.query.replace(")", ",{},@restrictions_" + this.stack.length + ")");
lastQuery.edgeQuery = false;
this.bindVars["restrictions_" + this.stack.length] = rest;
this.stack.push(lastQuery);
return this;
};
AQLGenerator.prototype.filter = function(example) {
this._clearCursor();
var ex = [];
if (Object.prototype.toString.call(example) !== "[object Array]") {
if (Object.prototype.toString.call(example) !== "[object Object]") {
throw "The example has to be an Object, or an Array";
}
ex = [example];
} else {
ex = example;
}
var query = "FILTER MATCHES(" + this.getLastVar() + "," + JSON.stringify(ex) + ")";
this.stack.push(new AQLStatement(query));
return this;
};
AQLGenerator.prototype.printQuery = function() {
return this.stack.map(function(stmt) {
return stmt.printQuery();
}).join(" ");
};
AQLGenerator.prototype.execute = function() {
this._clearCursor();
var query = this.printQuery();
var bindVars = this.bindVars;
query += " RETURN " + this.getLastVar();
return db._query(query, bindVars, {count: true});
};
AQLGenerator.prototype.toArray = function() {
this._createCursor();
return this.cursor.toArray();
};
AQLGenerator.prototype.count = function() {
this._createCursor();
return this.cursor.count();
};
AQLGenerator.prototype.hasNext = function() {
this._createCursor();
return this.cursor.hasNext();
};
AQLGenerator.prototype.next = function() {
this._createCursor();
return this.cursor.next();
};
// -----------------------------------------------------------------------------
// --SECTION-- public functions
// -----------------------------------------------------------------------------
////////////////////////////////////////////////////////////////////////////////
/// @fn JSF_general_graph_undirectedRelationDefinition
/// @brief define an undirected relation.
///
/// @FUN{general-graph._undirectedRelationDefinition(@FA{relationName}, @FA{vertexCollections})}
///
/// Defines an undirected relation with the name @FA{relationName} using the
/// list of @FA{vertexCollections}. This relation allows the user to store
/// edges in any direction between any pair of vertices within the
/// @FA{vertexCollections}.
///
/// @EXAMPLES
///
/// To define simple relation with only one vertex collection:
///
/// @EXAMPLE_ARANGOSH_OUTPUT{generalGraphUndirectedRelationDefinition1}
/// var graph = require("org/arangodb/general-graph");
/// graph._undirectedRelationDefinition("friend", "user");
/// @END_EXAMPLE_ARANGOSH_OUTPUT
///
/// To define a relation between several vertex collections:
///
/// @EXAMPLE_ARANGOSH_OUTPUT{generalGraphUndirectedRelationDefinition2}
/// var graph = require("org/arangodb/general-graph");
/// graph._undirectedRelationDefinition("marriage", ["female", "male"]);
/// @END_EXAMPLE_ARANGOSH_OUTPUT
///
////////////////////////////////////////////////////////////////////////////////
var _undirectedRelationDefinition = function (relationName, vertexCollections) {
if (arguments.length < 2) {
throw "method _undirectedRelationDefinition expects 2 arguments";
}
if (typeof relationName !== "string" || relationName === "") {
throw "<relationName> must be a not empty string";
}
if (!isValidCollectionsParameter(vertexCollections)) {
throw "<vertexCollections> must be a not empty string or array";
}
return {
collection: relationName,
from: stringToArray(vertexCollections),
to: stringToArray(vertexCollections)
};
};
////////////////////////////////////////////////////////////////////////////////
/// @brief define an directed relation.
////////////////////////////////////////////////////////////////////////////////
var _directedRelationDefinition = function (
relationName, fromVertexCollections, toVertexCollections) {
if (arguments.length < 3) {
throw "method _directedRelationDefinition expects 3 arguments";
}
if (typeof relationName !== "string" || relationName === "") {
throw "<relationName> must be a not empty string";
}
if (!isValidCollectionsParameter(fromVertexCollections)) {
throw "<fromVertexCollections> must be a not empty string or array";
}
if (!isValidCollectionsParameter(toVertexCollections)) {
throw "<toVertexCollections> must be a not empty string or array";
}
return {
collection: relationName,
from: stringToArray(fromVertexCollections),
to: stringToArray(toVertexCollections)
};
};
////////////////////////////////////////////////////////////////////////////////
/// @brief create a list of edge definitions
////////////////////////////////////////////////////////////////////////////////
var _edgeDefinitions = function () {
var res = [], args = arguments;
Object.keys(args).forEach(function (x) {
res.push(args[x]);
});
return res;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief extend an edge definitions
////////////////////////////////////////////////////////////////////////////////
var _extendEdgeDefinitions = function () {
var args = arguments;
var res = args[0];
args = args.slice(1);
res.concat(args);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief create a new graph
////////////////////////////////////////////////////////////////////////////////
var _create = function (graphName, edgeDefinitions) {
var gdb = getGraphCollection(),
g,
graphAlreadyExists = true,
collections;
if (!graphName) {
throw "a graph name is required to create a graph.";
}
if (!Array.isArray(edgeDefinitions) || edgeDefinitions.length === 0) {
throw "at least one edge definition is required to create a graph.";
}
try {
g = gdb.document(graphName);
} catch (e) {
if (e.errorNum !== errors.ERROR_ARANGO_DOCUMENT_NOT_FOUND.code) {
throw e;
}
graphAlreadyExists = false;
}
if (graphAlreadyExists) {
throw "graph " + graphName + " already exists.";
}
collections = findOrCreateCollectionsByEdgeDefinitions(edgeDefinitions, false);
gdb.save({
'edgeDefinitions' : edgeDefinitions,
'_key' : graphName
});
return new Graph(graphName, edgeDefinitions, collections[0], collections[1]);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief constructor.
////////////////////////////////////////////////////////////////////////////////
var Graph = function(graphName, edgeDefinitions, vertexCollections, edgeCollections) {
var self = this;
this.__name = graphName;
this.__vertexCollections = vertexCollections;
this.__edgeCollections = edgeCollections;
this.__edgeDefinitions = edgeDefinitions;
_.each(vertexCollections, function(obj, key) {
var wrap = wrapCollection(obj);
var old_remove = wrap.remove;
wrap.remove = function(vertexId, options) {
var myEdges = self._EDGES(vertexId);
myEdges.forEach(
function(edgeObj) {
var edgeId = edgeObj._id;
var edgeCollection = edgeId.split("/")[0];
if (db[edgeCollection] && db[edgeCollection].exists(edgeId)) {
db[edgeCollection].remove(edgeId);
}
}
);
if (options === null || options === undefined) {
return old_remove(vertexId);
}
return old_remove(vertexId, options);
};
self[key] = wrap;
});
_.each(edgeCollections, function(obj, key) {
var wrap = wrapCollection(obj);
var old_save = wrap.save;
wrap.save = function(from, to, data) {
edgeDefinitions.forEach(
function(edgeDefinition) {
if (edgeDefinition.collection === key) {
var fromCollection = from.split("/")[0];
var toCollection = to.split("/")[0];
if (! _.contains(edgeDefinition.from, fromCollection)
|| ! _.contains(edgeDefinition.to, toCollection)) {
throw "Edge is not allowed between " + from + " and " + to + ".";
}
}
}
);
return old_save(from, to, data);
};
self[key] = wrap;
});
};
////////////////////////////////////////////////////////////////////////////////
/// @brief load a graph.
////////////////////////////////////////////////////////////////////////////////
var _graph = function(graphName) {
var gdb = getGraphCollection(),
g, collections;
try {
g = gdb.document(graphName);
}
catch (e) {
if (e.errorNum !== errors.ERROR_ARANGO_DOCUMENT_NOT_FOUND.code) {
throw e;
}
throw "graph " + graphName + " does not exists.";
}
collections = findOrCreateCollectionsByEdgeDefinitions(g.edgeDefinitions, true);
return new Graph(graphName, g.edgeDefinitions, collections[0], collections[1]);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief check if a graph exists.
////////////////////////////////////////////////////////////////////////////////
var _exists = function(graphId) {
var gCol = getGraphCollection();
return gCol.exists(graphId);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief Helper for dropping collections of a graph.
////////////////////////////////////////////////////////////////////////////////
var checkIfMayBeDropped = function(colName, graphName, graphs) {
var result = true;
graphs.forEach(
function(graph) {
if (graph._key === graphName) {
return;
}
var edgeDefinitions = graph.edgeDefinitions;
if (graph.edgeDefinitions) {
edgeDefinitions.forEach(
function(edgeDefinition) {
var from = edgeDefinition.from;
var to = edgeDefinition.to;
var collection = edgeDefinition.collection;
if (collection === colName || from.indexOf(colName) !== -1 || to.indexOf(colName) !== -1) {
result = false;
}
}
);
}
}
);
return result;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief drop a graph.
////////////////////////////////////////////////////////////////////////////////
var _drop = function(graphId, dropCollections) {
var gdb = getGraphCollection();
if (!gdb.exists(graphId)) {
throw "Graph " + graphId + " does not exist.";
}
if (dropCollections !== false) {
var graph = gdb.document(graphId);
var edgeDefinitions = graph.edgeDefinitions;
edgeDefinitions.forEach(
function(edgeDefinition) {
var from = edgeDefinition.from;
var to = edgeDefinition.to;
var collection = edgeDefinition.collection;
var graphs = getGraphCollection().toArray();
if (checkIfMayBeDropped(collection, graph._key, graphs)) {
db._drop(collection);
}
from.forEach(
function(col) {
if (checkIfMayBeDropped(col, graph._key, graphs)) {
db._drop(col);
}
}
);
to.forEach(
function(col) {
if (checkIfMayBeDropped(col, graph._key, graphs)) {
db._drop(col);
}
}
);
}
);
}
gdb.remove(graphId);
return true;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief return all edge collections of the graph.
////////////////////////////////////////////////////////////////////////////////
Graph.prototype._edgeCollections = function() {
return _.values(this.__edgeCollections);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief return all vertex collections of the graph.
////////////////////////////////////////////////////////////////////////////////
Graph.prototype._vertexCollections = function() {
return _.values(this.__vertexCollections);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief _EDGES(vertexId).
////////////////////////////////////////////////////////////////////////////////
// might be needed from AQL itself
Graph.prototype._EDGES = function(vertexId) {
var edgeCollections = this._edgeCollections();
var result = [];
edgeCollections.forEach(
function(edgeCollection) {
//todo: test, if collection may point to vertex
result = result.concat(edgeCollection.edges(vertexId));
}
);
return result;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief INEDGES(vertexId).
////////////////////////////////////////////////////////////////////////////////
Graph.prototype._INEDGES = function(vertexId) {
var edgeCollections = this._edgeCollections();
var result = [];
edgeCollections.forEach(
function(edgeCollection) {
//todo: test, if collection may point to vertex
result = result.concat(edgeCollection.inEdges(vertexId));
}
);
return result;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief outEdges(vertexId).
////////////////////////////////////////////////////////////////////////////////
Graph.prototype._OUTEDGES = function(vertexId) {
var edgeCollections = this._edgeCollections();
var result = [];
edgeCollections.forEach(
function(edgeCollection) {
//todo: test, if collection may point to vertex
result = result.concat(edgeCollection.outEdges(vertexId));
}
);
return result;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief _edges(vertexId).
////////////////////////////////////////////////////////////////////////////////
Graph.prototype._edges = function(vertexId) {
var AQLStmt = new AQLGenerator(this);
return AQLStmt.edges(vertexId, "any");
};
////////////////////////////////////////////////////////////////////////////////
/// @brief _inEdges(vertexId).
////////////////////////////////////////////////////////////////////////////////
Graph.prototype._inEdges = function(vertexId) {
var AQLStmt = new AQLGenerator(this);
return AQLStmt.edges(vertexId, "inbound");
};
////////////////////////////////////////////////////////////////////////////////
/// @brief _outEdges(vertexId).
////////////////////////////////////////////////////////////////////////////////
Graph.prototype._outEdges = function(vertexId) {
var AQLStmt = new AQLGenerator(this);
return AQLStmt.edges(vertexId, "outbound");
};
////////////////////////////////////////////////////////////////////////////////
/// @brief _vertices(edgeExample||edgeId).
////////////////////////////////////////////////////////////////////////////////
Graph.prototype._verticies = function(example) {
var AQLStmt = new AQLGenerator(this);
return AQLStmt.verticies(example);
};
////////////////////////////////////////////////////////////////////////////////
/// @brief get ingoing vertex of an edge.
////////////////////////////////////////////////////////////////////////////////
Graph.prototype._getInVertex = function(edgeId) {
var edgeCollection = this._getEdgeCollectionByName(edgeId.split("/")[0]);
var document = edgeCollection.document(edgeId);
if (document) {
var vertexId = document._from;
var vertexCollection = this._getVertexCollectionByName(vertexId.split("/")[0]);
return vertexCollection.document(vertexId);
}
};
////////////////////////////////////////////////////////////////////////////////
/// @brief get outgoing vertex of an edge .
////////////////////////////////////////////////////////////////////////////////
Graph.prototype._getOutVertex = function(edgeId) {
var edgeCollection = this._getEdgeCollectionByName(edgeId.split("/")[0]);
var document = edgeCollection.document(edgeId);
if (document) {
var vertexId = document._to;
var vertexCollection = this._getVertexCollectionByName(vertexId.split("/")[0]);
return vertexCollection.document(vertexId);
}
};
////////////////////////////////////////////////////////////////////////////////
/// @brief get edge collection by name.
////////////////////////////////////////////////////////////////////////////////
Graph.prototype._getEdgeCollectionByName = function(name) {
if (this.__edgeCollections[name]) {
return this.__edgeCollections[name];
}
throw "Collection " + name + " does not exist in graph.";
};
////////////////////////////////////////////////////////////////////////////////
/// @brief get vertex collection by name.
////////////////////////////////////////////////////////////////////////////////
Graph.prototype._getVertexCollectionByName = function(name) {
if (this.__vertexCollections[name]) {
return this.__vertexCollections[name];
}
throw "Collection " + name + " does not exist in graph.";
};
// -----------------------------------------------------------------------------
// --SECTION-- MODULE EXPORTS
// -----------------------------------------------------------------------------
exports._undirectedRelationDefinition = _undirectedRelationDefinition;
exports._directedRelationDefinition = _directedRelationDefinition;
exports._graph = _graph;
exports._edgeDefinitions = _edgeDefinitions;
exports._extendEdgeDefinitions = _extendEdgeDefinitions;
exports._create = _create;
exports._drop = _drop;
exports._exists = _exists;
// -----------------------------------------------------------------------------
// --SECTION-- END-OF-FILE
// -----------------------------------------------------------------------------
// Local Variables:
// mode: outline-minor
// outline-regexp: "^\\(/// @brief\\|/// @addtogroup\\|// --SECTION--\\|/// @page\\|/// @}\\)"
// End: