Skip to main content

mnm_store/entities/
node.rs

1//! `node` entity queries — including the recursive parent_chain CTE that powers
2//! US4's `parent_chain` response field and the `chunks/{id}/parents` endpoint.
3
4use mnm_core::types::{Node, NodeKind};
5use serde::Serialize;
6use sqlx::PgPool;
7use time::OffsetDateTime;
8use uuid::Uuid;
9
10use crate::error::Result;
11
12/// Insert a node, returning the newly-minted id.
13///
14/// # Errors
15///
16/// Returns [`crate::error::StoreError::ForeignKeyViolation`] if
17/// `source_version_id` or `parent_node_id` are unknown.
18pub async fn insert(
19    pool: &PgPool,
20    source_version_id: Uuid,
21    parent_node_id: Option<Uuid>,
22    kind: NodeKind,
23    name: &str,
24    order_index: i32,
25) -> Result<Uuid> {
26    let kind_str = match kind {
27        NodeKind::Root => "root",
28        NodeKind::Group => "group",
29        NodeKind::Document => "document",
30        NodeKind::Chunk => "chunk",
31    };
32    let row: (Uuid,) = sqlx::query_as(
33        "INSERT INTO node (source_version_id, parent_node_id, kind, name, order_index) \
34         VALUES ($1, $2, $3, $4, $5) RETURNING id",
35    )
36    .bind(source_version_id)
37    .bind(parent_node_id)
38    .bind(kind_str)
39    .bind(name)
40    .bind(order_index)
41    .fetch_one(pool)
42    .await?;
43    Ok(row.0)
44}
45
46/// Walk the parent chain from `node_id` up to the source-version root.
47///
48/// Returns the chain ordered from immediate parent → root (root last). For a
49/// chunk-kind node, the first entry is the document node; subsequent entries
50/// are the document's enclosing groups, ending at the implicit root.
51///
52/// # Errors
53///
54/// Returns [`crate::error::StoreError::NotFound`] if `node_id` does not exist.
55pub async fn parent_chain(pool: &PgPool, node_id: Uuid) -> Result<Vec<Node>> {
56    let rows = sqlx::query_as::<_, NodeRow>(
57        "WITH RECURSIVE chain AS ( \
58             SELECT id, source_version_id, parent_node_id, kind, name, order_index, created_at, 0 AS depth \
59             FROM node WHERE id = $1 \
60             UNION ALL \
61             SELECT n.id, n.source_version_id, n.parent_node_id, n.kind, n.name, n.order_index, n.created_at, c.depth + 1 \
62             FROM node n JOIN chain c ON n.id = c.parent_node_id \
63         ) \
64         SELECT id, source_version_id, parent_node_id, kind, name, order_index, created_at FROM chain \
65         WHERE depth > 0 ORDER BY depth",
66    )
67    .bind(node_id)
68    .fetch_all(pool)
69    .await?;
70    rows.into_iter().map(TryInto::try_into).collect()
71}
72
73/// One ancestor in a chunk's parent chain, with the document id attached when
74/// the node is a document node (group/root nodes have `document_id: None`).
75#[derive(Debug, Clone, Serialize)]
76pub struct ParentNode {
77    /// Node id (structural hierarchy id — NOT a document id).
78    pub id: Uuid,
79    /// Owning source version.
80    pub source_version_id: Uuid,
81    /// Parent node id (`None` for the root).
82    pub parent_node_id: Option<Uuid>,
83    /// `document` / `group` / `root`.
84    pub kind: NodeKind,
85    /// Display name (file or folder name; `root` for the root).
86    pub name: String,
87    /// Sibling order.
88    pub order_index: i32,
89    /// The fetchable document id, present only on `kind == "document"` nodes.
90    pub document_id: Option<Uuid>,
91}
92
93/// [`parent_chain`] + a LEFT JOIN to `document` so document-kind nodes carry
94/// their fetchable document id. Ordered immediate parent → root.
95///
96/// # Errors
97///
98/// Returns [`crate::error::StoreError::Database`] on driver failure, or
99/// [`crate::error::StoreError::Json`] if a `node.kind` value fails to decode
100/// into [`NodeKind`].
101pub async fn parent_chain_with_documents(pool: &PgPool, node_id: Uuid) -> Result<Vec<ParentNode>> {
102    #[derive(sqlx::FromRow)]
103    struct Row {
104        id: Uuid,
105        source_version_id: Uuid,
106        parent_node_id: Option<Uuid>,
107        kind: String,
108        name: String,
109        order_index: i32,
110        document_id: Option<Uuid>,
111    }
112    let rows = sqlx::query_as::<_, Row>(
113        "WITH RECURSIVE chain AS ( \
114             SELECT id, source_version_id, parent_node_id, kind, name, order_index, 0 AS depth \
115             FROM node WHERE id = $1 \
116             UNION ALL \
117             SELECT n.id, n.source_version_id, n.parent_node_id, n.kind, n.name, n.order_index, c.depth + 1 \
118             FROM node n JOIN chain c ON n.id = c.parent_node_id \
119         ) \
120         SELECT chain.id, chain.source_version_id, chain.parent_node_id, chain.kind, chain.name, \
121                chain.order_index, d.id AS document_id \
122         FROM chain LEFT JOIN document d ON d.node_id = chain.id \
123         WHERE chain.depth > 0 ORDER BY chain.depth",
124    )
125    .bind(node_id)
126    .fetch_all(pool)
127    .await?;
128    rows.into_iter()
129        .map(|r| {
130            let kind: NodeKind = serde_json::from_value(serde_json::Value::String(r.kind))
131                .map_err(|e| crate::error::StoreError::Json(e.to_string()))?;
132            Ok(ParentNode {
133                id: r.id,
134                source_version_id: r.source_version_id,
135                parent_node_id: r.parent_node_id,
136                kind,
137                name: r.name,
138                order_index: r.order_index,
139                document_id: r.document_id,
140            })
141        })
142        .collect()
143}
144
145/// Fetch one node by id.
146///
147/// # Errors
148///
149/// Returns [`crate::error::StoreError::NotFound`] if id is unknown.
150pub async fn get_by_id(pool: &PgPool, id: Uuid) -> Result<Node> {
151    let row = sqlx::query_as::<_, NodeRow>(
152        "SELECT id, source_version_id, parent_node_id, kind, name, order_index, created_at \
153         FROM node WHERE id = $1",
154    )
155    .bind(id)
156    .fetch_one(pool)
157    .await?;
158    row.try_into()
159}
160
161#[derive(sqlx::FromRow)]
162struct NodeRow {
163    id: Uuid,
164    source_version_id: Uuid,
165    parent_node_id: Option<Uuid>,
166    kind: String,
167    name: String,
168    order_index: i32,
169    created_at: OffsetDateTime,
170}
171
172impl TryFrom<NodeRow> for Node {
173    type Error = crate::error::StoreError;
174
175    fn try_from(r: NodeRow) -> std::result::Result<Self, Self::Error> {
176        let kind: NodeKind = serde_json::from_value(serde_json::Value::String(r.kind))
177            .map_err(|e| crate::error::StoreError::Json(e.to_string()))?;
178        Ok(Self {
179            id: r.id,
180            source_version_id: r.source_version_id,
181            parent_node_id: r.parent_node_id,
182            kind,
183            name: r.name,
184            order_index: r.order_index,
185            created_at: r.created_at,
186        })
187    }
188}