Logo

Introduction

This book covers every major feature of the aragog library.

Note that everything in the lib is documented, don't forget to check the technical documentation for detailed information.

Aragog is an object-document mapper for Arango DB, including:

  • Document mapping with rust structs
  • complete CRUD operations with hooks (callbacks)
  • Type safe query engine
  • Graph and edges traversal

Set up ArangoDB

Installation (See official documentation Here)

  • Download Link
  • Run it with /usr/local/sbin/arangod The default installation contains one database _system and a user named root
  • Create a user and database for the project with the arangosh shell
arangosh> db._createDatabase("DB_NAME");
arangosh> var users = require("@arangodb/users");
arangosh> users.save("DB_USER", "DB_PASSWORD");
arangosh> users.grantDatabase("DB_USER", "DB_NAME");

It is a good practice to create a test db and a development db.

  • you can connect to the newly created db with
$> arangosh --server.username $DB_USER --server.database $DB_NAME

Installation

Aragog CLI

Install the aragog migration and schema generation command line interface with cargo:

cargo install aragog_cli

Aragog Lib

Add to your cargo.toml the following:

aragog = "0.17.1"

Cargo features

Async and Blocking

By default, all aragog items are asynchronous, you can compile aragog in a synchronous build using the blocking feature:

aragog = { version = "0.17.1", features = ["blocking"] }

OpenSSL and Rustls

aragog uses reqwest to query ArangoDB. By default, OpenSSL is used, but you can compile aragog to use rustls using the rustls feature:

aragog = { version = "0.17.1", features = ["rustls"], default-features = false }

You need to disable the default features. Don't forget to add the derive feature to use the derive macros.

Minimal Traits

If you don't need the following traits:

  • AuthorizeAction
  • New
  • Update

You can disable them with the minimal_traits feature:

aragog = { version = "0.17.1", features = ["minimal_traits"] }

Initialization

In order to work, Aragog needs you to define your database schema in an schema.yaml.

We provide aragog_cli for this purpose, generating a synchronized and versioned schema and a safe migration system.

With this schema you can initialize a database connection to provide a database access required for all database operations.

Schema Generation

aragog_cli provides the following basics:

Creating a Migration

Command: aragog create_migration MIGRATION_NAME

Creates a new migration file in SCHEMA_PATH/migrations/. If the db folder is missing it will be created automatically.

Launch migrations

Command: aragog migrate

Will launch every migration in SCHEMA_PATH/migrations/ and update the schema according to its current version. If there is no schema it will be generated.

Note: ArangoDB doesn't handle transactional operations for collection, index and graph management

Rollback migrations

Command: aragog rollback

Will rollback 1 migration in SCHEMA_PATH/migrations/ and update the schema according to its current version.

Command: aragog rollback COUNT

Will rollback COUNT migration in $CHEMA_PATH/migrations/ and update the schema according to its current version.

The schema is generated twice:

  • once on the file system (schema.yaml)
  • once in the database, the snapshot (synchronized version)

This allows seamless deployment, as the migrations launch will check the current snapshot

Using aragog with an exiting database

The aragog_cli provides a discover command creating a migration file for already initialized database and apply it to the schema.

The Database Connection

The database connection is the main handler of every ArangoDB communication. You need to initialize one to be able to use the other library features.

The connection loads a Database Schema (schema.yaml) file. Use aragog_cli to create migrations and generate the file.

Note: You can also create your own file but it's not recommended

Creating a Database Connection

To connect to the database and initialize a database connection you may use the following builder pattern options:


#![allow(unused_variables)]
fn main() {
let db_connection = DatabaseConnection::builder()
    // You can specify a host and credentials with this method.
    // Otherwise, the builder will look for the env vars: `DB_HOST`, `DB_NAME`, `DB_USER` and `DB_PASSWORD`.
    .with_credentials("http://localhost:8529", "db", "user", "password")
    // You can specify a authentication mode between `Basic` and `Jwt`
    // Otherwise the default value will be used (`Basic`).
    .with_auth_mode(AuthMode::Basic)
    // You can specify some operations options that will be used for every `write` operations like
    // `create`, `save` and `delete`.
    .with_operation_options(OperationOptions::default())
    // You can specify a schema path to initialize the database connection
    // Otherwise the env var `SCHEMA_PATH` or the default value `config/db/schema.yaml` will be used.
    .with_schema_path("config/db/schema.yaml")
    // If you prefer you can use your own custom schema
    .with_schema(DatabaseSchema::default())
    // The schema wil silently apply to the database, useful only if you don't use the CLI and migrations
    .apply_schema()
    // You then need to build the connection
    .build()
    .await
    .unwrap();
}

