#data-transfer #neo4j #cypher #dto #graph #query #graph-node

cypher-dto

A collection of traits and macros for working Data Transfer Objects (DTOs) Cypher and Neo4j

5 unstable releases

0.3.1 Mar 24, 2024
0.3.0 Mar 1, 2024
0.2.0 Aug 12, 2023
0.1.1 Aug 7, 2023
0.1.0 Aug 6, 2023

#629 in Database interfaces

MIT license

64KB
1K SLoC

cypher-dto

Crates.io Github.com Docs.rs License

A collection of traits and macros for working with Data Transfer Objects (DTOs) in Neo4j.

Brief Overview

use cypher_dto::{Node, Relation};

#[derive(Node)]
struct Person {
  name: String
}

#[derive(Relation)]
struct Knows {
  since: u16
}

let alice = Person::new("Alice");
let bob = Person::new("Bob");
let knows = Knows::new(2020);

let graph = neo4rs::Graph::new(/*...*/);


let query: neo4rs::Query = alice.create();
graph.execute(query);

let alice = alice.into_builder().name("Allison").build();
let query = alice.update();
graph.execute(query);

let query = knows.create(RelationBound::Match(&alice), RelationBound::Create(&bob));
graph.execute(query);

Examples

Basic usage

use cypher_dto::Node;
use neo4rs::Query;

#[derive(Node)]
struct Person {
    id: String,     // Inferred to be the only #[id] field.
    name: String,
    #[name = "zip_code"]
    zip: String,
}
assert_eq!(Person::typename(), "Person");
assert_eq!(Person::field_names(), &["id", "name", "zip_code"]);

// For building parameterized cypher queries...
assert_eq!(Person::as_query_fields(), "id: $id, name: $name, zip_code: $zip_code");
assert_eq!(Person::as_query_obj(), "Person { id: $id, name: $name, zip_code: $zip_code }");

let person = Person::new("123", "John", "12345");

// Unitary CRUD operations are provided for convenience.
let query = person.create();

// Equivalent to:
let mut query = Query::new(format!(
    "CREATE (n:{})",
    Person::as_query_obj()
));
query = person.add_values_to_params(query, None, StampMode::Create);
#[derive(Clone, Debug, PartialEq, Relation)]
struct Knows;
assert_eq!(Knows::typename(), "KNOWS");

Multi valued identifiers

use cypher_dto::Node;
use neo4rs::Query;

#[derive(Node)]
struct Company {
  #[id]
  name: String,
  #[id]
  state: String,
  phone: String,
}
let company = Company::new("Acme", "CA", "555-555-5555");
let id = company.identifier();
assert_eq!(id.name(), "Acme");
assert_eq!(id.state(), "CA");
assert_eq!(id, CompanyId::new("Acme", "CA"));

assert_eq!(CompanyId::typename(), "Company");
assert_eq!(CompanyId::field_names(), &["name", "state"]);

let query = id.read();
// Equivalent to:
let mut query = Query::new(format!(
    "MATCH (n:{}) RETURN n",
    CompanyId::as_query_obj()
));
query = id.add_values_to_params(query, None, StampMode::Read);

Builder, new, and getters

  • The generated ::new() method will accept &str for String fields, and &[T] for Vec<T> fields.

  • Doc comments are copied to the getters for the struct, the getter(s) on the FooId struct, and the methods on the FooBuilder struct.

#[derive(Node)]
struct Person {
  /// This comment is copied to the getter, the Id getter, and the builder method.
  name: String,
}
let p = Person::new("John");
let p = p.into_builder().name("Ferris").build();
assert_eq!(p.name(), "Ferris");

Timestamps

There's built-in support for special timestamp fields: created_at and updated_at, created and updated, or any single one of those four.

use cypher_dto::timestamps;

#[timestamps]
struct Person {
  name: String,
}
// Adds two fields:
//   created_at: Option<DateTime<Utc>>,
//   updated_at: Option<DateTime<Utc>>,

#[timestamps = "short"]
struct Person {
  name: String,
}
// Adds two fields:
//   created: Option<DateTime<Utc>>,
//   updated: Option<DateTime<Utc>>,

#[timestamps = "updated_at"]
struct Person {
  name: String,
}
// Adds one field:
//   updated_at: Option<DateTime<Utc>>,

The timestamp fields are treated a little bit differently than other fields:

  • They are not parameters in the generated ::new() method.
  • They sometimes have hardcoded values in ::to_query_fields().
    • Calling to_query_fields() with StampMode::Create will use datetime() in the query instead of $created_at for example.

Option<DateTime<Utc>> is used instead of DateTime<Utc> so that the fields can be None when creating a new instance, before it exists in the database.

For more details about the macro variations, see the cypher-dto-macros crate.

Unitary CRUD operations

This library takes the point of view that non-trivial queries should be managed by hand, but it does provide basic CRUD operations for convenience.

#[derive(Node)] and #[derive(Relation)] structs get create() and update() methods, while the corresponding FooId structs get read() and delete() methods, all of which return a neo4rs::Query.

None of those methods even take any arguments, with the exception of creating a relation, which needs to know if the start and end nodes it's between need created or already exist.

use cypher_dto::{Node, Relation};

#[derive(Node)]
Person {
  name: String,
}

#[derive(Clone, Debug, PartialEq, Relation)]
struct Knows;

let alice = Person::new("Alice");
let bob = Person::new("Bob");
let knows = Knows; // Relations can have fields and ids too.

let query = knows.create(RelationBound::Create(&alice), RelationBound::Create(&bob));

Node labels

While neo4rs::Node::labels() can be used to read the current labels of a node from the database, this library provides a way to define the labels a struct should use. Those labels are then used by the built in CRUD operations.

#[derive(Node)]
#[labels("Person", "Employee")]
Person {
  name: String,
}

let person = Person::new("Alice");
assert_eq!(person.labels(), &["Person", "Employee"]);

Dependencies

~15–26MB
~456K SLoC