khive_runtime/
note_create.rs1use khive_storage::types::{Edge, LinkId, SqlValue};
4use khive_storage::{EdgeRelation, SqlStatement};
5use serde_json::{json, Value};
6use uuid::Uuid;
7
8use crate::atomic_message::{
9 prepare_atomic_notes, AtomicNoteOptions, AtomicNoteSpec, PreparedAtomicNotes,
10};
11use crate::atomic_plan::{AffectedRowGuard, PlanStatement};
12use crate::atomic_runner::AtomicOpPlan;
13use crate::{KhiveRuntime, RuntimeError, RuntimeResult};
14
15pub enum EffectiveCreateTags<'a> {
18 TopLevel(&'a [String]),
19 Properties(Option<&'a Value>),
20}
21
22impl EffectiveCreateTags<'_> {
23 pub fn to_value(&self) -> Value {
26 match self {
27 Self::TopLevel(tags) => json!(tags),
28 Self::Properties(tags) => tags.cloned().unwrap_or(Value::Null),
29 }
30 }
31}
32
33pub fn effective_create_tags<'a>(
39 tags: Option<&'a [String]>,
40 properties: Option<&'a Value>,
41) -> EffectiveCreateTags<'a> {
42 match tags {
43 Some(tags) if !tags.is_empty() => EffectiveCreateTags::TopLevel(tags),
44 _ => EffectiveCreateTags::Properties(properties.and_then(|value| value.get("tags"))),
45 }
46}
47
48pub(crate) enum KeyPublication {
49 AtInsert,
50 AfterDependents,
51}
52
53pub(crate) const KEY_CLAIM: &str = "memory-key-claim";
54
55pub(crate) async fn prepare_note_create(
56 runtime: &KhiveRuntime,
57 spec: AtomicNoteSpec<'_>,
58 mut options: AtomicNoteOptions<'_>,
59 annotates: &[Uuid],
60 publication: KeyPublication,
61) -> RuntimeResult<(PreparedAtomicNotes, Vec<Uuid>)> {
62 for target in annotates {
63 if !runtime.substrate_exists_by_id(spec.token, *target).await? {
64 return Err(RuntimeError::NotFound(format!(
65 "create_note annotates target {target} not found"
66 )));
67 }
68 }
69 let claim_last = match publication {
70 KeyPublication::AtInsert => None,
71 KeyPublication::AfterDependents => Some(
72 options
73 .key
74 .take()
75 .ok_or_else(|| {
76 RuntimeError::InvalidInput("deferred key publication requires a key".into())
77 })?
78 .to_owned(),
79 ),
80 };
81 let mut prepared = prepare_atomic_notes(runtime, vec![spec], options).await?;
82 let note = &prepared.notes[0];
83 crate::secret_gate::reject_reserved_secret_gate_property(note.properties.as_ref())?;
84 let AtomicOpPlan::AddNote(plan) = &mut prepared.plans[0] else {
85 return Err(RuntimeError::Internal(
86 "expected prepared note creation".into(),
87 ));
88 };
89 if claim_last.is_some() {
90 plan.statements[0].statement =
91 khive_db::stores::note::note_insert_if_absent_statement(note);
92 }
93 let mut annotation_ids = Vec::with_capacity(annotates.len());
94 for target in annotates {
95 let now = chrono::Utc::now();
96 let id = Uuid::new_v4();
97 let edge = Edge {
98 id: LinkId::from(id),
99 namespace: note.namespace.clone(),
100 source_id: note.id,
101 target_id: *target,
102 relation: EdgeRelation::Annotates,
103 weight: 1.0,
104 created_at: now,
105 updated_at: now,
106 deleted_at: None,
107 metadata: None,
108 target_backend: None,
109 };
110 plan.statements.push(PlanStatement {
111 statement: khive_db::stores::graph::edge_insert_only_guarded_by_endpoints_statement(
112 &edge,
113 ),
114 guard: Some(AffectedRowGuard::exactly(1)),
115 });
116 annotation_ids.push(id);
117 }
118 if let Some(key) = claim_last {
119 plan.statements.push(PlanStatement {
120 statement: SqlStatement {
121 sql: "UPDATE OR IGNORE notes SET key = ?1 WHERE id = ?2 AND namespace = ?3 \
122 AND kind = ?4 AND key IS NULL AND deleted_at IS NULL"
123 .into(),
124 params: vec![
125 SqlValue::Text(key),
126 SqlValue::Text(note.id.to_string()),
127 SqlValue::Text(note.namespace.clone()),
128 SqlValue::Text(note.kind.clone()),
129 ],
130 label: Some(KEY_CLAIM.into()),
131 },
132 guard: Some(AffectedRowGuard::exactly(1)),
133 });
134 }
135 Ok((prepared, annotation_ids))
136}