//////////////////////////////////////////////////////////////////////////////// /// DISCLAIMER /// /// Copyright 2019 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 Andrey Abramov /// @author Vasiliy Nabatchikov //////////////////////////////////////////////////////////////////////////////// #include "v8-analyzers.h" #include #include #include "Basics/StringUtils.h" #include "Basics/StaticStrings.h" #include "IResearch/IResearchAnalyzerFeature.h" #include "IResearch/VelocyPackHelper.h" #include "RestServer/SystemDatabaseFeature.h" #include "Transaction/V8Context.h" #include "V8/v8-conv.h" #include "V8/v8-globals.h" #include "V8/v8-vpack.h" #include "V8Server/v8-externals.h" #include "V8Server/v8-vocbaseprivate.h" #include "VocBase/vocbase.h" namespace { //////////////////////////////////////////////////////////////////////////////// /// @brief unwraps an analyser wrapped via WrapAnalyzer(...) /// @return collection or nullptr on failure //////////////////////////////////////////////////////////////////////////////// arangodb::iresearch::AnalyzerPool* UnwrapAnalyzer( v8::Isolate* isolate, v8::Local const& holder) { return TRI_UnwrapClass( holder, WRP_IRESEARCH_ANALYZER_TYPE, TRI_IGETC); } //////////////////////////////////////////////////////////////////////////////// /// @brief wraps an Analyzer //////////////////////////////////////////////////////////////////////////////// v8::Handle WrapAnalyzer( v8::Isolate* isolate, arangodb::iresearch::AnalyzerPool::ptr const& analyzer) { v8::EscapableHandleScope scope(isolate); TRI_GET_GLOBALS(); TRI_GET_GLOBAL(IResearchAnalyzerTempl, v8::ObjectTemplate); auto result = IResearchAnalyzerTempl->NewInstance(); if (result.IsEmpty()) { return scope.Escape(result); } auto itr = TRI_v8_global_t::SharedPtrPersistent::emplace(*isolate, analyzer); auto& entry = itr.first; result->SetInternalField( // required for TRI_UnwrapClass(...) SLOT_CLASS_TYPE, v8::Integer::New(isolate, WRP_IRESEARCH_ANALYZER_TYPE) // args ); result->SetInternalField(SLOT_CLASS, entry.get()); return scope.Escape(result); } void JS_AnalyzerFeatures(v8::FunctionCallbackInfo const& args) { TRI_V8_TRY_CATCH_BEGIN(isolate); v8::HandleScope scope(isolate); auto* analyzer = UnwrapAnalyzer(isolate, args.Holder()); if (!analyzer) { TRI_V8_THROW_EXCEPTION_INTERNAL("cannot extract analyzer"); } // ........................................................................... // end of parameter parsing // ........................................................................... if (!arangodb::iresearch::IResearchAnalyzerFeature::canUse(analyzer->name(), arangodb::auth::Level::RO)) { TRI_V8_THROW_EXCEPTION_MESSAGE( // exception TRI_ERROR_FORBIDDEN, // code "insufficient rights to get analyzer" // message ); } try { auto i = 0; auto result = v8::Array::New(isolate); for (auto& feature: analyzer->features()) { if (feature) { // valid if (feature->name().null()) { result->Set(i++, v8::Null(isolate)); } else { result->Set( // set value i++, TRI_V8_STD_STRING(isolate, std::string(feature->name())) // args ); } } } TRI_V8_RETURN(result); } catch (arangodb::basics::Exception const& ex) { TRI_V8_THROW_EXCEPTION_MESSAGE(ex.code(), ex.what()); } catch (std::exception const& ex) { TRI_V8_THROW_EXCEPTION_MESSAGE(TRI_ERROR_INTERNAL, ex.what()); } catch (...) { TRI_V8_THROW_EXCEPTION_MESSAGE( // exception TRI_ERROR_INTERNAL, // code "cannot access analyzer features" // message ); } TRI_V8_TRY_CATCH_END } void JS_AnalyzerName(v8::FunctionCallbackInfo const& args) { TRI_V8_TRY_CATCH_BEGIN(isolate); v8::HandleScope scope(isolate); auto* analyzer = UnwrapAnalyzer(isolate, args.Holder()); if (!analyzer) { TRI_V8_THROW_EXCEPTION_INTERNAL("cannot extract analyzer"); } // ........................................................................... // end of parameter parsing // ........................................................................... if (!arangodb::iresearch::IResearchAnalyzerFeature::canUse(analyzer->name(), arangodb::auth::Level::RO)) { TRI_V8_THROW_EXCEPTION_MESSAGE( // exception TRI_ERROR_FORBIDDEN, // code "insufficient rights to get analyzer" // message ); } try { auto result = TRI_V8_STD_STRING(isolate, analyzer->name()); TRI_V8_RETURN(result); } catch (arangodb::basics::Exception const& ex) { TRI_V8_THROW_EXCEPTION_MESSAGE(ex.code(), ex.what()); } catch (std::exception const& ex) { TRI_V8_THROW_EXCEPTION_MESSAGE(TRI_ERROR_INTERNAL, ex.what()); } catch (...) { TRI_V8_THROW_EXCEPTION_MESSAGE( // exception TRI_ERROR_INTERNAL, // code "cannot access analyzer name" // message ); } TRI_V8_TRY_CATCH_END } void JS_AnalyzerProperties(v8::FunctionCallbackInfo const& args) { TRI_V8_TRY_CATCH_BEGIN(isolate); v8::HandleScope scope(isolate); auto* analyzer = UnwrapAnalyzer(isolate, args.Holder()); if (!analyzer) { TRI_V8_THROW_EXCEPTION_INTERNAL("cannot extract analyzer"); } // ........................................................................... // end of parameter parsing // ........................................................................... if (!arangodb::iresearch::IResearchAnalyzerFeature::canUse(analyzer->name(), arangodb::auth::Level::RO)) { TRI_V8_THROW_EXCEPTION_MESSAGE( // exception TRI_ERROR_FORBIDDEN, // code "insufficient rights to get analyzer" // message ); } try { auto result = TRI_VPackToV8(isolate, analyzer->properties()); TRI_V8_RETURN(result); } catch (arangodb::basics::Exception const& ex) { TRI_V8_THROW_EXCEPTION_MESSAGE(ex.code(), ex.what()); } catch (std::exception const& ex) { TRI_V8_THROW_EXCEPTION_MESSAGE(TRI_ERROR_INTERNAL, ex.what()); } catch (...) { TRI_V8_THROW_EXCEPTION_MESSAGE( // exception TRI_ERROR_INTERNAL, // code "cannot access analyzer properties" // message ); } TRI_V8_TRY_CATCH_END } void JS_AnalyzerType(v8::FunctionCallbackInfo const& args) { TRI_V8_TRY_CATCH_BEGIN(isolate); v8::HandleScope scope(isolate); auto* analyzer = UnwrapAnalyzer(isolate, args.Holder()); if (!analyzer) { TRI_V8_THROW_EXCEPTION_INTERNAL("cannot extract analyzer"); } // ........................................................................... // end of parameter parsing // ........................................................................... if (!arangodb::iresearch::IResearchAnalyzerFeature::canUse(analyzer->name(), arangodb::auth::Level::RO)) { TRI_V8_THROW_EXCEPTION_MESSAGE( // exception TRI_ERROR_FORBIDDEN, // code "insufficient rights to get analyzer" // message ); } try { if (analyzer->type().null()) { TRI_V8_RETURN(v8::Null(isolate)); } auto result = TRI_V8_STD_STRING(isolate, std::string(analyzer->type())); TRI_V8_RETURN(result); } catch (arangodb::basics::Exception const& ex) { TRI_V8_THROW_EXCEPTION_MESSAGE(ex.code(), ex.what()); } catch (std::exception const& ex) { TRI_V8_THROW_EXCEPTION_MESSAGE(TRI_ERROR_INTERNAL, ex.what()); } catch (...) { TRI_V8_THROW_EXCEPTION_MESSAGE( // exception TRI_ERROR_INTERNAL, // code "cannot access analyzer type" // message ); } TRI_V8_TRY_CATCH_END } void JS_Create(v8::FunctionCallbackInfo const& args) { TRI_V8_TRY_CATCH_BEGIN(isolate); v8::HandleScope scope(isolate); auto& vocbase = GetContextVocBase(isolate); if (vocbase.isDangling()) { TRI_V8_THROW_EXCEPTION(TRI_ERROR_ARANGO_DATABASE_NOT_FOUND); } // we require at least 2 but no more than 4 arguments // save(name: , type: [, parameters: [, features: ]]); if (args.Length() < 2 // too few args || args.Length() > 4 // too many args || !args[0]->IsString() // not a string || !args[1]->IsString() // not a string ) { TRI_V8_THROW_EXCEPTION_USAGE("save(, [, [, ]])"); } PREVENT_EMBEDDED_TRANSACTION(); auto& system = arangodb::application_features::ApplicationServer::server(); auto& analyzers = system.getFeature(); auto sysVocbase = system.hasFeature() ? system.getFeature().use() : nullptr; auto nameFromArgs = TRI_ObjectToString(isolate, args[0]); auto splittedAnalyzerName = arangodb::iresearch::IResearchAnalyzerFeature::splitAnalyzerName(nameFromArgs); if (!arangodb::iresearch::IResearchAnalyzerFeature::analyzerReachableFromDb( splittedAnalyzerName.first, vocbase.name())) { TRI_V8_THROW_EXCEPTION_MESSAGE( TRI_ERROR_FORBIDDEN, "Database in analyzer name does not match current database"); return; } auto name = splittedAnalyzerName.second; if (!TRI_vocbase_t::IsAllowedName(false, arangodb::velocypack::StringRef(name))) { TRI_V8_THROW_EXCEPTION_MESSAGE( TRI_ERROR_BAD_PARAMETER, std::string("invalid characters in analyzer name '").append(name).append("'") ); return; } std::string nameBuf; if (sysVocbase) { nameBuf = arangodb::iresearch::IResearchAnalyzerFeature::normalize( // normalize name, vocbase, *sysVocbase // args ); name = nameBuf; }; auto type = TRI_ObjectToString(isolate, args[1]); VPackSlice propertiesSlice = VPackSlice::emptyObjectSlice(); VPackBuilder propertiesBuilder; if (args.Length() > 2) { // have properties if (args[2]->IsString()) { std::string const propertiesBuf = TRI_ObjectToString(isolate, args[2]); arangodb::velocypack::Parser(propertiesBuilder).parse(propertiesBuf); propertiesSlice = propertiesBuilder.slice(); } else if (args[2]->IsObject()) { auto value = args[2]->ToObject(TRI_IGETC).FromMaybe(v8::Local()); auto res = TRI_V8ToVPack(isolate, propertiesBuilder, value, false); if (TRI_ERROR_NO_ERROR != res) { TRI_V8_THROW_EXCEPTION(res); } propertiesSlice = propertiesBuilder.slice(); } else if (!args[2]->IsNull()) { TRI_V8_THROW_TYPE_ERROR(" must be an object"); } } // properties at the end should be parsed into object if (!propertiesSlice.isObject()) { TRI_V8_THROW_TYPE_ERROR(" must be an object"); } irs::flags features; if (args.Length() > 3) { // have features if (!args[3]->IsArray()) { TRI_V8_THROW_TYPE_ERROR(" must be an array"); } auto value = v8::Local::Cast(args[3]); for (uint32_t i = 0, count = value->Length(); i < count; ++i) { auto subValue = value->Get(i); if (!subValue->IsString()) { TRI_V8_THROW_TYPE_ERROR(" must be a string"); } auto* feature = // feature irs::attribute::type_id::get(TRI_ObjectToString(isolate, subValue), false); if (!feature) { TRI_V8_THROW_TYPE_ERROR(" not supported"); } features.add(*feature); } } // ........................................................................... // end of parameter parsing // ........................................................................... if (!arangodb::iresearch::IResearchAnalyzerFeature::canUse(name, arangodb::auth::Level::RW)) { TRI_V8_THROW_EXCEPTION_MESSAGE( // exception TRI_ERROR_FORBIDDEN, // code "insufficient rights to create analyzer" // message ); } try { arangodb::iresearch::IResearchAnalyzerFeature::EmplaceResult result; auto res = analyzers.emplace(result, name, type, propertiesSlice, features); if (!res.ok()) { TRI_V8_THROW_EXCEPTION(res); } if (!result.first) { TRI_V8_THROW_EXCEPTION_MESSAGE( // exception TRI_ERROR_INTERNAL, // code "problem creating view" // message ); } auto v8Result = WrapAnalyzer(isolate, result.first); if (v8Result.IsEmpty()) { TRI_V8_THROW_EXCEPTION_MEMORY(); } TRI_V8_RETURN(v8Result); } catch (arangodb::basics::Exception const& ex) { TRI_V8_THROW_EXCEPTION_MESSAGE(ex.code(), ex.what()); } catch (std::exception const& ex) { TRI_V8_THROW_EXCEPTION_MESSAGE(TRI_ERROR_INTERNAL, ex.what()); } catch (...) { TRI_V8_THROW_EXCEPTION_MESSAGE( // exception TRI_ERROR_INTERNAL, // code "cannot create analyzer" // message ); } TRI_V8_TRY_CATCH_END } void JS_Get(v8::FunctionCallbackInfo const& args) { TRI_V8_TRY_CATCH_BEGIN(isolate); v8::HandleScope scope(isolate); auto& vocbase = GetContextVocBase(isolate); if (vocbase.isDropped()) { TRI_V8_THROW_EXCEPTION(TRI_ERROR_ARANGO_DATABASE_NOT_FOUND); } // expecting one argument // analyzer(name: ); if (args.Length() != 1 || !args[0]->IsString()) { TRI_V8_THROW_EXCEPTION_USAGE("analyzer()"); } PREVENT_EMBEDDED_TRANSACTION(); auto& system = arangodb::application_features::ApplicationServer::server(); ; auto& analyzers = system.getFeature(); auto sysVocbase = system.hasFeature() ? system.getFeature().use() : nullptr; auto name = TRI_ObjectToString(isolate, args[0]); std::string nameBuf; if (sysVocbase) { nameBuf = arangodb::iresearch::IResearchAnalyzerFeature::normalize( // normalize name, vocbase, *sysVocbase // args ); name = nameBuf; }; // ........................................................................... // end of parameter parsing // ........................................................................... const auto analyzerVocbase = arangodb::iresearch::IResearchAnalyzerFeature::extractVocbaseName(name); if (!arangodb::iresearch::IResearchAnalyzerFeature::analyzerReachableFromDb( analyzerVocbase, vocbase.name(), true)) { std::string errorMessage("Analyzer '"); errorMessage.append(name) .append("' is not accessible. Only analyzers from current database ('") .append(vocbase.name()) .append("')"); if (vocbase.name() != arangodb::StaticStrings::SystemDatabase) { errorMessage.append(" or system database"); } errorMessage.append(" are available"); TRI_V8_THROW_EXCEPTION_MESSAGE( TRI_ERROR_FORBIDDEN, // code errorMessage ); } if (!arangodb::iresearch::IResearchAnalyzerFeature::canUse(name, arangodb::auth::Level::RO)) { TRI_V8_THROW_EXCEPTION_MESSAGE( // exception TRI_ERROR_FORBIDDEN, // code "insufficient rights to get analyzer" // message ); } try { auto analyzer = analyzers.get(name); if (!analyzer) { TRI_V8_RETURN_NULL(); } auto result = WrapAnalyzer(isolate, analyzer); if (result.IsEmpty()) { TRI_V8_THROW_EXCEPTION_MEMORY(); } TRI_V8_RETURN(result); } catch (arangodb::basics::Exception const& ex) { TRI_V8_THROW_EXCEPTION_MESSAGE(ex.code(), ex.what()); } catch (std::exception const& ex) { TRI_V8_THROW_EXCEPTION_MESSAGE(TRI_ERROR_INTERNAL, ex.what()); } catch (...) { TRI_V8_THROW_EXCEPTION_MESSAGE(TRI_ERROR_INTERNAL, "cannot get analyzer"); } TRI_V8_TRY_CATCH_END } void JS_List(v8::FunctionCallbackInfo const& args) { TRI_V8_TRY_CATCH_BEGIN(isolate); v8::HandleScope scope(isolate); auto& vocbase = GetContextVocBase(isolate); if (vocbase.isDropped()) { TRI_V8_THROW_EXCEPTION(TRI_ERROR_ARANGO_DATABASE_NOT_FOUND); } auto& system = arangodb::application_features::ApplicationServer::server(); ; auto& analyzers = system.getFeature(); auto sysVocbase = system.hasFeature() ? system.getFeature().use() : nullptr; // ........................................................................... // end of parameter parsing // ........................................................................... typedef arangodb::iresearch::AnalyzerPool::ptr AnalyzerPoolPtr; std::vector result; auto visitor = [&result](AnalyzerPoolPtr const& analyzer)->bool { if (analyzer) { result.emplace_back(analyzer); } return true; // continue with next analyzer }; try { analyzers.visit(visitor, nullptr); // include static analyzers if (arangodb::iresearch::IResearchAnalyzerFeature::canUse(vocbase, arangodb::auth::Level::RO)) { analyzers.visit(visitor, &vocbase); } // include analyzers from the system vocbase if possible if (sysVocbase // have system vocbase && sysVocbase->name() != vocbase.name() // not same vocbase as current && arangodb::iresearch::IResearchAnalyzerFeature::canUse(*sysVocbase, arangodb::auth::Level::RO)) { analyzers.visit(visitor, sysVocbase.get()); } auto v8Result = v8::Array::New(isolate); for (size_t i = 0, count = result.size(); i < count; ++i) { auto analyzer = WrapAnalyzer(isolate, result[i]); if (analyzer.IsEmpty() || i > std::numeric_limits::max()) { TRI_V8_THROW_EXCEPTION_MEMORY(); } v8Result->Set(static_cast(i), analyzer); // cast safe because of check above } TRI_V8_RETURN(v8Result); } catch (arangodb::basics::Exception const& ex) { TRI_V8_THROW_EXCEPTION_MESSAGE(ex.code(), ex.what()); } catch (std::exception const& ex) { TRI_V8_THROW_EXCEPTION_MESSAGE(TRI_ERROR_INTERNAL, ex.what()); } catch (...) { TRI_V8_THROW_EXCEPTION_MESSAGE(TRI_ERROR_INTERNAL, "cannot list analyzers"); } TRI_V8_TRY_CATCH_END } void JS_Remove(v8::FunctionCallbackInfo const& args) { TRI_V8_TRY_CATCH_BEGIN(isolate); v8::HandleScope scope(isolate); auto& vocbase = GetContextVocBase(isolate); if (vocbase.isDangling()) { TRI_V8_THROW_EXCEPTION(TRI_ERROR_ARANGO_DATABASE_NOT_FOUND); } // we require 1 string argument and an optional boolean argument // remove(name: [, force: ]) if (args.Length() < 1 // too few args || args.Length() > 2 // too many args || !args[0]->IsString() // not a string ) { TRI_V8_THROW_EXCEPTION_USAGE("remove( [, ])"); } PREVENT_EMBEDDED_TRANSACTION(); auto& system = arangodb::application_features::ApplicationServer::server(); ; auto& analyzers = system.getFeature(); auto sysVocbase = system.hasFeature() ? system.getFeature().use() : nullptr; auto nameFromArgs = TRI_ObjectToString(isolate, args[0]); auto splittedAnalyzerName = arangodb::iresearch::IResearchAnalyzerFeature::splitAnalyzerName(nameFromArgs); if (!arangodb::iresearch::IResearchAnalyzerFeature::analyzerReachableFromDb( splittedAnalyzerName.first, vocbase.name())) { TRI_V8_THROW_EXCEPTION_MESSAGE( TRI_ERROR_FORBIDDEN, "Database in analyzer name does not match current database"); return; } auto name = splittedAnalyzerName.second; if (!TRI_vocbase_t::IsAllowedName(false, arangodb::velocypack::StringRef(name))) { TRI_V8_THROW_EXCEPTION_MESSAGE( TRI_ERROR_BAD_PARAMETER, std::string( "Invalid characters in analyzer name '").append(name) .append("'.") ); } std::string nameBuf; if (sysVocbase) { nameBuf = arangodb::iresearch::IResearchAnalyzerFeature::normalize( // normalize name, vocbase, *sysVocbase // args ); name = nameBuf; }; bool force = false; if (args.Length() > 1) { if (!args[1]->IsBoolean() && !args[1]->IsBooleanObject()) { TRI_V8_THROW_TYPE_ERROR(" must be a boolean"); } force = TRI_ObjectToBoolean(isolate, args[1]); } // ........................................................................... // end of parameter parsing // ........................................................................... if (!arangodb::iresearch::IResearchAnalyzerFeature::canUse(name, arangodb::auth::Level::RW)) { TRI_V8_THROW_EXCEPTION_MESSAGE( // exception TRI_ERROR_FORBIDDEN, // code "insufficient rights to remove analyzer" // message ); } try { auto res = analyzers.remove(name, force); if (!res.ok()) { TRI_V8_THROW_EXCEPTION(res); } TRI_V8_RETURN_UNDEFINED(); } catch (arangodb::basics::Exception const& ex) { TRI_V8_THROW_EXCEPTION_MESSAGE(ex.code(), ex.what()); } catch (std::exception const& ex) { TRI_V8_THROW_EXCEPTION_MESSAGE(TRI_ERROR_INTERNAL, ex.what()); } catch (...) { TRI_V8_THROW_EXCEPTION_MESSAGE( // exception TRI_ERROR_INTERNAL, // code "cannot remove analyzer" // message ); } TRI_V8_TRY_CATCH_END } } namespace arangodb { namespace iresearch { void TRI_InitV8Analyzers(TRI_v8_global_t& v8g, v8::Isolate* isolate) { // 'analyzers' feature functions { auto fnTemplate = v8::FunctionTemplate::New(isolate); fnTemplate->SetClassName(TRI_V8_ASCII_STRING(isolate, "ArangoAnalyzersCtor")); auto objTemplate = fnTemplate->InstanceTemplate(); objTemplate->SetInternalFieldCount(0); TRI_AddMethodVocbase(isolate, objTemplate, TRI_V8_ASCII_STRING(isolate, "analyzer"), JS_Get); TRI_AddMethodVocbase(isolate, objTemplate, TRI_V8_ASCII_STRING(isolate, "remove"), JS_Remove); TRI_AddMethodVocbase(isolate, objTemplate, TRI_V8_ASCII_STRING(isolate, "save"), JS_Create); TRI_AddMethodVocbase(isolate, objTemplate, TRI_V8_ASCII_STRING(isolate, "toArray"), JS_List); v8g.IResearchAnalyzersTempl.Reset(isolate, objTemplate); auto instance = objTemplate->NewInstance(); // register the global object accessable via JavaScipt if (!instance.IsEmpty()) { TRI_AddGlobalVariableVocbase(isolate, TRI_V8_ASCII_STRING(isolate, "ArangoAnalyzers"), instance); } } // individual analyzer functions { auto fnTemplate = v8::FunctionTemplate::New(isolate); fnTemplate->SetClassName(TRI_V8_ASCII_STRING(isolate, "ArangoAnalyzer")); auto objTemplate = fnTemplate->InstanceTemplate(); objTemplate->SetInternalFieldCount(2); // SLOT_CLASS_TYPE + SLOT_CLASS TRI_AddMethodVocbase(isolate, objTemplate, TRI_V8_ASCII_STRING(isolate, "features"), JS_AnalyzerFeatures); TRI_AddMethodVocbase(isolate, objTemplate, TRI_V8_ASCII_STRING(isolate, "name"), JS_AnalyzerName); TRI_AddMethodVocbase(isolate, objTemplate, TRI_V8_ASCII_STRING(isolate, "properties"), JS_AnalyzerProperties); TRI_AddMethodVocbase(isolate, objTemplate, TRI_V8_ASCII_STRING(isolate, "type"), JS_AnalyzerType); v8g.IResearchAnalyzerTempl.Reset(isolate, objTemplate); TRI_AddGlobalFunctionVocbase( // required only for pretty-printing via JavaScript (must to be defined AFTER v8g.IResearchAnalyzerTempl.Reset(...)) isolate, // isolate TRI_V8_ASCII_STRING(isolate, "ArangoAnalyzer"), // name fnTemplate->GetFunction() // impl ); } } } // iresearch } // arangodb // ----------------------------------------------------------------------------- // --SECTION-- END-OF-FILE // -----------------------------------------------------------------------------