Using env vars

None of the builder options are mandatory, the following works perfectly if all required environment variables are set:


#![allow(unused_variables)]
fn main() {
let db_connection = DatabaseConnection::builder()
    .build()
    .await
    .unwrap();
}

The env vars are the following:

NameDescription
DB_HOSTThe ArangoDB host, usually http://localhost:8259
DB_NAMEThe ArangoDB database name
DB_USERThe Database user you want to use
DB_PASSWORDThe DB_USER password
SCHEMA_PATHThe path of the schema file, by default config/db/schema.yaml

It is recommended to leave the SCHEMA_PATH unset, as the default value is idiomatic

Technical notes

DatabaseAccess trait

The object DatabaseConnection is the default handler for every database operations but you can create your own.

Every operation taking the connection as an argument is taking a Generic type implementing DatabaseAccess, so you can implement it on your own struct.

Note: This is not recommended, modification to DatabaseAccess can happen without considering them as breaking.

Trait object

All aragog API using a DatabaseAccess type also use ?Sized (Sized restriction relaxation)

This means you can use dynamically typed trait objects.

Example extract from boxed example:


#![allow(unused_variables)]
fn main() {
pub struct BoxedConnection {
    pub connection: Box<dyn DatabaseAccess>,
}

impl BoxedConnection {
    pub fn connection(&self) -> &dyn DatabaseAccess {
        self.connection.deref()
    }
}
}

Database truncation

The DatabaseConnection provides a truncate_database method but you should use it only for testing purposes, it is highly destructive as it will drop every collections known to the connection.

The Record trait

This trait defines aragog ODM (Object-Document mapper). Every type implementing this trait becomes a Model that can be mapped to an ArangoDB collection document.

Note: enums don't work as records but can be record fields

When declaring a model like the following:


#![allow(unused_variables)]
fn main() {
use aragog::Record;

#[derive(Serialize, Deserialize, Clone, Record)]
pub struct User {
    pub username: String,
    pub first_name: String,
    pub last_name: String,
    pub age: usize
}
}

To derive Record your structure needs to derive or implement Serialize, Deserialize and Clone which are needed to store the document data.

We don't specify the _key field, as we describe the document's data;

Note: An ArangoDB document is identified by its _key field which is its primary identifier and _id and _rev fields not yet used by aragog.

Custom collection name

By default, the collection name associated with the model will be the same. A User struct deriving Record will be stored in a User collection (case sensitive).

In the case were your model and collection name don't match you can specify a collection_name attribute along with the derive macro:


#![allow(unused_variables)]
fn main() {
use aragog::Record;

#[derive(Serialize, Deserialize, Clone, Record)]
#[collection_name = "Users"]
pub struct User {
    pub username: String,
    pub first_name: String,
    pub last_name: String,
    pub age: usize
}
}

In this example, the User models will be synced with the Users collection.

Synced documents

To create a document in the database we need to use the aragog generic struct DatabaseRecord<T>.

DatabaseRecord describes a document synchronized with the database:


#![allow(unused_variables)]
fn main() {
// The User document data
let user = User {
   username: String::from("LeRevenant1234"),
   first_name: String::from("Robert"),
   last_name: String::from("Surcouf"),
   age: 18
};
// We create the document on the database collection "User", returning a `DatabaseRecord<User>`
let mut user_record = DatabaseRecord::create(user, &database_connection).await.unwrap();
// We can now access the unique `_key` of the document
let document_key = user_record.key();
// The key can be used to retrieve documents, returning again a `DatabaseRecord<User>`
let found_user = User::find(document_key, &database_connection).await.unwrap();
// We can access the document data from the database record
assert_eq!(user.username, found_user.username);
}
  • key is the document primary identifier, certifying write action in the database collection
  • record is the document data, a generic containing your struct implementing the Record trait

Document operations

Documents can be:

  • created with DatabaseRecord::create
  • retrieved with YourRecord::find or DatabaseRecord::find (not recommended)
  • saved with DatabaseRecord::save
  • deleted with DatabaseRecord::delete

The DatabaseRecord structure wraps all ODM operations for any struct implementing Record

Complete Example:

use aragog::{Record, DatabaseConnection, DatabaseRecord, Validate, AuthMode};
use serde::{Serialize, Deserialize};
use tokio;

#[derive(Serialize, Deserialize, Clone, Record)]
pub struct User {
    pub username: String,
    pub first_name: String,
    pub last_name: String,
    pub age: usize
}

