Skip to main content

netget/server/bolt/
values.rs

1//! The model's JSON answers → PackStream, and the client's PackStream parameters → JSON.
2//!
3//! **Model → wire.** A record value is plain JSON: strings, numbers, booleans, null, arrays and
4//! objects become PackStream strings, integers or floats, booleans, null, lists and maps. Three
5//! single-key objects are graph values instead, so a client renders them as nodes, relationships
6//! and paths rather than as maps:
7//!
8//! ```json
9//! {"$node": {"id": 1, "labels": ["Person"], "properties": {"name": "Alice"}}}
10//! {"$relationship": {"id": 7, "type": "KNOWS", "start": 1, "end": 2, "properties": {}}}
11//! {"$path": {"nodes": [{"id": 1, "labels": ["Person"]}, {"id": 2, "labels": ["Person"]}],
12//!            "relationships": [{"id": 7, "type": "KNOWS", "start": 1, "end": 2}]}}
13//! ```
14//!
15//! `element_id` (and a relationship's `start_element_id` / `end_element_id`) may be given; each
16//! defaults to the decimal string of the matching integer id. A path's `relationships[i]` must
17//! connect `nodes[i]` and `nodes[i + 1]` in either direction, and NetGet computes the Bolt path
18//! index sequence (1-based relationship index, negative when traversed backwards; 0-based node
19//! index) itself. Temporal and spatial values are not in the set — give them as strings.
20//!
21//! **Client → model.** RUN's parameters arrive as PackStream and reach the model as JSON. Bytes
22//! become `{"$bytes_length": n}` (never the bytes: the event-design rule) and any structure
23//! becomes `{"$structure": "<name>", "fields": [...]}` using Bolt's structure names.
24//!
25//! Both directions are bounded to [`MAX_PACKSTREAM_DEPTH`], so NetGet never emits a value its own
26//! decoder would refuse.
27
28use super::packstream::{Value, MAX_PACKSTREAM_DEPTH};
29use serde_json::Value as Json;
30
31/// Bolt structure tags for the graph types.
32pub const TAG_NODE: u8 = 0x4E;
33pub const TAG_RELATIONSHIP: u8 = 0x52;
34pub const TAG_UNBOUND_RELATIONSHIP: u8 = 0x72;
35pub const TAG_PATH: u8 = 0x50;
36
37/// Convert a JSON value the model wrote into a PackStream value.
38pub fn json_to_value(json: &Json) -> Result<Value, String> {
39    to_value(json, 1)
40}
41
42fn to_value(json: &Json, depth: usize) -> Result<Value, String> {
43    Ok(match json {
44        Json::Null => Value::Null,
45        Json::Bool(b) => Value::Bool(*b),
46        Json::Number(n) => {
47            if let Some(i) = n.as_i64() {
48                Value::Int(i)
49            } else if n.is_u64() {
50                return Err(format!(
51                    "integer {n} does not fit a PackStream INT_64 (max {})",
52                    i64::MAX
53                ));
54            } else {
55                Value::Float(n.as_f64().unwrap_or(f64::NAN))
56            }
57        }
58        Json::String(s) => Value::String(s.clone()),
59        Json::Array(items) => {
60            check_depth(depth)?;
61            Value::List(
62                items
63                    .iter()
64                    .map(|v| to_value(v, depth + 1))
65                    .collect::<Result<_, _>>()?,
66            )
67        }
68        Json::Object(map) => {
69            check_depth(depth)?;
70            if map.len() == 1 {
71                if let Some(node) = map.get("$node") {
72                    return node_value(node, depth + 1).map(|n| n.value);
73                }
74                if let Some(rel) = map.get("$relationship") {
75                    return relationship_value(rel, depth + 1).map(|r| r.bound);
76                }
77                if let Some(path) = map.get("$path") {
78                    return path_value(path, depth + 1);
79                }
80            }
81            Value::Map(
82                map.iter()
83                    .map(|(k, v)| Ok((k.clone(), to_value(v, depth + 1)?)))
84                    .collect::<Result<_, String>>()?,
85            )
86        }
87    })
88}
89
90fn check_depth(depth: usize) -> Result<(), String> {
91    if depth > MAX_PACKSTREAM_DEPTH {
92        Err(format!(
93            "value nests deeper than {MAX_PACKSTREAM_DEPTH} levels"
94        ))
95    } else {
96        Ok(())
97    }
98}
99
100fn object<'a>(json: &'a Json, what: &str) -> Result<&'a serde_json::Map<String, Json>, String> {
101    json.as_object()
102        .ok_or_else(|| format!("{what} must be an object"))
103}
104
105fn int_field(map: &serde_json::Map<String, Json>, key: &str, what: &str) -> Result<i64, String> {
106    map.get(key)
107        .and_then(Json::as_i64)
108        .ok_or_else(|| format!("{what} needs an integer '{key}'"))
109}
110
111fn element_id(map: &serde_json::Map<String, Json>, key: &str, fallback: i64) -> Value {
112    Value::String(
113        map.get(key)
114            .and_then(Json::as_str)
115            .map(str::to_string)
116            .unwrap_or_else(|| fallback.to_string()),
117    )
118}
119
120fn properties(
121    map: &serde_json::Map<String, Json>,
122    what: &str,
123    depth: usize,
124) -> Result<Value, String> {
125    match map.get("properties") {
126        None | Some(Json::Null) => Ok(Value::Map(Vec::new())),
127        Some(Json::Object(_)) => to_value(&map["properties"], depth),
128        Some(_) => Err(format!("{what} 'properties' must be an object")),
129    }
130}
131
132struct Node {
133    id: i64,
134    value: Value,
135}
136
137/// `{"id", "labels"?, "properties"?, "element_id"?}` → Node, accepting the `{"$node": …}`
138/// wrapper too (inside a path).
139fn node_value(json: &Json, depth: usize) -> Result<Node, String> {
140    check_depth(depth)?;
141    let json = json.get("$node").unwrap_or(json);
142    let map = object(json, "$node")?;
143    let id = int_field(map, "id", "$node")?;
144    let labels = match map.get("labels") {
145        None | Some(Json::Null) => Vec::new(),
146        Some(Json::Array(labels)) => labels
147            .iter()
148            .map(|l| {
149                l.as_str()
150                    .map(Value::string)
151                    .ok_or_else(|| "$node 'labels' must be strings".to_string())
152            })
153            .collect::<Result<_, _>>()?,
154        Some(_) => return Err("$node 'labels' must be an array of strings".to_string()),
155    };
156    Ok(Node {
157        id,
158        value: Value::Struct {
159            tag: TAG_NODE,
160            fields: vec![
161                Value::Int(id),
162                Value::List(labels),
163                properties(map, "$node", depth + 1)?,
164                element_id(map, "element_id", id),
165            ],
166        },
167    })
168}
169
170struct Relationship {
171    id: i64,
172    start: i64,
173    end: i64,
174    bound: Value,
175    unbound: Value,
176}
177
178/// `{"id", "type", "start", "end", "properties"?, "element_id"?, "start_element_id"?,
179/// "end_element_id"?}` → Relationship (and its unbound form for paths).
180fn relationship_value(json: &Json, depth: usize) -> Result<Relationship, String> {
181    check_depth(depth)?;
182    let json = json.get("$relationship").unwrap_or(json);
183    let map = object(json, "$relationship")?;
184    let id = int_field(map, "id", "$relationship")?;
185    let start = int_field(map, "start", "$relationship")?;
186    let end = int_field(map, "end", "$relationship")?;
187    let rel_type = map
188        .get("type")
189        .and_then(Json::as_str)
190        .filter(|t| !t.is_empty())
191        .ok_or("$relationship needs a non-empty string 'type'")?
192        .to_string();
193    let props = properties(map, "$relationship", depth + 1)?;
194    let eid = element_id(map, "element_id", id);
195    Ok(Relationship {
196        id,
197        start,
198        end,
199        bound: Value::Struct {
200            tag: TAG_RELATIONSHIP,
201            fields: vec![
202                Value::Int(id),
203                Value::Int(start),
204                Value::Int(end),
205                Value::String(rel_type.clone()),
206                props.clone(),
207                eid.clone(),
208                element_id(map, "start_element_id", start),
209                element_id(map, "end_element_id", end),
210            ],
211        },
212        unbound: Value::Struct {
213            tag: TAG_UNBOUND_RELATIONSHIP,
214            fields: vec![Value::Int(id), Value::String(rel_type), props, eid],
215        },
216    })
217}
218
219fn path_value(json: &Json, depth: usize) -> Result<Value, String> {
220    check_depth(depth)?;
221    let map = object(json, "$path")?;
222    let nodes = map
223        .get("nodes")
224        .and_then(Json::as_array)
225        .ok_or("$path needs a 'nodes' array")?;
226    let rels = match map.get("relationships") {
227        None | Some(Json::Null) => Vec::new(),
228        Some(Json::Array(r)) => r.clone(),
229        Some(_) => return Err("$path 'relationships' must be an array".to_string()),
230    };
231    if nodes.is_empty() {
232        return Err("$path needs at least one node".to_string());
233    }
234    if rels.len() + 1 != nodes.len() {
235        return Err(format!(
236            "$path with {} nodes needs {} relationships, got {}",
237            nodes.len(),
238            nodes.len() - 1,
239            rels.len()
240        ));
241    }
242    let nodes: Vec<Node> = nodes
243        .iter()
244        .map(|n| node_value(n, depth + 1))
245        .collect::<Result<_, _>>()?;
246    let rels: Vec<Relationship> = rels
247        .iter()
248        .map(|r| relationship_value(r, depth + 1))
249        .collect::<Result<_, _>>()?;
250
251    // Distinct nodes and relationships in order of first appearance; the index sequence refers
252    // into these lists.
253    let mut unique_nodes: Vec<&Node> = Vec::new();
254    for node in &nodes {
255        if !unique_nodes.iter().any(|n| n.id == node.id) {
256            unique_nodes.push(node);
257        }
258    }
259    let mut unique_rels: Vec<&Relationship> = Vec::new();
260    for rel in &rels {
261        if !unique_rels.iter().any(|r| r.id == rel.id) {
262            unique_rels.push(rel);
263        }
264    }
265
266    let mut indices = Vec::with_capacity(rels.len() * 2);
267    for (i, rel) in rels.iter().enumerate() {
268        let (from, to) = (nodes[i].id, nodes[i + 1].id);
269        let rel_index = unique_rels
270            .iter()
271            .position(|r| r.id == rel.id)
272            .map(|p| p as i64 + 1)
273            .unwrap_or(1);
274        let signed = if rel.start == from && rel.end == to {
275            rel_index
276        } else if rel.start == to && rel.end == from {
277            -rel_index
278        } else {
279            return Err(format!(
280                "$path relationship {} ({}->{}) does not connect nodes {} and {}",
281                rel.id, rel.start, rel.end, from, to
282            ));
283        };
284        let node_index = unique_nodes
285            .iter()
286            .position(|n| n.id == to)
287            .map(|p| p as i64)
288            .unwrap_or(0);
289        indices.push(Value::Int(signed));
290        indices.push(Value::Int(node_index));
291    }
292
293    Ok(Value::Struct {
294        tag: TAG_PATH,
295        fields: vec![
296            Value::List(unique_nodes.iter().map(|n| n.value.clone()).collect()),
297            Value::List(unique_rels.iter().map(|r| r.unbound.clone()).collect()),
298            Value::List(indices),
299        ],
300    })
301}
302
303/// Bolt's name for a structure tag, for describing parameters to the model.
304pub fn structure_name(tag: u8) -> String {
305    match tag {
306        TAG_NODE => "Node".to_string(),
307        TAG_RELATIONSHIP => "Relationship".to_string(),
308        TAG_UNBOUND_RELATIONSHIP => "UnboundRelationship".to_string(),
309        TAG_PATH => "Path".to_string(),
310        0x44 => "Date".to_string(),
311        0x54 => "Time".to_string(),
312        0x74 => "LocalTime".to_string(),
313        0x49 => "DateTime".to_string(),
314        0x69 => "DateTimeZoneId".to_string(),
315        0x64 => "LocalDateTime".to_string(),
316        0x45 => "Duration".to_string(),
317        0x58 => "Point2D".to_string(),
318        0x59 => "Point3D".to_string(),
319        other => format!("0x{other:02X}"),
320    }
321}
322
323/// A decoded PackStream value as JSON for an event. Never carries raw bytes.
324pub fn value_to_json(value: &Value) -> Json {
325    match value {
326        Value::Null => Json::Null,
327        Value::Bool(b) => Json::Bool(*b),
328        Value::Int(i) => Json::from(*i),
329        Value::Float(f) => serde_json::Number::from_f64(*f)
330            .map(Json::Number)
331            .unwrap_or_else(|| Json::String(f.to_string())),
332        Value::Bytes(b) => serde_json::json!({"$bytes_length": b.len()}),
333        Value::String(s) => Json::String(s.clone()),
334        Value::List(items) => Json::Array(items.iter().map(value_to_json).collect()),
335        Value::Map(entries) => Json::Object(
336            entries
337                .iter()
338                .map(|(k, v)| (k.clone(), value_to_json(v)))
339                .collect(),
340        ),
341        Value::Struct { tag, fields } => serde_json::json!({
342            "$structure": structure_name(*tag),
343            "fields": fields.iter().map(value_to_json).collect::<Vec<_>>(),
344        }),
345    }
346}
347
348/// Query statistics Bolt reports in a result summary, as Neo4j names them on the wire.
349pub const STAT_COUNTERS: &[&str] = &[
350    "nodes-created",
351    "nodes-deleted",
352    "relationships-created",
353    "relationships-deleted",
354    "properties-set",
355    "labels-added",
356    "labels-removed",
357    "indexes-added",
358    "indexes-removed",
359    "constraints-added",
360    "constraints-removed",
361    "system-updates",
362];
363
364/// The model's `stats` object (`nodes_created` or `nodes-created`, integer counts) → Bolt's
365/// `stats` map, with `contains-updates` / `contains-system-updates` derived. Unknown keys and
366/// negative counts are refused.
367pub fn stats_value(json: Option<&Json>) -> Result<Option<Value>, String> {
368    let map = match json {
369        None | Some(Json::Null) => return Ok(None),
370        Some(Json::Object(map)) => map,
371        Some(_) => return Err("'stats' must be an object".to_string()),
372    };
373    let mut entries = Vec::new();
374    let mut updates = false;
375    let mut system_updates = false;
376    for (key, value) in map {
377        let wire = key.replace('_', "-");
378        if !STAT_COUNTERS.contains(&wire.as_str()) {
379            return Err(format!(
380                "unknown stats key '{key}' (allowed: {})",
381                STAT_COUNTERS
382                    .iter()
383                    .map(|k| k.replace('-', "_"))
384                    .collect::<Vec<_>>()
385                    .join(", ")
386            ));
387        }
388        let count = value
389            .as_i64()
390            .filter(|c| *c >= 0)
391            .ok_or_else(|| format!("stats '{key}' must be a non-negative integer"))?;
392        if count == 0 {
393            continue;
394        }
395        if wire == "system-updates" {
396            system_updates = true;
397        } else {
398            updates = true;
399        }
400        let wire_key = STAT_COUNTERS
401            .iter()
402            .find(|k| **k == wire)
403            .copied()
404            .unwrap_or("nodes-created");
405        entries.push((wire_key.to_string(), Value::Int(count)));
406    }
407    if entries.is_empty() {
408        return Ok(None);
409    }
410    if updates {
411        entries.push(("contains-updates".to_string(), Value::Bool(true)));
412    }
413    if system_updates {
414        entries.push(("contains-system-updates".to_string(), Value::Bool(true)));
415    }
416    Ok(Some(Value::Map(entries)))
417}
418
419/// A Neo4j status code the model may send in a FAILURE: `Neo.<Classification>.<Category>.<Title>`
420/// with the classification one of ClientError, TransientError or DatabaseError.
421pub fn valid_failure_code(code: &str) -> bool {
422    let parts: Vec<&str> = code.split('.').collect();
423    let word = |s: &str| {
424        !s.is_empty()
425            && s.len() <= 64
426            && s.chars().next().is_some_and(|c| c.is_ascii_uppercase())
427            && s.chars().all(|c| c.is_ascii_alphanumeric())
428    };
429    parts.len() == 4
430        && parts[0] == "Neo"
431        && matches!(parts[1], "ClientError" | "TransientError" | "DatabaseError")
432        && word(parts[2])
433        && word(parts[3])
434}
435
436/// A validated answer to one query: column names, rows, and the summary it ends with.
437#[derive(Debug, Clone)]
438pub struct QueryAnswer {
439    pub fields: Vec<String>,
440    pub records: Vec<Vec<Value>>,
441    pub stats: Option<Value>,
442    pub query_type: &'static str,
443}
444
445/// Preflight handler values before recursive copies or conversion. The JSON action
446/// envelope and graph wrappers need four additional levels beyond wire containers.
447pub const MAX_ACTION_DEPTH: usize = MAX_PACKSTREAM_DEPTH + 4;
448pub const MAX_ACTION_NODES: usize = 65536;
449pub const MAX_ACTION_RETAINED_BYTES: usize = 8 * 1024 * 1024;
450pub fn action_within_budget(action: &Json) -> bool {
451    crate::utils::json_budget::within_budget(
452        action,
453        MAX_ACTION_RETAINED_BYTES,
454        MAX_ACTION_NODES,
455        MAX_ACTION_DEPTH,
456    )
457}
458
459/// Check the actual encoded container shape, including the RECORD structure and row list.
460fn within_wire_depth(value: &Value, depth: usize) -> bool {
461    let mut pending = vec![(value, depth)];
462    while let Some((value, depth)) = pending.pop() {
463        let children = match value {
464            Value::List(v) | Value::Struct { fields: v, .. } => Some(v.as_slice()),
465            Value::Map(v) => {
466                if depth > MAX_PACKSTREAM_DEPTH {
467                    return false;
468                }
469                pending.extend(v.iter().map(|(_, v)| (v, depth + 1)));
470                None
471            }
472            _ => None,
473        };
474        if let Some(children) = children {
475            if depth > MAX_PACKSTREAM_DEPTH {
476                return false;
477            }
478            pending.extend(children.iter().map(|v| (v, depth + 1)));
479        }
480    }
481    true
482}
483
484/// Build a [`QueryAnswer`] from a `send_bolt_records` action, refusing anything a client would
485/// misread: a record whose width differs from `fields`, a duplicate or empty column name, an
486/// unknown `query_type` or statistic.
487pub fn query_answer(action: &Json) -> Result<QueryAnswer, String> {
488    if !action_within_budget(action) {
489        return Err("Bolt answer exceeds the in-memory depth/node/retained-content budget".into());
490    }
491    let fields: Vec<String> = match action.get("fields") {
492        Some(Json::Array(fields)) => fields
493            .iter()
494            .map(|f| {
495                f.as_str()
496                    .filter(|s| !s.is_empty())
497                    .map(str::to_string)
498                    .ok_or_else(|| "every entry in 'fields' must be a non-empty string".to_string())
499            })
500            .collect::<Result<_, _>>()?,
501        None | Some(Json::Null) => Vec::new(),
502        Some(_) => return Err("'fields' must be an array of column names".to_string()),
503    };
504    for (i, f) in fields.iter().enumerate() {
505        if fields[..i].contains(f) {
506            return Err(format!("column '{f}' appears twice in 'fields'"));
507        }
508    }
509    let rows = match action.get("records") {
510        None | Some(Json::Null) => Vec::new(),
511        Some(Json::Array(rows)) => rows.iter().collect::<Vec<_>>(),
512        Some(_) => return Err("'records' must be an array of rows".to_string()),
513    };
514    let mut records = Vec::with_capacity(rows.len());
515    for (i, row) in rows.iter().enumerate() {
516        let cells = row.as_array().ok_or_else(|| {
517            format!("record {i} must be an array with one value per field, in field order")
518        })?;
519        if cells.len() != fields.len() {
520            return Err(format!(
521                "record {i} has {} values but there are {} fields",
522                cells.len(),
523                fields.len()
524            ));
525        }
526        records.push(
527            cells
528                .iter()
529                .map(|c| {
530                    let value = json_to_value(c).map_err(|e| format!("record {i}: {e}"))?;
531                    if !within_wire_depth(&value, 3) {
532                        return Err(format!(
533                            "record {i}: encoded value exceeds wire depth {MAX_PACKSTREAM_DEPTH}"
534                        ));
535                    }
536                    Ok(value)
537                })
538                .collect::<Result<Vec<_>, _>>()?,
539        );
540    }
541    let query_type = match action.get("query_type").and_then(Json::as_str) {
542        None => "r",
543        Some("r") => "r",
544        Some("w") => "w",
545        Some("rw") => "rw",
546        Some("s") => "s",
547        Some(other) => {
548            return Err(format!(
549                "query_type must be r (read), w (write), rw (read-write) or s (schema), got \
550                 '{other}'"
551            ))
552        }
553    };
554    Ok(QueryAnswer {
555        fields,
556        records,
557        stats: stats_value(action.get("stats"))?,
558        query_type,
559    })
560}