mirror of https://gitee.com/bigwinds/arangodb
550 lines
18 KiB
JavaScript
550 lines
18 KiB
JavaScript
/*jslint indent: 2, nomen: true, maxlen: 100, sloppy: true, vars: true, white: true, plusplus: true */
|
|
/*global require, CURSOR, DELETE_CURSOR */
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief query results cursor actions
|
|
///
|
|
/// @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 Achim Brandt
|
|
/// @author Jan Steemann
|
|
/// @author Copyright 2012, triAGENS GmbH, Cologne, Germany
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
var arangodb = require("org/arangodb");
|
|
var actions = require("org/arangodb/actions");
|
|
var internal = require("internal");
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// --SECTION-- global variables
|
|
// -----------------------------------------------------------------------------
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// --SECTION-- private functions
|
|
// -----------------------------------------------------------------------------
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @addtogroup ArangoAPI
|
|
/// @{
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @startDocuBlock JSF_post_api_cursor
|
|
/// @brief create a cursor and return the first results
|
|
///
|
|
/// @RESTHEADER{POST /_api/cursor, Create cursor}
|
|
///
|
|
/// @RESTBODYPARAM{query,json,required}
|
|
/// A JSON object describing the query and query parameters.
|
|
///
|
|
/// @RESTDESCRIPTION
|
|
/// The query details include the query string plus optional query options and
|
|
/// bind parameters. These values need to be passed in a JSON representation in
|
|
/// the body of the POST request.
|
|
///
|
|
/// The following attributes can be used inside the JSON object:
|
|
///
|
|
/// - *query*: contains the query string to be executed (mandatory)
|
|
///
|
|
/// - *count*: boolean flag that indicates whether the number of documents
|
|
/// in the result set should be returned in the "count" attribute of the result (optional).
|
|
/// Calculating the "count" attribute might in the future have a performance
|
|
/// impact for some queries so this option is turned off by default, and "count"
|
|
/// is only returned when requested.
|
|
///
|
|
/// - *batchSize*: maximum number of result documents to be transferred from
|
|
/// the server to the client in one roundtrip (optional). If this attribute is
|
|
/// not set, a server-controlled default value will be used.
|
|
///
|
|
/// - *bindVars*: key/value list of bind parameters (optional).
|
|
///
|
|
/// - *options*: key/value list of extra options for the query (optional).
|
|
///
|
|
/// The following options are supported at the moment:
|
|
///
|
|
/// - *fullCount*: if set to *true* and the query contains a *LIMIT* clause, then the
|
|
/// result will contain an extra attribute *extra* with a sub-attribute *fullCount*.
|
|
/// This sub-attribute will contain the number of documents in the result before the
|
|
/// last LIMIT in the query was applied. It can be used to count the number of documents that
|
|
/// match certain filter criteria, but only return a subset of them, in one go.
|
|
/// It is thus similar to MySQL's *SQL_CALC_FOUND_ROWS* hint. Note that setting the option
|
|
/// will disable a few LIMIT optimizations and may lead to more documents being processed,
|
|
/// and thus make queries run longer. Note that the *fullCount* sub-attribute will only
|
|
/// be present in the result if the query has a LIMIT clause and the LIMIT clause is
|
|
/// actually used in the query.
|
|
///
|
|
/// If the result set can be created by the server, the server will respond with
|
|
/// *HTTP 201*. The body of the response will contain a JSON object with the
|
|
/// result set.
|
|
///
|
|
/// The returned JSON object has the following properties:
|
|
///
|
|
/// - *error*: boolean flag to indicate that an error occurred (*false*
|
|
/// in this case)
|
|
///
|
|
/// - *code*: the HTTP status code
|
|
///
|
|
/// - *result*: an array of result documents (might be empty if query has no results)
|
|
///
|
|
/// - *hasMore*: a boolean indicator whether there are more results
|
|
/// available for the cursor on the server
|
|
///
|
|
/// - *count*: the total number of result documents available (only
|
|
/// available if the query was executed with the *count* attribute set)
|
|
///
|
|
/// - *id*: id of temporary cursor created on the server (optional, see above)
|
|
///
|
|
/// - *extra*: an optional JSON object with extra information about the query result
|
|
///
|
|
/// If the JSON representation is malformed or the query specification is
|
|
/// missing from the request, the server will respond with *HTTP 400*.
|
|
///
|
|
/// The body of the response will contain a JSON object with additional error
|
|
/// details. The object has the following attributes:
|
|
///
|
|
/// - *error*: boolean flag to indicate that an error occurred (*true* in this case)
|
|
///
|
|
/// - *code*: the HTTP status code
|
|
///
|
|
/// - *errorNum*: the server error number
|
|
///
|
|
/// - *errorMessage*: a descriptive error message
|
|
///
|
|
/// If the query specification is complete, the server will process the query. If an
|
|
/// error occurs during query processing, the server will respond with *HTTP 400*.
|
|
/// Again, the body of the response will contain details about the error.
|
|
///
|
|
/// A list of query errors can be found @ref ArangoErrors here.
|
|
///
|
|
/// @RESTRETURNCODES
|
|
///
|
|
/// @RESTRETURNCODE{201}
|
|
/// is returned if the result set can be created by the server.
|
|
///
|
|
/// @RESTRETURNCODE{400}
|
|
/// is returned if the JSON representation is malformed or the query specification is
|
|
/// missing from the request.
|
|
///
|
|
/// @RESTRETURNCODE{404}
|
|
/// The server will respond with *HTTP 404* in case a non-existing collection is
|
|
/// accessed in the query.
|
|
///
|
|
/// @RESTRETURNCODE{405}
|
|
/// The server will respond with *HTTP 405* if an unsupported HTTP method is used.
|
|
///
|
|
/// @EXAMPLES
|
|
///
|
|
/// Executes a query and extract the result in a single go:
|
|
///
|
|
/// @EXAMPLE_ARANGOSH_RUN{RestCursorCreateCursorForLimitReturnSingle}
|
|
/// var cn = "products";
|
|
/// db._drop(cn);
|
|
/// db._create(cn);
|
|
///
|
|
/// db.products.save({"hello1":"world1"});
|
|
/// db.products.save({"hello2":"world1"});
|
|
///
|
|
/// var url = "/_api/cursor";
|
|
/// var body = {
|
|
/// query: "FOR p IN products LIMIT 2 RETURN p",
|
|
/// count: true,
|
|
/// batchSize: 2
|
|
/// };
|
|
///
|
|
/// var response = logCurlRequest('POST', url, JSON.stringify(body));
|
|
///
|
|
/// assert(response.code === 201);
|
|
///
|
|
/// logJsonResponse(response);
|
|
/// @END_EXAMPLE_ARANGOSH_RUN
|
|
///
|
|
/// Executes a query and extracts part of the result:
|
|
///
|
|
/// @EXAMPLE_ARANGOSH_RUN{RestCursorCreateCursorForLimitReturn}
|
|
/// var cn = "products";
|
|
/// db._drop(cn);
|
|
/// db._create(cn);
|
|
///
|
|
/// db.products.save({"hello1":"world1"});
|
|
/// db.products.save({"hello2":"world1"});
|
|
/// db.products.save({"hello3":"world1"});
|
|
/// db.products.save({"hello4":"world1"});
|
|
/// db.products.save({"hello5":"world1"});
|
|
///
|
|
/// var url = "/_api/cursor";
|
|
/// var body = {
|
|
/// query: "FOR p IN products LIMIT 5 RETURN p",
|
|
/// count: true,
|
|
/// batchSize: 2
|
|
/// };
|
|
///
|
|
/// var response = logCurlRequest('POST', url, JSON.stringify(body));
|
|
///
|
|
/// assert(response.code === 201);
|
|
///
|
|
/// logJsonResponse(response);
|
|
/// @END_EXAMPLE_ARANGOSH_RUN
|
|
///
|
|
/// Using a query option:
|
|
///
|
|
/// @EXAMPLE_ARANGOSH_RUN{RestCursorCreateCursorOption}
|
|
/// var url = "/_api/cursor";
|
|
/// var body = {
|
|
/// query: "FOR i IN 1..1000 FILTER i > 500 LIMIT 10 RETURN i",
|
|
/// count: true,
|
|
/// options: {
|
|
/// fullCount: true
|
|
/// }
|
|
/// };
|
|
///
|
|
/// var response = logCurlRequest('POST', url, JSON.stringify(body));
|
|
///
|
|
/// assert(response.code === 201);
|
|
///
|
|
/// logJsonResponse(response);
|
|
/// @END_EXAMPLE_ARANGOSH_RUN
|
|
///
|
|
/// Bad queries:
|
|
///
|
|
/// Missing body:
|
|
///
|
|
/// @EXAMPLE_ARANGOSH_RUN{RestCursorCreateCursorMissingBody}
|
|
/// var url = "/_api/cursor";
|
|
///
|
|
/// var response = logCurlRequest('POST', url, '');
|
|
///
|
|
/// assert(response.code === 400);
|
|
///
|
|
/// logJsonResponse(response);
|
|
/// @END_EXAMPLE_ARANGOSH_RUN
|
|
///
|
|
/// Unknown collection:
|
|
///
|
|
/// @EXAMPLE_ARANGOSH_RUN{RestCursorCreateCursorUnknownCollection}
|
|
/// var url = "/_api/cursor";
|
|
/// var body = {
|
|
/// query: "FOR u IN unknowncoll LIMIT 2 RETURN u",
|
|
/// count: true,
|
|
/// batchSize: 2
|
|
/// };
|
|
///
|
|
/// var response = logCurlRequest('POST', url, JSON.stringify(body));
|
|
///
|
|
/// assert(response.code === 404);
|
|
///
|
|
/// logJsonResponse(response);
|
|
/// @END_EXAMPLE_ARANGOSH_RUN
|
|
/// @endDocuBlock
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function post_api_cursor(req, res) {
|
|
if (req.suffix.length !== 0) {
|
|
actions.resultNotFound(req, res, arangodb.ERROR_CURSOR_NOT_FOUND);
|
|
return;
|
|
}
|
|
|
|
var json = actions.getJsonBody(req, res);
|
|
|
|
if (json === undefined) {
|
|
actions.resultBad(req, res, arangodb.ERROR_QUERY_EMPTY);
|
|
return;
|
|
}
|
|
|
|
var cursor;
|
|
|
|
if (json.query !== undefined) {
|
|
cursor = internal.AQL_QUERY(json.query,
|
|
json.bindVars,
|
|
{
|
|
count : json.count || false,
|
|
batchSize: json.batchSize || 1000
|
|
},
|
|
json.options);
|
|
}
|
|
else {
|
|
actions.resultBad(req, res, arangodb.ERROR_QUERY_EMPTY);
|
|
return;
|
|
}
|
|
|
|
// error occurred
|
|
if (cursor instanceof Error) {
|
|
actions.resultException(req, res, cursor, undefined, false);
|
|
return;
|
|
}
|
|
|
|
// this might dispose or persist the cursor
|
|
actions.resultCursor(req,
|
|
res,
|
|
cursor,
|
|
actions.HTTP_CREATED,
|
|
{
|
|
countRequested: json.count ? true : false
|
|
});
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @startDocuBlock JSF_post_api_cursor_identifier
|
|
/// @brief return the next results from an existing cursor
|
|
///
|
|
/// @RESTHEADER{PUT /_api/cursor/cursor-identifier, Read next batch from cursor}
|
|
///
|
|
/// @RESTURLPARAMETERS
|
|
///
|
|
/// @RESTURLPARAM{cursor-identifier,string,required}
|
|
/// The name of the cursor
|
|
///
|
|
/// @RESTDESCRIPTION
|
|
///
|
|
/// If the cursor is still alive, returns an object with the following
|
|
/// attributes.
|
|
///
|
|
/// - *id*: the *cursor-identifier*
|
|
/// - *result*: a list of documents for the current batch
|
|
/// - *hasMore*: *false* if this was the last batch
|
|
/// - *count*: if present the total number of elements
|
|
///
|
|
/// Note that even if *hasMore* returns *true*, the next call might
|
|
/// still return no documents. If, however, *hasMore* is *false*, then
|
|
/// the cursor is exhausted. Once the *hasMore* attribute has a value of
|
|
/// *false*, the client can stop.
|
|
///
|
|
/// @RESTRETURNCODES
|
|
///
|
|
/// @RESTRETURNCODE{200}
|
|
/// The server will respond with *HTTP 200* in case of success.
|
|
///
|
|
/// @RESTRETURNCODE{400}
|
|
/// If the cursor identifier is omitted, the server will respond with *HTTP 404*.
|
|
///
|
|
/// @RESTRETURNCODE{404}
|
|
/// If no cursor with the specified identifier can be found, the server will respond
|
|
/// with *HTTP 404*.
|
|
///
|
|
/// @EXAMPLES
|
|
///
|
|
/// Valid request for next batch:
|
|
///
|
|
/// @EXAMPLE_ARANGOSH_RUN{RestCursorForLimitReturnCont}
|
|
/// var url = "/_api/cursor";
|
|
/// var cn = "products";
|
|
/// db._drop(cn);
|
|
/// db._create(cn);
|
|
///
|
|
/// db.products.save({"hello1":"world1"});
|
|
/// db.products.save({"hello2":"world1"});
|
|
/// db.products.save({"hello3":"world1"});
|
|
/// db.products.save({"hello4":"world1"});
|
|
/// db.products.save({"hello5":"world1"});
|
|
///
|
|
/// var url = "/_api/cursor";
|
|
/// var body = {
|
|
/// query: "FOR p IN products LIMIT 5 RETURN p",
|
|
/// count: true,
|
|
/// batchSize: 2
|
|
/// };
|
|
/// var response = logCurlRequest('POST', url, JSON.stringify(body));
|
|
///
|
|
/// var body = response.body.replace(/\\/g, '');
|
|
/// var _id = JSON.parse(body).id;
|
|
/// response = logCurlRequest('PUT', url + '/' + _id, '');
|
|
/// assert(response.code === 200);
|
|
///
|
|
/// logJsonResponse(response);
|
|
/// @END_EXAMPLE_ARANGOSH_RUN
|
|
///
|
|
/// Missing identifier
|
|
///
|
|
/// @EXAMPLE_ARANGOSH_RUN{RestCursorMissingCursorIdentifier}
|
|
/// var url = "/_api/cursor";
|
|
///
|
|
/// var response = logCurlRequest('PUT', url, '');
|
|
///
|
|
/// assert(response.code === 400);
|
|
///
|
|
/// logJsonResponse(response);
|
|
/// @END_EXAMPLE_ARANGOSH_RUN
|
|
///
|
|
/// Unknown identifier
|
|
///
|
|
/// @EXAMPLE_ARANGOSH_RUN{RestCursorInvalidCursorIdentifier}
|
|
/// var url = "/_api/cursor/123123";
|
|
///
|
|
/// var response = logCurlRequest('PUT', url, '');
|
|
///
|
|
/// assert(response.code === 404);
|
|
///
|
|
/// logJsonResponse(response);
|
|
/// @END_EXAMPLE_ARANGOSH_RUN
|
|
/// @endDocuBlock
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function put_api_cursor (req, res) {
|
|
if (req.suffix.length !== 1) {
|
|
actions.resultBad(req, res, arangodb.ERROR_HTTP_BAD_PARAMETER);
|
|
return;
|
|
}
|
|
|
|
var cursorId = decodeURIComponent(req.suffix[0]);
|
|
var cursor = CURSOR(cursorId);
|
|
|
|
if (! (cursor instanceof arangodb.ArangoCursor)) {
|
|
actions.resultBad(req, res, arangodb.ERROR_CURSOR_NOT_FOUND);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// note: this might dispose or persist the cursor
|
|
actions.resultCursor(req, res, cursor, actions.HTTP_OK);
|
|
}
|
|
catch (e) {
|
|
}
|
|
|
|
// force garbage-collection
|
|
cursor = null;
|
|
internal.wait(0.0);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @startDocuBlock JSF_post_api_cursor_delete
|
|
/// @brief dispose an existing cursor
|
|
///
|
|
/// @RESTHEADER{DELETE /_api/cursor/cursor-identifier, Delete cursor}
|
|
///
|
|
/// @RESTURLPARAMETERS
|
|
///
|
|
/// @RESTURLPARAM{cursor-identifier,string,required}
|
|
/// The name of the cursor
|
|
///
|
|
/// @RESTDESCRIPTION
|
|
/// Deletes the cursor and frees the resources associated with it.
|
|
///
|
|
/// The cursor will automatically be destroyed on the server when the client has
|
|
/// retrieved all documents from it. The client can also explicitly destroy the
|
|
/// cursor at any earlier time using an HTTP DELETE request. The cursor id must
|
|
/// be included as part of the URL.
|
|
///
|
|
/// Note: the server will also destroy abandoned cursors automatically after a
|
|
/// certain server-controlled timeout to avoid resource leakage.
|
|
///
|
|
/// @RESTRETURNCODES
|
|
///
|
|
/// @RESTRETURNCODE{202}
|
|
/// is returned if the server is aware of the cursor.
|
|
///
|
|
/// @RESTRETURNCODE{404}
|
|
/// is returned if the server is not aware of the cursor. It is also
|
|
/// returned if a cursor is used after it has been destroyed.
|
|
///
|
|
/// @EXAMPLES
|
|
///
|
|
/// @EXAMPLE_ARANGOSH_RUN{RestCursorDelete}
|
|
/// var url = "/_api/cursor";
|
|
/// var cn = "products";
|
|
/// db._drop(cn);
|
|
/// db._create(cn);
|
|
///
|
|
/// db.products.save({"hello1":"world1"});
|
|
/// db.products.save({"hello2":"world1"});
|
|
/// db.products.save({"hello3":"world1"});
|
|
/// db.products.save({"hello4":"world1"});
|
|
/// db.products.save({"hello5":"world1"});
|
|
///
|
|
/// var url = "/_api/cursor";
|
|
/// var body = {
|
|
/// query: "FOR p IN products LIMIT 5 RETURN p",
|
|
/// count: true,
|
|
/// batchSize: 2
|
|
/// };
|
|
/// var response = logCurlRequest('POST', url, JSON.stringify(body));
|
|
/// logJsonResponse(response);
|
|
/// var body = response.body.replace(/\\/g, '');
|
|
/// var _id = JSON.parse(body).id;
|
|
/// response = logCurlRequest('DELETE', url + '/' + _id);
|
|
///
|
|
/// assert(response.code === 202);
|
|
/// @END_EXAMPLE_ARANGOSH_RUN
|
|
/// @endDocuBlock
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
function delete_api_cursor(req, res) {
|
|
if (req.suffix.length !== 1) {
|
|
actions.resultBad(req, res, arangodb.ERROR_HTTP_BAD_PARAMETER);
|
|
return;
|
|
}
|
|
|
|
var cursorId = decodeURIComponent(req.suffix[0]);
|
|
if (! DELETE_CURSOR(cursorId)) {
|
|
actions.resultNotFound(req, res, arangodb.ERROR_CURSOR_NOT_FOUND);
|
|
return;
|
|
}
|
|
|
|
actions.resultOk(req, res, actions.HTTP_ACCEPTED, { id : cursorId });
|
|
|
|
// we want the garbage collection to clean unused cursors immediately
|
|
internal.wait(0.0);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// --SECTION-- initialiser
|
|
// -----------------------------------------------------------------------------
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @brief cursor actions gateway
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
actions.defineHttp({
|
|
url : "_api/cursor",
|
|
context : "api",
|
|
|
|
callback : function (req, res) {
|
|
try {
|
|
switch (req.requestType) {
|
|
case actions.POST:
|
|
post_api_cursor(req, res);
|
|
break;
|
|
|
|
case actions.PUT:
|
|
put_api_cursor(req, res);
|
|
break;
|
|
|
|
case actions.DELETE:
|
|
delete_api_cursor(req, res);
|
|
break;
|
|
|
|
default:
|
|
actions.resultUnsupported(req, res);
|
|
}
|
|
}
|
|
catch (err) {
|
|
actions.resultException(req, res, err, undefined, false);
|
|
}
|
|
}
|
|
});
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
/// @}
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Local Variables:
|
|
// mode: outline-minor
|
|
// outline-regexp: "^\\(/// @brief\\|/// @addtogroup\\|// --SECTION--\\|/// @page\\|/// @}\\)"
|
|
// End:
|