#[tokio::main]
async fn main() {
    // Database connection Setup
    let database_connection = DatabaseConnection::builder()
        .build()
        .await
        .unwrap();
    // Define a document
    let mut user = User {
        username: String::from("LeRevenant1234"),
        first_name: String::from("Robert"),
        last_name: String::from("Surcouf"),
        age: 18
    };
    // We create the user
    let mut user_record = DatabaseRecord::create(user, &database_connection).await.unwrap();
    // You can access and edit the document
    user_record.username = String::from("LeRevenant1524356");
    // And directly save it
    user_record.save(&database_connection).await.unwrap();
    // Or delete it
    user_record.delete(&database_connection).await.unwrap();
}

Operation options

All the write operations (create, save and delete) provide a variant _with_option:

  • create_with_options
  • save_with_options
  • delete_with_options

These methods allow to customize some aspects of the operation:

  • wait_for_sync: Should aragog wait for the operations to be written on disk? (by default the collection behavior is kept)
  • ignore_revs: Should ArangoDB ignore the revision conflict (true by default)
  • ignore_hooks: Should the operation skip the related Hooks ?

These options are available but you should use them sparingly. Prefer defining a global option settings directly in the DatabaseConnection if you find yourself in a situation where you want:

  • To always or never wait for sync
  • To always or never ignore the revision system
  • To always skip the hooks

Keep in mind that all write operations also have force_ variants which:

  • explicitly ignore the revision system
  • explicitly ignore the hooks

No matter what the global options are.

Hooks

The Record trait provides the following hooks:

  • before hooks:
    • before_create : executed before document creation (DatabaseRecord::create)
    • before_save : executed before document save (DatabaseRecord::save)
    • before_delete : executed before document deletion (DatabaseRecord::delete)
    • before_write : executed before both document creation and save.
    • before_all : executed before document creation, save and deletion.
  • after hooks:
    • after_create : executed after the document creation (DatabaseRecord::create)
    • after_save : executed after the document save (DatabaseRecord::save)
    • after_delete : executed after the document deletion (DatabaseRecord::delete)
    • after_write : executed after both document creation and save.
    • after_all : executed after both document creation, save and deletion.

You can register various methods in these hooks with the following syntax:


#![allow(unused_variables)]
fn main() {
#[before_create(func = "my_method")]
}

The hooked methods can follow various patterns using the following options:

  • is_async the method is async
  • db_access the method uses the db access

By default both options are set to false.

You can combine options to have an async hook with db access to execute document operations automatically. If you combine a lot of operations, like creating documents in hooks or chaining operations make sure to:

Hook Patterns

Simple hook with no options


#![allow(unused_variables)]
fn main() {
#[before_create(func = "my_method")]
}

my_method can be either:

  • 
    #![allow(unused_variables)]
    fn main() {
    fn my_method(&self) -> Result<(), aragog::Error>
    }
    
  • 
    #![allow(unused_variables)]
    fn main() {
    fn my_method(&mut self) -> Result<(), aragog::Error>
    }
    

Async hook


#![allow(unused_variables)]
fn main() {
#[before_create(func = "my_method", is_async = true)]
}

my_method can be either:

  • 
    #![allow(unused_variables)]
    fn main() {
    async fn my_method(&self) -> Result<(), aragog::Error>
    }
    
  • 
    #![allow(unused_variables)]
    fn main() {
    async fn my_method(&mut self) -> Result<(), aragog::Error>
    }
    

If you use aragog with the blocking feature then this option will have no effect.

Hook with database access


#![allow(unused_variables)]
fn main() {
#[before_create(func = "my_method", db_access = true)]
}

my_method can be either:

  • 
    #![allow(unused_variables)]
    fn main() {
    fn my_method<D>(&self, db_access: &D) -> Result<(), aragog::Error> where D: aragog::DatabaseAccess
    }
    
  • 
    #![allow(unused_variables)]
    fn main() {
    fn my_method<D>(&mut self, db_access: &D) -> Result<(), aragog::Error> where D: aragog::DatabaseAccess
    }
    

If you want to use the database access, using also is_async = true would be recommended

Technical notes

The Validate derive case

If you derive the Validate trait, you may want validations to be launched automatically in hooks.


#![allow(unused_variables)]
fn main() {
#[derive(Serialize, Deserialize, Clone, Record, Validate)]
#[before_write(func = "validate")] // This hook will launch validations before `create` and `save`
 pub struct User {
     pub username: String,
     pub first_name: String,
     pub last_name: String,
     pub age: usize
 }
}

Forbidden method names

When using a hook like the following:

#[before_create(func("my_method"))]

The called method names can't be:

  • before_create_hook
  • before_save_hook
  • before_delete_hook
  • after_create_hook
  • after_save_hook
  • after_delete_hook

