Integrate client library into surrealdb
crate (#1514)
This commit is contained in:
parent
54738ea4de
commit
c2dce39f91
142 changed files with 11238 additions and 234 deletions
23
.github/workflows/ci.yml
vendored
23
.github/workflows/ci.yml
vendored
|
@ -15,6 +15,12 @@ jobs:
|
|||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
|
||||
- name: Free disk space
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
large-packages: false
|
||||
swap-storage: false
|
||||
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
|
||||
|
@ -33,7 +39,9 @@ jobs:
|
|||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Run cargo test
|
||||
run: cargo test --workspace
|
||||
run: |
|
||||
(&>/dev/null cargo run -- start --log trace --user root --pass root memory &)
|
||||
cargo test --workspace --features protocol-ws,protocol-http,kv-rocksdb
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
|
@ -57,6 +65,7 @@ jobs:
|
|||
- name: Install stable toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: wasm32-unknown-unknown
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Run cargo check --package surrealdb
|
||||
|
@ -70,3 +79,15 @@ jobs:
|
|||
|
||||
- name: Run cargo clippy
|
||||
run: cargo clippy -- -W warnings
|
||||
|
||||
- name: Run native cargo check --package surrealdb
|
||||
run: cargo check --no-default-features --package surrealdb
|
||||
|
||||
- name: Run Wasm cargo check --package surrealdb
|
||||
run: cargo check --features protocol-ws,protocol-http,kv-mem --target wasm32-unknown-unknown --package surrealdb
|
||||
|
||||
- name: Check IndxDB
|
||||
run: cargo check --no-default-features --features kv-indxdb --target wasm32-unknown-unknown --package surrealdb
|
||||
|
||||
- name: Run cargo check --workspace
|
||||
run: cargo check --workspace
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -27,6 +27,7 @@ Temporary Items
|
|||
|
||||
**/*.rs.bk
|
||||
*.db
|
||||
*.sw?
|
||||
|
||||
# -----------------------------------
|
||||
# Folders
|
||||
|
|
643
Cargo.lock
generated
643
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -6,7 +6,8 @@ version = "1.0.0-beta.8"
|
|||
authors = ["Tobie Morgan Hitchcock <tobie@surrealdb.com>"]
|
||||
|
||||
[features]
|
||||
default = ["storage-rocksdb", "scripting", "http"]
|
||||
default = ["storage-mem", "storage-rocksdb", "scripting", "http"]
|
||||
storage-mem = ["surrealdb/kv-mem"]
|
||||
storage-rocksdb = ["surrealdb/kv-rocksdb"]
|
||||
storage-tikv = ["surrealdb/kv-tikv"]
|
||||
storage-fdb = ["surrealdb/kv-fdb-6_3"]
|
||||
|
@ -14,7 +15,7 @@ scripting = ["surrealdb/scripting"]
|
|||
http = ["surrealdb/http"]
|
||||
|
||||
[workspace]
|
||||
members = ["lib"]
|
||||
members = ["lib", "lib/examples/actix"]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
@ -43,7 +44,7 @@ serde = { version = "1.0.145", features = ["derive"] }
|
|||
serde_cbor = "0.11.2"
|
||||
serde_json = "1.0.85"
|
||||
serde_pack = { version = "1.1.1", package = "rmp-serde" }
|
||||
surrealdb = { path = "lib", default-features = false, features = ["kv-mem", "parallel"] }
|
||||
surrealdb = { path = "lib", default-features = false }
|
||||
thiserror = "1.0.37"
|
||||
tokio = { version = "1.21.2", features = ["macros", "signal"] }
|
||||
warp = { version = "0.3.3", features = ["compression", "tls", "websocket"] }
|
||||
|
|
2
Makefile
2
Makefile
|
@ -10,7 +10,7 @@ setup:
|
|||
|
||||
.PHONY: docs
|
||||
docs:
|
||||
cargo doc --open --no-deps --package surrealdb
|
||||
cargo doc --open --no-deps --package surrealdb --features rustls,protocol-ws,protocol-http,kv-mem,kv-indxdb,kv-rocksdb,kv-tikv,http,scripting,parallel
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
|
|
|
@ -3,6 +3,7 @@ name = "surrealdb"
|
|||
publish = true
|
||||
edition = "2021"
|
||||
version = "1.0.0-beta.8"
|
||||
rust-version = "1.65.0"
|
||||
readme = "CARGO.md"
|
||||
authors = ["Tobie Morgan Hitchcock <tobie@surrealdb.com>"]
|
||||
description = "A scalable, distributed, collaborative, document-graph database, for the realtime web"
|
||||
|
@ -12,27 +13,42 @@ documentation = "https://docs.rs/surrealdb/"
|
|||
keywords = ["database", "embedded-database", "key-value", "key-value-store", "kv-store"]
|
||||
categories = ["database-implementations", "data-structures", "embedded"]
|
||||
license = "Apache-2.0"
|
||||
resolver = "2"
|
||||
|
||||
[features]
|
||||
# Public features
|
||||
default = ["parallel", "kv-mem", "kv-rocksdb", "scripting", "http"]
|
||||
parallel = ["dep:executor"]
|
||||
kv-mem = ["dep:echodb"]
|
||||
default = ["protocol-ws", "rustls"]
|
||||
protocol-http = ["dep:reqwest", "dep:tokio-util"]
|
||||
protocol-ws = ["dep:tokio-tungstenite", "dep:tokio-stream", "tokio/time"]
|
||||
kv-mem = ["dep:echodb", "parallel"]
|
||||
kv-indxdb = ["dep:indxdb"]
|
||||
kv-rocksdb = ["dep:rocksdb"]
|
||||
kv-tikv = ["dep:tikv"]
|
||||
kv-fdb-5_1 = ["foundationdb/fdb-5_1", "kv-fdb"]
|
||||
kv-fdb-5_2 = ["foundationdb/fdb-5_2", "kv-fdb"]
|
||||
kv-fdb-6_0 = ["foundationdb/fdb-6_0", "kv-fdb"]
|
||||
kv-fdb-6_1 = ["foundationdb/fdb-6_1", "kv-fdb"]
|
||||
kv-fdb-6_2 = ["foundationdb/fdb-6_2", "kv-fdb"]
|
||||
kv-fdb-6_3 = ["foundationdb/fdb-6_3", "kv-fdb"]
|
||||
kv-fdb-7_0 = ["foundationdb/fdb-7_0", "kv-fdb"]
|
||||
kv-fdb-7_1 = ["foundationdb/fdb-7_1", "kv-fdb"]
|
||||
kv-rocksdb = ["dep:rocksdb", "parallel"]
|
||||
kv-tikv = ["dep:tikv", "parallel"]
|
||||
kv-fdb-5_1 = ["foundationdb/fdb-5_1", "kv-fdb", "parallel"]
|
||||
kv-fdb-5_2 = ["foundationdb/fdb-5_2", "kv-fdb", "parallel"]
|
||||
kv-fdb-6_0 = ["foundationdb/fdb-6_0", "kv-fdb", "parallel"]
|
||||
kv-fdb-6_1 = ["foundationdb/fdb-6_1", "kv-fdb", "parallel"]
|
||||
kv-fdb-6_2 = ["foundationdb/fdb-6_2", "kv-fdb", "parallel"]
|
||||
kv-fdb-6_3 = ["foundationdb/fdb-6_3", "kv-fdb", "parallel"]
|
||||
kv-fdb-7_0 = ["foundationdb/fdb-7_0", "kv-fdb", "parallel"]
|
||||
kv-fdb-7_1 = ["foundationdb/fdb-7_1", "kv-fdb", "parallel"]
|
||||
scripting = ["dep:js", "dep:executor"]
|
||||
http = ["dep:reqwest"]
|
||||
native-tls = ["dep:native-tls", "reqwest?/native-tls", "tokio-tungstenite?/native-tls"]
|
||||
rustls = ["dep:rustls", "reqwest?/rustls-tls", "tokio-tungstenite?/__rustls-tls"]
|
||||
# Private features
|
||||
kv-fdb = ["foundationdb"]
|
||||
kv-fdb = ["foundationdb", "parallel"]
|
||||
parallel = ["dep:executor"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
features = [
|
||||
"protocol-ws", "protocol-http",
|
||||
"kv-mem", "kv-indxdb", "kv-rocksdb", "kv-tikv", "kv-fdb",
|
||||
"rustls", "native-tls",
|
||||
"http", "scripting"
|
||||
]
|
||||
targets = []
|
||||
|
||||
[dependencies]
|
||||
addr = { version = "0.15.6", default-features = false, features = ["std"] }
|
||||
|
@ -47,10 +63,13 @@ deunicode = "1.3.2"
|
|||
dmp = "0.1.1"
|
||||
echodb = { version = "0.3.0", optional = true }
|
||||
executor = { version = "1.4.1", package = "async-executor", optional = true }
|
||||
flume = "0.10.14"
|
||||
futures = "0.3.24"
|
||||
futures-concurrency = "7.0.0"
|
||||
foundationdb = { version = "0.7.0", default-features = false, features = ["embedded-fdb-include"], optional = true }
|
||||
fuzzy-matcher = "0.3.7"
|
||||
geo = { version = "0.23.0", features = ["use-serde"] }
|
||||
indexmap = { version = "1.9.2", features = ["serde"] }
|
||||
indxdb = { version = "0.2.0", optional = true }
|
||||
js = { version = "0.1.7", package = "rquickjs", features = ["bindgen", "classes", "futures", "loader", "macro", "properties", "parallel"], optional = true }
|
||||
lexical-sort = "0.3.1"
|
||||
|
@ -58,29 +77,42 @@ log = "0.4.17"
|
|||
md-5 = "0.10.5"
|
||||
msgpack = { version = "1.1.1", package = "rmp-serde" }
|
||||
nanoid = "0.4.0"
|
||||
native-tls = { version = "0.2.11", optional = true }
|
||||
nom = { version = "7.1.1", features = ["alloc"] }
|
||||
once_cell = "1.15.0"
|
||||
once_cell = "1.16.0"
|
||||
pbkdf2 = "0.11.0"
|
||||
rand = "0.8.5"
|
||||
regex = "1.6.0"
|
||||
reqwest = { version = "0.11.13", default-features = false, features = ["json", "stream"], optional = true }
|
||||
rocksdb = { version = "0.19.0", optional = true }
|
||||
rustls = { version = "0.20.7", optional = true }
|
||||
scrypt = "0.10.0"
|
||||
semver = { version = "1.0.14", default-features = false }
|
||||
serde = { version = "1.0.145", features = ["derive"] }
|
||||
semver = { version = "1.0.14", features = ["serde"] }
|
||||
serde = { version = "1.0.148", features = ["derive"] }
|
||||
serde_json = "1.0.89"
|
||||
sha-1 = "0.10.0"
|
||||
sha2 = "0.10.6"
|
||||
storekey = "0.3.0"
|
||||
thiserror = "1.0.37"
|
||||
tikv = { version = "0.1.0", package = "tikv-client", optional = true }
|
||||
tokio-stream = { version = "0.1.11", optional = true }
|
||||
tokio-util = { version = "0.7.4", optional = true, features = ["compat"] }
|
||||
trice = "0.1.0"
|
||||
url = "2.3.1"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.21.2", features = ["macros", "rt", "rt-multi-thread"] }
|
||||
time = { version = "0.3.17", features = ["serde"] }
|
||||
tokio = { version = "1.22.0", features = ["macros", "rt", "rt-multi-thread"] }
|
||||
ulid = { version = "1.0.0", features = ["serde"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
pharos = "0.5.3"
|
||||
tokio = { version = "1.22.0", default-features = false, features = ["rt"] }
|
||||
uuid = { version = "1.2.1", features = ["serde", "js", "v4", "v7"] }
|
||||
wasm-bindgen-futures = "0.4.33"
|
||||
ws_stream_wasm = "0.7.3"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tokio = { version = "1.22.0", default-features = false, features = ["io-util", "fs", "rt-multi-thread"] }
|
||||
tokio-tungstenite = { version = "0.17.2", optional = true }
|
||||
uuid = { version = "1.2.1", features = ["serde", "v4", "v7"] }
|
||||
|
|
146
lib/README.md
Normal file
146
lib/README.md
Normal file
|
@ -0,0 +1,146 @@
|
|||
# surrealdb
|
||||
|
||||
The official SurrealDB library for Rust.
|
||||
|
||||
[![](https://img.shields.io/badge/status-beta-ff00bb.svg?style=flat-square)](https://github.com/surrealdb/surrealdb) [![](https://img.shields.io/badge/docs-view-44cc11.svg?style=flat-square)](https://surrealdb.com/docs/integration/libraries/rust) [![](https://img.shields.io/badge/license-Apache_License_2.0-00bfff.svg?style=flat-square)](https://github.com/surrealdb/surrealdb)
|
||||
|
||||
<h2><img height="20" src="https://github.com/surrealdb/surrealdb/blob/main/img/whatissurreal.svg?raw=true"> What is SurrealDB?</h2>
|
||||
|
||||
SurrealDB is an end-to-end cloud native database for web, mobile, serverless, jamstack, backend, and traditional applications. SurrealDB reduces the development time of modern applications by simplifying your database and API stack, removing the need for most server-side components, allowing you to build secure, performant apps quicker and cheaper. SurrealDB acts as both a database and a modern, realtime, collaborative API backend layer. SurrealDB can run as a single server or in a highly-available, highly-scalable distributed mode - with support for SQL querying from client devices, GraphQL, ACID transactions, WebSocket connections, structured and unstructured data, graph querying, full-text indexing, geospatial querying, and row-by-row permissions-based access.
|
||||
|
||||
View the [features](https://surrealdb.com/features), the latest [releases](https://surrealdb.com/releases), the product [roadmap](https://surrealdb.com/roadmap), and [documentation](https://surrealdb.com/docs).
|
||||
|
||||
<h2><img height="20" src="https://github.com/surrealdb/surrealdb/blob/main/img/features.svg?raw=true"> Features</h2>
|
||||
|
||||
- [x] Can be used as an embedded database (`Surreal<Db>`)
|
||||
- [x] Connects to remote servers (`Surreal<ws::Client>` or `Surreal<http::Client>`)
|
||||
- [x] Allows picking any protocol or storage engine at run-time (`Surreal<Any>`)
|
||||
- [x] Compiles to WebAssembly
|
||||
- [x] Supports typed SQL statements
|
||||
- [x] Invalid SQL queries are never sent to the server, the client uses the same parser the server uses
|
||||
- [x] Static clients, no need for `once_cell` or `lazy_static`
|
||||
- [x] Clonable connections with auto-reconnect capabilities, no need for a connection pool
|
||||
- [x] Range queries
|
||||
- [x] Consistent API across all supported protocols and storage engines
|
||||
- [x] Asynchronous, lock-free connections
|
||||
- [x] TLS support via either [`rustls`](https://crates.io/crates/rustls) or [`native-tls`](https://crates.io/crates/native-tls)
|
||||
|
||||
<h2><img height="20" src="https://github.com/surrealdb/surrealdb/blob/main/img/installation.svg?raw=true"> Installation</h2>
|
||||
|
||||
To add this crate as a Rust dependency, simply run
|
||||
|
||||
```bash
|
||||
cargo add surrealdb
|
||||
```
|
||||
|
||||
**IMPORTANT**: The client supports SurrealDB `v1.0.0-beta.8+20221030.c12a1cc` or later. So please make sure you have that or a newer version of the server before proceeding. For now, that means a recent nightly version.
|
||||
|
||||
<h2><img height="20" src="https://github.com/surrealdb/surrealdb/blob/main/img/features.svg?raw=true"> Quick look</h2>
|
||||
|
||||
This library enables simple and advanced querying of an embedded or remote database from server-side or client-side (via Wasm) code. By default, all remote connections to SurrealDB are made over WebSockets, and automatically reconnect when the connection is terminated. Connections are automatically closed when they get dropped.
|
||||
|
||||
```rust
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::borrow::Cow;
|
||||
use surrealdb::sql;
|
||||
use surrealdb::Surreal;
|
||||
use surrealdb::engines::remote::ws::Ws;
|
||||
use surrealdb::opt::auth::Root;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Name {
|
||||
first: Cow<'static, str>,
|
||||
last: Cow<'static, str>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Person {
|
||||
#[serde(skip_serializing)]
|
||||
id: Option<String>,
|
||||
title: Cow<'static, str>,
|
||||
name: Name,
|
||||
marketing: bool,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> surrealdb::Result<()> {
|
||||
let db = Surreal::new::<Ws>("localhost:8000").await?;
|
||||
|
||||
// Signin as a namespace, database, or root user
|
||||
db.signin(Root {
|
||||
username: "root",
|
||||
password: "root",
|
||||
})
|
||||
.await?;
|
||||
|
||||
// Select a specific namespace and database
|
||||
db.use_ns("namespace").use_db("database").await?;
|
||||
|
||||
// Create a new person with a random ID
|
||||
let tobie: Person = db
|
||||
.create("person")
|
||||
.content(Person {
|
||||
id: None,
|
||||
title: "Founder & CEO".into(),
|
||||
name: Name {
|
||||
first: "Tobie".into(),
|
||||
last: "Morgan Hitchcock".into(),
|
||||
},
|
||||
marketing: true,
|
||||
})
|
||||
.await?;
|
||||
|
||||
assert!(tobie.id.is_some());
|
||||
|
||||
// Create a new person with a specific ID
|
||||
let mut jaime: Person = db
|
||||
.create(("person", "jaime"))
|
||||
.content(Person {
|
||||
id: None,
|
||||
title: "Founder & COO".into(),
|
||||
name: Name {
|
||||
first: "Jaime".into(),
|
||||
last: "Morgan Hitchcock".into(),
|
||||
},
|
||||
marketing: false,
|
||||
})
|
||||
.await?;
|
||||
|
||||
assert_eq!(jaime.id.unwrap(), "person:jaime");
|
||||
|
||||
// Update a person record with a specific ID
|
||||
jaime = db
|
||||
.update(("person", "jaime"))
|
||||
.merge(json!({ "marketing": true }))
|
||||
.await?;
|
||||
|
||||
assert!(jaime.marketing);
|
||||
|
||||
// Select all people records
|
||||
let people: Vec<Person> = db.select("person").await?;
|
||||
|
||||
assert!(!people.is_empty());
|
||||
|
||||
// Perform a custom advanced query
|
||||
let sql = sql! {
|
||||
SELECT marketing, count()
|
||||
FROM type::table($table)
|
||||
GROUP BY marketing
|
||||
};
|
||||
|
||||
let groups = db.query(sql)
|
||||
.bind(("table", "person"))
|
||||
.await?;
|
||||
|
||||
dbg!(groups);
|
||||
|
||||
// Delete all people upto but not including Jaime
|
||||
db.delete("person").range(.."jaime").await?;
|
||||
|
||||
// Delete all people
|
||||
db.delete("person").await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
11
lib/examples/actix/Cargo.toml
Normal file
11
lib/examples/actix/Cargo.toml
Normal file
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "actix-example"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
actix-web = { version = "4.2.1", features = ["macros"] }
|
||||
serde = { version = "1.0.147", features = ["derive"] }
|
||||
surrealdb = { path = "../.." }
|
||||
thiserror = "1.0.37"
|
23
lib/examples/actix/src/error.rs
Normal file
23
lib/examples/actix/src/error.rs
Normal file
|
@ -0,0 +1,23 @@
|
|||
use actix_web::{HttpResponse, ResponseError};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("database error")]
|
||||
Db,
|
||||
}
|
||||
|
||||
impl ResponseError for Error {
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
match self {
|
||||
Error::Db => HttpResponse::InternalServerError().body(self.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<surrealdb::Error> for Error {
|
||||
fn from(error: surrealdb::Error) -> Self {
|
||||
eprintln!("{error}");
|
||||
Self::Db
|
||||
}
|
||||
}
|
37
lib/examples/actix/src/main.rs
Normal file
37
lib/examples/actix/src/main.rs
Normal file
|
@ -0,0 +1,37 @@
|
|||
mod error;
|
||||
mod person;
|
||||
|
||||
use actix_web::{App, HttpServer};
|
||||
use surrealdb::engines::remote::ws::Client;
|
||||
use surrealdb::engines::remote::ws::Ws;
|
||||
use surrealdb::opt::auth::Root;
|
||||
use surrealdb::Surreal;
|
||||
|
||||
static DB: Surreal<Client> = Surreal::init();
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
DB.connect::<Ws>("localhost:8000").await?;
|
||||
|
||||
DB.signin(Root {
|
||||
username: "root",
|
||||
password: "root",
|
||||
})
|
||||
.await?;
|
||||
|
||||
DB.use_ns("namespace").use_db("database").await?;
|
||||
|
||||
HttpServer::new(|| {
|
||||
App::new()
|
||||
.service(person::create)
|
||||
.service(person::read)
|
||||
.service(person::update)
|
||||
.service(person::delete)
|
||||
.service(person::list)
|
||||
})
|
||||
.bind(("localhost", 8080))?
|
||||
.run()
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
44
lib/examples/actix/src/person.rs
Normal file
44
lib/examples/actix/src/person.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
use crate::error::Error;
|
||||
use crate::DB;
|
||||
use actix_web::web::Json;
|
||||
use actix_web::web::Path;
|
||||
use actix_web::{delete, get, post, put};
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
const PERSON: &str = "person";
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Person {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[post("/person/{id}")]
|
||||
pub async fn create(id: Path<String>, person: Json<Person>) -> Result<Json<Person>, Error> {
|
||||
let person = DB.create((PERSON, &*id)).content(person).await?;
|
||||
Ok(Json(person))
|
||||
}
|
||||
|
||||
#[get("/person/{id}")]
|
||||
pub async fn read(id: Path<String>) -> Result<Json<Option<Person>>, Error> {
|
||||
let person = DB.select((PERSON, &*id)).await?;
|
||||
Ok(Json(person))
|
||||
}
|
||||
|
||||
#[put("/person/{id}")]
|
||||
pub async fn update(id: Path<String>, person: Json<Person>) -> Result<Json<Person>, Error> {
|
||||
let person = DB.update((PERSON, &*id)).content(person).await?;
|
||||
Ok(Json(person))
|
||||
}
|
||||
|
||||
#[delete("/person/{id}")]
|
||||
pub async fn delete(id: Path<String>) -> Result<Json<()>, Error> {
|
||||
DB.delete((PERSON, &*id)).await?;
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
#[get("/people")]
|
||||
pub async fn list() -> Result<Json<Vec<Person>>, Error> {
|
||||
let people = DB.select(PERSON).await?;
|
||||
Ok(Json(people))
|
||||
}
|
37
lib/examples/concurrency/main.rs
Normal file
37
lib/examples/concurrency/main.rs
Normal file
|
@ -0,0 +1,37 @@
|
|||
use surrealdb::engines::remote::ws::Client;
|
||||
use surrealdb::engines::remote::ws::Ws;
|
||||
use surrealdb::Surreal;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
static DB: Surreal<Client> = Surreal::init();
|
||||
|
||||
const NUM: usize = 100_000;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> surrealdb::Result<()> {
|
||||
DB.connect::<Ws>("localhost:8000").with_capacity(NUM).await?;
|
||||
|
||||
DB.use_ns("namespace").use_db("database").await?;
|
||||
|
||||
let (tx, mut rx) = mpsc::channel::<()>(1);
|
||||
|
||||
for idx in 0..NUM {
|
||||
let sender = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut result = DB.query("SELECT * FROM $idx").bind(("idx", idx)).await.unwrap();
|
||||
|
||||
let db_idx: Option<usize> = result.take(0).unwrap();
|
||||
if let Some(db_idx) = db_idx {
|
||||
println!("{idx}: {db_idx}");
|
||||
}
|
||||
|
||||
drop(sender);
|
||||
});
|
||||
}
|
||||
|
||||
drop(tx);
|
||||
|
||||
rx.recv().await;
|
||||
|
||||
Ok(())
|
||||
}
|
54
lib/examples/query/main.rs
Normal file
54
lib/examples/query/main.rs
Normal file
|
@ -0,0 +1,54 @@
|
|||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use surrealdb::engines::remote::ws::Ws;
|
||||
use surrealdb::opt::auth::Root;
|
||||
use surrealdb::sql;
|
||||
use surrealdb::Surreal;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
struct User {
|
||||
id: String,
|
||||
name: String,
|
||||
company: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> surrealdb::Result<()> {
|
||||
let db = Surreal::new::<Ws>("localhost:8000").await?;
|
||||
|
||||
db.signin(Root {
|
||||
username: "root",
|
||||
password: "root",
|
||||
})
|
||||
.await?;
|
||||
|
||||
db.use_ns("namespace").use_db("database").await?;
|
||||
|
||||
let sql = sql! {
|
||||
CREATE user
|
||||
SET name = $name,
|
||||
company = $company
|
||||
};
|
||||
|
||||
let mut results = db
|
||||
.query(sql)
|
||||
.bind(User {
|
||||
id: "john".to_owned(),
|
||||
name: "John Doe".to_owned(),
|
||||
company: "ACME Corporation".to_owned(),
|
||||
})
|
||||
.await?;
|
||||
|
||||
// print the created user:
|
||||
let user: Option<User> = results.take(0)?;
|
||||
println!("{user:?}");
|
||||
|
||||
let mut response = db.query(sql!(SELECT * FROM user WHERE name.first = "John")).await?;
|
||||
|
||||
// print all users:
|
||||
let users: Vec<User> = response.take(0)?;
|
||||
println!("{users:?}");
|
||||
|
||||
Ok(())
|
||||
}
|
32
lib/examples/select/main.rs
Normal file
32
lib/examples/select/main.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
use serde::Deserialize;
|
||||
use surrealdb::engines::remote::ws::Ws;
|
||||
use surrealdb::opt::auth::Root;
|
||||
use surrealdb::Surreal;
|
||||
|
||||
const ACCOUNT: &str = "account";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
struct Account {
|
||||
id: String,
|
||||
balance: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> surrealdb::Result<()> {
|
||||
let db = Surreal::new::<Ws>("localhost:8000").await?;
|
||||
|
||||
db.signin(Root {
|
||||
username: "root",
|
||||
password: "root",
|
||||
})
|
||||
.await?;
|
||||
|
||||
db.use_ns("namespace").use_db("database").await?;
|
||||
|
||||
let accounts: Vec<Account> = db.select(ACCOUNT).range("one".."two").await?;
|
||||
|
||||
println!("{accounts:?}");
|
||||
|
||||
Ok(())
|
||||
}
|
45
lib/examples/transaction/main.rs
Normal file
45
lib/examples/transaction/main.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
use surrealdb::engines::remote::ws::Ws;
|
||||
use surrealdb::opt::auth::Root;
|
||||
use surrealdb::sql::statements::BeginStatement;
|
||||
use surrealdb::sql::statements::CommitStatement;
|
||||
use surrealdb::Surreal;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> surrealdb::Result<()> {
|
||||
let db = Surreal::new::<Ws>("localhost:8000").await?;
|
||||
|
||||
db.signin(Root {
|
||||
username: "root",
|
||||
password: "root",
|
||||
})
|
||||
.await?;
|
||||
|
||||
db.use_ns("namespace").use_db("database").await?;
|
||||
|
||||
#[rustfmt::skip]
|
||||
let response = db
|
||||
|
||||
// Start transaction
|
||||
.query(BeginStatement)
|
||||
|
||||
// Setup accounts
|
||||
.query("
|
||||
CREATE account:one SET balance = 135605.16;
|
||||
CREATE account:two SET balance = 91031.31;
|
||||
")
|
||||
|
||||
// Move money
|
||||
.query("
|
||||
UPDATE account:one SET balance += 300.00;
|
||||
UPDATE account:two SET balance -= 300.00;
|
||||
")
|
||||
|
||||
// Finalise
|
||||
.query(CommitStatement)
|
||||
.await?;
|
||||
|
||||
// See if any errors were returned
|
||||
response.check()?;
|
||||
|
||||
Ok(())
|
||||
}
|
13
lib/examples/version/main.rs
Normal file
13
lib/examples/version/main.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use surrealdb::engines::remote::ws::Ws;
|
||||
use surrealdb::Surreal;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> surrealdb::Result<()> {
|
||||
let db = Surreal::new::<Ws>("localhost:8000").await?;
|
||||
|
||||
let version = db.version().await?;
|
||||
|
||||
println!("{version:?}");
|
||||
|
||||
Ok(())
|
||||
}
|
215
lib/src/api/conn.rs
Normal file
215
lib/src/api/conn.rs
Normal file
|
@ -0,0 +1,215 @@
|
|||
use crate::api;
|
||||
use crate::api::method::query::Response;
|
||||
use crate::api::opt::Endpoint;
|
||||
use crate::api::ExtraFeatures;
|
||||
use crate::api::Result;
|
||||
use crate::api::Surreal;
|
||||
use crate::sql::Query;
|
||||
use crate::sql::Value;
|
||||
use flume::Receiver;
|
||||
use flume::Sender;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashSet;
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::path::PathBuf;
|
||||
use std::pin::Pin;
|
||||
use std::sync::atomic::AtomicI64;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)] // used by the embedded and remote connections
|
||||
pub(crate) struct Route {
|
||||
pub(crate) request: (i64, Method, Param),
|
||||
pub(crate) response: Sender<Result<DbResponse>>,
|
||||
}
|
||||
|
||||
/// Message router
|
||||
#[derive(Debug)]
|
||||
pub struct Router<C: api::Connection> {
|
||||
pub(crate) conn: PhantomData<C>,
|
||||
pub(crate) sender: Sender<Option<Route>>,
|
||||
pub(crate) last_id: AtomicI64,
|
||||
pub(crate) features: HashSet<ExtraFeatures>,
|
||||
}
|
||||
|
||||
impl<C> Router<C>
|
||||
where
|
||||
C: api::Connection,
|
||||
{
|
||||
pub(crate) fn next_id(&self) -> i64 {
|
||||
self.last_id.fetch_add(1, Ordering::SeqCst)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> Drop for Router<C>
|
||||
where
|
||||
C: api::Connection,
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
let _res = self.sender.send(None);
|
||||
}
|
||||
}
|
||||
|
||||
/// The query method
|
||||
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, Hash)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Method {
|
||||
/// Sends an authentication token to the server
|
||||
Authenticate,
|
||||
/// Perfoms a merge update operation
|
||||
Merge,
|
||||
/// Creates a record in a table
|
||||
Create,
|
||||
/// Deletes a record from a table
|
||||
Delete,
|
||||
/// Exports a database
|
||||
Export,
|
||||
/// Checks the health of the server
|
||||
Health,
|
||||
/// Imports a database
|
||||
Import,
|
||||
/// Invalidates a session
|
||||
Invalidate,
|
||||
/// Kills a live query
|
||||
#[doc(hidden)] // Not supported yet
|
||||
Kill,
|
||||
/// Starts a live query
|
||||
#[doc(hidden)] // Not supported yet
|
||||
Live,
|
||||
/// Perfoms a patch update operation
|
||||
Patch,
|
||||
/// Sends a raw query to the database
|
||||
Query,
|
||||
/// Selects a record or records from a table
|
||||
Select,
|
||||
/// Sets a parameter on the connection
|
||||
Set,
|
||||
/// Signs into the server
|
||||
Signin,
|
||||
/// Signs up on the server
|
||||
Signup,
|
||||
/// Removes a parameter from a connection
|
||||
Unset,
|
||||
/// Perfoms an update operation
|
||||
Update,
|
||||
/// Selects a namespace and database to use
|
||||
Use,
|
||||
/// Queries the version of the server
|
||||
Version,
|
||||
}
|
||||
|
||||
/// The database response sent from the router to the caller
|
||||
#[derive(Debug)]
|
||||
pub enum DbResponse {
|
||||
/// The response sent for the `query` method
|
||||
Query(Response),
|
||||
/// The response sent for any method except `query`
|
||||
Other(Value),
|
||||
}
|
||||
|
||||
/// Holds the parameters given to the caller
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)] // used by the embedded and remote connections
|
||||
pub struct Param {
|
||||
pub(crate) query: Option<(Query, BTreeMap<String, Value>)>,
|
||||
pub(crate) other: Vec<Value>,
|
||||
pub(crate) file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Param {
|
||||
pub(crate) fn new(other: Vec<Value>) -> Self {
|
||||
Self {
|
||||
other,
|
||||
query: None,
|
||||
file: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn query(query: Query, bindings: BTreeMap<String, Value>) -> Self {
|
||||
Self {
|
||||
query: Some((query, bindings)),
|
||||
other: Vec::new(),
|
||||
file: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn file(file: PathBuf) -> Self {
|
||||
Self {
|
||||
query: None,
|
||||
other: Vec::new(),
|
||||
file: Some(file),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Connection trait implemented by supported protocols
|
||||
pub trait Connection: Sized + Send + Sync + 'static {
|
||||
/// Constructs a new client without connecting to the server
|
||||
fn new(method: Method) -> Self;
|
||||
|
||||
/// Connect to the server
|
||||
fn connect(
|
||||
address: Endpoint,
|
||||
capacity: usize,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Surreal<Self>>> + Send + Sync + 'static>>
|
||||
where
|
||||
Self: api::Connection;
|
||||
|
||||
/// Send a query to the server
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn send<'r>(
|
||||
&'r mut self,
|
||||
router: &'r Router<Self>,
|
||||
param: Param,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Receiver<Result<DbResponse>>>> + Send + Sync + 'r>>
|
||||
where
|
||||
Self: api::Connection;
|
||||
|
||||
/// Receive responses for all methods except `query`
|
||||
fn recv<R>(
|
||||
&mut self,
|
||||
receiver: Receiver<Result<DbResponse>>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<R>> + Send + Sync + '_>>
|
||||
where
|
||||
R: DeserializeOwned;
|
||||
|
||||
/// Receive the response of the `query` method
|
||||
fn recv_query(
|
||||
&mut self,
|
||||
receiver: Receiver<Result<DbResponse>>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Response>> + Send + Sync + '_>>;
|
||||
|
||||
/// Execute all methods except `query`
|
||||
fn execute<'r, R>(
|
||||
&'r mut self,
|
||||
router: &'r Router<Self>,
|
||||
param: Param,
|
||||
) -> Pin<Box<dyn Future<Output = Result<R>> + Send + Sync + 'r>>
|
||||
where
|
||||
R: DeserializeOwned,
|
||||
Self: api::Connection,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let rx = self.send(router, param).await?;
|
||||
self.recv(rx).await
|
||||
})
|
||||
}
|
||||
|
||||
/// Execute the `query` method
|
||||
fn execute_query<'r>(
|
||||
&'r mut self,
|
||||
router: &'r Router<Self>,
|
||||
param: Param,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Response>> + Send + Sync + 'r>>
|
||||
where
|
||||
Self: api::Connection,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let rx = self.send(router, param).await?;
|
||||
self.recv_query(rx).await
|
||||
})
|
||||
}
|
||||
}
|
303
lib/src/api/engines/any/mod.rs
Normal file
303
lib/src/api/engines/any/mod.rs
Normal file
|
@ -0,0 +1,303 @@
|
|||
//! Dynamic support for any engine
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use serde::{Serialize, Deserialize};
|
||||
//! use serde_json::json;
|
||||
//! use std::borrow::Cow;
|
||||
//! use surrealdb::sql;
|
||||
//! use surrealdb::engines::any::connect;
|
||||
//! use surrealdb::opt::auth::Root;
|
||||
//!
|
||||
//! #[derive(Serialize, Deserialize)]
|
||||
//! struct Name {
|
||||
//! first: Cow<'static, str>,
|
||||
//! last: Cow<'static, str>,
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(Serialize, Deserialize)]
|
||||
//! struct Person {
|
||||
//! title: Cow<'static, str>,
|
||||
//! name: Name,
|
||||
//! marketing: bool,
|
||||
//! }
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() -> surrealdb::Result<()> {
|
||||
//! let db = connect("ws://localhost:8000").await?;
|
||||
//!
|
||||
//! // Signin as a namespace, database, or root user
|
||||
//! db.signin(Root {
|
||||
//! username: "root",
|
||||
//! password: "root",
|
||||
//! }).await?;
|
||||
//!
|
||||
//! // Select a specific namespace / database
|
||||
//! db.use_ns("namespace").use_db("database").await?;
|
||||
//!
|
||||
//! // Create a new person with a random ID
|
||||
//! let created: Person = db.create("person")
|
||||
//! .content(Person {
|
||||
//! title: "Founder & CEO".into(),
|
||||
//! name: Name {
|
||||
//! first: "Tobie".into(),
|
||||
//! last: "Morgan Hitchcock".into(),
|
||||
//! },
|
||||
//! marketing: true,
|
||||
//! })
|
||||
//! .await?;
|
||||
//!
|
||||
//! // Create a new person with a specific ID
|
||||
//! let created: Person = db.create(("person", "jaime"))
|
||||
//! .content(Person {
|
||||
//! title: "Founder & COO".into(),
|
||||
//! name: Name {
|
||||
//! first: "Jaime".into(),
|
||||
//! last: "Morgan Hitchcock".into(),
|
||||
//! },
|
||||
//! marketing: false,
|
||||
//! })
|
||||
//! .await?;
|
||||
//!
|
||||
//! // Update a person record with a specific ID
|
||||
//! let updated: Person = db.update(("person", "jaime"))
|
||||
//! .merge(json!({"marketing": true}))
|
||||
//! .await?;
|
||||
//!
|
||||
//! // Select all people records
|
||||
//! let people: Vec<Person> = db.select("person").await?;
|
||||
//!
|
||||
//! // Perform a custom advanced query
|
||||
//! let sql = sql! {
|
||||
//! SELECT marketing, count()
|
||||
//! FROM type::table($table)
|
||||
//! GROUP BY marketing
|
||||
//! };
|
||||
//!
|
||||
//! let groups = db.query(sql)
|
||||
//! .bind(("table", "person"))
|
||||
//! .await?;
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod native;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod wasm;
|
||||
|
||||
use crate::api::conn::Method;
|
||||
use crate::api::err::Error;
|
||||
use crate::api::opt::Endpoint;
|
||||
#[cfg(any(
|
||||
feature = "kv-mem",
|
||||
feature = "kv-tikv",
|
||||
feature = "kv-rocksdb",
|
||||
feature = "kv-fdb",
|
||||
feature = "kv-indxdb",
|
||||
))]
|
||||
use crate::api::opt::Strict;
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
use crate::api::opt::Tls;
|
||||
use crate::api::Connect;
|
||||
use crate::api::Result;
|
||||
use crate::api::Surreal;
|
||||
use std::marker::PhantomData;
|
||||
use url::Url;
|
||||
|
||||
/// A trait for converting inputs to a server address object
|
||||
pub trait IntoEndpoint {
|
||||
/// Converts an input into a server address object
|
||||
fn into_endpoint(self) -> Result<Endpoint>;
|
||||
}
|
||||
|
||||
impl IntoEndpoint for &str {
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse(self).map_err(|_| Error::InvalidUrl(self.to_owned()))?,
|
||||
strict: false,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoEndpoint for &String {
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
self.as_str().into_endpoint()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoEndpoint for String {
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse(&self).map_err(|_| Error::InvalidUrl(self))?,
|
||||
strict: false,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rustls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
|
||||
impl<T> IntoEndpoint for (T, rustls::ClientConfig)
|
||||
where
|
||||
T: Into<String>,
|
||||
{
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let (address, config) = self;
|
||||
let mut address = address.into().into_endpoint()?;
|
||||
address.tls_config = Some(Tls::Rust(config));
|
||||
Ok(address)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(
|
||||
feature = "kv-mem",
|
||||
feature = "kv-tikv",
|
||||
feature = "kv-rocksdb",
|
||||
feature = "kv-fdb",
|
||||
feature = "kv-indxdb",
|
||||
))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(
|
||||
feature = "kv-mem",
|
||||
feature = "kv-tikv",
|
||||
feature = "kv-rocksdb",
|
||||
feature = "kv-fdb",
|
||||
feature = "kv-indxdb",
|
||||
)))
|
||||
)]
|
||||
impl<T> IntoEndpoint for (T, Strict)
|
||||
where
|
||||
T: Into<String>,
|
||||
{
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let mut address = IntoEndpoint::into_endpoint(self.0.into())?;
|
||||
address.strict = true;
|
||||
Ok(address)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
any(
|
||||
feature = "kv-mem",
|
||||
feature = "kv-tikv",
|
||||
feature = "kv-rocksdb",
|
||||
feature = "kv-fdb",
|
||||
feature = "kv-indxdb",
|
||||
),
|
||||
feature = "rustls",
|
||||
))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(all(
|
||||
any(
|
||||
feature = "kv-mem",
|
||||
feature = "kv-tikv",
|
||||
feature = "kv-rocksdb",
|
||||
feature = "kv-fdb",
|
||||
feature = "kv-indxdb",
|
||||
),
|
||||
feature = "rustls",
|
||||
)))
|
||||
)]
|
||||
impl<T> IntoEndpoint for (T, rustls::ClientConfig, Strict)
|
||||
where
|
||||
T: Into<String>,
|
||||
{
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let (address, config, _) = self;
|
||||
let mut address = address.into().into_endpoint()?;
|
||||
address.tls_config = Some(Tls::Rust(config));
|
||||
address.strict = true;
|
||||
Ok(address)
|
||||
}
|
||||
}
|
||||
|
||||
/// A dynamic connection that supports any engine and allows you to pick at runtime
|
||||
#[derive(Debug)]
|
||||
pub struct Any {
|
||||
id: i64,
|
||||
method: Method,
|
||||
}
|
||||
|
||||
impl Surreal<Any> {
|
||||
/// Connects to a specific database endpoint, saving the connection on the static client
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use surrealdb::Surreal;
|
||||
/// use surrealdb::engines::any::Any;
|
||||
///
|
||||
/// static DB: Surreal<Any> = Surreal::init();
|
||||
///
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> surrealdb::Result<()> {
|
||||
/// DB.connect("ws://localhost:8000").await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn connect(&'static self, address: impl IntoEndpoint) -> Connect<Any, ()> {
|
||||
Connect {
|
||||
router: Some(&self.router),
|
||||
address: address.into_endpoint(),
|
||||
capacity: 0,
|
||||
client: PhantomData,
|
||||
response_type: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Connects to a local, remote or embedded database
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use surrealdb::engines::any::connect;
|
||||
///
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> surrealdb::Result<()> {
|
||||
/// // Connect to a local endpoint
|
||||
/// let db = connect("ws://localhost:8000").await?;
|
||||
///
|
||||
/// // Connect to a remote endpoint
|
||||
/// let db = connect("wss://cloud.surrealdb.com").await?;
|
||||
///
|
||||
/// // Connect using HTTP
|
||||
/// let db = connect("http://localhost:8000").await?;
|
||||
///
|
||||
/// // Connect using HTTPS
|
||||
/// let db = connect("https://cloud.surrealdb.com").await?;
|
||||
///
|
||||
/// // Instantiate an in-memory instance
|
||||
/// let db = connect("mem://").await?;
|
||||
///
|
||||
/// // Instantiate an file-backed instance
|
||||
/// let db = connect("file://temp.db").await?;
|
||||
///
|
||||
/// // Instantiate an IndxDB-backed instance
|
||||
/// let db = connect("indxdb://MyDatabase").await?;
|
||||
///
|
||||
/// // Instantiate a TiKV-backed instance
|
||||
/// let db = connect("tikv://localhost:2379").await?;
|
||||
///
|
||||
/// // Instantiate a FoundationDB-backed instance
|
||||
/// let db = connect("fdb://fdb.cluster").await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn connect(address: impl IntoEndpoint) -> Connect<'static, Any, Surreal<Any>> {
|
||||
Connect {
|
||||
router: None,
|
||||
address: address.into_endpoint(),
|
||||
capacity: 0,
|
||||
client: PhantomData,
|
||||
response_type: PhantomData,
|
||||
}
|
||||
}
|
224
lib/src/api/engines/any/native.rs
Normal file
224
lib/src/api/engines/any/native.rs
Normal file
|
@ -0,0 +1,224 @@
|
|||
use crate::api::conn::Connection;
|
||||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Route;
|
||||
use crate::api::conn::Router;
|
||||
#[allow(unused_imports)] // used by the DB engines
|
||||
use crate::api::engines;
|
||||
use crate::api::engines::any::Any;
|
||||
use crate::api::err::Error;
|
||||
use crate::api::opt::from_value;
|
||||
use crate::api::opt::Endpoint;
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
#[cfg(feature = "protocol-http")]
|
||||
use crate::api::opt::Tls;
|
||||
use crate::api::DbResponse;
|
||||
#[allow(unused_imports)] // used by the DB engines
|
||||
use crate::api::ExtraFeatures;
|
||||
use crate::api::Response;
|
||||
use crate::api::Result;
|
||||
use crate::api::Surreal;
|
||||
use flume::Receiver;
|
||||
use once_cell::sync::OnceCell;
|
||||
#[cfg(feature = "protocol-http")]
|
||||
use reqwest::header::HeaderMap;
|
||||
#[cfg(feature = "protocol-http")]
|
||||
use reqwest::header::HeaderValue;
|
||||
#[cfg(feature = "protocol-http")]
|
||||
use reqwest::header::ACCEPT;
|
||||
#[cfg(feature = "protocol-http")]
|
||||
use reqwest::ClientBuilder;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::collections::HashSet;
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
use std::sync::atomic::AtomicI64;
|
||||
use std::sync::Arc;
|
||||
#[cfg(feature = "protocol-ws")]
|
||||
use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
|
||||
#[cfg(feature = "protocol-ws")]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
use tokio_tungstenite::Connector;
|
||||
|
||||
impl crate::api::Connection for Any {}
|
||||
|
||||
impl Connection for Any {
|
||||
fn new(method: Method) -> Self {
|
||||
Self {
|
||||
method,
|
||||
id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_variables, unreachable_code, unused_mut)] // these are all used depending on feature
|
||||
fn connect(
|
||||
address: Endpoint,
|
||||
capacity: usize,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Surreal<Self>>> + Send + Sync + 'static>> {
|
||||
Box::pin(async move {
|
||||
let (route_tx, route_rx) = match capacity {
|
||||
0 => flume::unbounded(),
|
||||
capacity => flume::bounded(capacity),
|
||||
};
|
||||
|
||||
let (conn_tx, conn_rx) = flume::bounded::<Result<()>>(1);
|
||||
let mut features = HashSet::new();
|
||||
|
||||
match address.endpoint.scheme() {
|
||||
#[cfg(feature = "kv-fdb")]
|
||||
"fdb" => {
|
||||
features.insert(ExtraFeatures::Backup);
|
||||
engines::local::native::router(address, conn_tx, route_rx);
|
||||
conn_rx.into_recv_async().await??
|
||||
}
|
||||
|
||||
#[cfg(feature = "kv-mem")]
|
||||
"mem" => {
|
||||
features.insert(ExtraFeatures::Backup);
|
||||
engines::local::native::router(address, conn_tx, route_rx);
|
||||
conn_rx.into_recv_async().await??
|
||||
}
|
||||
|
||||
#[cfg(feature = "kv-rocksdb")]
|
||||
"rocksdb" => {
|
||||
features.insert(ExtraFeatures::Backup);
|
||||
engines::local::native::router(address, conn_tx, route_rx);
|
||||
conn_rx.into_recv_async().await??
|
||||
}
|
||||
|
||||
#[cfg(feature = "kv-rocksdb")]
|
||||
"file" => {
|
||||
features.insert(ExtraFeatures::Backup);
|
||||
engines::local::native::router(address, conn_tx, route_rx);
|
||||
conn_rx.into_recv_async().await??
|
||||
}
|
||||
|
||||
#[cfg(feature = "kv-tikv")]
|
||||
"tikv" => {
|
||||
features.insert(ExtraFeatures::Backup);
|
||||
engines::local::native::router(address, conn_tx, route_rx);
|
||||
conn_rx.into_recv_async().await??
|
||||
}
|
||||
|
||||
#[cfg(feature = "protocol-http")]
|
||||
"http" | "https" => {
|
||||
features.insert(ExtraFeatures::Auth);
|
||||
features.insert(ExtraFeatures::Backup);
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
|
||||
#[allow(unused_mut)]
|
||||
let mut builder = ClientBuilder::new().default_headers(headers);
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
if let Some(tls) = address.tls_config {
|
||||
builder = match tls {
|
||||
#[cfg(feature = "native-tls")]
|
||||
Tls::Native(config) => builder.use_preconfigured_tls(config),
|
||||
#[cfg(feature = "rustls")]
|
||||
Tls::Rust(config) => builder.use_preconfigured_tls(config),
|
||||
};
|
||||
}
|
||||
let client = builder.build()?;
|
||||
let base_url = address.endpoint;
|
||||
engines::remote::http::health(
|
||||
client.get(base_url.join(Method::Health.as_str())?),
|
||||
)
|
||||
.await?;
|
||||
engines::remote::http::native::router(base_url, client, route_rx);
|
||||
}
|
||||
|
||||
#[cfg(feature = "protocol-ws")]
|
||||
"ws" | "wss" => {
|
||||
features.insert(ExtraFeatures::Auth);
|
||||
let url = address.endpoint.join(engines::remote::ws::PATH)?;
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
let maybe_connector = address.tls_config.map(Connector::from);
|
||||
#[cfg(not(any(feature = "native-tls", feature = "rustls")))]
|
||||
let maybe_connector = None;
|
||||
let config = WebSocketConfig {
|
||||
max_send_queue: match capacity {
|
||||
0 => None,
|
||||
capacity => Some(capacity),
|
||||
},
|
||||
max_message_size: Some(engines::remote::ws::native::MAX_MESSAGE_SIZE),
|
||||
max_frame_size: Some(engines::remote::ws::native::MAX_FRAME_SIZE),
|
||||
accept_unmasked_frames: false,
|
||||
};
|
||||
let socket = engines::remote::ws::native::connect(
|
||||
&url,
|
||||
Some(config),
|
||||
maybe_connector.clone(),
|
||||
)
|
||||
.await?;
|
||||
engines::remote::ws::native::router(
|
||||
url,
|
||||
maybe_connector,
|
||||
capacity,
|
||||
config,
|
||||
socket,
|
||||
route_rx,
|
||||
);
|
||||
}
|
||||
|
||||
scheme => {
|
||||
return Err(Error::Scheme(scheme.to_owned()).into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Surreal {
|
||||
router: OnceCell::with_value(Arc::new(Router {
|
||||
features,
|
||||
conn: PhantomData,
|
||||
sender: route_tx,
|
||||
last_id: AtomicI64::new(0),
|
||||
})),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn send<'r>(
|
||||
&'r mut self,
|
||||
router: &'r Router<Self>,
|
||||
param: Param,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Receiver<Result<DbResponse>>>> + Send + Sync + 'r>> {
|
||||
Box::pin(async move {
|
||||
let (sender, receiver) = flume::bounded(1);
|
||||
self.id = router.next_id();
|
||||
let route = Route {
|
||||
request: (self.id, self.method, param),
|
||||
response: sender,
|
||||
};
|
||||
router.sender.send_async(Some(route)).await?;
|
||||
Ok(receiver)
|
||||
})
|
||||
}
|
||||
|
||||
fn recv<R>(
|
||||
&mut self,
|
||||
receiver: Receiver<Result<DbResponse>>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<R>> + Send + Sync + '_>>
|
||||
where
|
||||
R: DeserializeOwned,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let response = receiver.into_recv_async().await?;
|
||||
match response? {
|
||||
DbResponse::Other(value) => from_value(value),
|
||||
DbResponse::Query(..) => unreachable!(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn recv_query(
|
||||
&mut self,
|
||||
receiver: Receiver<Result<DbResponse>>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Response>> + Send + Sync + '_>> {
|
||||
Box::pin(async move {
|
||||
let response = receiver.into_recv_async().await?;
|
||||
match response? {
|
||||
DbResponse::Query(results) => Ok(results),
|
||||
DbResponse::Other(..) => unreachable!(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
179
lib/src/api/engines/any/wasm.rs
Normal file
179
lib/src/api/engines/any/wasm.rs
Normal file
|
@ -0,0 +1,179 @@
|
|||
use crate::api::conn::Connection;
|
||||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Route;
|
||||
use crate::api::conn::Router;
|
||||
#[allow(unused_imports)] // used by the DB engines
|
||||
use crate::api::engines;
|
||||
use crate::api::engines::any::Any;
|
||||
use crate::api::err::Error;
|
||||
use crate::api::opt::from_value;
|
||||
use crate::api::opt::Endpoint;
|
||||
use crate::api::DbResponse;
|
||||
#[allow(unused_imports)] // used by the `ws` and `http` protocols
|
||||
use crate::api::ExtraFeatures;
|
||||
use crate::api::Response;
|
||||
use crate::api::Result;
|
||||
use crate::api::Surreal;
|
||||
use flume::Receiver;
|
||||
use once_cell::sync::OnceCell;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::collections::HashSet;
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
use std::sync::atomic::AtomicI64;
|
||||
use std::sync::Arc;
|
||||
|
||||
impl crate::api::Connection for Any {}
|
||||
|
||||
impl Connection for Any {
|
||||
fn new(method: Method) -> Self {
|
||||
Self {
|
||||
method,
|
||||
id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_variables, unreachable_code, unused_mut)] // these are all used depending on feature
|
||||
fn connect(
|
||||
address: Endpoint,
|
||||
capacity: usize,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Surreal<Self>>> + Send + Sync + 'static>> {
|
||||
Box::pin(async move {
|
||||
let (route_tx, route_rx) = match capacity {
|
||||
0 => flume::unbounded(),
|
||||
capacity => flume::bounded(capacity),
|
||||
};
|
||||
|
||||
let (conn_tx, conn_rx) = flume::bounded::<Result<()>>(1);
|
||||
let mut features = HashSet::new();
|
||||
|
||||
match address.endpoint.scheme() {
|
||||
#[cfg(feature = "kv-fdb")]
|
||||
"fdb" => {
|
||||
engines::local::wasm::router(address, conn_tx, route_rx);
|
||||
if let Err(error) = conn_rx.into_recv_async().await? {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "kv-indxdb")]
|
||||
"indxdb" => {
|
||||
engines::local::wasm::router(address, conn_tx, route_rx);
|
||||
if let Err(error) = conn_rx.into_recv_async().await? {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "kv-mem")]
|
||||
"mem" => {
|
||||
engines::local::wasm::router(address, conn_tx, route_rx);
|
||||
if let Err(error) = conn_rx.into_recv_async().await? {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "kv-rocksdb")]
|
||||
"rocksdb" => {
|
||||
engines::local::wasm::router(address, conn_tx, route_rx);
|
||||
if let Err(error) = conn_rx.into_recv_async().await? {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "kv-rocksdb")]
|
||||
"file" => {
|
||||
engines::local::wasm::router(address, conn_tx, route_rx);
|
||||
if let Err(error) = conn_rx.into_recv_async().await? {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "kv-tikv")]
|
||||
"tikv" => {
|
||||
engines::local::wasm::router(address, conn_tx, route_rx);
|
||||
if let Err(error) = conn_rx.into_recv_async().await? {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "protocol-http")]
|
||||
"http" | "https" => {
|
||||
features.insert(ExtraFeatures::Auth);
|
||||
engines::remote::http::wasm::router(address, conn_tx, route_rx);
|
||||
}
|
||||
|
||||
#[cfg(feature = "protocol-ws")]
|
||||
"ws" | "wss" => {
|
||||
features.insert(ExtraFeatures::Auth);
|
||||
let mut address = address;
|
||||
address.endpoint = address.endpoint.join(engines::remote::ws::PATH)?;
|
||||
engines::remote::ws::wasm::router(address, capacity, conn_tx, route_rx);
|
||||
if let Err(error) = conn_rx.into_recv_async().await? {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
|
||||
scheme => {
|
||||
return Err(Error::Scheme(scheme.to_owned()).into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Surreal {
|
||||
router: OnceCell::with_value(Arc::new(Router {
|
||||
features,
|
||||
conn: PhantomData,
|
||||
sender: route_tx,
|
||||
last_id: AtomicI64::new(0),
|
||||
})),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn send<'r>(
|
||||
&'r mut self,
|
||||
router: &'r Router<Self>,
|
||||
param: Param,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Receiver<Result<DbResponse>>>> + Send + Sync + 'r>> {
|
||||
Box::pin(async move {
|
||||
let (sender, receiver) = flume::bounded(1);
|
||||
self.id = router.next_id();
|
||||
let route = Route {
|
||||
request: (self.id, self.method, param),
|
||||
response: sender,
|
||||
};
|
||||
router.sender.send_async(Some(route)).await?;
|
||||
Ok(receiver)
|
||||
})
|
||||
}
|
||||
|
||||
fn recv<R>(
|
||||
&mut self,
|
||||
receiver: Receiver<Result<DbResponse>>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<R>> + Send + Sync + '_>>
|
||||
where
|
||||
R: DeserializeOwned,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let response = receiver.into_recv_async().await?;
|
||||
match response? {
|
||||
DbResponse::Other(value) => from_value(value),
|
||||
DbResponse::Query(..) => unreachable!(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn recv_query(
|
||||
&mut self,
|
||||
receiver: Receiver<Result<DbResponse>>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Response>> + Send + Sync + '_>> {
|
||||
Box::pin(async move {
|
||||
let response = receiver.into_recv_async().await?;
|
||||
match response? {
|
||||
DbResponse::Query(results) => Ok(results),
|
||||
DbResponse::Other(..) => unreachable!(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
556
lib/src/api/engines/local/mod.rs
Normal file
556
lib/src/api/engines/local/mod.rs
Normal file
|
@ -0,0 +1,556 @@
|
|||
//! Embedded database instance
|
||||
//!
|
||||
//! `SurrealDB` itself can be embedded in this library, allowing you to query it using the same
|
||||
//! crate and API that you would use when connecting to it remotely via WebSockets or HTTP.
|
||||
//! All storage engines are supported but you have to activate their feature
|
||||
//! flags first.
|
||||
//!
|
||||
//! **NB**: Some storage engines like `TiKV` and `RocksDB` depend on non-Rust libraries so you need
|
||||
//! to install those libraries before you can build this crate when you activate their feature
|
||||
//! flags. Please refer to [these instructions](https://github.com/surrealdb/surrealdb/blob/main/doc/BUILDING.md)
|
||||
//! for more details on how to install them. If you are on Linux and you use
|
||||
//! [the Nix package manager](https://github.com/surrealdb/surrealdb/tree/main/pkg/nix#installing-nix)
|
||||
//! you can just run
|
||||
//!
|
||||
//! ```bash
|
||||
//! nix develop github:surrealdb/surrealdb
|
||||
//! ```
|
||||
//!
|
||||
//! which will drop you into a shell with all the dependencies available. One tip you may find
|
||||
//! useful is to only enable the in-memory engine (`kv-mem`) during development. Besides letting you not
|
||||
//! worry about those dependencies on your dev machine, it allows you to keep compile times low
|
||||
//! during development while allowing you to test your code fully.
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) mod native;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) mod wasm;
|
||||
|
||||
use crate::api::conn::DbResponse;
|
||||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::engines::create_statement;
|
||||
use crate::api::engines::delete_statement;
|
||||
use crate::api::engines::merge_statement;
|
||||
use crate::api::engines::patch_statement;
|
||||
use crate::api::engines::select_statement;
|
||||
use crate::api::engines::update_statement;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::api::err::Error;
|
||||
use crate::api::Connect;
|
||||
use crate::api::Response as QueryResponse;
|
||||
use crate::api::Result;
|
||||
use crate::api::Surreal;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::channel;
|
||||
use crate::dbs::Response;
|
||||
use crate::dbs::Session;
|
||||
use crate::kvs::Datastore;
|
||||
use crate::opt::IntoEndpoint;
|
||||
use crate::sql::Array;
|
||||
use crate::sql::Query;
|
||||
use crate::sql::Statement;
|
||||
use crate::sql::Statements;
|
||||
use crate::sql::Strand;
|
||||
use crate::sql::Value;
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::BTreeMap;
|
||||
use std::marker::PhantomData;
|
||||
use std::mem;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio::fs::OpenOptions;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio::io;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio::io::AsyncReadExt;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
const LOG: &str = "surrealdb::api::engines::local";
|
||||
|
||||
/// In-memory database
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Instantiating a global instance
|
||||
///
|
||||
/// ```
|
||||
/// use surrealdb::{Result, Surreal};
|
||||
/// use surrealdb::engines::local::Db;
|
||||
/// use surrealdb::engines::local::Mem;
|
||||
///
|
||||
/// static DB: Surreal<Db> = Surreal::init();
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() -> Result<()> {
|
||||
/// DB.connect::<Mem>(()).await?;
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Instantiating an in-memory instance
|
||||
///
|
||||
/// ```
|
||||
/// use surrealdb::Surreal;
|
||||
/// use surrealdb::engines::local::Mem;
|
||||
///
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> surrealdb::Result<()> {
|
||||
/// let db = Surreal::new::<Mem>(()).await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Instantiating an in-memory strict instance
|
||||
///
|
||||
/// ```
|
||||
/// use surrealdb::opt::Strict;
|
||||
/// use surrealdb::Surreal;
|
||||
/// use surrealdb::engines::local::Mem;
|
||||
///
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> surrealdb::Result<()> {
|
||||
/// let db = Surreal::new::<Mem>(Strict).await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg(feature = "kv-mem")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "kv-mem")))]
|
||||
#[derive(Debug)]
|
||||
pub struct Mem;
|
||||
|
||||
/// File database
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Instantiating a file-backed instance
|
||||
///
|
||||
/// ```no_run
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> surrealdb::Result<()> {
|
||||
/// use surrealdb::Surreal;
|
||||
/// use surrealdb::engines::local::File;
|
||||
///
|
||||
/// let db = Surreal::new::<File>("temp.db").await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Instantiating a file-backed strict instance
|
||||
///
|
||||
/// ```no_run
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> surrealdb::Result<()> {
|
||||
/// use surrealdb::opt::Strict;
|
||||
/// use surrealdb::Surreal;
|
||||
/// use surrealdb::engines::local::File;
|
||||
///
|
||||
/// let db = Surreal::new::<File>(("temp.db", Strict)).await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg(feature = "kv-rocksdb")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "kv-rocksdb")))]
|
||||
#[derive(Debug)]
|
||||
pub struct File;
|
||||
|
||||
/// RocksDB database
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Instantiating a RocksDB-backed instance
|
||||
///
|
||||
/// ```no_run
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> surrealdb::Result<()> {
|
||||
/// use surrealdb::Surreal;
|
||||
/// use surrealdb::engines::local::RocksDb;
|
||||
///
|
||||
/// let db = Surreal::new::<RocksDb>("temp.db").await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Instantiating a RocksDB-backed strict instance
|
||||
///
|
||||
/// ```no_run
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> surrealdb::Result<()> {
|
||||
/// use surrealdb::opt::Strict;
|
||||
/// use surrealdb::Surreal;
|
||||
/// use surrealdb::engines::local::RocksDb;
|
||||
///
|
||||
/// let db = Surreal::new::<RocksDb>(("temp.db", Strict)).await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg(feature = "kv-rocksdb")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "kv-rocksdb")))]
|
||||
#[derive(Debug)]
|
||||
pub struct RocksDb;
|
||||
|
||||
/// IndxDB database
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Instantiating a IndxDB-backed instance
|
||||
///
|
||||
/// ```no_run
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> surrealdb::Result<()> {
|
||||
/// use surrealdb::Surreal;
|
||||
/// use surrealdb::engines::local::IndxDb;
|
||||
///
|
||||
/// let db = Surreal::new::<IndxDb>("MyDatabase").await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Instantiating an IndxDB-backed strict instance
|
||||
///
|
||||
/// ```no_run
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> surrealdb::Result<()> {
|
||||
/// use surrealdb::opt::Strict;
|
||||
/// use surrealdb::Surreal;
|
||||
/// use surrealdb::engines::local::IndxDb;
|
||||
///
|
||||
/// let db = Surreal::new::<IndxDb>(("MyDatabase", Strict)).await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg(feature = "kv-indxdb")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "kv-indxdb")))]
|
||||
#[derive(Debug)]
|
||||
pub struct IndxDb;
|
||||
|
||||
/// TiKV database
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Instantiating a TiKV instance
|
||||
///
|
||||
/// ```no_run
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> surrealdb::Result<()> {
|
||||
/// use surrealdb::Surreal;
|
||||
/// use surrealdb::engines::local::TiKv;
|
||||
///
|
||||
/// let db = Surreal::new::<TiKv>("localhost:2379").await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Instantiating a TiKV strict instance
|
||||
///
|
||||
/// ```no_run
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> surrealdb::Result<()> {
|
||||
/// use surrealdb::opt::Strict;
|
||||
/// use surrealdb::Surreal;
|
||||
/// use surrealdb::engines::local::TiKv;
|
||||
///
|
||||
/// let db = Surreal::new::<TiKv>(("localhost:2379", Strict)).await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg(feature = "kv-tikv")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "kv-tikv")))]
|
||||
#[derive(Debug)]
|
||||
pub struct TiKv;
|
||||
|
||||
/// FoundationDB database
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Instantiating a FoundationDB-backed instance
|
||||
///
|
||||
/// ```no_run
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> surrealdb::Result<()> {
|
||||
/// use surrealdb::opt::Strict;
|
||||
/// use surrealdb::Surreal;
|
||||
/// use surrealdb::engines::local::FDb;
|
||||
///
|
||||
/// let db = Surreal::new::<FDb>("fdb.cluster").await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Instantiating a FoundationDB-backed strict instance
|
||||
///
|
||||
/// ```no_run
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> surrealdb::Result<()> {
|
||||
/// use surrealdb::opt::Strict;
|
||||
/// use surrealdb::Surreal;
|
||||
/// use surrealdb::engines::local::FDb;
|
||||
///
|
||||
/// let db = Surreal::new::<FDb>(("fdb.cluster", Strict)).await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg(feature = "kv-fdb")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "kv-fdb")))]
|
||||
#[derive(Debug)]
|
||||
pub struct FDb;
|
||||
|
||||
/// An embedded database
|
||||
///
|
||||
/// Authentication methods (`signup`, `signin`, `authentication` and `invalidate`) are not availabe
|
||||
/// on `Db`
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Db {
|
||||
pub(crate) method: crate::api::conn::Method,
|
||||
}
|
||||
|
||||
impl Surreal<Db> {
|
||||
/// Connects to a specific database endpoint, saving the connection on the static client
|
||||
pub fn connect<P>(
|
||||
&'static self,
|
||||
address: impl IntoEndpoint<P, Client = Db>,
|
||||
) -> Connect<Db, ()> {
|
||||
Connect {
|
||||
router: Some(&self.router),
|
||||
address: address.into_endpoint(),
|
||||
capacity: 0,
|
||||
client: PhantomData,
|
||||
response_type: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process(responses: Vec<Response>) -> Result<QueryResponse> {
|
||||
let mut map = IndexMap::with_capacity(responses.len());
|
||||
for (index, response) in responses.into_iter().enumerate() {
|
||||
match response.result {
|
||||
Ok(value) => match value {
|
||||
Value::Array(Array(array)) => map.insert(index, Ok(array)),
|
||||
Value::None | Value::Null => map.insert(index, Ok(vec![])),
|
||||
value => map.insert(index, Ok(vec![value])),
|
||||
},
|
||||
Err(error) => map.insert(index, Err(error.into())),
|
||||
};
|
||||
}
|
||||
Ok(QueryResponse(map))
|
||||
}
|
||||
|
||||
async fn take(one: bool, responses: Vec<Response>) -> Result<Value> {
|
||||
if let Some(result) = process(responses)?.0.remove(&0) {
|
||||
let mut vec = result?;
|
||||
match one {
|
||||
true => match vec.pop() {
|
||||
Some(Value::Array(Array(mut vec))) => {
|
||||
if let [value] = &mut vec[..] {
|
||||
return Ok(mem::take(value));
|
||||
}
|
||||
}
|
||||
Some(Value::None | Value::Null) | None => {}
|
||||
Some(value) => {
|
||||
return Ok(value);
|
||||
}
|
||||
},
|
||||
false => {
|
||||
return Ok(Value::Array(Array(vec)));
|
||||
}
|
||||
}
|
||||
}
|
||||
match one {
|
||||
true => Ok(Value::None),
|
||||
false => Ok(Value::Array(Array(vec![]))),
|
||||
}
|
||||
}
|
||||
|
||||
async fn router(
|
||||
(_, method, param): (i64, Method, Param),
|
||||
kvs: &Datastore,
|
||||
session: &mut Session,
|
||||
vars: &mut BTreeMap<String, Value>,
|
||||
strict: bool,
|
||||
) -> Result<DbResponse> {
|
||||
let mut params = param.other;
|
||||
|
||||
match method {
|
||||
Method::Use => {
|
||||
let (ns, db) = match &mut params[..] {
|
||||
[Value::Strand(Strand(ns)), Value::Strand(Strand(db))] => {
|
||||
(mem::take(ns), mem::take(db))
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
session.ns = Some(ns);
|
||||
session.db = Some(db);
|
||||
Ok(DbResponse::Other(Value::None))
|
||||
}
|
||||
Method::Signin | Method::Signup | Method::Authenticate | Method::Invalidate => {
|
||||
unreachable!()
|
||||
}
|
||||
Method::Create => {
|
||||
let statement = create_statement(&mut params);
|
||||
let query = Query(Statements(vec![Statement::Create(statement)]));
|
||||
let response = kvs.process(query, &*session, Some(vars.clone()), strict).await?;
|
||||
let value = take(true, response).await?;
|
||||
Ok(DbResponse::Other(value))
|
||||
}
|
||||
Method::Update => {
|
||||
let (one, statement) = update_statement(&mut params);
|
||||
let query = Query(Statements(vec![Statement::Update(statement)]));
|
||||
let response = kvs.process(query, &*session, Some(vars.clone()), strict).await?;
|
||||
let value = take(one, response).await?;
|
||||
Ok(DbResponse::Other(value))
|
||||
}
|
||||
Method::Patch => {
|
||||
let (one, statement) = patch_statement(&mut params);
|
||||
let query = Query(Statements(vec![Statement::Update(statement)]));
|
||||
let response = kvs.process(query, &*session, Some(vars.clone()), strict).await?;
|
||||
let value = take(one, response).await?;
|
||||
Ok(DbResponse::Other(value))
|
||||
}
|
||||
Method::Merge => {
|
||||
let (one, statement) = merge_statement(&mut params);
|
||||
let query = Query(Statements(vec![Statement::Update(statement)]));
|
||||
let response = kvs.process(query, &*session, Some(vars.clone()), strict).await?;
|
||||
let value = take(one, response).await?;
|
||||
Ok(DbResponse::Other(value))
|
||||
}
|
||||
Method::Select => {
|
||||
let (one, statement) = select_statement(&mut params);
|
||||
let query = Query(Statements(vec![Statement::Select(statement)]));
|
||||
let response = kvs.process(query, &*session, Some(vars.clone()), strict).await?;
|
||||
let value = take(one, response).await?;
|
||||
Ok(DbResponse::Other(value))
|
||||
}
|
||||
Method::Delete => {
|
||||
let statement = delete_statement(&mut params);
|
||||
let query = Query(Statements(vec![Statement::Delete(statement)]));
|
||||
let response = kvs.process(query, &*session, Some(vars.clone()), strict).await?;
|
||||
let value = take(true, response).await?;
|
||||
Ok(DbResponse::Other(value))
|
||||
}
|
||||
Method::Query => {
|
||||
let response = match param.query {
|
||||
Some((query, mut bindings)) => {
|
||||
let mut vars = vars.clone();
|
||||
vars.append(&mut bindings);
|
||||
kvs.process(query, &*session, Some(vars), strict).await?
|
||||
}
|
||||
None => unreachable!(),
|
||||
};
|
||||
let response = process(response)?;
|
||||
Ok(DbResponse::Query(response))
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
Method::Export | Method::Import => unreachable!(),
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
Method::Export => {
|
||||
let (tx, rx) = channel::new::<Vec<u8>>(1);
|
||||
let ns = session.ns.clone().unwrap_or_default();
|
||||
let db = session.db.clone().unwrap_or_default();
|
||||
let (mut writer, mut reader) = io::duplex(10_240);
|
||||
tokio::spawn(async move {
|
||||
while let Ok(value) = rx.recv().await {
|
||||
if let Err(error) = writer.write_all(&value).await {
|
||||
error!(target: LOG, "{error}");
|
||||
}
|
||||
}
|
||||
});
|
||||
if let Err(error) = kvs.export(ns, db, tx).await {
|
||||
error!(target: LOG, "{error}");
|
||||
}
|
||||
let path = param.file.expect("file to export into");
|
||||
let mut file = match OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&path)
|
||||
.await
|
||||
{
|
||||
Ok(path) => path,
|
||||
Err(error) => {
|
||||
return Err(Error::FileOpen {
|
||||
path,
|
||||
error,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
};
|
||||
if let Err(error) = io::copy(&mut reader, &mut file).await {
|
||||
return Err(Error::FileRead {
|
||||
path,
|
||||
error,
|
||||
}
|
||||
.into());
|
||||
};
|
||||
Ok(DbResponse::Other(Value::None))
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
Method::Import => {
|
||||
let path = param.file.expect("file to import from");
|
||||
let mut file = match OpenOptions::new().read(true).open(&path).await {
|
||||
Ok(path) => path,
|
||||
Err(error) => {
|
||||
return Err(Error::FileOpen {
|
||||
path,
|
||||
error,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
};
|
||||
let mut statements = String::new();
|
||||
if let Err(error) = file.read_to_string(&mut statements).await {
|
||||
return Err(Error::FileRead {
|
||||
path,
|
||||
error,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
let responses = kvs.execute(&statements, &*session, Some(vars.clone()), strict).await?;
|
||||
for response in responses {
|
||||
response.result?;
|
||||
}
|
||||
Ok(DbResponse::Other(Value::None))
|
||||
}
|
||||
Method::Health => Ok(DbResponse::Other(Value::None)),
|
||||
Method::Version => Ok(DbResponse::Other(crate::env::VERSION.into())),
|
||||
Method::Set => {
|
||||
let (key, value) = match &mut params[..2] {
|
||||
[Value::Strand(Strand(key)), value] => (mem::take(key), mem::take(value)),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
vars.insert(key, value);
|
||||
Ok(DbResponse::Other(Value::None))
|
||||
}
|
||||
Method::Unset => {
|
||||
if let [Value::Strand(Strand(key))] = ¶ms[..1] {
|
||||
vars.remove(key);
|
||||
}
|
||||
Ok(DbResponse::Other(Value::None))
|
||||
}
|
||||
Method::Live => {
|
||||
let table = match &mut params[..] {
|
||||
[value] => mem::take(value),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let mut vars = BTreeMap::new();
|
||||
vars.insert("table".to_owned(), table);
|
||||
let response = kvs
|
||||
.execute("LIVE SELECT * FROM type::table($table)", &*session, Some(vars), strict)
|
||||
.await?;
|
||||
let value = take(true, response).await?;
|
||||
Ok(DbResponse::Other(value))
|
||||
}
|
||||
Method::Kill => {
|
||||
let id = match &mut params[..] {
|
||||
[value] => mem::take(value),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let mut vars = BTreeMap::new();
|
||||
vars.insert("id".to_owned(), id);
|
||||
let response =
|
||||
kvs.execute("KILL type::string($id)", &*session, Some(vars), strict).await?;
|
||||
let value = take(true, response).await?;
|
||||
Ok(DbResponse::Other(value))
|
||||
}
|
||||
}
|
||||
}
|
154
lib/src/api/engines/local/native.rs
Normal file
154
lib/src/api/engines/local/native.rs
Normal file
|
@ -0,0 +1,154 @@
|
|||
use crate::api::conn::Connection;
|
||||
use crate::api::conn::DbResponse;
|
||||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Route;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::engines::local::Db;
|
||||
use crate::api::opt::from_value;
|
||||
use crate::api::opt::Endpoint;
|
||||
use crate::api::ExtraFeatures;
|
||||
use crate::api::Response as QueryResponse;
|
||||
use crate::api::Result;
|
||||
use crate::api::Surreal;
|
||||
use crate::dbs::Session;
|
||||
use crate::kvs::Datastore;
|
||||
use flume::Receiver;
|
||||
use flume::Sender;
|
||||
use futures::StreamExt;
|
||||
use once_cell::sync::OnceCell;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashSet;
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
use std::sync::atomic::AtomicI64;
|
||||
use std::sync::Arc;
|
||||
|
||||
impl crate::api::Connection for Db {}
|
||||
|
||||
impl Connection for Db {
|
||||
fn new(method: Method) -> Self {
|
||||
Self {
|
||||
method,
|
||||
}
|
||||
}
|
||||
|
||||
fn connect(
|
||||
address: Endpoint,
|
||||
capacity: usize,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Surreal<Self>>> + Send + Sync + 'static>> {
|
||||
Box::pin(async move {
|
||||
let (route_tx, route_rx) = match capacity {
|
||||
0 => flume::unbounded(),
|
||||
capacity => flume::bounded(capacity),
|
||||
};
|
||||
|
||||
let (conn_tx, conn_rx) = flume::bounded(1);
|
||||
|
||||
router(address, conn_tx, route_rx);
|
||||
|
||||
conn_rx.into_recv_async().await??;
|
||||
|
||||
let mut features = HashSet::new();
|
||||
features.insert(ExtraFeatures::Backup);
|
||||
|
||||
Ok(Surreal {
|
||||
router: OnceCell::with_value(Arc::new(Router {
|
||||
features,
|
||||
conn: PhantomData,
|
||||
sender: route_tx,
|
||||
last_id: AtomicI64::new(0),
|
||||
})),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn send<'r>(
|
||||
&'r mut self,
|
||||
router: &'r Router<Self>,
|
||||
param: Param,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Receiver<Result<DbResponse>>>> + Send + Sync + 'r>> {
|
||||
Box::pin(async move {
|
||||
let (sender, receiver) = flume::bounded(1);
|
||||
let route = Route {
|
||||
request: (0, self.method, param),
|
||||
response: sender,
|
||||
};
|
||||
router.sender.send_async(Some(route)).await?;
|
||||
Ok(receiver)
|
||||
})
|
||||
}
|
||||
|
||||
fn recv<R>(
|
||||
&mut self,
|
||||
rx: Receiver<Result<DbResponse>>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<R>> + Send + Sync + '_>>
|
||||
where
|
||||
R: DeserializeOwned,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let response = rx.into_recv_async().await?;
|
||||
match response? {
|
||||
DbResponse::Other(value) => from_value(value),
|
||||
DbResponse::Query(..) => unreachable!(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn recv_query(
|
||||
&mut self,
|
||||
rx: Receiver<Result<DbResponse>>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<QueryResponse>> + Send + Sync + '_>> {
|
||||
Box::pin(async move {
|
||||
let response = rx.into_recv_async().await?;
|
||||
match response? {
|
||||
DbResponse::Query(results) => Ok(results),
|
||||
DbResponse::Other(..) => unreachable!(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn router(
|
||||
address: Endpoint,
|
||||
conn_tx: Sender<Result<()>>,
|
||||
route_rx: Receiver<Option<Route>>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
let url = address.endpoint;
|
||||
|
||||
let path = match url.scheme() {
|
||||
"mem" => "memory",
|
||||
_ => url.as_str(),
|
||||
};
|
||||
|
||||
let kvs = match Datastore::new(path).await {
|
||||
Ok(kvs) => {
|
||||
let _ = conn_tx.into_send_async(Ok(())).await;
|
||||
kvs
|
||||
}
|
||||
Err(error) => {
|
||||
let _ = conn_tx.into_send_async(Err(error.into())).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut session = Session::for_kv();
|
||||
let mut vars = BTreeMap::new();
|
||||
let mut stream = route_rx.into_stream();
|
||||
|
||||
while let Some(Some(route)) = stream.next().await {
|
||||
match super::router(route.request, &kvs, &mut session, &mut vars, address.strict).await
|
||||
{
|
||||
Ok(value) => {
|
||||
let _ = route.response.into_send_async(Ok(value)).await;
|
||||
}
|
||||
Err(error) => {
|
||||
let _ = route.response.into_send_async(Err(error)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
156
lib/src/api/engines/local/wasm.rs
Normal file
156
lib/src/api/engines/local/wasm.rs
Normal file
|
@ -0,0 +1,156 @@
|
|||
use super::LOG;
|
||||
use crate::api::conn::Connection;
|
||||
use crate::api::conn::DbResponse;
|
||||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Route;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::engines::local::Db;
|
||||
use crate::api::opt::from_value;
|
||||
use crate::api::opt::Endpoint;
|
||||
use crate::api::Response as QueryResponse;
|
||||
use crate::api::Result;
|
||||
use crate::api::Surreal;
|
||||
use crate::dbs::Session;
|
||||
use crate::kvs::Datastore;
|
||||
use flume::Receiver;
|
||||
use flume::Sender;
|
||||
use futures::StreamExt;
|
||||
use once_cell::sync::OnceCell;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashSet;
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
use std::sync::atomic::AtomicI64;
|
||||
use std::sync::Arc;
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
|
||||
impl crate::api::Connection for Db {}
|
||||
|
||||
impl Connection for Db {
|
||||
fn new(method: Method) -> Self {
|
||||
Self {
|
||||
method,
|
||||
}
|
||||
}
|
||||
|
||||
fn connect(
|
||||
address: Endpoint,
|
||||
capacity: usize,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Surreal<Self>>> + Send + Sync + 'static>> {
|
||||
Box::pin(async move {
|
||||
let (route_tx, route_rx) = match capacity {
|
||||
0 => flume::unbounded(),
|
||||
capacity => flume::bounded(capacity),
|
||||
};
|
||||
|
||||
let (conn_tx, conn_rx) = flume::bounded(1);
|
||||
|
||||
router(address, conn_tx, route_rx);
|
||||
|
||||
if let Err(error) = conn_rx.into_recv_async().await? {
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
Ok(Surreal {
|
||||
router: OnceCell::with_value(Arc::new(Router {
|
||||
features: HashSet::new(),
|
||||
conn: PhantomData,
|
||||
sender: route_tx,
|
||||
last_id: AtomicI64::new(0),
|
||||
})),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn send<'r>(
|
||||
&'r mut self,
|
||||
router: &'r Router<Self>,
|
||||
param: Param,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Receiver<Result<DbResponse>>>> + Send + Sync + 'r>> {
|
||||
Box::pin(async move {
|
||||
let (sender, receiver) = flume::bounded(1);
|
||||
let route = Route {
|
||||
request: (0, self.method, param),
|
||||
response: sender,
|
||||
};
|
||||
router.sender.send_async(Some(route)).await?;
|
||||
Ok(receiver)
|
||||
})
|
||||
}
|
||||
|
||||
fn recv<R>(
|
||||
&mut self,
|
||||
rx: Receiver<Result<DbResponse>>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<R>> + Send + Sync + '_>>
|
||||
where
|
||||
R: DeserializeOwned,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let response = rx.into_recv_async().await?;
|
||||
trace!(target: LOG, "Response {response:?}");
|
||||
match response? {
|
||||
DbResponse::Other(value) => from_value(value),
|
||||
DbResponse::Query(..) => unreachable!(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn recv_query(
|
||||
&mut self,
|
||||
rx: Receiver<Result<DbResponse>>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<QueryResponse>> + Send + Sync + '_>> {
|
||||
Box::pin(async move {
|
||||
let response = rx.into_recv_async().await?;
|
||||
trace!(target: LOG, "Response {response:?}");
|
||||
match response? {
|
||||
DbResponse::Query(results) => Ok(results),
|
||||
DbResponse::Other(..) => unreachable!(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn router(
|
||||
address: Endpoint,
|
||||
conn_tx: Sender<Result<()>>,
|
||||
route_rx: Receiver<Option<Route>>,
|
||||
) {
|
||||
spawn_local(async move {
|
||||
let url = address.endpoint;
|
||||
|
||||
let path = match url.scheme() {
|
||||
"mem" => "memory",
|
||||
_ => url.as_str(),
|
||||
};
|
||||
|
||||
let kvs = match Datastore::new(path).await {
|
||||
Ok(kvs) => {
|
||||
let _ = conn_tx.into_send_async(Ok(())).await;
|
||||
kvs
|
||||
}
|
||||
Err(error) => {
|
||||
let _ = conn_tx.into_send_async(Err(error.into())).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut session = Session::for_kv();
|
||||
let mut vars = BTreeMap::new();
|
||||
let mut stream = route_rx.into_stream();
|
||||
|
||||
while let Some(Some(route)) = stream.next().await {
|
||||
match super::router(route.request, &kvs, &mut session, &mut vars, address.strict).await
|
||||
{
|
||||
Ok(value) => {
|
||||
let _ = route.response.into_send_async(Ok(value)).await;
|
||||
}
|
||||
Err(error) => {
|
||||
let _ = route.response.into_send_async(Err(error)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
133
lib/src/api/engines/mod.rs
Normal file
133
lib/src/api/engines/mod.rs
Normal file
|
@ -0,0 +1,133 @@
|
|||
//! Different embedded and remote database engines
|
||||
|
||||
pub mod any;
|
||||
#[cfg(any(
|
||||
feature = "kv-mem",
|
||||
feature = "kv-tikv",
|
||||
feature = "kv-rocksdb",
|
||||
feature = "kv-fdb",
|
||||
feature = "kv-indxdb",
|
||||
))]
|
||||
pub mod local;
|
||||
#[cfg(any(feature = "protocol-http", feature = "protocol-ws"))]
|
||||
pub mod remote;
|
||||
|
||||
use crate::sql::statements::CreateStatement;
|
||||
use crate::sql::statements::DeleteStatement;
|
||||
use crate::sql::statements::SelectStatement;
|
||||
use crate::sql::statements::UpdateStatement;
|
||||
use crate::sql::Array;
|
||||
use crate::sql::Data;
|
||||
use crate::sql::Field;
|
||||
use crate::sql::Fields;
|
||||
use crate::sql::Output;
|
||||
use crate::sql::Value;
|
||||
use crate::sql::Values;
|
||||
use std::mem;
|
||||
|
||||
#[allow(dead_code)] // used by the the embedded database and `http`
|
||||
fn split_params(params: &mut [Value]) -> (bool, Values, Value) {
|
||||
let (what, data) = match params {
|
||||
[what] => (mem::take(what), Value::None),
|
||||
[what, data] => (mem::take(what), mem::take(data)),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let one = what.is_thing();
|
||||
let what = match what {
|
||||
Value::Array(Array(vec)) => Values(vec),
|
||||
value => Values(vec![value]),
|
||||
};
|
||||
(one, what, data)
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // used by the the embedded database and `http`
|
||||
fn create_statement(params: &mut [Value]) -> CreateStatement {
|
||||
let (_, what, data) = split_params(params);
|
||||
let data = match data {
|
||||
Value::None | Value::Null => None,
|
||||
value => Some(Data::ContentExpression(value)),
|
||||
};
|
||||
CreateStatement {
|
||||
what,
|
||||
data,
|
||||
output: Some(Output::After),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // used by the the embedded database and `http`
|
||||
fn update_statement(params: &mut [Value]) -> (bool, UpdateStatement) {
|
||||
let (one, what, data) = split_params(params);
|
||||
let data = match data {
|
||||
Value::None | Value::Null => None,
|
||||
value => Some(Data::ContentExpression(value)),
|
||||
};
|
||||
(
|
||||
one,
|
||||
UpdateStatement {
|
||||
what,
|
||||
data,
|
||||
output: Some(Output::After),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // used by the the embedded database and `http`
|
||||
fn patch_statement(params: &mut [Value]) -> (bool, UpdateStatement) {
|
||||
let (one, what, data) = split_params(params);
|
||||
let data = match data {
|
||||
Value::None | Value::Null => None,
|
||||
value => Some(Data::PatchExpression(value)),
|
||||
};
|
||||
(
|
||||
one,
|
||||
UpdateStatement {
|
||||
what,
|
||||
data,
|
||||
output: Some(Output::Diff),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // used by the the embedded database and `http`
|
||||
fn merge_statement(params: &mut [Value]) -> (bool, UpdateStatement) {
|
||||
let (one, what, data) = split_params(params);
|
||||
let data = match data {
|
||||
Value::None | Value::Null => None,
|
||||
value => Some(Data::MergeExpression(value)),
|
||||
};
|
||||
(
|
||||
one,
|
||||
UpdateStatement {
|
||||
what,
|
||||
data,
|
||||
output: Some(Output::After),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // used by the the embedded database and `http`
|
||||
fn select_statement(params: &mut [Value]) -> (bool, SelectStatement) {
|
||||
let (one, what, _) = split_params(params);
|
||||
(
|
||||
one,
|
||||
SelectStatement {
|
||||
what,
|
||||
expr: Fields(vec![Field::All]),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // used by the the embedded database and `http`
|
||||
fn delete_statement(params: &mut [Value]) -> DeleteStatement {
|
||||
let (_, what, _) = split_params(params);
|
||||
DeleteStatement {
|
||||
what,
|
||||
output: Some(Output::None),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
578
lib/src/api/engines/remote/http/mod.rs
Normal file
578
lib/src/api/engines/remote/http/mod.rs
Normal file
|
@ -0,0 +1,578 @@
|
|||
//! HTTP engine
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) mod native;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) mod wasm;
|
||||
|
||||
use crate::api::conn::DbResponse;
|
||||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::engines::create_statement;
|
||||
use crate::api::engines::delete_statement;
|
||||
use crate::api::engines::merge_statement;
|
||||
use crate::api::engines::patch_statement;
|
||||
use crate::api::engines::remote::Status;
|
||||
use crate::api::engines::select_statement;
|
||||
use crate::api::engines::update_statement;
|
||||
use crate::api::err::Error;
|
||||
use crate::api::method::query::QueryResult;
|
||||
use crate::api::opt::from_json;
|
||||
use crate::api::opt::from_value;
|
||||
use crate::api::Connect;
|
||||
use crate::api::Response as QueryResponse;
|
||||
use crate::api::Result;
|
||||
use crate::api::Surreal;
|
||||
use crate::opt::IntoEndpoint;
|
||||
use crate::sql::Array;
|
||||
use crate::sql::Strand;
|
||||
use crate::sql::Value;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use futures::TryStreamExt;
|
||||
use indexmap::IndexMap;
|
||||
use reqwest::header::HeaderMap;
|
||||
use reqwest::header::HeaderValue;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use reqwest::header::ACCEPT;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use reqwest::header::CONTENT_TYPE;
|
||||
use reqwest::RequestBuilder;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::marker::PhantomData;
|
||||
use std::mem;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::path::PathBuf;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio::fs::OpenOptions;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio::io;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio::io::AsyncReadExt;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio_util::compat::FuturesAsyncReadCompatExt;
|
||||
use url::Url;
|
||||
|
||||
const SQL_PATH: &str = "sql";
|
||||
const LOG: &str = "surrealdb::engines::remote::http";
|
||||
|
||||
/// The HTTP scheme used to connect to `http://` endpoints
|
||||
#[derive(Debug)]
|
||||
pub struct Http;
|
||||
|
||||
/// The HTTPS scheme used to connect to `https://` endpoints
|
||||
#[derive(Debug)]
|
||||
pub struct Https;
|
||||
|
||||
/// An HTTP client for communicating with the server via HTTP
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Client {
|
||||
method: Method,
|
||||
}
|
||||
|
||||
impl Surreal<Client> {
|
||||
/// Connects to a specific database endpoint, saving the connection on the static client
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use surrealdb::Surreal;
|
||||
/// use surrealdb::engines::remote::http::Client;
|
||||
/// use surrealdb::engines::remote::http::Http;
|
||||
///
|
||||
/// static DB: Surreal<Client> = Surreal::init();
|
||||
///
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> surrealdb::Result<()> {
|
||||
/// DB.connect::<Http>("localhost:8000").await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn connect<P>(
|
||||
&'static self,
|
||||
address: impl IntoEndpoint<P, Client = Client>,
|
||||
) -> Connect<Client, ()> {
|
||||
Connect {
|
||||
router: Some(&self.router),
|
||||
address: address.into_endpoint(),
|
||||
capacity: 0,
|
||||
client: PhantomData,
|
||||
response_type: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Auth {
|
||||
Basic {
|
||||
user: String,
|
||||
pass: String,
|
||||
},
|
||||
Bearer {
|
||||
token: String,
|
||||
},
|
||||
}
|
||||
|
||||
trait Authenticate {
|
||||
fn auth(self, auth: &Option<Auth>) -> Self;
|
||||
}
|
||||
|
||||
impl Authenticate for RequestBuilder {
|
||||
fn auth(self, auth: &Option<Auth>) -> Self {
|
||||
match auth {
|
||||
Some(Auth::Basic {
|
||||
user,
|
||||
pass,
|
||||
}) => self.basic_auth(user, Some(pass)),
|
||||
Some(Auth::Bearer {
|
||||
token,
|
||||
}) => self.bearer_auth(token),
|
||||
None => self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct HttpQueryResponse {
|
||||
status: Status,
|
||||
result: Option<serde_json::Value>,
|
||||
detail: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Root {
|
||||
user: String,
|
||||
pass: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AuthResponse {
|
||||
token: Option<String>,
|
||||
}
|
||||
|
||||
async fn submit_auth(request: RequestBuilder) -> Result<Value> {
|
||||
let response = request.send().await?.error_for_status()?;
|
||||
let text = response.text().await?;
|
||||
info!(target: LOG, "Response {text}");
|
||||
let response: AuthResponse = match serde_json::from_str(&text) {
|
||||
Ok(response) => response,
|
||||
Err(error) => {
|
||||
return Err(Error::FromJsonString {
|
||||
string: text,
|
||||
error: error.to_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
};
|
||||
Ok(response.token.filter(|token| token != "NONE").into())
|
||||
}
|
||||
|
||||
async fn query(request: RequestBuilder) -> Result<QueryResponse> {
|
||||
info!(target: LOG, "{request:?}");
|
||||
let response = request.send().await?.error_for_status()?;
|
||||
let text = response.text().await?;
|
||||
info!(target: LOG, "Response {text}");
|
||||
let responses: Vec<HttpQueryResponse> = match serde_json::from_str(&text) {
|
||||
Ok(vec) => vec,
|
||||
Err(error) => {
|
||||
return Err(Error::FromJsonString {
|
||||
string: text,
|
||||
error: error.to_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
};
|
||||
let mut map = IndexMap::<usize, QueryResult>::with_capacity(responses.len());
|
||||
for (index, response) in responses.into_iter().enumerate() {
|
||||
match response.status {
|
||||
Status::Ok => {
|
||||
if let Some(value) = response.result {
|
||||
match from_json(value) {
|
||||
Value::Array(Array(array)) => map.insert(index, Ok(array).into()),
|
||||
Value::None | Value::Null => map.insert(index, Ok(vec![]).into()),
|
||||
value => map.insert(index, Ok(vec![value]).into()),
|
||||
};
|
||||
}
|
||||
}
|
||||
Status::Err => {
|
||||
if let Some(error) = response.detail {
|
||||
map.insert(index, Err(Error::Query(error).into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(QueryResponse(map))
|
||||
}
|
||||
|
||||
async fn take(one: bool, request: RequestBuilder) -> Result<Value> {
|
||||
if let Some(result) = query(request).await?.0.remove(&0) {
|
||||
let mut vec = result?;
|
||||
match one {
|
||||
true => match vec.pop() {
|
||||
Some(Value::Array(Array(mut vec))) => {
|
||||
if let [value] = &mut vec[..] {
|
||||
return Ok(mem::take(value));
|
||||
}
|
||||
}
|
||||
Some(Value::None | Value::Null) | None => {}
|
||||
Some(value) => {
|
||||
return Ok(value);
|
||||
}
|
||||
},
|
||||
false => {
|
||||
return Ok(Value::Array(Array(vec)));
|
||||
}
|
||||
}
|
||||
}
|
||||
match one {
|
||||
true => Ok(Value::None),
|
||||
false => Ok(Value::Array(Array(vec![]))),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
async fn export(request: RequestBuilder, path: PathBuf) -> Result<Value> {
|
||||
let mut file =
|
||||
match OpenOptions::new().write(true).create(true).truncate(true).open(&path).await {
|
||||
Ok(path) => path,
|
||||
Err(error) => {
|
||||
return Err(Error::FileOpen {
|
||||
path,
|
||||
error,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
};
|
||||
let mut response = request
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.bytes_stream()
|
||||
.map_err(|e| futures::io::Error::new(futures::io::ErrorKind::Other, e))
|
||||
.into_async_read()
|
||||
.compat();
|
||||
if let Err(error) = io::copy(&mut response, &mut file).await {
|
||||
return Err(Error::FileRead {
|
||||
path,
|
||||
error,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
Ok(Value::None)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
async fn import(request: RequestBuilder, path: PathBuf) -> Result<Value> {
|
||||
let mut file = match OpenOptions::new().read(true).open(&path).await {
|
||||
Ok(path) => path,
|
||||
Err(error) => {
|
||||
return Err(Error::FileOpen {
|
||||
path,
|
||||
error,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
};
|
||||
let mut contents = vec![];
|
||||
if let Err(error) = file.read_to_end(&mut contents).await {
|
||||
return Err(Error::FileRead {
|
||||
path,
|
||||
error,
|
||||
}
|
||||
.into());
|
||||
}
|
||||
// ideally we should pass `file` directly into the body
|
||||
// but currently that results in
|
||||
// "HTTP status client error (405 Method Not Allowed) for url"
|
||||
request.body(contents).send().await?.error_for_status()?;
|
||||
Ok(Value::None)
|
||||
}
|
||||
|
||||
async fn version(request: RequestBuilder) -> Result<Value> {
|
||||
let response = request.send().await?.error_for_status()?;
|
||||
let version = response.text().await?;
|
||||
Ok(version.into())
|
||||
}
|
||||
|
||||
pub(crate) async fn health(request: RequestBuilder) -> Result<Value> {
|
||||
request.send().await?.error_for_status()?;
|
||||
Ok(Value::None)
|
||||
}
|
||||
|
||||
async fn router(
|
||||
(_, method, param): (i64, Method, Param),
|
||||
base_url: &Url,
|
||||
client: &reqwest::Client,
|
||||
headers: &mut HeaderMap,
|
||||
vars: &mut IndexMap<String, String>,
|
||||
auth: &mut Option<Auth>,
|
||||
) -> Result<DbResponse> {
|
||||
let mut params = match param.query {
|
||||
Some((query, bindings)) => {
|
||||
vec![query.to_string().into(), bindings.into()]
|
||||
}
|
||||
None => param.other,
|
||||
};
|
||||
|
||||
match method {
|
||||
Method::Use => {
|
||||
let path = base_url.join(SQL_PATH)?;
|
||||
let (ns, db) = match &mut params[..] {
|
||||
[Value::Strand(Strand(ns)), Value::Strand(Strand(db))] => {
|
||||
(mem::take(ns), mem::take(db))
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let ns = match HeaderValue::try_from(&ns) {
|
||||
Ok(ns) => ns,
|
||||
Err(_) => {
|
||||
return Err(Error::InvalidNsName(ns).into());
|
||||
}
|
||||
};
|
||||
let db = match HeaderValue::try_from(&db) {
|
||||
Ok(db) => db,
|
||||
Err(_) => {
|
||||
return Err(Error::InvalidDbName(db).into());
|
||||
}
|
||||
};
|
||||
let request = client
|
||||
.post(path)
|
||||
.headers(headers.clone())
|
||||
.header("NS", &ns)
|
||||
.header("DB", &db)
|
||||
.auth(&auth)
|
||||
.body("RETURN true");
|
||||
take(true, request).await?;
|
||||
headers.insert("NS", ns);
|
||||
headers.insert("DB", db);
|
||||
Ok(DbResponse::Other(Value::None))
|
||||
}
|
||||
Method::Signin => {
|
||||
let path = base_url.join(Method::Signin.as_str())?;
|
||||
let credentials = match &mut params[..] {
|
||||
[credentials] => match serde_json::to_string(credentials) {
|
||||
Ok(json) => json,
|
||||
Err(error) => {
|
||||
return Err(Error::ToJsonString {
|
||||
value: mem::take(credentials),
|
||||
error: error.to_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let request = client.post(path).headers(headers.clone()).auth(&auth).body(credentials);
|
||||
let value = submit_auth(request).await?;
|
||||
if let [credentials] = &mut params[..] {
|
||||
if let Ok(Root {
|
||||
user,
|
||||
pass,
|
||||
}) = from_value(mem::take(credentials))
|
||||
{
|
||||
*auth = Some(Auth::Basic {
|
||||
user,
|
||||
pass,
|
||||
});
|
||||
} else {
|
||||
*auth = Some(Auth::Bearer {
|
||||
token: value.to_strand().as_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(DbResponse::Other(value))
|
||||
}
|
||||
Method::Signup => {
|
||||
let path = base_url.join(Method::Signup.as_str())?;
|
||||
let credentials = match &mut params[..] {
|
||||
[credentials] => match serde_json::to_string(credentials) {
|
||||
Ok(json) => json,
|
||||
Err(error) => {
|
||||
return Err(Error::ToJsonString {
|
||||
value: mem::take(credentials),
|
||||
error: error.to_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
},
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let request = client.post(path).headers(headers.clone()).auth(&auth).body(credentials);
|
||||
let value = submit_auth(request).await?;
|
||||
Ok(DbResponse::Other(value))
|
||||
}
|
||||
Method::Authenticate => {
|
||||
let path = base_url.join(SQL_PATH)?;
|
||||
let token = match &mut params[..1] {
|
||||
[Value::Strand(Strand(token))] => mem::take(token),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let request =
|
||||
client.post(path).headers(headers.clone()).bearer_auth(&token).body("RETURN true");
|
||||
take(true, request).await?;
|
||||
*auth = Some(Auth::Bearer {
|
||||
token,
|
||||
});
|
||||
Ok(DbResponse::Other(Value::None))
|
||||
}
|
||||
Method::Invalidate => {
|
||||
*auth = None;
|
||||
Ok(DbResponse::Other(Value::None))
|
||||
}
|
||||
Method::Create => {
|
||||
let path = base_url.join(SQL_PATH)?;
|
||||
let statement = create_statement(&mut params);
|
||||
let request =
|
||||
client.post(path).headers(headers.clone()).auth(&auth).body(statement.to_string());
|
||||
let value = take(true, request).await?;
|
||||
Ok(DbResponse::Other(value))
|
||||
}
|
||||
Method::Update => {
|
||||
let path = base_url.join(SQL_PATH)?;
|
||||
let (one, statement) = update_statement(&mut params);
|
||||
let request =
|
||||
client.post(path).headers(headers.clone()).auth(&auth).body(statement.to_string());
|
||||
let value = take(one, request).await?;
|
||||
Ok(DbResponse::Other(value))
|
||||
}
|
||||
Method::Patch => {
|
||||
let path = base_url.join(SQL_PATH)?;
|
||||
let (one, statement) = patch_statement(&mut params);
|
||||
let request =
|
||||
client.post(path).headers(headers.clone()).auth(&auth).body(statement.to_string());
|
||||
let value = take(one, request).await?;
|
||||
Ok(DbResponse::Other(value))
|
||||
}
|
||||
Method::Merge => {
|
||||
let path = base_url.join(SQL_PATH)?;
|
||||
let (one, statement) = merge_statement(&mut params);
|
||||
let request =
|
||||
client.post(path).headers(headers.clone()).auth(&auth).body(statement.to_string());
|
||||
let value = take(one, request).await?;
|
||||
Ok(DbResponse::Other(value))
|
||||
}
|
||||
Method::Select => {
|
||||
let path = base_url.join(SQL_PATH)?;
|
||||
let (one, statement) = select_statement(&mut params);
|
||||
let request =
|
||||
client.post(path).headers(headers.clone()).auth(&auth).body(statement.to_string());
|
||||
let value = take(one, request).await?;
|
||||
Ok(DbResponse::Other(value))
|
||||
}
|
||||
Method::Delete => {
|
||||
let path = base_url.join(SQL_PATH)?;
|
||||
let statement = delete_statement(&mut params);
|
||||
let request =
|
||||
client.post(path).headers(headers.clone()).auth(&auth).body(statement.to_string());
|
||||
let value = take(true, request).await?;
|
||||
Ok(DbResponse::Other(value))
|
||||
}
|
||||
Method::Query => {
|
||||
let path = base_url.join(SQL_PATH)?;
|
||||
let mut request = client.post(path).headers(headers.clone()).query(&vars).auth(&auth);
|
||||
match &mut params[..] {
|
||||
[Value::Strand(Strand(statements))] => {
|
||||
request = request.body(mem::take(statements));
|
||||
}
|
||||
[Value::Strand(Strand(statements)), Value::Object(bindings)] => {
|
||||
let bindings: Vec<_> =
|
||||
bindings.iter().map(|(key, value)| (key, value.to_string())).collect();
|
||||
request = request.query(&bindings).body(mem::take(statements));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
let values = query(request).await?;
|
||||
Ok(DbResponse::Query(values))
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
Method::Export | Method::Import => unreachable!(),
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
Method::Export => {
|
||||
let path = base_url.join(Method::Export.as_str())?;
|
||||
let file = param.file.expect("file to export into");
|
||||
let request = client
|
||||
.get(path)
|
||||
.headers(headers.clone())
|
||||
.auth(&auth)
|
||||
.header(ACCEPT, "application/octet-stream");
|
||||
let value = export(request, file).await?;
|
||||
Ok(DbResponse::Other(value))
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
Method::Import => {
|
||||
let path = base_url.join(Method::Import.as_str())?;
|
||||
let file = param.file.expect("file to import from");
|
||||
let request = client
|
||||
.post(path)
|
||||
.headers(headers.clone())
|
||||
.auth(&auth)
|
||||
.header(CONTENT_TYPE, "application/octet-stream");
|
||||
let value = import(request, file).await?;
|
||||
Ok(DbResponse::Other(value))
|
||||
}
|
||||
Method::Health => {
|
||||
let path = base_url.join(Method::Health.as_str())?;
|
||||
let request = client.get(path);
|
||||
let value = health(request).await?;
|
||||
Ok(DbResponse::Other(value))
|
||||
}
|
||||
Method::Version => {
|
||||
let path = base_url.join(method.as_str())?;
|
||||
let request = client.get(path);
|
||||
let value = version(request).await?;
|
||||
Ok(DbResponse::Other(value))
|
||||
}
|
||||
Method::Set => {
|
||||
let path = base_url.join(SQL_PATH)?;
|
||||
let (key, value) = match &mut params[..2] {
|
||||
[Value::Strand(Strand(key)), value] => (mem::take(key), value.to_string()),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let request = client
|
||||
.post(path)
|
||||
.headers(headers.clone())
|
||||
.auth(&auth)
|
||||
.query(&[(key.as_str(), value.as_str())])
|
||||
.body(format!("RETURN ${key}"));
|
||||
take(true, request).await?;
|
||||
vars.insert(key, value);
|
||||
Ok(DbResponse::Other(Value::None))
|
||||
}
|
||||
Method::Unset => {
|
||||
if let [Value::Strand(Strand(key))] = ¶ms[..1] {
|
||||
vars.remove(key);
|
||||
}
|
||||
Ok(DbResponse::Other(Value::None))
|
||||
}
|
||||
Method::Live => {
|
||||
let path = base_url.join(SQL_PATH)?;
|
||||
let table = match ¶ms[..] {
|
||||
[table] => table.to_string(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let request = client
|
||||
.post(path)
|
||||
.headers(headers.clone())
|
||||
.auth(&auth)
|
||||
.query(&[("table", table)])
|
||||
.body("LIVE SELECT * FROM type::table($table)");
|
||||
let value = take(true, request).await?;
|
||||
Ok(DbResponse::Other(value))
|
||||
}
|
||||
Method::Kill => {
|
||||
let path = base_url.join(SQL_PATH)?;
|
||||
let id = match ¶ms[..] {
|
||||
[id] => id.to_string(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let request = client
|
||||
.post(path)
|
||||
.headers(headers.clone())
|
||||
.auth(&auth)
|
||||
.query(&[("id", id)])
|
||||
.body("KILL type::string($id)");
|
||||
let value = take(true, request).await?;
|
||||
Ok(DbResponse::Other(value))
|
||||
}
|
||||
}
|
||||
}
|
167
lib/src/api/engines/remote/http/native.rs
Normal file
167
lib/src/api/engines/remote/http/native.rs
Normal file
|
@ -0,0 +1,167 @@
|
|||
use super::Client;
|
||||
use super::LOG;
|
||||
use crate::api::conn::Connection;
|
||||
use crate::api::conn::DbResponse;
|
||||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Route;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::opt::from_value;
|
||||
use crate::api::opt::Endpoint;
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
use crate::api::opt::Tls;
|
||||
use crate::api::ExtraFeatures;
|
||||
use crate::api::Response as QueryResponse;
|
||||
use crate::api::Result;
|
||||
use crate::api::Surreal;
|
||||
use flume::Receiver;
|
||||
use futures::StreamExt;
|
||||
use indexmap::IndexMap;
|
||||
use once_cell::sync::OnceCell;
|
||||
use reqwest::header::HeaderMap;
|
||||
use reqwest::header::HeaderValue;
|
||||
use reqwest::header::ACCEPT;
|
||||
use reqwest::ClientBuilder;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::collections::HashSet;
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
use std::sync::atomic::AtomicI64;
|
||||
use std::sync::Arc;
|
||||
use url::Url;
|
||||
|
||||
impl crate::api::Connection for Client {}
|
||||
|
||||
impl Connection for Client {
|
||||
fn new(method: Method) -> Self {
|
||||
Self {
|
||||
method,
|
||||
}
|
||||
}
|
||||
|
||||
fn connect(
|
||||
address: Endpoint,
|
||||
capacity: usize,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Surreal<Self>>> + Send + Sync + 'static>> {
|
||||
Box::pin(async move {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut builder = ClientBuilder::new().default_headers(headers);
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
if let Some(tls) = address.tls_config {
|
||||
builder = match tls {
|
||||
#[cfg(feature = "native-tls")]
|
||||
Tls::Native(config) => builder.use_preconfigured_tls(config),
|
||||
#[cfg(feature = "rustls")]
|
||||
Tls::Rust(config) => builder.use_preconfigured_tls(config),
|
||||
};
|
||||
}
|
||||
|
||||
let client = builder.build()?;
|
||||
|
||||
let base_url = address.endpoint;
|
||||
|
||||
super::health(client.get(base_url.join(Method::Health.as_str())?)).await?;
|
||||
|
||||
let (route_tx, route_rx) = match capacity {
|
||||
0 => flume::unbounded(),
|
||||
capacity => flume::bounded(capacity),
|
||||
};
|
||||
|
||||
router(base_url, client, route_rx);
|
||||
|
||||
let mut features = HashSet::new();
|
||||
features.insert(ExtraFeatures::Auth);
|
||||
features.insert(ExtraFeatures::Backup);
|
||||
|
||||
Ok(Surreal {
|
||||
router: OnceCell::with_value(Arc::new(Router {
|
||||
features,
|
||||
conn: PhantomData,
|
||||
sender: route_tx,
|
||||
last_id: AtomicI64::new(0),
|
||||
})),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn send<'r>(
|
||||
&'r mut self,
|
||||
router: &'r Router<Self>,
|
||||
param: Param,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Receiver<Result<DbResponse>>>> + Send + Sync + 'r>> {
|
||||
Box::pin(async move {
|
||||
let (sender, receiver) = flume::bounded(1);
|
||||
let route = Route {
|
||||
request: (0, self.method, param),
|
||||
response: sender,
|
||||
};
|
||||
router.sender.send_async(Some(route)).await?;
|
||||
Ok(receiver)
|
||||
})
|
||||
}
|
||||
|
||||
fn recv<R>(
|
||||
&mut self,
|
||||
rx: Receiver<Result<DbResponse>>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<R>> + Send + Sync + '_>>
|
||||
where
|
||||
R: DeserializeOwned,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let response = rx.into_recv_async().await?;
|
||||
trace!(target: LOG, "Response {response:?}");
|
||||
match response? {
|
||||
DbResponse::Other(value) => from_value(value),
|
||||
DbResponse::Query(..) => unreachable!(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn recv_query(
|
||||
&mut self,
|
||||
rx: Receiver<Result<DbResponse>>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<QueryResponse>> + Send + Sync + '_>> {
|
||||
Box::pin(async move {
|
||||
let response = rx.into_recv_async().await?;
|
||||
trace!(target: LOG, "Response {response:?}");
|
||||
match response? {
|
||||
DbResponse::Query(results) => Ok(results),
|
||||
DbResponse::Other(..) => unreachable!(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn router(base_url: Url, client: reqwest::Client, route_rx: Receiver<Option<Route>>) {
|
||||
tokio::spawn(async move {
|
||||
let mut headers = HeaderMap::new();
|
||||
let mut vars = IndexMap::new();
|
||||
let mut auth = None;
|
||||
let mut stream = route_rx.into_stream();
|
||||
|
||||
while let Some(Some(route)) = stream.next().await {
|
||||
match super::router(
|
||||
route.request,
|
||||
&base_url,
|
||||
&client,
|
||||
&mut headers,
|
||||
&mut vars,
|
||||
&mut auth,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(value) => {
|
||||
let _ = route.response.into_send_async(Ok(value)).await;
|
||||
}
|
||||
Err(error) => {
|
||||
let _ = route.response.into_send_async(Err(error)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
178
lib/src/api/engines/remote/http/wasm.rs
Normal file
178
lib/src/api/engines/remote/http/wasm.rs
Normal file
|
@ -0,0 +1,178 @@
|
|||
use super::Client;
|
||||
use super::LOG;
|
||||
use crate::api::conn::Connection;
|
||||
use crate::api::conn::DbResponse;
|
||||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Route;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::opt::from_value;
|
||||
use crate::api::opt::Endpoint;
|
||||
use crate::api::ExtraFeatures;
|
||||
use crate::api::Response as QueryResponse;
|
||||
use crate::api::Result;
|
||||
use crate::api::Surreal;
|
||||
use flume::Receiver;
|
||||
use flume::Sender;
|
||||
use futures::StreamExt;
|
||||
use indexmap::IndexMap;
|
||||
use once_cell::sync::OnceCell;
|
||||
use reqwest::header::HeaderMap;
|
||||
use reqwest::header::HeaderValue;
|
||||
use reqwest::header::ACCEPT;
|
||||
use reqwest::ClientBuilder;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::collections::HashSet;
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
use std::sync::atomic::AtomicI64;
|
||||
use std::sync::Arc;
|
||||
use url::Url;
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
|
||||
impl crate::api::Connection for Client {}
|
||||
|
||||
impl Connection for Client {
|
||||
fn new(method: Method) -> Self {
|
||||
Self {
|
||||
method,
|
||||
}
|
||||
}
|
||||
|
||||
fn connect(
|
||||
address: Endpoint,
|
||||
capacity: usize,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Surreal<Self>>> + Send + Sync + 'static>> {
|
||||
Box::pin(async move {
|
||||
let (route_tx, route_rx) = match capacity {
|
||||
0 => flume::unbounded(),
|
||||
capacity => flume::bounded(capacity),
|
||||
};
|
||||
|
||||
let (conn_tx, conn_rx) = flume::bounded(1);
|
||||
|
||||
router(address, conn_tx, route_rx);
|
||||
|
||||
if let Err(error) = conn_rx.into_recv_async().await? {
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
let mut features = HashSet::new();
|
||||
features.insert(ExtraFeatures::Auth);
|
||||
|
||||
Ok(Surreal {
|
||||
router: OnceCell::with_value(Arc::new(Router {
|
||||
features,
|
||||
conn: PhantomData,
|
||||
sender: route_tx,
|
||||
last_id: AtomicI64::new(0),
|
||||
})),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn send<'r>(
|
||||
&'r mut self,
|
||||
router: &'r Router<Self>,
|
||||
param: Param,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Receiver<Result<DbResponse>>>> + Send + Sync + 'r>> {
|
||||
Box::pin(async move {
|
||||
let (sender, receiver) = flume::bounded(1);
|
||||
trace!(target: LOG, "{param:?}");
|
||||
let route = Route {
|
||||
request: (0, self.method, param),
|
||||
response: sender,
|
||||
};
|
||||
router.sender.send_async(Some(route)).await?;
|
||||
Ok(receiver)
|
||||
})
|
||||
}
|
||||
|
||||
fn recv<R>(
|
||||
&mut self,
|
||||
rx: Receiver<Result<DbResponse>>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<R>> + Send + Sync + '_>>
|
||||
where
|
||||
R: DeserializeOwned,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let response = rx.into_recv_async().await?;
|
||||
trace!(target: LOG, "Response {response:?}");
|
||||
match response? {
|
||||
DbResponse::Other(value) => from_value(value),
|
||||
DbResponse::Query(..) => unreachable!(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn recv_query(
|
||||
&mut self,
|
||||
rx: Receiver<Result<DbResponse>>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<QueryResponse>> + Send + Sync + '_>> {
|
||||
Box::pin(async move {
|
||||
let response = rx.into_recv_async().await?;
|
||||
trace!(target: LOG, "Response {response:?}");
|
||||
match response? {
|
||||
DbResponse::Query(results) => Ok(results),
|
||||
DbResponse::Other(..) => unreachable!(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn client(base_url: &Url) -> Result<reqwest::Client> {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
|
||||
let builder = ClientBuilder::new().default_headers(headers);
|
||||
let client = builder.build()?;
|
||||
let health = base_url.join(Method::Health.as_str())?;
|
||||
super::health(client.get(health)).await?;
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
pub(crate) fn router(
|
||||
address: Endpoint,
|
||||
conn_tx: Sender<Result<()>>,
|
||||
route_rx: Receiver<Option<Route>>,
|
||||
) {
|
||||
spawn_local(async move {
|
||||
let base_url = address.endpoint;
|
||||
|
||||
let client = match client(&base_url).await {
|
||||
Ok(client) => {
|
||||
let _ = conn_tx.into_send_async(Ok(())).await;
|
||||
client
|
||||
}
|
||||
Err(error) => {
|
||||
let _ = conn_tx.into_send_async(Err(error.into())).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
let mut vars = IndexMap::new();
|
||||
let mut auth = None;
|
||||
let mut stream = route_rx.into_stream();
|
||||
|
||||
while let Some(Some(route)) = stream.next().await {
|
||||
match super::router(
|
||||
route.request,
|
||||
&base_url,
|
||||
&client,
|
||||
&mut headers,
|
||||
&mut vars,
|
||||
&mut auth,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(value) => {
|
||||
let _ = route.response.into_send_async(Ok(value)).await;
|
||||
}
|
||||
Err(error) => {
|
||||
let _ = route.response.into_send_async(Err(error)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
18
lib/src/api/engines/remote/mod.rs
Normal file
18
lib/src/api/engines/remote/mod.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
//! Protocols for communicating with the server
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[cfg(feature = "protocol-http")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "protocol-http")))]
|
||||
pub mod http;
|
||||
|
||||
#[cfg(feature = "protocol-ws")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "protocol-ws")))]
|
||||
pub mod ws;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub(crate) enum Status {
|
||||
Ok,
|
||||
Err,
|
||||
}
|
170
lib/src/api/engines/remote/ws/mod.rs
Normal file
170
lib/src/api/engines/remote/ws/mod.rs
Normal file
|
@ -0,0 +1,170 @@
|
|||
//! WebSocket engine
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) mod native;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) mod wasm;
|
||||
|
||||
use crate::api::conn::DbResponse;
|
||||
use crate::api::conn::Method;
|
||||
use crate::api::engines::remote::Status;
|
||||
use crate::api::err::Error;
|
||||
use crate::api::Connect;
|
||||
use crate::api::Response as QueryResponse;
|
||||
use crate::api::Result;
|
||||
use crate::api::Surreal;
|
||||
use crate::opt::IntoEndpoint;
|
||||
use crate::sql::Array;
|
||||
use crate::sql::Value;
|
||||
use serde::Deserialize;
|
||||
use std::marker::PhantomData;
|
||||
use std::mem;
|
||||
use std::time::Duration;
|
||||
|
||||
pub(crate) const PATH: &str = "rpc";
|
||||
const PING_INTERVAL: Duration = Duration::from_secs(5);
|
||||
const PING_METHOD: &str = "ping";
|
||||
const LOG: &str = "surrealdb::engines::remote::ws";
|
||||
|
||||
/// The WS scheme used to connect to `ws://` endpoints
|
||||
#[derive(Debug)]
|
||||
pub struct Ws;
|
||||
|
||||
/// The WSS scheme used to connect to `wss://` endpoints
|
||||
#[derive(Debug)]
|
||||
pub struct Wss;
|
||||
|
||||
/// A WebSocket client for communicating with the server via WebSockets
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Client {
|
||||
pub(crate) id: i64,
|
||||
method: Method,
|
||||
}
|
||||
|
||||
impl Surreal<Client> {
|
||||
/// Connects to a specific database endpoint, saving the connection on the static client
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use surrealdb::Surreal;
|
||||
/// use surrealdb::engines::remote::ws::Client;
|
||||
/// use surrealdb::engines::remote::ws::Ws;
|
||||
///
|
||||
/// static DB: Surreal<Client> = Surreal::init();
|
||||
///
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> surrealdb::Result<()> {
|
||||
/// DB.connect::<Ws>("localhost:8000").await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn connect<P>(
|
||||
&'static self,
|
||||
address: impl IntoEndpoint<P, Client = Client>,
|
||||
) -> Connect<Client, ()> {
|
||||
Connect {
|
||||
router: Some(&self.router),
|
||||
address: address.into_endpoint(),
|
||||
capacity: 0,
|
||||
client: PhantomData,
|
||||
response_type: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub(crate) struct Failure {
|
||||
pub(crate) code: i64,
|
||||
pub(crate) message: String,
|
||||
}
|
||||
|
||||
impl From<Failure> for Error {
|
||||
fn from(failure: Failure) -> Self {
|
||||
match failure.code {
|
||||
-32600 => Self::InvalidRequest(failure.message),
|
||||
-32602 => Self::InvalidParams(failure.message),
|
||||
-32603 => Self::InternalError(failure.message),
|
||||
-32700 => Self::ParseError(failure.message),
|
||||
_ => Self::Query(failure.message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub(crate) enum QueryMethodResponse {
|
||||
Value(Value),
|
||||
String(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub(crate) enum SuccessValue {
|
||||
Query(Vec<(String, Status, QueryMethodResponse)>),
|
||||
Other(Value),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) enum Content {
|
||||
#[serde(rename = "result")]
|
||||
Success(SuccessValue),
|
||||
#[serde(rename = "error")]
|
||||
Failure(Failure),
|
||||
}
|
||||
|
||||
impl DbResponse {
|
||||
fn from((method, content): (Method, Content)) -> Result<Self> {
|
||||
match content {
|
||||
Content::Success(SuccessValue::Query(results)) => Ok(DbResponse::Query(QueryResponse(
|
||||
results
|
||||
.into_iter()
|
||||
.map(|(_duration, status, result)| match status {
|
||||
Status::Ok => match result {
|
||||
QueryMethodResponse::Value(value) => match value {
|
||||
Value::Array(Array(values)) => Ok(values),
|
||||
Value::None | Value::Null => Ok(vec![]),
|
||||
value => Ok(vec![value]),
|
||||
},
|
||||
QueryMethodResponse::String(string) => Ok(vec![string.into()]),
|
||||
},
|
||||
Status::Err => match result {
|
||||
QueryMethodResponse::Value(message) => {
|
||||
Err(Error::Query(message.to_string()).into())
|
||||
}
|
||||
QueryMethodResponse::String(message) => {
|
||||
Err(Error::Query(message).into())
|
||||
}
|
||||
},
|
||||
})
|
||||
.enumerate()
|
||||
.collect(),
|
||||
))),
|
||||
Content::Success(SuccessValue::Other(mut value)) => {
|
||||
if let Method::Create | Method::Delete = method {
|
||||
if let Value::Array(Array(array)) = &mut value {
|
||||
match &mut array[..] {
|
||||
[] => {
|
||||
value = Value::None;
|
||||
}
|
||||
[v] => {
|
||||
value = mem::take(v);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(DbResponse::Other(value))
|
||||
}
|
||||
Content::Failure(failure) => Err(Error::from(failure).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct Response {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
id: Option<Value>,
|
||||
#[serde(flatten)]
|
||||
pub(crate) content: Content,
|
||||
}
|
467
lib/src/api/engines/remote/ws/native.rs
Normal file
467
lib/src/api/engines/remote/ws/native.rs
Normal file
|
@ -0,0 +1,467 @@
|
|||
use super::LOG;
|
||||
use super::PATH;
|
||||
use crate::api::conn::Connection;
|
||||
use crate::api::conn::DbResponse;
|
||||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Route;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::engines::remote::ws::Client;
|
||||
use crate::api::engines::remote::ws::Response;
|
||||
use crate::api::engines::remote::ws::PING_INTERVAL;
|
||||
use crate::api::engines::remote::ws::PING_METHOD;
|
||||
use crate::api::err::Error;
|
||||
use crate::api::opt::from_value;
|
||||
use crate::api::opt::Endpoint;
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
use crate::api::opt::Tls;
|
||||
use crate::api::ExtraFeatures;
|
||||
use crate::api::Response as QueryResponse;
|
||||
use crate::api::Result;
|
||||
use crate::api::Surreal;
|
||||
use crate::sql::Strand;
|
||||
use crate::sql::Value;
|
||||
use flume::Receiver;
|
||||
use futures::stream::SplitSink;
|
||||
use futures::SinkExt;
|
||||
use futures::StreamExt;
|
||||
use futures_concurrency::stream::Merge as _;
|
||||
use indexmap::IndexMap;
|
||||
use once_cell::sync::OnceCell;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::borrow::BorrowMut;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::mem;
|
||||
use std::pin::Pin;
|
||||
use std::sync::atomic::AtomicI64;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time;
|
||||
use tokio::time::MissedTickBehavior;
|
||||
use tokio_stream::wrappers::IntervalStream;
|
||||
use tokio_tungstenite::tungstenite::error::Error as WsError;
|
||||
use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tokio_tungstenite::Connector;
|
||||
use tokio_tungstenite::MaybeTlsStream;
|
||||
use tokio_tungstenite::WebSocketStream;
|
||||
use url::Url;
|
||||
|
||||
type WsResult<T> = std::result::Result<T, WsError>;
|
||||
|
||||
pub(crate) const MAX_MESSAGE_SIZE: usize = 64 << 20; // 64 MiB
|
||||
pub(crate) const MAX_FRAME_SIZE: usize = 16 << 20; // 16 MiB
|
||||
|
||||
pub(crate) enum Either {
|
||||
Request(Option<Route>),
|
||||
Response(WsResult<Message>),
|
||||
Ping,
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
impl From<Tls> for Connector {
|
||||
fn from(tls: Tls) -> Self {
|
||||
match tls {
|
||||
#[cfg(feature = "native-tls")]
|
||||
Tls::Native(config) => Self::NativeTls(config),
|
||||
#[cfg(feature = "rustls")]
|
||||
Tls::Rust(config) => Self::Rustls(Arc::new(config)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn connect(
|
||||
url: &Url,
|
||||
config: Option<WebSocketConfig>,
|
||||
#[allow(unused_variables)] maybe_connector: Option<Connector>,
|
||||
) -> Result<WebSocketStream<MaybeTlsStream<TcpStream>>> {
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
let (socket, _) =
|
||||
tokio_tungstenite::connect_async_tls_with_config(url, config, maybe_connector).await?;
|
||||
|
||||
#[cfg(not(any(feature = "native-tls", feature = "rustls")))]
|
||||
let (socket, _) = tokio_tungstenite::connect_async_with_config(url, config).await?;
|
||||
|
||||
Ok(socket)
|
||||
}
|
||||
|
||||
impl crate::api::Connection for Client {}
|
||||
|
||||
impl Connection for Client {
|
||||
fn new(method: Method) -> Self {
|
||||
Self {
|
||||
id: 0,
|
||||
method,
|
||||
}
|
||||
}
|
||||
|
||||
fn connect(
|
||||
address: Endpoint,
|
||||
capacity: usize,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Surreal<Self>>> + Send + Sync + 'static>> {
|
||||
Box::pin(async move {
|
||||
let url = address.endpoint.join(PATH)?;
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
let maybe_connector = address.tls_config.map(Connector::from);
|
||||
#[cfg(not(any(feature = "native-tls", feature = "rustls")))]
|
||||
let maybe_connector = None;
|
||||
|
||||
let config = WebSocketConfig {
|
||||
max_send_queue: match capacity {
|
||||
0 => None,
|
||||
capacity => Some(capacity),
|
||||
},
|
||||
max_message_size: Some(MAX_MESSAGE_SIZE),
|
||||
max_frame_size: Some(MAX_FRAME_SIZE),
|
||||
accept_unmasked_frames: false,
|
||||
};
|
||||
|
||||
let socket = connect(&url, Some(config), maybe_connector.clone()).await?;
|
||||
|
||||
let (route_tx, route_rx) = match capacity {
|
||||
0 => flume::unbounded(),
|
||||
capacity => flume::bounded(capacity),
|
||||
};
|
||||
|
||||
router(url, maybe_connector, capacity, config, socket, route_rx);
|
||||
|
||||
let mut features = HashSet::new();
|
||||
features.insert(ExtraFeatures::Auth);
|
||||
|
||||
Ok(Surreal {
|
||||
router: OnceCell::with_value(Arc::new(Router {
|
||||
features,
|
||||
conn: PhantomData,
|
||||
sender: route_tx,
|
||||
last_id: AtomicI64::new(0),
|
||||
})),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn send<'r>(
|
||||
&'r mut self,
|
||||
router: &'r Router<Self>,
|
||||
param: Param,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Receiver<Result<DbResponse>>>> + Send + Sync + 'r>> {
|
||||
Box::pin(async move {
|
||||
self.id = router.next_id();
|
||||
let (sender, receiver) = flume::bounded(1);
|
||||
let route = Route {
|
||||
request: (self.id, self.method, param),
|
||||
response: sender,
|
||||
};
|
||||
router.sender.send_async(Some(route)).await?;
|
||||
Ok(receiver)
|
||||
})
|
||||
}
|
||||
|
||||
fn recv<R>(
|
||||
&mut self,
|
||||
rx: Receiver<Result<DbResponse>>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<R>> + Send + Sync + '_>>
|
||||
where
|
||||
R: DeserializeOwned,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let response = rx.into_recv_async().await?;
|
||||
match response? {
|
||||
DbResponse::Other(value) => from_value(value),
|
||||
DbResponse::Query(..) => unreachable!(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn recv_query(
|
||||
&mut self,
|
||||
rx: Receiver<Result<DbResponse>>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<QueryResponse>> + Send + Sync + '_>> {
|
||||
Box::pin(async move {
|
||||
let response = rx.into_recv_async().await?;
|
||||
match response? {
|
||||
DbResponse::Query(results) => Ok(results),
|
||||
DbResponse::Other(..) => unreachable!(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub(crate) fn router(
|
||||
url: Url,
|
||||
maybe_connector: Option<Connector>,
|
||||
capacity: usize,
|
||||
config: WebSocketConfig,
|
||||
mut socket: WebSocketStream<MaybeTlsStream<TcpStream>>,
|
||||
route_rx: Receiver<Option<Route>>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
let ping = {
|
||||
let mut request = BTreeMap::new();
|
||||
request.insert("method".to_owned(), PING_METHOD.into());
|
||||
let value = Value::from(request);
|
||||
Message::Binary(value.into())
|
||||
};
|
||||
|
||||
let mut vars = IndexMap::new();
|
||||
let mut replay = IndexMap::new();
|
||||
|
||||
'router: loop {
|
||||
let (socket_sink, socket_stream) = socket.split();
|
||||
let mut socket_sink = Socket(Some(socket_sink));
|
||||
|
||||
if let Socket(Some(socket_sink)) = &mut socket_sink {
|
||||
let mut routes = match capacity {
|
||||
0 => HashMap::new(),
|
||||
capacity => HashMap::with_capacity(capacity),
|
||||
};
|
||||
|
||||
let mut interval = time::interval(PING_INTERVAL);
|
||||
// don't bombard the server with pings if we miss some ticks
|
||||
interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
|
||||
// Delay sending the first ping
|
||||
interval.tick().await;
|
||||
|
||||
let pinger = IntervalStream::new(interval);
|
||||
|
||||
let streams = (
|
||||
socket_stream.map(Either::Response),
|
||||
route_rx.stream().map(Either::Request),
|
||||
pinger.map(|_| Either::Ping),
|
||||
);
|
||||
|
||||
let mut merged = streams.merge();
|
||||
let mut last_activity = Instant::now();
|
||||
|
||||
while let Some(either) = merged.next().await {
|
||||
match either {
|
||||
Either::Request(Some(Route {
|
||||
request,
|
||||
response,
|
||||
})) => {
|
||||
let (id, method, param) = request;
|
||||
let params = match param.query {
|
||||
Some((query, bindings)) => {
|
||||
vec![query.to_string().into(), bindings.into()]
|
||||
}
|
||||
None => param.other,
|
||||
};
|
||||
match method {
|
||||
Method::Set => {
|
||||
if let [Value::Strand(Strand(key)), value] = ¶ms[..2] {
|
||||
vars.insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
Method::Unset => {
|
||||
if let [Value::Strand(Strand(key))] = ¶ms[..1] {
|
||||
vars.remove(key);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let method_str = match method {
|
||||
Method::Health => PING_METHOD,
|
||||
_ => method.as_str(),
|
||||
};
|
||||
let message = {
|
||||
let mut request = BTreeMap::new();
|
||||
request.insert("id".to_owned(), Value::from(id));
|
||||
request.insert("method".to_owned(), method_str.into());
|
||||
if !params.is_empty() {
|
||||
request.insert("params".to_owned(), params.into());
|
||||
}
|
||||
let payload = Value::from(request);
|
||||
trace!(target: LOG, "Request {payload}");
|
||||
Message::Binary(payload.into())
|
||||
};
|
||||
if let Method::Authenticate
|
||||
| Method::Invalidate
|
||||
| Method::Signin
|
||||
| Method::Signup
|
||||
| Method::Use = method
|
||||
{
|
||||
replay.insert(method, message.clone());
|
||||
}
|
||||
match socket_sink.send(message).await {
|
||||
Ok(..) => {
|
||||
last_activity = Instant::now();
|
||||
match routes.entry(id) {
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert((method, response));
|
||||
}
|
||||
Entry::Occupied(..) => {
|
||||
let error = Error::DuplicateRequestId(id);
|
||||
if response
|
||||
.into_send_async(Err(error.into()))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
trace!(target: LOG, "Receiver dropped");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
let error = Error::Ws(error.to_string());
|
||||
if response.into_send_async(Err(error.into())).await.is_err() {
|
||||
trace!(target: LOG, "Receiver dropped");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Either::Response(result) => {
|
||||
last_activity = Instant::now();
|
||||
match result {
|
||||
Ok(message) => match Response::try_from(message) {
|
||||
Ok(option) => {
|
||||
if let Some(response) = option {
|
||||
trace!(target: LOG, "{response:?}");
|
||||
if let Some(id) = response.id {
|
||||
if let Some((method, sender)) =
|
||||
routes.remove(&id.as_int())
|
||||
{
|
||||
let _res = sender
|
||||
.into_send_async(DbResponse::from((
|
||||
method,
|
||||
response.content,
|
||||
)))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_error) => {
|
||||
trace!(target: LOG, "Failed to deserialise message");
|
||||
}
|
||||
},
|
||||
Err(error) => {
|
||||
match error {
|
||||
WsError::ConnectionClosed => {
|
||||
trace!(
|
||||
target: LOG,
|
||||
"Connection successfully closed on the server"
|
||||
);
|
||||
}
|
||||
error => {
|
||||
trace!(target: LOG, "{error}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Either::Ping => {
|
||||
// only ping if we haven't talked to the server recently
|
||||
if last_activity.elapsed() >= PING_INTERVAL {
|
||||
trace!(target: LOG, "Pinging the server");
|
||||
if let Err(error) = socket_sink.send(ping.clone()).await {
|
||||
trace!(target: LOG, "failed to ping the server; {error:?}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Either::Request(None) => {
|
||||
break 'router;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
'reconnect: loop {
|
||||
trace!(target: LOG, "Reconnecting...");
|
||||
match connect(&url, Some(config), maybe_connector.clone()).await {
|
||||
Ok(s) => {
|
||||
socket = s;
|
||||
for (_, message) in &replay {
|
||||
if let Err(error) = socket.send(message.clone()).await {
|
||||
trace!(target: LOG, "{error}");
|
||||
time::sleep(time::Duration::from_secs(1)).await;
|
||||
continue 'reconnect;
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "protocol-ws")]
|
||||
for (key, value) in &vars {
|
||||
let mut request = BTreeMap::new();
|
||||
request.insert("method".to_owned(), Method::Set.as_str().into());
|
||||
request.insert(
|
||||
"params".to_owned(),
|
||||
vec![key.as_str().into(), value.clone()].into(),
|
||||
);
|
||||
let payload = Value::from(request);
|
||||
trace!(target: LOG, "Request {payload}");
|
||||
if let Err(error) = socket.send(Message::Binary(payload.into())).await {
|
||||
trace!(target: LOG, "{error}");
|
||||
time::sleep(time::Duration::from_secs(1)).await;
|
||||
continue 'reconnect;
|
||||
}
|
||||
}
|
||||
trace!(target: LOG, "Reconnected successfully");
|
||||
break;
|
||||
}
|
||||
Err(error) => {
|
||||
trace!(target: LOG, "Failed to reconnect; {error}");
|
||||
time::sleep(time::Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
impl Response {
|
||||
fn try_from(message: Message) -> Result<Option<Self>> {
|
||||
match message {
|
||||
Message::Text(text) => {
|
||||
trace!(target: LOG, "Received an unexpected text message; {text}");
|
||||
Ok(None)
|
||||
}
|
||||
Message::Binary(binary) => msgpack::from_slice(&binary).map(Some).map_err(|error| {
|
||||
Error::ResponseFromBinary {
|
||||
binary,
|
||||
error,
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
Message::Ping(..) => {
|
||||
trace!(target: LOG, "Received a ping from the server");
|
||||
Ok(None)
|
||||
}
|
||||
Message::Pong(..) => {
|
||||
trace!(target: LOG, "Received a pong from the server");
|
||||
Ok(None)
|
||||
}
|
||||
Message::Frame(..) => {
|
||||
trace!(target: LOG, "Received an unexpected raw frame");
|
||||
Ok(None)
|
||||
}
|
||||
Message::Close(..) => {
|
||||
trace!(target: LOG, "Received an unexpected close message");
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Socket(Option<SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>>);
|
||||
|
||||
impl Drop for Socket {
|
||||
fn drop(&mut self) {
|
||||
if let Some(mut conn) = mem::take(&mut self.0) {
|
||||
futures::executor::block_on(async move {
|
||||
match conn.borrow_mut().close().await {
|
||||
Ok(..) => trace!(target: LOG, "Connection closed successfully"),
|
||||
Err(error) => {
|
||||
trace!(target: LOG, "Failed to close database connection; {error}")
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
417
lib/src/api/engines/remote/ws/wasm.rs
Normal file
417
lib/src/api/engines/remote/ws/wasm.rs
Normal file
|
@ -0,0 +1,417 @@
|
|||
use super::LOG;
|
||||
use super::PATH;
|
||||
use crate::api::conn::Connection;
|
||||
use crate::api::conn::DbResponse;
|
||||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Route;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::engines::remote::ws::Client;
|
||||
use crate::api::engines::remote::ws::Response;
|
||||
use crate::api::engines::remote::ws::PING_INTERVAL;
|
||||
use crate::api::engines::remote::ws::PING_METHOD;
|
||||
use crate::api::err::Error;
|
||||
use crate::api::opt::from_value;
|
||||
use crate::api::opt::Endpoint;
|
||||
use crate::api::ExtraFeatures;
|
||||
use crate::api::Response as QueryResponse;
|
||||
use crate::api::Result;
|
||||
use crate::api::Surreal;
|
||||
use crate::sql::Strand;
|
||||
use crate::sql::Value;
|
||||
use flume::Receiver;
|
||||
use flume::Sender;
|
||||
use futures::SinkExt;
|
||||
use futures::StreamExt;
|
||||
use futures_concurrency::stream::Merge as _;
|
||||
use indexmap::IndexMap;
|
||||
use once_cell::sync::OnceCell;
|
||||
use pharos::Channel;
|
||||
use pharos::Observable;
|
||||
use pharos::ObserveConfig;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
use std::sync::atomic::AtomicI64;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use tokio::time;
|
||||
use tokio::time::MissedTickBehavior;
|
||||
use tokio_stream::wrappers::IntervalStream;
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
use ws_stream_wasm::WsEvent;
|
||||
use ws_stream_wasm::WsMessage as Message;
|
||||
use ws_stream_wasm::WsMeta;
|
||||
|
||||
pub(crate) enum Either {
|
||||
Request(Option<Route>),
|
||||
Response(Message),
|
||||
Event(WsEvent),
|
||||
Ping,
|
||||
}
|
||||
|
||||
impl crate::api::Connection for Client {}
|
||||
|
||||
impl Connection for Client {
|
||||
fn new(method: Method) -> Self {
|
||||
Self {
|
||||
id: 0,
|
||||
method,
|
||||
}
|
||||
}
|
||||
|
||||
fn connect(
|
||||
mut address: Endpoint,
|
||||
capacity: usize,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Surreal<Self>>> + Send + Sync + 'static>> {
|
||||
Box::pin(async move {
|
||||
address.endpoint = address.endpoint.join(PATH)?;
|
||||
|
||||
let (route_tx, route_rx) = match capacity {
|
||||
0 => flume::unbounded(),
|
||||
capacity => flume::bounded(capacity),
|
||||
};
|
||||
|
||||
let (conn_tx, conn_rx) = flume::bounded(1);
|
||||
|
||||
router(address, capacity, conn_tx, route_rx);
|
||||
|
||||
if let Err(error) = conn_rx.into_recv_async().await? {
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
let mut features = HashSet::new();
|
||||
features.insert(ExtraFeatures::Auth);
|
||||
|
||||
Ok(Surreal {
|
||||
router: OnceCell::with_value(Arc::new(Router {
|
||||
features,
|
||||
conn: PhantomData,
|
||||
sender: route_tx,
|
||||
last_id: AtomicI64::new(0),
|
||||
})),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn send<'r>(
|
||||
&'r mut self,
|
||||
router: &'r Router<Self>,
|
||||
param: Param,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Receiver<Result<DbResponse>>>> + Send + Sync + 'r>> {
|
||||
Box::pin(async move {
|
||||
self.id = router.next_id();
|
||||
let (sender, receiver) = flume::bounded(1);
|
||||
let route = Route {
|
||||
request: (self.id, self.method, param),
|
||||
response: sender,
|
||||
};
|
||||
router.sender.send_async(Some(route)).await?;
|
||||
Ok(receiver)
|
||||
})
|
||||
}
|
||||
|
||||
fn recv<R>(
|
||||
&mut self,
|
||||
rx: Receiver<Result<DbResponse>>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<R>> + Send + Sync + '_>>
|
||||
where
|
||||
R: DeserializeOwned,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let response = rx.into_recv_async().await?;
|
||||
match response? {
|
||||
DbResponse::Other(value) => from_value(value),
|
||||
DbResponse::Query(..) => unreachable!(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn recv_query(
|
||||
&mut self,
|
||||
rx: Receiver<Result<DbResponse>>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<QueryResponse>> + Send + Sync + '_>> {
|
||||
Box::pin(async move {
|
||||
let response = rx.into_recv_async().await?;
|
||||
match response? {
|
||||
DbResponse::Query(results) => Ok(results),
|
||||
DbResponse::Other(..) => unreachable!(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn router(
|
||||
address: Endpoint,
|
||||
capacity: usize,
|
||||
conn_tx: Sender<Result<()>>,
|
||||
route_rx: Receiver<Option<Route>>,
|
||||
) {
|
||||
spawn_local(async move {
|
||||
let (mut ws, mut socket) = match WsMeta::connect(&address.endpoint, None).await {
|
||||
Ok(pair) => pair,
|
||||
Err(error) => {
|
||||
let _ = conn_tx.into_send_async(Err(error.into())).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut events = {
|
||||
let result = match capacity {
|
||||
0 => ws.observe(ObserveConfig::default()).await,
|
||||
capacity => ws.observe(Channel::Bounded(capacity).into()).await,
|
||||
};
|
||||
match result {
|
||||
Ok(events) => events,
|
||||
Err(error) => {
|
||||
let _ = conn_tx.into_send_async(Err(error.into())).await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let _ = conn_tx.into_send_async(Ok(())).await;
|
||||
|
||||
let ping = {
|
||||
let mut request = BTreeMap::new();
|
||||
request.insert("method".to_owned(), PING_METHOD.into());
|
||||
let value = Value::from(request);
|
||||
Message::Binary(value.into())
|
||||
};
|
||||
|
||||
let mut vars = IndexMap::new();
|
||||
let mut replay = IndexMap::new();
|
||||
|
||||
'router: loop {
|
||||
let (mut socket_sink, socket_stream) = socket.split();
|
||||
|
||||
let mut routes = match capacity {
|
||||
0 => HashMap::new(),
|
||||
capacity => HashMap::with_capacity(capacity),
|
||||
};
|
||||
|
||||
let mut interval = time::interval(PING_INTERVAL);
|
||||
// don't bombard the server with pings if we miss some ticks
|
||||
interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
|
||||
// Delay sending the first ping
|
||||
interval.tick().await;
|
||||
|
||||
let pinger = IntervalStream::new(interval);
|
||||
|
||||
let streams = (
|
||||
socket_stream.map(Either::Response),
|
||||
route_rx.stream().map(Either::Request),
|
||||
pinger.map(|_| Either::Ping),
|
||||
events.map(Either::Event),
|
||||
);
|
||||
|
||||
let mut merged = streams.merge();
|
||||
let mut last_activity = Instant::now();
|
||||
|
||||
while let Some(either) = merged.next().await {
|
||||
match either {
|
||||
Either::Request(Some(Route {
|
||||
request,
|
||||
response,
|
||||
})) => {
|
||||
let (id, method, param) = request;
|
||||
let params = match param.query {
|
||||
Some((query, bindings)) => {
|
||||
vec![query.to_string().into(), bindings.into()]
|
||||
}
|
||||
None => param.other,
|
||||
};
|
||||
match method {
|
||||
Method::Set => {
|
||||
if let [Value::Strand(Strand(key)), value] = ¶ms[..2] {
|
||||
vars.insert(key.to_owned(), value.clone());
|
||||
}
|
||||
}
|
||||
Method::Unset => {
|
||||
if let [Value::Strand(Strand(key))] = ¶ms[..1] {
|
||||
vars.remove(key);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let method_str = match method {
|
||||
Method::Health => PING_METHOD,
|
||||
_ => method.as_str(),
|
||||
};
|
||||
let message = {
|
||||
let mut request = BTreeMap::new();
|
||||
request.insert("id".to_owned(), Value::from(id));
|
||||
request.insert("method".to_owned(), method_str.into());
|
||||
if !params.is_empty() {
|
||||
request.insert("params".to_owned(), params.into());
|
||||
}
|
||||
let payload = Value::from(request);
|
||||
trace!(target: LOG, "Request {payload}");
|
||||
Message::Binary(payload.into())
|
||||
};
|
||||
if let Method::Authenticate
|
||||
| Method::Invalidate
|
||||
| Method::Signin
|
||||
| Method::Signup
|
||||
| Method::Use = method
|
||||
{
|
||||
replay.insert(method, message.clone());
|
||||
}
|
||||
match socket_sink.send(message).await {
|
||||
Ok(..) => {
|
||||
last_activity = Instant::now();
|
||||
match routes.entry(id) {
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert((method, response));
|
||||
}
|
||||
Entry::Occupied(..) => {
|
||||
let error = Error::DuplicateRequestId(id);
|
||||
if response
|
||||
.into_send_async(Err(error.into()))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
trace!(target: LOG, "Receiver dropped");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
let error = Error::Ws(error.to_string());
|
||||
if response.into_send_async(Err(error.into())).await.is_err() {
|
||||
trace!(target: LOG, "Receiver dropped");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Either::Response(message) => {
|
||||
last_activity = Instant::now();
|
||||
match Response::try_from(message) {
|
||||
Ok(option) => {
|
||||
if let Some(response) = option {
|
||||
trace!(target: LOG, "{response:?}");
|
||||
if let Some(id) = response.id {
|
||||
if let Some((method, sender)) = routes.remove(&id.as_int())
|
||||
{
|
||||
let _ = sender
|
||||
.into_send_async(DbResponse::from((
|
||||
method,
|
||||
response.content,
|
||||
)))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_error) => {
|
||||
trace!(target: LOG, "Failed to deserialise message");
|
||||
}
|
||||
}
|
||||
}
|
||||
Either::Event(event) => match event {
|
||||
WsEvent::Error => {
|
||||
trace!(target: LOG, "connection errored");
|
||||
break;
|
||||
}
|
||||
WsEvent::WsErr(error) => {
|
||||
trace!(target: LOG, "{error}");
|
||||
}
|
||||
WsEvent::Closed(..) => {
|
||||
trace!(target: LOG, "connection closed");
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Either::Ping => {
|
||||
// only ping if we haven't talked to the server recently
|
||||
if last_activity.elapsed() >= PING_INTERVAL {
|
||||
trace!(target: LOG, "Pinging the server");
|
||||
if let Err(error) = socket_sink.send(ping.clone()).await {
|
||||
trace!(target: LOG, "failed to ping the server; {error:?}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Either::Request(None) => {
|
||||
break 'router;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
'reconnect: loop {
|
||||
trace!(target: LOG, "Reconnecting...");
|
||||
match WsMeta::connect(&address.endpoint, None).await {
|
||||
Ok((mut meta, stream)) => {
|
||||
socket = stream;
|
||||
events = {
|
||||
let result = match capacity {
|
||||
0 => meta.observe(ObserveConfig::default()).await,
|
||||
capacity => meta.observe(Channel::Bounded(capacity).into()).await,
|
||||
};
|
||||
match result {
|
||||
Ok(events) => events,
|
||||
Err(error) => {
|
||||
trace!(target: LOG, "{error}");
|
||||
time::sleep(time::Duration::from_secs(1)).await;
|
||||
continue 'reconnect;
|
||||
}
|
||||
}
|
||||
};
|
||||
for (_, message) in &replay {
|
||||
if let Err(error) = socket.send(message.clone()).await {
|
||||
trace!(target: LOG, "{error}");
|
||||
time::sleep(time::Duration::from_secs(1)).await;
|
||||
continue 'reconnect;
|
||||
}
|
||||
}
|
||||
for (key, value) in &vars {
|
||||
let mut request = BTreeMap::new();
|
||||
request.insert("method".to_owned(), Method::Set.as_str().into());
|
||||
request.insert(
|
||||
"params".to_owned(),
|
||||
vec![key.as_str().into(), value.clone()].into(),
|
||||
);
|
||||
let payload = Value::from(request);
|
||||
trace!(target: LOG, "Request {payload}");
|
||||
if let Err(error) = socket.send(Message::Binary(payload.into())).await {
|
||||
trace!(target: LOG, "{error}");
|
||||
time::sleep(time::Duration::from_secs(1)).await;
|
||||
continue 'reconnect;
|
||||
}
|
||||
}
|
||||
trace!(target: LOG, "Reconnected successfully");
|
||||
break;
|
||||
}
|
||||
Err(error) => {
|
||||
trace!(target: LOG, "Failed to reconnect; {error}");
|
||||
time::sleep(time::Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
impl Response {
|
||||
fn try_from(message: Message) -> Result<Option<Self>> {
|
||||
match message {
|
||||
Message::Text(text) => {
|
||||
trace!(target: LOG, "Received an unexpected text message; {text}");
|
||||
Ok(None)
|
||||
}
|
||||
Message::Binary(binary) => msgpack::from_slice(&binary).map(Some).map_err(|error| {
|
||||
Error::ResponseFromBinary {
|
||||
binary,
|
||||
error,
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
202
lib/src/api/err/mod.rs
Normal file
202
lib/src/api/err/mod.rs
Normal file
|
@ -0,0 +1,202 @@
|
|||
use crate::api::Response;
|
||||
use crate::sql::Array;
|
||||
use crate::sql::Edges;
|
||||
use crate::sql::Object;
|
||||
use crate::sql::Thing;
|
||||
use crate::sql::Value;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
|
||||
/// An error originating from a remote SurrealDB database.
|
||||
#[derive(Error, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
/// There was an error processing the query
|
||||
#[error("{0}")]
|
||||
Query(String),
|
||||
|
||||
/// There was an error processing a remote HTTP request
|
||||
#[error("There was an error processing a remote HTTP request")]
|
||||
Http(String),
|
||||
|
||||
/// There was an error processing a remote WS request
|
||||
#[error("There was an error processing a remote WS request")]
|
||||
Ws(String),
|
||||
|
||||
/// There specified scheme does not match any supported protocol or storage engine
|
||||
#[error("Unsupported protocol or storage engine, `{0}`")]
|
||||
Scheme(String),
|
||||
|
||||
/// Tried to run database queries without initialising the connection first
|
||||
#[error("Connection uninitialised")]
|
||||
ConnectionUninitialised,
|
||||
|
||||
/// `Query::bind` not called with an object nor a key/value tuple
|
||||
#[error("Invalid bindings: {0}")]
|
||||
InvalidBindings(Value),
|
||||
|
||||
/// Tried to use a range query on a record ID
|
||||
#[error("Range on record IDs not supported: {0}")]
|
||||
RangeOnRecordId(Thing),
|
||||
|
||||
/// Tried to use a range query on an object
|
||||
#[error("Range on objects not supported: {0}")]
|
||||
RangeOnObject(Object),
|
||||
|
||||
/// Tried to use a range query on an array
|
||||
#[error("Range on arrays not supported: {0}")]
|
||||
RangeOnArray(Array),
|
||||
|
||||
/// Tried to use a range query on an edge or edges
|
||||
#[error("Range on edges not supported: {0}")]
|
||||
RangeOnEdges(Edges),
|
||||
|
||||
/// Tried to use `table:id` syntax as a method parameter when `(table, id)` should be used instead
|
||||
#[error("`{table}:{id}` is not allowed as a method parameter; try `({table}, {id})`")]
|
||||
TableColonId {
|
||||
table: String,
|
||||
id: String,
|
||||
},
|
||||
|
||||
/// Duplicate request ID
|
||||
#[error("Duplicate request ID: {0}")]
|
||||
DuplicateRequestId(i64),
|
||||
|
||||
/// Invalid request
|
||||
#[error("Invalid request: {0}")]
|
||||
InvalidRequest(String),
|
||||
|
||||
/// Invalid params
|
||||
#[error("Invalid params: {0}")]
|
||||
InvalidParams(String),
|
||||
|
||||
/// Internal server error
|
||||
#[error("Internal error: {0}")]
|
||||
InternalError(String),
|
||||
|
||||
/// Parse error
|
||||
#[error("Parse error: {0}")]
|
||||
ParseError(String),
|
||||
|
||||
/// Invalid semantic version
|
||||
#[error("Invalid semantic version: {0}")]
|
||||
InvalidSemanticVersion(String),
|
||||
|
||||
/// Invalid URL
|
||||
#[error("Invalid URL: {0}")]
|
||||
InvalidUrl(String),
|
||||
|
||||
/// Failed to convert a `sql::Value` to `T`
|
||||
#[error("Failed to convert `{value}` to `T`: {error}")]
|
||||
FromValue {
|
||||
value: Value,
|
||||
error: String,
|
||||
},
|
||||
|
||||
/// Failed to deserialize a binary response
|
||||
#[error("Failed to deserialize a binary response: {error}")]
|
||||
ResponseFromBinary {
|
||||
binary: Vec<u8>,
|
||||
error: msgpack::decode::Error,
|
||||
},
|
||||
|
||||
/// Failed to serialize `sql::Value` to JSON string
|
||||
#[error("Failed to serialize `{value}` to JSON string: {error}")]
|
||||
ToJsonString {
|
||||
value: Value,
|
||||
error: String,
|
||||
},
|
||||
|
||||
/// Failed to serialize `sql::Value` to JSON string
|
||||
#[error("Failed to serialize `{string}` to JSON string: {error}")]
|
||||
FromJsonString {
|
||||
string: String,
|
||||
error: String,
|
||||
},
|
||||
|
||||
/// Invalid namespace name
|
||||
#[error("Invalid namespace name: {0:?}")]
|
||||
InvalidNsName(String),
|
||||
|
||||
/// Invalid database name
|
||||
#[error("Invalid database name: {0:?}")]
|
||||
InvalidDbName(String),
|
||||
|
||||
/// File open error
|
||||
#[error("Failed to open `{path}`: {error}")]
|
||||
FileOpen {
|
||||
path: PathBuf,
|
||||
error: io::Error,
|
||||
},
|
||||
|
||||
/// File read error
|
||||
#[error("Failed to read `{path}`: {error}")]
|
||||
FileRead {
|
||||
path: PathBuf,
|
||||
error: io::Error,
|
||||
},
|
||||
|
||||
/// Tried to take only a single result when the query returned multiple records
|
||||
#[error("Tried to take only a single result from a query that contains multiple")]
|
||||
LossyTake(Response),
|
||||
|
||||
/// The protocol or storage engine being used does not support backups on the architecture
|
||||
/// it's running on
|
||||
#[error("The protocol or storage engine does not support backups on this architecture")]
|
||||
BackupsNotSupported,
|
||||
|
||||
/// The protocol or storage engine being used does not support authentication on the
|
||||
/// architecture it's running on
|
||||
#[error("The protocol or storage engine does not support authentication on this architecture")]
|
||||
AuthNotSupported,
|
||||
}
|
||||
|
||||
#[cfg(feature = "protocol-http")]
|
||||
impl From<reqwest::Error> for crate::Error {
|
||||
fn from(e: reqwest::Error) -> Self {
|
||||
Self::Api(Error::Http(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "protocol-ws", not(target_arch = "wasm32")))]
|
||||
#[cfg_attr(docsrs, doc(cfg(all(feature = "protocol-ws", not(target_arch = "wasm32")))))]
|
||||
impl From<tokio_tungstenite::tungstenite::Error> for crate::Error {
|
||||
fn from(error: tokio_tungstenite::tungstenite::Error) -> Self {
|
||||
Self::Api(Error::Ws(error.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<flume::SendError<T>> for crate::Error {
|
||||
fn from(error: flume::SendError<T>) -> Self {
|
||||
Self::Api(Error::InternalError(error.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<flume::RecvError> for crate::Error {
|
||||
fn from(error: flume::RecvError) -> Self {
|
||||
Self::Api(Error::InternalError(error.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<url::ParseError> for crate::Error {
|
||||
fn from(error: url::ParseError) -> Self {
|
||||
Self::Api(Error::InternalError(error.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "protocol-ws", target_arch = "wasm32"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(all(feature = "protocol-ws", target_arch = "wasm32"))))]
|
||||
impl From<ws_stream_wasm::WsErr> for crate::Error {
|
||||
fn from(error: ws_stream_wasm::WsErr) -> Self {
|
||||
Self::Api(Error::Ws(error.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "protocol-ws", target_arch = "wasm32"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(all(feature = "protocol-ws", target_arch = "wasm32"))))]
|
||||
impl From<pharos::PharErr> for crate::Error {
|
||||
fn from(error: pharos::PharErr) -> Self {
|
||||
Self::Api(Error::Ws(error.to_string()))
|
||||
}
|
||||
}
|
37
lib/src/api/method/authenticate.rs
Normal file
37
lib/src/api/method/authenticate.rs
Normal file
|
@ -0,0 +1,37 @@
|
|||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::opt::auth::Jwt;
|
||||
use crate::api::Connection;
|
||||
use crate::api::Error;
|
||||
use crate::api::ExtraFeatures;
|
||||
use crate::api::Result;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// An authentication future
|
||||
#[derive(Debug)]
|
||||
pub struct Authenticate<'r, C: Connection> {
|
||||
pub(super) router: Result<&'r Router<C>>,
|
||||
pub(super) token: Jwt,
|
||||
}
|
||||
|
||||
impl<'r, Client> IntoFuture for Authenticate<'r, Client>
|
||||
where
|
||||
Client: Connection,
|
||||
{
|
||||
type Output = Result<()>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
let router = self.router?;
|
||||
if !router.features.contains(&ExtraFeatures::Auth) {
|
||||
return Err(Error::AuthNotSupported.into());
|
||||
}
|
||||
let mut conn = Client::new(Method::Authenticate);
|
||||
conn.execute(router, Param::new(vec![self.token.into()])).await
|
||||
})
|
||||
}
|
||||
}
|
69
lib/src/api/method/begin.rs
Normal file
69
lib/src/api/method/begin.rs
Normal file
|
@ -0,0 +1,69 @@
|
|||
use crate::api::method::Cancel;
|
||||
use crate::api::method::Commit;
|
||||
use crate::api::Connection;
|
||||
use crate::api::Result;
|
||||
use crate::api::Surreal;
|
||||
use crate::sql::statements::BeginStatement;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::ops::Deref;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// A beginning of a transaction
|
||||
#[derive(Debug)]
|
||||
pub struct Begin<C: Connection> {
|
||||
pub(super) client: Surreal<C>,
|
||||
}
|
||||
|
||||
impl<C> IntoFuture for Begin<C>
|
||||
where
|
||||
C: Connection,
|
||||
{
|
||||
type Output = Result<Transaction<C>>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'static>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
self.client.query(BeginStatement).await?;
|
||||
Ok(Transaction {
|
||||
client: self.client,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// An ongoing transaction
|
||||
#[derive(Debug)]
|
||||
pub struct Transaction<C: Connection> {
|
||||
client: Surreal<C>,
|
||||
}
|
||||
|
||||
impl<C> Transaction<C>
|
||||
where
|
||||
C: Connection,
|
||||
{
|
||||
/// Creates a commit future
|
||||
pub fn commit(self) -> Commit<C> {
|
||||
Commit {
|
||||
client: self.client,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a cancel future
|
||||
pub fn cancel(self) -> Cancel<C> {
|
||||
Cancel {
|
||||
client: self.client,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> Deref for Transaction<C>
|
||||
where
|
||||
C: Connection,
|
||||
{
|
||||
type Target = Surreal<C>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.client
|
||||
}
|
||||
}
|
28
lib/src/api/method/cancel.rs
Normal file
28
lib/src/api/method/cancel.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
use crate::api::Connection;
|
||||
use crate::api::Result;
|
||||
use crate::api::Surreal;
|
||||
use crate::sql::statements::CancelStatement;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// A transaction cancellation future
|
||||
#[derive(Debug)]
|
||||
pub struct Cancel<C: Connection> {
|
||||
pub(crate) client: Surreal<C>,
|
||||
}
|
||||
|
||||
impl<C> IntoFuture for Cancel<C>
|
||||
where
|
||||
C: Connection,
|
||||
{
|
||||
type Output = Result<Surreal<C>>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'static>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
self.client.query(CancelStatement).await?;
|
||||
Ok(self.client)
|
||||
})
|
||||
}
|
||||
}
|
28
lib/src/api/method/commit.rs
Normal file
28
lib/src/api/method/commit.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
use crate::api::Connection;
|
||||
use crate::api::Result;
|
||||
use crate::api::Surreal;
|
||||
use crate::sql::statements::CommitStatement;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// A transaction commit future
|
||||
#[derive(Debug)]
|
||||
pub struct Commit<C: Connection> {
|
||||
pub(crate) client: Surreal<C>,
|
||||
}
|
||||
|
||||
impl<C> IntoFuture for Commit<C>
|
||||
where
|
||||
C: Connection,
|
||||
{
|
||||
type Output = Result<Surreal<C>>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'static>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
self.client.query(CommitStatement).await?;
|
||||
Ok(self.client)
|
||||
})
|
||||
}
|
||||
}
|
65
lib/src/api/method/content.rs
Normal file
65
lib/src/api/method/content.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::opt::from_json;
|
||||
use crate::api::opt::Range;
|
||||
use crate::api::opt::Resource;
|
||||
use crate::api::Connection;
|
||||
use crate::api::Result;
|
||||
use crate::sql::Id;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// A content future
|
||||
///
|
||||
/// Content inserts or replaces the contents of a record entirely
|
||||
#[derive(Debug)]
|
||||
pub struct Content<'r, C: Connection, D, R> {
|
||||
pub(super) router: Result<&'r Router<C>>,
|
||||
pub(super) method: Method,
|
||||
pub(super) resource: Result<Resource>,
|
||||
pub(super) range: Option<Range<Id>>,
|
||||
pub(super) content: D,
|
||||
pub(super) response_type: PhantomData<R>,
|
||||
}
|
||||
|
||||
impl<'r, C, D, R> Content<'r, C, D, R>
|
||||
where
|
||||
C: Connection,
|
||||
D: Serialize,
|
||||
{
|
||||
fn split(self) -> Result<(&'r Router<C>, Method, Param)> {
|
||||
let resource = self.resource?;
|
||||
let param = match self.range {
|
||||
Some(range) => resource.with_range(range)?,
|
||||
None => resource.into(),
|
||||
};
|
||||
let content = json!(self.content);
|
||||
let param = Param::new(vec![param, from_json(content)]);
|
||||
Ok((self.router?, self.method, param))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r, Client, D, R> IntoFuture for Content<'r, Client, D, R>
|
||||
where
|
||||
Client: Connection,
|
||||
D: Serialize,
|
||||
R: DeserializeOwned + Send + Sync,
|
||||
{
|
||||
type Output = Result<R>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
let result = self.split();
|
||||
Box::pin(async move {
|
||||
let (router, method, param) = result?;
|
||||
let mut conn = Client::new(method);
|
||||
conn.execute(router, param).await
|
||||
})
|
||||
}
|
||||
}
|
87
lib/src/api/method/create.rs
Normal file
87
lib/src/api/method/create.rs
Normal file
|
@ -0,0 +1,87 @@
|
|||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::method::Content;
|
||||
use crate::api::opt::Resource;
|
||||
use crate::api::Connection;
|
||||
use crate::api::Result;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// A record create future
|
||||
#[derive(Debug)]
|
||||
pub struct Create<'r, C: Connection, R> {
|
||||
pub(super) router: Result<&'r Router<C>>,
|
||||
pub(super) resource: Result<Resource>,
|
||||
pub(super) response_type: PhantomData<R>,
|
||||
}
|
||||
|
||||
impl<'r, Client, R> Create<'r, Client, R>
|
||||
where
|
||||
Client: Connection,
|
||||
{
|
||||
async fn execute<T>(self) -> Result<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let mut conn = Client::new(Method::Create);
|
||||
conn.execute(self.router?, Param::new(vec![self.resource?.into()])).await
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r, Client, R> IntoFuture for Create<'r, Client, Option<R>>
|
||||
where
|
||||
Client: Connection,
|
||||
R: DeserializeOwned + Send + Sync + 'r,
|
||||
{
|
||||
type Output = Result<R>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(self.execute())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r, Client, R> IntoFuture for Create<'r, Client, Vec<R>>
|
||||
where
|
||||
Client: Connection,
|
||||
R: DeserializeOwned + Send + Sync + 'r,
|
||||
{
|
||||
type Output = Result<R>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(self.execute())
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! create_methods {
|
||||
($this:ty) => {
|
||||
impl<'r, C, R> Create<'r, C, $this>
|
||||
where
|
||||
C: Connection,
|
||||
{
|
||||
/// Sets content of a record
|
||||
pub fn content<D>(self, data: D) -> Content<'r, C, D, R>
|
||||
where
|
||||
D: Serialize,
|
||||
{
|
||||
Content {
|
||||
router: self.router,
|
||||
method: Method::Create,
|
||||
resource: self.resource,
|
||||
range: None,
|
||||
content: data,
|
||||
response_type: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
create_methods!(Option<R>);
|
||||
create_methods!(Vec<R>);
|
71
lib/src/api/method/delete.rs
Normal file
71
lib/src/api/method/delete.rs
Normal file
|
@ -0,0 +1,71 @@
|
|||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::opt::Range;
|
||||
use crate::api::opt::Resource;
|
||||
use crate::api::Connection;
|
||||
use crate::api::Result;
|
||||
use crate::sql::Id;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// A record delete future
|
||||
#[derive(Debug)]
|
||||
pub struct Delete<'r, C: Connection, R> {
|
||||
pub(super) router: Result<&'r Router<C>>,
|
||||
pub(super) resource: Result<Resource>,
|
||||
pub(super) range: Option<Range<Id>>,
|
||||
pub(super) response_type: PhantomData<R>,
|
||||
}
|
||||
|
||||
impl<'r, Client, R> Delete<'r, Client, R>
|
||||
where
|
||||
Client: Connection,
|
||||
{
|
||||
async fn execute(self) -> Result<()> {
|
||||
let resource = self.resource?;
|
||||
let param = match self.range {
|
||||
Some(range) => resource.with_range(range)?,
|
||||
None => resource.into(),
|
||||
};
|
||||
let mut conn = Client::new(Method::Delete);
|
||||
conn.execute(self.router?, Param::new(vec![param])).await
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r, Client> IntoFuture for Delete<'r, Client, Option<()>>
|
||||
where
|
||||
Client: Connection,
|
||||
{
|
||||
type Output = Result<()>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(self.execute())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r, Client> IntoFuture for Delete<'r, Client, Vec<()>>
|
||||
where
|
||||
Client: Connection,
|
||||
{
|
||||
type Output = Result<()>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(self.execute())
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> Delete<'_, C, Vec<()>>
|
||||
where
|
||||
C: Connection,
|
||||
{
|
||||
/// Restricts a range of records to delete
|
||||
pub fn range(mut self, bounds: impl Into<Range<Id>>) -> Self {
|
||||
self.range = Some(bounds.into());
|
||||
self
|
||||
}
|
||||
}
|
37
lib/src/api/method/export.rs
Normal file
37
lib/src/api/method/export.rs
Normal file
|
@ -0,0 +1,37 @@
|
|||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::Connection;
|
||||
use crate::api::Error;
|
||||
use crate::api::ExtraFeatures;
|
||||
use crate::api::Result;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::path::PathBuf;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// A database export future
|
||||
#[derive(Debug)]
|
||||
pub struct Export<'r, C: Connection> {
|
||||
pub(super) router: Result<&'r Router<C>>,
|
||||
pub(super) file: PathBuf,
|
||||
}
|
||||
|
||||
impl<'r, Client> IntoFuture for Export<'r, Client>
|
||||
where
|
||||
Client: Connection,
|
||||
{
|
||||
type Output = Result<()>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async {
|
||||
let router = self.router?;
|
||||
if !router.features.contains(&ExtraFeatures::Backup) {
|
||||
return Err(Error::BackupsNotSupported.into());
|
||||
}
|
||||
let mut conn = Client::new(Method::Export);
|
||||
conn.execute(router, Param::file(self.file)).await
|
||||
})
|
||||
}
|
||||
}
|
29
lib/src/api/method/health.rs
Normal file
29
lib/src/api/method/health.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::Connection;
|
||||
use crate::api::Result;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// A health check future
|
||||
#[derive(Debug)]
|
||||
pub struct Health<'r, C: Connection> {
|
||||
pub(super) router: Result<&'r Router<C>>,
|
||||
}
|
||||
|
||||
impl<'r, Client> IntoFuture for Health<'r, Client>
|
||||
where
|
||||
Client: Connection,
|
||||
{
|
||||
type Output = Result<()>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async {
|
||||
let mut conn = Client::new(Method::Health);
|
||||
conn.execute(self.router?, Param::new(Vec::new())).await
|
||||
})
|
||||
}
|
||||
}
|
37
lib/src/api/method/import.rs
Normal file
37
lib/src/api/method/import.rs
Normal file
|
@ -0,0 +1,37 @@
|
|||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::Connection;
|
||||
use crate::api::Error;
|
||||
use crate::api::ExtraFeatures;
|
||||
use crate::api::Result;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::path::PathBuf;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// An database import future
|
||||
#[derive(Debug)]
|
||||
pub struct Import<'r, C: Connection> {
|
||||
pub(super) router: Result<&'r Router<C>>,
|
||||
pub(super) file: PathBuf,
|
||||
}
|
||||
|
||||
impl<'r, Client> IntoFuture for Import<'r, Client>
|
||||
where
|
||||
Client: Connection,
|
||||
{
|
||||
type Output = Result<()>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async {
|
||||
let router = self.router?;
|
||||
if !router.features.contains(&ExtraFeatures::Backup) {
|
||||
return Err(Error::BackupsNotSupported.into());
|
||||
}
|
||||
let mut conn = Client::new(Method::Import);
|
||||
conn.execute(router, Param::file(self.file)).await
|
||||
})
|
||||
}
|
||||
}
|
35
lib/src/api/method/invalidate.rs
Normal file
35
lib/src/api/method/invalidate.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::Connection;
|
||||
use crate::api::Error;
|
||||
use crate::api::ExtraFeatures;
|
||||
use crate::api::Result;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// A session invalidate future
|
||||
#[derive(Debug)]
|
||||
pub struct Invalidate<'r, C: Connection> {
|
||||
pub(super) router: Result<&'r Router<C>>,
|
||||
}
|
||||
|
||||
impl<'r, Client> IntoFuture for Invalidate<'r, Client>
|
||||
where
|
||||
Client: Connection,
|
||||
{
|
||||
type Output = Result<()>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async {
|
||||
let router = self.router?;
|
||||
if !router.features.contains(&ExtraFeatures::Auth) {
|
||||
return Err(Error::AuthNotSupported.into());
|
||||
}
|
||||
let mut conn = Client::new(Method::Invalidate);
|
||||
conn.execute(router, Param::new(Vec::new())).await
|
||||
})
|
||||
}
|
||||
}
|
31
lib/src/api/method/kill.rs
Normal file
31
lib/src/api/method/kill.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::Connection;
|
||||
use crate::api::Result;
|
||||
use crate::sql::Uuid;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// A live query kill future
|
||||
#[derive(Debug)]
|
||||
pub struct Kill<'r, C: Connection> {
|
||||
pub(super) router: Result<&'r Router<C>>,
|
||||
pub(super) query_id: Uuid,
|
||||
}
|
||||
|
||||
impl<'r, Client> IntoFuture for Kill<'r, Client>
|
||||
where
|
||||
Client: Connection,
|
||||
{
|
||||
type Output = Result<()>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
let mut conn = Client::new(Method::Kill);
|
||||
conn.execute(self.router?, Param::new(vec![self.query_id.into()])).await
|
||||
})
|
||||
}
|
||||
}
|
33
lib/src/api/method/live.rs
Normal file
33
lib/src/api/method/live.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::Connection;
|
||||
use crate::api::Result;
|
||||
use crate::sql::Table;
|
||||
use crate::sql::Uuid;
|
||||
use crate::sql::Value;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// A live query future
|
||||
#[derive(Debug)]
|
||||
pub struct Live<'r, C: Connection> {
|
||||
pub(super) router: Result<&'r Router<C>>,
|
||||
pub(super) table_name: String,
|
||||
}
|
||||
|
||||
impl<'r, Client> IntoFuture for Live<'r, Client>
|
||||
where
|
||||
Client: Connection,
|
||||
{
|
||||
type Output = Result<Uuid>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
let mut conn = Client::new(Method::Live);
|
||||
conn.execute(self.router?, Param::new(vec![Value::Table(Table(self.table_name))])).await
|
||||
})
|
||||
}
|
||||
}
|
62
lib/src/api/method/merge.rs
Normal file
62
lib/src/api/method/merge.rs
Normal file
|
@ -0,0 +1,62 @@
|
|||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::opt::from_json;
|
||||
use crate::api::opt::Range;
|
||||
use crate::api::opt::Resource;
|
||||
use crate::api::Connection;
|
||||
use crate::api::Result;
|
||||
use crate::sql::Id;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// A merge future
|
||||
#[derive(Debug)]
|
||||
pub struct Merge<'r, C: Connection, D, R> {
|
||||
pub(super) router: Result<&'r Router<C>>,
|
||||
pub(super) resource: Result<Resource>,
|
||||
pub(super) range: Option<Range<Id>>,
|
||||
pub(super) content: D,
|
||||
pub(super) response_type: PhantomData<R>,
|
||||
}
|
||||
|
||||
impl<'r, C, D, R> Merge<'r, C, D, R>
|
||||
where
|
||||
C: Connection,
|
||||
D: Serialize,
|
||||
{
|
||||
fn split(self) -> Result<(&'r Router<C>, Method, Param)> {
|
||||
let resource = self.resource?;
|
||||
let param = match self.range {
|
||||
Some(range) => resource.with_range(range)?,
|
||||
None => resource.into(),
|
||||
};
|
||||
let content = json!(self.content);
|
||||
let param = Param::new(vec![param, from_json(content)]);
|
||||
Ok((self.router?, Method::Merge, param))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r, Client, D, R> IntoFuture for Merge<'r, Client, D, R>
|
||||
where
|
||||
Client: Connection,
|
||||
D: Serialize,
|
||||
R: DeserializeOwned + Send + Sync,
|
||||
{
|
||||
type Output = Result<R>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
let result = self.split();
|
||||
Box::pin(async move {
|
||||
let (router, method, param) = result?;
|
||||
let mut conn = Client::new(method);
|
||||
conn.execute(router, param).await
|
||||
})
|
||||
}
|
||||
}
|
1007
lib/src/api/method/mod.rs
Normal file
1007
lib/src/api/method/mod.rs
Normal file
File diff suppressed because it is too large
Load diff
59
lib/src/api/method/patch.rs
Normal file
59
lib/src/api/method/patch.rs
Normal file
|
@ -0,0 +1,59 @@
|
|||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::opt::PatchOp;
|
||||
use crate::api::opt::Range;
|
||||
use crate::api::opt::Resource;
|
||||
use crate::api::Connection;
|
||||
use crate::api::Result;
|
||||
use crate::sql::Array;
|
||||
use crate::sql::Id;
|
||||
use crate::sql::Value;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// A patch future
|
||||
#[derive(Debug)]
|
||||
pub struct Patch<'r, C: Connection, R> {
|
||||
pub(super) router: Result<&'r Router<C>>,
|
||||
pub(super) resource: Result<Resource>,
|
||||
pub(super) range: Option<Range<Id>>,
|
||||
pub(super) patches: Vec<Value>,
|
||||
pub(super) response_type: PhantomData<R>,
|
||||
}
|
||||
|
||||
impl<'r, C, R> Patch<'r, C, R>
|
||||
where
|
||||
C: Connection,
|
||||
{
|
||||
/// Applies JSON Patch changes to all records, or a specific record, in the database.
|
||||
pub fn patch(mut self, PatchOp(patch): PatchOp) -> Patch<'r, C, R> {
|
||||
self.patches.push(patch);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r, Client, R> IntoFuture for Patch<'r, Client, R>
|
||||
where
|
||||
Client: Connection,
|
||||
R: DeserializeOwned + Send + Sync,
|
||||
{
|
||||
type Output = Result<R>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
let resource = self.resource?;
|
||||
let param = match self.range {
|
||||
Some(range) => resource.with_range(range)?,
|
||||
None => resource.into(),
|
||||
};
|
||||
let patches = Value::Array(Array(self.patches));
|
||||
let mut conn = Client::new(Method::Patch);
|
||||
conn.execute(self.router?, Param::new(vec![param, patches])).await
|
||||
})
|
||||
}
|
||||
}
|
500
lib/src/api/method/query.rs
Normal file
500
lib/src/api/method/query.rs
Normal file
|
@ -0,0 +1,500 @@
|
|||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::err::Error;
|
||||
use crate::api::opt;
|
||||
use crate::api::opt::from_json;
|
||||
use crate::api::Connection;
|
||||
use crate::api::Result;
|
||||
use crate::sql;
|
||||
use crate::sql::Array;
|
||||
use crate::sql::Object;
|
||||
use crate::sql::Statement;
|
||||
use crate::sql::Statements;
|
||||
use crate::sql::Strand;
|
||||
use crate::sql::Value;
|
||||
use indexmap::IndexMap;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::mem;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// A query future
|
||||
#[derive(Debug)]
|
||||
pub struct Query<'r, C: Connection> {
|
||||
pub(super) router: Result<&'r Router<C>>,
|
||||
pub(super) query: Vec<Result<Vec<Statement>>>,
|
||||
pub(super) bindings: Result<BTreeMap<String, Value>>,
|
||||
}
|
||||
|
||||
impl<'r, Client> IntoFuture for Query<'r, Client>
|
||||
where
|
||||
Client: Connection,
|
||||
{
|
||||
type Output = Result<Response>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
let mut statements = Vec::with_capacity(self.query.len());
|
||||
for query in self.query {
|
||||
statements.extend(query?);
|
||||
}
|
||||
let query = sql::Query(Statements(statements));
|
||||
let param = Param::query(query, self.bindings?);
|
||||
let mut conn = Client::new(Method::Query);
|
||||
conn.execute_query(self.router?, param).await
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r, C> Query<'r, C>
|
||||
where
|
||||
C: Connection,
|
||||
{
|
||||
/// Chains a query onto an existing query
|
||||
pub fn query(mut self, query: impl opt::IntoQuery) -> Self {
|
||||
self.query.push(query.into_query());
|
||||
self
|
||||
}
|
||||
|
||||
/// Binds a parameter or parameters to a query
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Binding a key/value tuple
|
||||
///
|
||||
/// ```no_run
|
||||
/// use surrealdb::sql;
|
||||
///
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> surrealdb::Result<()> {
|
||||
/// # let db = surrealdb::engines::any::connect("mem://").await?;
|
||||
/// let response = db.query(sql!(CREATE user SET name = $name))
|
||||
/// .bind(("name", "John Doe"))
|
||||
/// .await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Binding an object
|
||||
///
|
||||
/// ```no_run
|
||||
/// use serde::Serialize;
|
||||
/// use surrealdb::sql;
|
||||
///
|
||||
/// #[derive(Serialize)]
|
||||
/// struct User<'a> {
|
||||
/// name: &'a str,
|
||||
/// }
|
||||
///
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> surrealdb::Result<()> {
|
||||
/// # let db = surrealdb::engines::any::connect("mem://").await?;
|
||||
/// let response = db.query(sql!(CREATE user SET name = $name))
|
||||
/// .bind(User {
|
||||
/// name: "John Doe",
|
||||
/// })
|
||||
/// .await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn bind(mut self, bindings: impl Serialize) -> Self {
|
||||
if let Ok(current) = &mut self.bindings {
|
||||
let mut bindings = from_json(json!(bindings));
|
||||
if let Value::Array(Array(array)) = &mut bindings {
|
||||
if let [Value::Strand(Strand(key)), value] = &mut array[..] {
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert(mem::take(key), mem::take(value));
|
||||
bindings = map.into();
|
||||
}
|
||||
}
|
||||
match &mut bindings {
|
||||
Value::Object(Object(map)) => current.append(map),
|
||||
_ => {
|
||||
self.bindings = Err(Error::InvalidBindings(bindings).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type QueryResult = Result<Vec<Value>>;
|
||||
|
||||
/// The response type of a `Surreal::query` request
|
||||
#[derive(Debug)]
|
||||
pub struct Response(pub(crate) IndexMap<usize, QueryResult>);
|
||||
|
||||
impl Response {
|
||||
/// Takes and returns records returned from the database
|
||||
///
|
||||
/// A query that only returns one result can be deserialized into an
|
||||
/// `Option<T>`, while those that return multiple results should be
|
||||
/// deserialized into a `Vec<T>`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use serde::Deserialize;
|
||||
/// use surrealdb::sql;
|
||||
///
|
||||
/// #[derive(Debug, Deserialize)]
|
||||
/// # #[allow(dead_code)]
|
||||
/// struct User {
|
||||
/// id: String,
|
||||
/// balance: String
|
||||
/// }
|
||||
///
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> surrealdb::Result<()> {
|
||||
/// # let db = surrealdb::engines::any::connect("mem://").await?;
|
||||
/// #
|
||||
/// let mut response = db
|
||||
/// // Get `john`'s details
|
||||
/// .query(sql!(SELECT * FROM user:john))
|
||||
/// // List all users whose first name is John
|
||||
/// .query(sql!(SELECT * FROM user WHERE name.first = "John"))
|
||||
/// // Get John's address
|
||||
/// .query(sql!(SELECT address FROM user:john))
|
||||
/// // Get all users' addresses
|
||||
/// .query(sql!(SELECT address FROM user))
|
||||
/// .await?;
|
||||
///
|
||||
/// // Get the first (and only) user from the first query
|
||||
/// let user: Option<User> = response.take(0)?;
|
||||
///
|
||||
/// // Get all users from the second query
|
||||
/// let users: Vec<User> = response.take(1)?;
|
||||
///
|
||||
/// // Retrieve John's address without making a special struct for it
|
||||
/// let address: Option<String> = response.take((2, "address"))?;
|
||||
///
|
||||
/// // Get all users' addresses
|
||||
/// let addresses: Vec<String> = response.take((3, "address"))?;
|
||||
///
|
||||
/// // You can continue taking more fields on the same response
|
||||
/// // object when extracting individual fields
|
||||
/// let mut response = db.query(sql!(SELECT * FROM user)).await?;
|
||||
///
|
||||
/// // Since the query we want to access is at index 0, we can use
|
||||
/// // a shortcut instead of `response.take((0, "field"))`
|
||||
/// let ids: Vec<String> = response.take("id")?;
|
||||
/// let names: Vec<String> = response.take("name")?;
|
||||
/// let addresses: Vec<String> = response.take("address")?;
|
||||
/// #
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// The indices are stable. Taking one index doesn't affect the numbering
|
||||
/// of the other indices, so you can take them in any order you see fit.
|
||||
pub fn take<R>(&mut self, index: impl opt::QueryResult<R>) -> Result<R>
|
||||
where
|
||||
R: DeserializeOwned,
|
||||
{
|
||||
index.query_result(self)
|
||||
}
|
||||
|
||||
/// Take all errors from the query response
|
||||
///
|
||||
/// The errors are keyed by the corresponding index of the statement that failed.
|
||||
/// Afterwards the response is left with only statements that did not produce any errors.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use surrealdb::sql;
|
||||
///
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> surrealdb::Result<()> {
|
||||
/// # let db = surrealdb::engines::any::connect("mem://").await?;
|
||||
/// # let mut response = db.query(sql!(SELECT * FROM user)).await?;
|
||||
/// let errors = response.take_errors();
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn take_errors(&mut self) -> HashMap<usize, crate::Error> {
|
||||
let mut keys = Vec::new();
|
||||
for (key, result) in &self.0 {
|
||||
if result.is_err() {
|
||||
keys.push(*key);
|
||||
}
|
||||
}
|
||||
let mut errors = HashMap::with_capacity(keys.len());
|
||||
for key in keys {
|
||||
if let Some(Err(error)) = self.0.remove(&key) {
|
||||
errors.insert(key, error);
|
||||
}
|
||||
}
|
||||
errors
|
||||
}
|
||||
|
||||
/// Check query response for errors and return the first error, if any, or the response
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use surrealdb::sql;
|
||||
///
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> surrealdb::Result<()> {
|
||||
/// # let db = surrealdb::engines::any::connect("mem://").await?;
|
||||
/// # let response = db.query(sql!(SELECT * FROM user)).await?;
|
||||
/// response.check()?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn check(mut self) -> Result<Self> {
|
||||
let mut first_error = None;
|
||||
for (key, result) in &self.0 {
|
||||
if result.is_err() {
|
||||
first_error = Some(*key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(key) = first_error {
|
||||
if let Some(Err(error)) = self.0.remove(&key) {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Returns the number of statements in the query
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// use surrealdb::sql;
|
||||
///
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> surrealdb::Result<()> {
|
||||
/// # let db = surrealdb::engines::any::connect("mem://").await?;
|
||||
/// let response = db.query(sql!(SELECT * FROM user:john; SELECT * FROM user;)).await?;
|
||||
///
|
||||
/// assert_eq!(response.num_statements(), 2);
|
||||
/// #
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
pub fn num_statements(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Error::Api;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct Summary {
|
||||
title: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct Article {
|
||||
title: String,
|
||||
body: String,
|
||||
}
|
||||
|
||||
fn to_map(vec: Vec<QueryResult>) -> IndexMap<usize, QueryResult> {
|
||||
vec.into_iter().enumerate().collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn take_from_an_empty_response() {
|
||||
let mut response = Response(Default::default());
|
||||
let option: Option<String> = response.take(0).unwrap();
|
||||
assert!(option.is_none());
|
||||
|
||||
let mut response = Response(Default::default());
|
||||
let vec: Vec<String> = response.take(0).unwrap();
|
||||
assert!(vec.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn take_from_an_errored_query() {
|
||||
let mut response = Response(to_map(vec![Err(Error::ConnectionUninitialised.into())]));
|
||||
response.take::<Option<()>>(0).unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn take_from_empty_records() {
|
||||
let mut response = Response(to_map(vec![Ok(vec![])]));
|
||||
let option: Option<String> = response.take(0).unwrap();
|
||||
assert!(option.is_none());
|
||||
|
||||
let mut response = Response(to_map(vec![Ok(vec![])]));
|
||||
let vec: Vec<String> = response.take(0).unwrap();
|
||||
assert!(vec.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn take_from_a_scalar_response() {
|
||||
let scalar = 265;
|
||||
|
||||
let mut response = Response(to_map(vec![Ok(vec![scalar.into()])]));
|
||||
let option: Option<_> = response.take(0).unwrap();
|
||||
assert_eq!(option, Some(scalar));
|
||||
|
||||
let mut response = Response(to_map(vec![Ok(vec![scalar.into()])]));
|
||||
let vec: Vec<usize> = response.take(0).unwrap();
|
||||
assert_eq!(vec, vec![scalar]);
|
||||
|
||||
let scalar = true;
|
||||
|
||||
let mut response = Response(to_map(vec![Ok(vec![scalar.into()])]));
|
||||
let option: Option<_> = response.take(0).unwrap();
|
||||
assert_eq!(option, Some(scalar));
|
||||
|
||||
let mut response = Response(to_map(vec![Ok(vec![scalar.into()])]));
|
||||
let vec: Vec<bool> = response.take(0).unwrap();
|
||||
assert_eq!(vec, vec![scalar]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn take_preserves_order() {
|
||||
let mut response = Response(to_map(vec![
|
||||
Ok(vec![0.into()]),
|
||||
Ok(vec![1.into()]),
|
||||
Ok(vec![2.into()]),
|
||||
Ok(vec![3.into()]),
|
||||
Ok(vec![4.into()]),
|
||||
Ok(vec![5.into()]),
|
||||
Ok(vec![6.into()]),
|
||||
Ok(vec![7.into()]),
|
||||
]));
|
||||
let Some(four): Option<i32> = response.take(4).unwrap() else {
|
||||
panic!("query not found");
|
||||
};
|
||||
assert_eq!(four, 4);
|
||||
let Some(six): Option<i32> = response.take(6).unwrap() else {
|
||||
panic!("query not found");
|
||||
};
|
||||
assert_eq!(six, 6);
|
||||
let Some(zero): Option<i32> = response.take(0).unwrap() else {
|
||||
panic!("query not found");
|
||||
};
|
||||
assert_eq!(zero, 0);
|
||||
let Some(one): Option<i32> = response.take(1).unwrap() else {
|
||||
panic!("query not found");
|
||||
};
|
||||
assert_eq!(one, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn take_key() {
|
||||
let summary = Summary {
|
||||
title: "Lorem Ipsum".to_owned(),
|
||||
};
|
||||
|
||||
let mut response = Response(to_map(vec![Ok(vec![from_json(json!(summary.clone()))])]));
|
||||
let Some(title): Option<String> = response.take("title").unwrap() else {
|
||||
panic!("title not found");
|
||||
};
|
||||
assert_eq!(title, summary.title);
|
||||
|
||||
let mut response = Response(to_map(vec![Ok(vec![from_json(json!(summary.clone()))])]));
|
||||
let vec: Vec<String> = response.take("title").unwrap();
|
||||
assert_eq!(vec, vec![summary.title]);
|
||||
|
||||
let article = Article {
|
||||
title: "Lorem Ipsum".to_owned(),
|
||||
body: "Lorem Ipsum Lorem Ipsum".to_owned(),
|
||||
};
|
||||
|
||||
let mut response = Response(to_map(vec![Ok(vec![from_json(json!(article.clone()))])]));
|
||||
let Some(title): Option<String> = response.take("title").unwrap() else {
|
||||
panic!("title not found");
|
||||
};
|
||||
assert_eq!(title, article.title);
|
||||
let Some(body): Option<String> = response.take("body").unwrap() else {
|
||||
panic!("body not found");
|
||||
};
|
||||
assert_eq!(body, article.body);
|
||||
|
||||
let mut response = Response(to_map(vec![Ok(vec![from_json(json!(article.clone()))])]));
|
||||
let vec: Vec<String> = response.take("title").unwrap();
|
||||
assert_eq!(vec, vec![article.title]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn take_partial_records() {
|
||||
let mut response = Response(to_map(vec![Ok(vec![true.into(), false.into()])]));
|
||||
let vec: Vec<bool> = response.take(0).unwrap();
|
||||
assert_eq!(vec, vec![true, false]);
|
||||
|
||||
let mut response = Response(to_map(vec![Ok(vec![true.into(), false.into()])]));
|
||||
let Err(Api(Error::LossyTake(Response(mut map)))): Result<Option<bool>> = response.take(0) else {
|
||||
panic!("silently dropping records not allowed");
|
||||
};
|
||||
let records = map.remove(&0).unwrap().unwrap();
|
||||
assert_eq!(records, vec![true.into(), false.into()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_returns_the_first_error() {
|
||||
let response = vec![
|
||||
Ok(vec![0.into()]),
|
||||
Ok(vec![1.into()]),
|
||||
Ok(vec![2.into()]),
|
||||
Err(Error::ConnectionUninitialised.into()),
|
||||
Ok(vec![3.into()]),
|
||||
Ok(vec![4.into()]),
|
||||
Ok(vec![5.into()]),
|
||||
Err(Error::BackupsNotSupported.into()),
|
||||
Ok(vec![6.into()]),
|
||||
Ok(vec![7.into()]),
|
||||
Err(Error::AuthNotSupported.into()),
|
||||
];
|
||||
let response = Response(to_map(response));
|
||||
let crate::Error::Api(Error::ConnectionUninitialised) = response.check().unwrap_err() else {
|
||||
panic!("check did not return the first error");
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn take_errors() {
|
||||
let response = vec![
|
||||
Ok(vec![0.into()]),
|
||||
Ok(vec![1.into()]),
|
||||
Ok(vec![2.into()]),
|
||||
Err(Error::ConnectionUninitialised.into()),
|
||||
Ok(vec![3.into()]),
|
||||
Ok(vec![4.into()]),
|
||||
Ok(vec![5.into()]),
|
||||
Err(Error::BackupsNotSupported.into()),
|
||||
Ok(vec![6.into()]),
|
||||
Ok(vec![7.into()]),
|
||||
Err(Error::AuthNotSupported.into()),
|
||||
];
|
||||
let mut response = Response(to_map(response));
|
||||
let errors = response.take_errors();
|
||||
assert_eq!(response.num_statements(), 8);
|
||||
assert_eq!(errors.len(), 3);
|
||||
let crate::Error::Api(Error::AuthNotSupported) = errors.get(&10).unwrap() else {
|
||||
panic!("index `10` is not `AuthNotSupported`");
|
||||
};
|
||||
let crate::Error::Api(Error::BackupsNotSupported) = errors.get(&7).unwrap() else {
|
||||
panic!("index `7` is not `BackupsNotSupported`");
|
||||
};
|
||||
let crate::Error::Api(Error::ConnectionUninitialised) = errors.get(&3).unwrap() else {
|
||||
panic!("index `3` is not `ConnectionUninitialised`");
|
||||
};
|
||||
let Some(value): Option<i32> = response.take(2).unwrap() else {
|
||||
panic!("statement not found");
|
||||
};
|
||||
assert_eq!(value, 2);
|
||||
let Some(value): Option<i32> = response.take(4).unwrap() else {
|
||||
panic!("statement not found");
|
||||
};
|
||||
assert_eq!(value, 3);
|
||||
}
|
||||
}
|
77
lib/src/api/method/select.rs
Normal file
77
lib/src/api/method/select.rs
Normal file
|
@ -0,0 +1,77 @@
|
|||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::opt::Range;
|
||||
use crate::api::opt::Resource;
|
||||
use crate::api::Connection;
|
||||
use crate::api::Result;
|
||||
use crate::sql::Id;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// A select future
|
||||
#[derive(Debug)]
|
||||
pub struct Select<'r, C: Connection, R> {
|
||||
pub(super) router: Result<&'r Router<C>>,
|
||||
pub(super) resource: Result<Resource>,
|
||||
pub(super) range: Option<Range<Id>>,
|
||||
pub(super) response_type: PhantomData<R>,
|
||||
}
|
||||
|
||||
impl<'r, Client, R> Select<'r, Client, R>
|
||||
where
|
||||
Client: Connection,
|
||||
{
|
||||
async fn execute<T>(self) -> Result<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let resource = self.resource?;
|
||||
let param = match self.range {
|
||||
Some(range) => resource.with_range(range)?,
|
||||
None => resource.into(),
|
||||
};
|
||||
let mut conn = Client::new(Method::Select);
|
||||
conn.execute(self.router?, Param::new(vec![param])).await
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r, Client, R> IntoFuture for Select<'r, Client, Option<R>>
|
||||
where
|
||||
Client: Connection,
|
||||
R: DeserializeOwned + Send + Sync + 'r,
|
||||
{
|
||||
type Output = Result<R>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(self.execute())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r, Client, R> IntoFuture for Select<'r, Client, Vec<R>>
|
||||
where
|
||||
Client: Connection,
|
||||
R: DeserializeOwned + Send + Sync + 'r,
|
||||
{
|
||||
type Output = Result<Vec<R>>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(self.execute())
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, R> Select<'_, C, Vec<R>>
|
||||
where
|
||||
C: Connection,
|
||||
{
|
||||
/// Restricts the records selected to those in the specified range
|
||||
pub fn range(mut self, bounds: impl Into<Range<Id>>) -> Self {
|
||||
self.range = Some(bounds.into());
|
||||
self
|
||||
}
|
||||
}
|
32
lib/src/api/method/set.rs
Normal file
32
lib/src/api/method/set.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::Connection;
|
||||
use crate::api::Result;
|
||||
use crate::sql::Value;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// A set future
|
||||
#[derive(Debug)]
|
||||
pub struct Set<'r, C: Connection> {
|
||||
pub(super) router: Result<&'r Router<C>>,
|
||||
pub(super) key: String,
|
||||
pub(super) value: Result<Value>,
|
||||
}
|
||||
|
||||
impl<'r, Client> IntoFuture for Set<'r, Client>
|
||||
where
|
||||
Client: Connection,
|
||||
{
|
||||
type Output = Result<()>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
let mut conn = Client::new(Method::Set);
|
||||
conn.execute(self.router?, Param::new(vec![self.key.into(), self.value?])).await
|
||||
})
|
||||
}
|
||||
}
|
41
lib/src/api/method/signin.rs
Normal file
41
lib/src/api/method/signin.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::Connection;
|
||||
use crate::api::Error;
|
||||
use crate::api::ExtraFeatures;
|
||||
use crate::api::Result;
|
||||
use crate::sql::Value;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// A signin future
|
||||
#[derive(Debug)]
|
||||
pub struct Signin<'r, C: Connection, R> {
|
||||
pub(super) router: Result<&'r Router<C>>,
|
||||
pub(super) credentials: Result<Value>,
|
||||
pub(super) response_type: PhantomData<R>,
|
||||
}
|
||||
|
||||
impl<'r, Client, R> IntoFuture for Signin<'r, Client, R>
|
||||
where
|
||||
Client: Connection,
|
||||
R: DeserializeOwned + Send + Sync,
|
||||
{
|
||||
type Output = Result<R>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
let router = self.router?;
|
||||
if !router.features.contains(&ExtraFeatures::Auth) {
|
||||
return Err(Error::AuthNotSupported.into());
|
||||
}
|
||||
let mut conn = Client::new(Method::Signin);
|
||||
conn.execute(router, Param::new(vec![self.credentials?])).await
|
||||
})
|
||||
}
|
||||
}
|
41
lib/src/api/method/signup.rs
Normal file
41
lib/src/api/method/signup.rs
Normal file
|
@ -0,0 +1,41 @@
|
|||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::Connection;
|
||||
use crate::api::Error;
|
||||
use crate::api::ExtraFeatures;
|
||||
use crate::api::Result;
|
||||
use crate::sql::Value;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// A signup future
|
||||
#[derive(Debug)]
|
||||
pub struct Signup<'r, C: Connection, R> {
|
||||
pub(super) router: Result<&'r Router<C>>,
|
||||
pub(super) credentials: Result<Value>,
|
||||
pub(super) response_type: PhantomData<R>,
|
||||
}
|
||||
|
||||
impl<'r, Client, R> IntoFuture for Signup<'r, Client, R>
|
||||
where
|
||||
Client: Connection,
|
||||
R: DeserializeOwned + Send + Sync,
|
||||
{
|
||||
type Output = Result<R>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
let router = self.router?;
|
||||
if !router.features.contains(&ExtraFeatures::Auth) {
|
||||
return Err(Error::AuthNotSupported.into());
|
||||
}
|
||||
let mut conn = Client::new(Method::Signup);
|
||||
conn.execute(router, Param::new(vec![self.credentials?])).await
|
||||
})
|
||||
}
|
||||
}
|
181
lib/src/api/method/tests/mod.rs
Normal file
181
lib/src/api/method/tests/mod.rs
Normal file
|
@ -0,0 +1,181 @@
|
|||
#![cfg(any(feature = "protocol-http", feature = "protocol-ws"))]
|
||||
#![cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
mod protocol;
|
||||
mod server;
|
||||
mod types;
|
||||
|
||||
use crate::api::method::tests::types::AuthParams;
|
||||
use crate::api::opt::auth::Database;
|
||||
use crate::api::opt::auth::Jwt;
|
||||
use crate::api::opt::auth::Namespace;
|
||||
use crate::api::opt::auth::Root;
|
||||
use crate::api::opt::auth::Scope;
|
||||
use crate::api::opt::PatchOp;
|
||||
use crate::api::Response as QueryResponse;
|
||||
use crate::api::Surreal;
|
||||
use crate::sql::statements::BeginStatement;
|
||||
use crate::sql::statements::CommitStatement;
|
||||
use protocol::Client;
|
||||
use protocol::Test;
|
||||
use semver::Version;
|
||||
use std::ops::Bound;
|
||||
use types::User;
|
||||
use types::USER;
|
||||
|
||||
static DB: Surreal<Client> = Surreal::init();
|
||||
|
||||
#[tokio::test]
|
||||
async fn api() {
|
||||
// connect to the mock server
|
||||
DB.connect::<Test>(()).with_capacity(512).await.unwrap();
|
||||
|
||||
// health
|
||||
let _: () = DB.health().await.unwrap();
|
||||
|
||||
// invalidate
|
||||
let _: () = DB.invalidate().await.unwrap();
|
||||
|
||||
// use
|
||||
let _: () = DB.use_ns("test-ns").use_db("test-db").await.unwrap();
|
||||
|
||||
// signup
|
||||
let _: Jwt = DB
|
||||
.signup(Scope {
|
||||
namespace: "test-ns",
|
||||
database: "test-db",
|
||||
scope: "scope",
|
||||
params: AuthParams {},
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// signin
|
||||
let _: () = DB
|
||||
.signin(Root {
|
||||
username: "root",
|
||||
password: "root",
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let _: Jwt = DB
|
||||
.signin(Namespace {
|
||||
namespace: "test-ns",
|
||||
username: "user",
|
||||
password: "pass",
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let _: Jwt = DB
|
||||
.signin(Database {
|
||||
namespace: "test-ns",
|
||||
database: "test-db",
|
||||
username: "user",
|
||||
password: "pass",
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let _: Jwt = DB
|
||||
.signin(Scope {
|
||||
namespace: "test-ns",
|
||||
database: "test-db",
|
||||
scope: "scope",
|
||||
params: AuthParams {},
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// authenticate
|
||||
let _: () = DB.authenticate(Jwt(String::new())).await.unwrap();
|
||||
|
||||
// query
|
||||
let _: QueryResponse = DB.query("SELECT * FROM user").await.unwrap();
|
||||
let _: QueryResponse =
|
||||
DB.query("CREATE user:john SET name = $name").bind(("name", "John Doe")).await.unwrap();
|
||||
let _: QueryResponse = DB
|
||||
.query("CREATE user:john SET name = $name")
|
||||
.bind(User {
|
||||
id: "john".to_owned(),
|
||||
name: "John Doe".to_owned(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let _: QueryResponse = DB
|
||||
.query(BeginStatement)
|
||||
.query("CREATE account:one SET balance = 135605.16")
|
||||
.query("CREATE account:two SET balance = 91031.31")
|
||||
.query("UPDATE account:one SET balance += 300.00")
|
||||
.query("UPDATE account:two SET balance -= 300.00")
|
||||
.query(CommitStatement)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// create
|
||||
let _: User = DB.create(USER).await.unwrap();
|
||||
let _: User = DB.create((USER, "john")).await.unwrap();
|
||||
let _: User = DB.create(USER).content(User::default()).await.unwrap();
|
||||
let _: User = DB.create((USER, "john")).content(User::default()).await.unwrap();
|
||||
|
||||
// select
|
||||
let _: Vec<User> = DB.select(USER).await.unwrap();
|
||||
let _: Option<User> = DB.select((USER, "john")).await.unwrap();
|
||||
let _: Vec<User> = DB.select(USER).range(..).await.unwrap();
|
||||
let _: Vec<User> = DB.select(USER).range(.."john").await.unwrap();
|
||||
let _: Vec<User> = DB.select(USER).range(..="john").await.unwrap();
|
||||
let _: Vec<User> = DB.select(USER).range("jane"..).await.unwrap();
|
||||
let _: Vec<User> = DB.select(USER).range("jane".."john").await.unwrap();
|
||||
let _: Vec<User> = DB.select(USER).range("jane"..="john").await.unwrap();
|
||||
let _: Vec<User> = DB.select(USER).range("jane"..="john").await.unwrap();
|
||||
let _: Vec<User> =
|
||||
DB.select(USER).range((Bound::Excluded("jane"), Bound::Included("john"))).await.unwrap();
|
||||
|
||||
// update
|
||||
let _: Vec<User> = DB.update(USER).await.unwrap();
|
||||
let _: Option<User> = DB.update((USER, "john")).await.unwrap();
|
||||
let _: Vec<User> = DB.update(USER).content(User::default()).await.unwrap();
|
||||
let _: Vec<User> =
|
||||
DB.update(USER).range("jane".."john").content(User::default()).await.unwrap();
|
||||
let _: Option<User> = DB.update((USER, "john")).content(User::default()).await.unwrap();
|
||||
|
||||
// merge
|
||||
let _: Vec<User> = DB.update(USER).merge(User::default()).await.unwrap();
|
||||
let _: Vec<User> = DB.update(USER).range("jane".."john").merge(User::default()).await.unwrap();
|
||||
let _: Option<User> = DB.update((USER, "john")).merge(User::default()).await.unwrap();
|
||||
|
||||
// patch
|
||||
let _: Vec<User> = DB.update(USER).patch(PatchOp::remove("/name")).await.unwrap();
|
||||
let _: Vec<User> =
|
||||
DB.update(USER).range("jane".."john").patch(PatchOp::remove("/name")).await.unwrap();
|
||||
let _: Option<User> = DB.update((USER, "john")).patch(PatchOp::remove("/name")).await.unwrap();
|
||||
|
||||
// delete
|
||||
let _: () = DB.delete(USER).await.unwrap();
|
||||
let _: () = DB.delete((USER, "john")).await.unwrap();
|
||||
let _: () = DB.delete(USER).range("jane".."john").await.unwrap();
|
||||
|
||||
// export
|
||||
let _: () = DB.export("backup.sql").await.unwrap();
|
||||
|
||||
// import
|
||||
let _: () = DB.import("backup.sql").await.unwrap();
|
||||
|
||||
// version
|
||||
let _: Version = DB.version().await.unwrap();
|
||||
}
|
||||
|
||||
fn send_and_sync(_: impl Send + Sync) {}
|
||||
|
||||
#[test]
|
||||
fn futures_are_send_and_sync() {
|
||||
send_and_sync(async {
|
||||
let db = Surreal::new::<Test>(()).await.unwrap();
|
||||
db.signin(Root {
|
||||
username: "root",
|
||||
password: "root",
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
db.use_ns("test-ns").use_db("test-db").await.unwrap();
|
||||
let _: Vec<User> = db.select(USER).await.unwrap();
|
||||
});
|
||||
}
|
144
lib/src/api/method/tests/protocol.rs
Normal file
144
lib/src/api/method/tests/protocol.rs
Normal file
|
@ -0,0 +1,144 @@
|
|||
use super::server;
|
||||
use crate::api::conn::Connection;
|
||||
use crate::api::conn::DbResponse;
|
||||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Route;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::opt::from_value;
|
||||
use crate::api::opt::Endpoint;
|
||||
use crate::api::opt::IntoEndpoint;
|
||||
use crate::api::Connect;
|
||||
use crate::api::ExtraFeatures;
|
||||
use crate::api::Response as QueryResponse;
|
||||
use crate::api::Result;
|
||||
use crate::api::Surreal;
|
||||
use flume::Receiver;
|
||||
use once_cell::sync::OnceCell;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::collections::HashSet;
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
use std::sync::atomic::AtomicI64;
|
||||
use std::sync::Arc;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Test;
|
||||
|
||||
impl IntoEndpoint<Test> for () {
|
||||
type Client = Client;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse("test://")?,
|
||||
strict: false,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Client {
|
||||
method: Method,
|
||||
}
|
||||
|
||||
impl Surreal<Client> {
|
||||
pub fn connect<P>(
|
||||
&'static self,
|
||||
address: impl IntoEndpoint<P, Client = Client>,
|
||||
) -> Connect<Client, ()> {
|
||||
Connect {
|
||||
router: Some(&self.router),
|
||||
address: address.into_endpoint(),
|
||||
capacity: 0,
|
||||
client: PhantomData,
|
||||
response_type: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::api::Connection for Client {}
|
||||
|
||||
impl Connection for Client {
|
||||
fn new(method: Method) -> Self {
|
||||
Self {
|
||||
method,
|
||||
}
|
||||
}
|
||||
|
||||
fn connect(
|
||||
_address: Endpoint,
|
||||
capacity: usize,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Surreal<Self>>> + Send + Sync + 'static>> {
|
||||
Box::pin(async move {
|
||||
let (route_tx, route_rx) = flume::bounded(capacity);
|
||||
let mut features = HashSet::new();
|
||||
features.insert(ExtraFeatures::Auth);
|
||||
features.insert(ExtraFeatures::Backup);
|
||||
let router = Router {
|
||||
features,
|
||||
conn: PhantomData,
|
||||
sender: route_tx,
|
||||
last_id: AtomicI64::new(0),
|
||||
};
|
||||
server::mock(route_rx);
|
||||
Ok(Surreal {
|
||||
router: OnceCell::with_value(Arc::new(router)),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn send<'r>(
|
||||
&'r mut self,
|
||||
router: &'r Router<Self>,
|
||||
param: Param,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Receiver<Result<DbResponse>>>> + Send + Sync + 'r>> {
|
||||
Box::pin(async move {
|
||||
let (sender, receiver) = flume::bounded(1);
|
||||
let route = Route {
|
||||
request: (0, self.method, param),
|
||||
response: sender,
|
||||
};
|
||||
router
|
||||
.sender
|
||||
.send_async(Some(route))
|
||||
.await
|
||||
.as_ref()
|
||||
.map_err(ToString::to_string)
|
||||
.unwrap();
|
||||
Ok(receiver)
|
||||
})
|
||||
}
|
||||
|
||||
fn recv<R>(
|
||||
&mut self,
|
||||
rx: Receiver<Result<DbResponse>>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<R>> + Send + Sync + '_>>
|
||||
where
|
||||
R: DeserializeOwned,
|
||||
{
|
||||
Box::pin(async move {
|
||||
let result = rx.into_recv_async().await.unwrap();
|
||||
match result.unwrap() {
|
||||
DbResponse::Other(value) => from_value(value),
|
||||
DbResponse::Query(..) => unreachable!(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn recv_query(
|
||||
&mut self,
|
||||
rx: Receiver<Result<DbResponse>>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<QueryResponse>> + Send + Sync + '_>> {
|
||||
Box::pin(async move {
|
||||
let result = rx.into_recv_async().await.unwrap();
|
||||
match result.unwrap() {
|
||||
DbResponse::Query(results) => Ok(results),
|
||||
DbResponse::Other(..) => unreachable!(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
106
lib/src/api/method/tests/server.rs
Normal file
106
lib/src/api/method/tests/server.rs
Normal file
|
@ -0,0 +1,106 @@
|
|||
use super::types::Credentials;
|
||||
use super::types::User;
|
||||
use crate::api::conn::DbResponse;
|
||||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Route;
|
||||
use crate::api::opt::from_json;
|
||||
use crate::api::opt::from_value;
|
||||
use crate::api::Response as QueryResponse;
|
||||
use crate::sql::Array;
|
||||
use crate::sql::Value;
|
||||
use flume::Receiver;
|
||||
use futures::StreamExt;
|
||||
use serde_json::json;
|
||||
use std::mem;
|
||||
|
||||
pub(super) fn mock(route_rx: Receiver<Option<Route>>) {
|
||||
tokio::spawn(async move {
|
||||
let mut stream = route_rx.into_stream();
|
||||
|
||||
while let Some(Some(Route {
|
||||
request,
|
||||
response,
|
||||
})) = stream.next().await
|
||||
{
|
||||
let (_, method, param) = request;
|
||||
let mut params = param.other;
|
||||
|
||||
let result = match method {
|
||||
Method::Invalidate | Method::Health => match ¶ms[..] {
|
||||
[] => Ok(DbResponse::Other(Value::None)),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
Method::Authenticate | Method::Kill | Method::Unset | Method::Delete => {
|
||||
match ¶ms[..] {
|
||||
[_] => Ok(DbResponse::Other(Value::None)),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
Method::Live => match ¶ms[..] {
|
||||
[_] => Ok(DbResponse::Other(
|
||||
"c6c0e36c-e2cf-42cb-b2d5-75415249b261".to_owned().into(),
|
||||
)),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
Method::Version => match ¶ms[..] {
|
||||
[] => Ok(DbResponse::Other("1.0.0".into())),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
Method::Use => match ¶ms[..] {
|
||||
[_] | [_, _] => Ok(DbResponse::Other(Value::None)),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
Method::Signup | Method::Signin => match &mut params[..] {
|
||||
[credentials] => {
|
||||
let credentials: Credentials = from_value(mem::take(credentials)).unwrap();
|
||||
match credentials {
|
||||
Credentials::Root {
|
||||
..
|
||||
} => Ok(DbResponse::Other(Value::None)),
|
||||
_ => Ok(DbResponse::Other("jwt".to_owned().into())),
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
Method::Set => match ¶ms[..] {
|
||||
[_, _] => Ok(DbResponse::Other(Value::None)),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
Method::Query => match param.query {
|
||||
Some(_) => Ok(DbResponse::Query(QueryResponse(Default::default()))),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
Method::Create => match ¶ms[..] {
|
||||
[_] => Ok(DbResponse::Other(from_json(json!(User::default())))),
|
||||
[_, user] => Ok(DbResponse::Other(user.clone())),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
Method::Select => match ¶ms[..] {
|
||||
[Value::Thing(..)] => Ok(DbResponse::Other(from_json(json!(User::default())))),
|
||||
[Value::Table(..) | Value::Array(..) | Value::Range(..)] => {
|
||||
Ok(DbResponse::Other(Value::Array(Array(Vec::new()))))
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
Method::Update | Method::Merge | Method::Patch => match ¶ms[..] {
|
||||
[Value::Thing(..)] | [Value::Thing(..), _] => {
|
||||
Ok(DbResponse::Other(from_json(json!(User::default()))))
|
||||
}
|
||||
[Value::Table(..) | Value::Array(..) | Value::Range(..)]
|
||||
| [Value::Table(..) | Value::Array(..) | Value::Range(..), _] => {
|
||||
Ok(DbResponse::Other(Value::Array(Array(Vec::new()))))
|
||||
}
|
||||
_ => unreachable!(),
|
||||
},
|
||||
Method::Export | Method::Import => match param.file {
|
||||
Some(_) => Ok(DbResponse::Other(Value::None)),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
};
|
||||
|
||||
if let Err(message) = response.into_send_async(result).await {
|
||||
panic!("message dropped; {message:?}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
38
lib/src/api/method/tests/types.rs
Normal file
38
lib/src/api/method/tests/types.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
pub const USER: &str = "user";
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum Credentials {
|
||||
Database {
|
||||
ns: String,
|
||||
db: String,
|
||||
user: String,
|
||||
pass: String,
|
||||
},
|
||||
Namespace {
|
||||
ns: String,
|
||||
user: String,
|
||||
pass: String,
|
||||
},
|
||||
Root {
|
||||
user: String,
|
||||
pass: String,
|
||||
},
|
||||
Scope {
|
||||
ns: String,
|
||||
db: String,
|
||||
sc: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AuthParams {}
|
30
lib/src/api/method/unset.rs
Normal file
30
lib/src/api/method/unset.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::Connection;
|
||||
use crate::api::Result;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// An unset future
|
||||
#[derive(Debug)]
|
||||
pub struct Unset<'r, C: Connection> {
|
||||
pub(super) router: Result<&'r Router<C>>,
|
||||
pub(super) key: String,
|
||||
}
|
||||
|
||||
impl<'r, Client> IntoFuture for Unset<'r, Client>
|
||||
where
|
||||
Client: Connection,
|
||||
{
|
||||
type Output = Result<()>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
let mut conn = Client::new(Method::Unset);
|
||||
conn.execute(self.router?, Param::new(vec![self.key.into()])).await
|
||||
})
|
||||
}
|
||||
}
|
135
lib/src/api/method/update.rs
Normal file
135
lib/src/api/method/update.rs
Normal file
|
@ -0,0 +1,135 @@
|
|||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::method::Content;
|
||||
use crate::api::method::Merge;
|
||||
use crate::api::method::Patch;
|
||||
use crate::api::opt::PatchOp;
|
||||
use crate::api::opt::Range;
|
||||
use crate::api::opt::Resource;
|
||||
use crate::api::Connection;
|
||||
use crate::api::Result;
|
||||
use crate::sql::Id;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// An update future
|
||||
#[derive(Debug)]
|
||||
pub struct Update<'r, C: Connection, R> {
|
||||
pub(super) router: Result<&'r Router<C>>,
|
||||
pub(super) resource: Result<Resource>,
|
||||
pub(super) range: Option<Range<Id>>,
|
||||
pub(super) response_type: PhantomData<R>,
|
||||
}
|
||||
|
||||
impl<'r, Client, R> Update<'r, Client, R>
|
||||
where
|
||||
Client: Connection,
|
||||
{
|
||||
async fn execute<T>(self) -> Result<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let resource = self.resource?;
|
||||
let param = match self.range {
|
||||
Some(range) => resource.with_range(range)?,
|
||||
None => resource.into(),
|
||||
};
|
||||
let mut conn = Client::new(Method::Update);
|
||||
conn.execute(self.router?, Param::new(vec![param])).await
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r, Client, R> IntoFuture for Update<'r, Client, Option<R>>
|
||||
where
|
||||
Client: Connection,
|
||||
R: DeserializeOwned + Send + Sync + 'r,
|
||||
{
|
||||
type Output = Result<R>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(self.execute())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r, Client, R> IntoFuture for Update<'r, Client, Vec<R>>
|
||||
where
|
||||
Client: Connection,
|
||||
R: DeserializeOwned + Send + Sync + 'r,
|
||||
{
|
||||
type Output = Result<Vec<R>>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(self.execute())
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, R> Update<'_, C, Vec<R>>
|
||||
where
|
||||
C: Connection,
|
||||
{
|
||||
/// Restricts the records to update to those in the specified range
|
||||
pub fn range(mut self, bounds: impl Into<Range<Id>>) -> Self {
|
||||
self.range = Some(bounds.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! update_methods {
|
||||
($this:ty, $res:ty) => {
|
||||
impl<'r, C, R> Update<'r, C, $this>
|
||||
where
|
||||
C: Connection,
|
||||
R: DeserializeOwned + Send + Sync,
|
||||
{
|
||||
/// Replaces the current document / record data with the specified data
|
||||
pub fn content<D>(self, data: D) -> Content<'r, C, D, $res>
|
||||
where
|
||||
D: Serialize,
|
||||
{
|
||||
Content {
|
||||
router: self.router,
|
||||
method: Method::Update,
|
||||
resource: self.resource,
|
||||
range: self.range,
|
||||
content: data,
|
||||
response_type: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Merges the current document / record data with the specified data
|
||||
pub fn merge<D>(self, data: D) -> Merge<'r, C, D, $res>
|
||||
where
|
||||
D: Serialize,
|
||||
{
|
||||
Merge {
|
||||
router: self.router,
|
||||
resource: self.resource,
|
||||
range: self.range,
|
||||
content: data,
|
||||
response_type: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Patches the current document / record data with the specified JSON Patch data
|
||||
pub fn patch(self, PatchOp(patch): PatchOp) -> Patch<'r, C, $res> {
|
||||
Patch {
|
||||
router: self.router,
|
||||
resource: self.resource,
|
||||
range: self.range,
|
||||
patches: vec![patch],
|
||||
response_type: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
update_methods!(Option<R>, R);
|
||||
update_methods!(Vec<R>, Vec<R>);
|
31
lib/src/api/method/use_db.rs
Normal file
31
lib/src/api/method/use_db.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
use crate::api::conn::Method;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::Connection;
|
||||
use crate::api::Result;
|
||||
use crate::api::conn::Router;
|
||||
use std::future::IntoFuture;
|
||||
use crate::sql::Value;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UseDb<'r, C: Connection> {
|
||||
pub(super) router: Result<&'r Router<C>>,
|
||||
pub(super) db: String,
|
||||
}
|
||||
|
||||
impl<'r, Client> IntoFuture for UseDb<'r, Client>
|
||||
where
|
||||
Client: Connection,
|
||||
{
|
||||
type Output = Result<()>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
let mut conn = Client::new(Method::Use);
|
||||
conn.execute(self.router?, Param::new(vec![Value::None, self.db.into()]))
|
||||
.await
|
||||
})
|
||||
}
|
||||
}
|
52
lib/src/api/method/use_ns.rs
Normal file
52
lib/src/api/method/use_ns.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::Connection;
|
||||
use crate::api::Result;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// Stores the namespace to use
|
||||
#[derive(Debug)]
|
||||
pub struct UseNs<'r, C: Connection> {
|
||||
pub(super) router: Result<&'r Router<C>>,
|
||||
pub(super) ns: String,
|
||||
}
|
||||
|
||||
/// A use NS and DB future
|
||||
#[derive(Debug)]
|
||||
pub struct UseNsDb<'r, C: Connection> {
|
||||
pub(super) router: Result<&'r Router<C>>,
|
||||
pub(super) ns: String,
|
||||
pub(super) db: String,
|
||||
}
|
||||
|
||||
impl<'r, C> UseNs<'r, C>
|
||||
where
|
||||
C: Connection,
|
||||
{
|
||||
/// Switch to a specific database
|
||||
pub fn use_db(self, db: impl Into<String>) -> UseNsDb<'r, C> {
|
||||
UseNsDb {
|
||||
db: db.into(),
|
||||
ns: self.ns,
|
||||
router: self.router,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r, Client> IntoFuture for UseNsDb<'r, Client>
|
||||
where
|
||||
Client: Connection,
|
||||
{
|
||||
type Output = Result<()>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
let mut conn = Client::new(Method::Use);
|
||||
conn.execute(self.router?, Param::new(vec![self.ns.into(), self.db.into()])).await
|
||||
})
|
||||
}
|
||||
}
|
32
lib/src/api/method/version.rs
Normal file
32
lib/src/api/method/version.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
use crate::api::conn::Method;
|
||||
use crate::api::conn::Param;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::err::Error;
|
||||
use crate::api::Connection;
|
||||
use crate::api::Result;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// A version future
|
||||
#[derive(Debug)]
|
||||
pub struct Version<'r, C: Connection> {
|
||||
pub(super) router: Result<&'r Router<C>>,
|
||||
}
|
||||
|
||||
impl<'r, Client> IntoFuture for Version<'r, Client>
|
||||
where
|
||||
Client: Connection,
|
||||
{
|
||||
type Output = Result<semver::Version>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async {
|
||||
let mut conn = Client::new(Method::Version);
|
||||
let version: String = conn.execute(self.router?, Param::new(Vec::new())).await?;
|
||||
let semantic = version.trim_start_matches("surrealdb-");
|
||||
semantic.parse().map_err(|_| Error::InvalidSemanticVersion(semantic.to_string()).into())
|
||||
})
|
||||
}
|
||||
}
|
192
lib/src/api/mod.rs
Normal file
192
lib/src/api/mod.rs
Normal file
|
@ -0,0 +1,192 @@
|
|||
//! Functionality for connecting to local and remote databases
|
||||
|
||||
pub mod engines;
|
||||
pub mod err;
|
||||
pub mod method;
|
||||
pub mod opt;
|
||||
|
||||
mod conn;
|
||||
|
||||
pub use method::query::Response;
|
||||
|
||||
use crate::api::conn::DbResponse;
|
||||
use crate::api::conn::Router;
|
||||
use crate::api::err::Error;
|
||||
use crate::api::opt::Endpoint;
|
||||
use once_cell::sync::OnceCell;
|
||||
use semver::BuildMetadata;
|
||||
use semver::VersionReq;
|
||||
use std::fmt::Debug;
|
||||
use std::future::Future;
|
||||
use std::future::IntoFuture;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A specialized `Result` type
|
||||
pub type Result<T> = std::result::Result<T, crate::Error>;
|
||||
|
||||
const SUPPORTED_VERSIONS: (&str, &str) = (">=1.0.0-beta.8, <2.0.0", "20221030.c12a1cc");
|
||||
const LOG: &str = "surrealdb::api";
|
||||
|
||||
/// Connection trait implemented by supported engines
|
||||
pub trait Connection: conn::Connection {}
|
||||
|
||||
/// The future returned when creating a new SurrealDB instance
|
||||
#[derive(Debug)]
|
||||
pub struct Connect<'r, C: Connection, Response> {
|
||||
router: Option<&'r OnceCell<Arc<Router<C>>>>,
|
||||
address: Result<Endpoint>,
|
||||
capacity: usize,
|
||||
client: PhantomData<C>,
|
||||
response_type: PhantomData<Response>,
|
||||
}
|
||||
|
||||
impl<C, R> Connect<'_, C, R>
|
||||
where
|
||||
C: Connection,
|
||||
{
|
||||
/// Sets the maximum capacity of the connection
|
||||
///
|
||||
/// This is used to set bounds of the channels used internally
|
||||
/// as well set the capacity of the `HashMap` used for routing
|
||||
/// responses in case of the WebSocket client.
|
||||
///
|
||||
/// Setting this capacity to `0` (the default) means that
|
||||
/// unbounded channels will be used. If your queries per second
|
||||
/// are so high that the client is running out of memory,
|
||||
/// it might be helpful to set this to a number that works best
|
||||
/// for you.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> surrealdb::Result<()> {
|
||||
/// use surrealdb::engines::remote::ws::Ws;
|
||||
/// use surrealdb::Surreal;
|
||||
///
|
||||
/// let db = Surreal::new::<Ws>("localhost:8000")
|
||||
/// .with_capacity(100_000)
|
||||
/// .await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub const fn with_capacity(mut self, capacity: usize) -> Self {
|
||||
self.capacity = capacity;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r, Client> IntoFuture for Connect<'r, Client, Surreal<Client>>
|
||||
where
|
||||
Client: Connection,
|
||||
{
|
||||
type Output = Result<Surreal<Client>>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
let client = Client::connect(self.address?, self.capacity).await?;
|
||||
client.check_server_version();
|
||||
Ok(client)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'r, Client> IntoFuture for Connect<'r, Client, ()>
|
||||
where
|
||||
Client: Connection,
|
||||
{
|
||||
type Output = Result<()>;
|
||||
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + Sync + 'r>>;
|
||||
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
match self.router {
|
||||
Some(router) => {
|
||||
let option =
|
||||
Client::connect(self.address?, self.capacity).await?.router.into_inner();
|
||||
match option {
|
||||
Some(client) => {
|
||||
let _res = router.set(client);
|
||||
}
|
||||
None => unreachable!(),
|
||||
}
|
||||
}
|
||||
None => unreachable!(),
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
pub(crate) enum ExtraFeatures {
|
||||
Auth,
|
||||
Backup,
|
||||
}
|
||||
|
||||
/// A database client instance for embedded or remote databases
|
||||
#[derive(Debug)]
|
||||
pub struct Surreal<C: Connection> {
|
||||
router: OnceCell<Arc<Router<C>>>,
|
||||
}
|
||||
|
||||
impl<C> Surreal<C>
|
||||
where
|
||||
C: Connection,
|
||||
{
|
||||
fn check_server_version(&self) {
|
||||
let conn = self.clone();
|
||||
tokio::spawn(async move {
|
||||
let (versions, build_meta) = SUPPORTED_VERSIONS;
|
||||
// invalid version requirements should be caught during development
|
||||
let req = VersionReq::parse(versions).expect("valid supported versions");
|
||||
let build_meta =
|
||||
BuildMetadata::new(build_meta).expect("valid supported build metadata");
|
||||
match conn.version().await {
|
||||
Ok(version) => {
|
||||
let server_build = &version.build;
|
||||
if !req.matches(&version) {
|
||||
warn!(target: LOG, "server version `{version}` does not match the range supported by the client `{versions}`");
|
||||
} else if !server_build.is_empty() && server_build < &build_meta {
|
||||
warn!(target: LOG, "server build `{server_build}` is older than the minimum supported build `{build_meta}`");
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
trace!(target: LOG, "failed to lookup the server version; {error:?}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> Clone for Surreal<C>
|
||||
where
|
||||
C: Connection,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
router: self.router.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait ExtractRouter<C>
|
||||
where
|
||||
C: Connection,
|
||||
{
|
||||
fn extract(&self) -> Result<&Router<C>>;
|
||||
}
|
||||
|
||||
impl<C> ExtractRouter<C> for OnceCell<Arc<Router<C>>>
|
||||
where
|
||||
C: Connection,
|
||||
{
|
||||
fn extract(&self) -> Result<&Router<C>> {
|
||||
let router = self.get().ok_or(Error::ConnectionUninitialised)?;
|
||||
Ok(router)
|
||||
}
|
||||
}
|
118
lib/src/api/opt/auth.rs
Normal file
118
lib/src/api/opt/auth.rs
Normal file
|
@ -0,0 +1,118 @@
|
|||
//! Authentication types
|
||||
|
||||
use crate::sql::Value;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::fmt;
|
||||
|
||||
/// A signup action
|
||||
#[derive(Debug)]
|
||||
pub struct Signup;
|
||||
|
||||
/// A signin action
|
||||
#[derive(Debug)]
|
||||
pub struct Signin;
|
||||
|
||||
/// Credentials for authenticating with the server
|
||||
pub trait Credentials<Action, Response>: Serialize {}
|
||||
|
||||
/// Credentials for the root user
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Root<'a> {
|
||||
/// The username of the root user
|
||||
#[serde(rename = "user")]
|
||||
pub username: &'a str,
|
||||
/// The password of the root user
|
||||
#[serde(rename = "pass")]
|
||||
pub password: &'a str,
|
||||
}
|
||||
|
||||
impl Credentials<Signin, ()> for Root<'_> {}
|
||||
|
||||
/// Credentials for the namespace user
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Namespace<'a> {
|
||||
/// The namespace the user has access to
|
||||
#[serde(rename = "ns")]
|
||||
pub namespace: &'a str,
|
||||
/// The username of the namespace user
|
||||
#[serde(rename = "user")]
|
||||
pub username: &'a str,
|
||||
/// The password of the namespace user
|
||||
#[serde(rename = "pass")]
|
||||
pub password: &'a str,
|
||||
}
|
||||
|
||||
impl Credentials<Signin, Jwt> for Namespace<'_> {}
|
||||
|
||||
/// Credentials for the database user
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Database<'a> {
|
||||
/// The namespace the user has access to
|
||||
#[serde(rename = "ns")]
|
||||
pub namespace: &'a str,
|
||||
/// The database the user has access to
|
||||
#[serde(rename = "db")]
|
||||
pub database: &'a str,
|
||||
/// The username of the database user
|
||||
#[serde(rename = "user")]
|
||||
pub username: &'a str,
|
||||
/// The password of the database user
|
||||
#[serde(rename = "pass")]
|
||||
pub password: &'a str,
|
||||
}
|
||||
|
||||
impl Credentials<Signin, Jwt> for Database<'_> {}
|
||||
|
||||
/// Credentials for the scope user
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Scope<'a, P> {
|
||||
/// The namespace the user has access to
|
||||
#[serde(rename = "ns")]
|
||||
pub namespace: &'a str,
|
||||
/// The database the user has access to
|
||||
#[serde(rename = "db")]
|
||||
pub database: &'a str,
|
||||
/// The scope to use for signin and signup
|
||||
#[serde(rename = "sc")]
|
||||
pub scope: &'a str,
|
||||
/// The additional params to use
|
||||
#[serde(flatten)]
|
||||
pub params: P,
|
||||
}
|
||||
|
||||
impl<T, P> Credentials<T, Jwt> for Scope<'_, P> where P: Serialize {}
|
||||
|
||||
/// A JSON Web Token for authenticating with the server
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Jwt(pub(crate) String);
|
||||
|
||||
impl From<String> for Jwt {
|
||||
fn from(jwt: String) -> Self {
|
||||
Jwt(jwt)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a String> for Jwt {
|
||||
fn from(jwt: &'a String) -> Self {
|
||||
Jwt(jwt.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Jwt {
|
||||
fn from(jwt: &'a str) -> Self {
|
||||
Jwt(jwt.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Jwt> for Value {
|
||||
fn from(Jwt(jwt): Jwt) -> Self {
|
||||
jwt.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Jwt {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Jwt(REDUCTED)")
|
||||
}
|
||||
}
|
54
lib/src/api/opt/endpoint/fdb.rs
Normal file
54
lib/src/api/opt/endpoint/fdb.rs
Normal file
|
@ -0,0 +1,54 @@
|
|||
use crate::api::engines::local::Db;
|
||||
use crate::api::engines::local::FDb;
|
||||
use crate::api::err::Error;
|
||||
use crate::api::opt::Endpoint;
|
||||
use crate::api::opt::IntoEndpoint;
|
||||
use crate::api::opt::Strict;
|
||||
use crate::api::Result;
|
||||
use std::path::Path;
|
||||
use url::Url;
|
||||
|
||||
impl IntoEndpoint<FDb> for &str {
|
||||
type Client = Db;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let url = format!("fdb://{self}");
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
|
||||
strict: false,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoEndpoint<FDb> for &Path {
|
||||
type Client = Db;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let url = format!("fdb://{}", self.display());
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
|
||||
strict: false,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoEndpoint<FDb> for (T, Strict)
|
||||
where
|
||||
T: AsRef<Path>,
|
||||
{
|
||||
type Client = Db;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let url = format!("fdb://{}", self.0.as_ref().display());
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
|
||||
strict: true,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
127
lib/src/api/opt/endpoint/http.rs
Normal file
127
lib/src/api/opt/endpoint/http.rs
Normal file
|
@ -0,0 +1,127 @@
|
|||
use crate::api::engines::remote::http::Client;
|
||||
use crate::api::engines::remote::http::Http;
|
||||
use crate::api::engines::remote::http::Https;
|
||||
use crate::api::err::Error;
|
||||
use crate::api::opt::IntoEndpoint;
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
use crate::api::opt::Tls;
|
||||
use crate::api::Endpoint;
|
||||
use crate::api::Result;
|
||||
use std::net::SocketAddr;
|
||||
use url::Url;
|
||||
|
||||
impl IntoEndpoint<Http> for &str {
|
||||
type Client = Client;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let url = format!("http://{self}");
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
|
||||
strict: false,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoEndpoint<Http> for SocketAddr {
|
||||
type Client = Client;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let url = format!("http://{self}");
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
|
||||
strict: false,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoEndpoint<Http> for String {
|
||||
type Client = Client;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let url = format!("http://{self}");
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
|
||||
strict: false,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoEndpoint<Https> for &str {
|
||||
type Client = Client;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let url = format!("https://{self}");
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
|
||||
strict: false,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoEndpoint<Https> for SocketAddr {
|
||||
type Client = Client;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let url = format!("https://{self}");
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
|
||||
strict: false,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoEndpoint<Https> for String {
|
||||
type Client = Client;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let url = format!("https://{self}");
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
|
||||
strict: false,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
|
||||
impl<T> IntoEndpoint<Https> for (T, native_tls::TlsConnector)
|
||||
where
|
||||
T: IntoEndpoint<Https>,
|
||||
{
|
||||
type Client = Client;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let (address, config) = self;
|
||||
let mut address = address.into_endpoint()?;
|
||||
address.tls_config = Some(Tls::Native(config));
|
||||
Ok(address)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rustls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
|
||||
impl<T> IntoEndpoint<Https> for (T, rustls::ClientConfig)
|
||||
where
|
||||
T: IntoEndpoint<Https>,
|
||||
{
|
||||
type Client = Client;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let (address, config) = self;
|
||||
let mut address = address.into_endpoint()?;
|
||||
address.tls_config = Some(Tls::Rust(config));
|
||||
Ok(address)
|
||||
}
|
||||
}
|
32
lib/src/api/opt/endpoint/indxdb.rs
Normal file
32
lib/src/api/opt/endpoint/indxdb.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
use crate::api::engines::local::Db;
|
||||
use crate::api::engines::local::IndxDb;
|
||||
use crate::api::err::Error;
|
||||
use crate::api::opt::Endpoint;
|
||||
use crate::api::opt::IntoEndpoint;
|
||||
use crate::api::opt::Strict;
|
||||
use crate::api::Result;
|
||||
use url::Url;
|
||||
|
||||
impl IntoEndpoint<IndxDb> for &str {
|
||||
type Client = Db;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let url = format!("indxdb://{self}");
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
|
||||
strict: false,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoEndpoint<IndxDb> for (&str, Strict) {
|
||||
type Client = Db;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let mut address = IntoEndpoint::<IndxDb>::into_endpoint(self.0)?;
|
||||
address.strict = true;
|
||||
Ok(address)
|
||||
}
|
||||
}
|
30
lib/src/api/opt/endpoint/mem.rs
Normal file
30
lib/src/api/opt/endpoint/mem.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
use crate::api::engines::local::Db;
|
||||
use crate::api::engines::local::Mem;
|
||||
use crate::api::opt::Endpoint;
|
||||
use crate::api::opt::IntoEndpoint;
|
||||
use crate::api::opt::Strict;
|
||||
use crate::api::Result;
|
||||
use url::Url;
|
||||
|
||||
impl IntoEndpoint<Mem> for () {
|
||||
type Client = Db;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse("mem://").unwrap(),
|
||||
strict: false,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoEndpoint<Mem> for Strict {
|
||||
type Client = Db;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let mut address = IntoEndpoint::<Mem>::into_endpoint(())?;
|
||||
address.strict = true;
|
||||
Ok(address)
|
||||
}
|
||||
}
|
38
lib/src/api/opt/endpoint/mod.rs
Normal file
38
lib/src/api/opt/endpoint/mod.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
#[cfg(feature = "protocol-http")]
|
||||
mod http;
|
||||
#[cfg(feature = "protocol-ws")]
|
||||
mod ws;
|
||||
|
||||
#[cfg(feature = "kv-fdb")]
|
||||
mod fdb;
|
||||
#[cfg(feature = "kv-indxdb")]
|
||||
mod indxdb;
|
||||
#[cfg(feature = "kv-mem")]
|
||||
mod mem;
|
||||
#[cfg(feature = "kv-rocksdb")]
|
||||
mod rocksdb;
|
||||
#[cfg(feature = "kv-tikv")]
|
||||
mod tikv;
|
||||
|
||||
use crate::api::Connection;
|
||||
use crate::api::Result;
|
||||
use url::Url;
|
||||
|
||||
/// A server address used to connect to the server
|
||||
#[derive(Debug)]
|
||||
#[allow(dead_code)] // used by the embedded and remote connections
|
||||
pub struct Endpoint {
|
||||
pub(crate) endpoint: Url,
|
||||
#[allow(dead_code)] // used by the embedded database
|
||||
pub(crate) strict: bool,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
pub(crate) tls_config: Option<super::Tls>,
|
||||
}
|
||||
|
||||
/// A trait for converting inputs to a server address object
|
||||
pub trait IntoEndpoint<Scheme> {
|
||||
/// The client implied by this scheme and address combination
|
||||
type Client: Connection;
|
||||
/// Converts an input into a server address object
|
||||
fn into_endpoint(self) -> Result<Endpoint>;
|
||||
}
|
100
lib/src/api/opt/endpoint/rocksdb.rs
Normal file
100
lib/src/api/opt/endpoint/rocksdb.rs
Normal file
|
@ -0,0 +1,100 @@
|
|||
use crate::api::engines::local::Db;
|
||||
use crate::api::engines::local::File;
|
||||
use crate::api::engines::local::RocksDb;
|
||||
use crate::api::err::Error;
|
||||
use crate::api::opt::Endpoint;
|
||||
use crate::api::opt::IntoEndpoint;
|
||||
use crate::api::opt::Strict;
|
||||
use crate::api::Result;
|
||||
use std::path::Path;
|
||||
use url::Url;
|
||||
|
||||
impl IntoEndpoint<RocksDb> for &str {
|
||||
type Client = Db;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let url = format!("rocksdb://{self}");
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
|
||||
strict: false,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoEndpoint<RocksDb> for &Path {
|
||||
type Client = Db;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let url = format!("rocksdb://{}", self.display());
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
|
||||
strict: false,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoEndpoint<RocksDb> for (T, Strict)
|
||||
where
|
||||
T: AsRef<Path>,
|
||||
{
|
||||
type Client = Db;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let url = format!("rocksdb://{}", self.0.as_ref().display());
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
|
||||
strict: true,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoEndpoint<File> for &str {
|
||||
type Client = Db;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let url = format!("file://{self}");
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
|
||||
strict: false,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoEndpoint<File> for &Path {
|
||||
type Client = Db;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let url = format!("file://{}", self.display());
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
|
||||
strict: false,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoEndpoint<File> for (T, Strict)
|
||||
where
|
||||
T: AsRef<Path>,
|
||||
{
|
||||
type Client = Db;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let url = format!("file://{}", self.0.as_ref().display());
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
|
||||
strict: true,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
64
lib/src/api/opt/endpoint/tikv.rs
Normal file
64
lib/src/api/opt/endpoint/tikv.rs
Normal file
|
@ -0,0 +1,64 @@
|
|||
use crate::api::engines::local::Db;
|
||||
use crate::api::engines::local::TiKv;
|
||||
use crate::api::err::Error;
|
||||
use crate::api::opt::Endpoint;
|
||||
use crate::api::opt::IntoEndpoint;
|
||||
use crate::api::opt::Strict;
|
||||
use crate::api::Result;
|
||||
use std::net::SocketAddr;
|
||||
use url::Url;
|
||||
|
||||
impl IntoEndpoint<TiKv> for &str {
|
||||
type Client = Db;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let url = format!("tikv://{self}");
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
|
||||
strict: false,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoEndpoint<TiKv> for SocketAddr {
|
||||
type Client = Db;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let url = format!("tikv://{self}");
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
|
||||
strict: false,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoEndpoint<TiKv> for String {
|
||||
type Client = Db;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let url = format!("tikv://{self}");
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
|
||||
strict: false,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoEndpoint<TiKv> for (T, Strict)
|
||||
where
|
||||
T: IntoEndpoint<TiKv>,
|
||||
{
|
||||
type Client = Db;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let mut address = self.0.into_endpoint()?;
|
||||
address.strict = true;
|
||||
Ok(address)
|
||||
}
|
||||
}
|
127
lib/src/api/opt/endpoint/ws.rs
Normal file
127
lib/src/api/opt/endpoint/ws.rs
Normal file
|
@ -0,0 +1,127 @@
|
|||
use crate::api::engines::remote::ws::Client;
|
||||
use crate::api::engines::remote::ws::Ws;
|
||||
use crate::api::engines::remote::ws::Wss;
|
||||
use crate::api::err::Error;
|
||||
use crate::api::opt::Endpoint;
|
||||
use crate::api::opt::IntoEndpoint;
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
use crate::api::opt::Tls;
|
||||
use crate::api::Result;
|
||||
use std::net::SocketAddr;
|
||||
use url::Url;
|
||||
|
||||
impl IntoEndpoint<Ws> for &str {
|
||||
type Client = Client;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let url = format!("ws://{self}");
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
|
||||
strict: false,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoEndpoint<Ws> for SocketAddr {
|
||||
type Client = Client;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let url = format!("ws://{self}");
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
|
||||
strict: false,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoEndpoint<Ws> for String {
|
||||
type Client = Client;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let url = format!("ws://{self}");
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
|
||||
strict: false,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoEndpoint<Wss> for &str {
|
||||
type Client = Client;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let url = format!("wss://{self}");
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
|
||||
strict: false,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoEndpoint<Wss> for SocketAddr {
|
||||
type Client = Client;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let url = format!("wss://{self}");
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
|
||||
strict: false,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoEndpoint<Wss> for String {
|
||||
type Client = Client;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let url = format!("wss://{self}");
|
||||
Ok(Endpoint {
|
||||
endpoint: Url::parse(&url).map_err(|_| Error::InvalidUrl(url))?,
|
||||
strict: false,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
tls_config: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
|
||||
impl<T> IntoEndpoint<Wss> for (T, native_tls::TlsConnector)
|
||||
where
|
||||
T: IntoEndpoint<Wss>,
|
||||
{
|
||||
type Client = Client;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let (address, config) = self;
|
||||
let mut address = address.into_endpoint()?;
|
||||
address.tls_config = Some(Tls::Native(config));
|
||||
Ok(address)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rustls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
|
||||
impl<T> IntoEndpoint<Wss> for (T, rustls::ClientConfig)
|
||||
where
|
||||
T: IntoEndpoint<Wss>,
|
||||
{
|
||||
type Client = Client;
|
||||
|
||||
fn into_endpoint(self) -> Result<Endpoint> {
|
||||
let (address, config) = self;
|
||||
let mut address = address.into_endpoint()?;
|
||||
address.tls_config = Some(Tls::Rust(config));
|
||||
Ok(address)
|
||||
}
|
||||
}
|
197
lib/src/api/opt/mod.rs
Normal file
197
lib/src/api/opt/mod.rs
Normal file
|
@ -0,0 +1,197 @@
|
|||
//! The different options and types for use in API functions
|
||||
|
||||
pub mod auth;
|
||||
|
||||
mod endpoint;
|
||||
mod query;
|
||||
mod resource;
|
||||
mod strict;
|
||||
mod tls;
|
||||
|
||||
use crate::api::err::Error;
|
||||
use crate::api::Result;
|
||||
use crate::sql;
|
||||
use crate::sql::Thing;
|
||||
use crate::sql::Value;
|
||||
use dmp::Diff;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub use endpoint::*;
|
||||
pub use query::*;
|
||||
pub use resource::*;
|
||||
pub use strict::*;
|
||||
pub use tls::*;
|
||||
|
||||
/// Record ID
|
||||
pub type RecordId = Thing;
|
||||
|
||||
type UnitOp<'a> = InnerOp<'a, ()>;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(tag = "op", rename_all = "lowercase")]
|
||||
enum InnerOp<'a, T> {
|
||||
Add {
|
||||
path: &'a str,
|
||||
value: T,
|
||||
},
|
||||
Remove {
|
||||
path: &'a str,
|
||||
},
|
||||
Replace {
|
||||
path: &'a str,
|
||||
value: T,
|
||||
},
|
||||
Change {
|
||||
path: &'a str,
|
||||
value: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// A [JSON Patch] operation
|
||||
///
|
||||
/// From the official website:
|
||||
///
|
||||
/// > JSON Patch is a format for describing changes to a JSON document.
|
||||
/// > It can be used to avoid sending a whole document when only a part has changed.
|
||||
///
|
||||
/// [JSON Patch]: https://jsonpatch.com/
|
||||
#[derive(Debug)]
|
||||
pub struct PatchOp(pub(crate) Value);
|
||||
|
||||
impl PatchOp {
|
||||
/// Adds a value to an object or inserts it into an array.
|
||||
///
|
||||
/// In the case of an array, the value is inserted before the given index.
|
||||
/// The `-` character can be used instead of an index to insert at the end of an array.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use serde_json::json;
|
||||
/// # use surrealdb::opt::PatchOp;
|
||||
/// PatchOp::add("/biscuits/1", json!({ "name": "Ginger Nut" }))
|
||||
/// # ;
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn add<T>(path: &str, value: T) -> Self
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
let value = from_json(json!(InnerOp::Add {
|
||||
path,
|
||||
value
|
||||
}));
|
||||
Self(value)
|
||||
}
|
||||
|
||||
/// Removes a value from an object or array.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use surrealdb::opt::PatchOp;
|
||||
/// PatchOp::remove("/biscuits")
|
||||
/// # ;
|
||||
/// ```
|
||||
///
|
||||
/// Remove the first element of the array at `biscuits`
|
||||
/// (or just removes the “0” key if `biscuits` is an object)
|
||||
///
|
||||
/// ```
|
||||
/// # use surrealdb::opt::PatchOp;
|
||||
/// PatchOp::remove("/biscuits/0")
|
||||
/// # ;
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn remove(path: &str) -> Self {
|
||||
let value = from_json(json!(UnitOp::Remove {
|
||||
path
|
||||
}));
|
||||
Self(value)
|
||||
}
|
||||
|
||||
/// Replaces a value.
|
||||
///
|
||||
/// Equivalent to a “remove” followed by an “add”.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use surrealdb::opt::PatchOp;
|
||||
/// PatchOp::replace("/biscuits/0/name", "Chocolate Digestive")
|
||||
/// # ;
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn replace<T>(path: &str, value: T) -> Self
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
let value = from_json(json!(InnerOp::Replace {
|
||||
path,
|
||||
value
|
||||
}));
|
||||
Self(value)
|
||||
}
|
||||
|
||||
/// Changes a value
|
||||
#[must_use]
|
||||
pub fn change(path: &str, diff: Diff) -> Self {
|
||||
let value = from_json(json!(UnitOp::Change {
|
||||
path,
|
||||
value: diff.text,
|
||||
}));
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserializes a value `T` from `SurrealDB` [`Value`]
|
||||
pub(crate) fn from_value<T>(value: sql::Value) -> Result<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let bytes = match msgpack::to_vec(&value) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(error) => {
|
||||
return Err(Error::FromValue {
|
||||
value,
|
||||
error: error.to_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
};
|
||||
match msgpack::from_slice(&bytes) {
|
||||
Ok(response) => Ok(response),
|
||||
Err(error) => Err(Error::FromValue {
|
||||
value,
|
||||
error: error.to_string(),
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_json(json: JsonValue) -> sql::Value {
|
||||
match json {
|
||||
JsonValue::Null => sql::Value::None,
|
||||
JsonValue::Bool(boolean) => boolean.into(),
|
||||
JsonValue::Number(number) => match (number.as_u64(), number.as_i64(), number.as_f64()) {
|
||||
(Some(number), _, _) => number.into(),
|
||||
(_, Some(number), _) => number.into(),
|
||||
(_, _, Some(number)) => number.into(),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
JsonValue::String(string) => match sql::thing(&string) {
|
||||
Ok(thing) => thing.into(),
|
||||
Err(_) => string.into(),
|
||||
},
|
||||
JsonValue::Array(array) => array.into_iter().map(from_json).collect::<Vec<_>>().into(),
|
||||
JsonValue::Object(object) => object
|
||||
.into_iter()
|
||||
.map(|(key, value)| (key, from_json(value)))
|
||||
.collect::<BTreeMap<_, _>>()
|
||||
.into(),
|
||||
}
|
||||
}
|
329
lib/src/api/opt/query.rs
Normal file
329
lib/src/api/opt/query.rs
Normal file
|
@ -0,0 +1,329 @@
|
|||
use crate::api::err::Error;
|
||||
use crate::api::opt::from_value;
|
||||
use crate::api::Response as QueryResponse;
|
||||
use crate::api::Result;
|
||||
use crate::sql;
|
||||
use crate::sql::statements::*;
|
||||
use crate::sql::Object;
|
||||
use crate::sql::Statement;
|
||||
use crate::sql::Statements;
|
||||
use crate::sql::Value;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::mem;
|
||||
|
||||
/// A trait for converting inputs into SQL statements
|
||||
pub trait IntoQuery {
|
||||
/// Converts an input into SQL statements
|
||||
fn into_query(self) -> Result<Vec<Statement>>;
|
||||
}
|
||||
|
||||
impl IntoQuery for sql::Query {
|
||||
fn into_query(self) -> Result<Vec<Statement>> {
|
||||
let sql::Query(Statements(statements)) = self;
|
||||
Ok(statements)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoQuery for Statements {
|
||||
fn into_query(self) -> Result<Vec<Statement>> {
|
||||
let Statements(statements) = self;
|
||||
Ok(statements)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoQuery for Vec<Statement> {
|
||||
fn into_query(self) -> Result<Vec<Statement>> {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoQuery for Statement {
|
||||
fn into_query(self) -> Result<Vec<Statement>> {
|
||||
Ok(vec![self])
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoQuery for UseStatement {
|
||||
fn into_query(self) -> Result<Vec<Statement>> {
|
||||
Ok(vec![Statement::Use(self)])
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoQuery for SetStatement {
|
||||
fn into_query(self) -> Result<Vec<Statement>> {
|
||||
Ok(vec![Statement::Set(self)])
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoQuery for InfoStatement {
|
||||
fn into_query(self) -> Result<Vec<Statement>> {
|
||||
Ok(vec![Statement::Info(self)])
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoQuery for LiveStatement {
|
||||
fn into_query(self) -> Result<Vec<Statement>> {
|
||||
Ok(vec![Statement::Live(self)])
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoQuery for KillStatement {
|
||||
fn into_query(self) -> Result<Vec<Statement>> {
|
||||
Ok(vec![Statement::Kill(self)])
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoQuery for BeginStatement {
|
||||
fn into_query(self) -> Result<Vec<Statement>> {
|
||||
Ok(vec![Statement::Begin(self)])
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoQuery for CancelStatement {
|
||||
fn into_query(self) -> Result<Vec<Statement>> {
|
||||
Ok(vec![Statement::Cancel(self)])
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoQuery for CommitStatement {
|
||||
fn into_query(self) -> Result<Vec<Statement>> {
|
||||
Ok(vec![Statement::Commit(self)])
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoQuery for OutputStatement {
|
||||
fn into_query(self) -> Result<Vec<Statement>> {
|
||||
Ok(vec![Statement::Output(self)])
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoQuery for IfelseStatement {
|
||||
fn into_query(self) -> Result<Vec<Statement>> {
|
||||
Ok(vec![Statement::Ifelse(self)])
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoQuery for SelectStatement {
|
||||
fn into_query(self) -> Result<Vec<Statement>> {
|
||||
Ok(vec![Statement::Select(self)])
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoQuery for CreateStatement {
|
||||
fn into_query(self) -> Result<Vec<Statement>> {
|
||||
Ok(vec![Statement::Create(self)])
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoQuery for UpdateStatement {
|
||||
fn into_query(self) -> Result<Vec<Statement>> {
|
||||
Ok(vec![Statement::Update(self)])
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoQuery for RelateStatement {
|
||||
fn into_query(self) -> Result<Vec<Statement>> {
|
||||
Ok(vec![Statement::Relate(self)])
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoQuery for DeleteStatement {
|
||||
fn into_query(self) -> Result<Vec<Statement>> {
|
||||
Ok(vec![Statement::Delete(self)])
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoQuery for InsertStatement {
|
||||
fn into_query(self) -> Result<Vec<Statement>> {
|
||||
Ok(vec![Statement::Insert(self)])
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoQuery for DefineStatement {
|
||||
fn into_query(self) -> Result<Vec<Statement>> {
|
||||
Ok(vec![Statement::Define(self)])
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoQuery for RemoveStatement {
|
||||
fn into_query(self) -> Result<Vec<Statement>> {
|
||||
Ok(vec![Statement::Remove(self)])
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoQuery for OptionStatement {
|
||||
fn into_query(self) -> Result<Vec<Statement>> {
|
||||
Ok(vec![Statement::Option(self)])
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoQuery for &str {
|
||||
fn into_query(self) -> Result<Vec<Statement>> {
|
||||
sql::parse(self)?.into_query()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoQuery for &String {
|
||||
fn into_query(self) -> Result<Vec<Statement>> {
|
||||
sql::parse(self)?.into_query()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoQuery for String {
|
||||
fn into_query(self) -> Result<Vec<Statement>> {
|
||||
sql::parse(&self)?.into_query()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a way to take a single query result from a list of responses
|
||||
pub trait QueryResult<Response>
|
||||
where
|
||||
Response: DeserializeOwned,
|
||||
{
|
||||
/// Extracts and deserializes a query result from a query response
|
||||
fn query_result(self, response: &mut QueryResponse) -> Result<Response>;
|
||||
}
|
||||
|
||||
impl<T> QueryResult<Option<T>> for usize
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
fn query_result(self, QueryResponse(map): &mut QueryResponse) -> Result<Option<T>> {
|
||||
let vec = match map.get_mut(&self) {
|
||||
Some(result) => match result {
|
||||
Ok(vec) => vec,
|
||||
Err(error) => {
|
||||
let error = mem::replace(error, Error::ConnectionUninitialised.into());
|
||||
map.remove(&self);
|
||||
return Err(error);
|
||||
}
|
||||
},
|
||||
None => {
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
let result = match &mut vec[..] {
|
||||
[] => Ok(None),
|
||||
[value] => {
|
||||
let value = mem::take(value);
|
||||
from_value(value)
|
||||
}
|
||||
_ => Err(Error::LossyTake(QueryResponse(mem::take(map))).into()),
|
||||
};
|
||||
map.remove(&self);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> QueryResult<Option<T>> for (usize, &str)
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
fn query_result(self, QueryResponse(map): &mut QueryResponse) -> Result<Option<T>> {
|
||||
let (index, key) = self;
|
||||
let vec = match map.get_mut(&index) {
|
||||
Some(result) => match result {
|
||||
Ok(vec) => vec,
|
||||
Err(error) => {
|
||||
let error = mem::replace(error, Error::ConnectionUninitialised.into());
|
||||
map.remove(&index);
|
||||
return Err(error);
|
||||
}
|
||||
},
|
||||
None => {
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
let mut value = match &mut vec[..] {
|
||||
[] => {
|
||||
map.remove(&index);
|
||||
return Ok(None);
|
||||
}
|
||||
[value] => value,
|
||||
_ => {
|
||||
return Err(Error::LossyTake(QueryResponse(mem::take(map))).into());
|
||||
}
|
||||
};
|
||||
match &mut value {
|
||||
Value::None | Value::Null => {
|
||||
map.remove(&index);
|
||||
Ok(None)
|
||||
}
|
||||
Value::Object(Object(object)) => {
|
||||
if object.is_empty() {
|
||||
map.remove(&index);
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(value) = object.remove(key) else {
|
||||
return Ok(None);
|
||||
};
|
||||
from_value(value)
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> QueryResult<Vec<T>> for usize
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
fn query_result(self, QueryResponse(map): &mut QueryResponse) -> Result<Vec<T>> {
|
||||
let vec = match map.remove(&self) {
|
||||
Some(result) => result?,
|
||||
None => {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
};
|
||||
from_value(vec.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> QueryResult<Vec<T>> for (usize, &str)
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
fn query_result(self, QueryResponse(map): &mut QueryResponse) -> Result<Vec<T>> {
|
||||
let (index, key) = self;
|
||||
let response = match map.get_mut(&index) {
|
||||
Some(result) => match result {
|
||||
Ok(vec) => vec,
|
||||
Err(error) => {
|
||||
let error = mem::replace(error, Error::ConnectionUninitialised.into());
|
||||
map.remove(&index);
|
||||
return Err(error);
|
||||
}
|
||||
},
|
||||
None => {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
};
|
||||
let mut vec = Vec::with_capacity(response.len());
|
||||
for value in response.iter_mut() {
|
||||
if let Value::Object(Object(object)) = value {
|
||||
if let Some(value) = object.remove(key) {
|
||||
vec.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
from_value(vec.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> QueryResult<Option<T>> for &str
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
fn query_result(self, response: &mut QueryResponse) -> Result<Option<T>> {
|
||||
(0, self).query_result(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> QueryResult<Vec<T>> for &str
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
fn query_result(self, response: &mut QueryResponse) -> Result<Vec<T>> {
|
||||
(0, self).query_result(response)
|
||||
}
|
||||
}
|
257
lib/src/api/opt/resource.rs
Normal file
257
lib/src/api/opt/resource.rs
Normal file
|
@ -0,0 +1,257 @@
|
|||
use crate::api::err::Error;
|
||||
use crate::api::Result;
|
||||
use crate::sql;
|
||||
use crate::sql::Array;
|
||||
use crate::sql::Edges;
|
||||
use crate::sql::Id;
|
||||
use crate::sql::Object;
|
||||
use crate::sql::Table;
|
||||
use crate::sql::Thing;
|
||||
use crate::sql::Value;
|
||||
use serde::Serialize;
|
||||
use std::ops;
|
||||
use std::ops::Bound;
|
||||
|
||||
/// A database resource
|
||||
#[derive(Serialize)]
|
||||
#[serde(untagged)]
|
||||
#[derive(Debug)]
|
||||
pub enum Resource {
|
||||
/// Table name
|
||||
Table(Table),
|
||||
/// Record ID
|
||||
RecordId(Thing),
|
||||
/// An object
|
||||
Object(Object),
|
||||
/// An array
|
||||
Array(Array),
|
||||
/// Edges
|
||||
Edges(Edges),
|
||||
}
|
||||
|
||||
impl Resource {
|
||||
pub(crate) fn with_range(self, range: Range<Id>) -> Result<Value> {
|
||||
match self {
|
||||
Resource::Table(Table(table)) => Ok(sql::Range {
|
||||
tb: table,
|
||||
beg: range.start,
|
||||
end: range.end,
|
||||
}
|
||||
.into()),
|
||||
Resource::RecordId(record_id) => Err(Error::RangeOnRecordId(record_id).into()),
|
||||
Resource::Object(object) => Err(Error::RangeOnObject(object).into()),
|
||||
Resource::Array(array) => Err(Error::RangeOnArray(array).into()),
|
||||
Resource::Edges(edges) => Err(Error::RangeOnEdges(edges).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Resource> for Value {
|
||||
fn from(resource: Resource) -> Self {
|
||||
match resource {
|
||||
Resource::Table(resource) => resource.into(),
|
||||
Resource::RecordId(resource) => resource.into(),
|
||||
Resource::Object(resource) => resource.into(),
|
||||
Resource::Array(resource) => resource.into(),
|
||||
Resource::Edges(resource) => resource.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for converting inputs into database resources
|
||||
pub trait IntoResource<Response>: Sized {
|
||||
/// Converts an input into a database resource
|
||||
fn into_resource(self) -> Result<Resource>;
|
||||
}
|
||||
|
||||
impl<R> IntoResource<Option<R>> for Object {
|
||||
fn into_resource(self) -> Result<Resource> {
|
||||
Ok(Resource::Object(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> IntoResource<Option<R>> for Thing {
|
||||
fn into_resource(self) -> Result<Resource> {
|
||||
Ok(Resource::RecordId(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<R, T, I> IntoResource<Option<R>> for (T, I)
|
||||
where
|
||||
T: Into<String>,
|
||||
I: Into<Id>,
|
||||
{
|
||||
fn into_resource(self) -> Result<Resource> {
|
||||
let (table, id) = self;
|
||||
let record_id = (table.into(), id.into());
|
||||
Ok(Resource::RecordId(record_id.into()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> IntoResource<Vec<R>> for Array {
|
||||
fn into_resource(self) -> Result<Resource> {
|
||||
Ok(Resource::Array(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> IntoResource<Vec<R>> for Edges {
|
||||
fn into_resource(self) -> Result<Resource> {
|
||||
Ok(Resource::Edges(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> IntoResource<Vec<R>> for Table {
|
||||
fn into_resource(self) -> Result<Resource> {
|
||||
Ok(Resource::Table(self))
|
||||
}
|
||||
}
|
||||
|
||||
fn blacklist_colon(input: &str) -> Result<()> {
|
||||
match input.contains(':') {
|
||||
true => {
|
||||
// We already know this string contains a colon
|
||||
let (table, id) = input.split_once(':').unwrap();
|
||||
Err(Error::TableColonId {
|
||||
table: table.to_owned(),
|
||||
id: id.to_owned(),
|
||||
}
|
||||
.into())
|
||||
}
|
||||
false => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> IntoResource<Vec<R>> for &str {
|
||||
fn into_resource(self) -> Result<Resource> {
|
||||
blacklist_colon(self)?;
|
||||
Ok(Resource::Table(Table(self.to_owned())))
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> IntoResource<Vec<R>> for &String {
|
||||
fn into_resource(self) -> Result<Resource> {
|
||||
blacklist_colon(self)?;
|
||||
Ok(Resource::Table(Table(self.to_owned())))
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> IntoResource<Vec<R>> for String {
|
||||
fn into_resource(self) -> Result<Resource> {
|
||||
blacklist_colon(&self)?;
|
||||
Ok(Resource::Table(Table(self)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds the `start` and `end` bounds of a range query
|
||||
#[derive(Debug)]
|
||||
pub struct Range<T> {
|
||||
pub(crate) start: Bound<T>,
|
||||
pub(crate) end: Bound<T>,
|
||||
}
|
||||
|
||||
impl<T> From<(Bound<T>, Bound<T>)> for Range<Id>
|
||||
where
|
||||
T: Into<Id>,
|
||||
{
|
||||
fn from((start, end): (Bound<T>, Bound<T>)) -> Self {
|
||||
Self {
|
||||
start: match start {
|
||||
Bound::Included(idx) => Bound::Included(idx.into()),
|
||||
Bound::Excluded(idx) => Bound::Excluded(idx.into()),
|
||||
Bound::Unbounded => Bound::Unbounded,
|
||||
},
|
||||
end: match end {
|
||||
Bound::Included(idx) => Bound::Included(idx.into()),
|
||||
Bound::Excluded(idx) => Bound::Excluded(idx.into()),
|
||||
Bound::Unbounded => Bound::Unbounded,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<ops::Range<T>> for Range<Id>
|
||||
where
|
||||
T: Into<Id>,
|
||||
{
|
||||
fn from(
|
||||
ops::Range {
|
||||
start,
|
||||
end,
|
||||
}: ops::Range<T>,
|
||||
) -> Self {
|
||||
Self {
|
||||
start: Bound::Included(start.into()),
|
||||
end: Bound::Excluded(end.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<ops::RangeInclusive<T>> for Range<Id>
|
||||
where
|
||||
T: Into<Id>,
|
||||
{
|
||||
fn from(range: ops::RangeInclusive<T>) -> Self {
|
||||
let (start, end) = range.into_inner();
|
||||
Self {
|
||||
start: Bound::Included(start.into()),
|
||||
end: Bound::Included(end.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<ops::RangeFrom<T>> for Range<Id>
|
||||
where
|
||||
T: Into<Id>,
|
||||
{
|
||||
fn from(
|
||||
ops::RangeFrom {
|
||||
start,
|
||||
}: ops::RangeFrom<T>,
|
||||
) -> Self {
|
||||
Self {
|
||||
start: Bound::Included(start.into()),
|
||||
end: Bound::Unbounded,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<ops::RangeTo<T>> for Range<Id>
|
||||
where
|
||||
T: Into<Id>,
|
||||
{
|
||||
fn from(
|
||||
ops::RangeTo {
|
||||
end,
|
||||
}: ops::RangeTo<T>,
|
||||
) -> Self {
|
||||
Self {
|
||||
start: Bound::Unbounded,
|
||||
end: Bound::Excluded(end.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<ops::RangeToInclusive<T>> for Range<Id>
|
||||
where
|
||||
T: Into<Id>,
|
||||
{
|
||||
fn from(
|
||||
ops::RangeToInclusive {
|
||||
end,
|
||||
}: ops::RangeToInclusive<T>,
|
||||
) -> Self {
|
||||
Self {
|
||||
start: Bound::Unbounded,
|
||||
end: Bound::Included(end.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ops::RangeFull> for Range<Id> {
|
||||
fn from(_: ops::RangeFull) -> Self {
|
||||
Self {
|
||||
start: Bound::Unbounded,
|
||||
end: Bound::Unbounded,
|
||||
}
|
||||
}
|
||||
}
|
10
lib/src/api/opt/strict.rs
Normal file
10
lib/src/api/opt/strict.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
/// Enables `strict` server mode
|
||||
#[cfg(any(
|
||||
feature = "kv-mem",
|
||||
feature = "kv-tikv",
|
||||
feature = "kv-rocksdb",
|
||||
feature = "kv-fdb",
|
||||
feature = "kv-indxdb",
|
||||
))]
|
||||
#[derive(Debug)]
|
||||
pub struct Strict;
|
14
lib/src/api/opt/tls.rs
Normal file
14
lib/src/api/opt/tls.rs
Normal file
|
@ -0,0 +1,14 @@
|
|||
/// TLS Configuration
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls"))))]
|
||||
#[derive(Debug)]
|
||||
pub enum Tls {
|
||||
/// Native TLS configuration
|
||||
#[cfg(feature = "native-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
|
||||
Native(native_tls::TlsConnector),
|
||||
/// Rustls configuration
|
||||
#[cfg(feature = "rustls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
|
||||
Rust(rustls::ClientConfig),
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
#[cfg(feature = "parallel")]
|
||||
#[allow(dead_code)]
|
||||
/// Specifies how many concurrent jobs can be buffered in the worker channel.
|
||||
pub const MAX_CONCURRENT_TASKS: usize = 64;
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ use channel::Sender;
|
|||
use std::ops::Bound;
|
||||
|
||||
impl Iterable {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) async fn channel(
|
||||
self,
|
||||
ctx: &Context<'_>,
|
||||
|
|
|
@ -17,7 +17,7 @@ use futures::lock::Mutex;
|
|||
use std::sync::Arc;
|
||||
use trice::Instant;
|
||||
|
||||
pub struct Executor<'a> {
|
||||
pub(crate) struct Executor<'a> {
|
||||
err: bool,
|
||||
kvs: &'a Datastore,
|
||||
txn: Option<Transaction>,
|
||||
|
|
|
@ -18,7 +18,7 @@ use std::cmp::Ordering;
|
|||
use std::collections::BTreeMap;
|
||||
use std::mem;
|
||||
|
||||
pub enum Iterable {
|
||||
pub(crate) enum Iterable {
|
||||
Value(Value),
|
||||
Table(Table),
|
||||
Thing(Thing),
|
||||
|
@ -28,20 +28,20 @@ pub enum Iterable {
|
|||
Relatable(Thing, Thing, Thing),
|
||||
}
|
||||
|
||||
pub enum Operable {
|
||||
pub(crate) enum Operable {
|
||||
Value(Value),
|
||||
Mergeable(Value, Value),
|
||||
Relatable(Thing, Value, Thing),
|
||||
}
|
||||
|
||||
pub enum Workable {
|
||||
pub(crate) enum Workable {
|
||||
Normal,
|
||||
Insert(Value),
|
||||
Relate(Thing, Thing),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Iterator {
|
||||
pub(crate) struct Iterator {
|
||||
// Iterator status
|
||||
run: Canceller,
|
||||
// Iterator limit value
|
||||
|
|
|
@ -10,14 +10,15 @@ mod transaction;
|
|||
mod variables;
|
||||
|
||||
pub use self::auth::*;
|
||||
pub use self::executor::*;
|
||||
pub use self::iterator::*;
|
||||
pub use self::options::*;
|
||||
pub use self::response::*;
|
||||
pub use self::session::*;
|
||||
pub use self::statement::*;
|
||||
pub use self::transaction::*;
|
||||
pub use self::variables::*;
|
||||
|
||||
pub(crate) use self::executor::*;
|
||||
pub(crate) use self::iterator::*;
|
||||
pub(crate) use self::statement::*;
|
||||
pub(crate) use self::transaction::*;
|
||||
pub(crate) use self::variables::*;
|
||||
|
||||
#[cfg(feature = "parallel")]
|
||||
mod channel;
|
||||
|
@ -28,4 +29,4 @@ pub use self::channel::*;
|
|||
#[cfg(test)]
|
||||
pub(crate) mod test;
|
||||
|
||||
pub const LOG: &str = "surrealdb::dbs";
|
||||
pub(crate) const LOG: &str = "surrealdb::dbs";
|
||||
|
|
|
@ -14,11 +14,10 @@ use crate::sql::statements::insert::InsertStatement;
|
|||
use crate::sql::statements::relate::RelateStatement;
|
||||
use crate::sql::statements::select::SelectStatement;
|
||||
use crate::sql::statements::update::UpdateStatement;
|
||||
use crate::sql::version::Version;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Statement<'a> {
|
||||
pub(crate) enum Statement<'a> {
|
||||
Select(&'a SelectStatement),
|
||||
Create(&'a CreateStatement),
|
||||
Update(&'a UpdateStatement),
|
||||
|
@ -164,14 +163,6 @@ impl<'a> Statement<'a> {
|
|||
_ => None,
|
||||
}
|
||||
}
|
||||
/// Returns any VERSION clause if specified
|
||||
#[inline]
|
||||
pub fn version(&self) -> Option<&Version> {
|
||||
match self {
|
||||
Statement::Select(v) => v.version.as_ref(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
/// Returns any RETURN clause if specified
|
||||
#[inline]
|
||||
pub fn output(&self) -> Option<&Output> {
|
||||
|
@ -186,6 +177,7 @@ impl<'a> Statement<'a> {
|
|||
}
|
||||
/// Returns any RETURN clause if specified
|
||||
#[inline]
|
||||
#[allow(dead_code)]
|
||||
pub fn parallel(&self) -> bool {
|
||||
match self {
|
||||
Statement::Select(v) => v.parallel,
|
||||
|
|
|
@ -2,4 +2,4 @@ use crate::kvs;
|
|||
use futures::lock::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub type Transaction = Arc<Mutex<kvs::Transaction>>;
|
||||
pub(crate) type Transaction = Arc<Mutex<kvs::Transaction>>;
|
||||
|
|
|
@ -4,7 +4,7 @@ use crate::err::Error;
|
|||
use crate::sql::value::Value;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub type Variables = Option<BTreeMap<String, Value>>;
|
||||
pub(crate) type Variables = Option<BTreeMap<String, Value>>;
|
||||
|
||||
pub(crate) trait Attach {
|
||||
fn attach(self, ctx: Context) -> Result<Context, Error>;
|
||||
|
|
|
@ -11,6 +11,7 @@ use crate::sql::value::Value;
|
|||
use channel::Sender;
|
||||
|
||||
impl<'a> Document<'a> {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) async fn compute(
|
||||
ctx: &Context<'_>,
|
||||
opt: &Options,
|
||||
|
|
|
@ -12,7 +12,7 @@ use crate::sql::value::Value;
|
|||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct Document<'a> {
|
||||
pub(crate) struct Document<'a> {
|
||||
pub(super) id: Option<Thing>,
|
||||
pub(super) extras: Workable,
|
||||
pub(super) current: Cow<'a, Value>,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
pub use self::document::*;
|
||||
pub(crate) use self::document::*;
|
||||
|
||||
#[cfg(feature = "parallel")]
|
||||
mod compute;
|
||||
|
|
|
@ -5,8 +5,9 @@ use storekey::decode::Error as DecodeError;
|
|||
use storekey::encode::Error as EncodeError;
|
||||
use thiserror::Error;
|
||||
|
||||
/// An error originating from the SurrealDB client library.
|
||||
/// An error originating from an embedded SurrealDB database.
|
||||
#[derive(Error, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
/// This error is used for ignoring a document when processing a query
|
||||
#[doc(hidden)]
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
#[quickjs(bare)]
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub mod package {
|
||||
pub const version: &str = crate::VERSION;
|
||||
pub const version: &str = crate::env::VERSION;
|
||||
}
|
||||
|
|
|
@ -41,8 +41,8 @@ impl Datastore {
|
|||
/// # Examples
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use surrealdb::Datastore;
|
||||
/// # use surrealdb::Error;
|
||||
/// # use surrealdb::kvs::Datastore;
|
||||
/// # use surrealdb::err::Error;
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<(), Error> {
|
||||
/// let ds = Datastore::new("memory").await?;
|
||||
|
@ -53,8 +53,8 @@ impl Datastore {
|
|||
/// Or to create a file-backed store:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use surrealdb::Datastore;
|
||||
/// # use surrealdb::Error;
|
||||
/// # use surrealdb::kvs::Datastore;
|
||||
/// # use surrealdb::err::Error;
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<(), Error> {
|
||||
/// let ds = Datastore::new("file://temp.db").await?;
|
||||
|
@ -65,8 +65,8 @@ impl Datastore {
|
|||
/// Or to connect to a tikv-backed distributed store:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use surrealdb::Datastore;
|
||||
/// # use surrealdb::Error;
|
||||
/// # use surrealdb::kvs::Datastore;
|
||||
/// # use surrealdb::err::Error;
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<(), Error> {
|
||||
/// let ds = Datastore::new("tikv://127.0.0.1:2379").await?;
|
||||
|
@ -155,8 +155,8 @@ impl Datastore {
|
|||
/// Create a new transaction on this datastore
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use surrealdb::Datastore;
|
||||
/// use surrealdb::Error;
|
||||
/// use surrealdb::kvs::Datastore;
|
||||
/// use surrealdb::err::Error;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() -> Result<(), Error> {
|
||||
|
@ -217,9 +217,9 @@ impl Datastore {
|
|||
/// Parse and execute an SQL query
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use surrealdb::Datastore;
|
||||
/// use surrealdb::Error;
|
||||
/// use surrealdb::Session;
|
||||
/// use surrealdb::kvs::Datastore;
|
||||
/// use surrealdb::err::Error;
|
||||
/// use surrealdb::dbs::Session;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() -> Result<(), Error> {
|
||||
|
@ -265,9 +265,9 @@ impl Datastore {
|
|||
/// Execute a pre-parsed SQL query
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use surrealdb::Datastore;
|
||||
/// use surrealdb::Error;
|
||||
/// use surrealdb::Session;
|
||||
/// use surrealdb::kvs::Datastore;
|
||||
/// use surrealdb::err::Error;
|
||||
/// use surrealdb::dbs::Session;
|
||||
/// use surrealdb::sql::parse;
|
||||
///
|
||||
/// #[tokio::main]
|
||||
|
@ -312,9 +312,9 @@ impl Datastore {
|
|||
/// Ensure a SQL [`Value`] is fully computed
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use surrealdb::Datastore;
|
||||
/// use surrealdb::Error;
|
||||
/// use surrealdb::Session;
|
||||
/// use surrealdb::kvs::Datastore;
|
||||
/// use surrealdb::err::Error;
|
||||
/// use surrealdb::dbs::Session;
|
||||
/// use surrealdb::sql::Future;
|
||||
/// use surrealdb::sql::Value;
|
||||
///
|
||||
|
|
|
@ -12,4 +12,4 @@ pub use self::ds::*;
|
|||
pub use self::kv::*;
|
||||
pub use self::tx::*;
|
||||
|
||||
pub const LOG: &str = "surrealdb::kvs";
|
||||
pub(crate) const LOG: &str = "surrealdb::kvs";
|
||||
|
|
|
@ -308,8 +308,8 @@ mod tests {
|
|||
let mut transaction = get_transaction().await;
|
||||
transaction.put("uh", "oh").await.unwrap();
|
||||
|
||||
async fn get_transaction() -> crate::Transaction {
|
||||
let datastore = crate::Datastore::new("rocksdb:/tmp/rocks.db").await.unwrap();
|
||||
async fn get_transaction() -> crate::kvs::Transaction {
|
||||
let datastore = crate::kvs::Datastore::new("rocksdb:/tmp/rocks.db").await.unwrap();
|
||||
datastore.transaction(true, false).await.unwrap()
|
||||
}
|
||||
}
|
||||
|
|
158
lib/src/lib.rs
158
lib/src/lib.rs
|
@ -1,14 +1,104 @@
|
|||
//! This library provides the low-level database library implementation, and query language
|
||||
//! definition, for [SurrealDB](https://surrealdb.com), the ultimate cloud database for
|
||||
//! This library provides a low-level database library implementation, a remote client
|
||||
//! and a query language definition, for [SurrealDB](https://surrealdb.com), the ultimate cloud database for
|
||||
//! tomorrow's applications. SurrealDB is a scalable, distributed, collaborative, document-graph
|
||||
//! database for the realtime web.
|
||||
//!
|
||||
//! This library can be used to start an embedded in-memory datastore, an embedded datastore
|
||||
//! persisted to disk, a browser-based embedded datastore backed by IndexedDB, or for connecting
|
||||
//! to a distributed [TiKV](https://tikv.org) key-value store.
|
||||
//!
|
||||
//! It also enables simple and advanced querying of a remote SurrealDB server from
|
||||
//! server-side or client-side code. All connections to SurrealDB are made over WebSockets by default,
|
||||
//! and automatically reconnect when the connection is terminated.
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use serde::{Serialize, Deserialize};
|
||||
//! use serde_json::json;
|
||||
//! use std::borrow::Cow;
|
||||
//! use surrealdb::{Result, Surreal};
|
||||
//! use surrealdb::sql;
|
||||
//! use surrealdb::opt::auth::Root;
|
||||
//! use surrealdb::engines::remote::ws::Ws;
|
||||
//!
|
||||
//! #[derive(Serialize, Deserialize)]
|
||||
//! struct Name {
|
||||
//! first: Cow<'static, str>,
|
||||
//! last: Cow<'static, str>,
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(Serialize, Deserialize)]
|
||||
//! struct Person {
|
||||
//! title: Cow<'static, str>,
|
||||
//! name: Name,
|
||||
//! marketing: bool,
|
||||
//! }
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() -> Result<()> {
|
||||
//! let db = Surreal::new::<Ws>("localhost:8000").await?;
|
||||
//!
|
||||
//! // Signin as a namespace, database, or root user
|
||||
//! db.signin(Root {
|
||||
//! username: "root",
|
||||
//! password: "root",
|
||||
//! }).await?;
|
||||
//!
|
||||
//! // Select a specific namespace / database
|
||||
//! db.use_ns("namespace").use_db("database").await?;
|
||||
//!
|
||||
//! // Create a new person with a random ID
|
||||
//! let created: Person = db.create("person")
|
||||
//! .content(Person {
|
||||
//! title: "Founder & CEO".into(),
|
||||
//! name: Name {
|
||||
//! first: "Tobie".into(),
|
||||
//! last: "Morgan Hitchcock".into(),
|
||||
//! },
|
||||
//! marketing: true,
|
||||
//! })
|
||||
//! .await?;
|
||||
//!
|
||||
//! // Create a new person with a specific ID
|
||||
//! let created: Person = db.create(("person", "jaime"))
|
||||
//! .content(Person {
|
||||
//! title: "Founder & COO".into(),
|
||||
//! name: Name {
|
||||
//! first: "Jaime".into(),
|
||||
//! last: "Morgan Hitchcock".into(),
|
||||
//! },
|
||||
//! marketing: false,
|
||||
//! })
|
||||
//! .await?;
|
||||
//!
|
||||
//! // Update a person record with a specific ID
|
||||
//! let updated: Person = db.update(("person", "jaime"))
|
||||
//! .merge(json!({"marketing": true}))
|
||||
//! .await?;
|
||||
//!
|
||||
//! // Select all people records
|
||||
//! let people: Vec<Person> = db.select("person").await?;
|
||||
//!
|
||||
//! // Perform a custom advanced query
|
||||
//! let sql = sql! {
|
||||
//! SELECT marketing, count()
|
||||
//! FROM type::table($table)
|
||||
//! GROUP BY marketing
|
||||
//! };
|
||||
//!
|
||||
//! let groups = db.query(sql)
|
||||
//! .bind(("table", "person"))
|
||||
//! .await?;
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
#![doc(html_favicon_url = "https://surrealdb.s3.amazonaws.com/favicon.png")]
|
||||
#![doc(html_logo_url = "https://surrealdb.s3.amazonaws.com/icon.png")]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(test, deny(warnings))]
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
@ -16,39 +106,63 @@ extern crate log;
|
|||
#[macro_use]
|
||||
mod mac;
|
||||
|
||||
mod api;
|
||||
mod cnf;
|
||||
mod ctx;
|
||||
mod dbs;
|
||||
mod doc;
|
||||
mod err;
|
||||
mod exe;
|
||||
mod fnc;
|
||||
mod key;
|
||||
mod kvs;
|
||||
|
||||
// ENV
|
||||
pub mod env;
|
||||
|
||||
// SQL
|
||||
pub mod sql;
|
||||
|
||||
// Exports
|
||||
pub use dbs::Auth;
|
||||
pub use dbs::Response;
|
||||
pub use dbs::Session;
|
||||
pub use err::Error;
|
||||
pub use kvs::Datastore;
|
||||
pub use kvs::Key;
|
||||
pub use kvs::Transaction;
|
||||
pub use kvs::Val;
|
||||
#[doc(hidden)]
|
||||
pub mod dbs;
|
||||
#[doc(hidden)]
|
||||
pub mod env;
|
||||
#[doc(hidden)]
|
||||
pub mod err;
|
||||
#[doc(hidden)]
|
||||
pub mod kvs;
|
||||
|
||||
// Re-exports
|
||||
#[doc(inline)]
|
||||
pub use api::engines;
|
||||
#[doc(inline)]
|
||||
pub use api::method;
|
||||
#[doc(inline)]
|
||||
pub use api::opt;
|
||||
#[doc(inline)]
|
||||
pub use api::Connect;
|
||||
#[doc(inline)]
|
||||
pub use api::Connection;
|
||||
#[doc(inline)]
|
||||
pub use api::Response;
|
||||
#[doc(inline)]
|
||||
pub use api::Result;
|
||||
#[doc(inline)]
|
||||
pub use api::Surreal;
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Channels for receiving a SurrealQL database export
|
||||
pub mod channel {
|
||||
pub use channel::bounded as new;
|
||||
pub use channel::Receiver;
|
||||
pub use channel::Sender;
|
||||
}
|
||||
|
||||
// Version
|
||||
#[doc(inline)]
|
||||
pub use env::VERSION;
|
||||
/// Different error types for embedded and remote databases
|
||||
pub mod error {
|
||||
pub use crate::api::err::Error as Api;
|
||||
pub use crate::err::Error as Db;
|
||||
}
|
||||
|
||||
/// An error originating from the SurrealDB client library
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
/// An error with an embedded storage engine
|
||||
#[error("Database error: {0}")]
|
||||
Db(#[from] crate::error::Db),
|
||||
/// An error with a remote database instance
|
||||
#[error("API error: {0}")]
|
||||
Api(#[from] crate::error::Api),
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
/// Converts some text into a new line byte string
|
||||
macro_rules! bytes {
|
||||
($expression:expr) => {
|
||||
format!("{}\n", $expression).into_bytes()
|
||||
};
|
||||
}
|
||||
|
||||
/// Creates a new b-tree map of key-value pairs
|
||||
macro_rules! map {
|
||||
($($k:expr => $v:expr),* $(,)?) => {{
|
||||
::std::collections::BTreeMap::from([
|
||||
|
@ -12,8 +14,33 @@ macro_rules! map {
|
|||
}};
|
||||
}
|
||||
|
||||
/// Matches on a specific config environment
|
||||
macro_rules! get_cfg {
|
||||
($i:ident : $($s:expr),+) => (
|
||||
let $i = || { $( if cfg!($i=$s) { return $s; } );+ "unknown"};
|
||||
)
|
||||
}
|
||||
|
||||
/// Parses a set of SurrealQL statements
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use surrealdb::sql;
|
||||
/// # fn main() -> surrealdb::Result<()> {
|
||||
/// let query = sql! {
|
||||
/// LET $name = "Tobie";
|
||||
/// SELECT * FROM user WHERE name = $name;
|
||||
/// };
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! sql {
|
||||
($($query:tt)*) => {
|
||||
match $crate::sql::parse(stringify!($($query)*)) {
|
||||
Ok(v) => v,
|
||||
Err(e) => { return Err(e.into()); },
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -48,6 +48,12 @@ impl From<Vec<&str>> for Array {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Vec<String>> for Array {
|
||||
fn from(v: Vec<String>) -> Self {
|
||||
Self(v.into_iter().map(Value::from).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Number>> for Array {
|
||||
fn from(v: Vec<Number>) -> Self {
|
||||
Self(v.into_iter().map(Value::from).collect())
|
||||
|
|
|
@ -81,6 +81,24 @@ impl From<&str> for Id {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<&String> for Id {
|
||||
fn from(v: &String) -> Self {
|
||||
Self::String(v.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<&str>> for Id {
|
||||
fn from(v: Vec<&str>) -> Self {
|
||||
Id::Array(v.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<String>> for Id {
|
||||
fn from(v: Vec<String>) -> Self {
|
||||
Id::Array(v.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Value>> for Id {
|
||||
fn from(v: Vec<Value>) -> Self {
|
||||
Id::Array(v.into())
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
//! The full type definitions for the SurrealQL query language
|
||||
|
||||
pub(crate) mod algorithm;
|
||||
pub(crate) mod array;
|
||||
pub(crate) mod base;
|
||||
|
|
169
lib/tests/api.rs
Normal file
169
lib/tests/api.rs
Normal file
|
@ -0,0 +1,169 @@
|
|||
#![allow(unused_imports, dead_code)]
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use std::borrow::Cow;
|
||||
use std::ops::Bound;
|
||||
use surrealdb::opt::auth::Database;
|
||||
use surrealdb::opt::auth::Jwt;
|
||||
use surrealdb::opt::auth::Namespace;
|
||||
use surrealdb::opt::auth::Root;
|
||||
use surrealdb::opt::auth::Scope;
|
||||
use surrealdb::opt::PatchOp;
|
||||
use surrealdb::sql::statements::BeginStatement;
|
||||
use surrealdb::sql::statements::CommitStatement;
|
||||
use surrealdb::Surreal;
|
||||
use ulid::Ulid;
|
||||
|
||||
const NS: &str = "test-ns";
|
||||
const ROOT_USER: &str = "root";
|
||||
const ROOT_PASS: &str = "root";
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct Record<'a> {
|
||||
name: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RecordId {
|
||||
id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RecordName {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd)]
|
||||
struct RecordBuf {
|
||||
id: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct AuthParams<'a> {
|
||||
email: &'a str,
|
||||
pass: &'a str,
|
||||
}
|
||||
|
||||
#[cfg(feature = "protocol-ws")]
|
||||
mod ws {
|
||||
use super::*;
|
||||
use surrealdb::engines::remote::ws::Client;
|
||||
use surrealdb::engines::remote::ws::Ws;
|
||||
|
||||
async fn new_db() -> Surreal<Client> {
|
||||
let db = Surreal::new::<Ws>("127.0.0.1:8000").await.unwrap();
|
||||
db.signin(Root {
|
||||
username: ROOT_USER,
|
||||
password: ROOT_PASS,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
db
|
||||
}
|
||||
|
||||
include!("api/mod.rs");
|
||||
include!("api/auth.rs");
|
||||
}
|
||||
|
||||
#[cfg(feature = "protocol-http")]
|
||||
mod http {
|
||||
use super::*;
|
||||
use surrealdb::engines::remote::http::Client;
|
||||
use surrealdb::engines::remote::http::Http;
|
||||
|
||||
async fn new_db() -> Surreal<Client> {
|
||||
let db = Surreal::new::<Http>("127.0.0.1:8000").await.unwrap();
|
||||
db.signin(Root {
|
||||
username: ROOT_USER,
|
||||
password: ROOT_PASS,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
db
|
||||
}
|
||||
|
||||
include!("api/mod.rs");
|
||||
include!("api/auth.rs");
|
||||
include!("api/backup.rs");
|
||||
}
|
||||
|
||||
#[cfg(feature = "kv-mem")]
|
||||
mod mem {
|
||||
use super::*;
|
||||
use surrealdb::engines::local::Db;
|
||||
use surrealdb::engines::local::Mem;
|
||||
|
||||
async fn new_db() -> Surreal<Db> {
|
||||
Surreal::new::<Mem>(()).await.unwrap()
|
||||
}
|
||||
|
||||
include!("api/mod.rs");
|
||||
include!("api/backup.rs");
|
||||
}
|
||||
|
||||
#[cfg(feature = "kv-rocksdb")]
|
||||
mod file {
|
||||
use super::*;
|
||||
use surrealdb::engines::local::Db;
|
||||
use surrealdb::engines::local::File;
|
||||
|
||||
async fn new_db() -> Surreal<Db> {
|
||||
let path = format!("/tmp/{}.db", Ulid::new());
|
||||
Surreal::new::<File>(path.as_str()).await.unwrap()
|
||||
}
|
||||
|
||||
include!("api/mod.rs");
|
||||
include!("api/backup.rs");
|
||||
}
|
||||
|
||||
#[cfg(feature = "kv-tikv")]
|
||||
mod tikv {
|
||||
use super::*;
|
||||
use surrealdb::engines::local::Db;
|
||||
use surrealdb::engines::local::TiKv;
|
||||
|
||||
async fn new_db() -> Surreal<Db> {
|
||||
Surreal::new::<TiKv>("127.0.0.1:2379").await.unwrap()
|
||||
}
|
||||
|
||||
include!("api/mod.rs");
|
||||
include!("api/backup.rs");
|
||||
}
|
||||
|
||||
#[cfg(feature = "kv-fdb")]
|
||||
mod fdb {
|
||||
use super::*;
|
||||
use surrealdb::engines::local::Db;
|
||||
use surrealdb::engines::local::FDb;
|
||||
|
||||
async fn new_db() -> Surreal<Db> {
|
||||
Surreal::new::<FDb>("/tmp/fdb.cluster").await.unwrap()
|
||||
}
|
||||
|
||||
include!("api/mod.rs");
|
||||
include!("api/backup.rs");
|
||||
}
|
||||
|
||||
#[cfg(feature = "protocol-http")]
|
||||
mod any {
|
||||
use super::*;
|
||||
use surrealdb::engines::any::Any;
|
||||
|
||||
async fn new_db() -> Surreal<Any> {
|
||||
let db = surrealdb::engines::any::connect("http://127.0.0.1:8000").await.unwrap();
|
||||
db.signin(Root {
|
||||
username: ROOT_USER,
|
||||
password: ROOT_PASS,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
db
|
||||
}
|
||||
|
||||
include!("api/mod.rs");
|
||||
include!("api/auth.rs");
|
||||
include!("api/backup.rs");
|
||||
}
|
138
lib/tests/api/auth.rs
Normal file
138
lib/tests/api/auth.rs
Normal file
|
@ -0,0 +1,138 @@
|
|||
// Auth tests
|
||||
// Supported by both HTTP and WS protocols
|
||||
|
||||
#[tokio::test]
|
||||
async fn invalidate() {
|
||||
let db = new_db().await;
|
||||
db.use_ns(NS).use_db(Ulid::new().to_string()).await.unwrap();
|
||||
db.invalidate().await.unwrap();
|
||||
let error = db.create::<Option<RecordId>>(("user", "john")).await.unwrap_err();
|
||||
assert!(error.to_string().contains("You don't have permission to perform this query type"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn signup_scope() {
|
||||
let db = new_db().await;
|
||||
let database = Ulid::new().to_string();
|
||||
db.use_ns(NS).use_db(&database).await.unwrap();
|
||||
let scope = Ulid::new().to_string();
|
||||
let sql = format!(
|
||||
"
|
||||
DEFINE SCOPE {scope} SESSION 1s
|
||||
SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
|
||||
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
|
||||
"
|
||||
);
|
||||
let response = db.query(sql).await.unwrap();
|
||||
response.check().unwrap();
|
||||
db.signup(Scope {
|
||||
namespace: NS,
|
||||
database: &database,
|
||||
scope: &scope,
|
||||
params: AuthParams {
|
||||
email: "john.doe@example.com",
|
||||
pass: "password123",
|
||||
},
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn signin_ns() {
|
||||
let db = new_db().await;
|
||||
db.use_ns(NS).use_db(Ulid::new().to_string()).await.unwrap();
|
||||
let user = Ulid::new().to_string();
|
||||
let pass = "password123";
|
||||
let sql = format!("DEFINE LOGIN {user} ON NAMESPACE PASSWORD '{pass}'");
|
||||
let response = db.query(sql).await.unwrap();
|
||||
response.check().unwrap();
|
||||
db.signin(Namespace {
|
||||
namespace: NS,
|
||||
username: &user,
|
||||
password: pass,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn signin_db() {
|
||||
let db = new_db().await;
|
||||
let database = Ulid::new().to_string();
|
||||
db.use_ns(NS).use_db(&database).await.unwrap();
|
||||
let user = Ulid::new().to_string();
|
||||
let pass = "password123";
|
||||
let sql = format!("DEFINE LOGIN {user} ON DATABASE PASSWORD '{pass}'");
|
||||
let response = db.query(sql).await.unwrap();
|
||||
response.check().unwrap();
|
||||
db.signin(Database {
|
||||
namespace: NS,
|
||||
database: &database,
|
||||
username: &user,
|
||||
password: pass,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn signin_scope() {
|
||||
let db = new_db().await;
|
||||
let database = Ulid::new().to_string();
|
||||
db.use_ns(NS).use_db(&database).await.unwrap();
|
||||
let scope = Ulid::new().to_string();
|
||||
let email = format!("{scope}@example.com");
|
||||
let pass = "password123";
|
||||
let sql = format!(
|
||||
"
|
||||
DEFINE SCOPE {scope} SESSION 1s
|
||||
SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
|
||||
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
|
||||
"
|
||||
);
|
||||
let response = db.query(sql).await.unwrap();
|
||||
response.check().unwrap();
|
||||
db.signup(Scope {
|
||||
namespace: NS,
|
||||
database: &database,
|
||||
scope: &scope,
|
||||
params: AuthParams {
|
||||
pass,
|
||||
email: &email,
|
||||
},
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
db.signin(Scope {
|
||||
namespace: NS,
|
||||
database: &database,
|
||||
scope: &scope,
|
||||
params: AuthParams {
|
||||
pass,
|
||||
email: &email,
|
||||
},
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn authenticate() {
|
||||
let db = new_db().await;
|
||||
db.use_ns(NS).use_db(Ulid::new().to_string()).await.unwrap();
|
||||
let user = Ulid::new().to_string();
|
||||
let pass = "password123";
|
||||
let sql = format!("DEFINE LOGIN {user} ON NAMESPACE PASSWORD '{pass}'");
|
||||
let response = db.query(sql).await.unwrap();
|
||||
response.check().unwrap();
|
||||
let token = db
|
||||
.signin(Namespace {
|
||||
namespace: NS,
|
||||
username: &user,
|
||||
password: pass,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
db.authenticate(token).await.unwrap();
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue