Skip to content

Commit 1ecb8c8

Browse files
author
system
committed
Cursor Example
1 parent d9da044 commit 1ecb8c8

13 files changed

Lines changed: 262 additions & 12 deletions

File tree

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ members = [
1818
"examples/request-tracking",
1919
"examples/engine",
2020
"rwf-admin",
21-
"examples/files", "examples/users", "examples/openapi", "examples/oidc", "examples/callbacks",
21+
"examples/files", "examples/users", "examples/openapi", "examples/oidc", "examples/callbacks", "examples/cursor",
2222
]
2323
exclude = ["examples/rails", "rwf-ruby", "examples/django", "rwf-fuzz"]

examples/cursor/Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[package]
2+
name = "cursor"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[dependencies]
7+
rwf = {path = "../../rwf"}
8+
tokio = {version = "1.49.0", features = ["full"]}
9+
serde = { version = "1.0.228", features = ["derive"] }
10+
serde_json = "1.0.149"

examples/cursor/README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Cursor
2+
Rwf has support for Cursors - with all advantages and disadvantages they come with.
3+
Every qualified (not data modifying) query can be used to create a Cursor.
4+
5+
6+
## Basics
7+
There are two base types of cursors. `ModelCursor` and `SelectiveCursor` the former is used fetch rows which can be mapped to a struct implements the `Model` trait.
8+
The later ist used to fetch arbitrary rows.
9+
10+
## Example
11+
12+
Just create a `Query` and use the `Query::declare` method to create Declare Statement.
13+
This can be used to configure the Cursor and provides Methods to construct a cursor then.
14+
15+
```rust
16+
use rwf::prelude;
17+
use rwf::model::prelude::*;
18+
#[derive(Clone, macros::Model, Serialize, Deserialize)]
19+
struct User {
20+
id: Option<i64>,
21+
name: String,
22+
mail: String
23+
}
24+
#[tokio::main]
25+
async fn main() -> Result<(), rwf::model::Error> {
26+
let mut cursor = User::all()
27+
.filter_not_ends_with("mail", ".tld")
28+
.order_by(("id", "asc")) // Always Order the underlying query to make results reproducible
29+
.declare("cursor_name") // Give the cursor a name to refer in FETCH Queries
30+
.asensitive() // Make Table changes like inserts available to the cursor
31+
.scroll() // Allow all kind of fetches. If not sepcified only forward directed fetches are allowed
32+
.hold() // Allow creation outside a transaction.
33+
.create_tx_model_cursor(None) // Creat3 a cursor which fetches the Queries Model. Create a new transaction for the cursor.
34+
.await?;
35+
let first_user = cursor.fetch_one(cursor.fetch_stmt().next()).await?;
36+
Ok(())
37+
}
38+
```
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DROP TABLE app_logs;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CREATE TABLE app_logs (
2+
id bigserial primary key,
3+
ts timestamptz not null default NOW(),
4+
data text not null
5+
);
6+
7+
INSERT INTO app_logs(data) VALUES ('TTest Entr'), ('Is it working'), ('Last on page one'), ('This is going to be on page two');

examples/cursor/rwf.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[database]
2+
name = "rwf_cursor"

