├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE ├── README.md ├── Todo.md ├── compile-protobufs.rs ├── docker-compose-1-1.yaml ├── docker-compose-1-2.yaml ├── docker-compose-20-03.yaml ├── examples ├── chrono.rs ├── geo_geojson.rs ├── geo_simple.rs ├── simple.rs └── tls │ ├── README.md │ └── main.rs ├── src ├── client.rs ├── errors.rs ├── lib.rs ├── protos │ ├── api.proto │ ├── api.rs │ ├── api_grpc.rs │ └── mod.rs └── txn.rs └── tests ├── client.rs ├── common └── mod.rs └── txn.rs /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /target/ 3 | **/*.rs.bk 4 | Cargo.lock 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | services: 3 | - docker 4 | 5 | matrix: 6 | allow_failures: 7 | - rust: nightly 8 | fast_finish: true 9 | include: 10 | - name: clippy & fmt 11 | rust: stable 12 | before_install: 13 | - rustup component add rustfmt clippy 14 | install: 15 | - wget https://dl.google.com/go/go1.14.1.linux-amd64.tar.gz; 16 | tar -xf go1.14.1.linux-amd64.tar.gz -C $HOME; 17 | export PATH="$HOME/go/bin:$PATH"; 18 | export GOROOT=$HOME/go; 19 | script: 20 | - cargo clippy --all-features -- -D warnings 21 | - cargo fmt -- --check 22 | - name: Dgraph 1.1 tests 23 | rust: stable 24 | before_install: 25 | - docker-compose -f docker-compose-1-1.yaml up -d 26 | - docker ps 27 | install: 28 | - wget https://dl.google.com/go/go1.14.1.linux-amd64.tar.gz; 29 | tar -xf go1.14.1.linux-amd64.tar.gz -C $HOME; 30 | export PATH="$HOME/go/bin:$PATH"; 31 | export GOROOT=$HOME/go; 32 | script: 33 | - cargo test --verbose -- --test-threads=1 34 | - name: Dgraph 1.2 tests 35 | rust: stable 36 | before_install: 37 | - docker-compose -f docker-compose-1-2.yaml up -d 38 | - docker ps 39 | install: 40 | - wget https://dl.google.com/go/go1.14.1.linux-amd64.tar.gz; 41 | tar -xf go1.14.1.linux-amd64.tar.gz -C $HOME; 42 | export PATH="$HOME/go/bin:$PATH"; 43 | export GOROOT=$HOME/go; 44 | script: 45 | - cargo test --verbose -- --test-threads=1 46 | - name: Dgraph 20.03 tests 47 | rust: stable 48 | before_install: 49 | - docker-compose -f docker-compose-20-03.yaml up -d 50 | - docker ps 51 | install: 52 | - wget https://dl.google.com/go/go1.14.1.linux-amd64.tar.gz; 53 | tar -xf go1.14.1.linux-amd64.tar.gz -C $HOME; 54 | export PATH="$HOME/go/bin:$PATH"; 55 | export GOROOT=$HOME/go; 56 | script: 57 | - cargo test --verbose -- --test-threads=1 58 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dgraph" 3 | version = "0.4.0" 4 | authors = ["Swoorup Joshi"] 5 | description = "A rust client for Dgraph database" 6 | readme = "README.md" 7 | license = "MIT" 8 | keywords = ["dgraph", "grpc"] 9 | repository = "https://github.com/Swoorup/dgraph-rs" 10 | homepage = "https://github.com/Swoorup/dgraph-rs" 11 | edition = "2018" 12 | 13 | [dependencies] 14 | grpcio = "0.6.0" 15 | futures = "0.3.5" 16 | protobuf = { version = "2.16.2", features = ["with-serde"] } 17 | protobuf-codegen = "2.16.2" 18 | rand = "0.7.2" 19 | serde = "1.0.114" 20 | serde_derive = "1.0.114" 21 | serde_json = "1.0.56" 22 | protoc-grpcio = { version = "2.0.0", optional = true } 23 | log = "0.4.11" 24 | 25 | [dev-dependencies] 26 | chrono = { version = "0.4.13", features = ["serde"] } 27 | geojson = "0.19.0" 28 | 29 | [features] 30 | default = ["with-serde"] 31 | with-serde = [] 32 | compile-protobufs = ["protoc-grpcio"] 33 | openssl = ["grpcio/openssl"] 34 | openssl-vendored = ["grpcio/openssl-vendored"] 35 | 36 | [[bin]] 37 | doc = false 38 | name = "protoc" 39 | path = "compile-protobufs.rs" 40 | required-features = ["compile-protobufs"] 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Swoorup Joshi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A rust client for dgraph 2 | 3 | [![crates.io version](https://img.shields.io/crates/v/dgraph.svg)](https://crates.io/crates/dgraph) 4 | [![Build Status](https://travis-ci.org/Swoorup/dgraph-rs.svg?branch=master)](https://travis-ci.org/Swoorup/dgraph-rs) 5 | 6 | Dgraph Rust client which communicates with the server using 7 | [gRPC](https://grpc.io/). 8 | 9 | Before using this client, it is highly recommended to go through 10 | [tour.dgraph.io] and [docs.dgraph.io] to understand how to run and work 11 | with Dgraph. 12 | 13 | [docs.dgraph.io]: https://docs.dgraph.io 14 | [tour.dgraph.io]: https://tour.dgraph.io 15 | 16 | ## Table of contents 17 | 18 | - [Installation](#install) 19 | - [Using a client](#using-a-client) 20 | - [Create a client](#create-a-client) 21 | - [Alter the database](#alter-the-database) 22 | - [Create a transaction](#create-a-transaction) 23 | - [Run a mutation](#run-a-mutation) 24 | - [Run a query](#run-a-query) 25 | - [Commit a transaction](#commit-a-transaction) 26 | - [Integration tests](#integration-tests) 27 | - [Contributing](#contributing) 28 | 29 | ## Prerequisites 30 | 31 | `dgraph` supports only Dgraph versions 1.1 or higher. 32 | 33 | Since this crate uses `grpcio`, which is a wrapper around C++ library, there are 34 | certain prerequisites needed before it can be installed. You can find them in 35 | [`grpcio` documentation](https://github.com/pingcap/grpc-rs#prerequisites). 36 | 37 | ## Installation 38 | 39 | `dgraph` is available on crates.io. Add the following dependency to your 40 | `Cargo.toml`. 41 | 42 | ```toml 43 | [dependencies] 44 | dgraph = "0.4.0" 45 | ``` 46 | 47 | There are also a couple of passthrough `grpcio` features available: 48 | 49 | - `openssl` 50 | - `openssl-vendored` 51 | 52 | Those are described in [`grpcio` documentation](https://github.com/tikv/grpc-rs#feature-openssl-and-openssl-vendored). 53 | 54 | ## Using a client 55 | 56 | ### Create a client 57 | 58 | `Dgraph` object can be initialised by passing it a list of `dgraph::DgraphClient` 59 | clients as a vector. Connecting to multiple Dgraph servers in the same 60 | cluster allows for better distribution of workload. The library provides 61 | a macro to do so. 62 | 63 | The following code snippet shows just one connection. 64 | 65 | ```rust 66 | let dgraph = make_dgraph!(dgraph::new_dgraph_client("localhost:9080")); 67 | ``` 68 | 69 | Alternatively, secure client can be used: 70 | 71 | ```rust 72 | fn open_cert_file(path: &str) -> Vec { 73 | ... 74 | } 75 | 76 | let root_ca = open_cert_file("./tls/ca.crt"); 77 | let cert = open_cert_file("./tls/client.user.crt"); 78 | let private_key = open_cert_file("./tls/client.user.key"); 79 | 80 | let dgraph = make_dgraph!(dgraph::new_secure_dgraph_client( 81 | "localhost:9080", 82 | root_ca, 83 | cert, 84 | private_key 85 | )); 86 | ``` 87 | 88 | ### Alter the database 89 | 90 | To set the schema, create an instance of `dgraph::Operation` and use the 91 | `Alter` endpoint. 92 | 93 | ```rust 94 | let op = dgraph::Operation{ 95 | schema: "name: string @index(exact) .".to_string(), ..Default::default() 96 | }; 97 | let result = dgraph.alter(&op); 98 | // Check error 99 | ``` 100 | 101 | `Operation` contains other fields as well, including `DropAttr` and `DropAll`. 102 | `DropAll` is useful if you wish to discard all the data, and start from a clean 103 | slate, without bringing the instance down. `DropAttr` is used to drop all the data 104 | related to a predicate. 105 | 106 | ### Create a transaction 107 | 108 | To create a transaction, call `dgraph.new_txn()`, which returns a `dgraph::Txn` object. This 109 | operation incurs no network overhead. 110 | 111 | Once `dgraph::Txn` goes out of scope, `txn.discard()` is automatically called via the `Drop` trait. 112 | Calling `txn.discard()` after `txn.commit()` is a no-op and calling this multiple 113 | times has no additional side-effects. 114 | 115 | ```rust 116 | let txn = dgraph.new_txn(); 117 | ``` 118 | 119 | ### Run a mutation 120 | 121 | `txn.mutate(mu)` runs a mutation. It takes in a `dgraph::Mutation` 122 | object. You can set the data using JSON or RDF N-Quad format. 123 | 124 | We define a Person struct to represent a Person and marshal an instance of it to use with `Mutation` 125 | object. 126 | 127 | ```rust 128 | #[derive(Serialize, Deserialize, Default, Debug)] 129 | struct Person { 130 | uid: String, 131 | name: String, 132 | } 133 | 134 | let p = Person { 135 | uid: "_:alice".to_string(), 136 | Name: "Alice".to_string(), 137 | } 138 | 139 | let pb = serde_json::to_vec(&p).expect("Invalid json"); 140 | 141 | let mut mu = dgraph::Mutation { 142 | json: pb, ..Default::default() 143 | }; 144 | 145 | let assigned = txn.mutate(mu).expect("failed to create data"); 146 | ``` 147 | 148 | For a more complete example, see the simple example [simple](https://github.com/Swoorup/dgraph-rs/blob/master/examples/simple/main.rs) (or [the same example with secure client](https://github.com/Swoorup/dgraph-rs/blob/master/examples/tls/main.rs)). 149 | 150 | Sometimes, you only want to commit a mutation, without querying anything further. 151 | In such cases, you can use `mu.commit_now = true` to indicate that the 152 | mutation must be immediately committed. 153 | 154 | ### Run a query 155 | 156 | You can run a query by calling `txn.query(q)`. You will need to pass in a GraphQL+- query string. If 157 | you want to pass an additional map of any variables that you might want to set in the query, call 158 | `txn.query_with_vars(q, vars)` with the variables map as third argument. 159 | 160 | Let's run the following query with a variable \$a: 161 | 162 | ```rust 163 | let q = r#"query all($a: string) { 164 | all(func: eq(name, $a)) { 165 | name 166 | } 167 | }"#; 168 | 169 | let mut vars = HashMap::new(); 170 | vars.insert("$a".to_string(), "Alice".to_string()); 171 | 172 | let resp = dgraph.new_readonly_txn().query_with_vars(&q, vars).expect("query"); 173 | let root: Root = serde_json::from_slice(&resp.json).expect("parsing"); 174 | println!("Root: {:#?}", root); 175 | ``` 176 | 177 | When running a schema query, the schema response is found in the `Schema` field of `dgraph::Response`. 178 | 179 | ```rust 180 | let q = r#"schema(pred: [name]) { 181 | type 182 | index 183 | reverse 184 | tokenizer 185 | list 186 | count 187 | upsert 188 | lang 189 | }"#; 190 | 191 | let resp = txn.query(&q)?; 192 | println!("{:#?}", resp.schema); 193 | ``` 194 | 195 | ### Commit a transaction 196 | 197 | A transaction can be committed using the `txn.commit()` method. If your transaction 198 | consisted solely of calls to `txn.query` or `txn.query_with_vars`, and no calls to 199 | `txn.mutate`, then calling `txn.commit` is not necessary. 200 | 201 | An error will be returned if other transactions running concurrently modify the same 202 | data that was modified in this transaction. It is up to the user to retry 203 | transactions when they fail. 204 | 205 | ```rust 206 | let txn = dgraph.new_txn(); 207 | // Perform some queries and mutations. 208 | 209 | let res = txn.commit(); 210 | if res.is_err() { 211 | // Retry or handle error 212 | } 213 | ``` 214 | 215 | ## Integration tests 216 | 217 | Tests require Dgraph running on `localhost:19080`. For the convenience there 218 | are a couple of `docker-compose-*.yaml` files - depending on Dgraph you are 219 | testing against - prepared in the root directory: 220 | 221 | ```bash 222 | docker-compose -f docker-compose-*.yaml up -d 223 | ``` 224 | 225 | Since we are working with database, tests also need to be run in a single 226 | thread to prevent aborts. Eg.: 227 | 228 | ```bash 229 | cargo test -- --test-threads=1 230 | ``` 231 | 232 | ## Contributing 233 | 234 | Contributions are welcome. Feel free to raise an issue, for feature requests, bug fixes and improvements. 235 | 236 | If you have made changes to one of `src/protos/api.proto` files, you need to 237 | regenerate the source files generated by Protocol Buffer tools. To do that, 238 | install the [Protocol Buffer Compiler](https://github.com/protocolbuffers/protobuf#readme) 239 | and then run the following command: 240 | 241 | ```bash 242 | cargo run --features="compile-protobufs" --bin protoc 243 | ``` 244 | 245 | ## Release checklist 246 | 247 | These have to be done with every version we support: 248 | 249 | - Run tests 250 | - Try examples 251 | 252 | Update the version and publish crate: 253 | 254 | - Update tag in Cargo.toml 255 | - Update tag in README.md 256 | - `git tag v0.X.X` 257 | - `git push origin v0.X.X` 258 | - Write release log on GitHub 259 | - `cargo publish` 260 | -------------------------------------------------------------------------------- /Todo.md: -------------------------------------------------------------------------------- 1 | # Todos 2 | 3 | - [ ] Add integration tests and add related docs 4 | - [ ] Fix jwt implementation and add related docs 5 | - [ ] Adding pooling? Is it even required? 6 | - [ ] Polish docs 7 | - [x] Add drop trait to Txn to discard transaction 8 | - [ ] Custom Errors with failure crate. 9 | - [ ] Use Cow or interned strings? 10 | - [ ] Use query builder for type safety? 11 | -------------------------------------------------------------------------------- /compile-protobufs.rs: -------------------------------------------------------------------------------- 1 | extern crate protoc_grpcio; 2 | 3 | fn main() { 4 | let proto_root = "src/protos/dgraph11"; 5 | println!("cargo:rerun-if-changed={}", proto_root); 6 | 7 | protoc_grpcio::compile_grpc_protos( 8 | &["api.proto"], 9 | &[proto_root], 10 | &proto_root, 11 | Some(protobuf_codegen::Customize { 12 | serde_derive: Some(true), 13 | ..Default::default() 14 | }), 15 | ) 16 | .expect("Failed to compile gRPC definitions!"); 17 | } 18 | -------------------------------------------------------------------------------- /docker-compose-1-1.yaml: -------------------------------------------------------------------------------- 1 | version: "3.2" 2 | services: 3 | zero: 4 | image: dgraph/dgraph:v1.1.1 5 | volumes: 6 | - type: volume 7 | source: dgraph 8 | target: /dgraph-1-1 9 | volume: 10 | nocopy: true 11 | ports: 12 | - 15080:5080 13 | - 16080:6080 14 | restart: on-failure 15 | command: dgraph zero --my=zero:5080 16 | server: 17 | image: dgraph/dgraph:v1.1.1 18 | volumes: 19 | - type: volume 20 | source: dgraph 21 | target: /dgraph-1-1 22 | volume: 23 | nocopy: true 24 | ports: 25 | - 18080:8080 26 | - 19080:9080 27 | restart: on-failure 28 | command: dgraph alpha --my=server:7080 --lru_mb=2048 --zero=zero:5080 29 | 30 | volumes: 31 | dgraph: 32 | -------------------------------------------------------------------------------- /docker-compose-1-2.yaml: -------------------------------------------------------------------------------- 1 | version: "3.2" 2 | services: 3 | zero: 4 | image: dgraph/dgraph:v1.2.5 5 | volumes: 6 | - type: volume 7 | source: dgraph 8 | target: /dgraph-1-2 9 | volume: 10 | nocopy: true 11 | ports: 12 | - 15080:5080 13 | - 16080:6080 14 | restart: on-failure 15 | command: dgraph zero --my=zero:5080 16 | server: 17 | image: dgraph/dgraph:v1.2.5 18 | volumes: 19 | - type: volume 20 | source: dgraph 21 | target: /dgraph-1-2 22 | volume: 23 | nocopy: true 24 | ports: 25 | - 18080:8080 26 | - 19080:9080 27 | restart: on-failure 28 | command: dgraph alpha --my=server:7080 --lru_mb=2048 --zero=zero:5080 29 | 30 | volumes: 31 | dgraph: 32 | -------------------------------------------------------------------------------- /docker-compose-20-03.yaml: -------------------------------------------------------------------------------- 1 | version: "3.2" 2 | services: 3 | zero: 4 | image: dgraph/dgraph:v20.03.3 5 | volumes: 6 | - type: volume 7 | source: dgraph 8 | target: /dgraph-20-03 9 | volume: 10 | nocopy: true 11 | ports: 12 | - 15080:5080 13 | - 16080:6080 14 | restart: on-failure 15 | command: dgraph zero --my=zero:5080 16 | server: 17 | image: dgraph/dgraph:v20.03.3 18 | volumes: 19 | - type: volume 20 | source: dgraph 21 | target: /dgraph-20-03 22 | volume: 23 | nocopy: true 24 | ports: 25 | - 18080:8080 26 | - 19080:9080 27 | restart: on-failure 28 | command: dgraph alpha --my=server:7080 --lru_mb=2048 --zero=zero:5080 29 | 30 | volumes: 31 | dgraph: 32 | -------------------------------------------------------------------------------- /examples/chrono.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use dgraph::{make_dgraph, Dgraph}; 4 | use serde_derive::{Deserialize, Serialize}; 5 | 6 | fn drop_schema(dgraph: &Dgraph) { 7 | let op_drop = dgraph::Operation { 8 | drop_all: true, 9 | ..Default::default() 10 | }; 11 | 12 | dgraph.alter(&op_drop).expect("Failed to drop schema."); 13 | 14 | println!("Dropped the schema."); 15 | } 16 | 17 | fn set_schema(dgraph: &Dgraph) { 18 | let op_schema = dgraph::Operation { 19 | schema: r#" 20 | name: string @index(exact) . 21 | last_seen: datetime . 22 | "# 23 | .to_string(), 24 | ..Default::default() 25 | }; 26 | 27 | dgraph.alter(&op_schema).expect("Failed to set schema."); 28 | 29 | println!("Altered schema."); 30 | } 31 | 32 | #[derive(Serialize, Deserialize, Debug)] 33 | struct Root { 34 | pub people: Vec, 35 | } 36 | 37 | // Don't forget chrono needs to be installed with feature "serde" to be 38 | // serializable. 39 | #[derive(Serialize, Deserialize, Debug)] 40 | struct Person { 41 | pub uid: Option, 42 | pub name: String, 43 | pub last_seen: chrono::DateTime, 44 | } 45 | 46 | fn main() { 47 | let dgraph = make_dgraph!(dgraph::new_dgraph_client("localhost:19080")); 48 | 49 | println!("Connected to dgraph via gRPC at localhost:19080."); 50 | 51 | drop_schema(&dgraph); 52 | set_schema(&dgraph); 53 | 54 | // Insert 55 | 56 | let casey = Person { 57 | uid: None, 58 | name: "Casey".to_string(), 59 | last_seen: chrono::Utc::now(), 60 | }; 61 | 62 | let mut txn = dgraph.new_txn(); 63 | let mut mutation = dgraph::Mutation::new(); 64 | 65 | mutation.set_set_json(serde_json::to_vec(&casey).expect("Failed to serialize JSON.")); 66 | txn.mutate(mutation).expect("Failed to create data."); 67 | txn.commit().expect("Failed to commit mutation"); 68 | 69 | // Query 70 | 71 | let query = r#"query all($a: string){ 72 | people(func: eq(name, $a)) { 73 | uid 74 | name 75 | last_seen 76 | } 77 | }"#; 78 | 79 | let mut vars = HashMap::new(); 80 | vars.insert("$a".to_string(), "Casey".to_string()); 81 | 82 | let resp = dgraph 83 | .new_readonly_txn() 84 | .query_with_vars(&query, vars) 85 | .expect("query"); 86 | let root: Root = serde_json::from_slice(&resp.json).expect("Failed to parse JSON."); 87 | 88 | println!("\nQuery result for `eq(name, Casey)`:\n\n{:#?}", root); 89 | } 90 | -------------------------------------------------------------------------------- /examples/geo_geojson.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use dgraph::{make_dgraph, Dgraph}; 4 | use serde_derive::{Deserialize, Serialize}; 5 | 6 | fn drop_schema(dgraph: &Dgraph) { 7 | let op_drop = dgraph::Operation { 8 | drop_all: true, 9 | ..Default::default() 10 | }; 11 | 12 | dgraph.alter(&op_drop).expect("Failed to drop schema."); 13 | 14 | println!("Dropped the schema."); 15 | } 16 | 17 | fn set_schema(dgraph: &Dgraph) { 18 | let op_schema = dgraph::Operation { 19 | schema: r#" 20 | name: string @index(exact) . 21 | location: geo @index(geo) . 22 | "# 23 | .to_string(), 24 | ..Default::default() 25 | }; 26 | 27 | dgraph.alter(&op_schema).expect("Failed to set schema."); 28 | 29 | println!("Altered schema."); 30 | } 31 | 32 | #[derive(Serialize, Deserialize, Debug)] 33 | struct Root { 34 | pub cities: Vec, 35 | } 36 | 37 | #[derive(Serialize, Deserialize, Debug)] 38 | struct City { 39 | pub uid: Option, 40 | pub name: String, 41 | pub location: geojson::Geometry, 42 | } 43 | 44 | fn main() { 45 | let dgraph = make_dgraph!(dgraph::new_dgraph_client("localhost:19080")); 46 | 47 | println!("Connected to dgraph via gRPC at localhost:19080."); 48 | 49 | drop_schema(&dgraph); 50 | set_schema(&dgraph); 51 | 52 | // Insert 53 | 54 | let city = City { 55 | uid: None, 56 | name: "Bratislava".to_string(), 57 | location: geojson::Geometry::new(geojson::Value::Point(vec![48.148_16, 17.106_74])), 58 | }; 59 | 60 | let mut txn = dgraph.new_txn(); 61 | let mut mutation = dgraph::Mutation::new(); 62 | 63 | mutation.set_set_json(serde_json::to_vec(&city).expect("Failed to serialize JSON.")); 64 | txn.mutate(mutation).expect("Failed to create data."); 65 | txn.commit().expect("Failed to commit mutation"); 66 | 67 | // Query 68 | 69 | let query = r#"query all($a: string){ 70 | cities(func: eq(name, $a)) { 71 | uid 72 | name 73 | location 74 | } 75 | }"#; 76 | 77 | let mut vars = HashMap::new(); 78 | vars.insert("$a".to_string(), "Bratislava".to_string()); 79 | 80 | let resp = dgraph 81 | .new_readonly_txn() 82 | .query_with_vars(&query, vars) 83 | .expect("query"); 84 | let root: Root = serde_json::from_slice(&resp.json).expect("Failed to parse JSON."); 85 | 86 | println!("\nQuery result for `eq(name, Bratislava)`:\n\n{:#?}", root); 87 | } 88 | -------------------------------------------------------------------------------- /examples/geo_simple.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use dgraph::{make_dgraph, Dgraph}; 4 | use serde_derive::{Deserialize, Serialize}; 5 | 6 | fn drop_schema(dgraph: &Dgraph) { 7 | let op_drop = dgraph::Operation { 8 | drop_all: true, 9 | ..Default::default() 10 | }; 11 | 12 | dgraph.alter(&op_drop).expect("Failed to drop schema."); 13 | 14 | println!("Dropped the schema."); 15 | } 16 | 17 | fn set_schema(dgraph: &Dgraph) { 18 | let op_schema = dgraph::Operation { 19 | schema: r#" 20 | name: string @index(exact) . 21 | location: geo @index(geo) . 22 | "# 23 | .to_string(), 24 | ..Default::default() 25 | }; 26 | 27 | dgraph.alter(&op_schema).expect("Failed to set schema."); 28 | 29 | println!("Altered schema."); 30 | } 31 | 32 | #[derive(Serialize, Deserialize, Debug)] 33 | struct Root { 34 | pub cities: Vec, 35 | } 36 | 37 | #[derive(Serialize, Deserialize, Debug)] 38 | struct Point { 39 | #[serde(rename = "type")] 40 | ty: String, 41 | coordinates: [f64; 2], 42 | } 43 | 44 | #[derive(Serialize, Deserialize, Debug)] 45 | struct City { 46 | pub uid: Option, 47 | pub name: String, 48 | pub location: Point, 49 | } 50 | 51 | fn main() { 52 | let dgraph = make_dgraph!(dgraph::new_dgraph_client("localhost:19080")); 53 | 54 | println!("Connected to dgraph via gRPC at localhost:19080."); 55 | 56 | drop_schema(&dgraph); 57 | set_schema(&dgraph); 58 | 59 | // Insert 60 | 61 | let city = City { 62 | uid: None, 63 | name: "Vienna".to_string(), 64 | location: Point { 65 | ty: "Point".to_string(), 66 | coordinates: [48.208_49, 16.372_08], 67 | }, 68 | }; 69 | 70 | let mut txn = dgraph.new_txn(); 71 | let mut mutation = dgraph::Mutation::new(); 72 | 73 | mutation.set_set_json(serde_json::to_vec(&city).expect("Failed to serialize JSON.")); 74 | txn.mutate(mutation).expect("Failed to create data."); 75 | txn.commit().expect("Failed to commit mutation"); 76 | 77 | // Query 78 | 79 | let query = r#"query all($a: string){ 80 | cities(func: eq(name, $a)) { 81 | uid 82 | name 83 | location 84 | } 85 | }"#; 86 | 87 | let mut vars = HashMap::new(); 88 | vars.insert("$a".to_string(), "Vienna".to_string()); 89 | 90 | let resp = dgraph 91 | .new_readonly_txn() 92 | .query_with_vars(&query, vars) 93 | .expect("query"); 94 | let root: Root = serde_json::from_slice(&resp.json).expect("Failed to parse JSON."); 95 | 96 | println!("\nQuery result for `eq(name, Vienna)`:\n\n{:#?}", root); 97 | } 98 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use chrono::prelude::*; 4 | use dgraph::{make_dgraph, Dgraph}; 5 | use serde_derive::{Deserialize, Serialize}; 6 | 7 | #[derive(Serialize, Deserialize, Default, Debug)] 8 | struct Root { 9 | me: Vec, 10 | } 11 | 12 | #[derive(Serialize, Deserialize, Default, Debug)] 13 | struct School { 14 | name: String, 15 | } 16 | 17 | #[derive(Serialize, Deserialize, Default, Debug)] 18 | struct Location { 19 | #[serde(rename = "type")] 20 | kind: String, 21 | coordinates: Vec, 22 | } 23 | 24 | #[derive(Serialize, Deserialize, Default, Debug)] 25 | struct Person { 26 | name: String, 27 | age: Option, 28 | dob: Option>, 29 | married: Option, 30 | friend: Option>, 31 | loc: Option, 32 | school: Option>, 33 | } 34 | 35 | fn drop_schema(dgraph: &Dgraph) { 36 | let op_drop = dgraph::Operation { 37 | drop_all: true, 38 | ..Default::default() 39 | }; 40 | 41 | dgraph.alter(&op_drop).expect("Failed to drop schema."); 42 | 43 | println!("Dropped the schema."); 44 | } 45 | 46 | fn set_schema(dgraph: &Dgraph) { 47 | let op_schema = dgraph::Operation { 48 | schema: r#" 49 | name: string @index(exact) . 50 | age: int . 51 | married: bool . 52 | loc: geo . 53 | dob: datetime . 54 | "# 55 | .to_string(), 56 | ..Default::default() 57 | }; 58 | 59 | dgraph.alter(&op_schema).expect("Failed to set schema."); 60 | 61 | println!("Altered schema."); 62 | } 63 | 64 | fn main() { 65 | let dgraph = make_dgraph!(dgraph::new_dgraph_client("localhost:19080")); 66 | 67 | println!("Connected to dgraph via gRPC at localhost:19080."); 68 | 69 | drop_schema(&dgraph); 70 | set_schema(&dgraph); 71 | 72 | let mut txn = dgraph.new_txn(); 73 | 74 | // While setting an object if a struct has a Uid then its properties in 75 | // the graph are updated. Else a new node is created. 76 | // 77 | // In the example below new nodes for Alice, Bob and Charlie and school 78 | // are created (since they don't have a Uid). 79 | let date_of_birth = Utc.ymd(1980, 1, 1).and_hms(23, 0, 0); 80 | let p = Person { 81 | name: "Alice".to_string(), 82 | age: Some(26), 83 | married: Some(true), 84 | loc: Some(Location { 85 | kind: "Point".to_string(), 86 | coordinates: vec![1.1f64, 2f64], 87 | }), 88 | dob: Some(date_of_birth), 89 | friend: Some(vec![ 90 | Person { 91 | name: "Bob".to_string(), 92 | age: Some(24), 93 | ..Default::default() 94 | }, 95 | Person { 96 | name: "Charlie".to_string(), 97 | age: Some(29), 98 | ..Default::default() 99 | }, 100 | ]), 101 | school: Some(vec![School { 102 | name: "Crown Public School".to_string(), 103 | }]), 104 | }; 105 | 106 | // Run mutation 107 | let mut mutation = dgraph::Mutation::new(); 108 | mutation.set_set_json(serde_json::to_vec(&p).expect("Failed to serialize JSON.")); 109 | let assigned = txn.mutate(mutation).expect("Failed to create data."); 110 | 111 | // Commit transaction 112 | txn.commit().expect("Failed to commit mutation"); 113 | 114 | println!("All created nodes (map from blank node names to uids):\n"); 115 | 116 | for (key, val) in assigned.uids.iter() { 117 | println!("\t{} => {}", key, val); 118 | } 119 | 120 | let query = r#"query all($a: string){ 121 | me(func: eq(name, $a)) { 122 | uid 123 | name 124 | age 125 | married 126 | loc 127 | dob 128 | friend { 129 | name 130 | age 131 | } 132 | school { 133 | name 134 | } 135 | } 136 | }"#; 137 | 138 | let mut vars = HashMap::new(); 139 | vars.insert("$a".to_string(), "Alice".to_string()); 140 | 141 | let resp = dgraph 142 | .new_readonly_txn() 143 | .query_with_vars(&query, vars) 144 | .expect("query"); 145 | let root: Root = serde_json::from_slice(&resp.json).expect("Failed to convert slice to JSON."); 146 | 147 | println!("\nQuery result for `eq(name, Alice)`:\n\n{:#?}", root); 148 | } 149 | -------------------------------------------------------------------------------- /examples/tls/README.md: -------------------------------------------------------------------------------- 1 | # Mutual TLS example project 2 | 3 | Project demonstrating the use of `dgraph-rs` and Dgraph set up with client-server 4 | mutual TLS. The following guide shows how to set up a single-group two-node 5 | cluster (1 Dgraph Zero and 1 Dgraph Alpha) configured with mutual TLS. 6 | 7 | ## Running 8 | 9 | ### Install Dgraph 10 | 11 | You will need to [install Dgraph v1.1 or 12 | above](https://docs.dgraph.io/get-started/#step-1-install-dgraph). 13 | 14 | A quick-start installation script is available for Linux and Mac: 15 | 16 | ```sh 17 | curl -sSf https://get.dgraph.io | bash 18 | ``` 19 | 20 | ### Create TLS certificates 21 | 22 | Dgraph provides a `dgraph cert` tool to create and manage self-signed 23 | server and client certificates using a generated Dgraph Root CA. See the [TLS 24 | documentation](https://docs.dgraph.io/deploy/#tls-configuration) for more 25 | information. 26 | 27 | Create the root CA. All certificates and keys are created in the `tls` directory. 28 | 29 | ```sh 30 | dgraph cert 31 | ``` 32 | 33 | Now create the Alpha server certificate (`node.crt`) and key (`node.key`) and client 34 | certificate (`client.user.crt`) key (`client.user.key`). 35 | 36 | ```sh 37 | dgraph cert -n localhost 38 | ``` 39 | 40 | ```sh 41 | dgraph cert -c user 42 | ``` 43 | 44 | The following files should now be in the `tls` directory: 45 | 46 | ```sh 47 | $ ls tls 48 | ca.crt ca.key client.user.crt client.user.key node.crt node.key 49 | ``` 50 | 51 | Using `dgraph cert ls` provides more details about each file. For instance, it 52 | shows that the `node.crt` is valid only for the host named `localhost` and the 53 | corresponding file permissions. 54 | 55 | ```sh 56 | $ dgraph cert ls 57 | -rw-r--r-- ca.crt - Dgraph Root CA certificate 58 | Issuer: Dgraph Labs, Inc. 59 | S/N: 3dfb9c54929d703b 60 | Expiration: 19 Feb 29 00:57 UTC 61 | MD5 hash: C82CF5D4C344668E34A61D590D6A4B77 62 | 63 | -r-------- ca.key - Dgraph Root CA key 64 | MD5 hash: C82CF5D4C344668E34A61D590D6A4B77 65 | 66 | -rw-r--r-- client.user.crt - Dgraph client certificate: user 67 | Issuer: Dgraph Labs, Inc. 68 | CA Verify: PASSED 69 | S/N: 5991417e75ba14c7 70 | Expiration: 21 Feb 24 01:04 UTC 71 | MD5 hash: BA35D4ABD8DFF1ED137E8D8E5D921D06 72 | 73 | -rw------- client.user.key - Dgraph Client key 74 | MD5 hash: BA35D4ABD8DFF1ED137E8D8E5D921D06 75 | 76 | -rw-r--r-- node.crt - Dgraph Node certificate 77 | Issuer: Dgraph Labs, Inc. 78 | CA Verify: PASSED 79 | S/N: 51d53048b6845d8c 80 | Expiration: 21 Feb 24 01:00 UTC 81 | Hosts: localhost 82 | MD5 hash: 5D71F59AAEE294F1CFDA9E3232761018 83 | 84 | -rw------- node.key - Dgraph Node key 85 | MD5 hash: 5D71F59AAEE294F1CFDA9E3232761018 86 | ``` 87 | 88 | ### Start Dgraph cluster 89 | 90 | Start Dgraph Zero: 91 | 92 | ```sh 93 | dgraph zero 94 | ``` 95 | 96 | Start Dgraph Alpha with TLS options. `REQUIREANDVERIFY` sets mutual TLS (server authentication and client authentication): 97 | 98 | ```sh 99 | dgraph alpha --lru_mb=1024 --zero=localhost:5080 --tls_dir=./tls --tls_client_auth=REQUIREANDVERIFY 100 | ``` 101 | 102 | ### Run example 103 | 104 | ```sh 105 | cargo run --example tls 106 | ``` 107 | -------------------------------------------------------------------------------- /examples/tls/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fs::File; 3 | use std::io::prelude::*; 4 | use std::io::BufReader; 5 | 6 | use chrono::prelude::*; 7 | use dgraph::{make_dgraph, Dgraph}; 8 | use serde_derive::{Deserialize, Serialize}; 9 | 10 | #[derive(Serialize, Deserialize, Default, Debug)] 11 | struct Root { 12 | me: Vec, 13 | } 14 | 15 | #[derive(Serialize, Deserialize, Default, Debug)] 16 | struct School { 17 | name: String, 18 | } 19 | 20 | #[derive(Serialize, Deserialize, Default, Debug)] 21 | struct Location { 22 | #[serde(rename = "type")] 23 | kind: String, 24 | coordinates: Vec, 25 | } 26 | 27 | #[derive(Serialize, Deserialize, Default, Debug)] 28 | struct Person { 29 | name: String, 30 | age: Option, 31 | dob: Option>, 32 | married: Option, 33 | friend: Option>, 34 | loc: Option, 35 | school: Option>, 36 | } 37 | 38 | fn drop_schema(dgraph: &Dgraph) { 39 | let op_drop = dgraph::Operation { 40 | drop_all: true, 41 | ..Default::default() 42 | }; 43 | 44 | dgraph.alter(&op_drop).expect("Failed to drop schema."); 45 | 46 | println!("Dropped the schema."); 47 | } 48 | 49 | fn set_schema(dgraph: &Dgraph) { 50 | let op_schema = dgraph::Operation { 51 | schema: r#" 52 | name: string @index(exact) . 53 | age: int . 54 | married: bool . 55 | loc: geo . 56 | dob: datetime . 57 | "# 58 | .to_string(), 59 | ..Default::default() 60 | }; 61 | 62 | dgraph.alter(&op_schema).expect("Failed to set schema."); 63 | 64 | println!("Altered schema."); 65 | } 66 | 67 | fn open_cert_file(path: &str) -> Vec { 68 | if let Result::Ok(file) = File::open(path) { 69 | let mut reader = BufReader::new(file); 70 | let mut contents = String::new(); 71 | reader 72 | .read_to_string(&mut contents) 73 | .unwrap_or_else(|_| panic!("Read contents of file {} into string.", path)); 74 | 75 | contents.into_bytes() 76 | } else { 77 | panic!("File {} not found!", path); 78 | } 79 | } 80 | 81 | fn main() { 82 | let root_ca = open_cert_file("./tls/ca.crt"); 83 | let cert = open_cert_file("./tls/client.user.crt"); 84 | let private_key = open_cert_file("./tls/client.user.key"); 85 | 86 | let dgraph = make_dgraph!(dgraph::new_secure_dgraph_client( 87 | "localhost:19080", 88 | root_ca, 89 | cert, 90 | private_key 91 | )); 92 | 93 | println!("Connected to dgraph via gRPC at localhost:19080."); 94 | 95 | drop_schema(&dgraph); 96 | set_schema(&dgraph); 97 | 98 | let mut txn = dgraph.new_txn(); 99 | 100 | // While setting an object if a struct has a Uid then its properties in 101 | // the graph are updated. Else a new node is created. 102 | // 103 | // In the example below new nodes for Alice, Bob and Charlie and school 104 | // are created (since they don't have a Uid). 105 | let date_of_birth = Utc.ymd(1980, 1, 1).and_hms(23, 0, 0); 106 | let p = Person { 107 | name: "Alice".to_string(), 108 | age: Some(26), 109 | married: Some(true), 110 | loc: Some(Location { 111 | kind: "Point".to_string(), 112 | coordinates: vec![1.1f64, 2f64], 113 | }), 114 | dob: Some(date_of_birth), 115 | friend: Some(vec![ 116 | Person { 117 | name: "Bob".to_string(), 118 | age: Some(24), 119 | ..Default::default() 120 | }, 121 | Person { 122 | name: "Charlie".to_string(), 123 | age: Some(29), 124 | ..Default::default() 125 | }, 126 | ]), 127 | school: Some(vec![School { 128 | name: "Crown Public School".to_string(), 129 | }]), 130 | }; 131 | 132 | // Run mutation 133 | let mut mutation = dgraph::Mutation::new(); 134 | mutation.set_set_json(serde_json::to_vec(&p).expect("Failed to serialize JSON.")); 135 | let assigned = txn.mutate(mutation).expect("Failed to create data."); 136 | 137 | // Commit transaction 138 | txn.commit().expect("Failed to commit mutation"); 139 | 140 | println!("All created nodes (map from blank node names to uids):\n"); 141 | 142 | for (key, val) in assigned.uids.iter() { 143 | println!("\t{} => {}", key, val); 144 | } 145 | 146 | let query = r#"query all($a: string){ 147 | me(func: eq(name, $a)) { 148 | uid 149 | name 150 | age 151 | married 152 | loc 153 | dob 154 | friend { 155 | name 156 | age 157 | } 158 | school { 159 | name 160 | } 161 | } 162 | }"#; 163 | 164 | let mut vars = HashMap::new(); 165 | vars.insert("$a".to_string(), "Alice".to_string()); 166 | 167 | let resp = dgraph 168 | .new_readonly_txn() 169 | .query_with_vars(&query, vars) 170 | .expect("query"); 171 | let root: Root = serde_json::from_slice(&resp.json).expect("Failed to convert slice to JSON."); 172 | 173 | println!("\nQuery result for `eq(name, Alice)`:\n\n{:#?}", root); 174 | } 175 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | use rand::prelude::*; 2 | use std::sync::Mutex; 3 | 4 | use crate::errors::DgraphError; 5 | use crate::protos::api; 6 | use crate::protos::api_grpc; 7 | use crate::txn::Txn; 8 | 9 | // Dgraph is a transaction aware client to a set of dgraph server instances. 10 | pub struct Dgraph { 11 | jwt: Mutex, 12 | dc: Vec, 13 | } 14 | 15 | impl Dgraph { 16 | /// Creates a new Dgraph for interacting with the Dgraph store connected to in 17 | /// conns. 18 | /// The client can be backed by multiple connections (to the same server, or multiple servers in a 19 | /// cluster). 20 | /// 21 | /// A single client is thread safe for sharing with multiple go routines. 22 | pub fn new(clients: Vec) -> Dgraph { 23 | Dgraph { 24 | jwt: Mutex::new(api::Jwt::new()), 25 | dc: clients, 26 | } 27 | } 28 | 29 | pub fn login(&mut self, userid: String, password: String) -> Result<(), DgraphError> { 30 | let dc = self.any_client().expect("Cannot login. No client present"); 31 | 32 | let login_request = api::LoginRequest { 33 | userid, 34 | password, 35 | ..Default::default() 36 | }; 37 | 38 | let res = dc.login(&login_request)?; 39 | let jwt = protobuf::parse_from_bytes::(res.get_json()).unwrap(); 40 | 41 | *self 42 | .jwt 43 | .lock() 44 | .expect("Unable to block or acquire lock to jwt mutex") = jwt; 45 | 46 | Ok(()) 47 | } 48 | 49 | pub fn alter(&self, op: &api::Operation) -> Result { 50 | let dc = self.any_client().expect("Cannot alter. No client present"); 51 | let res = dc.alter(op); 52 | 53 | match res { 54 | Ok(res) => Ok(res), 55 | Err(err) => { 56 | if self.is_jwt_expired(&err) { 57 | self.retry_login()?; 58 | 59 | let res = dc.alter(op)?; 60 | 61 | Ok(res) 62 | } else { 63 | Err(err.into()) 64 | } 65 | } 66 | } 67 | } 68 | 69 | pub fn any_client(&self) -> Option<&api_grpc::DgraphClient> { 70 | let mut rng = thread_rng(); 71 | 72 | self.dc.choose(&mut rng) 73 | } 74 | 75 | pub fn new_txn(&self) -> Txn { 76 | Txn { 77 | context: Default::default(), 78 | finished: false, 79 | mutated: false, 80 | read_only: false, 81 | best_effort: false, 82 | client: self 83 | .any_client() 84 | .expect("Cannot create transactions. No client present!"), 85 | dgraph: self, 86 | } 87 | } 88 | 89 | pub fn new_readonly_txn(&self) -> Txn { 90 | let mut txn = self.new_txn(); 91 | txn.read_only = true; 92 | txn 93 | } 94 | 95 | pub fn is_jwt_expired(&self, grpc_error: &grpcio::Error) -> bool { 96 | if let grpcio::Error::RpcFailure(rpc_failure) = grpc_error { 97 | if rpc_failure.status == grpcio::RpcStatusCode::UNAUTHENTICATED { 98 | return true; 99 | } 100 | } 101 | 102 | false 103 | } 104 | 105 | pub fn retry_login(&self) -> Result<(), DgraphError> { 106 | let mut jwt = self 107 | .jwt 108 | .lock() 109 | .expect("Unable to block or acquire lock to jwt mutex"); 110 | 111 | if jwt.refresh_jwt.is_empty() { 112 | return Err(DgraphError::JwtRefreshTokenEmpty); 113 | } 114 | 115 | let dc = self.any_client().expect("Cannot alter. No client present"); 116 | let login_request = api::LoginRequest { 117 | refresh_token: jwt.refresh_jwt.clone(), 118 | ..Default::default() 119 | }; 120 | let response = dc.login(&login_request)?; 121 | 122 | *jwt = serde_json::from_str(std::str::from_utf8(&response.json).unwrap()).unwrap(); 123 | 124 | Ok(()) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fmt; 3 | 4 | /// The error type for Dgraph operations. 5 | /// 6 | /// Native gRPC errors are wrapped into `GrpcError`. 7 | #[derive(Debug)] 8 | pub enum DgraphError { 9 | TxnReadOnly, 10 | TxnFinished, 11 | EmptyTxn, 12 | MissingTxnContext, 13 | WriteTxnBestEffort, 14 | StartTsMismatch, 15 | JwtRefreshTokenEmpty, 16 | GrpcError(grpcio::Error), 17 | } 18 | 19 | impl Error for DgraphError { 20 | fn source(&self) -> Option<&(dyn Error + 'static)> { 21 | match self { 22 | DgraphError::GrpcError(grpc_error) => Some(grpc_error), 23 | _ => None, 24 | } 25 | } 26 | } 27 | 28 | impl fmt::Display for DgraphError { 29 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 30 | match *self { 31 | DgraphError::TxnFinished => { 32 | write!(f, "Transaction has already been committed or discarded") 33 | } 34 | DgraphError::TxnReadOnly => write!( 35 | f, 36 | "Readonly transaction cannot run mutations or be committed" 37 | ), 38 | DgraphError::WriteTxnBestEffort => { 39 | write!(f, "Best effort only works for read-only queries") 40 | } 41 | DgraphError::EmptyTxn => write!(f, "Got empty Txn response back from query"), 42 | DgraphError::MissingTxnContext => write!(f, "Missing Txn context on mutation response"), 43 | DgraphError::StartTsMismatch => write!(f, "StartTs mismatch"), 44 | DgraphError::JwtRefreshTokenEmpty => write!(f, "JWT refresh token is empty"), 45 | DgraphError::GrpcError(ref grpc_error) => write!(f, "Grpc error: {}", grpc_error), 46 | } 47 | } 48 | } 49 | 50 | impl From for DgraphError { 51 | fn from(err: grpcio::Error) -> Self { 52 | DgraphError::GrpcError(err) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod client; 2 | mod errors; 3 | mod protos; 4 | mod txn; 5 | 6 | use grpcio::{ChannelBuilder, ChannelCredentialsBuilder, EnvBuilder}; 7 | use std::sync::Arc; 8 | 9 | pub use grpcio; 10 | 11 | pub use client::Dgraph; 12 | pub use errors::DgraphError; 13 | pub use protos::api::*; 14 | pub use protos::api_grpc::*; 15 | pub use txn::Txn; 16 | 17 | #[cfg(feature = "with-serde")] 18 | extern crate serde; 19 | #[cfg(feature = "with-serde")] 20 | #[macro_use] 21 | extern crate serde_derive; 22 | #[cfg(feature = "with-serde")] 23 | extern crate serde_json; 24 | 25 | pub fn new_secure_dgraph_client( 26 | addr: &str, 27 | root_ca: Vec, 28 | cert: Vec, 29 | private_key: Vec, 30 | ) -> DgraphClient { 31 | let env = Arc::new(EnvBuilder::new().build()); 32 | let credentials = ChannelCredentialsBuilder::new() 33 | .root_cert(root_ca) 34 | .cert(cert, private_key) 35 | .build(); 36 | let channel = ChannelBuilder::new(env).secure_connect(addr, credentials); 37 | DgraphClient::new(channel) 38 | } 39 | 40 | pub fn new_dgraph_client(addr: &str) -> DgraphClient { 41 | let env = Arc::new(EnvBuilder::new().build()); 42 | let channel = ChannelBuilder::new(env).connect(addr); 43 | DgraphClient::new(channel) 44 | } 45 | 46 | #[macro_export] 47 | macro_rules! make_dgraph { 48 | ($( $x:expr ),* ) => { 49 | { 50 | let mut temp_vec: Vec = vec![$($x,)*]; 51 | dgraph::Dgraph::new(temp_vec) 52 | } 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/protos/api.proto: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Dgraph Labs, Inc. and Contributors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // Style guide for Protocol Buffer 3. 18 | // Use PascalCase (camelCase with an initial capital) for message names – for 19 | // example, SongServerRequest. Use snake_case (underscore_separated_names) for 20 | // field names – for example, song_name. 21 | 22 | syntax = "proto3"; 23 | 24 | package api; 25 | 26 | /* import "gogoproto/gogo.proto"; */ 27 | 28 | /* option (gogoproto.marshaler_all) = true; */ 29 | /* option (gogoproto.sizer_all) = true; */ 30 | /* option (gogoproto.unmarshaler_all) = true; */ 31 | /* option (gogoproto.goproto_getters_all) = true; */ 32 | 33 | option java_package = "io.dgraph"; 34 | option java_outer_classname = "DgraphProto"; 35 | 36 | // Graph response. 37 | service Dgraph { 38 | rpc Login(LoginRequest) returns (Response) {} 39 | rpc Query(Request) returns (Response) {} 40 | rpc Alter(Operation) returns (Payload) {} 41 | rpc CommitOrAbort(TxnContext) returns (TxnContext) {} 42 | rpc CheckVersion(Check) returns (Version) {} 43 | } 44 | 45 | message Request { 46 | uint64 start_ts = 1; 47 | 48 | string query = 4; 49 | map vars = 5; // Support for GraphQL like variables. 50 | bool read_only = 6; 51 | bool best_effort = 7; 52 | 53 | repeated Mutation mutations = 12; 54 | bool commit_now = 13; 55 | } 56 | 57 | message Uids { repeated string uids = 1; } 58 | 59 | message Response { 60 | bytes json = 1; 61 | TxnContext txn = 2; 62 | Latency latency = 3; 63 | // Metrics contains all metrics related to the query. 64 | Metrics metrics = 4; 65 | // uids contains a mapping of blank_node => uid for the node. It only returns 66 | // uids that were created as part of a mutation. 67 | map uids = 12; 68 | } 69 | 70 | message Mutation { 71 | bytes set_json = 1; 72 | bytes delete_json = 2; 73 | bytes set_nquads = 3; 74 | bytes del_nquads = 4; 75 | repeated NQuad set = 5; 76 | repeated NQuad del = 6; 77 | 78 | // This is being used for upserts. 79 | string cond = 9; 80 | 81 | // This field is a duplicate of the one in Request and placed here for 82 | // convenience. 83 | bool commit_now = 14; 84 | } 85 | 86 | message Operation { 87 | string schema = 1; 88 | string drop_attr = 2; 89 | bool drop_all = 3; 90 | 91 | enum DropOp { 92 | NONE = 0; 93 | ALL = 1; 94 | DATA = 2; 95 | ATTR = 3; 96 | TYPE = 4; 97 | } 98 | DropOp drop_op = 4; 99 | 100 | // If drop_op is ATTR or TYPE, drop_value holds the name of the predicate or 101 | // type to delete. 102 | string drop_value = 5; 103 | 104 | // run indexes in background. 105 | bool run_in_background = 6; 106 | } 107 | 108 | // Worker services. 109 | message Payload { bytes Data = 1; } 110 | 111 | message TxnContext { 112 | uint64 start_ts = 1; 113 | uint64 commit_ts = 2; 114 | bool aborted = 3; 115 | repeated string keys = 4; // List of keys to be used for conflict detection. 116 | repeated string preds = 5; // List of predicates involved in this transaction. 117 | } 118 | 119 | message Check {} 120 | 121 | message Version { string tag = 1; } 122 | 123 | message Latency { 124 | uint64 parsing_ns = 1; 125 | uint64 processing_ns = 2; 126 | uint64 encoding_ns = 3; 127 | uint64 assign_timestamp_ns = 4; 128 | uint64 total_ns = 5; 129 | } 130 | 131 | message Metrics { 132 | // num_uids is the map of number of uids processed by each attribute. 133 | map num_uids = 1; 134 | } 135 | 136 | message NQuad { 137 | string subject = 1; 138 | string predicate = 2; 139 | string object_id = 3; 140 | Value object_value = 4; 141 | string label = 5; 142 | string lang = 6; 143 | repeated Facet facets = 7; 144 | } 145 | 146 | message Value { 147 | oneof val { 148 | string default_val = 1; 149 | bytes bytes_val = 2; 150 | int64 int_val = 3; 151 | bool bool_val = 4; 152 | string str_val = 5; 153 | double double_val = 6; 154 | bytes geo_val = 7; // Geo data in WKB format 155 | bytes date_val = 8; 156 | bytes datetime_val = 9; 157 | string password_val = 10; 158 | uint64 uid_val = 11; 159 | } 160 | } 161 | 162 | message Facet { 163 | enum ValType { 164 | STRING = 0; 165 | INT = 1; 166 | FLOAT = 2; 167 | BOOL = 3; 168 | DATETIME = 4; 169 | } 170 | 171 | string key = 1; 172 | bytes value = 2; 173 | ValType val_type = 3; 174 | repeated string tokens = 4; // tokens of value. 175 | string alias = 5; // not stored, only used for query. 176 | } 177 | 178 | message LoginRequest { 179 | string userid = 1; 180 | string password = 2; 181 | string refresh_token = 3; 182 | } 183 | 184 | message Jwt { 185 | string access_jwt = 1; 186 | string refresh_jwt = 2; 187 | } 188 | 189 | // vim: noexpandtab sw=2 ts=2 -------------------------------------------------------------------------------- /src/protos/api_grpc.rs: -------------------------------------------------------------------------------- 1 | // This file is generated. Do not edit 2 | // @generated 3 | 4 | // https://github.com/Manishearth/rust-clippy/issues/702 5 | #![allow(unknown_lints)] 6 | #![allow(clippy::all)] 7 | 8 | #![cfg_attr(rustfmt, rustfmt_skip)] 9 | 10 | #![allow(box_pointers)] 11 | #![allow(dead_code)] 12 | #![allow(missing_docs)] 13 | #![allow(non_camel_case_types)] 14 | #![allow(non_snake_case)] 15 | #![allow(non_upper_case_globals)] 16 | #![allow(trivial_casts)] 17 | #![allow(unsafe_code)] 18 | #![allow(unused_imports)] 19 | #![allow(unused_results)] 20 | 21 | const METHOD_DGRAPH_LOGIN: ::grpcio::Method = 22 | ::grpcio::Method { 23 | ty: ::grpcio::MethodType::Unary, 24 | name: "/api.Dgraph/Login", 25 | req_mar: ::grpcio::Marshaller { 26 | ser: ::grpcio::pb_ser, 27 | de: ::grpcio::pb_de, 28 | }, 29 | resp_mar: ::grpcio::Marshaller { 30 | ser: ::grpcio::pb_ser, 31 | de: ::grpcio::pb_de, 32 | }, 33 | }; 34 | 35 | const METHOD_DGRAPH_QUERY: ::grpcio::Method = 36 | ::grpcio::Method { 37 | ty: ::grpcio::MethodType::Unary, 38 | name: "/api.Dgraph/Query", 39 | req_mar: ::grpcio::Marshaller { 40 | ser: ::grpcio::pb_ser, 41 | de: ::grpcio::pb_de, 42 | }, 43 | resp_mar: ::grpcio::Marshaller { 44 | ser: ::grpcio::pb_ser, 45 | de: ::grpcio::pb_de, 46 | }, 47 | }; 48 | 49 | const METHOD_DGRAPH_ALTER: ::grpcio::Method = 50 | ::grpcio::Method { 51 | ty: ::grpcio::MethodType::Unary, 52 | name: "/api.Dgraph/Alter", 53 | req_mar: ::grpcio::Marshaller { 54 | ser: ::grpcio::pb_ser, 55 | de: ::grpcio::pb_de, 56 | }, 57 | resp_mar: ::grpcio::Marshaller { 58 | ser: ::grpcio::pb_ser, 59 | de: ::grpcio::pb_de, 60 | }, 61 | }; 62 | 63 | const METHOD_DGRAPH_COMMIT_OR_ABORT: ::grpcio::Method< 64 | super::api::TxnContext, 65 | super::api::TxnContext, 66 | > = ::grpcio::Method { 67 | ty: ::grpcio::MethodType::Unary, 68 | name: "/api.Dgraph/CommitOrAbort", 69 | req_mar: ::grpcio::Marshaller { 70 | ser: ::grpcio::pb_ser, 71 | de: ::grpcio::pb_de, 72 | }, 73 | resp_mar: ::grpcio::Marshaller { 74 | ser: ::grpcio::pb_ser, 75 | de: ::grpcio::pb_de, 76 | }, 77 | }; 78 | 79 | const METHOD_DGRAPH_CHECK_VERSION: ::grpcio::Method = 80 | ::grpcio::Method { 81 | ty: ::grpcio::MethodType::Unary, 82 | name: "/api.Dgraph/CheckVersion", 83 | req_mar: ::grpcio::Marshaller { 84 | ser: ::grpcio::pb_ser, 85 | de: ::grpcio::pb_de, 86 | }, 87 | resp_mar: ::grpcio::Marshaller { 88 | ser: ::grpcio::pb_ser, 89 | de: ::grpcio::pb_de, 90 | }, 91 | }; 92 | 93 | #[derive(Clone)] 94 | pub struct DgraphClient { 95 | client: ::grpcio::Client, 96 | } 97 | 98 | impl DgraphClient { 99 | pub fn new(channel: ::grpcio::Channel) -> Self { 100 | DgraphClient { 101 | client: ::grpcio::Client::new(channel), 102 | } 103 | } 104 | 105 | pub fn login_opt( 106 | &self, 107 | req: &super::api::LoginRequest, 108 | opt: ::grpcio::CallOption, 109 | ) -> ::grpcio::Result { 110 | self.client.unary_call(&METHOD_DGRAPH_LOGIN, req, opt) 111 | } 112 | 113 | pub fn login(&self, req: &super::api::LoginRequest) -> ::grpcio::Result { 114 | self.login_opt(req, ::grpcio::CallOption::default()) 115 | } 116 | 117 | pub fn login_async_opt( 118 | &self, 119 | req: &super::api::LoginRequest, 120 | opt: ::grpcio::CallOption, 121 | ) -> ::grpcio::Result<::grpcio::ClientUnaryReceiver> { 122 | self.client.unary_call_async(&METHOD_DGRAPH_LOGIN, req, opt) 123 | } 124 | 125 | pub fn login_async( 126 | &self, 127 | req: &super::api::LoginRequest, 128 | ) -> ::grpcio::Result<::grpcio::ClientUnaryReceiver> { 129 | self.login_async_opt(req, ::grpcio::CallOption::default()) 130 | } 131 | 132 | pub fn query_opt( 133 | &self, 134 | req: &super::api::Request, 135 | opt: ::grpcio::CallOption, 136 | ) -> ::grpcio::Result { 137 | self.client.unary_call(&METHOD_DGRAPH_QUERY, req, opt) 138 | } 139 | 140 | pub fn query(&self, req: &super::api::Request) -> ::grpcio::Result { 141 | self.query_opt(req, ::grpcio::CallOption::default()) 142 | } 143 | 144 | pub fn query_async_opt( 145 | &self, 146 | req: &super::api::Request, 147 | opt: ::grpcio::CallOption, 148 | ) -> ::grpcio::Result<::grpcio::ClientUnaryReceiver> { 149 | self.client.unary_call_async(&METHOD_DGRAPH_QUERY, req, opt) 150 | } 151 | 152 | pub fn query_async( 153 | &self, 154 | req: &super::api::Request, 155 | ) -> ::grpcio::Result<::grpcio::ClientUnaryReceiver> { 156 | self.query_async_opt(req, ::grpcio::CallOption::default()) 157 | } 158 | 159 | pub fn alter_opt( 160 | &self, 161 | req: &super::api::Operation, 162 | opt: ::grpcio::CallOption, 163 | ) -> ::grpcio::Result { 164 | self.client.unary_call(&METHOD_DGRAPH_ALTER, req, opt) 165 | } 166 | 167 | pub fn alter(&self, req: &super::api::Operation) -> ::grpcio::Result { 168 | self.alter_opt(req, ::grpcio::CallOption::default()) 169 | } 170 | 171 | pub fn alter_async_opt( 172 | &self, 173 | req: &super::api::Operation, 174 | opt: ::grpcio::CallOption, 175 | ) -> ::grpcio::Result<::grpcio::ClientUnaryReceiver> { 176 | self.client.unary_call_async(&METHOD_DGRAPH_ALTER, req, opt) 177 | } 178 | 179 | pub fn alter_async( 180 | &self, 181 | req: &super::api::Operation, 182 | ) -> ::grpcio::Result<::grpcio::ClientUnaryReceiver> { 183 | self.alter_async_opt(req, ::grpcio::CallOption::default()) 184 | } 185 | 186 | pub fn commit_or_abort_opt( 187 | &self, 188 | req: &super::api::TxnContext, 189 | opt: ::grpcio::CallOption, 190 | ) -> ::grpcio::Result { 191 | self.client 192 | .unary_call(&METHOD_DGRAPH_COMMIT_OR_ABORT, req, opt) 193 | } 194 | 195 | pub fn commit_or_abort( 196 | &self, 197 | req: &super::api::TxnContext, 198 | ) -> ::grpcio::Result { 199 | self.commit_or_abort_opt(req, ::grpcio::CallOption::default()) 200 | } 201 | 202 | pub fn commit_or_abort_async_opt( 203 | &self, 204 | req: &super::api::TxnContext, 205 | opt: ::grpcio::CallOption, 206 | ) -> ::grpcio::Result<::grpcio::ClientUnaryReceiver> { 207 | self.client 208 | .unary_call_async(&METHOD_DGRAPH_COMMIT_OR_ABORT, req, opt) 209 | } 210 | 211 | pub fn commit_or_abort_async( 212 | &self, 213 | req: &super::api::TxnContext, 214 | ) -> ::grpcio::Result<::grpcio::ClientUnaryReceiver> { 215 | self.commit_or_abort_async_opt(req, ::grpcio::CallOption::default()) 216 | } 217 | 218 | pub fn check_version_opt( 219 | &self, 220 | req: &super::api::Check, 221 | opt: ::grpcio::CallOption, 222 | ) -> ::grpcio::Result { 223 | self.client 224 | .unary_call(&METHOD_DGRAPH_CHECK_VERSION, req, opt) 225 | } 226 | 227 | pub fn check_version(&self, req: &super::api::Check) -> ::grpcio::Result { 228 | self.check_version_opt(req, ::grpcio::CallOption::default()) 229 | } 230 | 231 | pub fn check_version_async_opt( 232 | &self, 233 | req: &super::api::Check, 234 | opt: ::grpcio::CallOption, 235 | ) -> ::grpcio::Result<::grpcio::ClientUnaryReceiver> { 236 | self.client 237 | .unary_call_async(&METHOD_DGRAPH_CHECK_VERSION, req, opt) 238 | } 239 | 240 | pub fn check_version_async( 241 | &self, 242 | req: &super::api::Check, 243 | ) -> ::grpcio::Result<::grpcio::ClientUnaryReceiver> { 244 | self.check_version_async_opt(req, ::grpcio::CallOption::default()) 245 | } 246 | pub fn spawn(&self, f: F) 247 | where 248 | F: ::futures::Future + Send + 'static, 249 | { 250 | self.client.spawn(f) 251 | } 252 | } 253 | 254 | pub trait Dgraph { 255 | fn login( 256 | &mut self, 257 | ctx: ::grpcio::RpcContext, 258 | req: super::api::LoginRequest, 259 | sink: ::grpcio::UnarySink, 260 | ); 261 | fn query( 262 | &mut self, 263 | ctx: ::grpcio::RpcContext, 264 | req: super::api::Request, 265 | sink: ::grpcio::UnarySink, 266 | ); 267 | fn alter( 268 | &mut self, 269 | ctx: ::grpcio::RpcContext, 270 | req: super::api::Operation, 271 | sink: ::grpcio::UnarySink, 272 | ); 273 | fn commit_or_abort( 274 | &mut self, 275 | ctx: ::grpcio::RpcContext, 276 | req: super::api::TxnContext, 277 | sink: ::grpcio::UnarySink, 278 | ); 279 | fn check_version( 280 | &mut self, 281 | ctx: ::grpcio::RpcContext, 282 | req: super::api::Check, 283 | sink: ::grpcio::UnarySink, 284 | ); 285 | } 286 | 287 | pub fn create_dgraph(s: S) -> ::grpcio::Service { 288 | let mut builder = ::grpcio::ServiceBuilder::new(); 289 | let mut instance = s.clone(); 290 | builder = builder.add_unary_handler(&METHOD_DGRAPH_LOGIN, move |ctx, req, resp| { 291 | instance.login(ctx, req, resp) 292 | }); 293 | let mut instance = s.clone(); 294 | builder = builder.add_unary_handler(&METHOD_DGRAPH_QUERY, move |ctx, req, resp| { 295 | instance.query(ctx, req, resp) 296 | }); 297 | let mut instance = s.clone(); 298 | builder = builder.add_unary_handler(&METHOD_DGRAPH_ALTER, move |ctx, req, resp| { 299 | instance.alter(ctx, req, resp) 300 | }); 301 | let mut instance = s.clone(); 302 | builder = builder.add_unary_handler(&METHOD_DGRAPH_COMMIT_OR_ABORT, move |ctx, req, resp| { 303 | instance.commit_or_abort(ctx, req, resp) 304 | }); 305 | let mut instance = s; 306 | builder = builder.add_unary_handler(&METHOD_DGRAPH_CHECK_VERSION, move |ctx, req, resp| { 307 | instance.check_version(ctx, req, resp) 308 | }); 309 | builder.build() 310 | } 311 | -------------------------------------------------------------------------------- /src/protos/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod api; 2 | pub mod api_grpc; 3 | -------------------------------------------------------------------------------- /src/txn.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::errors::DgraphError; 4 | use crate::protos::api; 5 | use crate::protos::api_grpc; 6 | 7 | pub struct Txn<'a> { 8 | pub(super) context: api::TxnContext, 9 | pub(super) finished: bool, 10 | pub(super) read_only: bool, 11 | pub(super) best_effort: bool, 12 | pub(super) mutated: bool, 13 | pub(super) client: &'a api_grpc::DgraphClient, 14 | pub(super) dgraph: &'a crate::Dgraph, 15 | } 16 | 17 | /// Call Txn::discard() once txn goes out of scope. 18 | /// This is safe to do so, and is possible a no-op 19 | impl Drop for Txn<'_> { 20 | fn drop(&mut self) { 21 | let _ = self.discard(); 22 | } 23 | } 24 | 25 | impl Txn<'_> { 26 | /// `best_effort` enables best effort in read-only queries. Using this flag 27 | /// will ask the Dgraph Alpha to try to get timestamps from memory in a best 28 | /// effort to reduce the number of outbound requests to Zero. This may yield 29 | /// improved latencies in read-bound datasets. Returns the transaction itself. 30 | pub fn best_effort(&mut self) -> Result<&Txn, DgraphError> { 31 | if !self.read_only { 32 | return Err(DgraphError::WriteTxnBestEffort); 33 | } 34 | self.best_effort = true; 35 | Ok(self) 36 | } 37 | 38 | pub fn query(&mut self, query: &str) -> Result { 39 | self.query_with_vars(query, HashMap::new()) 40 | } 41 | 42 | pub fn query_with_vars( 43 | &mut self, 44 | query: &str, 45 | vars: HashMap, 46 | ) -> Result { 47 | let mut request = api::Request { 48 | query: query.to_string(), 49 | vars, 50 | start_ts: self.context.get_start_ts(), 51 | read_only: self.read_only, 52 | best_effort: self.best_effort, 53 | ..Default::default() 54 | }; 55 | 56 | self.do_request(&mut request) 57 | } 58 | 59 | pub fn mutate(&mut self, mu: api::Mutation) -> Result { 60 | let mut request = api::Request::new(); 61 | let mutations = vec![mu.clone()]; 62 | 63 | request.set_mutations(mutations.into()); 64 | request.set_commit_now(mu.get_commit_now()); 65 | 66 | self.do_request(&mut request) 67 | } 68 | 69 | pub fn do_request(&mut self, request: &mut api::Request) -> Result { 70 | let mutation_list = request.get_mutations(); 71 | 72 | if self.finished { 73 | return Err(DgraphError::TxnFinished); 74 | } 75 | 76 | if !mutation_list.is_empty() { 77 | if self.read_only { 78 | return Err(DgraphError::TxnReadOnly); 79 | } 80 | 81 | self.mutated = true; 82 | } 83 | 84 | request.set_start_ts(self.context.get_start_ts()); 85 | 86 | let response = match self.client.query(&request) { 87 | Ok(response) => response, 88 | Err(err) => { 89 | let retry_result = if self.dgraph.is_jwt_expired(&err) { 90 | match self.dgraph.retry_login() { 91 | Ok(_) => match self.client.query(&request) { 92 | Ok(response) => Ok(response), 93 | Err(err) => Err(err.into()), 94 | }, 95 | Err(err) => Err(err), 96 | } 97 | } else { 98 | Err(err.into()) 99 | }; 100 | 101 | match retry_result { 102 | Ok(response) => response, 103 | Err(err) => { 104 | let _ = self.discard(); 105 | return Err(err); 106 | } 107 | } 108 | } 109 | }; 110 | 111 | if request.commit_now { 112 | self.finished = true; 113 | } 114 | 115 | self.merge_context(response.get_txn())?; 116 | 117 | Ok(response) 118 | } 119 | 120 | pub fn commit(mut self) -> Result<(), DgraphError> { 121 | match (self.finished, self.read_only) { 122 | (true, _) => return Err(DgraphError::TxnFinished), 123 | (_, true) => return Err(DgraphError::TxnReadOnly), 124 | _ => (), 125 | } 126 | 127 | self.commit_or_abort() 128 | } 129 | 130 | pub fn discard(&mut self) -> Result<(), DgraphError> { 131 | self.context.aborted = true; 132 | self.commit_or_abort() 133 | } 134 | 135 | fn commit_or_abort(&mut self) -> Result<(), DgraphError> { 136 | if self.finished { 137 | return Ok(()); 138 | } 139 | self.finished = true; 140 | 141 | if !self.mutated { 142 | return Ok(()); 143 | } 144 | 145 | let res = self.client.commit_or_abort(&self.context); 146 | 147 | match res { 148 | Ok(_) => Ok(()), 149 | Err(err) => { 150 | if self.dgraph.is_jwt_expired(&err) { 151 | self.dgraph.retry_login()?; 152 | self.client.commit_or_abort(&self.context)?; 153 | 154 | Ok(()) 155 | } else { 156 | Err(err.into()) 157 | } 158 | } 159 | } 160 | } 161 | 162 | fn merge_context(&mut self, src: &api::TxnContext) -> Result<(), DgraphError> { 163 | if self.context.start_ts == 0 { 164 | self.context.start_ts = src.start_ts; 165 | } 166 | 167 | if self.context.start_ts != src.start_ts { 168 | return Err(DgraphError::StartTsMismatch); 169 | } 170 | 171 | for key in src.keys.iter() { 172 | self.context.keys.push(key.clone()); 173 | } 174 | 175 | for pred in src.preds.iter() { 176 | self.context.preds.push(pred.clone()); 177 | } 178 | 179 | Ok(()) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /tests/client.rs: -------------------------------------------------------------------------------- 1 | use dgraph::{make_dgraph, Dgraph, Operation}; 2 | 3 | mod common; 4 | 5 | fn is_connected(dgraph: &Dgraph) -> bool { 6 | let q = "schema {}".to_string(); 7 | let response = dgraph.new_readonly_txn().query(&q); 8 | 9 | response.is_ok() 10 | } 11 | 12 | #[test] 13 | fn it_connects() { 14 | let dgraph = make_dgraph!(dgraph::new_dgraph_client(common::DGRAPH_URL)); 15 | 16 | assert_eq!(is_connected(&dgraph), true); 17 | } 18 | 19 | #[test] 20 | fn it_does_not_connect_to_wrong_url() { 21 | let dgraph = make_dgraph!(dgraph::new_dgraph_client("no_dgraph_url:9080")); 22 | 23 | assert_eq!(is_connected(&dgraph), false); 24 | } 25 | 26 | #[test] 27 | fn it_alters_schema() { 28 | let dgraph = make_dgraph!(dgraph::new_dgraph_client(common::DGRAPH_URL)); 29 | 30 | let result = dgraph.alter(&Operation { 31 | schema: "something: string .".to_string(), 32 | ..Default::default() 33 | }); 34 | 35 | assert_eq!(result.is_ok(), true); 36 | } 37 | 38 | #[test] 39 | #[should_panic] 40 | fn it_does_not_alter_without_client() { 41 | let dgraph = make_dgraph!(); 42 | let _ = dgraph.alter(&Operation { 43 | schema: "something: string .".to_string(), 44 | ..Default::default() 45 | }); 46 | } 47 | 48 | #[test] 49 | #[should_panic] 50 | fn it_does_not_crate_transaction_without_client() { 51 | let dgraph = make_dgraph!(); 52 | dgraph.new_txn(); 53 | } 54 | -------------------------------------------------------------------------------- /tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | pub const DGRAPH_URL: &str = "localhost:19080"; 2 | -------------------------------------------------------------------------------- /tests/txn.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use dgraph::Request; 4 | use dgraph::{make_dgraph, DgraphError}; 5 | use serde_derive::{Deserialize, Serialize}; 6 | use serde_json; 7 | 8 | mod common; 9 | 10 | #[derive(Serialize, Deserialize, Default, Debug)] 11 | pub struct UidJson { 12 | pub uids: Vec, 13 | } 14 | 15 | #[derive(Serialize, Deserialize, Default, Debug)] 16 | pub struct Uid { 17 | pub uid: String, 18 | } 19 | 20 | #[test] 21 | fn it_runs_simple_query() { 22 | let dgraph = make_dgraph!(dgraph::new_dgraph_client(common::DGRAPH_URL)); 23 | 24 | let uid = "0x1"; 25 | let query = format!( 26 | r#"{{ 27 | uids(func: uid({})) {{ 28 | uid, 29 | }} 30 | }}"#, 31 | uid 32 | ); 33 | let resp = dgraph.new_readonly_txn().query(&query); 34 | let json: UidJson = serde_json::from_slice(&resp.unwrap().json).unwrap(); 35 | 36 | assert_eq!(json.uids[0].uid, uid); 37 | } 38 | 39 | #[test] 40 | fn it_runs_query_with_vars() { 41 | let dgraph = make_dgraph!(dgraph::new_dgraph_client(common::DGRAPH_URL)); 42 | 43 | let uid = "0x1"; 44 | let query = r#"query all($a: string){ 45 | uids(func: uid($a)) { 46 | uid, 47 | } 48 | }"# 49 | .to_string(); 50 | let mut vars = HashMap::new(); 51 | vars.insert("$a".to_string(), uid.to_string()); 52 | let resp = dgraph.new_readonly_txn().query_with_vars(&query, vars); 53 | let json: UidJson = serde_json::from_slice(&resp.unwrap().json).unwrap(); 54 | 55 | assert_eq!(json.uids[0].uid, uid); 56 | } 57 | 58 | #[test] 59 | fn it_returns_error_if_mandatory_var_is_omitted() { 60 | let dgraph = make_dgraph!(dgraph::new_dgraph_client(common::DGRAPH_URL)); 61 | 62 | let query = r#"query all($a: string!){ 63 | uids(func: eq(name, $a)) { 64 | uid 65 | } 66 | }"# 67 | .to_string(); 68 | let vars = HashMap::new(); 69 | let resp = dgraph.new_readonly_txn().query_with_vars(&query, vars); 70 | 71 | let error_matched = match resp.unwrap_err() { 72 | DgraphError::GrpcError(grpcio::Error::RpcFailure(_)) => true, 73 | _ => false, 74 | }; 75 | assert!(error_matched); 76 | } 77 | 78 | #[test] 79 | fn it_runs_multiple_queries_in_a_single_transaction() { 80 | let dgraph = make_dgraph!(dgraph::new_dgraph_client(common::DGRAPH_URL)); 81 | 82 | let uid = "0x1"; 83 | let query = format!( 84 | r#"{{ 85 | uids(func: uid({})) {{ 86 | uid, 87 | }} 88 | }}"#, 89 | uid 90 | ); 91 | let mut txn = dgraph.new_readonly_txn(); 92 | let resp1 = txn.query(&query); 93 | let resp2 = txn.query(&query); 94 | 95 | assert!(resp1.is_ok()); 96 | assert!(resp2.is_ok()); 97 | } 98 | 99 | #[test] 100 | fn it_commits_a_mutation() { 101 | let dgraph = make_dgraph!(dgraph::new_dgraph_client(common::DGRAPH_URL)); 102 | 103 | let mut txn = dgraph.new_txn(); 104 | let mut mutation = dgraph::Mutation::new(); 105 | 106 | mutation.set_set_json(br#"{"name": "Alice"}"#.to_vec()); 107 | txn.mutate(mutation).unwrap(); 108 | let result = txn.commit(); 109 | 110 | assert_eq!(result.is_ok(), true); 111 | } 112 | 113 | #[test] 114 | fn it_returns_error_if_autocommited_mutation_is_commited_again() { 115 | let dgraph = make_dgraph!(dgraph::new_dgraph_client(common::DGRAPH_URL)); 116 | 117 | let mut txn = dgraph.new_txn(); 118 | let mut mutation = dgraph::Mutation { 119 | commit_now: true, 120 | ..Default::default() 121 | }; 122 | 123 | mutation.set_set_json(br#"{"name": "Alice"}"#.to_vec()); 124 | txn.mutate(mutation).unwrap(); 125 | let result = txn.commit(); 126 | 127 | let error_matched = match result.unwrap_err() { 128 | DgraphError::TxnFinished => true, 129 | _ => false, 130 | }; 131 | assert!(error_matched); 132 | } 133 | 134 | #[test] 135 | fn it_does_not_allow_mutation_in_readonly_transaction() { 136 | let dgraph = make_dgraph!(dgraph::new_dgraph_client(common::DGRAPH_URL)); 137 | 138 | let mut txn = dgraph.new_readonly_txn(); 139 | let mut mutation = dgraph::Mutation::new(); 140 | 141 | mutation.set_set_json(br#"{"name": "Alice"}"#.to_vec()); 142 | let result = txn.mutate(mutation); 143 | 144 | let error_matched = match result.unwrap_err() { 145 | DgraphError::TxnReadOnly => true, 146 | _ => false, 147 | }; 148 | assert!(error_matched); 149 | } 150 | 151 | // #[test] 152 | // fn it_discards_a_transaction() { 153 | // let dgraph = make_dgraph!(dgraph::new_dgraph_client(common::DGRAPH_URL)); 154 | 155 | // let mut txn = dgraph.new_txn(); 156 | // let mut mutation = dgraph::Mutation::new(); 157 | 158 | // mutation.set_set_json(br#"{"name": "Alice"}"#.to_vec()); 159 | // txn.mutate(mutation).unwrap(); 160 | // let result = txn.discard(); 161 | 162 | // assert_eq!(result.is_ok(), true); 163 | // } 164 | 165 | #[test] 166 | fn it_does_nothing_if_autocommited_mutation_is_discarded() { 167 | let dgraph = make_dgraph!(dgraph::new_dgraph_client(common::DGRAPH_URL)); 168 | 169 | let mut txn = dgraph.new_txn(); 170 | let mut mutation = dgraph::Mutation { 171 | commit_now: true, 172 | ..Default::default() 173 | }; 174 | 175 | mutation.set_set_json(br#"{"name": "Alice"}"#.to_vec()); 176 | txn.mutate(mutation).unwrap(); 177 | let result = txn.discard(); 178 | 179 | assert_eq!(result.is_ok(), true); 180 | } 181 | 182 | #[test] 183 | fn it_does_not_commit_discarded_transaction() { 184 | let dgraph = make_dgraph!(dgraph::new_dgraph_client(common::DGRAPH_URL)); 185 | 186 | let mut txn = dgraph.new_txn(); 187 | let mut mutation = dgraph::Mutation::new(); 188 | 189 | mutation.set_set_json(br#"{"name": "Alice"}"#.to_vec()); 190 | txn.mutate(mutation).unwrap(); 191 | let _ = txn.discard(); 192 | let result = txn.commit(); 193 | 194 | let error_matched = match result.unwrap_err() { 195 | DgraphError::TxnFinished => true, 196 | _ => false, 197 | }; 198 | assert!(error_matched); 199 | } 200 | 201 | #[test] 202 | fn it_runs_query_through_do_request() { 203 | let dgraph = make_dgraph!(dgraph::new_dgraph_client(common::DGRAPH_URL)); 204 | 205 | let uid = "0x1"; 206 | let query = format!( 207 | r#"{{ 208 | uids(func: uid({})) {{ 209 | uid, 210 | }} 211 | }}"#, 212 | uid 213 | ) 214 | .to_string(); 215 | let mut request = Request { 216 | query, 217 | ..Default::default() 218 | }; 219 | 220 | let resp = dgraph.new_readonly_txn().do_request(&mut request); 221 | let json: UidJson = serde_json::from_slice(&resp.unwrap().json).unwrap(); 222 | 223 | assert_eq!(json.uids[0].uid, uid); 224 | } 225 | 226 | #[test] 227 | fn it_runs_query_and_mutation_without_variables_through_do_request() { 228 | let dgraph = make_dgraph!(dgraph::new_dgraph_client(common::DGRAPH_URL)); 229 | 230 | let mut txn = dgraph.new_txn(); 231 | let uid = "0x1"; 232 | let query = format!( 233 | r#"{{ 234 | uids(func: uid({})) {{ 235 | uid, 236 | }} 237 | }}"#, 238 | uid 239 | ) 240 | .to_string(); 241 | let mut mutation = dgraph::Mutation::new(); 242 | mutation.set_set_json(br#"{"name": "Alice"}"#.to_vec()); 243 | 244 | let mut request = Request { 245 | query, 246 | mutations: vec![mutation].into(), 247 | ..Default::default() 248 | }; 249 | 250 | txn.do_request(&mut request).expect("Failed to do request."); 251 | let result = txn.commit(); 252 | 253 | assert!(result.is_ok()); 254 | } 255 | 256 | #[test] 257 | fn it_runs_query_and_mutation_through_do_request() { 258 | let dgraph = make_dgraph!(dgraph::new_dgraph_client(common::DGRAPH_URL)); 259 | 260 | let mut txn = dgraph.new_txn(); 261 | let uid = "0x1"; 262 | let query = format!( 263 | r#"{{ 264 | u as var(func: uid({})) {{ 265 | uid, 266 | }} 267 | }}"#, 268 | uid 269 | ) 270 | .to_string(); 271 | let mut mutation = dgraph::Mutation::new(); 272 | mutation.set_set_nquads(br#"uid(u) "Alice" ."#.to_vec()); 273 | 274 | let mut request = Request { 275 | query, 276 | mutations: vec![mutation].into(), 277 | ..Default::default() 278 | }; 279 | 280 | let _ = txn.do_request(&mut request); 281 | let result = txn.commit(); 282 | 283 | assert!(result.is_ok()); 284 | } 285 | --------------------------------------------------------------------------------