to avoid unexpected behaviors like unwanted recursive hooks.

Try using explicit names for your hooks

Direct implementation

You can implement Record directly instead of deriving it.

We strongly suggest you derive the Record trait instead of implementing it, as in the future more hooks may be added to this trait without considering it breaking changes

You need to specify the COLLECTION_NAME const which, when deriving, takes the name of the structure. You also need to implement directly the different hooks.

Example:


#![allow(unused_variables)]
fn main() {
use aragog::Record;

#[derive(Serialize, Deserialize, Clone)]
pub struct User {
    pub username: String,
    pub first_name: String,
    pub last_name: String,
    pub age: usize
}

impl Record for User {
    const COLLECTION_NAME :&'static str = "User";

    async fn before_create_hook<D>(&mut self, db_accessor: &D) -> Result<(), Error> where
        D: DatabaseAccess + ?Sized {
        // Your implementation
        Ok(())
    }

    async fn before_save_hook<D>(&mut self, db_accessor: &D) -> Result<(), Error> where
        D: DatabaseAccess + ?Sized {
        // Your implementation
        Ok(())
    }
    
    async fn before_delete_hook<D>(&mut self, db_accessor: &D) -> Result<(), Error> where
        D: DatabaseAccess + ?Sized {
        // Your implementation
        Ok(())
    }
    
    async fn after_create_hook<D>(&mut self, db_accessor: &D) -> Result<(), Error> where
        D: DatabaseAccess + ?Sized {
        // Your implementation
        Ok(())
    }

    async fn after_save_hook<D>(&mut self, db_accessor: &D) -> Result<(), Error> where
        D: DatabaseAccess + ?Sized {
        // Your implementation
        Ok(())
    }

    async fn after_delete_hook<D>(&mut self, db_accessor: &D) -> Result<(), Error> where
        D: DatabaseAccess + ?Sized {
        // Your implementation
        Ok(())
    }
}
}

Unstable hooks state

The hook macros work pretty well but are difficult to test, especially compilation errors: Are the errors messages relevant? correctly spanned? etc.

So please report any bug or strange behaviour as this feature is still in its early stages.

The Validate trait

As aragog is trying to be a complete ODM, being able to add validations to models is a useful feature.

Macro validations

Let's take the record example model:


#![allow(unused_variables)]
fn main() {
use aragog::{Record, Validate};

#[derive(Serialize, Deserialize, Clone, Record, Validate)]
pub struct User {
    pub username: String,
    pub first_name: String,
    pub last_name: String,
    pub roles: Vec<String>,
    pub age: usize,
}
}

We added the Validate trait derive, making this model perform validations before being written to the database.

Field validations

We can add quite some pre-made validation operations as following:


#![allow(unused_variables)]
fn main() {
use aragog::{Record, Validate};

#[derive(Serialize, Deserialize, Clone, Record, Validate)]
pub struct User {
   // The *username* field must contain exactly 10 characters
   #[validate(length = 10)]
    pub username: String,
   // The *username* field must have at least 3 characters
   #[validate(min_length = 3)]
    pub first_name: String,
   // The *username* field must have its lenth between 3 and 30
   #[validate(min_length = 5, max_length = 30)]
    pub last_name: String,
   // Each role must have an exact length of 5
   #[validate_each(length = 5)]
   pub roles: Vec<String>,
   // The *age* field must be at least 18
   #[validate(greater_or_equal(18))]
    pub age: usize,
}
}

When trying to create or save a User document all validations must match or a Error::ValidationError will be returned with an explicit message.

The current available field attribute validation operation macros (can be chained):

  • String or string slice fields:

    • length(VAL) validates the field has exact length of VAL
    • min_length(VAL) validates the field has a minimum length of VAL
    • max_length(VAL) validates the field has a maximum length of VAL
    • regex(REGEX) validates the field matches the REGEX regular expression (which can be consts like Validate::SIMPLE_EMAIL_REGEX)
  • Numeric fields or ordered types

    • greater_than(VAL) validated the field is greater than VAL
    • greater_or_equal(VAL) validated the field is greater or equal to VAL
    • lesser_than(VAL) validated the field is lesser than VAL
    • lesser_or_equal(VAL) validated the field is lesser or equal to VAL
  • Vector fields or ordered iterable types

    • max_count(VAL) validates the field has a maximal number of elements of VAL
    • min_count(VAL) validates the field has a minimal number of elements of VAL
    • count(VAL) validates the field has a number of elements of VAL

    Note: These operations use the Iterator::count method which might be expensive

  • Other

    • is_some validates the Option field contains a value
    • is_none validates the Option field does not contain a value
    • func(FUNC) calls the FUNC method (see Extra Validations)
    • call_validations calls the validations of the field allowing to propagate the validation calls. The field must be an type implementing Validate