examples/cursor/src/main.rs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
use rwf::controller::TurboStream;
2+
use rwf::model::migrate;
3+
use rwf::model::pool::Transaction;
4+
use rwf::model::prelude::*;
5+
use rwf::prelude::*;
6+
use std::collections::BTreeMap;
7+
use std::str::FromStr;
8+
use tokio::sync::Mutex;
9+
10+
#[derive(Debug, Clone, Serialize, Deserialize, macros::Model)]
11+
pub struct AppLog {
12+
id: Option<i64>,
13+
ts: OffsetDateTime,
14+
data: String,
15+
}
16+
impl AppLog {
17+
pub fn q() -> Scope<Self> {
18+
Self::all().order(("id", "asc"))
19+
}
20+
pub async fn cursor(name: impl ToString, tx: &mut Transaction) -> ModelCursor<Self> {
21+
Self::q()
22+
.declare_cursor(name)
23+
.expect("Static Query is correct")
24+
.scroll()
25+
.hold()
26+
.create_model_cursor(tx)
27+
.await
28+
.expect("Database is able to serve the request")
29+
}
30+
}
31+
32+
#[derive(Debug, Default)]
33+
struct TxModelController {
34+
cursors: Mutex<BTreeMap<SessionId, ModelCursor<AppLog>>>,
35+
}
36+
enum Direction {
37+
Next,
38+
Prev,
39+
}
40+
impl FromStr for Direction {
41+
type Err = String;
42+
fn from_str(s: &str) -> Result<Self, Self::Err> {
43+
match s.to_lowercase().as_str() {
44+
"next" => Ok(Self::Next),
45+
"prev" => Ok(Self::Prev),
46+
s => Err(format!("{} is not a valid direction", s)),
47+
}
48+
}
49+
}
50+
static FETCH_CHUNK: i64 = 3;
51+
#[async_trait]
52+
impl Controller for TxModelController {
53+
async fn handle(&self, request: &Request) -> Result<Response, rwf::controller::Error> {
54+
let mut tx = Pool::begin().await?;
55+
let sess_id = request.session_id();
56+
let cur_name = format!("sess_cur_{}", sess_id.to_string());
57+
match request.query().get::<Direction>("direction") {
58+
None => {
59+
if let Some(old) = self.cursors.lock().await.remove(&sess_id) {
60+
tx.query_cached(old.close_stmt().as_str(), &[]).await?;
61+
}
62+
self.cursors
63+
.lock()
64+
.await
65+
.insert(sess_id, AppLog::cursor(cur_name, &mut tx).await);
66+
tx.commit().await?;
67+
render!(request, "templates/index.html")
68+
}
69+
Some(direction) => {
70+
let mut cur = match self.cursors.lock().await.remove(&sess_id) {
71+
None => return Ok(Response::new().redirect(request.path().path())),
72+
Some(cursor) => cursor,
73+
}
74+
.to_transaction_cursor(tx);
75+
let entries = match direction {
76+
Direction::Next => cur.fetch_optional(cur.fetch_stmt().forward(FETCH_CHUNK)),
77+
Direction::Prev => cur.fetch_optional(cur.fetch_stmt().backward(FETCH_CHUNK)),
78+
}
79+
.await?
80+
.unwrap_or(Vec::new());
81+
let (cur, tx) = cur.decouple();
82+
tx.commit().await?;
83+
self.cursors.lock().await.insert(sess_id, cur);
84+
Ok(Response::new().turbo_stream(&[turbo_stream!(request, "templates/entries.html", "entries", "entries" => entries).action("replace")]))
85+
}
86+
}
87+
}
88+
}
89+
90+
#[tokio::main]
91+
async fn main() -> Result<(), rwf::http::Error> {
92+
migrate().await?;
93+
rwf::http::server::Server::new(vec![
94+
route!("/" => TxModelController),
95+
route!("/ws" => TurboStream),
96+
])
97+
.launch()
98+
.await
99+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<ul class="list-group" id="entries">
2+
<% for entry in entries %>
3+
<li class="list-group-item">
4+
<%= entry.data %>
5+
</li>
6+
<% end %>
7+
</ul>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<html>
2+
<head>
3+
<title>RWF Cursor Example</title>
4+
<%- rwf_head %>
5+
</head>
6+
<body>
7+
<%= rwf_turbo_stream("/ws") %>
8+
<div class="container">
9+
<div class="card">
10+
<div class="card-body">
11+
<ul class="list-group" id="entries"></ul>
12+
</div>
13+
<div class="card-footer">
14+
<form method="get" id="direction-form">
15+
<label for="direction">Direction: </label>
16+
<select id="direction" name="direction">
17+
<option value="next">Next</option>
18+
<option vaLue="prev">Previous</option>
19+
</select>
20+
<input type="submit" value="send">
21+
</form>
22+
</div>
23+
</div>
24+
</div>
25+
</body>
26+
</html>

0 commit comments

Comments
 (0)