1
0
Fork 0

Merge branch 'devel' of https://github.com/triAGENS/ArangoDB into devel

This commit is contained in:
Thomas Richter 2013-04-17 14:38:27 +02:00
commit a3da7b9897
20 changed files with 925 additions and 52 deletions

View File

@ -1,6 +1,8 @@
v1.3.0-alpha1 (XXXX-XX-XX)
--------------------------
* added API for user-defined AQL functions
* issue #475: A better error message for deleting a non-existent graph
* issue #474: Web interface problems with the JS Shell

View File

@ -0,0 +1,10 @@
> curl --data @- -X POST --dump - http://localhost:8529/_api/aqlfunction
{ "name" : "myfunctions:temperature:celsiustofahrenheit, "code" : "function (celsius) { return celsius * 1.8 + 32; }" }
HTTP/1.1 201 Created
content-type: application/json; charset=utf-8
{
"error" : false,
"code" : 201
}

View File

@ -0,0 +1,34 @@
HTTP Interface for AQL User Functions Management {#HttpAqlFunctions}
====================================================================
@NAVIGATE_HttpAqlFunctions
@EMBEDTOC{HttpAqlFunctionsTOC}
AQL User Functions Management {#HttpAqlFunctionsIntro}
======================================================
This is an introduction to ArangoDB's Http interface for managing AQL
user functions. AQL user functions are a means to extend the functionality
of ArangoDB's query language (AQL) with user-defined Javascript code.
For an overview of how AQL user functions work, please refer to @ref
ExtendingAql.
The Http interface provides an API for adding, deleting, and listing
previously registered AQL user functions.
All user functions managed through this interface will be stored in the
system collection `_aqlfunctions`. Documents in this collection should not
be accessed directly, but only via the dedicated interfaces.
@anchor HttpAqlFunctionsSave
@copydetails JSF_POST_api_aqlfunction
@CLEARPAGE
@anchor HttpAqlFunctionsRemove
@copydetails JSF_DELETE_api_aqlfunction
@CLEARPAGE
@anchor HttpAqlFunctionsList
@copydetails JSF_GET_api_aqlfunction

View File

@ -0,0 +1,8 @@
TOC {#HttpAqlFunctionsTOC}
==========================
- @ref HttpAqlFunctions
- @ref HttpAqlFunctionsIntro
- @ref HttpAqlFunctionsSave "POST /_api/aqlfunction"
- @ref HttpAqlFunctionsRemove "DELETE /_api/aqlfunction/functionname"
- @ref HttpAqlFunctionsList "GET /_api/aqlfunction"

View File

@ -7,6 +7,7 @@ ArangoDB for API Implementors (@VERSION) {#ImplementorManual}
@CHAPTER_REF{RestEdge}
@CHAPTER_REF{HttpCursor}
@CHAPTER_REF{HttpQuery}
@CHAPTER_REF{HttpAqlFunctions}
@CHAPTER_REF{HttpSimple}
@CHAPTER_REF{HttpCollection}
@CHAPTER_REF{HttpIndex}

View File

@ -33,6 +33,7 @@ dist_man_MANS += \
################################################################################
DOXYGEN = \
Doxygen/js/actions/api-aqlfunction.c \
Doxygen/js/actions/api-collection.c \
Doxygen/js/actions/api-cursor.c \
Doxygen/js/actions/api-edges.c \
@ -49,6 +50,7 @@ DOXYGEN = \
Doxygen/js/common/bootstrap/module-fs.c \
Doxygen/js/common/bootstrap/modules.c \
Doxygen/js/common/modules/jsunity.c \
Doxygen/js/common/modules/org/arangodb/aql/functions.c \
Doxygen/js/common/modules/org/arangodb/arango-collection-common.c \
Doxygen/js/common/modules/org/arangodb/arango-statement-common.c \
Doxygen/js/common/modules/org/arangodb/graph-common.c \
@ -78,6 +80,7 @@ WIKI = \
DbaManualBasics \
DbaManualDatafileDebugger \
DbaManualEmergencyConsole \
ExtendingAql \
FirstStepsArangoDB \
Glossary \
HandlingCollections \
@ -85,6 +88,7 @@ WIKI = \
HandlingEdges \
HandlingIndexes \
Home \
HttpAqlFunctions \
HttpBatch \
HttpCollection \
HttpCursor \
@ -155,6 +159,7 @@ Doxygen/.setup-directories:
@test -d Doxygen/website/images || mkdir -p Doxygen/website/images
@test -d Doxygen/js/actions || mkdir -p Doxygen/js/actions
@test -d Doxygen/js/common/bootstrap || mkdir -p Doxygen/js/common/bootstrap
@test -d Doxygen/js/common/modules/org/arangodb/aql || mkdir -p Doxygen/js/common/modules/org/arangodb/aql
@test -d Doxygen/js/common/modules/org/arangodb || mkdir -p Doxygen/js/common/modules/org/arangodb
@test -d Doxygen/js/server/modules/org/arangodb || mkdir -p Doxygen/js/server/modules/org/arangodb
@test -d Doxygen/js/client || mkdir -p Doxygen/js/client

View File

@ -779,7 +779,7 @@ AQL supports functions to allow more complex computations. Functions can be
called at any query position where an expression is allowed. The general
function call syntax is:
FUNCTIONAME(arguments)
FUNCTIONNAME(arguments)
where `FUNCTIONNAME` is the name of the function to be called, and `arguments`
is a comma-separated list of function arguments. If a function does not need any
@ -793,7 +793,30 @@ Some example function calls:
LENGTH(friends)
COLLECTIONS()
Function names are not case-sensitive.
In contrast to collection and variable names, function names are case-insensitive,
i.e. `LENGTH(foo)` and `length(foo)` are equivalent.
@subsubsection AqlFunctionsExtending Extending AQL
Since ArangoDB 1.3, it is possible to extend AQL with user-defined functions.
These functions need to be written in Javascript, and be registered before usage
in a query.
Please refer to @ref ExtendingAql for more details on this.
By default, any function used in an AQL query will be sought in the built-in
function namespace `_aql`. This is the default namespace that contains all AQL
functions that are shipped with ArangoDB.
To refer to a user-defined AQL function, the function name must be fully qualified
to also include the user-defined namespace. The `:` symbol is used as the namespace
separator:
MYGROUP:MYFUNC()
MYFUNCTIONS.MATH.RANDOM()
As all AQL function names, user function names are also case-insensitive.
@subsubsection AqlFunctionsCasting Type cast functions

View File

@ -0,0 +1,81 @@
Extending AQL with User Functions {#ExtendingAql}
=================================================
@NAVIGATE_ExtendingAql
@EMBEDTOC{ExtendingAqlTOC}
Conventions {#ExtendingAqlConventions}
======================================
AQL comes with a built-in set of functions, but it is no
full-feature programming language.
To add missing functionality or to simplifiy queries, users
may add own functions to AQL. These functions can be written
in Javascript, and must be registered via an API.
In order to avoid conflicts with existing or future built-in
function names, all user functions must be put into separate
namespaces. Invoking a user functions is then possible by referring
to the fully-qualified function name, which includes the namespace,
too.
The `:` symbol is used inside AQL as the namespace separator. Using
the namespace separator, users can create a multi-level hierarchy of
function groups if required.
Examples:
RETURN myfunctions:myfunc()
RETURN myfunctions:math:random()
Note: as all function names in AQL, user function names are also
case-insensitive.
Built-in AQL functions reside in the namespace `_aql`, which is also
the default namespace to look in if an unqualified function name is
found. Adding user functions to the `_aql` namespace is disallowed and
will fail.
User functions can take any number of input arguments and should
provide one result. They should be kept purely functional and thus free of
side effects and state.
User function code is late-bound, and may thus not rely on any variables
that existed at the time of declaration. If user function code requires
access to any external data, it must take care to set up the data by
itself.
User functions must only return primitive types (i.e. `null`, boolean
values, numeric values, string values) or aggregate types (lists or
documents) composed of these types.
Returning any other Javascript object type from a user function may lead
to undefined behavior and should be avoided.
Registering and Unregistering User Functions {#ExtendingAqlHowTo}
=================================================================
AQL user functions can be registered using the `aqlfunctions` object as
follows:
var aqlfunctions = require("org/arangodb/aql/functions");
To register a function, the fully qualified function name plus the
function code must be specified.
@copydetails JSF_aqlfunctions_register
It is possible to unregister a single user function, or a whole group of
user functions (identified by their namespace) in one go:
@copydetails JSF_aqlfunctions_unregister
@copydetails JSF_aqlfunctions_unregister_group
To get an overview of which functions are currently registered, the
`toArray` function can be used:
@copydetails JSF_aqlfunctions_toArray

View File

@ -0,0 +1,6 @@
TOC {#ExtendingAqlTOC}
======================
- @ref ExtendingAql
- @ref ExtendingAqlConventions
- @ref ExtendingAqlHowTo

View File

@ -11,6 +11,7 @@ ArangoDB's User Manual (@VERSION) {#UserManual}
@CHAPTER_REF{HandlingEdges}
@CHAPTER_REF{SimpleQueries}
@CHAPTER_REF{Aql}
@CHAPTER_REF{ExtendingAql}
@CHAPTER_REF{AqlExamples}
@CHAPTER_REF{UserManualActions}
@CHAPTER_REF{Transactions}

View File

@ -0,0 +1,192 @@
# coding: utf-8
require 'rspec'
require './arangodb.rb'
describe ArangoDB do
api = "/_api/aqlfunction"
prefix = "api-aqlfunction"
context "AQL user functions:" do
################################################################################
## error handling
################################################################################
context "error handling" do
it "add function, without name" do
body = "{ \"code\" : \"function () { return 1; }\" }"
doc = ArangoDB.log_post("#{prefix}-add-no-name", api, :body => body)
doc.code.should eq(400)
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['errorNum'].should eq(1580)
end
it "add function, invalid name" do
body = "{ \"name\" : \"\", \"code\" : \"function () { return 1; }\" }"
doc = ArangoDB.log_post("#{prefix}-add-invalid1", api, :body => body)
doc.code.should eq(400)
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['errorNum'].should eq(1580)
end
it "add function, invalid name" do
body = "{ \"name\" : \"_aql:foobar\", \"code\" : \"function () { return 1; }\" }"
doc = ArangoDB.log_post("#{prefix}-add-invalid2", api, :body => body)
doc.code.should eq(400)
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['errorNum'].should eq(1580)
end
it "add function, invalid name" do
body = "{ \"name\" : \"foobar\", \"code\" : \"function () { return 1; }\" }"
doc = ArangoDB.log_post("#{prefix}-add-invalid3", api, :body => body)
doc.code.should eq(400)
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['errorNum'].should eq(1580)
end
it "add function, no code" do
body = "{ \"name\" : \"myfunc:mytest\" }"
doc = ArangoDB.log_post("#{prefix}-add-no-code", api, :body => body)
doc.code.should eq(400)
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['errorNum'].should eq(1581)
end
it "add function, invalid code" do
body = "{ \"name\" : \"myfunc:mytest\", \"code\" : \"function ()\" }"
doc = ArangoDB.log_post("#{prefix}-add-invalid-code", api, :body => body)
doc.code.should eq(400)
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['errorNum'].should eq(1581)
end
it "deleting non-existing function" do
doc = ArangoDB.log_delete("#{prefix}-delete", api + "/mytest%3Amynonfunc")
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(404)
doc.parsed_response['errorNum'].should eq(1582)
end
end
################################################################################
## adding and deleting functions
################################################################################
context "adding functions" do
before do
ArangoDB.delete("/_api/aqlfunction/UnitTests%3Amytest")
end
after do
ArangoDB.delete("/_api/aqlfunction/UnitTests%3Amytest")
end
it "add function, valid code" do
body = "{ \"name\" : \"UnitTests:mytest\", \"code\": \"function () { return 1; }\" }"
doc = ArangoDB.log_post("#{prefix}-add-function1", api, :body => body)
doc.code.should eq(201)
doc.headers['content-type'].should eq("application/json; charset=utf-8")
doc.parsed_response['error'].should eq(false)
doc.parsed_response['code'].should eq(201)
end
it "add function, update" do
body = "{ \"name\" : \"UnitTests:mytest\", \"code\": \"function () { return 1; }\" }"
doc = ArangoDB.log_post("#{prefix}-add-function2", api, :body => body)
doc.code.should eq(201)
doc.headers['content-type'].should eq("application/json; charset=utf-8")
doc.parsed_response['error'].should eq(false)
doc.parsed_response['code'].should eq(201)
doc = ArangoDB.log_post("#{prefix}-add-function2", api, :body => body)
doc.code.should eq(200)
doc.headers['content-type'].should eq("application/json; charset=utf-8")
doc.parsed_response['error'].should eq(false)
doc.parsed_response['code'].should eq(200)
end
it "add function, delete" do
body = "{ \"name\" : \"UnitTests:mytest\", \"code\": \"function () { return 1; }\" }"
doc = ArangoDB.log_post("#{prefix}-add-function3", api, :body => body)
doc.code.should eq(201)
doc.headers['content-type'].should eq("application/json; charset=utf-8")
doc.parsed_response['error'].should eq(false)
doc.parsed_response['code'].should eq(201)
doc = ArangoDB.log_delete("#{prefix}-add-function3", api + "/UnitTests%3Amytest")
doc.code.should eq(200)
doc.headers['content-type'].should eq("application/json; charset=utf-8")
doc.parsed_response['error'].should eq(false)
doc.parsed_response['code'].should eq(200)
doc = ArangoDB.log_delete("#{prefix}-add-function3", api + "/UnitTests%3Amytest")
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(404)
doc.parsed_response['errorNum'].should eq(1582)
end
end
################################################################################
## retrieving the list of functions
################################################################################
context "adding functions" do
before do
ArangoDB.delete("/_api/aqlfunction/UnitTests%3Amytest")
end
after do
ArangoDB.delete("/_api/aqlfunction/UnitTests%3Amytest")
end
it "add function and retrieve the list" do
body = "{ \"name\" : \"UnitTests:mytest\", \"code\": \"function () { return 1; }\" }"
doc = ArangoDB.log_post("#{prefix}-list-functions", api, :body => body)
doc.code.should eq(201)
doc.headers['content-type'].should eq("application/json; charset=utf-8")
doc.parsed_response['error'].should eq(false)
doc.parsed_response['code'].should eq(201)
doc = ArangoDB.log_get("#{prefix}-list-functions", api + "?prefix=UnitTests")
doc.code.should eq(200)
doc.headers['content-type'].should eq("application/json; charset=utf-8")
doc.parsed_response.length.should eq(1)
doc.parsed_response[0]['name'].should eq("UnitTests:mytest")
end
end
end
end

View File

@ -4,6 +4,7 @@ test -d logs || mkdir logs
rspec --color --format d \
api-http-spec.rb \
api-admin-spec.rb \
api-aqlfunction-spec.rb \
api-attributes-spec.rb \
api-batch-spec.rb \
api-collection-spec.rb \

View File

@ -134,6 +134,15 @@ start-server:
unittests-make:
@(ctags --version > /dev/null 2> /dev/null && make tags > /dev/null || test "x$(FORCE)" == "x1") || true
################################################################################
### @brief static codebase tests (a.k.a. Visual Studio cries)
################################################################################
unittests-codebase-static:
@rm -f duplicates
@(find lib arangosh arangod arangoirb -regex ".*/.*\.\(c\|h\|cpp\)" -printf "%f\n" | sort | uniq -c | grep -v "^ \+1 \+" > duplicates) || true
@if [ "`grep " " duplicates`" != "" ]; then echo ; echo "Duplicate filenames found. These should be fixed to allow compilation with Visual Studio:"; cat duplicates; false; fi
################################################################################
### @brief BOOST TESTS
################################################################################

View File

@ -0,0 +1,235 @@
////////////////////////////////////////////////////////////////////////////////
/// @brief AQL user functions management
///
/// @file
///
/// DISCLAIMER
///
/// Copyright 2012 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 Jan Steemann
/// @author Copyright 2012, triAGENS GmbH, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////
var arangodb = require("org/arangodb");
var actions = require("org/arangodb/actions");
var aqlfunctions = require("org/arangodb/aql/functions");
// -----------------------------------------------------------------------------
// --SECTION-- private functions
// -----------------------------------------------------------------------------
////////////////////////////////////////////////////////////////////////////////
/// @addtogroup ArangoAPI
/// @{
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
/// @brief fetch a user
///
/// @RESTHEADER{GET /_api/aqlfunction,returns registered AQL user functions}
///
/// @REST{GET /_api/aqlfunction}
///
/// Returns all registered AQL user functions.
///
/// @REST{GET /_api/aqlfunction?namespace=@FA{namespace}}
///
/// Returns all registered AQL user functions from namespace @FA{namespace}.
///
/// The call will return a JSON list with all user functions found. Each user
/// function will at least have the following attributes:
///
/// - @LIT{name}: The fully qualified name of the user function
///
/// - @LIT{code}: A string representation of the function body
////////////////////////////////////////////////////////////////////////////////
function GET_api_aqlfunction (req, res) {
if (req.suffix.length != 0) {
actions.resultBad(req, res, arangodb.ERROR_HTTP_BAD_PARAMETER);
return;
}
var namespace = undefined;
if (req.parameters.hasOwnProperty('namespace')) {
namespace = req.parameters.namespace;
}
var result = aqlfunctions.toArray(namespace);
actions.resultOk(req, res, actions.HTTP_OK, result)
}
////////////////////////////////////////////////////////////////////////////////
/// @brief create a new AQL user function
///
/// @RESTHEADER{POST /_api/aqlfunction,creates or replaces an AQL user function}
///
/// @REST{POST /_api/aqlfunction}
///
/// The following data need to be passed in a JSON representation in the body of
/// the POST request:
///
/// - @LIT{name}: the fully qualified name of the user functions.
///
/// - @LIT{code}: a string representation of the function body.
///
/// If the function can be registered by the server, the server will respond with
/// @LIT{HTTP 201}. If the function already existed and was replaced by the
/// call, the server will respond with @LIT{HTTP 200}.
///
/// In case of success, the returned JSON object has the following properties:
///
/// - @LIT{error}: boolean flag to indicate that an error occurred (@LIT{false}
/// in this case)
///
/// - @LIT{code}: the HTTP status code
///
/// If the JSON representation is malformed or mandatory data is missing from the
/// request, the server will respond with @LIT{HTTP 400}.
///
/// The body of the response will contain a JSON object with additional error
/// details. The object has the following attributes:
///
/// - @LIT{error}: boolean flag to indicate that an error occurred (@LIT{true} in this case)
///
/// - @LIT{code}: the HTTP status code
///
/// - @LIT{errorNum}: the server error number
///
/// - @LIT{errorMessage}: a descriptive error message
///
/// @EXAMPLES
///
/// @verbinclude api-aqlfunction-create
////////////////////////////////////////////////////////////////////////////////
function POST_api_aqlfunction (req, res) {
var json = actions.getJsonBody(req, res, actions.HTTP_BAD);
if (json === undefined) {
return;
}
var result = aqlfunctions.register(json.name, json.code);
actions.resultOk(req, res, result ? actions.HTTP_OK : actions.HTTP_CREATED, { });
}
////////////////////////////////////////////////////////////////////////////////
/// @brief remove an existing AQL user function
///
/// @RESTHEADER{DELETE /_api/aqlfunction,remove an existing AQL user function}
///
/// @REST{DELETE /_api/aqlfunction/@FA{name}}
///
/// Removes an existing AQL user function, identified by @FA{name}.
/// The function name provided in @FA{name} must be fully qualified, including
/// any namespaces.
///
/// If the function can be removed by the server, the server will respond with
/// @LIT{HTTP 200}.
///
/// In case of success, the returned JSON object has the following properties:
///
/// - @LIT{error}: boolean flag to indicate that an error occurred (@LIT{false}
/// in this case)
///
/// - @LIT{code}: the HTTP status code
///
/// If the JSON representation is malformed or mandatory data is missing from the
/// request, the server will respond with @LIT{HTTP 400}. If the specified user
/// does not exist, the server will respond with @LIT{HTTP 404}.
///
/// The body of the response will contain a JSON object with additional error
/// details. The object has the following attributes:
///
/// - @LIT{error}: boolean flag to indicate that an error occurred (@LIT{true} in this case)
///
/// - @LIT{code}: the HTTP status code
///
/// - @LIT{errorNum}: the server error number
///
/// - @LIT{errorMessage}: a descriptive error message
////////////////////////////////////////////////////////////////////////////////
function DELETE_api_aqlfunction (req, res) {
if (req.suffix.length !== 1) {
actions.resultBad(req, res, arangodb.ERROR_HTTP_BAD_PARAMETER);
return;
}
var name = decodeURIComponent(req.suffix[0]);
try {
aqlfunctions.unregister(name);
actions.resultOk(req, res, actions.HTTP_OK, { });
}
catch (err) {
if (err.errorNum === arangodb.errors.ERROR_QUERY_FUNCTION_NOT_FOUND.code) {
actions.resultNotFound(req, res, arangodb.errors.ERROR_QUERY_FUNCTION_NOT_FOUND.code);
}
else {
throw err;
}
}
}
// -----------------------------------------------------------------------------
// --SECTION-- initialiser
// -----------------------------------------------------------------------------
////////////////////////////////////////////////////////////////////////////////
/// @brief gateway
////////////////////////////////////////////////////////////////////////////////
actions.defineHttp({
url : "_api/aqlfunction",
context : "api",
callback : function (req, res) {
try {
switch (req.requestType) {
case actions.GET:
GET_api_aqlfunction(req, res);
break;
case actions.POST:
POST_api_aqlfunction(req, res);
break;
case actions.DELETE:
DELETE_api_aqlfunction(req, res);
break;
default:
actions.resultUnsupported(req, res);
}
}
catch (err) {
actions.resultException(req, res, err);
}
}
});
////////////////////////////////////////////////////////////////////////////////
/// @}
////////////////////////////////////////////////////////////////////////////////
// Local Variables:
// mode: outline-minor
// outline-regexp: "^\\(/// @brief\\|/// @addtogroup\\|// --SECTION--\\|/// @page\\|/// @}\\)"
// End:

View File

@ -21,7 +21,6 @@
///
/// Copyright holder is triAGENS GmbH, Cologne, Germany
///
/// @author Achim Brandt
/// @author Jan Steemann
/// @author Copyright 2012, triAGENS GmbH, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////
@ -316,9 +315,8 @@ function PATCH_api_user (req, res) {
///
/// - @LIT{code}: the HTTP status code
///
/// If the JSON representation is malformed or mandatory data is missing from the
/// request, the server will respond with @LIT{HTTP 400}. If the specified user
/// does not exist, the server will respond with @LIT{HTTP 404}.
/// If the specified user does not exist, the server will respond with
/// @LIT{HTTP 404}.
///
/// The body of the response will contain a JSON object with additional error
/// details. The object has the following attributes:

View File

@ -158,13 +158,13 @@
}
////////////////////////////////////////////////////////////////////////////////
/// @brief plattform
/// @brief platform
////////////////////////////////////////////////////////////////////////////////
exports.plattform = "unknown";
exports.platform = "unknown";
if (typeof SYS_PLATFORM !== "undefined") {
exports.plattform = SYS_PLATFORM;
exports.platform = SYS_PLATFORM;
delete SYS_PLATFORM;
}

View File

@ -47,6 +47,33 @@ var ArangoError = arangodb.ArangoError;
/// @{
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
/// @brief apply a prefix filter on the functions
////////////////////////////////////////////////////////////////////////////////
var getFiltered = function (group) {
var result = [ ];
if (group !== null && group !== undefined && group.length > 0) {
var prefix = group.toUpperCase();
if (group.substr(group.length - 1, 1) !== ':') {
prefix += ':';
}
getStorage().toArray().forEach(function (f) {
if (f.name.toUpperCase().substr(0, prefix.length) === prefix) {
result.push(f);
}
});
}
else {
result = getStorage().toArray();
}
return result;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief validate a function name
////////////////////////////////////////////////////////////////////////////////
@ -109,7 +136,7 @@ var getStorage = function () {
if (functions === null) {
var err = new ArangoError();
err.errorNum = arangodb.errors.ERROR_ARANGO_COLLECTION_NOT_FOUND.code;
err.errorMessage = "collection _aqlfunctions not found";
err.errorMessage = "collection '_aqlfunctions' not found";
throw err;
}
@ -131,7 +158,22 @@ var getStorage = function () {
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////
/// @fn JSF_aqlfunctions_unregister
/// @brief delete an existing AQL user function
///
/// @FUN{aqlfunctions.unregister(@FA{name})}
///
/// Unregisters an existing AQL user function, identified by the fully qualified
/// function name.
///
/// Trying to unregister a function that does not exist will result in an
/// exception.
///
/// @EXAMPLES
///
/// @code
/// arangosh> require("org/arangodb/aql/functions").unregister("myfunctions:temperature:celsiustofahrenheit");
/// @endcode
////////////////////////////////////////////////////////////////////////////////
var unregisterFunction = function (name) {
@ -148,7 +190,7 @@ var unregisterFunction = function (name) {
if (func === null) {
var err = new ArangoError();
err.errorNum = arangodb.errors.ERROR_QUERY_FUNCTION_NOT_FOUND.code;
err.errorMessage = arangodb.errors.ERROR_QUERY_FUNCTION_NOT_FOUND.message;
err.errorMessage = internal.sprintf(arangodb.errors.ERROR_QUERY_FUNCTION_NOT_FOUND.message, name);
throw err;
}
@ -160,16 +202,79 @@ var unregisterFunction = function (name) {
};
////////////////////////////////////////////////////////////////////////////////
/// @fn JSF_aqlfunctions_unregister_group
/// @brief delete a group of AQL user functions
///
/// @FUN{aqlfunctions.unregisterGroup(@FA{prefix})}
///
/// Unregisters a group of AQL user function, identified by a common function
/// group prefix.
///
/// This will return the number of functions unregistered.
///
/// @EXAMPLES
///
/// @code
/// arangosh> require("org/arangodb/aql/functions").unregisterGroup("myfunctions:temperature");
///
/// arangosh> require("org/arangodb/aql/functions").unregisterGroup("myfunctions");
/// @endcode
////////////////////////////////////////////////////////////////////////////////
var unregisterFunctionsGroup = function (group) {
if (group.length === 0) {
var err = new ArangoError();
err.errorNum = arangodb.errors.ERROR_BAD_PARAMETER.code;
err.errorMessage = arangodb.errors.ERROR_BAD_PARAMETER.message;
}
var deleted = 0;
getFiltered(group).forEach(function (f) {
getStorage().remove(f._id);
deleted++;
});
if (deleted > 0) {
internal.reloadAqlFunctions();
}
return deleted;
};
////////////////////////////////////////////////////////////////////////////////
/// @fn JSF_aqlfunctions_register
/// @brief register an AQL user function
///
/// @FUN{aqlfunctions.register(@FA{name}, @FA{code})}
///
/// Registers an AQL user function, identified by a fully qualified function
/// name. The function code in @FA{code} must be specified as a Javascript
/// function or a string representation of a Javascript function.
///
/// If a function identified by @FA{name} already exists, the previous function
/// definition will be updated.
///
/// @EXAMPLES
///
/// @code
/// arangosh> require("org/arangodb/aql/functions").register("myfunctions:temperature:celsiustofahrenheit",
/// function (celsius) {
/// return celsius * 1.8 + 32;
/// });
/// @endcode
////////////////////////////////////////////////////////////////////////////////
var registerFunction = function (name, code, isDeterministic) {
// validate input
validateName(name);
code = stringifyFunction(code, name);
var exists = false;
try {
unregisterFunction(name);
exists = true;
}
catch (err) {
}
@ -184,15 +289,52 @@ var registerFunction = function (name, code, isDeterministic) {
getStorage().save(data);
internal.reloadAqlFunctions();
return true;
return exists;
};
////////////////////////////////////////////////////////////////////////////////
/// @brief reloads the AQL user functons
/// @fn JSF_aqlfunctions_toArray
/// @brief list all AQL user functions
///
/// @FUN{aqlfunctions.toArray()}
///
/// Returns all previously registered AQL user functions, with their fully
/// qualified names and function code.
///
/// The result may optionally be restricted to a specified group of functions
/// by specifying a group prefix:
///
/// @FUN{aqlfunctions.toArray(@FA{prefix})}
///
/// @EXAMPLES
///
/// To list all available user functions:
///
/// @code
/// arangosh> require("org/arangodb/aql/functions").toArray();
/// @endcode
///
/// To list all available user functions in the `myfunctions` namespace:
///
/// @code
/// arangosh> require("org/arangodb/aql/functions").toArray("myfunctions");
/// @endcode
///
/// To list all available user functions in the `myfunctions:temperature` namespace:
///
/// @code
/// arangosh> require("org/arangodb/aql/functions").toArray("myfunctions:temperature");
/// @endcode
////////////////////////////////////////////////////////////////////////////////
var toArrayFunctions = function (group) {
var result = [ ];
var reloadFunctions = function () {
throw "cannot use abstract reload function";
getFiltered(group).forEach(function (f) {
result.push({ name: f.name, code: f.code });
});
return result;
};
////////////////////////////////////////////////////////////////////////////////
@ -208,9 +350,10 @@ var reloadFunctions = function () {
/// @{
////////////////////////////////////////////////////////////////////////////////
exports.register = registerFunction;
exports.unregister = unregisterFunction;
exports.reload = reloadFunctions;
exports.unregister = unregisterFunction;
exports.unregisterGroup = unregisterFunctionsGroup;
exports.register = registerFunction;
exports.toArray = toArrayFunctions;
////////////////////////////////////////////////////////////////////////////////
/// @}

View File

@ -158,8 +158,8 @@ var getStorage = function () {
/// @EXAMPLES
///
/// @code
/// arangosh> require("users").save("my-user", "my-secret-password");
/// arangosh> require("users").reload();
/// arangosh> require("org/arangodb/users").save("my-user", "my-secret-password");
/// arangosh> require("org/arangodb/users").reload();
/// @endcode
////////////////////////////////////////////////////////////////////////////////
@ -231,8 +231,8 @@ exports.save = function (username, passwd, active, extra) {
/// @EXAMPLES
///
/// @code
/// arangosh> require("users").replace("my-user", "my-changed-password");
/// arangosh> require("users").reload();
/// arangosh> require("org/arangodb/users").replace("my-user", "my-changed-password");
/// arangosh> require("org/arangodb/users").reload();
/// @endcode
////////////////////////////////////////////////////////////////////////////////
@ -304,8 +304,8 @@ exports.replace = function (username, passwd, active, extra) {
/// @EXAMPLES
///
/// @code
/// arangosh> require("users").replace("my-user", "my-secret-password");
/// arangosh> require("users").reload();
/// arangosh> require("org/arangodb/users").replace("my-user", "my-secret-password");
/// arangosh> require("org/arangodb/users").reload();
/// @endcode
////////////////////////////////////////////////////////////////////////////////
@ -364,8 +364,8 @@ exports.update = function (username, passwd, active, extra) {
/// @EXAMPLES
///
/// @code
/// arangosh> require("users").remove("my-user");
/// arangosh> require("users").reload();
/// arangosh> require("org/arangodb/users").remove("my-user");
/// arangosh> require("org/arangodb/users").reload();
/// @endcode
////////////////////////////////////////////////////////////////////////////////

View File

@ -60,6 +60,7 @@ function AqlFunctionsSuite () {
////////////////////////////////////////////////////////////////////////////////
setUp : function () {
aqlfunctions.unregisterGroup("UnitTests:");
},
////////////////////////////////////////////////////////////////////////////////
@ -67,6 +68,43 @@ function AqlFunctionsSuite () {
////////////////////////////////////////////////////////////////////////////////
tearDown : function () {
aqlfunctions.unregisterGroup("UnitTests:");
},
////////////////////////////////////////////////////////////////////////////////
/// @brief toArray
////////////////////////////////////////////////////////////////////////////////
testToArray1 : function () {
aqlfunctions.register("UnitTests:tryme:foo", function (what) { return what * 2; }, true);
aqlfunctions.register("UnitTests:tryme:bar", function (what) { return what * 2; }, true);
assertEqual([ "UnitTests:tryme:bar", "UnitTests:tryme:foo" ], aqlfunctions.toArray("UnitTests").sort().map(function (f) { return f.name; }));
assertEqual([ "UnitTests:tryme:bar", "UnitTests:tryme:foo" ], aqlfunctions.toArray("UnitTests:").sort().map(function (f) { return f.name; }));
assertEqual([ "UnitTests:tryme:bar", "UnitTests:tryme:foo" ], aqlfunctions.toArray("UnitTests:tryme").sort().map(function (f) { return f.name; }));
assertEqual([ "UnitTests:tryme:bar", "UnitTests:tryme:foo" ], aqlfunctions.toArray("UnitTests:tryme:").sort().map(function (f) { return f.name; }));
},
////////////////////////////////////////////////////////////////////////////////
/// @brief toArray
////////////////////////////////////////////////////////////////////////////////
testToArray2 : function () {
aqlfunctions.register("UnitTests:tryme:foo", function (what) { return what * 2; }, true);
aqlfunctions.register("UnitTests:tryme:bar", function (what) { return what * 2; }, true);
aqlfunctions.register("UnitTests58:tryme:bar", function (what) { return what * 2; }, true);
aqlfunctions.register("UnitTests58:whyme:bar", function (what) { return what * 2; }, true);
assertEqual([ "UnitTests:tryme:bar", "UnitTests:tryme:foo" ], aqlfunctions.toArray("UnitTests").sort().map(function (f) { return f.name; }));
assertEqual([ "UnitTests:tryme:bar", "UnitTests:tryme:foo" ], aqlfunctions.toArray("UnitTests:").sort().map(function (f) { return f.name; }));
assertEqual([ "UnitTests:tryme:bar", "UnitTests:tryme:foo" ], aqlfunctions.toArray("UnitTests:tryme").sort().map(function (f) { return f.name; }));
assertEqual([ "UnitTests58:tryme:bar", "UnitTests58:whyme:bar" ], aqlfunctions.toArray("UnitTests58").sort().map(function (f) { return f.name; }));
assertEqual([ "UnitTests58:tryme:bar", "UnitTests58:whyme:bar" ], aqlfunctions.toArray("UnitTests58:").sort().map(function (f) { return f.name; }));
assertEqual([ "UnitTests58:tryme:bar" ], aqlfunctions.toArray("UnitTests58:tryme").sort().map(function (f) { return f.name; }));
assertEqual([ "UnitTests58:tryme:bar" ], aqlfunctions.toArray("UnitTests58:tryme:").sort().map(function (f) { return f.name; }));
aqlfunctions.unregister("UnitTests58:tryme:bar", function (what) { return what * 2; }, true);
aqlfunctions.unregister("UnitTests58:whyme:bar", function (what) { return what * 2; }, true);
},
////////////////////////////////////////////////////////////////////////////////

View File

@ -49,9 +49,14 @@ function FileSystemSuite () {
////////////////////////////////////////////////////////////////////////////////
setUp : function () {
// create a name for a temporary directory we will run all tests in
// as we are creating a new name, this shouldn't collide with any existing
// directories, and we can also remove our directory when the tests are
// over
tempDir = fs.join(fs.getTempFile('', false));
try {
// create this directory before any tests
fs.makeDirectoryRecursive(tempDir);
}
catch (err) {
@ -63,38 +68,49 @@ function FileSystemSuite () {
////////////////////////////////////////////////////////////////////////////////
tearDown : function () {
// some sanity check as we don't want to unintentionally remove "." or "/"
if (tempDir.length > 5) {
// remove our temporary directory with all its subdirectories
// we created it, so we don't care what's in it
fs.removeDirectoryRecursive(tempDir);
}
},
////////////////////////////////////////////////////////////////////////////////
/// @brief exists()
/// @brief exists() - test if a file / directory exists
////////////////////////////////////////////////////////////////////////////////
testExists : function () {
var tempName;
// existing directory
// create the name for a new directory
tempName = fs.join(tempDir, 'foo');
try {
// remove it if it exists
fs.removeDirectory(tempName);
}
catch (err) {
}
// create the directory with all paths to it
fs.makeDirectoryRecursive(tempName);
// now the directory should exist
assertTrue(fs.exists(tempName));
// remove it again
fs.removeDirectory(tempName);
// non-existing directory/file
tempName = fs.join(tempDir, 'meow');
// this file should not exist
assertFalse(fs.exists(tempName));
// file
// create a new file
tempName = fs.getTempFile('', true);
// the file should now exist
assertTrue(fs.exists(tempName));
// clean it up
fs.remove(tempName);
},
@ -105,30 +121,40 @@ function FileSystemSuite () {
testGetTempFile : function () {
var tempName;
// creating a new file
// creating a new empty file with a temporary name
tempName = fs.getTempFile('', true);
assertTrue(tempName !== '');
// the file should be located inside the tempPath
assertEqual(fs.getTempPath(), tempName.substr(0, fs.getTempPath().length));
// the file should exist
assertTrue(fs.exists(tempName));
// clean up
fs.remove(tempName);
// without creating
// create a filename only, without creating the file itself
tempName = fs.getTempFile('', false);
assertTrue(tempName !== '');
// filename should be located underneath tempPath
assertEqual(fs.getTempPath(), tempName.substr(0, fs.getTempPath().length));
// the file should not exist
assertFalse(fs.exists(tempName));
// in a subdirectory
// create a temporary filename for a file in a subdirectory, without creating the file
tempName = fs.getTempFile('tests', false);
assertTrue(tempName !== '');
// filename should be located underneath tempPath
assertEqual(fs.join(fs.getTempPath(), 'test'), tempName.substr(0, fs.join(fs.getTempPath(), 'test').length));
// file should not yet exist
assertFalse(fs.exists(tempName));
// clean up
fs.removeDirectory(fs.join(fs.getTempPath(), 'tests'));
},
////////////////////////////////////////////////////////////////////////////////
/// @brief getTempPath()
/// @brief getTempPath() - test the system's temp path
////////////////////////////////////////////////////////////////////////////////
testGetTempPath : function () {
@ -137,76 +163,102 @@ function FileSystemSuite () {
tempName = fs.getTempPath();
assertTrue(tempName !== '');
// the temp path should also be an existing directory
assertTrue(fs.isDirectory(tempName));
},
////////////////////////////////////////////////////////////////////////////////
/// @brief isDirectory()
/// @brief isDirectory() - test if a directory exists
////////////////////////////////////////////////////////////////////////////////
testIsDirectory : function () {
var tempName;
// existing directory
// create a new directory name
tempName = fs.join(tempDir, 'foo');
try {
// remove any existing directory first
fs.removeDirectory(tempName);
}
catch (err) {
}
// create the directory with the full path to it
fs.makeDirectoryRecursive(tempName);
// check if it does exist
assertTrue(fs.exists(tempName));
// should also be a directory
assertTrue(fs.isDirectory(tempName));
// remove the directory
fs.removeDirectory(tempName);
// non-existing directory/file
tempName = fs.join(tempDir, 'meow');
// this file shouldn't exist
assertFalse(fs.exists(tempName));
// and thus shouldn't be a directory
assertFalse(fs.isDirectory(tempName));
// file
// create a new file
tempName = fs.getTempFile('', true);
// should exist
assertTrue(fs.exists(tempName));
// but shouldn't be a directory
assertFalse(fs.isDirectory(tempName));
// clean up
fs.remove(tempName);
},
////////////////////////////////////////////////////////////////////////////////
/// @brief isFile()
/// @brief isFile() - test if a file exists
////////////////////////////////////////////////////////////////////////////////
testIsFile : function () {
var tempName;
// existing directory
// create a new directory name
tempName = fs.join(tempDir, 'foo');
try {
// remove any existing directory if it exists
fs.removeDirectory(tempName);
}
catch (err) {
}
try {
// remove any existing file if it exists
fs.remove(tempName);
}
catch (err) {
}
// now create the whole directory with all paths to it
fs.makeDirectoryRecursive(tempName);
// directory should now exist
assertTrue(fs.exists(tempName));
// but should not be a file
assertFalse(fs.isFile(tempName));
// remove it
fs.removeDirectory(tempName);
// non-existing directory/file
tempName = fs.join(tempDir, 'meow');
// this file shouldn't exist
assertFalse(fs.exists(tempName));
// and shouldn't be a file
assertFalse(fs.isFile(tempName));
// file
// now create a new file
tempName = fs.getTempFile('', true);
// should exist
assertTrue(fs.exists(tempName));
// and should be a file
assertTrue(fs.isFile(tempName));
// clean up and remove the file
fs.remove(tempName);
},
@ -218,37 +270,53 @@ function FileSystemSuite () {
},
////////////////////////////////////////////////////////////////////////////////
/// @brief makeDirectory()
/// @brief makeDirectory() - create a directory
////////////////////////////////////////////////////////////////////////////////
testMakeDirectory : function () {
var tempName;
// create the name for a subdirectory
tempName = fs.join(tempDir, 'bar');
try {
// remove an existing directory if it already exists
fs.removeDirectoryRecursive(tempName);
}
catch (err) {
}
// the directory shouldn't exist. we just deleted it if it was there
assertFalse(fs.exists(tempName));
// now create the directory
fs.makeDirectory(tempName);
// directory should be there after creation
assertTrue(fs.exists(tempName));
// now remove it
fs.removeDirectory(tempName);
// create the name for another subdirectory
tempName = fs.join(tempDir, 'baz');
try {
// remove it if it does exist
fs.removeDirectoryRecursive(tempName);
}
catch (err) {
}
// make with an existing directory
// the directory shouldn't be there...
assertFalse(fs.exists(tempName));
// now create it
fs.makeDirectory(tempName);
// should have succeeded
assertTrue(fs.exists(tempName));
try {
// try to create the same directory again. this should fail
fs.makeDirectory(tempName);
fail();
}
@ -256,16 +324,21 @@ function FileSystemSuite () {
assertEqual(ERRORS.ERROR_SYS_ERROR.code, err.errorNum);
}
// make subdirectory
// create a subdirectory in the directory we created
tempName = fs.join(tempDir, 'baz', 'foo');
fs.makeDirectory(tempName);
// the subdirectory should now be there
assertTrue(fs.exists(tempName));
// create a file in the subdirecory
tempName = fs.join(tempDir, 'baz', 'foo', 'test');
// write something to the file
fs.write(tempName, "this is a test");
// the file should exist after writing to it
assertTrue(fs.exists(tempName));
try {
// create a directory with the name of an already existing file. this should fail.
fs.makeDirectory(tempName);
fail();
}
@ -273,73 +346,83 @@ function FileSystemSuite () {
assertEqual(ERRORS.ERROR_SYS_ERROR.code, err.errorNum);
}
// remove all stuff we created for testing
fs.removeDirectoryRecursive(fs.join(tempDir, 'baz'));
},
////////////////////////////////////////////////////////////////////////////////
/// @brief makeDirectoryRecursive()
/// @brief makeDirectoryRecursive() - create a directory will all paths to it
////////////////////////////////////////////////////////////////////////////////
testMakeDirectoryRecursive : function () {
var tempName;
// create the name for a new subdirectory
tempName = fs.join(tempDir, 'bar');
try {
// make sure it does not yet exist
fs.removeDirectoryRecursive(tempName);
}
catch (err) {
}
// create
// create the subdirectory
fs.makeDirectoryRecursive(tempName);
// check if it is there
assertTrue(fs.isDirectory(tempName));
// create again
// create the subdirectory again. this should not fail
fs.makeDirectoryRecursive(tempName);
// should still be a directory
assertTrue(fs.isDirectory(tempName));
// create subdirectories
// create subdirectory in subdirectory of subdirectory
tempName = fs.join(tempDir, 'bar', 'baz', 'test');
fs.makeDirectoryRecursive(tempName);
assertTrue(fs.isDirectory(tempName));
},
////////////////////////////////////////////////////////////////////////////////
/// @brief move()
/// @brief move() - TODO
////////////////////////////////////////////////////////////////////////////////
testMove : function () {
},
////////////////////////////////////////////////////////////////////////////////
/// @brief read()
/// @brief read() - TODO
////////////////////////////////////////////////////////////////////////////////
testRead : function () {
},
////////////////////////////////////////////////////////////////////////////////
/// @brief remove()
/// @brief remove() - TODO
////////////////////////////////////////////////////////////////////////////////
testRemove : function () {
},
////////////////////////////////////////////////////////////////////////////////
/// @brief size()
/// @brief test size() - the filesize of a file
////////////////////////////////////////////////////////////////////////////////
testSize : function () {
var tempName;
// existing file
// create a new file with a specific content
tempName = fs.join(tempDir, 'foo');
fs.write(tempName, "this is a test file");
// test the size of the new file
assertEqual(19, fs.size(tempName));
// remove the new file
fs.remove(tempName);
// non-existing file
// now the file does not exist
try {
// try to read filesize. this should fail
fs.size(tempName);
fail();
}
@ -350,12 +433,15 @@ function FileSystemSuite () {
// directory
fs.makeDirectory(tempName);
try {
// try to read the filesize of a directory. this should fail
fs.size(tempName);
fail();
}
catch (err) {
assertEqual(ERRORS.ERROR_FILE_NOT_FOUND.code, err.errorNum);
}
// remove the directory
fs.removeDirectory(tempName);
}