Note: The macro doesn't guarantee the order of validations

Validate comparison between custom types

The following validation operations:

  • greater_than(VAL)
  • greater_or_equal(VAL)
  • lesser_than(VAL)
  • lesser_or_equal(VAL)

Can allow custom types implementing PartialOrd

Note: It also requires Display to generate the error, and Copy for the borrow checker

So you can use a custom struct or enum like this:


#![allow(unused_variables)]
fn main() {
#[derive(Debug, Clone, Copy, PartialOrd, PartialEq)]
enum CustomOrd {
  A,
  B,
  C,
}

#[derive(Validate)]
struct Comparator {
  #[validate(lesser_than(CustomOrd::C), greater_than(CustomOrd::A))]
  pub field: CustomOrd,
}
}

Extra validations

On more complex cases, simple field validations are not enough and you may want to add custom validations.

use aragog::{Record, Validate};

#[derive(Serialize, Deserialize, Clone, Record, Validate)]
+ #[validate(func("custom_validations"))] // We added this global validation attribute on top of the struct
pub struct User {
+   #[validate(length = 10, func("validate_username"))] // We added this field validation attribute
    pub username: String,
    #[validate(min_length = 3)]
    pub first_name: String,
    #[validate(min_length = 5, max_length = 30)]
    pub last_name: String,
    #[validate(greater_or_equal(18))]
    pub age: usize,
+   // These two fields require a more complex validation
+   pub phone: Option<String>,
+   pub phone_country_code: Option<String>,
}

+ impl User {
+     // We added the global custom validation method (uses multi-fields)
+     fn custom_validations(&self, errors: &mut Vec<String>) {
+         if self.phone.is_some() || self.phone_country_code.is_some() {
+             // We use built-in validation methods
+             Self::validate_field_presence("phone", &self.phone, errors);
+             Self::validate_field_presence("phone_country_code", &self.phone_country_code, erros);
+         }
+     }
+     
+     // We added the field custom validation method (field-specific)
+     fn validate_username(field_name: &str, value: &str, errors: &mut Vec<String>) {
+         if value == "SUPERADMIN" {
+             // We can push our own validation errors
+             errors.push(format!("{} can't be SUPERADMIN", field_name))
+         }   
+     }
+ }

The macro attribute #[validate(func("METHOD"))]" must link to an existing method of your struct.

This method can follow various patterns:

  • global validation method (top of the struct)

#![allow(unused_variables)]
fn main() {
fn my_method(&self, errors: &mut Vec<String>) {}
}
  • field validation method

#![allow(unused_variables)]
fn main() {
fn my_method(field_name: &str, field_value: &T, errors: &mut Vec<String>) {}
}

T being your field type

Note: The method can have any visibility and can return whatever you want

the errors argument is a mutable array of error messages it contains all current errors and you can push your own errors in it. When the validate() method is called, this errors vector is used to build the error message.

Technical notes

Validate with Record

If you derive the Record trait, you may want validations to be launched automatically in record hooks.


#![allow(unused_variables)]
fn main() {
#[derive(Serialize, Deserialize, Clone, Record, Validate)]
#[before_write(func = "validate")] // This hook will launch validations before `create` and `save`
 pub struct User {
     pub username: String,
     pub first_name: String,
     pub last_name: String,
     pub age: usize
 }
}

Enum validations

Enums can derive Validate but field validation attributes are not supported.

Forbidden method name

When using a custom validation method like the following:

#[validate(func("my_method"))]

The called method names can't be validations to avoid unexpected behaviors like recursive validations.

This is caused by the Validate method validations already being built and called by the derive macro.

Try using explicit names for your custom validation methods

If your objective is to call the validations of a compound object implementing Validate use the call_validations operation.

Direct implementation

You can implement Validate directly instead of deriving it.

We suggest you derive the Validate trait instead of implementing it unless you need specific operation order

You need to specify the validations method which, when deriving is filled with all the macro attributes

Example:


#![allow(unused_variables)]
fn main() {
use aragog::Validate;

pub struct User {
    pub username: String,
    pub first_name: String,
    pub last_name: String,
    pub age: usize
}

impl Validate for User {
    fn validations(&self, errors: &mut Vec<String>) {
        // Your validations
    }
}
}

Unstable state

The validation macros work pretty well but are difficult to test, especially the compilation errors: Are the errors messages relevant? correctly spanned? etc.

So please report any bug or strange behaviour as this feature is still in its early stages

The EdgeRecord struct

This struct defines the OGM (Object Graph mapper) aspect of aragog, an Edge Model that can be mapped to an ArangoDB edge document.

An Edge document is part of an Edge Collection, and links regular Documents together

To use edges you need to define a Record for your edge collection:


#![allow(unused_variables)]
fn main() {
use aragog::Record;

#[derive(Serialize, Deserialize, Clone, Record)]
pub struct ChildOf {
    pub notes: Option<String>,
    pub adopted: bool
}
}

The ChildOf collection must be an edge collection

And now ChildOf can be used as a EdgeRecord<ChildOf>.

Linking documents

ArangoDB edge documents have two mandatory additional fields:

  • _from containing the id of the target document
  • _to containing the id of the target document

This fields are wrapped in the EdgeRecord struct, this is why you don't need to worry about specifyng it yourself

In order to link two Person:


#![allow(unused_variables)]
fn main() {
let parent = Person {
    first_name: String::from("Charles-Ange"),
    last_name: String::from("Surcouf"),
};
let parent_record= DatabaseRecord::create(parent, &db_connection).await.unwrap();
let child = Person {
    first_name: String::from("Robert"),
    last_name: String::from("Surcouf")
};
let child_record= DatabaseRecord::create(child, &db_connection).await.unwrap();
}

We can do this manually:


#![allow(unused_variables)]
fn main() {
let edge_document = EdgeRecord::new(parent_record.id(), child_record.id(), ChildOf {
    notes: None,
    adopted: false,
}).unwrap();
let edge_record = DatabaseRecord::create(edge_document, db_connection).await.unwrap();
}

or use the safer built in method:


#![allow(unused_variables)]
fn main() {
let edge_record = DatabaseRecord::link(&parent_record, &child_record, &db_connection,
    ChildOf {
        notes: None,
        adopted: false,
    }
}).await.unwrap();
}

In both cases we have edge_record of type DatabaseRecord<EdgeRecord<ChildOf>>.

Both DatabaseRecord and EdgeRecord implement Deref and DerefMut towards the inner type so you can access inner values:


#![allow(unused_variables)]
fn main() {
 edge_record.adopted = true;
}

Validation and hooks

EdgeRecord validates the format of its _from and _to fields and calls the hooks of the inner document.

Retrieval

If you wish to retrieve an edge document from its key or a query you need to specify the EdgeRecord wrapper to use the edge document features.

example:


#![allow(unused_variables)]
fn main() {
// These will work but you won't have the `from` and `to` value
let edge = ChildOf::find("key", &db_access).await.unwrap();
let edge: DatabaseRecord<ChildOf> = DatabaseRecord::find("key", &db_access).await.unwrap();
// These will work and retrieve also the `from`and `to` values
let edge = EdgeRecord::<ChildOf>::find("key", &db_access).await.unwrap();
let edge: DatabaseRecord<EdgeRecord<ChildOf>> = DatabaseRecord::find("key", &db_access).await.unwrap();
}

The Query system

You can retrieve document from the database two ways:

  • from the unique ArangoDB _key (see the record section)
  • from an AQL query

aragog provides an AQL query builder system, allowing safer queries than direct string literals.

For a created object like the following:


#![allow(unused_variables)]
fn main() {
#[derive(Serialize, Deserialize, Record, Clone)]
struct User {
   first_name: String,
   last_name: String,
   age: u16,
}

let user = User {
   first_name: "Robert".to_string(),
   last_name: "Surcouf".to_string(),
   age: 25,
};
DatabaseRecord::create(user, &database_connection).await.unwrap();
}

You can define a query:


#![allow(unused_variables)]
fn main() {
let query = User::query()
    .filter(Filter::new(
        Comparison::field("last_name").equals_str("Surcouf"))
        .and(Comparison::field("age").greater_than(15))
    );
}

Typed querying

Typed querying will allow only one type of document to be retrieved, in this case User collection documents.

In case of corrupted documents they may not be returned, see safe querying for resilient querying

  • Through Record::get:

#![allow(unused_variables)]
fn main() {
 let result = User::get(&query, &database_connection).await.unwrap();
}
  • Through DatabaseRecord::get (requires type):

#![allow(unused_variables)]
fn main() {
 let result :QueryResult<User> = DatabaseRecord::get(&query, &database_connection).await.unwrap();
}
  • Through Query::call (requires type):

#![allow(unused_variables)]
fn main() {
 let result :QueryResult<User> = query.call(&database_connection).await.unwrap()
}

Safe querying

safe querying will allow multiple types of document to be retrieved as json objects (UndefinedRecord) and then dynamically parsed.

This version may be slightly slower, but you have a guarantee to retrieve all documents

  • Through Query::raw_call:

