https://github.com/thenativeweb/eventsourcingdb-client-rust
The official Rust client SDK for EventSourcingDB.
https://github.com/thenativeweb/eventsourcingdb-client-rust
Last synced: 8 months ago
JSON representation
The official Rust client SDK for EventSourcingDB.
- Host: GitHub
- URL: https://github.com/thenativeweb/eventsourcingdb-client-rust
- Owner: thenativeweb
- License: mit
- Created: 2025-05-05T09:04:54.000Z (about 1 year ago)
- Default Branch: main
- Last Pushed: 2025-08-25T07:33:41.000Z (9 months ago)
- Last Synced: 2025-08-25T09:26:52.195Z (9 months ago)
- Language: Rust
- Homepage: https://www.eventsourcingdb.io
- Size: 363 KB
- Stars: 4
- Watchers: 3
- Forks: 1
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- License: LICENSE.md
- Codeowners: .github/CODEOWNERS
Awesome Lists containing this project
README
# eventsourcingdb
The official Rust client SDK for [EventSourcingDB](https://www.eventsourcingdb.io) – a purpose-built database for event sourcing.
EventSourcingDB enables you to build and operate event-driven applications with native support for writing, reading, and observing events. This client SDK provides convenient access to its capabilities in Rust.
For more information on EventSourcingDB, see its [official documentation](https://docs.eventsourcingdb.io/).
This client SDK includes support for [Testcontainers](https://testcontainers.com/) to spin up EventSourcingDB instances in integration tests. For details, see [Using Testcontainers](#using-testcontainers).
## Getting Started
Install the client SDK:
```shell
cargo add eventsourcingdb
```
Import the package and create an instance by providing the URL of your EventSourcingDB instance and the API token to use:
```rust
use eventsourcingdb::client::Client;
// ...
let base_url: Url = "localhost:3000".parse().unwrap();
let api_token = "secret";
let client = Client::new(base_url, api_token);
```
Then call the `ping` function to check whether the instance is reachable. If it is not, the function will return an error:
```rust
let result = client.ping().await;
if let Err(err) = result {
// handle error...
}
```
*Note that `ping` does not require authentication, so the call may succeed even if the API token is invalid.*
If you want to verify the API token, call `verify_api_token`. If the token is invalid, the function will return an error:
```rust
let result = client.verify_api_token().await;
if let Err(err) = result {
// handle error...
}
```
### Writing Events
Call the `write_events` function and hand over a vector with one or more events. You do not have to provide all event fields – some are automatically added by the server.
Specify `source`, `subject`, `type` (using `ty`), and `data` according to the [CloudEvents](https://docs.eventsourcingdb.io/fundamentals/cloud-events/) format.
For `data` provide a JSON object using a `serde_json:Value`.
The function returns the written events, including the fields added by the server:
```rust
let event = EventCandidate::builder()
.source("https://library.eventsourcingdb.io".to_string())
.subject("/books/42".to_string())
.ty("io.eventsourcingdb.library.book-acquired")
.data(json!({
"title": "2001 - A Space Odyssey",
"author": "Arthur C. Clarke",
"isbn": "978-0756906788",
}))
.build();
let result = client.write_events(vec![event.clone()], vec![]).await;
match result {
Ok(written_events) => // ...
Err(err) => // ...
}
```
#### Using the `IsSubjectPristine` precondition
If you only want to write events in case a subject (such as `/books/42`) does not yet have any events, use the `IsSubjectPristine` precondition to create a precondition and pass it in a vector as the second argument:
```rust
let result = client.write_events(
vec![event.clone()],
vec![Precondition::IsSubjectPristine {
subject: "/books/42".to_string(),
}],
).await;
match result {
Ok(written_events) => // ...
Err(err) => // ...
}
```
#### Using the `IsSubjectOnEventId` precondition
If you only want to write events in case the last event of a subject (such as `/books/42`) has a specific ID (e.g., `0`), use the `IsSubjectOnEventId` precondition to create a precondition and pass it in a vector as the second argument:
```rust
let result = client.write_events(
vec![event.clone()],
vec![Precondition::IsSubjectOnEventId {
subject: "/books/42".to_string(),
event_id: "0".to_string(),
}],
).await;
match result {
Ok(written_events) => // ...
Err(err) => // ...
}
```
*Note that according to the CloudEvents standard, event IDs must be of type string.*
#### Using the `IsEventQLQueryTrue` precondition
If you want to write events depending on an EventQL query, use the `IsEventQLQueryTrue` precondition to create a precondition and pass it in a vector as the second argument:
```rust
let result = client.write_events(
vec![event.clone()],
vec![Precondition::IsEventQLQueryTrue {
query: "FROM e IN events WHERE e.type == 'io.eventsourcingdb.library.book-borrowed' PROJECT INTO COUNT() < 10".to_string(),
}],
).await;
match result {
Ok(written_events) => // ...
Err(err) => // ...
}
```
### Reading Events
To read all events of a subject, call the `read_events` function with the subject and an options object. Set the `recursive` option to `false`. This ensures that only events of the given subject are returned, not events of nested subjects.
The function returns a stream from which you can retrieve one event at a time:
```rust
let result = client
.read_events("/books/42", Some(
ReadEventsOptions {
recursive: false,
...Default::default(),
}
))
.await;
match result {
Err(err) => // ...
Ok(mut stream) => {
while let Some(event) = stream.next().await {
// ...
}
}
}
```
#### Reading From Subjects Recursively
If you want to read not only all the events of a subject, but also the events of all nested subjects, set the `recursive` option to `true`:
```rust
let result = client
.read_events("/books/42", Some(
ReadEventsOptions {
recursive: true,
..Default::default(),
}
))
.await;
```
This also allows you to read *all* events ever written. To do so, provide `/` as the subject and set `recursive` to `true`, since all subjects are nested under the root subject.
#### Reading in Anti-Chronological Order
By default, events are read in chronological order. To read in anti-chronological order, provide the `order` option and set it using the `Antichronological` ordering:
```rust
let result = client
.read_events("/books/42", Some(
ReadEventsOptions {
recursive: false,
order: Some(Ordering::Antichronological)
..Default::default(),
}
))
.await;
```
*Note that you can also use the `Chronological` ordering to explicitly enforce the default order.*
#### Specifying Bounds
Sometimes you do not want to read all events, but only a range of events. For that, you can specify the `lower_bound` and `upper_bound` options – either one of them or even both at the same time.
Specify the ID and whether to include or exclude it, for both the lower and upper bound:
```rust
let result = client
.read_events("/books/42", Some(
ReadEventsOptions {
recursive: false,
lower_bound: Some(Bound {
bound_type: BoundType::Inclusive,
id: "100",
}),
upper_bound: Some(Bound {
bound_type: BoundType::Exclusive,
id: "200",
}),
..Default::default(),
}
))
.await;
```
#### Starting From the Latest Event of a Given Type
To read starting from the latest event of a given type, provide the `from_latest_event` option and specify the subject, the type, and how to proceed if no such event exists.
Possible options are `ReadNothing`, which skips reading entirely, or `ReadEverything`, which effectively behaves as if `from_latest_event` was not specified:
```rust
let result = client
.read_events("/books/42", Some(
ReadEventsOptions {
recursive: false,
from_latest_event: Some(
FromLatestEventOptions {
subject: "/books/42",
ty: "io.eventsourcingdb.library.book-borrowed",
if_event_is_missing: ReadEventMissingStrategy::ReadEverything,
}
)
..Default::default(),
}
))
.await;
```
*Note that `from_latest_event` and `lower_bound` can not be provided at the same time.*
### Running EventQL Queries
To run an EventQL query, call the `run_eventql_query` function and provide the query as argument. The function returns a stream.
```rust
let result = client
.run_eventql_query("FROM e IN events PROJECT INTO e")
.await;
match result {
Err(err) => // ...
Ok(mut stream) => {
while let Some(row) = stream.next().await {
// ...
}
}
}
```
*Note that each row returned by the stream is of type `serde_json::Value` and matches the projection specified in your query.*
### Observing Events
To observe all events of a subject, call the `observe_events` function with the subject and an options object. Set the `recursive` option to `false`. This ensures that only events of the given subject are returned, not events of nested subjects.
The function returns a stream from which you can retrieve one event at a time:
```rust
let result = client
.observe_events("/books/42", Some(
ObserveEventsOptions {
recursive: false,
from_latest_event: None,
lower_bound: None,
}
))
.await;
match result {
Err(err) => // ...
Ok(mut stream) => {
while let Some(event) = stream.next().await {
// ...
}
}
}
```
#### Observing From Subjects Recursively
If you want to observe not only all the events of a subject, but also the events of all nested subjects, set the `recursive` option to `true`:
```rust
let result = client
.observe_events("/books/42", Some(
ObserveEventsOptions {
recursive: true,
..Default::default(),
}
))
.await
```
This also allows you to observe *all* events ever written. To do so, provide `/` as the subject and set `recursive` to `true`, since all subjects are nested under the root subject.
#### Specifying Bounds
Sometimes you do not want to observe all events, but only a range of events. For that, you can specify the `lower_bound` option.
Specify the ID and whether to include or exclude it:
```rust
let result = client
.observe_events("/books/42", Some(
ObserveEventsOptions {
recursive: false,
lower_bound: Some(Bound {
bound_type: BoundType::Inclusive,
id: "100",
}),
..Default::default(),
}
))
.await
```
#### Starting From the Latest Event of a Given Type
To observe starting from the latest event of a given type, provide the `from_latest_event` option and specify the subject, the type, and how to proceed if no such event exists.
Possible options are `WaitForEvent`, which waits for an event of the given type to happen, or `ObserveEverything`, which effectively behaves as if `from_latest_event` was not specified:
```rust
let result = client
.observe_events("/books/42", Some(
ObserveEventsOptions {
recursive: false,
from_latest_event: Some(
ObserveFromLatestEventOptions {
subject: "/books/42",
ty: "io.eventsourcingdb.library.book-borrowed",
if_event_is_missing: ObserveEventMissingStrategy::ObserveEverything,
}
)
..Default::default(),
}
))
.await
```
*Note that `from_latest_event` and `lower_bound` can not be provided at the same time.*
#### Aborting Observing
The observe will automatically be canceled if the stream is dropped from scope.
### Registering an Event Schema
To register an event schema, call the `register_event_schema` function and hand over an event type and the desired schema:
```rust
client.register_event_schema(
"io.eventsourcingdb.library.book-acquired",
&json!({
"type": "object",
"properties": {
"title": { "type": "string" },
"author": { "type": "string" },
"isbn": { "type": "string" },
},
"required": [
"title",
"author",
"isbn",
],
"additionalProperties": false,
}).await;,
)
```
### Listing Subjects
To list all subjects, call the `list_subjects` function with `/` as the base subject. The function returns a stream from which you can retrieve one subject at a time:
```rust
let result = client.list_subjects(Some("/")).await;
match result {
Ok(subjects) => // ...
Err(err) => // ...
}
```
If you only want to list subjects within a specific branch, provide the desired base subject instead:
```rust
let result := client.list_subjects("/books");
```
### Listing Event Types
To list all event types, call the `list_event_types` function. The function returns a stream from which you can retrieve one event type at a time:
```rust
let result := client.list_event_types().await;
match result {
Ok(event_types) => // ...
Err(err) => // ...
}
```
### Listing a Specific Event Types
To list a specific event type, call the `read_event_type` function. The function returns the detailed event type, which includes the schema:
```rust
let event_type_name = "io.eventsourcingdb.library.book-acquired";
let result := client.read_event_type(event_type_name).await;
match result {
Ok(event_type) => // ...
Err(err) => // ...
}
```
### Using Testcontainers
Call the `Container::start_default()` function, get a client, and run your test code:
```rust
let container = Container::start_default().await.unwrap();
let client = container.get_client().await.unwrap();
```
#### Configuring the Container Instance
By default, `Container` uses the `latest` tag of the official EventSourcingDB Docker image. To change that use the provided builder and call the `with_image_tag` function.
```rust
let container = Container::builder()
.with_image_tag("1.0.0")
.build()
.await.unwrap()
```
Similarly, you can configure the port to use and the API token. Call the `with_port` or the `with_api_token` function respectively:
```rust
let container = Container::builder()
.with_port(4000)
.with_api_token("secret")
.build()
.await.unwrap()
```
#### Configuring the Client Manually
In case you need to set up the client yourself, use the following functions to get details on the container:
- `get_host()` returns the host name
- `get_mapped_port()` returns the port
- `get_base_url()` returns the full URL of the container
- `get_api_token()` returns the API token