1use std::{
4 collections::BTreeSet,
5 sync::{Arc, Mutex},
6};
7
8use feltdb::{
9 state_contract::{
10 AuthorizationContext, CanonicalQuery, QueryResult, TransactionOperation,
11 TransactionOperationKind, TransactionPrecondition, TransactionRequest, TransactionResult,
12 },
13 ReadBasis, ReadSet,
14};
15use serde::{Deserialize, Serialize};
16use serde_json::{json, Value};
17
18use crate::{encode, Client, Error, EventStream, Method, Result};
19
20#[derive(Debug, Clone, PartialEq, Deserialize)]
22pub struct ApplicationInfo {
23 pub application_id: String,
24 #[serde(default)]
25 pub application_name: Option<String>,
26 pub environment: String,
27 pub revision_id: String,
29 #[serde(default)]
30 pub version: Option<u64>,
31 pub contract_hash: String,
32 #[serde(default)]
33 pub flow_hash: Option<String>,
34 #[serde(default)]
35 pub dsl_version: Option<u32>,
36 #[serde(default)]
38 pub snapshot: Value,
39}
40
41#[derive(Debug, Clone)]
44pub struct AppTransaction {
45 transaction_id: String,
46 operations: Vec<TransactionOperation>,
47 preconditions: Vec<TransactionPrecondition>,
48 fences: Vec<ReadBasis>,
49}
50
51impl AppTransaction {
52 pub fn new(transaction_id: impl Into<String>) -> Self {
54 Self {
55 transaction_id: transaction_id.into(),
56 operations: Vec::new(),
57 preconditions: Vec::new(),
58 fences: Vec::new(),
59 }
60 }
61
62 pub fn generated() -> Self {
64 Self::new(format!("tx-{}", uuid::Uuid::new_v4()))
65 }
66
67 pub fn id(&self) -> &str {
68 &self.transaction_id
69 }
70
71 fn push(
72 self,
73 kind: TransactionOperationKind,
74 collection: String,
75 id: String,
76 value: Value,
77 ) -> Self {
78 self.operation(TransactionOperation {
79 kind,
80 collection,
81 id,
82 value,
83 if_version: None,
84 })
85 }
86
87 pub fn insert(
89 self,
90 collection: impl Into<String>,
91 id: impl Into<String>,
92 value: Value,
93 ) -> Self {
94 self.push(
95 TransactionOperationKind::Insert,
96 collection.into(),
97 id.into(),
98 value,
99 )
100 }
101
102 pub fn update(
104 self,
105 collection: impl Into<String>,
106 id: impl Into<String>,
107 value: Value,
108 ) -> Self {
109 self.push(
110 TransactionOperationKind::Update,
111 collection.into(),
112 id.into(),
113 value,
114 )
115 }
116
117 pub fn delete(self, collection: impl Into<String>, id: impl Into<String>) -> Self {
118 self.push(
119 TransactionOperationKind::Delete,
120 collection.into(),
121 id.into(),
122 Value::Null,
123 )
124 }
125
126 pub fn update_if_version(
129 self,
130 collection: impl Into<String>,
131 id: impl Into<String>,
132 value: Value,
133 version: u64,
134 ) -> Self {
135 self.operation(TransactionOperation {
136 kind: TransactionOperationKind::Update,
137 collection: collection.into(),
138 id: id.into(),
139 value,
140 if_version: Some(version),
141 })
142 }
143
144 pub fn delete_if_version(
147 self,
148 collection: impl Into<String>,
149 id: impl Into<String>,
150 version: u64,
151 ) -> Self {
152 self.operation(TransactionOperation {
153 kind: TransactionOperationKind::Delete,
154 collection: collection.into(),
155 id: id.into(),
156 value: Value::Null,
157 if_version: Some(version),
158 })
159 }
160
161 pub fn operation(mut self, operation: TransactionOperation) -> Self {
163 self.operations.push(operation);
164 self
165 }
166
167 pub fn precondition(mut self, precondition: TransactionPrecondition) -> Self {
182 self.preconditions.push(precondition);
183 self
184 }
185
186 pub fn expect_version(
188 self,
189 collection: impl Into<String>,
190 id: impl Into<String>,
191 version: u64,
192 ) -> Self {
193 self.precondition(TransactionPrecondition::Record {
194 collection: collection.into(),
195 id: id.into(),
196 require_absent: false,
197 expected_version: Some(version),
198 expected_epoch: None,
199 expected_lease_id: None,
200 })
201 }
202
203 pub fn expect_absent(self, collection: impl Into<String>, id: impl Into<String>) -> Self {
205 self.precondition(TransactionPrecondition::Record {
206 collection: collection.into(),
207 id: id.into(),
208 require_absent: true,
209 expected_version: None,
210 expected_epoch: None,
211 expected_lease_id: None,
212 })
213 }
214
215 pub fn fence(mut self, basis: ReadBasis) -> Self {
217 self.fences.push(basis);
218 self
219 }
220}
221
222#[derive(Debug, Clone, Deserialize)]
224pub struct AppCommit {
225 #[serde(flatten)]
226 pub result: TransactionResult,
227 pub revision_id: String,
228 #[serde(default)]
229 pub operation_ids: Vec<String>,
230}
231
232#[derive(Debug, Clone, PartialEq, Deserialize)]
234pub struct StateVersion {
235 pub application_id: String,
236 pub revision_id: String,
237 pub schema_version: u64,
238 pub state_version: u64,
240 #[serde(default)]
242 pub state_namespace: Option<String>,
243 #[serde(default)]
245 pub causal_cursor: Value,
246}
247
248#[derive(Debug, Clone, PartialEq, Deserialize)]
250pub struct ReplayPage {
251 pub contract_version: String,
252 pub cursor_format: String,
253 pub application_id: String,
254 pub environment: String,
255 pub events: Vec<Value>,
257 pub next_cursor: String,
259}
260
261#[derive(Debug, Clone)]
263pub struct Application {
264 client: Client,
265 application_id: String,
266 environment: String,
267 revision: Arc<Mutex<Option<String>>>,
269}
270
271#[derive(Serialize)]
272struct Envelope<'a, T: Serialize> {
273 application_id: &'a str,
274 revision_id: &'a str,
275 environment: &'a str,
276 #[serde(flatten)]
277 payload: T,
278}
279
280impl Application {
281 pub(crate) fn new(client: Client, application_id: String) -> Self {
282 Self {
283 client,
284 application_id,
285 environment: "production".into(),
286 revision: Arc::default(),
287 }
288 }
289
290 pub fn environment(mut self, environment: impl Into<String>) -> Self {
292 self.environment = environment.into();
293 self.revision = Arc::default();
294 self
295 }
296
297 pub fn id(&self) -> &str {
298 &self.application_id
299 }
300
301 pub fn environment_name(&self) -> &str {
302 &self.environment
303 }
304
305 fn scoped(&self, path: &str) -> String {
306 let separator = if path.contains('?') { '&' } else { '?' };
307 format!(
308 "/v1{path}{separator}application_id={}&environment={}",
309 encode(&self.application_id),
310 encode(&self.environment)
311 )
312 }
313
314 pub async fn discover(&self) -> Result<ApplicationInfo> {
316 let info: ApplicationInfo = self
317 .client
318 .send::<_, ()>(Method::GET, &self.scoped("/application"), None)
319 .await?;
320 *self
321 .revision
322 .lock()
323 .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(info.revision_id.clone());
324 Ok(info)
325 }
326
327 pub async fn revision_id(&self) -> Result<String> {
330 let cached = self
331 .revision
332 .lock()
333 .unwrap_or_else(|poisoned| poisoned.into_inner())
334 .clone();
335 match cached {
336 Some(revision) => Ok(revision),
337 None => Ok(self.discover().await?.revision_id),
338 }
339 }
340
341 async fn canonical<T: serde::de::DeserializeOwned>(
342 &self,
343 path: &str,
344 payload: impl Serialize,
345 ) -> Result<T> {
346 let revision_id = self.revision_id().await?;
347 let body = Envelope {
348 application_id: &self.application_id,
349 revision_id: &revision_id,
350 environment: &self.environment,
351 payload,
352 };
353 self.client
354 .send(Method::POST, &self.scoped(path), Some(&body))
355 .await
356 }
357
358 pub async fn state_version(&self) -> Result<StateVersion> {
360 let revision_id = self.revision_id().await?;
361 let path = self.scoped(&format!(
362 "/state/version?revision_id={}",
363 encode(&revision_id)
364 ));
365 self.client.send::<_, ()>(Method::GET, &path, None).await
366 }
367
368 pub async fn query(&self, query: &CanonicalQuery) -> Result<QueryResult> {
370 self.canonical("/query", json!({ "query": query })).await
371 }
372
373 pub async fn commit(&self, transaction: &AppTransaction) -> Result<AppCommit> {
375 if transaction.operations.is_empty() {
376 return Err(Error::InvalidRequest(
377 "a transaction requires at least one operation".into(),
378 ));
379 }
380 let revision_id = self.revision_id().await?;
381 let tenant_id = match &self.client.inner.scope {
385 Some(crate::AuthorityScope::Tenant { tenant_id, .. }) => tenant_id.clone(),
386 _ => String::new(),
387 };
388 let request = TransactionRequest {
389 transaction_id: Some(transaction.transaction_id.clone()),
390 tenant_id: tenant_id.clone(),
391 application_id: self.application_id.clone(),
392 revision_id: revision_id.clone(),
393 schema_version: 0,
394 state_namespace: None,
395 causal_parent: None,
396 authorization: AuthorizationContext {
397 subject: String::new(),
398 tenant_id,
399 application_id: self.application_id.clone(),
400 revision_id,
401 capabilities: BTreeSet::new(),
402 readable_collections: BTreeSet::new(),
403 writable_collections: BTreeSet::new(),
404 field_projections: Default::default(),
405 },
406 operations: transaction.operations.clone(),
407 preconditions: transaction.preconditions.clone(),
408 fences: transaction.fences.clone(),
409 };
410 self.canonical("/transactions", json!({ "transaction": request }))
411 .await
412 }
413
414 pub async fn read_basis(&self, reads: &ReadSet) -> Result<ReadBasis> {
416 self.canonical("/transactions/read-basis", json!({ "reads": reads }))
417 .await
418 }
419
420 pub async fn events(&self, cursor: Option<&str>) -> Result<EventStream> {
422 self.client
423 .open_events(&self.scoped("/events"), cursor)
424 .await
425 }
426
427 pub async fn replay(&self, cursor: Option<&str>, limit: Option<usize>) -> Result<ReplayPage> {
430 let mut path = self.scoped("/public/events/replay");
431 if let Some(cursor) = cursor {
432 path.push_str(&format!("&cursor={}", encode(cursor)));
433 }
434 if let Some(limit) = limit {
435 path.push_str(&format!("&limit={limit}"));
436 }
437 self.client.send::<_, ()>(Method::GET, &path, None).await
438 }
439
440 pub async fn provenance(&self, collection: &str, id: &str) -> Result<Value> {
442 let path = self.scoped(&format!(
443 "/control-plane/provenance/{}/{}",
444 encode(collection),
445 encode(id)
446 ));
447 self.client.send::<_, ()>(Method::GET, &path, None).await
448 }
449
450 pub async fn sign_in(&self, email: &str, password: &str) -> Result<Value> {
453 let body =
454 json!({ "application_id": self.application_id, "email": email, "password": password });
455 self.authenticate("/v1/auth/signin", &body).await
456 }
457
458 pub async fn sign_up(
460 &self,
461 email: &str,
462 password: &str,
463 display_name: Option<&str>,
464 ) -> Result<Value> {
465 let body = json!({
466 "application_id": self.application_id,
467 "email": email,
468 "password": password,
469 "display_name": display_name,
470 });
471 self.authenticate("/v1/auth/signup", &body).await
472 }
473
474 async fn authenticate(&self, path: &str, body: &Value) -> Result<Value> {
475 let session: Value = self.client.send(Method::POST, path, Some(body)).await?;
476 if let Some(token) = session.get("access_token").and_then(Value::as_str) {
477 self.client.set_token(token);
478 }
479 Ok(session)
480 }
481
482 pub async fn session(&self) -> Result<Value> {
484 self.client
485 .send::<_, ()>(Method::GET, "/v1/auth/session", None)
486 .await
487 }
488
489 pub async fn sign_out(&self) -> Result<()> {
491 self.client
492 .exchange::<()>(Method::POST, "/v1/auth/signout", None)
493 .await?;
494 self.client.set_token("");
495 Ok(())
496 }
497}
498
499impl Client {
500 pub async fn capabilities(&self) -> Result<Value> {
502 self.send::<_, ()>(Method::GET, "/v1/control-plane/capabilities", None)
503 .await
504 }
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510
511 #[test]
512 fn scoped_paths_carry_the_application_and_environment() {
513 let app = Client::builder("http://localhost:1")
514 .build()
515 .unwrap()
516 .application("app 1")
517 .environment("staging");
518 assert_eq!(
519 app.scoped("/application"),
520 "/v1/application?application_id=app%201&environment=staging"
521 );
522 assert_eq!(
523 app.scoped("/x?y=1"),
524 "/v1/x?y=1&application_id=app%201&environment=staging"
525 );
526 }
527}