/*jshint strict: false */ //////////////////////////////////////////////////////////////////////////////// /// @brief querying and managing structures /// /// @file /// /// DISCLAIMER /// /// Copyright 2014 ArangoDB GmbH, Cologne, Germany /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. /// You may obtain a copy of the License at /// /// http://www.apache.org/licenses/LICENSE-2.0 /// /// Unless required by applicable law or agreed to in writing, software /// distributed under the License is distributed on an "AS IS" BASIS, /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. /// See the License for the specific language governing permissions and /// limitations under the License. /// /// Copyright holder is ArangoDB GmbH, Cologne, Germany /// /// @author Dr. Frank Celler /// @author Copyright 2014, ArangoDB GmbH, Cologne, Germany /// @author Copyright 2013, triAGENS GmbH, Cologne, Germany //////////////////////////////////////////////////////////////////////////////// var arangodb = require("org/arangodb"); var console = require("console"); var actions = require("org/arangodb/actions"); var arangodb = require("org/arangodb"); var db = arangodb.db; var DEFAULT_KEY = "default"; var API = "_api/structure"; var checkedIndex = false; // ----------------------------------------------------------------------------- // --SECTION-- private functions // ----------------------------------------------------------------------------- /* Configuration example document: { "_key" : "a_collection_name", <- name of the collection with structure "attributes": { <- List of all attributes "number": { <- Name of the attribute "type": "number", <- Type of the attribute "formatter": { <- Output formatter configuration "default": { "args": { "decPlaces": 4, "decSeparator": ".", "thouSeparator": "," }, "module": "org/arangodb/formatter", "do": "formatFloat" }, "de": { "args": { "decPlaces": 4, "decSeparator": ",", "thouSeparator": "." }, "module": "org/arangodb/formatter", "do": "formatFloat" } }, "parser": { <- Input parser configuration "default": { "args": { "decPlaces": 4, "decSeparator": ".", "thouSeparator": "," }, "module": "org/arangodb/formatter", "do": "parseFloat" }, "de": { "args": { "decPlaces": 4, "decSeparator": ",", "thouSeparator": "." }, "module": "org/arangodb/formatter", "do": "parseFloat" } }, "validators": <- List of input validators [ { "module": "org/arangodb/formatter", "do": "validateNotNull" } ] }, "string": { <- Name of the attribute "type": "string", <- Type of the attribute "formatter": {}, "validators": [] }, "zahlen": { <- Name of the attribute "type": "number_list_type" <- Type of the attribute }, "number2": { <- Name of the attribute "type": "number", <- Type of the attribute "formatter": { "default": { "args": { "decPlaces": 0, "decSeparator": ".", "thouSeparator": "," }, "module": "org/arangodb/formatter", "do": "formatFloat" } }, "validators": [] }, "no_structure": { <- Name of the attribute "type": "mixed" <- Type of the attribute }, "timestamp": { "type": "number", "formatter": { "default": { "module": "org/arangodb/formatter", "do": "formatDatetime", "args": { "lang": "en", "timezone": "GMT", "pattern": "yyyy-MM-dd'T'HH:mm:ssZ" } }, "de": { "module": "org/arangodb/formatter", "do": "formatDatetime", "args": { "lang": "de", "timezone": "Europe/Berlin", "pattern": "yyyy.MM.dd HH:mm:ss zzz" } } }, "parser": { "default": { "module": "org/arangodb/formatter", "do": "parseDatetime", "args": { "lang": "en", "timezone": "GMT", "pattern": "yyyy-MM-dd'T'HH:mm:ssZ" } }, "de": { "module": "org/arangodb/formatter", "do": "parseDatetime", "args": { "lang": "de", "timezone": "Europe/Berlin", "pattern": "yyyy.MM.dd HH:mm:ss zzz" } } }, "validators": [] }, "object1": { <- Name of the attribute "type": "complex_type1" <- Type of the attribute } }, "arrayTypes": { <- Array type definitions "number_list_type": { <- Name of type "type": "number", "formatter": { "default": { "args": { "decPlaces": 2, "decSeparator": ".", "thouSeparator": "," }, "module": "org/arangodb/formatter", "do": "formatFloat" }, "de": { "args": { "decPlaces": 2, "decSeparator": ",", "thouSeparator": "." }, "module": "org/arangodb/formatter", "do": "formatFloat" } }, "parser": { "default": { "args": { "decPlaces": 2, "decSeparator": ".", "thouSeparator": "," }, "module": "org/arangodb/formatter", "do": "parseFloat" }, "de": { "args": { "decPlaces": 2, "decSeparator": ",", "thouSeparator": "." }, "module": "org/arangodb/formatter", "do": "parseFloat" } } } }, "objectTypes": { <- Object type definitions "complex_type1": { <- Name of type "attributes": { <- Attributes of the object type "aNumber": { "type": "number" }, "aList": { "type": "number_list_type" <- reference to array type } } } } } */ //////////////////////////////////////////////////////////////////////////////// /// @brief get structures collection //////////////////////////////////////////////////////////////////////////////// function getCollection () { var c; c = db._collection('_structures'); if (c === null) { c = db._create('_structures', { isSystem : true }); } if (c !== null && ! checkedIndex) { c.ensureUniqueConstraint('collection', { sparse: true }); checkedIndex = true; } return c; } //////////////////////////////////////////////////////////////////////////////// /// @brief convert a string to boolean //////////////////////////////////////////////////////////////////////////////// function stringToBoolean (string){ if (undefined === string || null === string) { return false; } switch(string.toLowerCase()){ case "true": case "yes": case "1": return true; case "false": case "no": case "0": case null: return false; default: return Boolean(string); } } //////////////////////////////////////////////////////////////////////////////// /// @brief returns a (OK) result //////////////////////////////////////////////////////////////////////////////// function resultOk (req, res, httpReturnCode, keyvals, headers) { 'use strict'; res.responseCode = httpReturnCode; res.contentType = "application/json; charset=utf-8"; if (undefined !== keyvals) { res.body = JSON.stringify(keyvals); } if (headers !== undefined && headers !== null) { res.headers = headers; } } //////////////////////////////////////////////////////////////////////////////// /// @brief returns a (error) result //////////////////////////////////////////////////////////////////////////////// function resultError (req, res, httpReturnCode, errorNum, errorMessage, keyvals, headers) { 'use strict'; var i; res.responseCode = httpReturnCode; res.contentType = "application/json; charset=utf-8"; var result = {}; if (keyvals !== undefined) { for (i in keyvals) { if (keyvals.hasOwnProperty(i)) { result[i] = keyvals[i]; } } } result.error = true; result.code = httpReturnCode; if (undefined !== errorMessage.errorNum) { result.errorNum = errorMessage.errorNum; } else { result.errorNum = errorNum; } if (undefined !== errorMessage.errorMessage) { result.errorMessage = errorMessage.errorMessage; } else { result.errorMessage = errorMessage; } res.body = JSON.stringify(result); if (headers !== undefined && headers !== null) { res.headers = headers; } } //////////////////////////////////////////////////////////////////////////////// /// @brief returns true if a "if-match" or "if-none-match" errer happens //////////////////////////////////////////////////////////////////////////////// function matchError (req, res, doc) { if (req.headers["if-none-match"] !== undefined) { if (doc._rev === req.headers["if-none-match"]) { // error res.responseCode = actions.HTTP_NOT_MODIFIED; res.contentType = "application/json; charset=utf-8"; res.body = ''; res.headers = {}; return true; } } if (req.headers["if-match"] !== undefined) { if (doc._rev !== req.headers["if-match"]) { // error resultError(req, res, actions.HTTP_PRECONDITION_FAILED, arangodb.ERROR_ARANGO_CONFLICT, "wrong revision", {'_id': doc._id, '_rev': doc._rev, '_key': doc._key}); return true; } } var rev = req.parameters.rev; if (rev !== undefined) { if (doc._rev !== rev) { // error resultError(req, res, actions.HTTP_PRECONDITION_FAILED, arangodb.ERROR_ARANGO_CONFLICT, "wrong revision", {'_id': doc._id, '_rev': doc._rev, '_key': doc._key}); return true; } } return false; } //////////////////////////////////////////////////////////////////////////////// /// @brief returns the collection //////////////////////////////////////////////////////////////////////////////// function getCollectionByRequest(req, res) { if (req.suffix.length === 0) { // GET /_api/structure (missing collection) resultError(req, res, actions.HTTP_BAD, arangodb.ERROR_ARANGO_COLLECTION_NOT_FOUND, "collection not found"); return; } // TODO: check parameter createCollection=true and create collection return db._collection(req.suffix[0]); } //////////////////////////////////////////////////////////////////////////////// /// @brief returns the overwite policy //////////////////////////////////////////////////////////////////////////////// function getOverwritePolicy(req) { var policy = req.parameters.policy; if (undefined !== policy && "error" === policy) { return false; } return true; } //////////////////////////////////////////////////////////////////////////////// /// @brief returns the overwite policy //////////////////////////////////////////////////////////////////////////////// function getKeepNull(req) { return stringToBoolean(req.parameters.keepNull); } //////////////////////////////////////////////////////////////////////////////// /// @brief returns wait for sync //////////////////////////////////////////////////////////////////////////////// function getWaitForSync(req, collection) { if (collection.properties().waitForSync) { return true; } return stringToBoolean(req.parameters.waitForSync); } //////////////////////////////////////////////////////////////////////////////// /// @brief save a document //////////////////////////////////////////////////////////////////////////////// function saveDocument(req, res, collection, document) { var doc; var waitForSync = getWaitForSync(req, collection); try { doc = collection.save(document, waitForSync); } catch(err) { resultError(req, res, actions.HTTP_BAD, arangodb.ERROR_FAILED, err); return; } var headers = { "Etag" : doc._rev, "location" : "/_api/structure/" + doc._id }; if (req.hasOwnProperty('compatibility') && req.compatibility >= 10400) { // 1.4+ style location header headers.location = "/_db/" + encodeURIComponent(arangodb.db._name()) + headers.location; } var returnCode = waitForSync ? actions.HTTP_CREATED : actions.HTTP_ACCEPTED; doc.error = false; resultOk(req, res, returnCode, doc, headers); } //////////////////////////////////////////////////////////////////////////////// /// @brief replace a document //////////////////////////////////////////////////////////////////////////////// function replaceDocument(req, res, collection, oldDocument, newDocument) { var doc; var waitForSync = getWaitForSync(req, collection); var overwrite = getOverwritePolicy(req); if (! overwrite && undefined !== newDocument._rev && oldDocument._rev !== newDocument._rev) { resultError(req, res, actions.HTTP_BAD, arangodb.ERROR_FAILED, "wrong version"); return; } try { doc = collection.replace(oldDocument, newDocument, true, waitForSync); } catch(err) { resultError(req, res, actions.HTTP_BAD, arangodb.ERROR_FAILED, err); return; } var headers = { "Etag" : doc._rev }; var returnCode = waitForSync ? actions.HTTP_CREATED : actions.HTTP_ACCEPTED; resultOk(req, res, returnCode, doc, headers); } //////////////////////////////////////////////////////////////////////////////// /// @brief update a document //////////////////////////////////////////////////////////////////////////////// function patchDocument(req, res, collection, oldDocument, newDocument) { var doc; var waitForSync = getWaitForSync(req, collection); var overwrite = getOverwritePolicy(req); var keepNull = getKeepNull(req); if (!overwrite && undefined !== newDocument._rev && oldDocument._rev !== newDocument._rev) { resultError(req, res, actions.HTTP_BAD, arangodb.ERROR_FAILED, "wrong version"); return; } try { doc = collection.update(oldDocument, newDocument, true, keepNull, waitForSync); } catch(err) { resultError(req, res, actions.HTTP_BAD, arangodb.ERROR_FAILED, err); return; } var headers = { "Etag" : doc._rev }; var returnCode = waitForSync ? actions.HTTP_CREATED : actions.HTTP_ACCEPTED; resultOk(req, res, returnCode, doc, headers); } //////////////////////////////////////////////////////////////////////////////// /// @brief returns the document //////////////////////////////////////////////////////////////////////////////// function getDocumentByRequest(req, res, collection) { if (req.suffix.length < 2) { resultError(req, res, actions.HTTP_BAD, arangodb.ERROR_ARANGO_DOCUMENT_HANDLE_BAD, "expecting GET /_api/structure/"); return; } try { return collection.document(req.suffix[1]); } catch (err) { resultError(req, res, actions.HTTP_NOT_FOUND, arangodb.ERROR_ARANGO_DOCUMENT_NOT_FOUND, "document /_api/structure/" + req.suffix[0] + "/" + req.suffix[1] + " not found"); } } //////////////////////////////////////////////////////////////////////////////// /// @brief returns the types //////////////////////////////////////////////////////////////////////////////// function getTypes (structure) { var predefinedTypes = { "boolean" : { }, "string" : { }, "number" : { }, "mixed" : { } }; var types = { "predefinedTypes" : predefinedTypes, "arrayTypes" : {}, "objectTypes" : {} }; if (undefined !== structure.arrayTypes) { types.arrayTypes = structure.arrayTypes; } if (undefined !== structure.objectTypes) { types.objectTypes = structure.objectTypes; } return types; } //////////////////////////////////////////////////////////////////////////////// /// @brief returns the request language //////////////////////////////////////////////////////////////////////////////// function getLang (req) { if (undefined !== req.parameters.lang) { return req.parameters.lang; } return null; } //////////////////////////////////////////////////////////////////////////////// /// @brief returns formatter //////////////////////////////////////////////////////////////////////////////// function selectFormatter (formatter1, formatter2, lang) { var formatter = formatter1; if (undefined === formatter1 || JSON.stringify(formatter1) === "{}") { formatter = formatter2; } if (undefined !== formatter) { if (undefined === lang) { return formatter[DEFAULT_KEY]; } if (undefined === formatter[lang]) { return formatter[DEFAULT_KEY]; } return formatter[lang]; } } //////////////////////////////////////////////////////////////////////////////// /// @brief returns the parser //////////////////////////////////////////////////////////////////////////////// function selectParser (parser1, parser2, lang) { var parser = parser1; if (undefined === parser1 || JSON.stringify(parser1) === "{}") { parser = parser2; } if (undefined !== parser) { if (undefined === lang) { return parser[DEFAULT_KEY]; } if (undefined === parser[lang]) { return parser[DEFAULT_KEY]; } return parser[lang]; } } //////////////////////////////////////////////////////////////////////////////// /// @brief call a module function //////////////////////////////////////////////////////////////////////////////// function callModuleFunction(value, moduleName, functionName, functionArgs) { if (undefined === moduleName) { return value; } try { var formatModule = require(moduleName); if (formatModule.hasOwnProperty(functionName)) { // call the function return formatModule[functionName].call(null, value, functionArgs); } } catch (err) { // could not load module console.warn("module error for module: " + moduleName + " error: " + err); return value; } // function not found console.warn("module function '" + functionName + "' of module '" + moduleName + "' not found."); return value; } //////////////////////////////////////////////////////////////////////////////// /// @brief returns the formatted value //////////////////////////////////////////////////////////////////////////////// function formatValue (value, structure, types, lang) { var result; var key; var section; try { var type = types.predefinedTypes[structure.type]; if (type) { //console.warn("predefined type found: " + structure.type); section = selectFormatter(structure.formatter, types.predefinedTypes[structure.type].formatter, lang); if (undefined === section) { return value; } return callModuleFunction(value, section.module, section['do'], section.args); } // array types type = types.arrayTypes[structure.type]; if (type) { //console.warn("array type found: " + structure.type); // check for array formatter section = selectFormatter(structure.formatter, undefined, lang); if (undefined !== section) { return callModuleFunction(value, section.module, section['do'], section.args); } // format each element result = []; if(value instanceof Array) { for (key = 0; key < value.length; ++key) { result[key] = formatValue(value[key], type, types, lang); } } return result; } // object types type = types.objectTypes[structure.type]; if (type) { //console.warn("object type found: " + structure.type); // TODO check type of value // check for object formatter section = selectFormatter(structure.formatter, undefined, lang); if (undefined !== section) { return callModuleFunction(value, section.module, section['do'], section.args); } var attributes = type.attributes; if (undefined === attributes) { // no attributes return null; } // TODO check type of attribute // format each property result = {}; for (key in attributes) { if (attributes.hasOwnProperty(key)) { if (value.hasOwnProperty(key)) { var subStructure = attributes[key]; if (undefined === subStructure) { result[key] = value[key]; } else { result[key] = formatValue(value[key], subStructure, types, lang); } } else { result[key] = null; } } } return result; } } catch (err) { //console.warn("error = " + err); } return value; } //////////////////////////////////////////////////////////////////////////////// /// @brief returns the parsed value //////////////////////////////////////////////////////////////////////////////// function parseValue (value, structure, types, lang) { var result; var key; var section; // console.warn("in parseValue"); try { var type = types.predefinedTypes[structure.type]; if (type) { // console.warn("predefined type found: " + structure.type); // TODO check type of value section = selectParser(structure.parser, types.predefinedTypes[structure.type].parser, lang); if (undefined === section) { //console.warn("section is undefined"); return value; } var x = callModuleFunction(value, section.module, section['do'], section.args); // console.warn("parsing " + value + " to " + x ); return x; } // array types type = types.arrayTypes[structure.type]; if (type) { //console.warn("array type found: " + structure.type); // check for array formatter section = selectParser(structure.parser, undefined, lang); if (undefined !== section) { return callModuleFunction(value, section.module, section['do'], section.args); } // parse each element result = []; if(value instanceof Array) { for (key = 0; key < value.length; ++key) { result[key] = parseValue(value[key], type, types, lang); } } return result; } // object types type = types.objectTypes[structure.type]; if (type) { //console.warn("object type found: " + structure.type); // TODO check type of value // check for object parser section = selectParser(structure.parser, undefined, lang); if (undefined !== section) { return callModuleFunction(value, section.module, section['do'], section.args); } var attributes = type.attributes; if (undefined === attributes) { // no attributes return null; } // TODO check type of attribute // parse each property result = {}; for (key in attributes) { if (attributes.hasOwnProperty(key)) { if (value.hasOwnProperty(key)) { var subStructure = attributes[key]; if (undefined === subStructure) { result[key] = value[key]; } else { result[key] = parseValue(value[key], subStructure, types, lang); } } else { result[key] = null; } } } return result; } } catch (err) { //console.warn("error = " + err); } return value; } //////////////////////////////////////////////////////////////////////////////// /// @brief returns true if the value is valid //////////////////////////////////////////////////////////////////////////////// function validateValue (value, structure, types, lang) { var result; var key; var validators; var v; //console.warn("in validateValue(): " + structure.type); try { var type = types.predefinedTypes[structure.type]; if (type) { //console.warn("predefined type found: " + structure.type); // TODO check type of value validators = structure.validators; if (undefined !== validators) { for (key = 0; key < validators.length; ++key) { //console.warn("call function: " + validators[key]['do']); result = callModuleFunction(value, validators[key].module, validators[key]['do'], validators[key].args); if (!result) { return false; } } } validators = types.predefinedTypes[structure.type].validators; if (undefined !== validators) { for (key = 0; key < validators.length; ++key) { //console.warn("call function: " + validators[key]['do']); result = callModuleFunction(value, validators[key].module, validators[key]['do'], validators[key].args); if (!result) { return false; } } } return true; } // array types type = types.arrayTypes[structure.type]; if (type) { //console.warn("array type found: " + structure.type); // TODO check type of value // check for array validator validators = structure.validators; if (undefined !== validators) { for (key = 0; key < validators.length; ++key) { result = callModuleFunction(value, validators[key].module, validators[key]['do'], validators[key].args); if (!result) { return false; } } return true; } // validate each element for (key = 0; key < value.length; ++key) { var valid = validateValue(value[key], type, types); if (!valid) { return false; } } return true; } // object types type = types.objectTypes[structure.type]; if (type) { //console.warn("object type found: " + structure.type); // TODO check type of value // check for object validator validators = structure.validators; if (undefined !== validators) { for (key = 0; key < validators.length; ++key) { result = callModuleFunction(value, validators[key].module, validators[key]['do'], validators[key].args); if (!result) { return false; } } } var attributes = type.attributes; if (undefined === attributes) { // no attributes return true; } // validate each property for (key in attributes) { if (attributes.hasOwnProperty(key)) { if (value.hasOwnProperty(key)) { v = value[key]; } else { v = null; } var subStructure = attributes[key]; if (undefined !== subStructure) { if (!validateValue(v, subStructure, types, lang)) { return false; } } } } return true; } } catch (err) { //console.warn("error = " + err); } return false; } //////////////////////////////////////////////////////////////////////////////// /// @brief returns the structured document //////////////////////////////////////////////////////////////////////////////// function resultStructure (req, res, doc, structure, headers) { var result = {}; var key; var types = getTypes(structure); var lang = getLang(req); var format = true; if (undefined !== req.parameters.format) { format = stringToBoolean(req.parameters.format); } if (structure.attributes !== undefined) { for (key in structure.attributes) { if (structure.attributes.hasOwnProperty(key)) { var value = doc[key]; // format value if (format) { result[key] = formatValue(value, structure.attributes[key], types, lang); } else { result[key] = value; } } } } result._id = doc._id; result._rev = doc._rev; result._key = doc._key; resultOk(req, res, actions.HTTP_OK, result, headers); } //////////////////////////////////////////////////////////////////////////////// /// @brief parse body and return the parsed document /// @throws exception //////////////////////////////////////////////////////////////////////////////// function parseDocumentByStructure(req, res, structure, body, isPatch) { var document = {}; var key; var value; var types = getTypes(structure); var lang = getLang(req); var format = true; if (undefined !== req.parameters.format) { format = stringToBoolean(req.parameters.format); } for (key in structure.attributes) { if (structure.attributes.hasOwnProperty(key)) { value = body[key]; if (!isPatch || undefined !== value) { if (format) { value = parseValue(value, structure.attributes[key], types, lang); } //console.warn("validate key: " + key); if (validateValue(value, structure.attributes[key], types)) { document[key] = value; } else { throw("value of attribute '" + key + "' is not valid."); } } } } if (undefined !== body._id) { document._id = body._id; } if (undefined !== body._rev) { document._rev = body._rev; } if (undefined !== body._key) { document._key = body._key; } return document; } //////////////////////////////////////////////////////////////////////////////// /// @brief get the parsed document //////////////////////////////////////////////////////////////////////////////// function saveDocumentByStructure(req, res, collection, structure, body) { try { var document = parseDocumentByStructure(req, res, structure, body, false); saveDocument(req, res, collection, document); } catch(err) { resultError(req, res, actions.HTTP_BAD, arangodb.ERROR_FAILED, err); return; } } //////////////////////////////////////////////////////////////////////////////// /// @brief replace the parsed document //////////////////////////////////////////////////////////////////////////////// function replaceDocumentByStructure(req, res, collection, structure, oldDocument, body) { try { var document = parseDocumentByStructure(req, res, structure, body, false); replaceDocument(req, res, collection, oldDocument, document); } catch(err) { resultError(req, res, actions.HTTP_BAD, arangodb.ERROR_FAILED, err); return; } } //////////////////////////////////////////////////////////////////////////////// /// @brief patch the parsed document //////////////////////////////////////////////////////////////////////////////// function patchDocumentByStructure(req, res, collection, structure, oldDocument, body) { try { var document = parseDocumentByStructure(req, res, structure, body, true); patchDocument(req, res, collection, oldDocument, document); } catch(err) { resultError(req, res, actions.HTTP_BAD, arangodb.ERROR_FAILED, err); return; } } // ----------------------------------------------------------------------------- // --SECTION-- public functions // ----------------------------------------------------------------------------- //////////////////////////////////////////////////////////////////////////////// /// @brief reads a single document /// /// @RESTHEADER{GET /_api/structure/`document-handle`,reads a document} /// /// @RESTURLPARAMETERS /// /// @RESTURLPARAM{document-handle,string,required} /// The Handle of the Document. /// /// @RESTQUERYPARAM{rev,string,optional} /// You can conditionally select a document based on a target revision id by /// using the `rev` URL parameter. /// /// @RESTQUERYPARAM{lang,string,optional} /// Language of the data. /// /// @RESTQUERYPARAM{format,boolean,optional} /// False for unformated values (default: true). /// /// @RESTHEADERPARAMETERS /// /// @RESTHEADERPARAM{If-None-Match,string,optional} /// If the "If-None-Match" header is given, then it must contain exactly one /// etag. The document is returned, if it has a different revision than the /// given etag. Otherwise a `HTTP 304` is returned. /// /// @RESTHEADERPARAM{If-Match,string,optional} /// If the "If-Match" header is given, then it must contain exactly one /// etag. The document is returned, if it has the same revision ad the /// given etag. Otherwise a `HTTP 412` is returned. As an alternative /// you can supply the etag in an attribute `rev` in the URL. /// /// @RESTDESCRIPTION /// Returns the document identified by `document-handle`. The returned /// document contains two special attributes: `_id` containing the document /// handle and `_rev` containing the revision. /// /// @RESTRETURNCODES /// /// @RESTRETURNCODE{200} /// is returned if the document was found /// /// @RESTRETURNCODE{404} /// is returned if the document or collection was not found /// /// @RESTRETURNCODE{304} /// is returned if the "If-None-Match" header is given and the document has /// same version /// /// @RESTRETURNCODE{412} /// is returned if a "If-Match" header or `rev` is given and the found /// document has a different version /// //////////////////////////////////////////////////////////////////////////////// function get_api_structure(req, res) { var structure; var collection = getCollectionByRequest(req, res); if (undefined === collection) { return; } var doc = getDocumentByRequest(req, res, collection); if (undefined === doc) { return; } if (matchError(req, res, doc)) { return; } var headers = { "Etag" : doc._rev }; try { structure = getCollection().document(collection.name()); } catch (err) { // return the doc resultOk(req, res, actions.HTTP_OK, doc, headers); return; } resultStructure(req, res, doc, structure, headers); } //////////////////////////////////////////////////////////////////////////////// /// @brief reads a single document head /// /// @RESTHEADER{HEAD /_api/structure/`document-handle`,reads a document header} /// /// @RESTURLPARAMETERS /// /// @RESTURLPARAM{document-handle,string,required} /// The Handle of the Document. /// /// @RESTQUERYPARAMETERS /// /// @RESTQUERYPARAM{rev,string,optional} /// You can conditionally select a document based on a target revision id by /// using the `rev` URL parameter. /// /// @RESTHEADERPARAMETERS /// /// @RESTHEADERPARAM{If-Match,string,optional} /// You can conditionally get a document based on a target revision id by /// using the `if-match` HTTP header. /// /// @RESTDESCRIPTION /// Like `GET`, but only returns the header fields and not the body. You /// can use this call to get the current revision of a document or check if /// the document was deleted. /// /// @RESTRETURNCODES /// /// @RESTRETURNCODE{200} /// is returned if the document was found /// /// @RESTRETURNCODE{404} /// is returned if the document or collection was not found /// /// @RESTRETURNCODE{304} /// is returned if the "If-None-Match" header is given and the document has /// same version /// /// @RESTRETURNCODE{412} /// is returned if a "If-Match" header or `rev` is given and the found /// document has a different version /// //////////////////////////////////////////////////////////////////////////////// function head_api_structure(req, res) { var collection = getCollectionByRequest(req, res); if (undefined === collection) { return; } var doc = getDocumentByRequest(req, res, collection); if (undefined === doc) { return; } if (matchError(req, res, doc)) { return; } var headers = { "Etag" : doc._rev }; resultOk(req, res, actions.HTTP_OK, undefined, headers); } //////////////////////////////////////////////////////////////////////////////// /// @brief deletes a document /// /// @RESTHEADER{DELETE /_api/structure/`document-handle`,deletes a document} /// /// @RESTURLPARAMETERS /// /// @RESTURLPARAM{document-handle,string,required} /// Deletes the document identified by `document-handle`. /// /// @RESTQUERYPARAMETERS /// /// @RESTQUERYPARAM{rev,string,optional} /// You can conditionally delete a document based on a target revision id by /// using the `rev` URL parameter. /// /// @RESTQUERYPARAM{policy,string,optional} /// To control the update behavior in case there is a revision mismatch, you /// can use the `policy` parameter. This is the same as when replacing /// documents (see replacing documents for more details). /// /// @RESTQUERYPARAM{waitForSync,boolean,optional} /// Wait until document has been sync to disk. /// /// @RESTHEADERPARAMETERS /// /// @RESTHEADERPARAM{If-Match,string,optional} /// You can conditionally delete a document based on a target revision id by /// using the `if-match` HTTP header. /// /// @RESTDESCRIPTION /// The body of the response contains a JSON object with the information about /// the handle and the revision. The attribute `_id` contains the known /// `document-handle` of the updated document, the attribute `_rev` /// contains the known document revision. /// /// If the `waitForSync` parameter is not specified or set to /// `false`, then the collection's default `waitForSync` behavior is /// applied. The `waitForSync` URL parameter cannot be used to disable /// synchronization for collections that have a default `waitForSync` value /// of `true`. /// /// @RESTRETURNCODES /// /// @RESTRETURNCODE{200} /// is returned if the document was deleted successfully and `waitForSync` was /// `true`. /// /// @RESTRETURNCODE{202} /// is returned if the document was deleted successfully and `waitForSync` was /// `false`. /// /// @RESTRETURNCODE{404} /// is returned if the collection or the document was not found. /// The response body contains an error document in this case. /// /// @RESTRETURNCODE{412} /// is returned if a "If-Match" header or `rev` is given and the current /// document has a different version /// //////////////////////////////////////////////////////////////////////////////// function delete_api_structure (req, res) { var collection = getCollectionByRequest(req, res); if (undefined === collection) { return; } var doc = getDocumentByRequest(req, res, collection); if (undefined === doc) { return; } if (matchError(req, res, doc)) { return; } var waitForSync = getWaitForSync(req, collection); try { collection.remove( doc, true, waitForSync); resultOk(req, res, waitForSync ? actions.HTTP_OK : actions.HTTP_ACCEPTED, { "deleted" : true }); } catch(err) { resultError(req, res, actions.HTTP_BAD, arangodb.ERROR_FAILED, err); return; } } //////////////////////////////////////////////////////////////////////////////// /// @brief updates a document /// /// @RESTHEADER{PATCH /_api/structure/`document-handle`,patches a document} /// /// @RESTURLPARAMETERS /// /// @RESTURLPARAM{document-handle,string,required} /// The Handle of the Document. /// /// @RESTQUERYPARAMETERS /// /// @RESTQUERYPARAM{keepNull,boolean,optional} /// If the intention is to delete existing attributes with the patch command, /// the URL query parameter `keepNull` can be used with a value of `false`. /// This will modify the behavior of the patch command to remove any attributes /// from the existing document that are contained in the patch document with an /// attribute value of `null`. /// /// @RESTQUERYPARAM{waitForSync,boolean,optional} /// Wait until document has been sync to disk. /// /// @RESTQUERYPARAM{rev,string,optional} /// You can conditionally patch a document based on a target revision id by /// using the `rev` URL parameter. /// /// @RESTQUERYPARAM{policy,string,optional} /// To control the update behavior in case there is a revision mismatch, you /// can use the `policy` parameter. /// /// @RESTQUERYPARAM{lang,string,optional} /// Language of the data. /// /// @RESTQUERYPARAM{format,boolean,optional} /// False for unformated values (default: true). /// /// @RESTHEADERPARAMETERS /// /// @RESTHEADERPARAM{If-Match,string,optional} /// You can conditionally delete a document based on a target revision id by /// using the `if-match` HTTP header. /// /// @RESTDESCRIPTION /// Partially updates the document identified by `document-handle`. /// The body of the request must contain a JSON document with the attributes /// to patch (the patch document). All attributes from the patch document will /// be added to the existing document if they do not yet exist, and overwritten /// in the existing document if they do exist there. /// /// Setting an attribute value to `null` in the patch document will cause a /// value of `null` be saved for the attribute by default. /// /// Optionally, the URL parameter `waitForSync` can be used to force /// synchronization of the document update operation to disk even in case /// that the `waitForSync` flag had been disabled for the entire collection. /// Thus, the `waitForSync` URL parameter can be used to force synchronization /// of just specific operations. To use this, set the `waitForSync` parameter /// to `true`. If the `waitForSync` parameter is not specified or set to /// `false`, then the collection's default `waitForSync` behavior is /// applied. The `waitForSync` URL parameter cannot be used to disable /// synchronization for collections that have a default `waitForSync` value /// of `true`. /// /// The body of the response contains a JSON object with the information about /// the handle and the revision. The attribute `_id` contains the known /// `document-handle` of the updated document, the attribute `_rev` /// contains the new document revision. /// /// If the document does not exist, then a `HTTP 404` is returned and the /// body of the response contains an error document. /// /// You can conditionally update a document based on a target revision id by /// using either the `rev` URL parameter or the `if-match` HTTP header. /// To control the update behavior in case there is a revision mismatch, you /// can use the `policy` parameter. This is the same as when replacing /// documents (see replacing documents for details). /// /// @RESTRETURNCODES /// /// @RESTRETURNCODE{201} /// is returned if the document was created successfully and `waitForSync` was /// `true`. /// /// @RESTRETURNCODE{202} /// is returned if the document was created successfully and `waitForSync` was /// `false`. /// /// @RESTRETURNCODE{400} /// is returned if the body does not contain a valid JSON representation of a /// document. The response body contains an error document in this case. /// /// @RESTRETURNCODE{404} /// is returned if collection or the document was not found /// /// @RESTRETURNCODE{412} /// is returned if a "If-Match" header or `rev` is given and the found /// document has a different version /// //////////////////////////////////////////////////////////////////////////////// function patch_api_structure (req, res) { var body; var structure; var collection = getCollectionByRequest(req, res); if (undefined === collection) { return; } var doc = getDocumentByRequest(req, res, collection); if (undefined === doc) { return; } if (matchError(req, res, doc)) { return; } body = actions.getJsonBody(req, res); if (body === undefined) { resultError(req, res, actions.HTTP_BAD, arangodb.ERROR_FAILED, "no body data"); return; } try { structure = getCollection().document(collection.name()); patchDocumentByStructure(req, res, collection, structure, doc, body); } catch (err) { patchDocument(req, res, collection, doc, body); } } //////////////////////////////////////////////////////////////////////////////// /// @brief replaces a document /// /// @RESTHEADER{PUT /_api/structure/`document-handle`,replaces a document} /// /// @RESTURLPARAMETERS /// /// @RESTURLPARAM{document-handle,string,required} /// The Handle of the Document. /// /// @RESTQUERYPARAMETERS /// /// @RESTQUERYPARAM{waitForSync,boolean,optional} /// Wait until document has been sync to disk. /// /// @RESTQUERYPARAM{rev,string,optional} /// You can conditionally replace a document based on a target revision id by /// using the `rev` URL parameter. /// /// @RESTQUERYPARAM{policy,string,optional} /// To control the update behavior in case there is a revision mismatch, you /// can use the `policy` parameter. This is the same as when replacing /// documents (see replacing documents for more details). /// /// @RESTQUERYPARAM{lang,string,optional} /// Language of the data. /// /// @RESTQUERYPARAM{format,boolean,optional} /// False for unformated values (default: true). /// /// @RESTHEADERPARAMETERS /// /// @RESTHEADERPARAM{If-Match,string,optional} /// You can conditionally replace a document based on a target revision id by /// using the `if-match` HTTP header. /// /// @RESTDESCRIPTION /// Completely updates (i.e. replaces) the document identified by `document-handle`. /// If the document exists and can be updated, then a `HTTP 201` is returned /// and the "ETag" header field contains the new revision of the document. /// /// If the new document passed in the body of the request contains the /// `document-handle` in the attribute `_id` and the revision in `_rev`, /// these attributes will be ignored. Only the URI and the "ETag" header are /// relevant in order to avoid confusion when using proxies. /// /// Optionally, the URL parameter `waitForSync` can be used to force /// synchronization of the document replacement operation to disk even in case /// that the `waitForSync` flag had been disabled for the entire collection. /// Thus, the `waitForSync` URL parameter can be used to force synchronization /// of just specific operations. To use this, set the `waitForSync` parameter /// to `true`. If the `waitForSync` parameter is not specified or set to /// `false`, then the collection's default `waitForSync` behavior is /// applied. The `waitForSync` URL parameter cannot be used to disable /// synchronization for collections that have a default `waitForSync` value /// of `true`. /// /// The body of the response contains a JSON object with the information about /// the handle and the revision. The attribute `_id` contains the known /// `document-handle` of the updated document, the attribute `_rev` /// contains the new document revision. /// /// If the document does not exist, then a `HTTP 404` is returned and the /// body of the response contains an error document. /// /// There are two ways for specifying the targeted document revision id for /// conditional replacements (i.e. replacements that will only be executed if /// the revision id found in the database matches the document revision id specified /// in the request): /// - specifying the target revision in the `rev` URL query parameter /// - specifying the target revision in the `if-match` HTTP header /// /// Specifying a target revision is optional, however, if done, only one of the /// described mechanisms must be used (either the `rev` URL parameter or the /// `if-match` HTTP header). /// Regardless which mechanism is used, the parameter needs to contain the target /// document revision id as returned in the `_rev` attribute of a document or /// by an HTTP `etag` header. /// /// For example, to conditionally replace a document based on a specific revision /// id, you the following request: /// /// - PUT /_api/document/`document-handle`?rev=`etag` /// /// If a target revision id is provided in the request (e.g. via the `etag` value /// in the `rev` URL query parameter above), ArangoDB will check that /// the revision id of the document found in the database is equal to the target /// revision id provided in the request. If there is a mismatch between the revision /// id, then by default a `HTTP 412` conflict is returned and no replacement is /// performed. /// /// The conditional update behavior can be overridden with the `policy` URL query parameter: /// /// - PUT /_api/document/`document-handle`?policy=`policy` /// /// If `policy` is set to `error`, then the behavior is as before: replacements /// will fail if the revision id found in the database does not match the target /// revision id specified in the request. /// /// If `policy` is set to `last`, then the replacement will succeed, even if the /// revision id found in the database does not match the target revision id specified /// in the request. You can use the `last` `policy` to force replacements. /// /// @RESTRETURNCODES /// /// @RESTRETURNCODE{201} /// is returned if the document was created successfully and `waitForSync` was /// `true`. /// /// @RESTRETURNCODE{202} /// is returned if the document was created successfully and `waitForSync` was /// `false`. /// /// @RESTRETURNCODE{400} /// is returned if the body does not contain a valid JSON representation of a /// document. The response body contains an error document in this case. /// /// @RESTRETURNCODE{404} /// is returned if collection or the document was not found /// /// @RESTRETURNCODE{412} /// is returned if a "If-Match" header or `rev` is given and the found /// document has a different version /// //////////////////////////////////////////////////////////////////////////////// function put_api_structure (req, res) { var body; var structure; var collection = getCollectionByRequest(req, res); if (undefined === collection) { return; } var doc = getDocumentByRequest(req, res, collection); if (undefined === doc) { return; } if (matchError(req, res, doc)) { return; } body = actions.getJsonBody(req, res); if (body === undefined) { resultError(req, res, actions.HTTP_BAD, arangodb.ERROR_FAILED, "no body data"); return; } try { structure = getCollection().document(collection.name()); replaceDocumentByStructure(req, res, collection, structure, doc, body); } catch (err) { replaceDocument(req, res, collection, doc, body); } } //////////////////////////////////////////////////////////////////////////////// /// @brief creates a document /// /// @RESTHEADER{POST /_api/structure,creates a document} /// /// @RESTBODYPARAM{document,json,required} /// A JSON representation of document. /// /// @RESTQUERYPARAMETERS /// /// @RESTQUERYPARAM{collection,string,required} /// The collection name. /// /// @RESTQUERYPARAM{createCollection,boolean,optional} /// If this parameter has a value of `true` or `yes`, then the collection is /// created if it does not yet exist. Other values will be ignored so the /// collection must be present for the operation to succeed. /// /// @RESTQUERYPARAM{waitForSync,boolean,optional} /// Wait until document has been sync to disk. /// /// @RESTQUERYPARAM{lang,string,optional} /// Language of the send data. /// /// @RESTQUERYPARAM{format,boolean,optional} /// True, if the document contains formatted values (default: true). /// /// @RESTDESCRIPTION /// Creates a new document in the collection named `collection`. A JSON /// representation of the document must be passed as the body of the POST /// request. /// /// If the document was created successfully, then the "Location" header /// contains the path to the newly created document. The "ETag" header field /// contains the revision of the document. /// /// The body of the response contains a JSON object with the following /// attributes: /// /// - `_id` contains the document handle of the newly created document /// - `_key` contains the document key /// - `_rev` contains the document revision /// /// If the collection parameter `waitForSync` is `false`, then the call returns /// as soon as the document has been accepted. It will not wait, until the /// documents has been sync to disk. /// /// Optionally, the URL parameter `waitForSync` can be used to force /// synchronization of the document creation operation to disk even in case that /// the `waitForSync` flag had been disabled for the entire collection. Thus, /// the `waitForSync` URL parameter can be used to force synchronization of just /// this specific operations. To use this, set the `waitForSync` parameter to /// `true`. If the `waitForSync` parameter is not specified or set to `false`, /// then the collection's default `waitForSync` behavior is applied. The /// `waitForSync` URL parameter cannot be used to disable synchronization for /// collections that have a default `waitForSync` value of `true`. /// /// @RESTRETURNCODES /// /// @RESTRETURNCODE{201} /// is returned if the document was created successfully and `waitForSync` was /// `true`. /// /// @RESTRETURNCODE{202} /// is returned if the document was created successfully and `waitForSync` was /// `false`. /// /// @RESTRETURNCODE{400} /// is returned if the body does not contain a valid JSON representation of a /// document. The response body contains an error document in this case. /// /// @RESTRETURNCODE{404} /// is returned if the collection specified by `collection` is unknown. The /// response body contains an error document in this case. //////////////////////////////////////////////////////////////////////////////// function post_api_structure (req, res) { // POST /_api/structure var body; var structure; var collection; var collectionName = req.parameters.collection; if (undefined === collectionName) { resultError(req, res, actions.HTTP_NOT_FOUND, arangodb.ERROR_ARANGO_COLLECTION_NOT_FOUND, "collection not found"); return; } try { collection = db._collection(collectionName); } catch (err) { } if (null === collection) { var createCollection = stringToBoolean(req.parameters.createCollection); if (createCollection) { try { db._create(collectionName); collection = db._collection(collectionName); } catch(err2) { resultError(req, res, actions.HTTP_NOT_FOUND, arangodb.ERROR_ARANGO_COLLECTION_NOT_FOUND, err2); return; } } } if (undefined === collection || null === collection) { resultError(req, res, actions.HTTP_NOT_FOUND, arangodb.ERROR_ARANGO_COLLECTION_NOT_FOUND, "collection not found"); return; } body = actions.getJsonBody(req, res); if (body === undefined) { resultError(req, res, actions.HTTP_BAD, arangodb.ERROR_FAILED, "no body data"); return; } try { structure = getCollection().document(collection.name()); saveDocumentByStructure(req, res, collection, structure, body); } catch (err3) { saveDocument(req, res, collection, body); } } //////////////////////////////////////////////////////////////////////////////// /// @brief handles a structure request //////////////////////////////////////////////////////////////////////////////// actions.defineHttp({ url : API, callback : function (req, res) { try { if (req.requestType === actions.DELETE) { delete_api_structure(req, res); } else if (req.requestType === actions.GET) { get_api_structure(req, res); } else if (req.requestType === actions.HEAD) { head_api_structure(req, res); } else if (req.requestType === actions.PATCH) { patch_api_structure(req, res); } else if (req.requestType === actions.POST) { post_api_structure(req, res); } else if (req.requestType === actions.PUT) { put_api_structure(req, res); } else { actions.resultUnsupported(req, res); } } catch (err) { actions.resultException(req, res, err, undefined, false); } } }); // ----------------------------------------------------------------------------- // --SECTION-- END-OF-FILE // ----------------------------------------------------------------------------- // Local Variables: // mode: outline-minor // outline-regexp: "/// @brief\\|/// {@inheritDoc}\\|/// @page\\|// --SECTION--\\|/// @\\}" // End: