catcolab_backend/
document.rs

1//! Procedures to create and manipulate documents.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use ts_rs::TS;
6use uuid::Uuid;
7
8use super::app::{AppCtx, AppError, AppState};
9
10/// Creates a new document ref with initial content.
11pub async fn new_ref(ctx: AppCtx, content: Value) -> Result<Uuid, AppError> {
12    let ref_id = Uuid::now_v7();
13
14    let mut transaction = ctx.state.db.begin().await?;
15
16    let user_id = ctx.user.map(|user| user.user_id);
17    let insert_ref = sqlx::query!(
18        "
19        WITH snapshot AS (
20            INSERT INTO snapshots(for_ref, content, last_updated)
21            VALUES ($1, $2, NOW())
22            RETURNING id
23        )
24        INSERT INTO refs(id, head, created)
25        VALUES ($1, (SELECT id FROM snapshot), NOW())
26        ",
27        ref_id,
28        content
29    );
30    insert_ref.execute(&mut *transaction).await?;
31
32    let insert_permission = sqlx::query!(
33        "
34        INSERT INTO permissions(subject, object, level)
35        VALUES ($1, $2, 'own')
36        ",
37        user_id,
38        ref_id,
39    );
40    insert_permission.execute(&mut *transaction).await?;
41
42    transaction.commit().await?;
43    Ok(ref_id)
44}
45
46/// Gets the content of the head snapshot for a document ref.
47pub async fn head_snapshot(state: AppState, ref_id: Uuid) -> Result<Value, AppError> {
48    let query = sqlx::query!(
49        "
50        SELECT content FROM snapshots
51        WHERE id = (SELECT head FROM refs WHERE id = $1)
52        ",
53        ref_id
54    );
55    Ok(query.fetch_one(&state.db).await?.content)
56}
57
58/// Saves the document by overwriting the snapshot at the current head.
59pub async fn autosave(state: AppState, data: RefContent) -> Result<(), AppError> {
60    let RefContent { ref_id, content } = data;
61    let query = sqlx::query!(
62        "
63        UPDATE snapshots
64        SET content = $2, last_updated = NOW()
65        WHERE id = (SELECT head FROM refs WHERE id = $1)
66        ",
67        ref_id,
68        content
69    );
70    query.execute(&state.db).await?;
71    Ok(())
72}
73
74/** Saves the document by replacing the head with a new snapshot.
75
76The snapshot at the previous head is *not* deleted.
77*/
78pub async fn save_snapshot(state: AppState, data: RefContent) -> Result<(), AppError> {
79    let RefContent { ref_id, content } = data;
80    let query = sqlx::query!(
81        "
82        WITH snapshot AS (
83            INSERT INTO snapshots(for_ref, content, last_updated)
84            VALUES ($1, $2, NOW())
85            RETURNING id
86        )
87        UPDATE refs
88        SET head = (SELECT id FROM snapshot)
89        WHERE id = $1
90        ",
91        ref_id,
92        content
93    );
94    query.execute(&state.db).await?;
95    Ok(())
96}
97
98/// Gets an Automerge document ID for the document ref.
99pub async fn doc_id(state: AppState, ref_id: Uuid) -> Result<String, AppError> {
100    let automerge_io = &state.automerge_io;
101    let ack = automerge_io.emit_with_ack::<Vec<Option<String>>>("get_doc", ref_id).unwrap();
102    let mut response = ack.await?;
103
104    let maybe_doc_id = response.data.pop().flatten();
105    if let Some(doc_id) = maybe_doc_id {
106        // If an Automerge doc handle for this ref already exists, return it.
107        Ok(doc_id)
108    } else {
109        // Otherwise, fetch the content from the database and create a new
110        // Automerge doc handle.
111        let content = head_snapshot(state.clone(), ref_id).await?;
112        let data = RefContent { ref_id, content };
113        let ack = automerge_io.emit_with_ack::<Vec<String>>("create_doc", data).unwrap();
114        let response = ack.await?;
115        Ok(response.data[0].to_string())
116    }
117}
118
119/// A document ref along with its content.
120#[derive(Debug, Serialize, Deserialize, TS)]
121pub struct RefContent {
122    #[serde(rename = "refId")]
123    pub ref_id: Uuid,
124    pub content: Value,
125}