#![allow(unused_variables)]
fn main() {
 let result = query.raw_call(&database_connection).await.unwrap()
}
  • Through DatabaseAccess::query (requires type):

#![allow(unused_variables)]
fn main() {
 let result = database_connection.query(query).await.unwrap();
}

The QueryResult<UndefinedRecord> provides a get_records method to dynamically retrieve custom Record types.

Batch calls

Each and every query variant shown above have a batched version:

  • Record::get => Record::get_in_batches
  • DatabaseRecord::get => DatabaseRecord::get_in_batches
  • Query::call => Query::call_in_batches
  • Query::raw_call => Query::raw_call_in_batches
  • DatabaseAccess::query => DatabaseAccess::query_in_batches

They will return a QueryCursor instead of a QueryResult allowing to customize the number of returned document and easy iteration through the returned batches.

If you use the blocking feature, QueryCursor has an Iterator implementation. Otherwise use the next_batch method

Query Object

You can initialize a query in the following ways:

  • The recommended way:
    • Object::query() (only works if Object implements Record)
  • Unsafe ways:
    • Query::new("CollectionName")
    • query!("CollectionName")

You can customize the query with the following methods:

  • filter() you can specify AQL comparisons
  • prune() you can specify blocking AQL comparisons for traversal queries
  • sort() you can specify fields to sort with
  • limit() you can skip and limit the query results
  • distinct() you can skip duplicate documents

The order of operations will be respected in the rendered AQL query (except for distinct)

