From 9599a021b999d26ff3554c9e364178a3c1f06811 Mon Sep 17 00:00:00 2001 From: Jan Steemann Date: Wed, 12 Jun 2013 17:04:47 +0200 Subject: [PATCH] moved some graph traversal validation into the traversal module - that simplifies some of the traversal-calling routines - make returning error codes more consistent (specific 404 errors when referring to unknown collections instead of simple 400 errors) - optionally limit traversals to a certain number of iterations so they don't run endlessly in cyclic graphs --- CHANGELOG | 15 +- Documentation/UserManual/Aql.md | 4 + UnitTests/HttpInterface/api-cursor-spec.rb | 4 +- UnitTests/HttpInterface/api-explain-spec.rb | 4 +- .../HttpInterface/api-transactions-spec.rb | 4 +- UnitTests/HttpInterface/api-traversal-spec.rb | 36 +++- html/admin/js/bootstrap/errors.js | 1 + .../modules/org/arangodb/graph/traversal.js | 157 +++++++++++++++--- js/actions/api-traversal.js | 78 +++------ js/common/bootstrap/errors.js | 1 + .../modules/org/arangodb/graph/traversal.js | 157 +++++++++++++++--- js/server/modules/org/arangodb/actions.js | 6 + js/server/modules/org/arangodb/ahuacatl.js | 66 +------- lib/BasicsC/errors.dat | 3 +- lib/BasicsC/voc-errors.c | 1 + lib/BasicsC/voc-errors.h | 12 ++ 16 files changed, 375 insertions(+), 174 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 15a383a8fe..0b892de8bb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,18 @@ v1.4 ------- +---- + +* changed the HTTP return code from 400 to 404 for some cases when there is a referral + to a non-existing collection or document. + +* introduced error code 1909 `too many iterations` that is thrown when graph traversals + hit the `maxIterations` threshold. + +* optionally limit traversals to a certain number of iterations + the limitation can be achieved via the traversal API by setting the `maxIterations` + attribute, and also via the AQL `TRAVERSAL` and `TRAVERSAL_TREE` functions by setting + the same attribute. If traversals are not limited by the end user, a server-defined + limit for `maxIterations` may be used to prevent server-side traversals from running + endlessly. * added graph traversal API at `/_api/traversal` diff --git a/Documentation/UserManual/Aql.md b/Documentation/UserManual/Aql.md index 170ce35227..47483625b2 100644 --- a/Documentation/UserManual/Aql.md +++ b/Documentation/UserManual/Aql.md @@ -1277,6 +1277,10 @@ Example calls: - `minDepth`: Minimum path depths for vertices to be included. This can be used to include only vertices in the result that are found after a certain minimum depth. Defaults to 0. + - `maxIterations`: Maximum number of iterations in each traversal. This number can be + set to prevent endless loops in traversal of cyclic graphs. When a traversal performs + as many iterations as the `maxIterations` value, the traversal will abort with an + error. If `maxIterations` is not set, a server-defined value may be used. - `maxDepth`: Maximum path depth for sub-edges expansion. This can be used to limit the depth of the traversal to a sensible amount. This should especially be used for big graphs to limit the traversal to some sensible amount, and for graphs diff --git a/UnitTests/HttpInterface/api-cursor-spec.rb b/UnitTests/HttpInterface/api-cursor-spec.rb index f4e6126806..b2167260ee 100644 --- a/UnitTests/HttpInterface/api-cursor-spec.rb +++ b/UnitTests/HttpInterface/api-cursor-spec.rb @@ -33,10 +33,10 @@ describe ArangoDB do body = "{ \"query\" : \"FOR u IN unknowncollection LIMIT 2 RETURN u.n\", \"count\" : true, \"bindVars\" : {}, \"batchSize\" : 2 }" doc = ArangoDB.log_post("#{prefix}-unknown-collection", cmd, :body => body) - doc.code.should eq(400) + doc.code.should eq(404) doc.headers['content-type'].should eq("application/json; charset=utf-8") doc.parsed_response['error'].should eq(true) - doc.parsed_response['code'].should eq(400) + doc.parsed_response['code'].should eq(404) doc.parsed_response['errorNum'].should eq(1203) end diff --git a/UnitTests/HttpInterface/api-explain-spec.rb b/UnitTests/HttpInterface/api-explain-spec.rb index 1d993b4c49..2dbbeec047 100644 --- a/UnitTests/HttpInterface/api-explain-spec.rb +++ b/UnitTests/HttpInterface/api-explain-spec.rb @@ -29,10 +29,10 @@ describe ArangoDB do body = "{ \"query\" : \"FOR u IN unknowncollection LIMIT 2 RETURN u.n\" }" doc = ArangoDB.log_post("#{prefix}-unknown-collection", cmd, :body => body) - doc.code.should eq(400) + doc.code.should eq(404) doc.headers['content-type'].should eq("application/json; charset=utf-8") doc.parsed_response['error'].should eq(true) - doc.parsed_response['code'].should eq(400) + doc.parsed_response['code'].should eq(404) doc.parsed_response['errorNum'].should eq(1203) end diff --git a/UnitTests/HttpInterface/api-transactions-spec.rb b/UnitTests/HttpInterface/api-transactions-spec.rb index e6745c4efa..1bcf317a22 100644 --- a/UnitTests/HttpInterface/api-transactions-spec.rb +++ b/UnitTests/HttpInterface/api-transactions-spec.rb @@ -180,10 +180,10 @@ describe ArangoDB do body = "{ \"collections\" : { \"write\": \"_meow\" }, \"action\" : \"function () { return 1; }\" }" doc = ArangoDB.log_post("#{prefix}-non-existing-collection", cmd, :body => body) - doc.code.should eq(400) + doc.code.should eq(404) doc.headers['content-type'].should eq("application/json; charset=utf-8") doc.parsed_response['error'].should eq(true) - doc.parsed_response['code'].should eq(400) + doc.parsed_response['code'].should eq(404) doc.parsed_response['errorNum'].should eq(1203) end diff --git a/UnitTests/HttpInterface/api-traversal-spec.rb b/UnitTests/HttpInterface/api-traversal-spec.rb index 5b2f7f8248..c610d7d203 100644 --- a/UnitTests/HttpInterface/api-traversal-spec.rb +++ b/UnitTests/HttpInterface/api-traversal-spec.rb @@ -22,7 +22,7 @@ describe ArangoDB do cmd = "/_api/document?collection=#{@cv}" [ - "World", "Nothing", "Europe", "Asia", "America", "Australia", "Antarctica", "Africa", "Blackhole", + "World", "Nothing", "Europe", "Asia", "America", "Australia", "Antarctica", "Africa", "Blackhole", "Blackhole2", "DE", "FR", "GB", "IE", "CN", "JP", "TW", "US", "MX", "AU", "EG", "ZA", "AN", "London", "Paris", "Lyon", "Cologne","Dusseldorf", "Beijing", "Shanghai", "Tokyo", "Kyoto", "Taipeh", "Perth", "Sydney" ].each do|loc| @@ -44,7 +44,9 @@ describe ArangoDB do ["Asia", "TW"], ["America", "US"], ["America", "MX"], - ["Australia", "AU"] + ["Australia", "AU"], + ["Blackhole", "Blackhole2"], + ["Blackhole2", "Blackhole"] ].each do|pair| from = pair[0] to = pair[1] @@ -151,7 +153,7 @@ describe ArangoDB do it "invalid direction" do body = "{ \"edgeCollection\" : \"#{@ce}\", \"startVertex\" : \"#{@cv}/World\", \"direction\" : \"foo\" }" doc = ArangoDB.log_post("#{prefix}-visit-invalid-direction", api, :body => body) - + doc.code.should eq(400) doc.headers['content-type'].should eq("application/json; charset=utf-8") @@ -176,6 +178,34 @@ describe ArangoDB do doc.parsed_response['errorNum'].should eq(500) end +################################################################################ +## traversal abortion +################################################################################ + + it "traversal abortion, few iterations" do + body = "{ \"edgeCollection\" : \"#{@ce}\", \"startVertex\" : \"#{@cv}/Blackhole\", \"direction\" : \"outbound\", \"uniqueness\" : { \"vertices\" : \"none\", \"edges\" : \"none\" }, \"maxIterations\" : 5 }" + doc = ArangoDB.log_post("#{prefix}-visit-traversal-abort1", api, :body => body) + + doc.code.should eq(500) + + doc.headers['content-type'].should eq("application/json; charset=utf-8") + doc.parsed_response['error'].should eq(true) + doc.parsed_response['code'].should eq(500) + doc.parsed_response['errorNum'].should eq(1909) + end + + it "traversal abortion, many iterations" do + body = "{ \"edgeCollection\" : \"#{@ce}\", \"startVertex\" : \"#{@cv}/Blackhole\", \"direction\" : \"outbound\", \"uniqueness\" : { \"vertices\" : \"none\", \"edges\" : \"none\" }, \"maxIterations\" : 5000, \"maxDepth\" : 999999, \"visitor\" : \"\" }" + doc = ArangoDB.log_post("#{prefix}-visit-traversal-abort2", api, :body => body) + + doc.code.should eq(500) + + doc.headers['content-type'].should eq("application/json; charset=utf-8") + doc.parsed_response['error'].should eq(true) + doc.parsed_response['code'].should eq(500) + doc.parsed_response['errorNum'].should eq(1909) + end + end ################################################################################ diff --git a/html/admin/js/bootstrap/errors.js b/html/admin/js/bootstrap/errors.js index 7132aabfb9..8f8b4790e3 100644 --- a/html/admin/js/bootstrap/errors.js +++ b/html/admin/js/bootstrap/errors.js @@ -144,6 +144,7 @@ "ERROR_GRAPH_INVALID_EDGE" : { "code" : 1906, "message" : "invalid edge" }, "ERROR_GRAPH_COULD_NOT_CREATE_EDGE" : { "code" : 1907, "message" : "could not create edge" }, "ERROR_GRAPH_COULD_NOT_CHANGE_EDGE" : { "code" : 1908, "message" : "could not change edge" }, + "ERROR_GRAPH_TOO_MANY_ITERATIONS" : { "code" : 1909, "message" : "too many iterations" }, "ERROR_SESSION_INVALID_SESSION" : { "code" : 1951, "message" : "invalid session" }, "ERROR_SESSION_COULD_NOT_CREATE_SESSION" : { "code" : 1952, "message" : "could not create session" }, "ERROR_SESSION_COULD_NOT_CHANGE_SESSION" : { "code" : 1953, "message" : "could not change session" }, diff --git a/html/admin/js/modules/org/arangodb/graph/traversal.js b/html/admin/js/modules/org/arangodb/graph/traversal.js index 54464b1e9b..001d3ea189 100644 --- a/html/admin/js/modules/org/arangodb/graph/traversal.js +++ b/html/admin/js/modules/org/arangodb/graph/traversal.js @@ -31,6 +31,7 @@ module.define("org/arangodb/graph/traversal", function(exports, module) { var graph = require("org/arangodb/graph"); var arangodb = require("org/arangodb"); +var ArangoError = arangodb.ArangoError; var db = arangodb.db; @@ -661,6 +662,7 @@ function breadthFirstSearch () { }, run: function (config, result, startVertex) { + var maxIterations = config.maxIterations, visitCounter = 0; var toVisit = [ { edge: null, vertex: startVertex, parentIndex: -1 } ]; var visited = { edges: { }, vertices: { } }; @@ -674,6 +676,13 @@ function breadthFirstSearch () { var vertex = current.vertex; var edge = current.edge; var path; + + if (visitCounter++ > maxIterations) { + var err = new ArangoError(); + err.errorNum = arangodb.errors.ERROR_GRAPH_TOO_MANY_ITERATIONS.code; + err.errorMessage = arangodb.errors.ERROR_GRAPH_TOO_MANY_ITERATIONS.message; + throw err; + } if (current.visit === null || current.visit === undefined) { current.visit = false; @@ -757,12 +766,20 @@ function depthFirstSearch () { }, run: function (config, result, startVertex) { + var maxIterations = config.maxIterations, visitCounter = 0; var toVisit = [ { edge: null, vertex: startVertex, visit: null } ]; var path = { edges: [ ], vertices: [ ] }; var visited = { edges: { }, vertices: { } }; var reverse = checkReverse(config); while (toVisit.length > 0) { + if (visitCounter++ > maxIterations) { + var err = new ArangoError(); + err.errorNum = arangodb.errors.ERROR_GRAPH_TOO_MANY_ITERATIONS.code; + err.errorMessage = arangodb.errors.ERROR_GRAPH_TOO_MANY_ITERATIONS.message; + throw err; + } + // peek at the top of the stack var current = toVisit[toVisit.length - 1]; var vertex = current.vertex; @@ -871,52 +888,150 @@ ArangoTraverser = function (config) { visitor: trackingVisitor, filter: visitAllFilter, expander: outboundExpander, - datasource: null + datasource: null, + maxIterations: 10000, + minDepth: 0, + maxDepth: 256 }, d; + var err; + if (typeof config !== "object") { - throw "invalid configuration"; + err = new ArangoError(); + err.errorNum = arangodb.errors.ERROR_BAD_PARAMETER.code; + err.errorMessage = arangodb.errors.ERROR_BAD_PARAMETER.message; + throw err; } // apply defaults for (d in defaults) { if (defaults.hasOwnProperty(d)) { - if (! config.hasOwnProperty(d)) { + if (! config.hasOwnProperty(d) || config[d] === undefined) { config[d] = defaults[d]; } } } - if (typeof config.visitor !== "function") { - throw "invalid visitor"; - } - - if (Array.isArray(config.filter)) { - config.filter.forEach( function (f) { - if (typeof f !== "function") { - throw "invalid filter"; + function validate (value, map, param) { + var m; + + if (value === null || value === undefined) { + // use first key from map + for (m in map) { + if (map.hasOwnProperty(m)) { + value = m; + break; + } } - }); + } + if (typeof value === 'string') { + value = value.toLowerCase().replace(/-/, ""); + if (map[value] !== null) { + return map[value]; + } + } + for (m in map) { + if (map.hasOwnProperty(m)) { + if (map[m] === value) { + return value; + } + } + } - var innerFilters = config.filter.slice(); + err = new ArangoError(); + err.errorNum = arangodb.errors.ERROR_BAD_PARAMETER.code; + err.errorMessage = "invalid value for " + param; + throw err; + } + + config.uniqueness = { + vertices: validate(config.uniqueness && config.uniqueness.vertices, { + global: ArangoTraverser.UNIQUE_GLOBAL, + none: ArangoTraverser.UNIQUE_NONE, + path: ArangoTraverser.UNIQUE_PATH + }, "uniqueness.vertices"), + edges: validate(config.uniqueness && config.uniqueness.edges, { + global: ArangoTraverser.UNIQUE_GLOBAL, + none: ArangoTraverser.UNIQUE_NONE, + path: ArangoTraverser.UNIQUE_PATH + }, "uniqueness.edges") + }; + + config.strategy = validate(config.strategy, { + depthfirst: ArangoTraverser.DEPTH_FIRST, + breadthfirst: ArangoTraverser.BREADTH_FIRST + }, "strategy"); - var combinedFilter = function (config, vertex, path) { - return combineFilters(innerFilters, config, vertex, path); - }; + config.order = validate(config.order, { + preorder: ArangoTraverser.PRE_ORDER, + postorder: ArangoTraverser.POST_ORDER + }, "order"); - config.filter = combinedFilter; + config.itemOrder = validate(config.itemOrder, { + forward: ArangoTraverser.FORWARD, + backward: ArangoTraverser.BACKWARD + }, "itemOrder"); + + + if (typeof config.visitor !== "function") { + err = new ArangoError(); + err.errorNum = arangodb.errors.ERROR_BAD_PARAMETER.code; + err.errorMessage = "invalid visitor function"; + throw err; } - if (typeof config.filter !== "function") { - throw "invalid filter"; + // prepare an array of filters + var filters = [ ]; + if (config.minDepth !== undefined && config.minDepth >= 0) { + filters.push(minDepthFilter); + } + if (config.maxDepth !== undefined && config.maxDepth > 0) { + filters.push(maxDepthFilter); + } + + if (! Array.isArray(config.filter)) { + config.filter = [ config.filter ]; + } + + config.filter.forEach( function (f) { + if (typeof f !== "function") { + err = new ArangoError(); + err.errorNum = arangodb.errors.ERROR_BAD_PARAMETER.code; + err.errorMessage = "invalid filter function"; + throw err; + } + + filters.push(f); + }); + + if (filters.length === 0) { + filters.push(visitAllFilter); + } + + config.filter = function (config, vertex, path) { + return combineFilters(filters, config, vertex, path); + }; + + if (typeof config.expander !== "function") { + config.expander = validate(config.expander, { + outbound: outboundExpander, + inbound: inboundExpander, + any: anyExpander + }, "expander"); } if (typeof config.expander !== "function") { - throw "invalid expander"; + err = new ArangoError(); + err.errorNum = arangodb.errors.ERROR_BAD_PARAMETER.code; + err.errorMessage = "invalid expander function"; + throw err; } if (typeof config.datasource !== "object") { - throw "invalid datasource"; + err = new ArangoError(); + err.errorNum = arangodb.errors.ERROR_BAD_PARAMETER.code; + err.errorMessage = "invalid datasource"; + throw err; } this.config = config; diff --git a/js/actions/api-traversal.js b/js/actions/api-traversal.js index 617c7b3d98..9889fd6b0b 100644 --- a/js/actions/api-traversal.js +++ b/js/actions/api-traversal.js @@ -65,32 +65,6 @@ function notFound (req, res, code, message) { message); } -//////////////////////////////////////////////////////////////////////////////// -/// @brief validate and translate the argument given in value -//////////////////////////////////////////////////////////////////////////////// - -function validateArg (value, map) { - if (value === null || value === undefined) { - var m; - // use first key from map - for (m in map) { - if (map.hasOwnProperty(m)) { - value = m; - break; - } - } - } - - if (typeof value === 'string') { - value = value.toLowerCase().replace(/-/, ""); - if (map[value] !== null) { - return map[value]; - } - } - - return undefined; -} - //////////////////////////////////////////////////////////////////////////////// /// @brief execute a server-side traversal /// @@ -134,6 +108,11 @@ function validateArg (value, map) { /// uniqueness: specifies uniqueness for vertices and edges visited, optional /// if set, must be an object like this: "uniqueness": { "vertices": "none"|"global"|path", "edges": "none"|"global"|"path" } /// +/// maxIterations: Maximum number of iterations in each traversal. This number can be +/// set to prevent endless loops in traversal of cyclic graphs. When a traversal performs +/// as many iterations as the `maxIterations` value, the traversal will abort with an +/// error. If `maxIterations` is not set, a server-defined value may be used. +/// /// the "result" object will be returned by the traversal /// /// example: @@ -181,7 +160,8 @@ function post_api_traversal(req, res) { catch (err2) { } - if (edgeCollection === undefined || edgeCollection == null) { + if (edgeCollection === undefined || + edgeCollection === null) { return notFound(req, res, arangodb.ERROR_ARANGO_COLLECTION_NOT_FOUND, "invalid edgeCollection"); } @@ -232,11 +212,7 @@ function post_api_traversal(req, res) { var expander; if (json.direction !== undefined) { - expander = validateArg(json.direction, { - 'outbound': traversal.outboundExpander, - 'inbound': traversal.inboundExpander, - 'any': traversal.anyExpander - }); + expander = json.direction; } else if (json.expander !== undefined) { try { @@ -259,35 +235,16 @@ function post_api_traversal(req, res) { params: json, edgeCollection: edgeCollection, datasource: traversal.collectionDatasourceFactory(edgeCollection), - strategy: validateArg(json.strategy, { - 'depthfirst': Traverser.DEPTH_FIRST, - 'breadthfirst': Traverser.BREADTH_FIRST - }), - order: validateArg(json.order, { - 'preorder': Traverser.PRE_ORDER, - 'postorder': Traverser.POST_ORDER - }), - itemOrder: validateArg(json.itemOrder, { - 'forward': Traverser.FORWARD, - 'backward': Traverser.BACKWARD - }), + strategy: json.strategy, + order: json.order, + itemOrder: json.itemOrder, expander: expander, visitor: visitor, filter: filters, - minDepth: json.minDepth || 0, + minDepth: json.minDepth, maxDepth: json.maxDepth, - uniqueness: { - vertices: validateArg(json.uniqueness && json.uniqueness.vertices, { - 'global': Traverser.UNIQUE_GLOBAL, - 'none': Traverser.UNIQUE_NONE, - 'path': Traverser.UNIQUE_PATH - }), - edges: validateArg(json.uniqueness && json.uniqueness.edges, { - 'global': Traverser.UNIQUE_GLOBAL, - 'none': Traverser.UNIQUE_NONE, - 'path': Traverser.UNIQUE_PATH - }) - } + maxIterations: json.maxIterations, + uniqueness: json.uniqueness }; // assemble result object @@ -313,12 +270,17 @@ function post_api_traversal(req, res) { // run the traversal // ----------------------------------------- - var traverser = new Traverser(config); + var traverser; try { + traverser = new Traverser(config); traverser.traverse(result, doc); actions.resultOk(req, res, actions.HTTP_OK, { result : result }); } catch (err7) { + if (traverser === undefined) { + // error during traversal setup + return badParam(req, res, err7); + } actions.resultException(req, res, err7, undefined, false); } } diff --git a/js/common/bootstrap/errors.js b/js/common/bootstrap/errors.js index 7132aabfb9..8f8b4790e3 100644 --- a/js/common/bootstrap/errors.js +++ b/js/common/bootstrap/errors.js @@ -144,6 +144,7 @@ "ERROR_GRAPH_INVALID_EDGE" : { "code" : 1906, "message" : "invalid edge" }, "ERROR_GRAPH_COULD_NOT_CREATE_EDGE" : { "code" : 1907, "message" : "could not create edge" }, "ERROR_GRAPH_COULD_NOT_CHANGE_EDGE" : { "code" : 1908, "message" : "could not change edge" }, + "ERROR_GRAPH_TOO_MANY_ITERATIONS" : { "code" : 1909, "message" : "too many iterations" }, "ERROR_SESSION_INVALID_SESSION" : { "code" : 1951, "message" : "invalid session" }, "ERROR_SESSION_COULD_NOT_CREATE_SESSION" : { "code" : 1952, "message" : "could not create session" }, "ERROR_SESSION_COULD_NOT_CHANGE_SESSION" : { "code" : 1953, "message" : "could not change session" }, diff --git a/js/common/modules/org/arangodb/graph/traversal.js b/js/common/modules/org/arangodb/graph/traversal.js index 8c4e5bf9be..5b429abe07 100644 --- a/js/common/modules/org/arangodb/graph/traversal.js +++ b/js/common/modules/org/arangodb/graph/traversal.js @@ -30,6 +30,7 @@ var graph = require("org/arangodb/graph"); var arangodb = require("org/arangodb"); +var ArangoError = arangodb.ArangoError; var db = arangodb.db; @@ -660,6 +661,7 @@ function breadthFirstSearch () { }, run: function (config, result, startVertex) { + var maxIterations = config.maxIterations, visitCounter = 0; var toVisit = [ { edge: null, vertex: startVertex, parentIndex: -1 } ]; var visited = { edges: { }, vertices: { } }; @@ -673,6 +675,13 @@ function breadthFirstSearch () { var vertex = current.vertex; var edge = current.edge; var path; + + if (visitCounter++ > maxIterations) { + var err = new ArangoError(); + err.errorNum = arangodb.errors.ERROR_GRAPH_TOO_MANY_ITERATIONS.code; + err.errorMessage = arangodb.errors.ERROR_GRAPH_TOO_MANY_ITERATIONS.message; + throw err; + } if (current.visit === null || current.visit === undefined) { current.visit = false; @@ -756,12 +765,20 @@ function depthFirstSearch () { }, run: function (config, result, startVertex) { + var maxIterations = config.maxIterations, visitCounter = 0; var toVisit = [ { edge: null, vertex: startVertex, visit: null } ]; var path = { edges: [ ], vertices: [ ] }; var visited = { edges: { }, vertices: { } }; var reverse = checkReverse(config); while (toVisit.length > 0) { + if (visitCounter++ > maxIterations) { + var err = new ArangoError(); + err.errorNum = arangodb.errors.ERROR_GRAPH_TOO_MANY_ITERATIONS.code; + err.errorMessage = arangodb.errors.ERROR_GRAPH_TOO_MANY_ITERATIONS.message; + throw err; + } + // peek at the top of the stack var current = toVisit[toVisit.length - 1]; var vertex = current.vertex; @@ -870,52 +887,150 @@ ArangoTraverser = function (config) { visitor: trackingVisitor, filter: visitAllFilter, expander: outboundExpander, - datasource: null + datasource: null, + maxIterations: 10000, + minDepth: 0, + maxDepth: 256 }, d; + var err; + if (typeof config !== "object") { - throw "invalid configuration"; + err = new ArangoError(); + err.errorNum = arangodb.errors.ERROR_BAD_PARAMETER.code; + err.errorMessage = arangodb.errors.ERROR_BAD_PARAMETER.message; + throw err; } // apply defaults for (d in defaults) { if (defaults.hasOwnProperty(d)) { - if (! config.hasOwnProperty(d)) { + if (! config.hasOwnProperty(d) || config[d] === undefined) { config[d] = defaults[d]; } } } - if (typeof config.visitor !== "function") { - throw "invalid visitor"; - } - - if (Array.isArray(config.filter)) { - config.filter.forEach( function (f) { - if (typeof f !== "function") { - throw "invalid filter"; + function validate (value, map, param) { + var m; + + if (value === null || value === undefined) { + // use first key from map + for (m in map) { + if (map.hasOwnProperty(m)) { + value = m; + break; + } } - }); + } + if (typeof value === 'string') { + value = value.toLowerCase().replace(/-/, ""); + if (map[value] !== null) { + return map[value]; + } + } + for (m in map) { + if (map.hasOwnProperty(m)) { + if (map[m] === value) { + return value; + } + } + } - var innerFilters = config.filter.slice(); + err = new ArangoError(); + err.errorNum = arangodb.errors.ERROR_BAD_PARAMETER.code; + err.errorMessage = "invalid value for " + param; + throw err; + } + + config.uniqueness = { + vertices: validate(config.uniqueness && config.uniqueness.vertices, { + global: ArangoTraverser.UNIQUE_GLOBAL, + none: ArangoTraverser.UNIQUE_NONE, + path: ArangoTraverser.UNIQUE_PATH + }, "uniqueness.vertices"), + edges: validate(config.uniqueness && config.uniqueness.edges, { + global: ArangoTraverser.UNIQUE_GLOBAL, + none: ArangoTraverser.UNIQUE_NONE, + path: ArangoTraverser.UNIQUE_PATH + }, "uniqueness.edges") + }; + + config.strategy = validate(config.strategy, { + depthfirst: ArangoTraverser.DEPTH_FIRST, + breadthfirst: ArangoTraverser.BREADTH_FIRST + }, "strategy"); - var combinedFilter = function (config, vertex, path) { - return combineFilters(innerFilters, config, vertex, path); - }; + config.order = validate(config.order, { + preorder: ArangoTraverser.PRE_ORDER, + postorder: ArangoTraverser.POST_ORDER + }, "order"); - config.filter = combinedFilter; + config.itemOrder = validate(config.itemOrder, { + forward: ArangoTraverser.FORWARD, + backward: ArangoTraverser.BACKWARD + }, "itemOrder"); + + + if (typeof config.visitor !== "function") { + err = new ArangoError(); + err.errorNum = arangodb.errors.ERROR_BAD_PARAMETER.code; + err.errorMessage = "invalid visitor function"; + throw err; } - if (typeof config.filter !== "function") { - throw "invalid filter"; + // prepare an array of filters + var filters = [ ]; + if (config.minDepth !== undefined && config.minDepth >= 0) { + filters.push(minDepthFilter); + } + if (config.maxDepth !== undefined && config.maxDepth > 0) { + filters.push(maxDepthFilter); + } + + if (! Array.isArray(config.filter)) { + config.filter = [ config.filter ]; + } + + config.filter.forEach( function (f) { + if (typeof f !== "function") { + err = new ArangoError(); + err.errorNum = arangodb.errors.ERROR_BAD_PARAMETER.code; + err.errorMessage = "invalid filter function"; + throw err; + } + + filters.push(f); + }); + + if (filters.length === 0) { + filters.push(visitAllFilter); + } + + config.filter = function (config, vertex, path) { + return combineFilters(filters, config, vertex, path); + }; + + if (typeof config.expander !== "function") { + config.expander = validate(config.expander, { + outbound: outboundExpander, + inbound: inboundExpander, + any: anyExpander + }, "expander"); } if (typeof config.expander !== "function") { - throw "invalid expander"; + err = new ArangoError(); + err.errorNum = arangodb.errors.ERROR_BAD_PARAMETER.code; + err.errorMessage = "invalid expander function"; + throw err; } if (typeof config.datasource !== "object") { - throw "invalid datasource"; + err = new ArangoError(); + err.errorNum = arangodb.errors.ERROR_BAD_PARAMETER.code; + err.errorMessage = "invalid datasource"; + throw err; } this.config = config; diff --git a/js/server/modules/org/arangodb/actions.js b/js/server/modules/org/arangodb/actions.js index 9580177bf0..01b05d9d93 100644 --- a/js/server/modules/org/arangodb/actions.js +++ b/js/server/modules/org/arangodb/actions.js @@ -1853,8 +1853,14 @@ function resultException (req, res, err, headers, verbose) { switch (num) { case arangodb.ERROR_INTERNAL: case arangodb.ERROR_OUT_OF_MEMORY: + case arangodb.ERROR_GRAPH_TOO_MANY_ITERATIONS: code = exports.HTTP_SERVER_ERROR; break; + + case arangodb.ERROR_ARANGO_COLLECTION_NOT_FOUND: + case arangodb.ERROR_ARANGO_DOCUMENT_NOT_FOUND: + code = exports.HTTP_NOT_FOUND; + break; case arangodb.ERROR_ARANGO_DUPLICATE_NAME: case arangodb.ERROR_ARANGO_DUPLICATE_IDENTIFIER: diff --git a/js/server/modules/org/arangodb/ahuacatl.js b/js/server/modules/org/arangodb/ahuacatl.js index 16a0074e32..5efdce8d55 100644 --- a/js/server/modules/org/arangodb/ahuacatl.js +++ b/js/server/modules/org/arangodb/ahuacatl.js @@ -3478,27 +3478,6 @@ function TRAVERSAL_FUNC (func, vertexCollection, edgeCollection, startVertex, di }); } - function validate (value, map) { - if (value === null || value === undefined) { - var m; - // use first key from map - for (m in map) { - if (map.hasOwnProperty(m)) { - value = m; - break; - } - } - } - if (typeof value === 'string') { - value = value.toLowerCase().replace(/-/, ""); - if (map[value] !== null) { - return map[value]; - } - } - - THROW(INTERNAL.errors.ERROR_QUERY_FUNCTION_ARGUMENT_TYPE_MISMATCH, func); - } - if (typeof params.visitor !== "function") { THROW(INTERNAL.errors.ERROR_QUERY_FUNCTION_ARGUMENT_TYPE_MISMATCH, func); } @@ -3508,55 +3487,16 @@ function TRAVERSAL_FUNC (func, vertexCollection, edgeCollection, startVertex, di params.maxDepth = 256; } - // prepare an array of filters - var filters = [ ]; - if (params.minDepth !== undefined) { - filters.push(TRAVERSAL.minDepthFilter); - } - if (params.maxDepth !== undefined) { - filters.push(TRAVERSAL.maxDepthFilter); - } - if (filters.length === 0) { - filters.push(TRAVERSAL.visitAllFilter); - } - var config = { connect: params.connect, datasource: TRAVERSAL.collectionDatasourceFactory(edgeCollection), - strategy: validate(params.strategy, { - 'depthfirst': TRAVERSAL.Traverser.DEPTH_FIRST, - 'breadthfirst': TRAVERSAL.Traverser.BREADTH_FIRST - }), - order: validate(params.order, { - 'preorder': TRAVERSAL.Traverser.PRE_ORDER, - 'postorder': TRAVERSAL.Traverser.POST_ORDER - }), - itemOrder: validate(params.itemOrder, { - 'forward': TRAVERSAL.Traverser.FORWARD, - 'backward': TRAVERSAL.Traverser.BACKWARD - }), trackPaths: params.paths || false, visitor: params.visitor, maxDepth: params.maxDepth, minDepth: params.minDepth, - filter: filters, - uniqueness: { - vertices: validate(params.uniqueness && params.uniqueness.vertices, { - 'global': TRAVERSAL.Traverser.UNIQUE_GLOBAL, - 'none': TRAVERSAL.Traverser.UNIQUE_NONE, - 'path': TRAVERSAL.Traverser.UNIQUE_PATH - }), - edges: validate(params.uniqueness && params.uniqueness.edges, { - 'global': TRAVERSAL.Traverser.UNIQUE_GLOBAL, - 'none': TRAVERSAL.Traverser.UNIQUE_NONE, - 'path': TRAVERSAL.Traverser.UNIQUE_PATH - }) - }, - expander: validate(direction, { - 'outbound': TRAVERSAL.outboundExpander, - 'inbound': TRAVERSAL.inboundExpander, - 'any': TRAVERSAL.anyExpander - }) + maxIterations: params.maxIterations, + uniqueness: params.uniqueness, + expander: direction }; if (params.followEdges) { diff --git a/lib/BasicsC/errors.dat b/lib/BasicsC/errors.dat index edd9e754cf..a3d23ced3f 100755 --- a/lib/BasicsC/errors.dat +++ b/lib/BasicsC/errors.dat @@ -198,7 +198,7 @@ ERROR_KEYVALUE_KEY_NOT_REMOVED,1805,"key value not removed","Will be raised when ERROR_KEYVALUE_NO_VALUE,1806,"missing value","Will be raised when the value is missing" ################################################################################ -## Graph errors +## Graph / traversal errors ################################################################################ ERROR_GRAPH_INVALID_GRAPH,1901,"invalid graph","Will be raised when an invalid name is passed to the server" @@ -209,6 +209,7 @@ ERROR_GRAPH_COULD_NOT_CHANGE_VERTEX,1905,"could not change vertex","Will be rais ERROR_GRAPH_INVALID_EDGE,1906,"invalid edge","Will be raised when an invalid edge id is passed to the server" ERROR_GRAPH_COULD_NOT_CREATE_EDGE,1907,"could not create edge","Will be raised when the edge could not be created" ERROR_GRAPH_COULD_NOT_CHANGE_EDGE,1908,"could not change edge","Will be raised when the edge could not be changed" +ERROR_GRAPH_TOO_MANY_ITERATIONS,1909,"too many iterations","Will be raised when too many iterations are done in a graph traversal" ################################################################################ ## Session errors diff --git a/lib/BasicsC/voc-errors.c b/lib/BasicsC/voc-errors.c index 0e3cc28d6b..bef1e2a985 100644 --- a/lib/BasicsC/voc-errors.c +++ b/lib/BasicsC/voc-errors.c @@ -140,6 +140,7 @@ void TRI_InitialiseErrorMessages (void) { REG_ERROR(ERROR_GRAPH_INVALID_EDGE, "invalid edge"); REG_ERROR(ERROR_GRAPH_COULD_NOT_CREATE_EDGE, "could not create edge"); REG_ERROR(ERROR_GRAPH_COULD_NOT_CHANGE_EDGE, "could not change edge"); + REG_ERROR(ERROR_GRAPH_TOO_MANY_ITERATIONS, "too many iterations"); REG_ERROR(ERROR_SESSION_INVALID_SESSION, "invalid session"); REG_ERROR(ERROR_SESSION_COULD_NOT_CREATE_SESSION, "could not create session"); REG_ERROR(ERROR_SESSION_COULD_NOT_CHANGE_SESSION, "could not change session"); diff --git a/lib/BasicsC/voc-errors.h b/lib/BasicsC/voc-errors.h index 41c0e9dcc2..1de2cbafb2 100644 --- a/lib/BasicsC/voc-errors.h +++ b/lib/BasicsC/voc-errors.h @@ -306,6 +306,8 @@ extern "C" { /// Will be raised when the edge could not be created /// - 1908: @LIT{could not change edge} /// Will be raised when the edge could not be changed +/// - 1909: @LIT{too many iterations} +/// Will be raised when too many iterations are done in a graph traversal /// - 1951: @LIT{invalid session} /// Will be raised when an invalid session id is passed to the server /// - 1952: @LIT{could not create session} @@ -1751,6 +1753,16 @@ void TRI_InitialiseErrorMessages (void); #define TRI_ERROR_GRAPH_COULD_NOT_CHANGE_EDGE (1908) +//////////////////////////////////////////////////////////////////////////////// +/// @brief 1909: ERROR_GRAPH_TOO_MANY_ITERATIONS +/// +/// too many iterations +/// +/// Will be raised when too many iterations are done in a graph traversal +//////////////////////////////////////////////////////////////////////////////// + +#define TRI_ERROR_GRAPH_TOO_MANY_ITERATIONS (1909) + //////////////////////////////////////////////////////////////////////////////// /// @brief 1951: ERROR_SESSION_INVALID_SESSION ///