diff --git a/.env b/.env new file mode 100644 index 0000000..591d630 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DATABASE_URL=sqlite:////Users/michael/Projects/panorama/test.db \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1e2d465..0beeccc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ node_modules dist target .DS_Store -**/export/export.json \ No newline at end of file +**/export/export.json +test.db \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index d54acdc..414e929 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3291,6 +3291,7 @@ dependencies = [ "tantivy", "tokio", "uuid", + "walkdir", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e40f164..174ac6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,2 @@ workspace.resolver = "2" -workspace.members = ["crates/*", "app/src-tauri"] +workspace.members = ["crates/*", "ui/src-tauri"] diff --git a/apps/codetrack/manifest.yml b/apps/codetrack/manifest.yml new file mode 100644 index 0000000..e69de29 diff --git a/apps/codetrack/register.rn b/apps/codetrack/register.rn new file mode 100644 index 0000000..cf74f49 --- /dev/null +++ b/apps/codetrack/register.rn @@ -0,0 +1,3 @@ +fn main() { + +} \ No newline at end of file diff --git a/crates/panorama-core/Cargo.toml b/crates/panorama-core/Cargo.toml index 02d3444..a9f9511 100644 --- a/crates/panorama-core/Cargo.toml +++ b/crates/panorama-core/Cargo.toml @@ -17,6 +17,7 @@ sugars = "3.0.1" tantivy = { version = "0.22.0", features = ["zstd"] } tokio = { version = "1.38.0", features = ["full"] } uuid = { version = "1.8.0", features = ["v7"] } +walkdir = "2.5.0" [dependencies.async-imap] version = "0.9.7" diff --git a/crates/panorama-core/migrations/00001_initial.sql b/crates/panorama-core/migrations/00001_initial.sql index 750590b..0d706a2 100644 --- a/crates/panorama-core/migrations/00001_initial.sql +++ b/crates/panorama-core/migrations/00001_initial.sql @@ -1,6 +1,40 @@ -CREATE TABLE "node" ( - id TEXT PRIMARY KEY, - type TEXT, - updated_at DATETIME DEFAULT NOW(), +CREATE TABLE node ( + node_id TEXT PRIMARY KEY, + node_type TEXT NOT NULL, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, extra_data JSON +); + +CREATE TABLE node_has_key ( + node_id TEXT NOT NULL, + full_key TEXT NOT NULL, + PRIMARY KEY (node_id, full_key) +); +CREATE INDEX node_has_key_idx_node_id ON node_has_key(node_id); +CREATE INDEX node_has_key_idx_full_key ON node_has_key(full_key); + +-- App-related tables +CREATE TABLE app ( + app_id INTEGER PRIMARY KEY AUTOINCREMENT, + app_name TEXT NOT NULL, + app_version TEXT NOT NULL, + app_version_hash TEXT, + app_description TEXT, + app_homepage TEXT, + app_repository TEXT, + app_license TEXT +); + +CREATE TABLE app_table ( + app_id INTEGER NOT NULL, + app_table_name TEXT NOT NULL, + db_table_name TEXT NOT NULL +); + +CREATE TABLE full_key_to_db_key ( + full_key TEXT NOT NULL, + app_id INTEGER NOT NULL, + app_table_name TEXT NOT NULL, + app_table_field TEXT NOT NULL, + is_fts_enabled BOOLEAN NOT NULL DEFAULT FALSE ); \ No newline at end of file diff --git a/crates/panorama-core/src/state/apps.rs b/crates/panorama-core/src/state/apps.rs new file mode 100644 index 0000000..ddf4238 --- /dev/null +++ b/crates/panorama-core/src/state/apps.rs @@ -0,0 +1,46 @@ +use std::{ + fs, + path::{Path, PathBuf}, +}; + +use miette::{IntoDiagnostic, Result}; + +use crate::AppState; + +impl AppState { + pub async fn install_apps_from_search_paths(&self) -> Result<()> { + let search_paths = + vec![PathBuf::from("/Users/michael/Projects/panorama/apps")]; + + let mut found = Vec::new(); + + for path in search_paths { + let read_dir = fs::read_dir(path).into_diagnostic()?; + + for dir_entry in read_dir { + let dir_entry = dir_entry.into_diagnostic()?; + let path = dir_entry.path(); + + let manifest_path = path.join("manifest.yml"); + if manifest_path.exists() { + found.push(path); + } + } + } + + for path in found { + self.install_app_from_path(path).await?; + } + + Ok(()) + } + + async fn install_app_from_path(&self, path: impl AsRef) -> Result<()> { + let app_path = path.as_ref(); + let manifest_path = app_path.join("manifest.yml"); + + // Install tables + + Ok(()) + } +} diff --git a/crates/panorama-core/src/state/mod.rs b/crates/panorama-core/src/state/mod.rs index 3ff2532..77810df 100644 --- a/crates/panorama-core/src/state/mod.rs +++ b/crates/panorama-core/src/state/mod.rs @@ -1,8 +1,9 @@ +pub mod apps; // pub mod codetrack; // pub mod export; // pub mod journal; // pub mod mail; -// pub mod node; +pub mod node; // pub mod utils; use std::{collections::HashMap, fs, path::Path}; @@ -10,8 +11,9 @@ use std::{collections::HashMap, fs, path::Path}; use bimap::BiMap; use miette::{Context, IntoDiagnostic, Result}; use sqlx::{ + pool::PoolConnection, sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions}, - SqlitePool, + Sqlite, SqliteConnection, SqlitePool, }; use tantivy::{ directory::MmapDirectory, @@ -69,7 +71,8 @@ impl AppState { let db_path = panorama_dir.join("db.sqlite"); let sqlite_connect_options = SqliteConnectOptions::new() .filename(db_path) - .journal_mode(SqliteJournalMode::Wal); + .journal_mode(SqliteJournalMode::Wal) + .create_if_missing(true); let db = SqlitePoolOptions::new() .connect_with(sqlite_connect_options) .await @@ -86,6 +89,10 @@ impl AppState { Ok(state) } + pub async fn conn(&self) -> Result> { + self.db.acquire().await.into_diagnostic() + } + async fn init(&self) -> Result<()> { // run_migrations(&self.db).await?; MIGRATOR diff --git a/crates/panorama-core/src/state/node.rs b/crates/panorama-core/src/state/node.rs index 6286249..64b5a38 100644 --- a/crates/panorama-core/src/state/node.rs +++ b/crates/panorama-core/src/state/node.rs @@ -7,17 +7,13 @@ use chrono::{DateTime, Utc}; use itertools::Itertools; use miette::{bail, IntoDiagnostic, Result}; use serde_json::Value; -use tantivy::{ - collector::TopDocs, - query::QueryParser, - schema::{OwnedValue, Value as _}, - Document, TantivyDocument, Term, -}; +use sqlx::{Acquire, Connection, FromRow}; +use tantivy::schema::{OwnedValue, Value as _}; use uuid::Uuid; use crate::{AppState, NodeId}; -use super::utils::owned_value_to_json_value; +// use super::utils::owned_value_to_json_value; pub type ExtraData = BTreeMap; @@ -43,71 +39,98 @@ impl AppState { /// Get all properties of a node pub async fn get_node(&self, node_id: impl AsRef) -> Result { let node_id = node_id.as_ref().to_owned(); - let tx = self.db.multi_transaction(false); + let conn = self.conn().await?; - let result = tx.run_script( - " - ?[key, relation, field_name, type, is_fts_enabled] := - *node_has_key { key, id }, - *fqkey_to_dbkey { key, relation, field_name, type, is_fts_enabled }, - id = $node_id - ", - btmap! {"node_id".to_owned() => node_id.to_string().into()}, - )?; + #[derive(FromRow)] + struct FieldMappingRow { + full_key: String, + app_id: i64, + app_table_name: String, + app_table_field: String, + } - let field_mapping = AppState::rows_to_field_mapping(result)?; + conn + .transaction(|tx| { + Box::pin(async move { + let result = sqlx::query_as!( + FieldMappingRow, + " + SELECT + node_has_key.full_key, app_id, app_table_name, app_table_field + FROM node_has_key + INNER JOIN full_key_to_db_key + ON node_has_key.full_key = full_key_to_db_key.full_key + WHERE node_id = $1 + ", + node_id + ) + .fetch_all(&mut **tx) + .await + .into_diagnostic()?; - // Group the keys by which relation they're in - let result_by_relation = field_mapping - .iter() - .into_group_map_by(|(_, FieldInfo { relation_name, .. })| relation_name); + let field_mapping = result + .into_iter() + .map(|row| (row.full_key.clone(), row)) + .collect::>(); - let mut all_relation_queries = vec![]; - let mut all_relation_constraints = vec![]; - let mut all_fields = vec![]; - let mut field_counter = 0; - for (i, (relation, fields)) in result_by_relation.iter().enumerate() { - let constraint_name = format!("c{i}"); + // Group the keys by which relation they're in + let result_by_relation = field_mapping.iter().into_group_map_by( + |( + _, + FieldMappingRow { + app_id, + app_table_name, + .. + }, + )| (app_id, app_table_name), + ); - let mut keys = vec![]; - let mut constraints = vec![]; - for (key, field_info) in fields.iter() { - let counted_field_name = format!("f{field_counter}"); - field_counter += 1; + let mut all_relation_queries = vec![]; + let mut all_relation_constraints = vec![]; + let mut all_fields = vec![]; + let mut field_counter = 0; + for (i, (relation, fields)) in result_by_relation.iter().enumerate() { + let constraint_name = format!("c{i}"); - keys.push(counted_field_name.clone()); - constraints.push(format!( - "{}: {}", - field_info.relation_field.to_owned(), - counted_field_name, - )); - all_fields.push(( - counted_field_name, - field_info.relation_field.to_owned(), - key, - )) - } + let mut keys = vec![]; + let mut constraints = vec![]; + for (key, field_info) in fields.iter() { + let counted_field_name = format!("f{field_counter}"); + field_counter += 1; - let keys = keys.join(", "); - let constraints = constraints.join(", "); - all_relation_queries.push(format!( - " + keys.push(counted_field_name.clone()); + constraints.push(format!( + "{}: {}", + field_info.relation_field.to_owned(), + counted_field_name, + )); + all_fields.push(( + counted_field_name, + field_info.relation_field.to_owned(), + key, + )) + } + + let keys = keys.join(", "); + let constraints = constraints.join(", "); + all_relation_queries.push(format!( + " {constraint_name}[{keys}] := *{relation}{{ node_id, {constraints} }}, node_id = $node_id " - )); - all_relation_constraints.push(format!("{constraint_name}[{keys}],")) - } + )); + all_relation_constraints.push(format!("{constraint_name}[{keys}],")) + } - let all_relation_constraints = all_relation_constraints.join("\n"); - let all_relation_queries = all_relation_queries.join("\n\n"); - let all_field_names = all_fields - .iter() - .map(|(field_name, _, _)| field_name) - .join(", "); - let query = format!( - " + let all_relation_constraints = all_relation_constraints.join("\n"); + let all_relation_queries = all_relation_queries.join("\n\n"); + let all_field_names = all_fields + .iter() + .map(|(field_name, _, _)| field_name) + .join(", "); + let query = format!( + " {all_relation_queries} ?[type, extra_data, created_at, updated_at, {all_field_names}] := @@ -115,361 +138,369 @@ impl AppState { {all_relation_constraints} id = $node_id " - ); + ); - let result = tx.run_script( - &query, - btmap! { "node_id".to_owned() => node_id.to_string().into(), }, - )?; + let result = tx.run_script( + &query, + btmap! { "node_id".to_owned() => node_id.to_string().into(), }, + )?; - if result.rows.is_empty() { - bail!("Not found") - } - - let created_at = DateTime::from_timestamp_millis( - (result.rows[0][2].get_float().unwrap() * 1000.0) as i64, - ) - .unwrap(); - - let updated_at = DateTime::from_timestamp_millis( - (result.rows[0][3].get_float().unwrap() * 1000.0) as i64, - ) - .unwrap(); - - let mut fields = HashMap::new(); - - for row in result - .rows - .into_iter() - .map(|row| row.into_iter().skip(4).zip(all_fields.iter())) - { - for (value, (_, _, field_name)) in row { - fields.insert(field_name.to_string(), data_value_to_json_value(&value)); - } - } - - Ok(NodeInfo { - node_id: NodeId(Uuid::from_str(&node_id).unwrap()), - created_at, - updated_at, - fields: Some(fields), - }) - } -} - -#[derive(Debug)] -pub enum CreateOrUpdate { - Create { r#type: String }, - Update { node_id: NodeId }, -} - -impl AppState { - // TODO: Split this out into create and update - pub async fn create_or_update_node( - &self, - opts: CreateOrUpdate, - extra_data: Option, - ) -> Result { - let node_id = match opts { - CreateOrUpdate::Create { .. } => NodeId(Uuid::now_v7()), - CreateOrUpdate::Update { ref node_id } => node_id.clone(), - }; - let node_id = node_id.to_string(); - - let action = match opts { - CreateOrUpdate::Create { .. } => "put", - CreateOrUpdate::Update { .. } => "update", - }; - - println!("Request: {opts:?} {extra_data:?}"); - - let tx = self.db.multi_transaction(true); - - let (created_at, updated_at) = match opts { - CreateOrUpdate::Create { ref r#type } => { - let node_result = tx.run_script( - " - ?[id, type] <- [[$node_id, $type]] - :put node { id, type } - :returning - ", - btmap! { - "node_id".to_owned() => DataValue::from(node_id.clone()), - "type".to_owned() => DataValue::from(r#type.to_owned()), - }, - )?; - let created_at = DateTime::from_timestamp_millis( - (node_result.rows[0][3].get_float().unwrap() * 1000.0) as i64, - ) - .unwrap(); - let updated_at = DateTime::from_timestamp_millis( - (node_result.rows[0][4].get_float().unwrap() * 1000.0) as i64, - ) - .unwrap(); - (created_at, updated_at) - } - CreateOrUpdate::Update { .. } => { - let node_result = tx.run_script( - " - ?[id, type, created_at, updated_at] := *node { id, type, created_at, updated_at }, - id = $node_id - ", - btmap! { - "node_id".to_owned() => DataValue::from(node_id.clone()), - }, - )?; - let created_at = DateTime::from_timestamp_millis( - (node_result.rows[0][2].get_float().unwrap() * 1000.0) as i64, - ) - .unwrap(); - let updated_at = DateTime::from_timestamp_millis( - (node_result.rows[0][3].get_float().unwrap() * 1000.0) as i64, - ) - .unwrap(); - (created_at, updated_at) - } - }; - - if let Some(extra_data) = extra_data { - let node_id_field = self - .tantivy_field_map - .get_by_left("node_id") - .unwrap() - .clone(); - - if !extra_data.is_empty() { - let keys = extra_data.keys().map(|s| s.to_owned()).collect::>(); - let field_mapping = - self.get_rows_for_extra_keys(&tx, keys.as_slice())?; - - // Group the keys by which relation they're in - let result_by_relation = field_mapping.iter().into_group_map_by( - |(_, FieldInfo { relation_name, .. })| relation_name, - ); - - for (relation, fields) in result_by_relation.iter() { - let mut doc = btmap! { node_id_field.clone() => OwnedValue::Str(node_id.to_owned()) }; - let fields_mapping = fields - .into_iter() - .map( - |( - key, - FieldInfo { - relation_field, - r#type, - is_fts_enabled, - .. - }, - )| { - let new_value = extra_data.get(*key).unwrap(); - - // TODO: Make this more generic - let new_value = match r#type.as_str() { - "int" => DataValue::from(new_value.as_i64().unwrap()), - _ => DataValue::from(new_value.as_str().unwrap()), - }; - - if *is_fts_enabled { - if let Some(field) = self.tantivy_field_map.get_by_left(*key) - { - doc.insert( - field.clone(), - OwnedValue::Str(new_value.get_str().unwrap().to_owned()), - ); - } - } - - (relation_field.to_owned(), new_value) - }, - ) - .collect::>(); - - let mut writer = - self.tantivy_index.writer(15_000_000).into_diagnostic()?; - - let delete_term = - Term::from_field_text(node_id_field.clone(), &node_id); - writer.delete_term(delete_term); - - writer.add_document(doc).into_diagnostic()?; - writer.commit().into_diagnostic()?; - drop(writer); - - let keys = fields_mapping.keys().collect::>(); - let keys_joined = keys.iter().join(", "); - - if !keys.is_empty() { - let query = format!( - " - ?[ node_id, {keys_joined} ] <- [$input_data] - :{action} {relation} {{ node_id, {keys_joined} }} - " - ); - - let mut params = vec![]; - params.push(DataValue::from(node_id.clone())); - for key in keys { - params.push(fields_mapping[key].clone()); - } - - let result = tx.run_script( - &query, - btmap! { - "input_data".to_owned() => DataValue::List(params), - }, - ); + if result.rows.is_empty() { + bail!("Not found") } - } - let input = DataValue::List( - keys - .iter() - .map(|s| { - DataValue::List(vec![ - DataValue::from(s.to_owned()), - DataValue::from(node_id.clone()), - ]) - }) - .collect_vec(), - ); + let created_at = DateTime::from_timestamp_millis( + (result.rows[0][2].get_float().unwrap() * 1000.0) as i64, + ) + .unwrap(); - tx.run_script( - " - ?[key, id] <- $input_data - :put node_has_key { key, id } - ", - btmap! { - "input_data".to_owned() => input - }, - )?; - } - } + let updated_at = DateTime::from_timestamp_millis( + (result.rows[0][3].get_float().unwrap() * 1000.0) as i64, + ) + .unwrap(); - tx.commit()?; + let mut fields = HashMap::new(); - Ok(NodeInfo { - node_id: NodeId(Uuid::from_str(&node_id).unwrap()), - created_at, - updated_at, - fields: None, - }) - } - - pub async fn update_node() {} - - pub async fn search_nodes( - &self, - query: impl AsRef, - ) -> Result> { - let query = query.as_ref(); - - let reader = self.tantivy_index.reader().into_diagnostic()?; - let searcher = reader.searcher(); - - let node_id_field = self - .tantivy_field_map - .get_by_left("node_id") - .unwrap() - .clone(); - let journal_page_field = self - .tantivy_field_map - .get_by_left("panorama/journal/page/content") - .unwrap() - .clone(); - let mut query_parser = - QueryParser::for_index(&self.tantivy_index, vec![journal_page_field]); - query_parser.set_field_fuzzy(journal_page_field, true, 2, true); - let query = query_parser.parse_query(query).into_diagnostic()?; - - let top_docs = searcher - .search(&query, &TopDocs::with_limit(10)) - .into_diagnostic()?; - - Ok( - top_docs - .into_iter() - .map(|(score, doc_address)| { - let retrieved_doc = - searcher.doc::(doc_address).unwrap(); - let node_id = retrieved_doc - .get_first(node_id_field.clone()) - .unwrap() - .as_str() - .unwrap(); - let all_fields = retrieved_doc.get_sorted_field_values(); - let node_id = NodeId(Uuid::from_str(node_id).unwrap()); - let fields = all_fields + for row in result + .rows .into_iter() - .map(|(field, values)| { - ( - self.tantivy_field_map.get_by_right(&field).unwrap(), - if values.len() == 1 { - owned_value_to_json_value(values[0]) - } else { - Value::Array( - values - .into_iter() - .map(owned_value_to_json_value) - .collect_vec(), - ) - }, - ) - }) - .collect::>(); - ( - node_id, - json!({ - "score": score, - "fields": fields, - }), - ) + .map(|row| row.into_iter().skip(4).zip(all_fields.iter())) + { + for (value, (_, _, field_name)) in row { + fields.insert( + field_name.to_string(), + data_value_to_json_value(&value), + ); + } + } + + Ok(NodeInfo { + node_id: NodeId(Uuid::from_str(&node_id).unwrap()), + created_at, + updated_at, + fields: Some(fields), + }) }) - .collect::>(), - ) - } + }) + .await?; - fn get_rows_for_extra_keys( - &self, - tx: &MultiTransaction, - keys: &[String], - ) -> Result { - let result = tx.run_script( - " - ?[key, relation, field_name, type, is_fts_enabled] := - *fqkey_to_dbkey{key, relation, field_name, type, is_fts_enabled}, - is_in(key, $keys) - ", - btmap! { - "keys".to_owned() => DataValue::List( - keys.into_iter() - .map(|s| DataValue::from(s.as_str())) - .collect::>() - ), - }, - )?; - - AppState::rows_to_field_mapping(result) - } - - fn rows_to_field_mapping(result: NamedRows) -> Result { - let s = |s: &DataValue| s.get_str().unwrap().to_owned(); - - Ok( - result - .rows - .into_iter() - .map(|row| { - ( - s(&row[0]), - FieldInfo { - relation_name: s(&row[1]), - relation_field: s(&row[2]), - r#type: s(&row[3]), - is_fts_enabled: row[4].get_bool().unwrap(), - }, - ) - }) - .collect::>(), - ) + Ok(()) } } + +// #[derive(Debug)] +// pub enum CreateOrUpdate { +// Create { r#type: String }, +// Update { node_id: NodeId }, +// } + +// impl AppState { +// // TODO: Split this out into create and update +// pub async fn create_or_update_node( +// &self, +// opts: CreateOrUpdate, +// extra_data: Option, +// ) -> Result { +// let node_id = match opts { +// CreateOrUpdate::Create { .. } => NodeId(Uuid::now_v7()), +// CreateOrUpdate::Update { ref node_id } => node_id.clone(), +// }; +// let node_id = node_id.to_string(); + +// let action = match opts { +// CreateOrUpdate::Create { .. } => "put", +// CreateOrUpdate::Update { .. } => "update", +// }; + +// println!("Request: {opts:?} {extra_data:?}"); + +// let tx = self.db.multi_transaction(true); + +// let (created_at, updated_at) = match opts { +// CreateOrUpdate::Create { ref r#type } => { +// let node_result = tx.run_script( +// " +// ?[id, type] <- [[$node_id, $type]] +// :put node { id, type } +// :returning +// ", +// btmap! { +// "node_id".to_owned() => DataValue::from(node_id.clone()), +// "type".to_owned() => DataValue::from(r#type.to_owned()), +// }, +// )?; +// let created_at = DateTime::from_timestamp_millis( +// (node_result.rows[0][3].get_float().unwrap() * 1000.0) as i64, +// ) +// .unwrap(); +// let updated_at = DateTime::from_timestamp_millis( +// (node_result.rows[0][4].get_float().unwrap() * 1000.0) as i64, +// ) +// .unwrap(); +// (created_at, updated_at) +// } +// CreateOrUpdate::Update { .. } => { +// let node_result = tx.run_script( +// " +// ?[id, type, created_at, updated_at] := *node { id, type, created_at, updated_at }, +// id = $node_id +// ", +// btmap! { +// "node_id".to_owned() => DataValue::from(node_id.clone()), +// }, +// )?; +// let created_at = DateTime::from_timestamp_millis( +// (node_result.rows[0][2].get_float().unwrap() * 1000.0) as i64, +// ) +// .unwrap(); +// let updated_at = DateTime::from_timestamp_millis( +// (node_result.rows[0][3].get_float().unwrap() * 1000.0) as i64, +// ) +// .unwrap(); +// (created_at, updated_at) +// } +// }; + +// if let Some(extra_data) = extra_data { +// let node_id_field = self +// .tantivy_field_map +// .get_by_left("node_id") +// .unwrap() +// .clone(); + +// if !extra_data.is_empty() { +// let keys = extra_data.keys().map(|s| s.to_owned()).collect::>(); +// let field_mapping = +// self.get_rows_for_extra_keys(&tx, keys.as_slice())?; + +// // Group the keys by which relation they're in +// let result_by_relation = field_mapping.iter().into_group_map_by( +// |(_, FieldInfo { relation_name, .. })| relation_name, +// ); + +// for (relation, fields) in result_by_relation.iter() { +// let mut doc = btmap! { node_id_field.clone() => OwnedValue::Str(node_id.to_owned()) }; +// let fields_mapping = fields +// .into_iter() +// .map( +// |( +// key, +// FieldInfo { +// relation_field, +// r#type, +// is_fts_enabled, +// .. +// }, +// )| { +// let new_value = extra_data.get(*key).unwrap(); + +// // TODO: Make this more generic +// let new_value = match r#type.as_str() { +// "int" => DataValue::from(new_value.as_i64().unwrap()), +// _ => DataValue::from(new_value.as_str().unwrap()), +// }; + +// if *is_fts_enabled { +// if let Some(field) = self.tantivy_field_map.get_by_left(*key) +// { +// doc.insert( +// field.clone(), +// OwnedValue::Str(new_value.get_str().unwrap().to_owned()), +// ); +// } +// } + +// (relation_field.to_owned(), new_value) +// }, +// ) +// .collect::>(); + +// let mut writer = +// self.tantivy_index.writer(15_000_000).into_diagnostic()?; + +// let delete_term = +// Term::from_field_text(node_id_field.clone(), &node_id); +// writer.delete_term(delete_term); + +// writer.add_document(doc).into_diagnostic()?; +// writer.commit().into_diagnostic()?; +// drop(writer); + +// let keys = fields_mapping.keys().collect::>(); +// let keys_joined = keys.iter().join(", "); + +// if !keys.is_empty() { +// let query = format!( +// " +// ?[ node_id, {keys_joined} ] <- [$input_data] +// :{action} {relation} {{ node_id, {keys_joined} }} +// " +// ); + +// let mut params = vec![]; +// params.push(DataValue::from(node_id.clone())); +// for key in keys { +// params.push(fields_mapping[key].clone()); +// } + +// let result = tx.run_script( +// &query, +// btmap! { +// "input_data".to_owned() => DataValue::List(params), +// }, +// ); +// } +// } + +// let input = DataValue::List( +// keys +// .iter() +// .map(|s| { +// DataValue::List(vec![ +// DataValue::from(s.to_owned()), +// DataValue::from(node_id.clone()), +// ]) +// }) +// .collect_vec(), +// ); + +// tx.run_script( +// " +// ?[key, id] <- $input_data +// :put node_has_key { key, id } +// ", +// btmap! { +// "input_data".to_owned() => input +// }, +// )?; +// } +// } + +// tx.commit()?; + +// Ok(NodeInfo { +// node_id: NodeId(Uuid::from_str(&node_id).unwrap()), +// created_at, +// updated_at, +// fields: None, +// }) +// } + +// pub async fn update_node() {} + +// pub async fn search_nodes( +// &self, +// query: impl AsRef, +// ) -> Result> { +// let query = query.as_ref(); + +// let reader = self.tantivy_index.reader().into_diagnostic()?; +// let searcher = reader.searcher(); + +// let node_id_field = self +// .tantivy_field_map +// .get_by_left("node_id") +// .unwrap() +// .clone(); +// let journal_page_field = self +// .tantivy_field_map +// .get_by_left("panorama/journal/page/content") +// .unwrap() +// .clone(); +// let mut query_parser = +// QueryParser::for_index(&self.tantivy_index, vec![journal_page_field]); +// query_parser.set_field_fuzzy(journal_page_field, true, 2, true); +// let query = query_parser.parse_query(query).into_diagnostic()?; + +// let top_docs = searcher +// .search(&query, &TopDocs::with_limit(10)) +// .into_diagnostic()?; + +// Ok( +// top_docs +// .into_iter() +// .map(|(score, doc_address)| { +// let retrieved_doc = +// searcher.doc::(doc_address).unwrap(); +// let node_id = retrieved_doc +// .get_first(node_id_field.clone()) +// .unwrap() +// .as_str() +// .unwrap(); +// let all_fields = retrieved_doc.get_sorted_field_values(); +// let node_id = NodeId(Uuid::from_str(node_id).unwrap()); +// let fields = all_fields +// .into_iter() +// .map(|(field, values)| { +// ( +// self.tantivy_field_map.get_by_right(&field).unwrap(), +// if values.len() == 1 { +// owned_value_to_json_value(values[0]) +// } else { +// Value::Array( +// values +// .into_iter() +// .map(owned_value_to_json_value) +// .collect_vec(), +// ) +// }, +// ) +// }) +// .collect::>(); +// ( +// node_id, +// json!({ +// "score": score, +// "fields": fields, +// }), +// ) +// }) +// .collect::>(), +// ) +// } + +// fn get_rows_for_extra_keys( +// &self, +// tx: &MultiTransaction, +// keys: &[String], +// ) -> Result { +// let result = tx.run_script( +// " +// ?[key, relation, field_name, type, is_fts_enabled] := +// *fqkey_to_dbkey{key, relation, field_name, type, is_fts_enabled}, +// is_in(key, $keys) +// ", +// btmap! { +// "keys".to_owned() => DataValue::List( +// keys.into_iter() +// .map(|s| DataValue::from(s.as_str())) +// .collect::>() +// ), +// }, +// )?; + +// AppState::rows_to_field_mapping(result) +// } + +// fn rows_to_field_mapping(result: NamedRows) -> Result { +// let s = |s: &DataValue| s.get_str().unwrap().to_owned(); + +// Ok( +// result +// .rows +// .into_iter() +// .map(|row| { +// ( +// s(&row[0]), +// FieldInfo { +// relation_name: s(&row[1]), +// relation_field: s(&row[2]), +// r#type: s(&row[3]), +// is_fts_enabled: row[4].get_bool().unwrap(), +// }, +// ) +// }) +// .collect::>(), +// ) +// } +// }