Then you can call a query in the following ways:

  • query.call::<Object>(&database_connection)
  • Object::get(&query, &database_connection

Which will return a JsonQueryResult containing a Vec of serde_json::Value. JsonQueryResult can return deserialized models as DatabaseRecord by calling .get_records::<T>()

Filter

You can initialize a Filter with Filter::new(comparison)

Each comparison is a Comparison struct built via ComparisonBuilder:


#![allow(unused_variables)]
fn main() {
// for a simple field comparison

// Explicit
Comparison::field("some_field").some_comparison("compared_value");
// Macro
compare!(field "some_field").some_comparison("compared_value");

// for field arrays (see ArangoDB operators)

// Explicit
Comparison::all("some_field_array").some_comparison("compared_value");
// Macro
compare!(all "some_field_array").some_comparison("compared_value");

// Explicit
Comparison::any("some_field_array").some_comparison("compared_value");
// Macro
compare!(any "some_field_array").some_comparison("compared_value");

// Explicit
Comparison::none("some_field_array").some_comparison("compared_value");
// Macro
compare!(none "some_field_array").some_comparison("compared_value");
}

All the currently implemented comparison methods are listed under ComparisonBuilder documentation page.

Filters can be defined explicitly like this:


#![allow(unused_variables)]
fn main() {
let filter = Filter::new(Comparison::field("name").equals_str("felix"));
}

or


#![allow(unused_variables)]
fn main() {
use aragog::query::{Filter, Comparison};
let filter :Filter = Comparison::field("name").equals_str("felix").into();
}

Example


#![allow(unused_variables)]
fn main() {
    let query = Query::new("Company").filter(
        Filter::new(
        Comparison::field("company_name").not_like("%google%"))
            .and(Comparison::field("company_age").greater_than(15))
            .or(Comparison::any("emails").like("%gmail.com"))
            .and(Comparison::field("roles").in_str_array(&["SHIPPER", "FORWARDER"]))
    );
}

Traversal Querying

You can use graph features with sub-queries with different ways:

Straightforward Traversal query

  • Explicit way

#![allow(unused_variables)]
fn main() {
let query = Query::outbound(1, 2, "edgeCollection", "User/123");
let query = Query::inbound(1, 2, "edgeCollection", "User/123");
let query = Query::any(1, 2, "edgeCollection", "User/123");
// Named graph
let query = Query::outbound_graph(1, 2, "NamedGraph", "User/123");
let query = Query::inbound_graph(1, 2, "NamedGraph", "User/123");
let query = Query::any_graph(1, 2, "NamedGraph", "User/123");
}
  • Implicit way from a DatabaseRecord<T>

#![allow(unused_variables)]
fn main() {
let query = user_record.outbound_query(1, 2, "edgeCollection");
let query = user_record.inbound_query(1, 2, "edgeCollection");
// Named graph
let query = user_record.outbound_graph(1, 2, "NamedGraph");
let query = user_record.inbound_graph(1, 2, "NamedGraph");
}

Sub queries

Queries can be joined together through

  • Edge traversal:

#![allow(unused_variables)]
fn main() {
let query = Query::new("User")
    .join_inbound(1, 2, false, Query::new("edgeCollection"));
}
  • Named Graph traversal:

#![allow(unused_variables)]
fn main() {
let query = Query::new("User")
    .join_inbound(1, 2, true, Query::new("SomeGraph"));
}

It works with complex queries:


#![allow(unused_variables)]
fn main() {
let query = Query::new("User")
    .filter(Comparison::field("age").greater_than(10).into())
    .join_inbound(1, 2, false,
        Query::new("edgeCollection")
            .sort("_key", None)
            .join_outbound(1, 5, true,
                Query::new("SomeGraph")
                    .filter(Comparison::any("roles").like("%Manager%").into())
                    .distinct()
                )
    );
}

Transactions

aragog supports transactional operations without using a specific API thanks to the new Transaction Object.

Creating a transaction

To initiate a transaction we need to use the DatabaseConnection, as the transaction will create an equivalent DatabaseAccess object: The TransactionDatabaseConnection, that can be used instead of the classic connection to use the transactional features.

Example:


#![allow(unused_variables)]
fn main() {
// We build the connection
let database_connection = DatabaseConnection::builder()
         .build().await.unwrap();
// We instantiate a new transaction
let transaction = Transaction::new(&database_connection).await.unwrap();
}

Transaction states

An ArangoDB transaction has three states:

  • Running
  • Committed
  • Aborted

After successfully initializing a Transaction Object, a Running transaction is created. We can now use its connection:

Example:


#![allow(unused_variables)]
fn main() {
let database_connection = DatabaseConnection::builder()
         .build().await.unwrap();
// Instantiate a new transaction
let transaction = Transaction::new(&database_connection).await.unwrap();
// Retrieve the connection
let transaction_connection = transaction.database_connection();
// We use the transaction connection instead of the classic connection
DatabaseRecord::create(
    Dish {
        name: "Pizza".to_string(),
        price: 10,
    },
    transaction_connection
).await.unwrap();
// We commit the transaction
transaction.commit().await.unwrap();
}

The create operations is using the transaction, meaning it won't be written in ArangoDB until the transaction is committed. The operation will simply be cancelled if the transaction is aborted.

Make sure to always commit or abort a transaction !

Safe execution

To avoid remembering to commit and manually handling when to abort a transaction, prefer using the safe execution.

The safe execution allows to execute multiple operations in a block and make sure the transaction is either committed or aborted.


#![allow(unused_variables)]
fn main() {
let database_connection = DatabaseConnection::builder()
.build().await.unwrap();
// Instantiate a new transaction
let transaction = Transaction::new(&database_connection).await.unwrap();
// Safely execute operations:
let output = transaction.safe_execute(|transaction_connection| async move {
   // We use the provided `transaction_connection` instead of the classic connection
   DatabaseRecord::create(Dish {
       name: "Pizza".to_string(),
       price: 10,
   }, &transaction_connection).await?;
   DatabaseRecord::create(Dish {
       name: "Pasta".to_string(),
       price: 8,
   }, &transaction_connection).await?;
   DatabaseRecord::create(Dish {
       name: "Sandwich".to_string(),
       price: 5,
   }, &transaction_connection).await?;
   // You can return any type of data here
   Ok(())
}).await.unwrap();
// The output allows to check the transaction state: Committed or Aborted
assert!(output.is_committed());
}

If an operation fails in the safe_execute block the transaction will be aborted and every operation cancelled.

Don't use unwrap() or any panicking functions in the block as the transaction won't be aborted.

The safe_execute method returns a TransactionOutput if everything went correctly (No Database or connection errors). This output allows to check the state of the transaction, Aborted or Committed and retrieve the result of the block stored as a generic.

Note: Transactions can be committed multiple times, so feel free to use multiple safe execution blocks.

Warning: An aborted transaction can no longer be committed ! Make sure to handle the TransactionOuput cases.

Custom transactions

The Transaction object implements a builder pattern through TransactionBuilder

Restricted transactions

The Transaction::new pattern build a valid transaction for all collections (defined in the schema). You may want more restricted transactions, limited to a single Collection.

All structs deriving Record, here User, have access to:

  • User::transaction building a transaction on this collection only.
  • User::transaction_builder returning a builder for a transaction on this collection only.

Restricted transaction will fail if you try to Write on another collection.

Transaction options

The TransactionBuilder allows optional parameters:

  • wait_for_sync, forcing the transaction to be synced to the database on commit.
  • lock_timeout, specifying the transaction lock timeout (set to 60000 by default)
  • collections, specifying the allowed collections (all collections are allowed by default)

Technical notes

The transactions use the ArangoDB steam transaction API.